Hi, my name is Kevin, and I used to write Selenium tests for a living.
I don’t say that with shame, exactly. More the way someone might admit they used to ride a fixed-gear bike to work in the snow. It was a choice. It built character. I wouldn’t do it again.
For a long stretch of my career, Selenium was browser automation. If you tested web apps and you weren’t writing Selenium, you were either writing something that wrapped Selenium, writing something that was about to be replaced by Selenium, or you were Cypress and we weren’t sure about you yet. I built page objects. I argued about page objects. I wrote WebDriverWait so many times my fingers can still type ExpectedConditions.elementToBeClickable faster than my own phone number.
And then, at some point in the last few years, I stopped. Not gradually. Decisively. I’m now an enthusiastic, occasionally insufferable Playwright advocate, and I want to talk about why.
What Selenium got right
Before I throw rocks, credit where it’s due. Selenium did something genuinely important: it standardized browser automation into a protocol — WebDriver — that every major browser vendor agreed to implement. That is not a small thing. The W3C WebDriver spec is the reason “drive a real browser from code” is a solved problem at all. We owe Selenium for that, full stop.
Selenium also has the deepest language coverage in the space, a massive community, and roughly two decades of accumulated knowledge on Stack Overflow. If you need to automate Internet Explorer 11 because a bank told you to, Selenium is still your friend. I am sorry for your situation, but Selenium is your friend.
The papercuts
My Selenium-to-Playwright story isn’t a story about Selenium being bad. It’s a story about a thousand small papercuts that, after enough years, started to feel less like the cost of doing business and more like things a framework could just… handle for me.
A short, incomplete tour of the mild annoyances:
- Explicit waits everywhere. Modern web apps are asynchronous top to bottom. Selenium’s default is “find this element now, fail if it’s not there,” so every action gets wrapped in
WebDriverWait, then in a helper, then in a customExpectedCondition, and eventually somebody slips in aThread.sleep(2000)and you live with it. - Driver management. Chromedriver, geckodriver, the safaridriver opt-in, and a CI matrix that needs all of them at versions that match the browser the build agent happens to have today. WebDriverManager helps. Selenium Manager helps more. The model is still “binary per browser, version-pinned.”
- Network, headers, cookies as side quests. Analytics validation,
traceparentinjection for distributed tracing, auth flows that need a specific cookie set before navigation — all doable in Selenium, none of them easy. You bolt on a proxy, you reach into CDP and hope Chrome doesn’t change it, you evaluate WebDriver BiDi and conclude (accurately, today) that it’s not ready for header modification yet. - Safari. Real, actual Safari. The browser that requires you to manually enable Remote Automation, fight a separate driver that ships with the OS, and accept that parallel execution is essentially “lol, no.” If you’ve ever had a green CI run go red the morning after a macOS update, you know.
- The protocol tax. Every Selenium command is an HTTP round-trip to a driver process that then talks to the browser. It’s fine. It also adds up.
None of these are bad, exactly. They’re just the kind of friction you stop noticing until something removes it.
What Playwright did about all of it
Playwright’s pitch, the way I’d phrase it now: it took every mildly annoying thing about Selenium and just… fixed it. Not reinvented testing. Not replaced the mental model. Fixed the friction.
Going down the same list:
- Auto-waiting locators.
page.getByRole("button", { name: "Submit" }).click()waits for the element to exist, be visible, be stable, and be actionable, then clicks. If any of those never resolve, you get a clean timeout with a useful error. The entireWebDriverWaitindustrial complex in your codebase just isn’t necessary anymore. - One install, every browser.
npx playwright install(or the Java/Python/.NET equivalent) and you get Chromium, Firefox, and WebKit, pinned to versions that match your Playwright version. No driver drift, no separate binaries to chase. - Network interception is first-class.
context.route()lets you mock, modify, or assert on any request the page makes. No proxy. No CDP archaeology. I use this constantly for analytics validation, and it’s the kind of thing that used to take a week of plumbing in Selenium. - Tracing is built in. One config flag and you get a time-travel debugger for failed tests — DOM snapshots, network log, console, screenshots, the works. I’ll write a whole post about this; it changed how my team triages flakes.
- Browser contexts give you cheap test isolation. A new context is essentially a fresh incognito window in milliseconds, with its own cookies and storage. Parallelism gets a lot easier when isolation is free.
- The Safari problem disappears. This one deserves its own paragraph.
Safari (well, WebKit)
In Selenium, automating Safari is a small ordeal. Enable Remote Automation in the Develop menu. Use the system safaridriver. Accept that you can’t run parallel sessions of it. Cross your fingers when the next macOS lands.
In Playwright, “test on Safari” is changing a variable. Pick the webkit browser instead of chromium or firefox. That’s it. You’re now running against Safari’s actual rendering and JavaScript engine, in CI, on Linux, in parallel, with the same API as the other browsers. It’s not Safari-the-app, it’s WebKit — but for catching engine-level rendering and behavior differences, which is the entire reason most of us run Safari tests in the first place, it’s the same thing.
That capability, casually exposed as a config switch, is something Selenium could not dream of offering. And it’s the moment Playwright stopped feeling like a different framework and started feeling like a different category of tool.
Authoring and debugging: screenshots and videos and tracing – oh my!
The other place Playwright lapped the field is in the write a test, figure out why it broke loop. Selenium’s debugging story is, broadly, screenshots. You can wire up videos with third-party recorders, and you can wire up reporters that capture extra context, but every one of those is a thing you build, not a thing you turn on.
Playwright’s debugging story comes in the box:
- Screenshots on failure. Of course.
- Video recording. Per test, configurable to “on,” “off,” or “retain on failure.” Watch the test run back as a video file. No third-party glue.
- Tracing. I keep coming back to this because it is genuinely the feature I miss most when I’m not in Playwright. A trace is a time-traveling DOM-and-network snapshot of a test run — you can scrub through every action, see the page state at that exact moment, inspect the network log, the console, the source. When a test fails in CI at 2 a.m., the trace is most of the investigation done for you.
- Screencasting. The newer screencast capabilities — capturing live frames of the page during a run — open up some genuinely interesting workflows for video evidence and AI-assisted analysis. (More on that in a future post; I’m building toward something here.)
- Network and JavaScript error capture. Subscribe to
page.on("pageerror")and uncaught JS exceptions land in your test output.page.on("console")gives you every console message the app emits during the run.page.on("requestfailed")andpage.on("response")hand you every failed request or non-2xx status, with the URL, headers, and timing already attached. Most teams I show this to immediately wire up “fail the test if the page logged an uncaught error” — it’s a one-liner, and it catches a real category of bugs that would otherwise sail past a green Selenium suite.
And then there’s authoring, which is where Playwright quietly turns into a tutorial for itself:
npx playwright codegen <url>opens the target site, records your clicks and types, and emits Playwright code with sensible locator strategies. Pass--targetand it’ll spit the same recording out as JavaScript, TypeScript, Python, Java, or C# — the recorder doesn’t care what language your suite is in. It is the fastest “first test” you’ll ever write, and I still use it for exploring unfamiliar pages.await page.pause()drops a breakpoint that opens the Playwright Inspector, where you can step through actions, try locators against the live DOM, and copy them straight back into your code. This is the moment most Selenium veterans I show it to go quiet for a few seconds.
If you’re on the Node test runner (@playwright/test), the box gets even fuller. Out of the gate, with no extra dependencies:
- Parallel execution across files, with configurable workers — the default is “use your cores.”
- Automatic retries on failure, configurable globally or per test, with the trace from the failing attempt preserved so you can tell whether a flake is a flake or a real bug.
- Sharding for splitting a suite across CI machines.
- Projects for running the same tests across multiple browsers or configurations from a single config file.
- Fixtures for clean, composable test setup that beats the JUnit/Pytest pattern most of us grew up on.
- UI mode (
--ui), which is essentially a local dashboard for running, watching, and time-traveling through your tests — picture the trace viewer, but live, on every run. - Built-in HTML reporter that embeds traces and videos directly. No Allure plugin gymnastics required (though Allure still works fine if you want it).
The Java, Python, and .NET bindings give you the core library — browsers, contexts, locators, network interception, tracing — but the runner-level conveniences above are Node-specific. That’s a real consideration for which language you pick, and I’ll write more about how I’m threading that needle in Java land at work.
The honest caveat
Playwright is not a silver bullet, and I’ll lose credibility fast if I pretend otherwise. WebKit is close to Safari, not Safari-the-app — if you have a contractual obligation to certify against Safari 17.2 on a specific MacBook, you still need a real-device cloud. Mobile testing through Playwright is browser emulation, not real devices, so iOS Safari on actual hardware is still Appium-or-cloud territory. And the plugin/integration ecosystem, while moving fast, is younger than Selenium’s twenty-year head start.
For the overwhelming majority of “test a web app on evergreen browsers, in CI, without fighting your framework” — which is most of us — Playwright is the answer, and it isn’t close.
More of my Playwright pontifications to come in later posts — there’s a lot of ground to cover, and the auto-waiting story is just the entry ticket.
Credit where it’s wildly due: Microsoft made this. A trillion-dollar company took on browser automation — a space everyone else had quietly accepted as “good enough” — built something genuinely better, and open-sourced it under Apache 2.0. That is not the usual playbook for a company that size, and it deserves to be said out loud. Whoever greenlit Playwright at Microsoft, thank you. The rest of us are getting hours of our lives back because of it.
Selenium taught a generation of us how to drive a browser. Playwright is what happens when someone takes those lessons, throws out the parts that hurt, and ships the framework we should have had all along.
I am, formally, a recovering Selenium developer. I am going forth, and I am testing.
Discover more from Go Forth And Test
Subscribe to get the latest posts sent to your email.
Can’t wait for more Kevin!
What an excellent post! This is 100% KR! So well done. Congrats!