Halmurat T.
Halmurat T.

Senior SDET

Home Blog Books ask About

The Dispatch

Weekly QA notes from the trenches.

Welcome aboard!

You're on the list. Expect real-world QA insights — no fluff, no spam.

© 2026 Halmurat T.

Automation 24
  • Selenium
  • Playwright
  • Appium
  • Cypress
AI Testing 5
CI/CD 6
  • GitHub Actions
  • Slack Reporting
QA Strategy 4
Case Studies 5
Blog/Automation
AutomationHalmurat T./March 5, 2026/15 min

Selenium vs Playwright in 2026: Same Test, Two Frameworks

Filed underselenium/playwright/framework-design/design-patterns
Selenium vs Playwright in 2026: Same Test, Two Frameworks

Table of Contents
  • The scenario we’re building
  • What does it take to get started?
  • How do Selenium and Playwright talk to the browser?
  • Why does Playwright make locators easier?
  • Locator strategies compared
  • Why can’t Selenium mock API responses natively?
  • How do assertions and debugging compare?
  • Debugging: Trace Viewer vs logs
  • What surprised us during migration?
  • Implicit waits behave differently
  • Global driver state doesn’t exist
  • Custom wrappers became dead code
  • Team ramp-up was faster than expected
  • Network interception changed our test strategy
  • When to choose which
  • Your next step

On this page

  • The scenario we’re building
  • What does it take to get started?
  • How do Selenium and Playwright talk to the browser?
  • Why does Playwright make locators easier?
  • Locator strategies compared
  • Why can’t Selenium mock API responses natively?
  • How do assertions and debugging compare?
  • Debugging: Trace Viewer vs logs
  • What surprised us during migration?
  • Implicit waits behave differently
  • Global driver state doesn’t exist
  • Custom wrappers became dead code
  • Team ramp-up was faster than expected
  • Network interception changed our test strategy
  • When to choose which
  • Your next step

Every team I’ve consulted with in the last two years has asked the same question: “Should we switch from Selenium to Playwright?” Instead of handing you another feature comparison table, I’m going to build the same test in both frameworks — same language, same scenario, same assertions — and let the code answer.

The difference between Selenium and Playwright isn’t about which one can do more. It’s about how much ceremony each one demands to do the same thing. After migrating a ~250-test enterprise suite from Selenium to Playwright and shaving roughly 30 minutes off every CI run, I’ve seen both sides. Playwright is newer and shinier, but Selenium still runs the majority of enterprise test suites for good reasons. This post won’t declare a winner — it’ll give you the evidence to decide for yourself.

Full disclosure: I’ve used Selenium for 10+ years and Playwright for the last 3-4. I’m partial to Playwright for new projects, but I’ve also seen migrations that weren’t worth the effort. Bias acknowledged.

The scenario we’re building

Here’s what we’re implementing in both frameworks, in Java:

  1. Log in to a dashboard application
  2. Mock the /api/dashboard/stats endpoint to return controlled data
  3. Assert the UI renders the mocked response correctly

This scenario naturally surfaces the key differences: project setup, locators, auto-waiting, network interception, assertions, and debugging. Everything runs against the same hypothetical dashboard app.

What does it take to get started?

Selenium needs 4-5 Maven dependencies to get a single test running. Playwright needs one. That gap tells you something about each framework’s philosophy.

Selenium — pom.xml

pom.xml (Selenium)
<dependencies>
<!-- Selenium 4.41 -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.41.0</version>
</dependency>
<!-- Test runner -->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.10.2</version>
</dependency>
</dependencies>

Playwright — pom.xml

pom.xml (Playwright)
<dependencies>
<!-- Playwright 1.58 — includes everything -->
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.58.0</version>
</dependency>
<!-- Test runner -->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.10.2</version>
</dependency>
</dependencies>

Selenium 4.41 includes Selenium Manager — a built-in utility that automatically downloads the correct ChromeDriver for your browser version. If you’re still adding WebDriverManager to your pom.xml, you can drop it. Selenium Manager has handled this automatically since version 4.6.

