I spent three years writing Selenium tests. I learned to love it — the explicit waits, the Page Object pattern, the way it forced you to think carefully about browser state before you were allowed to act on it. Then I moved to a project using Playwright, and after two weeks I never opened a Selenium file again.
That's the honest version of the story. The less honest version — the one I wrote the first time I put this post up — treated Selenium as a framework frozen in 2019, still requiring a separate proxy tool for network mocking, still without any answer to auto-waiting. That was true when I first learned Selenium. It's not entirely true anymore, and pretending otherwise does a disservice to anyone deciding between the two frameworks today with real, current information rather than a snapshot of how things looked years ago. So this is a rewrite: the real case for Playwright, the real ground Selenium has actually made up, and the one thing that hasn't changed no matter how much Selenium's tooling has improved.
The Actual Problem: Selenium Was Built Before the Web It Now Has to Test
Selenium's core design — a synchronous WebDriver protocol issuing one command at a time, waiting for a response before issuing the next — dates back to an era of server-rendered pages where "the page is loaded" was a single, well-defined moment. The modern web isn't that: single-page apps, hydration, lazy-loaded components, WebSocket-driven UI updates. None of that maps cleanly onto "issue a command, wait for a response," and the practical result, for as long as I used Selenium, was code like this:
WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, "#submit-btn"))
)
You get used to writing this. It becomes muscle memory, the same way time.sleep(2) hacks become muscle memory when you're in a hurry and the explicit wait feels like overkill for one line. But "used to it" isn't the same as "not a real cost" — every explicit wait is a line of code whose entire job is compensating for something the framework doesn't do for you automatically, and across a suite of hundreds of tests, that's a genuinely large amount of code that exists purely to work around timing, not to test behavior.
What Playwright Actually Does Differently — And What Hasn't Changed
Playwright's core bet: auto-waiting should be the default on every single action, not an opt-in tool you reach for when you happen to remember. Call page.click('#submit-btn'), and Playwright automatically checks, before the click ever fires, that the element is attached to the DOM, visible, stable (not mid-animation), enabled, and actually able to receive pointer events — not obscured by something else on top of it. Every action gets this, automatically, with no explicit wait written anywhere:
// This just works — no waits needed
await page.click('#submit-btn');
await expect(page.locator('.success-message')).toBeVisible();
This is the one piece of the original comparison that's held up completely unchanged. I went and checked, rather than assuming: Selenium 4 still has no equivalent built-in actionability system. Selenium's WebDriverWait and ExpectedConditions did get real, worthwhile improvements — Selenium 4 switched from the old TimeUnit API to Java's Duration class, which is more readable and less error-prone to configure — but that's a quality-of-life improvement to how you write an explicit wait, not a change to whether you need to write one. You still decide, for every interaction, whether this specific element needs a wait before it, and you're still the one responsible for getting that decision right. Playwright made that decision once, for every action, and you never have to make it again.
Where I Was Wrong: Selenium's Network Interception Story Has Actually Changed
The original version of this post said, flatly, that "mocking APIs in Selenium requires a proxy (like BrowserMob)." I wrote that because it was true when I learned Selenium, and I never went back to check whether it still was. It isn't — not entirely.
Selenium 4.18 shipped native network interception through the WebDriver BiDi protocol — the same bidirectional, WebSocket-based protocol architecture that Playwright, Cypress, and WebdriverIO have all converged on for their own newer capabilities. The real API:
try (Network network = new Network(driver)) {
network.addIntercept(new AddInterceptParameters(InterceptPhase.BEFORE_REQUEST_SENT));
network.onBeforeRequestSent(
responseDetails -> network.failRequest(responseDetails.getRequest().getRequestId())
);
driver.get("https://example.com");
}
This is real, and it means the flat claim "Selenium can't do this without a separate proxy" is no longer accurate. What's still true, and worth stating precisely rather than glossing over: this API is genuinely early. The Python bindings expose it through underscore-prefixed private methods (driver.network._add_intercept()), which the project's own documentation says plainly will change once a public API stabilizes. Interception phases are limited compared to what a mature mocking layer offers, and some capabilities — handling authentication challenges, for instance — are currently Firefox-specific rather than working uniformly across browsers. Compare that to Playwright's page.route(), which has been stable, full-featured, and browser-agnostic for a long time now:
await page.route('**/api/products', route =>
route.fulfill({ json: { products: mockData } })
);
The honest comparison, as of right now, isn't "Selenium can't mock network requests." It's "Selenium's native network interception exists, is real, and is meaningfully less mature than Playwright's" — a different, more defensible claim, and one that's likely to keep narrowing as Selenium's BiDi implementation matures. Writing the first version of this comparison as a permanent, settled fact rather than a snapshot of a specific point in time was the actual mistake, not the underlying preference for Playwright.
The Features That Still Make the Difference Day to Day
Parallel execution as a first-class citizen. Selenium parallelism historically meant standing up Selenium Grid, wiring in Docker, and maintaining CI infrastructure that broke in ways unrelated to your actual tests. Playwright's parallelism is a config flag:
// playwright.config.ts
export default defineConfig({
workers: 4,
fullyParallel: true,
});
I cut a CI pipeline from 18 minutes to 5 on the same machine, changing nothing but this one setting.
Screenshots and video on failure, with zero extra infrastructure.
use: {
screenshot: 'only-on-failure',
video: 'retain-on-failure',
}
When a test fails in CI, I get a video of exactly what happened, generated automatically, uploaded as a build artifact. No third-party service, no extra service running alongside the test suite.
The trace viewer, which is a step further than either of the above: a complete, replayable timeline of DOM snapshots, network activity, and console output for a specific run, openable locally with npx playwright show-trace. Selenium has no equivalent built-in artifact this rich.
Codegen, for bootstrapping — not production code, but genuinely useful for a new tester learning the locator API by watching real interactions turn into real code:
npx playwright codegen https://saucedemo.com
Where Selenium Genuinely Still Wins
Cross-company standardization. If fifty teams in a large enterprise already run Selenium, switching frameworks isn't a technical decision alone — it's an organizational one, and the existing investment in Selenium Grid infrastructure, reporting pipelines, and institutional knowledge is real and doesn't disappear because a newer tool exists.
Mobile native app testing. Selenium plus Appium remains the standard path for testing native iOS and Android apps, not just mobile web. Playwright is browser-only — it has no answer to native app automation at all, and that's not a maturity gap that's likely to close, since it's outside what Playwright is built to do.
Legacy and enterprise browser requirements. An internal tool that only runs correctly in IE-mode within Edge, or a regulated environment mandating a specific certified browser configuration, is squarely Selenium's territory. It's an increasingly small corner of the industry, but for the teams actually in it, it's not optional.
Should You Switch?
Starting a new project today: Playwright, with more confidence than the original version of this post had, now that I've actually gone back and checked what's changed rather than repeating a comparison from memory. The auto-waiting difference alone remains the single largest, most consistent driver of flaky-test reduction, and nothing in Selenium 4's genuine improvements — the Duration-based waits, the BiDi network interception, the relative locators — changes that specific architectural gap.
Maintaining an existing Selenium suite: migrate gradually, new files in Playwright, old ones moved over when they need significant rework anyway rather than as a big-bang rewrite.
Learning automation from scratch: learn Playwright first, but don't treat Selenium as obsolete — understanding the WebDriver protocol Selenium is built on, and the explicit-wait discipline it teaches, is still genuinely useful groundwork, especially since Playwright, Cypress, and WebdriverIO all originate from engineers who understood Selenium's limitations firsthand.
The Actual Bottom Line
Playwright is closer to what a test framework designed today, for today's web, would look like — auto-waiting as a true default, network mocking that's been stable for years rather than months, parallelism and rich failure artifacts built in rather than bolted on. Selenium has genuinely improved in ways worth acknowledging rather than dismissing, and the network-interception gap in particular is real ground it's made up. What hasn't changed, and what actually decided this switch for me, is the one thing no amount of incremental improvement to Selenium's explicit-wait ergonomics fixes: Playwright doesn't ask you to remember to wait. It just does.
Check out the Playwright tutorial series on this site if you want to go deep.
