Power Your Software Testing with AI Agents and Cloud

The Native AI-Agentic Cloud Platform to Supercharge Quality Engineering. Test Intelligently and Ship Faster.

AutomationSeleniumPlaywright Testing

How to Migrate from Selenium to Playwright

A Selenium to Playwright migration guide with verified API mapping tables, the wait-deletion rule, harness conversion, and what does not convert at all.

Published on:

You convert your first Selenium spec to Playwright, run it, and it fails on line one with an error you have never seen: strict mode violation, locator resolved to 7 elements. The selector is the same one that worked in Selenium for three years. Nothing is broken. Playwright is telling you the selector was always ambiguous and Selenium picked the first match without complaint.

That is the shape of this migration. The syntax translation is mechanical and an agent can do most of it. What costs real engineering time is the handful of places where a line-for-line port compiles, runs, and means something different from the test you started with. This guide gives you the mappings, the rules that stop a converted suite being flakier than the original, and an honest list of what does not convert at all.

Overview

Migrating from Selenium to Playwright means replacing eager WebElement handles with lazy locators, deleting explicit waits in favour of built-in auto-waiting, and rebuilding the harness around fixtures instead of driver factories. The syntax converts mechanically; strict mode, non-retrying query methods and the harness are where the work actually is.

What Changes First

  • Strict mode: a Playwright locator matching more than one element throws instead of silently using the first, so ambiguous selectors that Selenium tolerated fail on day one.
  • Lazy locators: a locator stores the query rather than the DOM node and re-resolves on every use, which is why StaleElementReferenceException has no Playwright equivalent.
  • Auto-waiting: before acting, Playwright checks that an element is attached, visible, stable, able to receive events and enabled, which makes most explicit waits redundant.
  • Query methods do not retry: textContent and getAttribute resolve once, so porting getText into a plain assertion removes the retry your Selenium wait was providing.

What Does Not Move

Appium mobile tests have no Playwright equivalent and stay where they are. Running the migrated suite across real browser and OS combinations is infrastructure work, and TestMu AI runs Selenium and Playwright on the same cloud grid so both suites can coexist during the transition.

Should You Migrate from Selenium to Playwright?

Migrate when you are hitting a limit, not when you are bored of the syntax. A working Selenium suite that your team maintains comfortably is not a problem that needs solving, and the migration will introduce fresh instability while the new suite settles.

The honest triggers are:

  • Wait-driven flakiness - your suite is a thicket of explicit waits and the occasional sleep, and failures correlate with CI load rather than code changes.
  • Stale element churn - your app re-renders and you spend time re-finding elements you already found.
  • Grid maintenance - nodes, driver drift and browser version pinning cost more attention than the tests do.
  • Cross-origin and multi-tab flows - you are working around the browser rather than driving it.
  • Debugging time - a CI failure gives you a stack trace and a screenshot, and reproducing it locally takes longer than fixing it.

Reasons to Stay on Selenium

A browser requirement or a mobile dependency can cancel a migration outright, and both are easy to discover late.

The first is browser requirements. Playwright drives its own bundled browser builds, including a WebKit build that is not Safari. If you have a compliance or support requirement to test on a specific real browser version, or on Safari proper, Selenium plus a real-browser grid still answers that and Playwright's bundled builds do not.

The second is mobile. Playwright has no Appium equivalent. If a meaningful part of your estate drives native apps or real devices, that part is not migrating, and your plan needs to say so rather than discovering it in week three.

It is also worth being accurate about what Selenium is in 2026 rather than comparing against the Selenium of five years ago. Selenium Manager resolves drivers automatically, so manual driver downloads are no longer a differentiator, and WebDriver BiDi gives Selenium a bidirectional protocol of its own. If you are building a migration case, build it on waits, locator resilience and debugging, which are real, rather than on driver management, which is not. For a broader side-by-side, see the Playwright vs Selenium vs Cypress comparison.

Triage Your Suite Before You Convert Anything