Playwright’s setup is simpler: one dependency, then run mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install" to download browser binaries. No driver version mismatches. No session not created: This version of ChromeDriver only supports Chrome version X errors.

4-5

Selenium dependencies

1

Playwright dependencies

Auto vs CLI

Browser setup

How do Selenium and Playwright talk to the browser?

Selenium sends each command as a separate HTTP request using the W3C WebDriver protocol. Click a button? That’s an HTTP POST. Read text? HTTP GET. Every action is a round-trip. Playwright maintains a single persistent WebSocket connection using the Chrome DevTools Protocol (CDP). Multiple commands can be in-flight simultaneously without waiting for responses.

This architectural difference ripples through everything — speed, reliability, and what features each tool can offer natively.

Selenium — HTTP round-trips

selenium-architecture.txt
Test Code
│
├──▶ HTTP POST ──▶ WebDriver Server
◀── HTTP Response ◀──┘
│
▼
Browser

Each command = separate HTTP request/response.

Playwright — persistent WebSocket

playwright-architecture.txt
Test Code
│
◀══▶ WebSocket ◀══▶ Playwright Server
│
◀══▶ CDP WebSocket
│
Browser

Single connection, multiple commands in-flight.

The practical impact shows up in how each framework handles test isolation:

Selenium — New browser per test

SeleniumBaseTest.java
public class BaseTest {
protected WebDriver driver;
@BeforeMethod
public void setup() {
// ~3-6 seconds per test
driver = new ChromeDriver();
}
@AfterMethod
public void teardown() {
driver.quit();
}
}

Playwright — New context per test

PlaywrightBaseTest.java
public class BaseTest {
static Playwright playwright;
static Browser browser;
protected BrowserContext context;
protected Page page;
@BeforeMethod
public void setup() {
// ~50ms per test
context = browser.newContext();
page = context.newPage();
}
@AfterMethod
public void teardown() {
context.close();
}
}

Selenium launches a new browser process for each test — typically 3-6 seconds of overhead. Playwright reuses one browser and creates a lightweight BrowserContext in roughly 50 milliseconds. Each context is fully isolated — separate cookies, storage, and cache — so tests can’t contaminate each other.

[ TIP ]

In a 250-test suite running in parallel, the difference between 3-6s and 50ms per test adds up fast. This is a major reason our migration cut ~30 minutes off the CI run. More on that in the thread safety deep dive.

When you run 8 parallel threads, Selenium needs 8 browser processes consuming ~300MB each. Playwright runs 8 contexts inside a single browser — roughly 60-120x less startup overhead. That’s the difference between a CI machine that struggles and one that breezes through.

Why does Playwright make locators easier?

Playwright auto-waits for element actionability before every action — visible, stable, enabled, and receiving events. No WebDriverWait boilerplate. No ExpectedConditions. You call click(), and Playwright handles the rest with a default 5-second timeout.

Here’s the login flow in both frameworks:

Selenium — Explicit waits required

SeleniumLoginTest.java
public void login(String user, String pass) {
driver.get("https://app.example.com/login");
WebDriverWait wait =
new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions
.visibilityOfElementLocated(By.id("username")))
.sendKeys(user);
driver.findElement(By.id("password"))
.sendKeys(pass);
driver.findElement(
By.cssSelector("[data-testid='login-btn']"))
.click();
wait.until(ExpectedConditions
.urlContains("/dashboard"));
}

Playwright — Auto-wait built in

PlaywrightLoginTest.java
public void login(Page page, String user, String pass) {
page.navigate("https://app.example.com/login");
page.getByTestId("username").fill(user);
page.getByTestId("password").fill(pass);
page.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Log in"))
.click();
page.waitForURL("**/dashboard");
}

The Selenium version needs 19 lines with explicit waits and ExpectedConditions. The Playwright version does the same thing in 9. Playwright’s getByRole() queries the accessibility tree — not the DOM — which means locators survive UI redesigns that break CSS selectors. We covered this shift in depth in our post on how Playwright’s locator API eliminates custom wrapper classes.

