Selenium vs Playwright in 2026: Same Test, Two Frameworks

Table of Contents
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:
- Log in to a dashboard application
- Mock the
/api/dashboard/statsendpoint to return controlled data - 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
<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
<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
Test Code │ ├──▶ HTTP POST ──▶ WebDriver Server ◀── HTTP Response ◀──┘ │ ▼ BrowserEach command = separate HTTP request/response.
Playwright — persistent WebSocket
Test Code │ ◀══▶ WebSocket ◀══▶ Playwright Server │ ◀══▶ CDP WebSocket │ BrowserSingle connection, multiple commands in-flight.
The practical impact shows up in how each framework handles test isolation:
Selenium — New browser per test
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
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.
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
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
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
| Strategy | Selenium | Playwright |
|---|---|---|
| By ID | By.id("username") | page.locator("#username") |
| By CSS | By.cssSelector(".btn") | page.locator(".btn") |
| By test ID | By.cssSelector("[data-testid='x']") | page.getByTestId("x") |
| By role | Not built-in | page.getByRole(AriaRole.BUTTON) |
| By text | By.xpath("//*[text()='Submit']") | page.getByText("Submit") |
| By label | Not built-in | page.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
// 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
// Built-in — no external dependenciespage.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.
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
// Wait for element, then assert separatelyWebDriverWait 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
// Single call — retries until passing or timeoutassertThat(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
// Start tracing before the testcontext.tracing().start(new Tracing.StartOptions() .setScreenshots(true).setSnapshots(true));
// ... run your test ...
// Save trace on failurecontext.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.
When to choose which
| Your situation | Recommendation |
|---|---|
| New project, greenfield | Playwright — less setup, modern API, built-in features |
| Legacy Selenium suite, stable, no pain | Stay with Selenium — migration cost isn’t justified |
| Flaky tests from timing/wait issues | Playwright migration likely worth it — auto-wait helps here |
| Need network mocking in tests | Playwright — no contest, built-in route() API |
| Team is Java-only, no TypeScript | Both work — Playwright Java is solid for browser automation |
| Need Selenium Grid for massive distribution | Selenium — Playwright has no built-in grid equivalent |
| Need video recording/tracing | Playwright — built-in, zero external tooling |
| Heavy iframe or multi-tab workflows | Playwright — first-class frameLocator() and independent Page objects |
| Team has deep Selenium expertise, tight deadline | Stay with Selenium — ramp-up cost matters under pressure |
| Need mobile native app testing | Selenium + 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.
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.