Every migration guide tells you to audit your suite and none of them say how. The audit is a handful of greps, and it tells you the size of the job in about ten minutes.

# How much of your suite is wait logic?
grep -rc "WebDriverWait\|ExpectedConditions" src/test/ | grep -v ":0"

# Sleeps are the specs that will need real thought
grep -rn "Thread.sleep" src/test/

# Locator strategy mix - XPath volume predicts rewrite effort
grep -rho "By\.\w*" src/test/ | sort | uniq -c | sort -rn

# Custom infrastructure with no automatic equivalent
grep -rln "ThreadLocal<WebDriver>\|DriverFactory\|@FindBy" src/test/

# The blocker: anything Appium is not migrating
grep -rln "io.appium\|AndroidDriver\|IOSDriver" src/test/

Read the output as a plan. A high Thread.sleep count means specs that hide real race conditions, and those need attention rather than translation. Heavy @FindBy and DriverFactory usage means the harness is the bulk of the work, not the tests. Any Appium hit is scope you remove from the migration entirely.

Note

Note: Run your Selenium suite and your new Playwright suite side by side on the same cloud grid during the migration. Try TestMu AI Today!

Conceptual Shifts Behind the Mappings

Three conceptual shifts explain almost every surprise in a converted suite. Understanding them first makes the mapping tables read as obvious rather than arbitrary.

Eager Handles Become Lazy Queries

Selenium's findElement queries the DOM at the moment you call it and hands back a WebElement bound to that one node. If the page re-renders, that handle is stale and the next use throws.

A Playwright locator performs no DOM query when you create it. It stores the query, and every time you use it, Playwright locates an up-to-date element. You can build a locator before the page has even loaded. The practical consequence is that StaleElementReferenceException has no equivalent, because there is no stored node to go stale. It is also why you cannot test existence by constructing a locator; use a count or an assertion instead.

Strict Mode Surfaces Ambiguity You Already Had

This is the error that greets most people on their first converted spec. Any locator action that resolves to more than one element throws rather than acting on the first match. Selenium's findElement silently returned the first, which meant an ambiguous selector could pass for years while testing the wrong element.

Treat each violation as a bug report about your selector, not an obstacle. Narrow with a scope or a filter rather than reaching for first(), which just reinstates the old behaviour and the old blind spot. Counting operations are exempt, so count(), all() and a count assertion all work happily on many elements. The Playwright locators guide covers the filtering patterns in depth.

Language Choice Decides How Much Rewriting You Do

Selenium has official bindings for Java, Python, C#, Ruby and JavaScript. Playwright has Java, Python, .NET and Node. A Java Selenium suite can become a Java Playwright suite, and the Java, Python and .NET APIs are synchronous in style, so the control flow you already have survives.

Node is the exception: it is async throughout and every call needs await. Advice that every migrated method becomes async is true only if you are moving to Node. Ruby has no official Playwright binding at all, which makes a Ruby Selenium suite a language migration as well as a framework one. Decide this before you convert a single spec, because it is the one choice that touches every file.

Locators and Interactions

The third column is the one that matters. A row marked Needs care compiles and runs but does not mean the same thing as the Selenium line it replaced.

