June 23, 2026
Selenium to Playwright Migration Guide
A practical Selenium to Playwright migration guide covering locators, waits, fixtures, assertions, CI updates, and when Endtest may be a simpler alternative.
If you have a mature Selenium suite, a Selenium to Playwright migration usually looks simple on paper and painful in practice. The APIs are different, the timing model is different, the locator philosophy is different, and the test runner story changes too. That is exactly why migrations stall halfway through: teams underestimate how much of their test suite is really about synchronization, element targeting, and framework plumbing rather than just browser clicks.
I have migrated enough UI automation to know that the code rewrite is only one part of the work. The real question is whether you are moving to Playwright because you want better reliability, better developer ergonomics, easier parallelism, or a cleaner path for future test maintenance. If that is your goal, the migration can be worth it. If your main goal is just to stop paying Selenium maintenance tax, there are also alternatives worth evaluating, including Endtest, which uses an agentic AI workflow to import and run existing tests with less rebuild effort.
This guide focuses on the technical details that matter during a Selenium Playwright migration, locators, waits, fixtures, CI changes, and the places where teams typically get surprised.
Why teams migrate from Selenium to Playwright
Selenium is still widely used and well documented, and it remains a valid choice for many organizations, especially when they need broad language support or have existing grid investments. The official Selenium docs are a good reference for the current state of the framework, especially if you need to review how WebDriver works under the hood (Selenium documentation).
Playwright changes the testing model in a few important ways:
- It gives you stronger auto-waiting behavior.
- It has richer locator primitives.
- It usually produces less test code for the same scenario.
- It has a strong test runner story, especially in TypeScript.
- It handles browser contexts and parallel isolation cleanly.
For many teams, the migration decision is less about raw browser coverage and more about reducing the amount of framework code they have to babysit.
If your Selenium suite is mostly failing because of timing issues, brittle selectors, and hard-to-debug setup code, Playwright often gives you a more opinionated and more forgiving testing model.
Before you migrate, classify your existing Selenium suite
Do not treat every Selenium test the same way. A migration is easier when you split the suite into categories:
- Smoke tests, login, checkout, critical flows.
- Regression tests, medium-size workflows with meaningful assertions.
- Legacy edge-case tests, long, brittle, or heavily utility-driven tests.
- Platform-specific tests, embedded iframes, downloads, file uploads, multi-tab flows.
- Tests with business value but poor implementation quality, these are often the best candidates for rewrite.
This classification matters because you may not want to port everything at once. A direct Selenium Playwright migration is usually more successful when you start with the highest-value flows and prove the new framework in CI before moving the long tail.
A practical rule I use is this:
- migrate critical smoke paths first,
- keep Selenium alive for lower-value legacy coverage,
- only rewrite fragile flows once you understand the new failure modes.
Map the biggest conceptual differences first
The syntax differences are obvious. The behavioral differences are where migrations go wrong.
1. Driver-centric versus context-centric design
In Selenium, many teams build around a shared WebDriver instance plus helper utilities. In Playwright, the mental model is usually browser, context, page. Each test can get its own isolated browser context, which simplifies state management and parallel execution.
2. Explicit waits versus built-in auto-waiting
Selenium often teaches you to think in terms of WebDriverWait, expected conditions, and manual retry logic. Playwright already waits for many actions to be ready, which means you often remove more wait code than you add.
3. Element interaction model
Playwright encourages locators that stay active and re-resolve elements at action time. That is a very different style from storing raw element references too early.
4. Assertion style
Playwright’s test runner expects assertions that understand async timing and DOM state. Your old assertion utilities may no longer be the right fit.
Selenium locators to Playwright locators
Most Selenium to Playwright migration work starts with selectors.
Selenium code often leans on findElement, XPath, CSS selectors, and custom helper methods.
# Selenium Python
from selenium.webdriver.common.by import By
button = driver.find_element(By.CSS_SELECTOR, “button.primary”) button.click()
Playwright in TypeScript is more concise and encourages locators as first-class objects.
typescript // Playwright
await page.locator('button.primary').click();
That looks similar, but the behavior is not identical. In Playwright, the locator remains live and can be evaluated against the page at action time. That helps when the DOM changes between test steps.
Prefer semantic locators where possible
One of the best migration habits is to reduce selector brittleness at the same time you change frameworks. Instead of turning every Selenium XPath into a Playwright XPath, prefer locators by role, label, text, or test id.
typescript
await page.getByRole('button', { name: 'Sign in' }).click();
await page.getByLabel('Email').fill('qa@example.com');
await page.getByTestId('checkout-submit').click();
This is often the fastest way to lower flake rates during migration. If your application does not expose stable accessibility semantics yet, the migration may be a good forcing function to improve them.
Be careful with brittle XPath carryover
A common mistake is to lift XPath from Selenium into Playwright unchanged.
That works mechanically, but it does not improve maintainability.
If your XPath is deeply coupled to layout structure, you are just carrying the same fragility into a different runtime. If you need XPath for a tricky DOM, use it surgically, not as the default translation layer.
Waits: the area where most migrations either succeed or fail
Selenium teams often accumulate wait helpers like this:
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.ID, “results”)) )
In Playwright, much of this disappears because the framework already waits for actions and assertions. A better translation is usually to let the locator and assertion carry the synchronization.
typescript
await page.getByRole('heading', { name: 'Results' }).waitFor();
await expect(page.getByText('1 item found')).toBeVisible();
Remove sleep-based synchronization
If your Selenium suite has explicit sleeps, they are usually the first thing to remove.
# Avoid this
import time
time.sleep(3)
In Playwright, use state-based waiting instead of time-based waiting. Wait for the network response, the element, or the expected UI state. That gives you faster and more stable tests.
Watch for hidden timing assumptions
Some Selenium tests accidentally depend on implicit ordering, for example:
- a click triggers a request, then a toast appears,
- a route change happens after a spinner disappears,
- a modal closes and focus shifts,
- a button becomes disabled during submission.
Playwright is better at expressing those states, but it still requires you to assert the actual user-visible outcome. If you migrate a test and it suddenly becomes flaky, that is usually a sign the original test was under-specified rather than a Playwright bug.
Fixtures and test structure in Playwright
Selenium suites often build their own setup and teardown patterns, then bury them in a base class or custom wrapper. That can work, but it usually becomes awkward as the suite grows.
Playwright fixtures are one of the biggest reasons teams like the framework. They let you define reusable setup logic while keeping tests isolated.
import { test, expect } from '@playwright/test';
test('user can log in', async ({ page }) => {
await page.goto('https://example.com/login');
await page.getByLabel('Email').fill('qa@example.com');
await page.getByLabel('Password').fill('secret');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByText('Dashboard')).toBeVisible();
});
Use fixtures for authentication and test data
If your Selenium suite has login helpers in every test, move that logic into Playwright fixtures or storage state setup. This keeps tests shorter and makes auth reuse explicit.
A common migration pattern is:
- create one setup test that logs in,
- save storage state,
- reuse that state in multiple tests.
This is often cleaner than logging in through the UI in every single test, especially for regression suites.
Avoid over-abstracting too early
A lot of Selenium frameworks become difficult to migrate because they hide too much behind custom wrappers. In Playwright, keep your first pass simple. Use page objects only where they reduce duplication or isolate a genuinely complex screen.
If you immediately rebuild a deep abstraction stack, you may recreate the same maintenance burden in a different framework.
Translating common Selenium patterns
Clicking and typing
# Selenium
email = driver.find_element(By.ID, "email")
email.clear()
email.send_keys("qa@example.com")
typescript // Playwright
await page.getByLabel('Email').fill('qa@example.com');
Waiting for navigation
Selenium often requires explicit navigation waits.
WebDriverWait(driver, 10).until(EC.url_contains('/dashboard'))
Playwright can usually express this more directly.
typescript
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL(/\/dashboard/);
Checking element state
python
Selenium
assert button.is_enabled()
typescript // Playwright
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled();
Handling dialogs or popups
Playwright gives you good primitives for browser contexts, tabs, and popup interactions. When migrating Selenium tests that switch windows, rewrite those flows deliberately instead of trying to port every window-handle helper as-is.
CI/CD changes you should plan for
A framework migration is not finished when the test code compiles. It is finished when the suite runs reliably in CI.
Playwright changes your pipeline in a few ways:
- browser installation may be part of the build step,
- parallelization becomes easier, but may expose hidden state coupling,
- screenshots, traces, and videos become useful debugging artifacts,
- test runtime can change enough to affect job timeouts and resource sizing.
The Playwright docs are worth reading early, especially for runner behavior and configuration conventions (Playwright intro).
Example GitHub Actions setup
name: playwright-tests
on: push: pull_request:
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npx playwright install –with-deps - run: npx playwright test
Watch parallel execution carefully
Playwright makes parallel execution feel easy, but your app data and test setup still need to support it. If two tests reuse the same account, same tenant, or same email address, you can create new failures that were hidden in your old sequential Selenium run.
That is not a Playwright problem. It is a test isolation problem that the new framework simply reveals faster.
Migration strategy that actually works
I recommend a phased approach.
Phase 1, prove parity on a small flow
Pick one stable, business-critical test. Migrate it end to end. Make sure it passes locally and in CI. Use that test to learn the new framework conventions before you rewrite the whole suite.
Phase 2, create a mapping guide
Document how you will translate:
- Selenium finders to Playwright locators,
- waits to assertions,
- shared setup to fixtures,
- window handling to context or page handling,
- screenshots and logs to Playwright trace artifacts.
This prevents every engineer from inventing their own migration style.
Phase 3, migrate by value, not by file order
Do not migrate the suite alphabetically. Migrate the tests most likely to reduce pain:
- flaky tests,
- critical smoke coverage,
- tests that need better debugging,
- tests blocked by old framework patterns.
Phase 4, deprecate Selenium utilities gradually
As you migrate, remove stale helpers only after nothing depends on them. Otherwise, you end up supporting two frameworks and two sets of abstractions longer than necessary.
When Playwright is the right destination, and when it is not
Playwright is a strong choice when you want modern browser automation with less hand-rolled synchronization and better test ergonomics. It is especially attractive if your team is already comfortable in TypeScript or wants to standardize on one language for frontend and testing work.
But it is not automatically the best answer for every team.
You may want to pause and reconsider if:
- your team needs a low-code migration path,
- you have a large Selenium suite and little appetite for rewrite work,
- you want to reduce maintenance without retraining everyone on a new code-first framework,
- your existing tests are valuable but not worth rewriting line by line.
That is where a platform like Endtest vs Selenium becomes relevant. Endtest is positioned as an agentic AI Test automation platform with low-code and no-code workflows, so teams can bring Selenium assets forward instead of rebuilding everything manually. Its AI Test Import can ingest existing Selenium, Playwright, or Cypress files and convert them into editable platform-native tests, which is useful when the migration bottleneck is human rewrite time, not browser execution.
If you are trying to escape Selenium maintenance without taking on a full rewrite to Playwright, that is a materially different problem. It is worth evaluating separately.
What Endtest changes in a migration conversation
A lot of teams say they want to migrate off Selenium, but what they really want is fewer flakes, less code to maintain, and a path that does not stall six months in.
Endtest can be a simpler alternative for that scenario because it focuses on imported, editable tests and self-healing behavior rather than asking you to rebuild all automation as source code. Its self-healing tests help when locators change, which is one of the main reasons Selenium suites become expensive to maintain.
The practical difference is this:
- Selenium to Playwright migration, you are still doing a code migration, but to a more modern framework.
- Selenium to Endtest migration, you are moving toward a platform that can import existing tests and reduce rewrite burden.
For some organizations, especially QA teams supporting multiple products or CTOs trying to reduce automation overhead, that tradeoff is better than paying for a full rewrite.
If your highest priority is not learning a new code framework, but preserving existing test investment while lowering maintenance, a migration platform can be more pragmatic than a framework swap.
Common mistakes during Selenium Playwright migration
Mistake 1, copying Selenium abstractions too literally
If your Selenium framework has a giant base class, do not recreate it unchanged in Playwright. Start simpler.
Mistake 2, porting brittle selectors unchanged
If the selector was flaky in Selenium, it will usually still be flaky in Playwright.
Mistake 3, keeping manual sleep calls
You are leaving one of the biggest Playwright advantages on the table.
Mistake 4, ignoring CI resource differences
A suite that passes locally may fail in CI if browser concurrency, container permissions, or test data isolation are not tuned.
Mistake 5, rewriting too much before proving value
Migration momentum dies when teams spend weeks on framework plumbing before getting a single stable test into CI.
A practical decision checklist
Choose a Selenium to Playwright migration if:
- you want a modern code-first browser automation stack,
- your team can adopt TypeScript or is already using it,
- you are willing to rewrite selectors, waits, and some test architecture,
- you want better debugging artifacts and cleaner parallelization.
Consider Endtest if:
- you want to move away from Selenium without a full manual rewrite,
- you want agentic AI to help import existing tests into editable steps,
- you need a lower-maintenance path for a large legacy suite,
- you prefer platform-managed test execution and self-healing locators.
Final take
A Selenium Playwright migration is absolutely doable, and for many teams it is a good engineering investment. Playwright gives you a more modern model for locators, waits, and isolation, which often translates into fewer flakes and less framework code. But the migration is not just a syntax conversion. It is a chance to simplify the way your team thinks about synchronization, test data, and CI reliability.
If you approach the migration incrementally, prioritize stable selectors, remove sleep-based waits, and redesign your fixtures with isolation in mind, the move is much less painful.
If that still sounds like too much rewrite work, especially for a large Selenium estate, then it is worth comparing the code-first route with a platform-based alternative. That comparison is often the difference between a migration plan that gets approved and one that quietly dies in backlog.
For a broader perspective, I also recommend reading the official Selenium documentation and the Playwright docs, then deciding whether your real problem is framework choice or migration cost.