ProductHow it worksPricingBlogDocsLoginFind Your First Bug
Selenium to Playwright migration showing WebDriver protocol stack being replaced by direct browser automation with built-in auto-waiting
TestingPlaywrightSelenium

Selenium to Playwright Migration: What to Rewrite vs. Wrap

Tom Piaggio
Tom PiaggioCo-Founder at Autonoma

Selenium to Playwright migration moves your test suite from the WebDriver HTTP protocol (with its explicit waits, Grid infrastructure, and per-action round-trip latency) to Playwright's direct browser control with built-in auto-waiting and native parallelism. The mechanics are a locator rewrite, a wait-strategy deletion, and a CI reconfiguration. Most teams escape the Selenium Grid entirely within one sprint of Playwright being green.

The tipping point is usually the same across teams: a Monday CI run where 40% of the suite is red, half of it from explicit waits that no longer match the page's load timing. Someone re-runs. Half of those pass. Someone else re-runs again. The team has spent 90 minutes on a test suite that found zero real bugs. The Selenium + Grid stack has been doing this for months, and the cost is finally visible enough to act on.

This post is about the mechanics of the move, not the rationale. If you want the "why" with numbers, the Playwright vs Selenium 2026 comparison has the full cost breakdown, and the Selenium, Playwright, and Cypress comparison sets all three frameworks side by side. This post assumes you have already decided. The question now is how. If your suite runs on Cypress rather than Selenium, the Cypress to Playwright migration guide covers that path instead.

The mental-model shift: WebDriver to Playwright

The most important thing to understand before writing a single line of Playwright is that the frameworks have opposite defaults on waiting.

Selenium assumes the page is ready unless you tell it otherwise. You call driver.findElement(By.id("submit")) and Selenium looks for that element right now. If it is not there, the call fails. The WebDriverWait + ExpectedConditions pattern is the fix: you tell Selenium explicitly to wait up to N seconds for a condition before proceeding. Every WebDriverWait block in your suite represents a place where someone got burned by that default and added a guard.

Playwright inverts this. Every action (click, fill, check, hover) automatically waits for the target element to be attached, visible, enabled, and stable before executing. This is called actionability checking, and it runs before every interaction without any configuration. The practical effect: almost every WebDriverWait block you have written becomes deletable. Not replaceable. Deletable.

The second shift is locators. Selenium's findElement returns a WebElement reference, a snapshot of the DOM at the moment you called it. If the DOM updates (navigation, re-render, animation), that reference goes stale and throws a StaleElementReferenceException. Playwright's Locator is lazy: it stores a query, not a reference. Each time you act on a locator, Playwright re-evaluates the query against the current DOM. No stale references. This is why migrating the locator layer is a rewrite, not a copy-paste: the semantics are different enough that direct translation produces subtly broken tests.

The third shift is execution architecture. Selenium Grid distributes tests across remote browser nodes via the WebDriver protocol, adding network hops and node management overhead. Playwright's parallelism is built into the test runner: --workers spins up isolated browser contexts in the same process. For most CI matrix use cases (cross-browser, cross-config), Playwright's projects config replaces the Grid entirely without a separate infrastructure component.

Under the hood, Playwright talks directly to Chromium via CDP (Chrome DevTools Protocol), and uses patched browser builds for Firefox and WebKit. Every command is a direct connection, not an HTTP round-trip through a WebDriver server. This is why the 50-200ms per-action overhead that characterizes Selenium disappears in Playwright.

Diagram comparing Selenium WebDriver HTTP round-trips and explicit waits against Playwright's direct CDP connection and auto-waiting

Selenium routes every action through a WebDriver server and guards it with an explicit wait. Playwright talks to the browser directly and waits for actionability on its own.

The API mapping: Selenium to Playwright

The table below maps the Selenium WebDriver APIs you encounter most often to their Playwright equivalents. The Notes column flags where the behavior differs enough to matter.

SeleniumPlaywrightNotes
driver.findElement
(By.id("x"))
page.locator("#x")
or page.getByTestId("x")
Locator is lazy (re-evaluated each action). No StaleElementReferenceException.
driver.findElement
(By.css(".cls"))
page.locator(".cls")Playwright strict mode throws if selector matches more than one element.
driver.findElement
(By.xpath("//button"))
page.locator("xpath=//button")
or page.getByRole("button")
Prefer role/text/label locators over XPath. More resilient to DOM changes.
WebDriverWait
+ ExpectedConditions
(not needed)Auto-waiting covers visibility, enabled, stable. Delete the wait block entirely.
driver.manage()
.timeouts()
.implicitlyWait()
(not needed)Implicit waits are removed. Playwright's actionability checks make them redundant.
driver.get(url)page.goto(url)Waits for load event by default. Pass waitUntil: "networkidle" for SPAs if needed.
element.sendKeys("text")locator.fill("text")fill() clears the field first. Use pressSequentially() to simulate keystrokes.
element.click()locator.click()Playwright scrolls into view and waits for actionability before clicking.
Select(element).select_by_value()locator.selectOption
({value: 'x'})
Supports value, label, or index. Pass an array for multi-select.
Actions()
.moveToElement()
.build().perform()
locator.hover()Complex drag chains: locator.dragTo(target). Much less ceremony.
driver.switchTo().frame(element)page.frameLocator("iframe")
.locator("...")
FrameLocator chains naturally. No explicit context switch needed.
driver.switchTo().alert().accept()page.on("dialog",
d => d.accept())
Register handler before triggering the dialog. Event-based, not imperative.
driver.getScreenshotAs
(OutputType.BYTES)
page.screenshot
({ path: 'shot.png' })
Element-level: locator.screenshot(). Full page: pass fullPage: true.
TestNG/JUnit assertionsexpect(locator).toBeVisible()Web-first assertions: retry until timeout. No explicit wait loop needed.
RemoteWebDriver
+ Grid URL
--workers N
or projects config
Cross-browser via projects (chromium/firefox/webkit). No separate Grid process.
driver.quit()Context/browser closed in afterEach fixturePlaywright test fixtures handle teardown automatically in most setups.