Selenium (Java)PlaywrightNotes
driver.get(url)await page.goto(url)Needs care. goto does not throw on a 404 or 500 response. Check response.status() if your test relied on that.
By.id("username")page.getByLabel('Username')Mechanical form is page.locator('#username'). Role and label locators survive refactors better.
By.name("email")page.locator('[name="email"]')Needs care. There is no name engine. getByTestId only supports id, data-testid, data-test-id and data-test.
By.cssSelector("[data-test=submit]")page.getByTestId('submit')Needs care. getByTestId reads data-testid by default. For data-test you must set testIdAttribute in the config or it matches nothing.
By.className("information")page.locator('.information')Clean. Do not swap this for getByLabel, which matches label text rather than the class attribute.
By.xpath("//button[text()='Pay']")page.getByRole('button', { name: 'Pay' })Needs care. XPath works verbatim, but it is the one locator type that does not pierce shadow DOM, so a working Selenium XPath can fail on a web-components page.
By.linkText("Sign in")page.getByRole('link', { name: 'Sign in', exact: true })Needs care. Matches accessible name, not raw text. Name matching is substring and case-insensitive unless exact is set.
element.sendKeys("text")await locator.fill('text')Needs care. fill sets the value in one step. Use pressSequentially only when the app depends on per-keystroke events.
new Select(el).selectByVisibleText("A")await locator.selectOption({ label: 'A' })Clean. There is no Select wrapper class to construct.
new Actions(driver).moveToElement(el)await locator.hover()Clean, and more faithful than the Actions chain it replaces.
((JavascriptExecutor) driver).executeScript(js)await page.evaluate(js)Clean. Runs in the page context the same way.
element.sendKeys("/path/file.pdf")await locator.setInputFiles('/path/file.pdf')Clean, and works on inputs that are hidden behind a styled button.

Waits and Assertions

This section is where a mechanically converted suite becomes flakier than the Selenium original. The wait-deletion rule and the retry rule prevent it.

The Wait Deletion Rule

Common advice is to delete your explicit waits. Followed literally, that converts explicit waits into silent races. The rule is narrower: delete the wait when the thing you were waiting for is one of the five actionability checks Playwright already performs, and keep it as an assertion when you were waiting for application state.

Before acting, Playwright checks that the element is attached to the DOM, visible, stable (not animating), able to receive events (not obscured), and enabled. A wait for any of those is redundant. A wait for a spinner to disappear, a row count to settle or a toast to appear is application state and becomes a web-first assertion.

Selenium waitPlaywrightNotes
ExpectedConditions.elementToBeClickable(delete)Covered by the visible, stable, receives-events and enabled checks before every action.
ExpectedConditions.visibilityOfElementLocatedawait expect(locator).toBeVisible()Delete it if the next line acts on the element; keep it as an assertion if visibility is the thing under test.
ExpectedConditions.invisibilityOfawait expect(locator).toBeHidden()Keep. A spinner disappearing is application state, not actionability.
ExpectedConditions.textToBePresentInElementawait expect(locator).toHaveText('x')Keep as an assertion. This one auto-retries.
ExpectedConditions.numberOfElementsToBeawait expect(locator).toHaveCount(n)Keep. Exempt from strict mode.
ExpectedConditions.urlContainsawait expect(page).toHaveURL(/x/)Keep.
ExpectedConditions.stalenessOf(no equivalent needed)Lazy locators mean nothing goes stale.
implicitlyWait(Duration)(delete)No equivalent and none needed. Timeouts are configured per action and per assertion.
Thread.sleep(2000)(delete, then fix the race)A sleep marks a race the original author did not diagnose. Replace with the assertion that describes what you were really waiting for.

Query Methods Do Not Auto-Retry

This is the subtler of the two rules and the one that caught us in testing. expect(locator) auto-retries until it passes or times out. textContent(), getAttribute() and inputValue() resolve once and return immediately. So the natural port of Selenium's getText followed by assertEquals produces a test that reads the DOM before the application has updated it.

We hit exactly this while verifying the code for this guide. Running a migrated flow against the Selenium Playground on the TestMu AI cloud grid, the first version read the output element straight after the click and asserted an empty string:

// Reads once, before the handler has written the value
const shown = await page.locator('#message').textContent();
// -> ASSERTED MESSAGE: ""

The web-first assertion passes against the identical page, because it polls until the DOM matches:

await page.getByPlaceholder('Please enter your Message').fill('Migrated from Selenium');
await page.locator('#showInput').click();
await expect(page.locator('#message')).toHaveText('Migrated from Selenium');
// -> PASS  #message = "Migrated from Selenium"

Nothing about the page changed between those two runs. The only difference is whether the assertion retries. If you take one rule from this guide, take this one: assert on the locator, not on a value you already extracted.