Locator strategies compared

StrategySeleniumPlaywright
By IDBy.id("username")page.locator("#username")
By CSSBy.cssSelector(".btn")page.locator(".btn")
By test IDBy.cssSelector("[data-testid='x']")page.getByTestId("x")
By roleNot built-inpage.getByRole(AriaRole.BUTTON)
By textBy.xpath("//*[text()='Submit']")page.getByText("Submit")
By labelNot built-inpage.getByLabel("Email")

Playwright offers 6 built-in locator strategies compared to Selenium’s 2 primary approaches (ID/CSS/XPath through By.*). The semantic locators — getByRole, getByLabel, getByText — are what make the biggest difference in practice. They match how users actually see the page, not how developers structured the HTML.

Why can’t Selenium mock API responses natively?

Selenium has no built-in network interception. If you want to mock an API response, you need either an external proxy (like the now-abandoned BrowserMob Proxy, last updated in 2017), a separate mock server like WireMock, or Selenium 4’s experimental BiDi network module. Playwright does it in 5 lines.

This is the centerpiece of the comparison. Watch the ceremony gap:

Selenium — External proxy required

SeleniumMockTest.java
// Option 1: BrowserMob Proxy (deprecated since 2017)
BrowserMobProxy proxy = new BrowserMobProxyServer();
proxy.start(0);
proxy.addResponseFilter(
(response, contents, info) -> {
if (info.getOriginalUrl()
.contains("/api/dashboard/stats")) {
contents.setTextContents(
"{\"visitors\": 1234}");
response.setStatus(
HttpResponseStatus.valueOf(200));
}
});
Proxy seleniumProxy = ClientUtil
.createSeleniumProxy(proxy);
ChromeOptions options = new ChromeOptions();
options.setProxy(seleniumProxy);
WebDriver driver = new ChromeDriver(options);

Playwright — Built-in route() API

PlaywrightMockTest.java
// Built-in — no external dependencies
page.route("**/api/dashboard/stats", route -> {
route.fulfill(new Route.FulfillOptions()
.setStatus(200)
.setContentType("application/json")
.setBody("{\"visitors\": 1234}"));
});
page.navigate("https://app.example.com/dashboard");

The Selenium side needs 19 lines, an external dependency that hasn’t been maintained in 8 years, proxy configuration, and modified browser options. The Playwright side needs 7 lines and zero external dependencies. The route() API is built on CDP’s network interception — one of the direct benefits of Playwright’s WebSocket architecture.

[ NOTE ]

Selenium 4.18+ introduced WebDriver BiDi network interception, which adds WebSocket-based network events. It’s a step in the right direction, but it’s still experimental, requires "webSocketUrl": true capability, and has uneven browser support. Firefox has the most complete BiDi implementation; Chrome’s is partial.

Network mocking isn’t a niche feature. Once you have it, you unlock test scenarios that were previously impractical: testing error states without breaking your backend, simulating slow responses for loading UI verification, testing edge cases in data formatting without creating complex test data. In our migration, this single capability eliminated an entire class of “we can’t test that” conversations.

How do assertions and debugging compare?

Playwright’s built-in assertThat() auto-retries until a timeout (5 seconds by default), eliminating the wait-then-assert pattern Selenium requires. Its Trace Viewer gives you a full timeline of every action, network call, and DOM snapshot — something Selenium has no built-in equivalent for.

Selenium — Wait, then assert

SeleniumAssertTest.java
// Wait for element, then assert separately
WebDriverWait wait =
new WebDriverWait(driver, Duration.ofSeconds(10));
WebElement stats = wait.until(
ExpectedConditions.visibilityOfElementLocated(
By.cssSelector("[data-testid='visitors']")));
assertEquals(stats.getText(), "1,234 visitors");

Playwright — Auto-retry assertion

PlaywrightAssertTest.java
// Single call — retries until passing or timeout
assertThat(page.getByTestId("visitors"))
.hasText("1,234 visitors");