What to rewrite vs wrap

Not everything ports. Not everything needs to. Being clear about the boundary saves weeks.

Rewrite completely. The locator and assertion layer is the heart of a Selenium test, and it does not port. Selenium's By selectors, WebElement references, and WebDriverWait + ExpectedConditions have no direct Playwright equivalent. You are not translating syntax; you are adopting a different execution model. The same is true for thread-local driver management (the ThreadLocal<WebDriver> pattern common in Java Selenium frameworks): Playwright isolates tests via browser contexts, not threads, so the entire driver lifecycle design changes. Custom ExpectedConditions helpers and page object base classes that extend Selenium's LoadableComponent are also rewrite targets. Keep the structure and intent. Discard the implementation.

Keep and adapt. Test data factories and fixture builders survive intact if they are decoupled from WebDriver. The page object concept (a class per page, methods per action) carries over cleanly; only the method bodies change. CI orchestration config outside of Grid setup (parallelism parameters, environment variables, artifact paths) is mostly reusable. Domain knowledge encoded in helper utilities (date formatting, test ID generators, API client wrappers) travels unchanged.

Do not bother porting. Selenium Grid configuration is the largest category of deliberate abandonment. The hub/node topology, RemoteWebDriver setup code, and Grid capacity planning are all superseded. Playwright's --workers flag and the projects configuration handle the same cross-browser and parallel execution use cases with zero infrastructure. Any utility code that exists to work around Selenium's stale element problem (retry wrappers, element re-fetch helpers) is also not worth porting: the problem it solves does not exist in Playwright.

The migration scope is usually smaller than it feels. In most codebases, the locator and wait layer is 60-70% of what changes. The business logic, test data, and scenario structure survive.

Three-column diagram sorting Selenium suite components into rewrite, keep and adapt, or discard during a Playwright migration

The locator and wait layer gets rewritten, test data and page object structure carry over, and the Grid infrastructure is discarded outright.

A staged migration plan

The mistake most teams make is treating the migration as a flag-day rewrite: freeze new development, port everything, cut over. That approach produces a six-month project that runs over and gets cancelled halfway. The alternative is parallel execution.

Stage 1: Stand up Playwright alongside Selenium in CI. Do not touch the Selenium suite. Add a separate Playwright config pointing at the same test environment. Write two or three new tests in Playwright: a happy-path login, a checkout flow, whatever is highest-value. Get them green. At this point, you have confirmed that Playwright runs in your CI environment, your application is reachable from Playwright's browser context, and your team knows how to write Playwright tests. This takes a sprint or less.

Stage 2: Migrate by flow, starting with the flakiest. Pull a report of your Selenium tests by failure rate over the last 30 days. The flakiest tests are the ones whose explicit waits are most out of step with the current UI behavior. Start there. Migrating a flaky Selenium test to Playwright often reveals that the underlying scenario is straightforward; the flakiness was entirely in the wait strategy. As each flow is migrated and green, delete the Selenium equivalent. Do not run both. Parallel coverage creates confusion about which suite is authoritative.

Stage 3: Retire the Grid. Once Playwright covers your cross-browser matrix via the projects config, the Grid has no remaining workload. Schedule the decommission. Depending on your organization, this might be a Terraform teardown, a cloud instance termination, or a conversation with the platform team. Either way, it is a line item that disappears from the infrastructure budget. On average, teams running a mid-sized suite get to this stage in two to four months.

The sequencing matters because it keeps the Selenium suite as the safety net throughout the process. You are never in a state where both the old suite and the new suite are broken simultaneously. The Playwright suite is green and growing; the Selenium suite shrinks as flows are retired.

How Autonoma handles what frameworks leave unmaintained

Migration to Playwright trades one maintenance problem for a smaller one. You no longer manage stale references, WebDriver version pinning, or Grid node failures. What remains is the locator layer itself: the selectors that tie tests to the current UI structure. When the UI changes, those selectors break, and someone has to fix them.