Next-generation test execution with TestMu AI

The Harness Is Where the Time Goes

Converters handle test bodies. They do not handle the scaffolding around them, and on a mature suite that scaffolding is most of the work.

Selenium harnessPlaywright equivalent
@BeforeMethod / @AfterMethodtest.beforeEach / test.afterEach
@BeforeClass / @AfterClasstest.beforeAll / test.afterAll
ThreadLocal<WebDriver> and driver factoriesNothing. The page fixture is per-test and isolated by default.
@FindBy plus PageFactory.initElementsNothing. A page object holds locators as plain fields.
@DataProviderA loop over an array that generates tests, or projects for config-level variation
TestNG suite XML thread-countworkers in playwright.config, and --shard across machines
Custom login helper called in setupA setup project writing storageState, reused by every test
ExtentReports or AllureThe built-in HTML reporter and trace viewer

Page Objects Get Simpler

A Selenium page object takes a driver, declares fields with @FindBy and calls PageFactory to populate them. The Playwright equivalent takes a page and assigns locators directly, because a locator is already a lazy query and needs no initialisation step.

export class LoginPage {
  constructor(page) {
    this.page = page;
    this.email = page.getByLabel('Email');
    this.password = page.getByLabel('Password');
    this.submit = page.getByRole('button', { name: 'Sign in' });
  }

  async login(email, password) {
    await this.email.fill(email);
    await this.password.fill(password);
    await this.submit.click();
  }
}

The locators are assigned in the constructor but resolve nothing until used, so there is no staleness risk in holding them. If you are formalising this layer during the migration, the page object model guide covers the structure in more detail.

Authentication Is the Biggest Wall-Clock Win

If every Selenium test logs in through the UI in setup, that cost is paid once per test. Playwright lets you log in once in a setup project, save the browser state, and have every other test start authenticated. On a large suite this removes more runtime than any other single change in the migration.

What Does Not Convert?

Selenium featureWhy it does not convertWhat to do
Appium mobile testsPlaywright has no Appium equivalent and does not drive native apps or real devices.Out of scope. Keep that suite where it is.
Thread.sleep and implicit waitsNo equivalent exists, by design.Delete, then fix the race each one was hiding.
StaleElementReferenceException handlingThe exception cannot occur against a lazy locator.Delete the retry wrappers built around it.
driver.switchTo().frame()No driver-level mode switch.page.frameLocator(), which chains like any locator.
driver.switchTo().alert()Playwright auto-dismisses dialogs by default.Register page.on('dialog') before the action that triggers it, or the dialog is already gone.
driver.switchTo().window()Tabs are separate Page objects, not a driver mode.Wait for the popup event and work with the returned page.
DesiredCapabilities and ChromeOptionsDifferent configuration model.launchOptions and projects in playwright.config.
Actions chainsMost chains exist to work around missing primitives.Use hover, dragTo and the keyboard API directly.
Real-browser version pinningPlaywright ships its own bundled builds; its WebKit is not Safari.Keep a real-browser grid for the specs that genuinely require it.

Running Both Suites During the Transition

Migrate incrementally and keep Selenium authoritative until each replacement has been stable for several days. Two practical rules keep that period honest.

First, never let a spec exist in both suites as a passing test at the same time without comparing them. A Playwright test that passes while its Selenium twin fails deserves investigation before you trust it; more than once it means the new test is asserting less than the old one.

Second, decide what a red build means while both run. The usual answer is that Selenium gates the merge until its replacement has earned the gate, and the Playwright job runs non-blocking until then.

You do not have to retire your Grid on day one either. Playwright can connect to a Selenium 4 Grid through the SELENIUM_REMOTE_URL environment variable for Chromium-based browsers, which Playwright describes as experimental. It buys you a transition period on infrastructure you already pay for, but Firefox and WebKit will not run that way.