Selenium needs 6 lines to wait for an element and assert its text. Playwright does it in 2. The assertThat() method from com.microsoft.playwright.assertions.PlaywrightAssertions retries the assertion continuously — if the element isn’t visible yet or the text hasn’t updated, it keeps checking until the 5-second default timeout expires.

Debugging: Trace Viewer vs logs

When a test fails in Selenium, you get a stack trace and maybe a screenshot if you configured one. When a test fails in Playwright, you can generate a trace file that captures:

  • A timeline of every action with timestamps
  • DOM snapshots before and after each action
  • Network requests and responses
  • Console logs
  • Screenshots at each step
PlaywrightTraceTest.java
// Start tracing before the test
context.tracing().start(new Tracing.StartOptions()
.setScreenshots(true).setSnapshots(true));
// ... run your test ...
// Save trace on failure
context.tracing().stop(
new Tracing.StopOptions().setPath(Paths.get("trace.zip")));
// Open with: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace trace.zip"

This is the debugging feature I miss most when working with Selenium. Instead of adding Thread.sleep() and System.out.println() to reproduce a failure, you replay the entire test execution step by step. Playwright also records video natively — no external tools needed.

What surprised us during migration?

The code translation is the easy part. What catches teams off guard is the architectural shift — Playwright’s scoped model forces you to rethink patterns you’ve relied on for years.

Implicit waits behave differently

In Selenium, many teams rely on a global implicit wait (driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5))) as a safety net. Playwright has no implicit wait — every action auto-waits independently. Tests that “worked” in Selenium because of the implicit wait safety net sometimes revealed timing assumptions we hadn’t noticed. The fix was always better: explicit assertions instead of hidden waits.

Global driver state doesn’t exist

Selenium’s WebDriver is a single stateful object. Playwright’s Page is scoped to a BrowserContext. This means patterns like storing the driver in a static field or passing it through a base class need rethinking. The upside: once you scope everything to Page, cross-test contamination from shared sessions vanishes.

Custom wrappers became dead code

We had ~2,000 lines of custom wait utilities, retry wrappers, and element interaction helpers built for Selenium over 4 years. Playwright’s auto-wait and locator API made roughly 80% of that code unnecessary. The remaining 20% was business logic that should have been separated from framework plumbing all along.

Team ramp-up was faster than expected

Engineers who’d used Selenium for 5+ years were writing productive Playwright tests within a week. The API is more intuitive — page.getByRole(AriaRole.BUTTON).click() reads like a sentence. The biggest adjustment wasn’t learning Playwright; it was unlearning Selenium habits like wrapping everything in try-catch-wait blocks.

Network interception changed our test strategy

Once page.route() was available, we started testing error states, loading states, and edge cases that were previously “too hard to set up.” Our test coverage for API failure scenarios went from nearly zero to comprehensive. That single capability shifted the team’s mindset about what UI tests should verify.

[ WARNING ]

Don’t migrate a stable Selenium suite just because Playwright is newer. If your tests are reliable, your CI is fast, and your team is productive, the migration cost isn’t worth it. Migrate when you’re hitting real pain — flakiness, slow execution, or missing capabilities like network mocking.

When to choose which

Your situationRecommendation
New project, greenfieldPlaywright — less setup, modern API, built-in features
Legacy Selenium suite, stable, no painStay with Selenium — migration cost isn’t justified
Flaky tests from timing/wait issuesPlaywright migration likely worth it — auto-wait helps here
Need network mocking in testsPlaywright — no contest, built-in route() API
Team is Java-only, no TypeScriptBoth work — Playwright Java is solid for browser automation
Need Selenium Grid for massive distributionSelenium — Playwright has no built-in grid equivalent
Need video recording/tracingPlaywright — built-in, zero external tooling
Heavy iframe or multi-tab workflowsPlaywright — first-class frameLocator() and independent Page objects
Team has deep Selenium expertise, tight deadlineStay with Selenium — ramp-up cost matters under pressure
Need mobile native app testingSelenium + Appium — Playwright doesn’t support native mobile apps