This is the gap that locator-level self-healing tools try to fill. They store a selector and a fallback, and when the primary selector fails, they try the fallback and update the stored value. It is a useful band-aid for CSS class renames and ID changes. The locator is patched; the test keeps running.

We built Autonoma one level above that. Our four agents (Planner, Executor, Reviewer, and Diffs Agent) do not maintain selectors at all. The Planner agent reads your codebase: routes, components, user flows, database schema. It plans test cases from that analysis, not from recorded sessions or natural-language prompts. The Executor agent runs those tests against a live preview environment. The Reviewer agent classifies each result: real application bug, agent error, or test/plan mismatch. The Diffs Agent runs on every PR, analyzes what changed in the code, and updates the test plan to match. When your UI changes, the Diffs Agent generates updated tests from the new code, not healed versions of the old tests.

The practical difference: a migration to Playwright gives you a better framework to maintain. Autonoma gives you a suite that maintains itself. The two are not mutually exclusive. Teams use Playwright for bespoke edge cases that need hand-written logic, and Autonoma for the routine E2E surface that would otherwise require constant upkeep.

For teams on the Playwright locators learning curve after the migration, or using Playwright codegen to scaffold the new tests, Autonoma sits above both: it is not a Playwright plugin and not a test recorder. It is the layer that generates and maintains E2E tests directly from your codebase so you do not have to.

FAQ

For most teams migrating today: yes. Playwright's auto-waiting eliminates the explicit-wait flakiness that defines the Selenium maintenance experience. Its locator API avoids stale element references. Built-in parallelism replaces Selenium Grid. The exception cases -- multi-language shops, regulated environments with approved-framework lists, suites with 5,000+ tests where migration risk is high -- are real and worth respecting. For a detailed breakdown, see the Playwright vs Selenium 2026 comparison on this blog.

The mechanical path: rewrite the locator and assertion layer (By selectors to Playwright locators, WebDriverWait to nothing, TestNG/JUnit assertions to Playwright web-first assertions), delete all explicit wait code, replace thread-local driver management with Playwright's fixture system, and swap RemoteWebDriver + Grid for Playwright's --workers flag and projects config. Keep test data factories, page object structure, and CI orchestration config. The migration is most manageable as a staged parallel run: new Playwright suite green alongside the existing Selenium suite, migrating flow by flow from flakiest to most stable, retiring each Selenium test once the Playwright equivalent is confirmed.

Yes, for virtually all CI matrix use cases. Playwright's projects configuration covers cross-browser execution (Chromium, Firefox, WebKit) and cross-config matrices without a separate infrastructure component. The --workers flag handles parallelism in-process. Teams running Selenium Grid for distributed execution across physical nodes at very large scale may need a cloud testing platform (BrowserStack, Sauce Labs, LambdaTest all support Playwright), but the vast majority of Grid users find Playwright's built-in parallelism sufficient and dramatically simpler to operate.

Nothing. Playwright's auto-waiting replaces explicit waits entirely. Every action (click, fill, check, hover) automatically waits for the target element to be attached, visible, enabled, and stable before executing. The web-first assertions (expect(locator).toBeVisible(), toHaveText(), toBeEnabled()) also retry until the condition is met or the timeout expires. If you find yourself reaching for a wait in Playwright, it usually means the assertion target is not specific enough. Make the assertion more precise rather than adding a wait.

Rough benchmarks: a 100-test suite takes 2-4 weeks with one experienced engineer. A 500-test suite takes 2-3 months. A 1,000+ test suite should be treated as a phased migration over 6-12 months: new tests in Playwright, existing Selenium tests retired as they are migrated. Budget more time for validation (confirming the migrated test covers the same scenario with equivalent reliability) than for the mechanical rewrite. The syntax changes are fast; confirming correctness is where time actually goes.

Related articles

Playwright vs Selenium 2026 comparison showing the hidden maintenance tax of WebDriver-based testing suites versus modern browser automation

Playwright vs Selenium in 2026: $216,000 in Annual Costs Your Budget Does Not Show

Playwright vs Selenium compared across 20 dimensions with real cost data. The Selenium Maintenance Tax framework shows what your suite actually costs per year.

Quara the frog mascot inspecting a dark browser recording console with a codegen snapshot freezing in place while the live UI behind it continues to change

How to Use Playwright Codegen (and Why Recorded Tests Rot)

Complete guide to Playwright codegen: run the Inspector, record authenticated sessions, generate assertions in 5 languages, and avoid the test-rot trap.

Playwright locators priority order diagram showing getByRole, getByLabel, getByTestId and selector stability hierarchy

Playwright Locators: Types, Priority Order, and Why They Break

Complete guide to Playwright locators: every built-in type, the official priority order, locator vs ElementHandle, strict mode, and why locators break in 2026.

A cracked open server rack revealing gears, dollar signs, and hourglass elements representing the hidden total cost of ownership of a self-hosted Selenium grid

The True Cost of an In-House Selenium/Playwright Grid

Self-hosted Selenium grid cost isn't $0. Model the true total cost of ownership: infra, setup engineer-time, ongoing maintenance, and flake-chasing. As of mid-2026.