Longer term, the infrastructure question outlives the framework question. TestMu AI's test automation cloud runs Selenium, Playwright, Cypress and Puppeteer across 3,000+ real browser and OS combinations with no grid to stand up or patch, which means both suites can run on one place while you migrate rather than you maintaining two sets of infrastructure. To point an existing config at it, follow the setup guide for Playwright testing with TestMu AI.

How Long a Selenium to Playwright Migration Takes

Published per-test estimates circulate widely and almost none of them state a methodology, so treat any number you read, including this one, as a shape rather than a forecast. What actually drives effort is not the test count but how much shared machinery sits underneath.

The first ten specs take disproportionately long, because that is when the config, fixtures, auth setup and page objects get built. After that the per-spec cost drops sharply, and the long tail is whatever depended on custom infrastructure. What reliably pushes a migration past its estimate is a high Thread.sleep count, heavy @FindBy and driver-factory usage, or the discovery that part of the suite is Appium and was never in scope.

Run the triage greps from earlier in this guide before you commit to a date. They tell you which of those three you are dealing with in about ten minutes, which is more useful than any per-test average.

Wrapping Up

Start with the triage greps, convert one spec end to end before converting many, and expect strict mode to tell you uncomfortable things about selectors you trusted. Keep both suites running until the new one has earned your confidence, and remember that the one rule that prevents a flakier suite is to assert on the locator rather than on a value you already read.

If you are still deciding whether to move at all, read the comparison first and let your own flakiness and maintenance numbers make the argument. If you have decided, the Playwright tutorial is the fastest way to get the rest of the team fluent in the locator and fixture model that replaces WebDriver.

Author

...

Prince Dewani

Blogs: 31

  • Linkedin

Prince Dewani is a Community Contributor at TestMu AI specializing in AI agents, software testing, QA, and SEO. He is certified in Selenium, Cypress, Playwright, Appium, Automation Testing, and KaneAI, and presented academic research on AI agents at PBCON-01. At TestMu AI, he has also carried out extensive cross-browser research on the support of modern web technologies such as WebGPU, WebAssembly, WebXR, WebGL2 and other web technologies, validating their compatibility and feature parity across major browsers and rendering engines through rigorous hands-on testing. Prince has hands-on experience building AI agent workflows using Anthropic Claude, Google Antigravity, n8n, LangChain, and other agentic frameworks, and works regularly with MCP and A2A protocols. He shares his work with 5,500+ QA engineers, developers, DevOps experts, tech leaders, and AI agent practitioners on LinkedIn.

Reviewer

...

Himanshu Sheth

Reviewer

  • Linkedin

Himanshu Sheth is the Director of Marketing (Technical Content) at TestMu AI, with over 8 years of hands-on experience in Selenium, Cypress, and other test automation frameworks. He has authored more than 130 technical blogs for TestMu AI, covering software testing, automation strategy, and CI/CD. At TestMu AI, he leads the technical content efforts across blogs, YouTube, and social media, while closely collaborating with contributors to enhance content quality and product feedback loops. He has done his graduation with a B.E. in Computer Engineering from Mumbai University. Before TestMu AI, Himanshu led engineering teams in embedded software domains at companies like Samsung Research, Motorola, and NXP Semiconductors. He is a core member of DZone and has been a speaker at several unconferences focused on technical writing and software quality.

Add to Google preferred sources

Summarise with AI

Copied to Clipboard!
...

3000+ Browsers. One Platform.

See exactly how your site performs everywhere.

Try it free
...

Write Tests in Plain English with KaneAI

Create, debug, and evolve tests using natural language.

Try for free

Selenium to Playwright Migration FAQs

Did you find this page helpful?

More Related Blogs

TestMu AI forEnterprise

Get access to solutions built on Enterprise
grade security, privacy, & compliance

  • Advanced access controls
  • Advanced data retention rules
  • Advanced Local Testing
  • Premium Support options
  • Early access to beta features
  • Private Slack Channel
  • Unlimited Manual Accessibility DevTools Tests