We covered a similar decision framework for Cypress migration decisions — many of the same principles apply. The question is never “which tool is better?” It’s “which tool fits your team’s context right now?”

Your next step

Pick one flaky test from your Selenium suite — the one that fails every other Thursday and nobody can explain why. Rewrite it in Playwright this week. Compare the experience: how long the rewrite took, whether the flakiness disappeared, and how the debugging felt when something went wrong. That single test will tell you more about whether migration is right for your team than any comparison article — including this one.

§ Frequently Asked FAQ
+ Can Playwright replace Selenium in 2026?

Yes for most web automation use cases. Playwright handles modern web apps with built-in auto-waiting, network interception via route(), and browser context isolation. However, Selenium still has advantages in Selenium Grid for distributed execution, broader browser version support, and native mobile testing via Appium. The right choice depends on your team’s context, existing infrastructure, and what pain points you’re trying to solve.

+ Is Playwright faster than Selenium?

Yes. Playwright’s WebSocket architecture and BrowserContext isolation make it significantly faster. Context creation takes roughly 50ms versus 3-6 seconds for a new Selenium browser instance. In our 250-test enterprise migration, we saved approximately 30 minutes per CI run — driven by faster test isolation, less browser startup overhead, and elimination of explicit wait boilerplate.

+ Should I migrate my existing Selenium suite to Playwright?

Only if you’re experiencing real pain — flaky tests from timing issues, slow CI execution, or missing capabilities like network interception. A stable, well-maintained Selenium suite doesn’t need migration for migration’s sake. The cost of rewriting tests and retraining a team is significant. Start with a single flaky test rewrite to evaluate the effort before committing to a full migration.

+ Does Playwright support Java?

Yes. Playwright has supported Java since version 1.10 (March 2021) with the same browser automation API as the TypeScript version. Note that some developer tooling — UI Mode, HTML Reporter, and the VS Code extension — are TypeScript-only. The core automation API (Page, Locator, BrowserContext, route(), assertThat()) is fully available in Java.

+ Can I use Selenium and Playwright in the same project during migration?

Yes. Many enterprise teams run both frameworks in parallel during migration, converting tests incrementally. A common pattern is keeping Selenium tests in a legacy/ folder while new tests use Playwright, with separate Maven profiles for each. This avoids the “big bang rewrite” risk and lets you validate Playwright’s benefits on real tests before committing fully.

§ Further Reading 03 of 03
01Automation

XPath text() vs Dot — Why Your Text Match Fails

The real difference between XPath text(), dot, contains(), and normalize-space() for test automation — with examples that explain real flaky failures.

Read →
02Automation

Stop Launching a Browser Per Test in Playwright

Launching a new browser per test in Playwright wastes 3-6 seconds each time. BrowserContext gives you the same isolation in milliseconds. Here's how to switch.

Read →
03Automation

Stop Using locator() for Everything in Playwright

Most Playwright teams still use CSS selectors via locator() out of habit. getByRole and getByText find elements the way users do — and survive redesigns.

Read →

Don't miss a thing

Subscribe to get updates straight to your inbox.

HT

No spam · Unsubscribe anytime

Welcome aboard!

You're on the list. Expect real-world QA insights — no fluff, no spam.

§ Colophon

Halmurat T. — Senior SDET writing about test automation, CI/CD, and QA strategy from 10+ years in the enterprise trenches.

Set in
IBM Plex Sans, Lora, and IBM Plex Mono.
Built with
Astro, MDX, Tailwind CSS & Expressive Code. Served by Vercel.
Privacy
No cookies. No tracking scripts on the main thread — analytics run sandboxed via Partytown.
Source
github.com/Halmurat-Uyghur
Terminal
Try /ask to query Halmurat's notes in a shell prompt.

© 2026 Halmurat T. · Written in plain text, shipped in plain time.

Search
Esc

Search is not available in dev mode.

Run npm run build then npm run preview:local to test search locally.