7 real-world failure stories from a QA engineer who introduced Playwright for E2E test automation. Covers over-reliance on auto-waiting, wrong language choice, CI environment issues, team resistance, and more — Playwright-specific pitfalls with concrete fixes based on hands-on experience.
“Switching to Playwright will fix everything” — that assumption was the beginning of the next set of failures. Changing the tool doesn’t help if you use it the wrong way.
📌 Who This Article Is For
- QA engineers considering Playwright for E2E test automation
- Engineers planning a migration from Selenium to Playwright
- Anyone who has introduced Playwright and felt “this isn’t what I expected”
- Leads and managers trying to roll out Playwright across a team
✅ What You Will Learn
- 7 Playwright-specific failure patterns and their root causes
- Decision-level and adoption-level mistakes — not just coding pitfalls
- Concrete avoidance strategies and fixes for each failure
👤 About the Author
Written by Yoshi, a QA engineer and test automation engineer with over 15 years of hands-on experience using Selenium, Playwright, and pytest in real production projects. These are the “I wish I’d known this” moments from actual Playwright adoption — shared without filter.
📖 How This Article Differs from Related Content
- Top 5 Test Automation Failures: Tool-agnostic strategy and design mistakes → Start here if you failed at “what to automate”
- Playwright × pytest Best Practices: Code-level implementation pitfalls → Start here if you’re stuck on how to write tests
- This article: Experience-level failures in choosing, introducing, and running Playwright as a team
📌 Key Takeaways
- Most Playwright adoption failures are not tool problems — they are adoption planning, language selection, and team design problems
- auto-waiting, page.route(), and CI environments have Playwright-specific traps that are easy to avoid if you know them in advance
- Playwright itself is an excellent tool. Knowing these failures before you start lets you avoid repeating them
“We’re drowning in flaky Selenium tests — let’s switch to Playwright.” My decision two years ago was half right and half wrong.
Playwright really is an excellent tool. But charging ahead with the assumption that “switching will fix everything” led to a whole new set of Playwright-specific failures. This article shares those experiences — all seven of them — without holding back.
- 7 Playwright Adoption Failures: Quick Reference
- ① What Is the “Switching Will Fix Everything” Over-Confidence Trap?
- ② What Is Over-Relying on Auto-Waiting and Drowning in Timeouts?
- ③ What Is Starting with TypeScript and Ending with a Codebase No One Could Maintain?
- ④ What Is Thinking page.route() Mocked the API — but It Was Hitting the Real One?
- ⑤ What Is Leaving Browser Launch Failures in CI Unresolved?
- ⑥ What Is a Version Upgrade That Crashed 30 Tests Overnight?
- ⑦ What Is Not Being Able to Explain “Why Playwright” to the Team?
- FAQ
7 Playwright Adoption Failures: Quick Reference
| # | Failure | Root Cause |
|---|---|---|
| ① | “Switching to Playwright will fix everything” — the over-confidence trap | Fundamental misunderstanding of what the tool solves |
| ② | Over-relying on auto-waiting and drowning in timeouts | Misunderstanding of how Playwright’s auto-waiting actually works |
| ③ | Starting with TypeScript and ending with a codebase no one could maintain | Language choice that ignored team skill level |
| ④ | Thinking page.route() mocked the API — but it was hitting the real one | Misunderstanding of how network mocking works |
| ⑤ | Leaving browser launch failures in CI unresolved | Local-only development habits |
| ⑥ | A version upgrade that crashed 30 tests overnight | No dependency management or update strategy |
| ⑦ | Not being able to explain “why Playwright” to the team | Rationale that existed only in one person’s head |
① What Is the “Switching Will Fix Everything” Over-Confidence Trap?
What happened
When flaky Selenium tests hit a breaking point, I believed the hype: “Playwright has auto-waiting so flakiness goes away” and “the modern API makes maintenance easier.” I launched a project to rewrite all 200 existing Selenium tests in Playwright. The result: flakiness decreased, but never reached zero. And the three months spent rewriting never touched the underlying test design problems.
Flaky issues Playwright fixes vs. ones it doesn’t
| Root Cause of Flakiness | Improved by Playwright? | Reason |
|---|---|---|
| Insufficient implicit waits in Selenium | ✅ Yes | auto-waiting handles this automatically |
| ChromeDriver version mismatch | ✅ Yes | Browser binary managed alongside the library (though install + CI cache setup is still required) |
| Animation timing during interactions | △ Partially | Stable check exists but isn’t complete for all CSS animations |
| Unstable external API responses | ❌ No | External dependencies can’t be fixed by the tool |
| DB or state conflicts between tests | ❌ No | Test design problems can’t be fixed by the tool |
| Unstable specs at early development stage | ❌ No | Spec changes break the tests themselves |
| CI machine CPU/memory constraints | △ Partially | Can mitigate with timeout tuning, but not a root fix |
| Network latency / jitter | ❌ No | Mock the network instead |
| Race conditions during parallel test execution | ❌ No | Fix test isolation in the design — not in the tool |
② What Is Over-Relying on Auto-Waiting and Drowning in Timeouts?
What happened
“Playwright has auto-waiting, so you don’t need explicit waits.” Tests written with that assumption started producing mysterious timeout errors a few weeks later.
expect() are still required.# ❌ Over-relying on auto-waiting
# locator.click() runs an actionability check, but for async state changes
# or custom UI components, explicit waits may still be needed
page.locator("#submit-btn").click()
# ↑ If the button transitions from disabled → enabled asynchronously,
# this may time out depending on timing
# ✅ Modern Locator API + expect() to make intent explicit
from playwright.sync_api import expect
submit_btn = page.locator("#submit-btn")
expect(submit_btn).to_be_enabled() # wait until enabled
submit_btn.click() # then click- Buttons that transition disabled → enabled asynchronously: May attempt the click while still disabled, causing a timeout. Use
expect(locator).to_be_enabled()first - Content that appears after an API response: Playwright waits while the element doesn’t exist in the DOM, but a slow API can still hit the overall timeout
- Elements mid-animation: The stable check exists, but certain CSS animation implementations may not behave as expected
- Shadow DOM elements: Depending on how selectors are written, they may not be recognized as attached
expect(locator).to_be_enabled() or expect(locator).to_be_visible() makes the intent explicit and reduces flakiness. Note: page.wait_for_selector() is not fully deprecated — it still has legitimate uses for waiting on hidden or detached states — but for new code, Locator API + expect() is the current recommended style.③ What Is Starting with TypeScript and Ending with a Codebase No One Could Maintain?
What happened
“Playwright’s TypeScript/Node.js version has the most samples and documentation” and “type safety makes maintenance easier” — so I chose TypeScript. I could write it, having JavaScript experience. But the rest of the team was almost entirely Python engineers. Six months later, I was the only person reviewing test code. Defacto knowledge silos: complete.
| Decision Factor | TypeScript | Python |
|---|---|---|
| Official samples and documentation depth | ✅ Most abundant | ✅ Well supported |
| Type safety and autocomplete | ✅ Powerful | △ Type hints available |
| Maintainability on a Python-heavy team | ❌ High silo risk | ✅ Everyone can contribute |
| pytest integration | ❌ Separate ecosystem | ✅ Native pytest support |
④ What Is Thinking page.route() Mocked the API — but It Was Hitting the Real One?
What happened
I used page.route() to mock the API, but test results kept depending on real production data. After investigation, it turned out the API was being called server-side during SSR — not through browser JavaScript.
page.route() intercepts HTTP traffic within the Browser Context only. Playwright’s API testing request fixture operates as a completely separate APIRequestContext — an independent HTTP client at a different network layer. The assumption “it’s all Playwright, so page.route() covers everything” is what causes this failure.# page.route() intercepts Browser Context HTTP traffic only
await page.route(
"**/api/users",
lambda route: route.fulfill(status=200, body='{"users": []}')
)
# ⚠️ Does NOT intercept:
# - SSR / SSG (Next.js getServerSideProps, etc.) — server-side fetch
# - Playwright API Testing request fixture (APIRequestContext)
# - Some Service Worker-proxied fetch calls- SSR/SSG: Server-side fetch calls (e.g., Next.js getServerSideProps, Nuxt server routes). These run in Node.js, not the browser. Upstream API mocking or server-side mock libraries are needed instead
- Service Worker: In PWA or MSW (Mock Service Worker) environments, if fetch is proxied through the SW, page.route() may not reach it
- WebSocket: Use
page.on("websocket")for WebSocket traffic - Playwright request fixture (APIRequestContext): Runs as an independent HTTP client — completely separate from the browser layer
route.fulfill() responses are coming back using the network tab or by asserting the mock response content. For SSR or Service Worker environments, combine with server-side or SW-level mock libraries (msw, Nock, WireMock, etc.). Never assume “page.route() covers everything” as a design premise.⑤ What Is Leaving Browser Launch Failures in CI Unresolved?
What happened
Tests ran perfectly locally, but pushing to GitHub Actions produced a cascade of “browser won’t launch” and “missing dependency” errors. I knew the cause but kept deferring it — and spent two weeks running tests locally and manually reporting results to CI in the most backwards way possible.
Common Playwright launch errors in CI
| Error Type | Cause | Fix |
|---|---|---|
BrowserType.launch: Executable doesn't exist | Browser binary not installed | Run npx playwright install --with-deps |
error while loading shared libraries | Missing Linux system libraries | Use the --with-deps flag |
| More timeouts than locally | CI machine has lower CPU/memory than local | Set timeout 2–3x higher in CI config |
| Different behavior in headless mode | Viewport differences, no GPU rendering, hover not triggered, animation timing gaps, Intersection Observer behavior differences | Debug with --headed flag and revisit test design |
⑥ What Is a Version Upgrade That Crashed 30 Tests Overnight?
What happened
After running pip install playwright --upgrade, the next morning’s CI run had 30 tests failing. The cause: API changes between versions and a version mismatch between the library and browser binary.
# ❌ No version pinning (breaks with every upgrade)
playwright
# ✅ Pin the version in requirements.txt
playwright==1.44.0
# After updating the library, ALWAYS reinstall the browser binary
# playwright install chromiumrequirements.txt (non-negotiable). Tools like Renovate or Dependabot can automate upgrade PRs, making version management much easier to track.⑦ What Is Not Being Able to Explain “Why Playwright” to the Team?
What happened
When I decided unilaterally that “Playwright is better” and tried to roll it out, a senior engineer asked: “Why can’t we just keep using Selenium?” I had no good answer. “It feels more modern” and “I’ve seen good things on Twitter” don’t hold up as technical decision-making. The result: “let’s just keep Selenium for now” — and the migration project floated in limbo for six months.
The evidence that can actually convince a team
| Comparison Point | Selenium | Playwright |
|---|---|---|
| auto-waiting | Manual time.sleep / wait calls required | Actionability check runs automatically on interactions |
| ChromeDriver management | Must be manually matched to Chrome version | Browser binary managed with the library |
| API test integration | Requires a separate library (requests, etc.) | E2E + API in one suite via request fixture |
| Network mocking | Requires a separate mock server | page.route() for browser-layer mocking |
| HTML Report / Trace | Requires separate Allure setup | Built-in HTML Report and Trace Viewer |
FAQ
Q. Is it worth migrating from Selenium to Playwright?
Yes — but don’t fall for the “migration alone will fix everything” trap. Eliminating ChromeDriver management, auto-waiting, and built-in HTML reports do meaningfully reduce operational overhead. The key is to clearly identify “what specifically is painful about our current Selenium setup” before deciding. That framing maximizes the value you’ll get from migrating.
Q. Where is the right place to start with Playwright adoption?
Write 1–2 new tests in Playwright and get them all the way to running in CI — that’s the best first step. Starting with a full migration of existing tests bogs down in time and learning cost. Build a small success story first, then expand from there. That approach has the lowest failure rate.
Q. Should I choose Python or TypeScript for Playwright?
Choose whichever language most of your team can read and write. If your team runs on Python, the Python version (playwright-pytest) is easier to maintain and integrates natively with your existing pytest stack. TypeScript has the richest documentation and samples, but test code that nobody can maintain is the worst outcome. Team readability beats documentation richness every time.
Q. Why do flaky tests still happen when auto-waiting exists?
auto-waiting runs an actionability check at the moment of action — it does not track async state changes over time. For things like “wait until the API response arrives” or “wait until this button becomes enabled,” explicit waits using expect() are still necessary. When flakiness persists, check whether any async state changes are slipping past what auto-waiting covers.
Q. How should Playwright versions be managed?
Pin the version in requirements.txt and only merge upgrades after confirming all tests pass in CI on a dedicated upgrade branch. Since Playwright’s library version and browser binary version are tightly coupled, always run playwright install after upgrading the library. Renovate or Dependabot can automate upgrade PRs and make this process much more manageable.
📖 Related Articles
Playwright Implementation
- Playwright × pytest Best Practices | 7 Implementation Pitfalls to Avoid
- How to Automate E2E Tests with GitHub Actions × Playwright
Automation Strategy and Decision Making
- Top 5 Test Automation Failures | Strategy and Design-Level Mistakes
- 7 Test Cases You Should Not Automate | How to Spot Flaky-Prone Tests
- What to Automate vs What Not to Automate | Decision Framework
- How to Think About ROI in Test Automation
Roadmap
Playwright really is an excellent tool. Every failure in this article was not a problem with Playwright — it was a problem with how it was used, how it was introduced, and how the team was set up around it. Knowing these failure patterns before you start means you don’t have to repeat them. Start small, get it running in CI, build to a state where the whole team can use it — that’s the sequence that makes Playwright adoption succeed.
📋 Summary
- “Migrating to Playwright will fix everything” is half true. Test design problems don’t go away by switching tools
- auto-waiting runs an actionability check — it does not track async state changes. Use
expect()for those - Language choice should prioritize what most of the team can maintain. Documentation richness comes second
- page.route() mocks Browser Context traffic only. SSR, Service Workers, and request fixture have separate layers
- Get CI working on day one. “I’ll deal with it later” is the biggest time sink of any Playwright project
- Version upgrades require both library and browser binary to be updated together — always pin versions
- Team adoption requires a “pain points → solutions → cost estimate” three-part case — not just “it’s better”
