Many Selenium-to-Playwright guides start from an outdated picture of Selenium. They describe driver binaries you download by hand and the JSON Wire Protocol, with no way to listen to browser events. Selenium 4 changed all of that. It speaks the W3C WebDriver standard, manages drivers and browsers on its own with Selenium Manager, and adds bidirectional, event-driven features through WebDriver BiDi.
Both frameworks are mature, actively maintained, and able to test modern web apps well.
Migrating from Selenium to Playwright is a choice about fit, not a fix for a broken tool. The question to settle first is which tool suits your team, your languages, the browsers you need to cover, and the tooling your developers already use. This guide helps you answer it, then walks through the conversion step by step if Playwright is the right fit. Start with how the two frameworks differ.
Why migrate from Selenium to Playwright?
The differences between the two frameworks come mostly from design choices and developer experience, and the capabilities overlap more than many comparisons suggest.
Selenium implements the W3C WebDriver standard. Your test code sends commands to a driver (chromedriver, geckodriver, safaridriver, msedgedriver), and the driver controls the browser. The browser vendors build and ship these drivers themselves, and Selenium Manager (included since Selenium 4.6) downloads the matching driver, and the browser if needed, so version mismatches are rare today. Playwright has a driver layer of its own.
Its Python, Java, and .NET clients talk to a bundled Node.js process, which controls Chromium through the Chrome DevTools Protocol and controls Firefox and WebKit through patched browser builds that Playwright ships. Playwright keeps one open connection for the whole session, which lowers per-command overhead. WebDriver BiDi brings the same kind of persistent connection to Selenium.
Then there is waiting. Selenium asks you to say what you are waiting for, using explicit waits such as WebDriverWait with ExpectedConditions. Explicit waits are a deliberate design choice: The test states its expectations. When teams scatter waits inconsistently, or use Thread.sleep (which Selenium's own docs advise against), flakiness follows. Playwright makes a different choice: it auto-waits for elements to be visible, stable, enabled, and able to receive input before acting, and its assertions retry until they pass or time out. Less code, but the waiting is implicit, so you need to know what the framework checks for you.
Playwright Test, the runner for JavaScript and TypeScript, bundles parallel execution, retries, fixtures, sharding, and web-first assertions in one package. The bundled runner is a real strength for JavaScript and TypeScript teams. Playwright for Java, Python, and .NET uses the same runners Selenium teams already use: JUnit or TestNG, pytest, NUnit or MSTest. A Java team that moves to Playwright for Java keeps the same runner and the same parallelization strategy it has today while gaining Playwright’s fixtures and trace viewer.
Debugging is where Playwright has a clear edge. Its trace viewer records actions, DOM snapshots, network calls, and console logs in one file, in every language. Selenium has no built-in equivalent. Selenium teams usually get video, screenshots, logs, and network captures from their grid or cloud provider. Selenium Grid 4 also offers live session viewing and video recording through its Docker images.
Two more points that comparisons often get wrong:
- Playwright creates isolated browser contexts inside one browser process, which makes starting a clean session very fast. Selenium usually starts a new browser session per test, which is slower but gives full isolation. WebDriver BiDi adds user contexts, which give Selenium a similar lightweight isolation model as browser and language-binding support matures.
- Network interception, console log capture, and mobile device emulation are available in Selenium 4 through CDP in Chromium browsers and, increasingly, through WebDriver BiDi across browsers. Playwright's APIs for these are more polished. API testing is a real difference: Playwright includes an HTTP client, while Selenium leaves that to libraries like RestAssured or requests.
None of that means every team should migrate. The decision is a separate question worth answering honestly first.
Should you migrate at all?
Migration makes sense for some teams, but it carries real cost and fixes only the problems the framework causes.
Look at the cause of your pain before you pick a fix. Flaky tests caused by Thread.sleep, mixed implicit and explicit waits, brittle XPath, shared test data, or test order dependencies will follow you to Playwright. Many of these can be fixed inside Selenium in less time than a migration takes. Migrate if your team wants Playwright's integrated JavaScript or TypeScript tooling, the trace viewer, its fixtures, or its developer workflow, and you have checked that the gain is worth the cost.
Stay on Selenium if your framework is stable, if you depend on Selenium Grid or a cloud setup that works well, or if you need coverage Playwright does not offer. Playwright does not drive real Safari. Its WebKit build is close to Safari's engine, but it is not the browser your users run. Selenium drives real Safari through safaridriver, plus branded Chrome, Edge, and Firefox, and legacy Internet Explorer through IE mode in Edge. Your Selenium skills also carry over to Appium, which uses the same WebDriver protocol for native and mobile web tests on real devices. If any of this matters, keep Selenium for it, or run both frameworks side by side.
Language fit matters too. Playwright's best experience is in TypeScript and JavaScript. Python, Java, and .NET are well supported, but some features, such as Playwright Test, UI mode, and built-in sharding, are only in the JavaScript version. Selenium officially supports Java, Python, C#, Ruby, and JavaScript. Ruby teams have no official Playwright option.
If what draws you to Playwright is an all-in-one framework, but you want to keep Selenium’s browser and device reach, consider WebdriverIO. WebdriverIO is a JavaScript and TypeScript framework with its own test runner, automatic waiting, parallel execution, and built-in reporting. Because it runs on the WebDriver and WebDriver BiDi standards, it drives real Safari and tests native mobile apps through Appium. For many teams, it gives the Playwright-style developer experience without giving up Selenium's reach.
Estimate the ROI in concrete terms. Count the engineer-hours per sprint spent on test maintenance and find out how much of it comes from the framework and how much from test design. Then weigh the framework share against the cost of a phased migration. If most of the cost comes from test design, cleaning up the Selenium suite is the cheaper fix. If your team wants to standardize on JavaScript or TypeScript tooling anyway, migration can pay off.
Here is a side-by-side look for anyone still weighing the two.
Selenium vs. Playwright: key differences
The table below compares the two frameworks on the points that most affect daily test work.
Once the decision is made, preparation decides whether the migration goes smoothly or drags.
How to prepare your migration
Preparation sets the timeline. Skip it and the migration stalls halfway, with two suites running and neither fully trusted.
Start by auditing the existing Selenium tests. Count the tests and rank them by business value, flagging the flaky and redundant ones along the way. Migration is the best pruning opportunity most teams ever get. A 400-test suite that is really 280 useful tests and 120 duplicates or dead paths should migrate 280, not 400. For each flaky test, note why it is flaky. If the cause is test design, fix the design during conversion, or the flakiness moves with it.
Pick critical user flows and smoke tests as your pilot set. They give you fast, visible proof that the new framework works before the team commits to the long tail.
Choose the Playwright language that matches your team and your current suite. If your Selenium tests are in Java, Playwright for Java is the shortest path, and you keep JUnit or TestNG. If you want Playwright Test's runner, fixtures, and sharding, you need JavaScript or TypeScript, which means a language change as well as a framework change. Plan for both.
Install Playwright and its browsers, and pin versions in your project manifest. For JavaScript or TypeScript, run npm init playwright@latest. For Java, add the com.microsoft.playwright:playwright dependency, then install browsers with mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install". Each Playwright release is tied to specific browser builds, so upgrade Playwright and its browsers together.
Decide on a strategy. Incremental migration with both suites running in parallel is the low-risk default. Big-bang rewrites only fit small suites where the full conversion takes a sprint or less.
Set up the target project structure early. A three-layer split of page objects, reusable modules, and test specs keeps converted code clean and stops old habits from moving over with the syntax.
With that structure in place, the conversion work can start.
How to convert Selenium tests to Playwright
Converting Selenium tests to Playwright follows a few common patterns. Each subsection shows one pattern with side-by-side Java and JavaScript code for both frameworks, so the comparison separates framework differences from language differences.
Map WebDriver calls to Playwright APIs
Selenium finds elements with findElement and acts on them with methods like click and sendKeys. Playwright uses page.locator, plus user-facing locators like getByRole and getByLabel. Selenium has no built-in role or label locators, so Selenium tests usually use IDs, CSS selectors, or data-testid attributes.
Selenium (Java):
driver.findElement(By.id("submit-btn")).click();
driver.findElement(By.cssSelector(".username")).sendKeys("testuser");
Selenium (JavaScript):
await driver.findElement(By.id('submit-btn')).click();
await driver.findElement(By.css('.username')).sendKeys('testuser');
Playwright (Java):
page.locator("#submit-btn").click();
page.getByLabel("Username").fill("testuser");
Playwright (JavaScript):
await page.locator('#submit-btn').click();
await page.getByLabel('Username').fill('testuser');
Replace driver setup and teardown with your runner's lifecycle hooks or Playwright fixtures. In JavaScript and TypeScript, Playwright Test handles browser launch and context creation, then tears everything down after each test. In Java, the @UsePlaywright annotation for JUnit 5 does the same.
Where it makes sense, swap brittle CSS selectors and XPath for user-facing locators. getByRole, getByLabel, getByText, and getByTestId survive UI refactors better than selectors tied to class names or DOM structure. A data-testid strategy gives Selenium suites a similar benefit, so this is a good habit no matter which framework you use.
Replace explicit waits with auto-waiting
Removing explicit waits is where most of the code reduction comes from. Find every WebDriverWait, implicit wait, fluent wait, and Thread.sleep in the suite. Most WebDriverWait calls map to Playwright's built-in actionability checks or to a retrying assertion. Every Thread.sleep should go, in either framework.
Selenium (Java):
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.elementToBeClickable(By.id("status"))).click();
Selenium (JavaScript):
const status = await driver.wait(until.elementLocated(By.id('status')), 10000);
await driver.wait(until.elementIsVisible(status), 10000);
await status.click();
Playwright (Java):
page.locator("#status").click();
Playwright (JavaScript):
await page.locator('#status').click();
Playwright waits for the element to be visible, stable, enabled, and able to receive input before it clicks. In Selenium, the wait logic is usually written once in a base class or helper, so the per-call cost in a real suite is smaller than it looks here.
For assertions, use Playwright's web-first assertions, which retry until the condition is met. Selenium can do the same with a wait condition.
Selenium (Java):
wait.until(ExpectedConditions.textToBe(By.id("confirmation"), "Order placed"));
Selenium (JavaScript):
const confirmation = await driver.wait(until.elementLocated(By.id('confirmation')), 10000);
await driver.wait(until.elementTextIs(confirmation, 'Order placed'), 10000);
Playwright (Java):
assertThat(page.locator("#confirmation")).hasText("Order placed");
Playwright (JavaScript):
await expect(page.locator('#confirmation')).toHaveText('Order placed');
One rule: Do not port Thread.sleep calls as page.waitForTimeout. Porting them brings the same timing flakiness into Playwright. If auto-waiting does not cover a case, wait for a specific condition, such as a network response or an element state. Fixed delays are the problem in any framework.
Rebuild page objects, don't transplant them
Keep the page object model, but slim it down. Auto-waiting removes most of the wait helpers that Selenium suites keep in their base page classes.
Selenium page object method (Java):
public void login(String user, String pass) {
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("username"))).sendKeys(user);
driver.findElement(By.id("password")).sendKeys(pass);
driver.findElement(By.id("login-btn")).click();
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("dashboard")));
}
Selenium page object method (JavaScript):
async login(user, pass) {
const username = await this.driver.wait(until.elementLocated(By.id('username')), 10000);
await this.driver.wait(until.elementIsVisible(username), 10000);
await username.sendKeys(user);
await this.driver.findElement(By.id('password')).sendKeys(pass);
await this.driver.findElement(By.id('login-btn')).click();
await this.driver.wait(until.elementLocated(By.id('dashboard')), 10000);
}
Playwright equivalent (Java):
public void login(String user, String pass) {
page.getByLabel("Username").fill(user);
page.getByLabel("Password").fill(pass);
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Log in")).click();
assertThat(page.locator("#dashboard")).isVisible();
}
Playwright equivalent (JavaScript):
async login(user, pass) {
await this.page.getByLabel('Username').fill(user);
await this.page.getByLabel('Password').fill(pass);
await this.page.getByRole('button', { name: 'Log in' }).click();
await expect(this.page.locator('#dashboard')).toBeVisible();
}
The length is about the same. The difference is where the waiting lives: in your code with Selenium, in the framework with Playwright.
Convert API setup and teardown
Selenium is a browser automation tool, so Selenium suites use a separate HTTP client (RestAssured in Java, requests in Python, fetch in JavaScript, HttpClient in .NET) for API-level preconditions such as test data setup and authentication. The approach works well, and many teams keep these libraries after migrating.
Playwright also offers APIRequestContext, which runs API calls inside the framework.
Selenium approach (separate library, Java):
Response response = given()
.contentType("application/json")
.body("{\"username\":\"admin\",\"password\":\"secret\"}")
.post("https://app.example.com/api/auth/login");
String token = response.jsonPath().getString("token");
// Pass the token to the browser, for example as a cookie
Selenium approach (separate client, JavaScript):
const response = await fetch('https://app.example.com/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: 'secret' }),
});
const { token } = await response.json();
// Pass the token to the browser, for example as a cookie
Playwright equivalent (Java):
APIRequestContext api = playwright.request().newContext(
new APIRequest.NewContextOptions().setBaseURL("https://app.example.com"));
APIResponse response = api.post("/api/auth/login",
RequestOptions.create().setData(Map.of("username", "admin", "password", "secret")));
String token = JsonParser.parseString(response.text()).getAsJsonObject().get("token").getAsString();
// Use the token in the browser context, for example with storageState
Playwright equivalent (JavaScript):
const api = await request.newContext({ baseURL: 'https://app.example.com' });
const response = await api.post('/api/auth/login', {
data: { username: 'admin', password: 'secret' },
});
const { token } = await response.json();
// Use the token in the browser context, for example with storageState
The gain goes beyond fewer lines. API calls appear in the same trace and the same report as the browser actions, so a failure during data setup shows up in one place.
Use AI tools to accelerate conversion
Coding agents and AI assistants handle file-level syntax conversion well. The mechanical parts of a migration (rewriting findElement calls, removing wait blocks, swapping assertions, and updating imports) are the kind of repetitive, pattern-based work that AI tools do fastest. Give the tool one test file plus your target conventions (locator policy, fixture patterns, assertion style, and naming rules) and review the output. For a 300-file suite, this can turn weeks of mechanical work into days of review.
A workflow that works: Convert one test file by hand first and use it as the reference. Then give the AI tool each next file together with the reference and your rules. Output is much more consistent when the model has a concrete target instead of a general instruction.
Two caveats:
- First, run a linter on generated code. AI output reads well but is inconsistent across files. If your rule is
getByRoleoverlocator('#id'), a linter catches the drift that review might miss. - Second, manual review is still required. A converted test that passes on one path may miss behavior the original checked through one of its explicit waits. An explicit wait is also an assertion about the page, and removing it can remove a check. Generation is not verification.
For that reason, validation gets its own step.
How to validate migrated tests
Validation is what separates a migration from a rewrite you just hope works.
Run each converted Playwright test at least 20 to 30 times before it replaces its Selenium version. A test that passes once only proves it runs. A test that passes 30 times in a row is one you can trust.
Compare execution time and flake rate per test against the Selenium baseline, and keep the numbers. They show whether the migration is paying off, both to the engineers doing the work and to the people funding it. If a Selenium test was flaky and the Playwright version is not, check whether the fix came from the framework or from cleaning up the test.
Check your browser coverage carefully. Playwright runs Chromium, Firefox, and WebKit, but its Firefox and WebKit are patched builds, and WebKit is not Safari. If your Selenium suite tested real Safari or specific branded browser versions, decide how you will keep that coverage. Some teams keep a small Selenium suite for real Safari.
Scale up with your runner's parallel settings (workers and sharding in Playwright Test) and isolate parallel runs with browser contexts. If a test that passed in serial fails in parallel, it has a state dependency that the migration just exposed.
Run the full suite in your CI pipeline on pull requests before removing any Selenium tests. The goal is a Playwright suite that runs reliably enough that removing the Selenium version is a formality, not a leap of faith.
Track the numbers. A spreadsheet with test name, Selenium flake rate, Playwright flake rate, Selenium execution time, and Playwright execution time tells the migration's story better than any status meeting.
Teams running both suites during migration can run Selenium and Playwright tests on the same Sauce Labs infrastructure. Sharing infrastructure keeps the comparison fair and avoids standing up new capacity mid-migration. Both frameworks get the same cloud, same reporting, same debug artifacts, and the same parallel capacity.
Even with solid validation, a few problems show up in almost every migration of this size.
Common migration challenges
Dual maintenance. Budget for the time when both suites run. Time-box it per test group (two weeks per tier, for example) so the parallel period does not become permanent. Dual maintenance that drags past a quarter can cost more than the problems you set out to fix.
Browser coverage gaps. Real Safari, specific branded browser versions, and legacy Internet Explorer coverage stay on Selenium. Write down the split so the team knows which tests live where and why.
Selector debt. Brittle XPath should usually not be ported as is. In many cases, "converting" a test means rewriting its locators to use getByRole or getByTestId. Locator work takes more than a syntax swap, so plan for it in your estimates.
Team skills. Fixtures, locators, web-first assertions, and the trace viewer work differently from the patterns Selenium developers know, so schedule training early. Playwright reads differently even in the same language. If the migration also means a move from Java or Python to JavaScript or TypeScript, budget for that learning curve too.
Parallel CI cost. Running two suites on every pull request raises CI spend for a while. Remove the most expensive Selenium suites first to get capacity back as you go.
Planning for these challenges up front is what a realistic timeline looks like.
A phased migration plan
For a 400-test Selenium suite, this sequence fits in about one quarter.
Weeks 1 to 2: Audit and prune. Inventory the Selenium suite, rank tests by business value, and cut the dead and redundant ones. If you start with 400 and prune to 300, you just saved 25% of the migration effort before writing a line of Playwright.
Weeks 2 to 3: Stand up the framework. Create the Playwright project structure, configure fixtures, connect the CI job, and set up reporting. Run a handful of simple tests to confirm the pipeline works end to end.
Weeks 3 to 5: Convert the pilot set. Migrate smoke tests and critical user flows first, and validate each test with repeated runs. The pilot is your proof of concept. If the pilot set is stable and the team works faster with it, you have evidence to continue. If not, you have learned that cheaply.
Weeks 5 to 10: Convert by priority tier. Work through the remaining tests in business-value order, running both suites in parallel and comparing results. Remove Selenium tests tier by tier as their Playwright versions prove stable.
Week 6 onward: Gate new development. Write all new tests in Playwright from a fixed date, so the Selenium backlog does not grow while you work it down. Tests that need real Safari or other Selenium-only coverage stay in Selenium.
Weeks 10 to 12: Decommission. Remove the remaining Selenium tests once their replacements have run clean through several release cycles. Keep the Selenium tests that cover what Playwright cannot.
Adjust the timeline to your suite size and team capacity. The order matters more than the dates. Whatever the timeline, the infrastructure underneath should not be one more thing to manage.
How Sauce Labs supports the migration
The test code has to change because Selenium and Playwright do not share an API. The platform underneath does not have to change. Sauce Labs runs both frameworks on the same infrastructure, so converting code does not mean rebuilding your test setup from scratch.
Sauce Labs’ co-founder created Selenium, and the platform runs Selenium and Playwright (as well as Appium, Cypress, and other test frameworks), so the migration stays a code change while the infrastructure stays put. Sauce Labs' AURA platform is framework-agnostic, so moving between two testing frameworks does not have to mean moving between two platforms. No new grid to provision, no second cloud to configure, and no vendor switch in the middle of a framework transition.
During the transition, run both frameworks on the same cloud. Keep real Safari and other branded browser coverage with Selenium, use Playwright where it fits, with Appium covering real mobile devices. Between them, the platform offers 10,000+ real devices and 3,000+ browser and OS combinations.
Parallel capacity handles the dual-suite period without slowing pull request feedback. The comparison period is where migrations succeed or stall. If running two suites on every pull request doubles your feedback loop, teams cut corners. Enough cloud capacity keeps that loop fast.
Because both suites run on the same infrastructure, test analytics can compare flake rates and execution times for old and new tests on the same dashboard. That dashboard becomes the migration's scoreboard, showing where Playwright helps and where your Selenium tests were already doing fine.
Every run, Selenium or Playwright, produces video, screenshots, logs, and network captures for debugging. When a migrated test behaves differently from its Selenium version, the artifacts show you where the difference started.
Migrating carefully, step by step, is the whole point.
Make the switch without losing coverage
Migrate in steps, holding every converted test to a stability bar and measuring execution time and flake rate before and after. Keep Selenium where it covers what Playwright cannot. The numbers tell you whether the migration is working and give you the evidence to finish it, or to stop where you are.
Try Sauce Labs for free or book a demo to run Selenium and Playwright side by side during your migration.






