How to Migrate from Selenium to Playwright
Move a Selenium suite to Playwright in stages: choose the right runner, port a representative test, then validate selectors, waits, isolation, browser coverage, and CI.
Short answer: migrate in stages. Inventory your Selenium suite, choose the Playwright language API and test runner that fit your team, port one representative test, validate its behavior, then expand feature by feature and move the validated suite into CI. This is more than replacing method names: Playwright Test, for example, uses async tests, explicit imports, and fixtures such as page.
The exact code and lifecycle mapping depend on your Selenium language and test framework. The runnable examples below use JavaScript with Playwright Test. If your existing suite is Java, Python, or .NET, use the corresponding Playwright language API and runner rather than translating JavaScript fixture examples literally. Playwright’s official migration example covers Protractor, not Selenium, so the mapping here is a practical adaptation of documented Playwright behavior.
1. Inventory the Selenium suite before editing
Record what the suite does today. This makes hidden assumptions visible before a new runner changes execution order, browser lifecycle, or test isolation.
- Language and runner: Selenium binding and version, test framework, assertion library, hooks, and build commands.
- Browser setup: driver creation and teardown, browser and operating-system matrix, headless or headed mode, Selenium Grid or other remote execution.
- Test structure: base classes, page objects, shared helpers, fixtures, global state, and test ordering assumptions.
- Synchronization: every explicit or implicit wait, sleep, polling helper, and the specific condition it protects.
- Test data and identity: shared accounts, mutable records, authentication state, environment setup, and cleanup.
- Diagnostics: screenshots, logs, video, reports, retries, and how a failed CI run is investigated.
Tag each test by feature area and risk. A useful first migration candidate exercises a common user journey without depending on fragile shared state. Also choose a representative case with the interactions that matter to the suite, such as a form, an authenticated page, a frame, or a popup.
2. Choose the Playwright language API and runner
Playwright Test is the Node.js end-to-end test runner. Playwright also provides language APIs for other ecosystems, but their setup, syntax, and test lifecycle differ. Decide whether the team will adopt the Node.js runner or keep its current language and use that language’s Playwright API and test framework. Confirm current language support and runner guidance in the Playwright installation documentation.
The rest of this walkthrough uses Playwright Test. It assumes Node.js is installed and the project uses npm.
npm init -y
npm install --save-dev @playwright/test
npx playwright install
Create tests/login.spec.js and add a script to package.json:
{
"scripts": {
"test:e2e": "playwright test"
}
}
Install the browsers required by the project. Playwright browser binaries are tied to the installed Playwright version, so reinstall them when upgrading Playwright. You can install only selected browsers, for example npx playwright install chromium. See the browser installation guide.
3. Port one representative test
Start by moving one test end to end: navigation, user input, the key action, and an assertion that proves the behavior. Here is a minimal Playwright Test example. Replace the example URL, accessible labels, and expected result with values from your application.
import { test, expect } from '@playwright/test';
test('user can sign in', async ({ page }) => {
await page.goto('https://example.com/login');
await page.getByLabel('Email').fill('qa@example.com');
await page.getByLabel('Password').fill('example-password');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('heading', { name: 'Account' })).toBeVisible();
});
Run it with npx playwright test tests/login.spec.js. Run in headed mode with npx playwright test tests/login.spec.js --headed when you need to watch the browser. An assertion should check the user-visible outcome that makes the test meaningful, not merely that the click returned.
Before porting more tests, confirm the test uses the intended environment, handles authentication correctly, and proves the same business behavior as its Selenium counterpart. Keep a record of differences in setup or behavior rather than silently changing what the test covers.
4. Translate Selenium selectors by intent
A Selenium By selector and a Playwright locator may target the same node, but translating the string mechanically can preserve a brittle test. Reconsider what uniquely identifies the control from a user or accessibility perspective.
| Selenium concept | Playwright direction | Migration check |
|---|---|---|
findElement(By.id(...)) |
page.getByRole(...), getByLabel(...), or getByTestId(...) |
Does the locator reflect user intent or an explicit test contract? |
findElement(By.cssSelector(...)) |
page.locator('...'), where CSS is still a stable choice |
Does the selector depend on generated classes or layout structure? |
findElement(By.xpath(...)) |
Prefer role, label, text, or a stable locator; use XPath only when justified | Will a markup rearrangement break a long DOM path? |
| Find a form input | page.getByLabel('Email') |
Is the form control properly labeled? |
| Find a named button or link | page.getByRole('button', { name: 'Save' }) |
Is the accessible name unique in the relevant scope? |
| Find an application-owned stable hook | page.getByTestId('order-row') |
Is the test ID an intentional, maintained contract? |
Playwright recommends user-facing locators such as role, label, text, placeholder, alt text, and title, or explicit test IDs when that is the right contract. Locators resolve against the current DOM when used, which helps when a framework re-renders the page. Locator actions are strict: an action that needs one target fails if multiple elements match. Prefer narrowing the locator to the intended region or adding a meaningful name over reaching for first() or nth() as a quick fix. See Playwright locators.
// Prefer an accessible name and scope repeated controls to their row.
const order = page.getByRole('row').filter({ hasText: 'Order 1042' });
await order.getByRole('button', { name: 'View' }).click();
await expect(order).toHaveCount(1);
5. Replace waits according to what they prove
For each Selenium wait, write down its protected condition. Then decide whether a Playwright action or retrying assertion already expresses that condition. Do not remove a wait simply because Playwright has auto-waiting: an element becoming clickable and an order finishing on a backend are different conditions.
| Existing wait purpose | Playwright approach | Important limit |
|---|---|---|
| Wait until a button can be clicked | Call await locator.click() |
Playwright checks actionability, including uniqueness, visibility, stability, event reception, and enabled state. |
| Wait until text or an element appears | await expect(locator).toBeVisible() or another web-first assertion |
The assertion retries until its condition passes or times out; choose the assertion that matches the required state. |
| Wait for a known URL after navigation | Assert with await expect(page).toHaveURL(...) |
Check the route or destination that demonstrates the intended navigation. |
| Wait for a business process, job, or external event | Wait for an observable application signal, or poll the relevant API or system with a bounded timeout | Actionability does not prove the backend or third-party service has completed its work. |
| Fixed sleep used to cover a race | Replace it with a condition that proves readiness or completion | A sleep can be too short on a slow run and waste time on a fast run. |
Actions wait for their documented actionability checks, and web-first assertions retry until the expected state is true or the assertion times out. These mechanics reduce the need for waits that duplicate readiness checks. They do not establish arbitrary application or external conditions. Read actionability and assertions when mapping complex waits.
// Avoid sleeping for a render. Wait for the state the test needs.
await page.getByRole('button', { name: 'Submit order' }).click();
await expect(page.getByRole('status')).toHaveText('Order submitted');
// For a new tab, wait for the actual popup event.
const popupPromise = page.waitForEvent('popup');
await page.getByRole('link', { name: 'Open receipt' }).click();
const popup = await popupPromise;
await expect(popup).toHaveURL(/receipt/);
Timeouts should be bounded and meaningful. If an assertion repeatedly reaches its timeout, determine whether the condition is wrong, the app is slow, the selector is ambiguous, or the environment cannot reach a dependency. Raising a timeout can be appropriate for a known slow operation, but it does not repair an incorrect synchronization condition.
6. Rebuild browser lifecycle and test isolation
With Playwright Test, the built-in page fixture is a page in a browser context isolated for that test. A browser can be reused for efficiency while tests receive isolated contexts. Fixtures have setup and teardown lifecycles, and page objects remain an option; adapt their methods to async Playwright locators and make their dependencies explicit.
Do not mechanically preserve a global mutable WebDriver or a shared page just because the Selenium suite used one. Map old setup and teardown based on ownership: per-test setup belongs in test-scoped fixtures or hooks, while expensive resources that can safely be shared may be worker-scoped. See fixtures and page object models.
import { test as base, expect } from '@playwright/test';
export const test = base.extend({
signedInPage: async ({ page }, use) => {
await page.goto('https://example.com/login');
await page.getByLabel('Email').fill(process.env.TEST_EMAIL);
await page.getByLabel('Password').fill(process.env.TEST_PASSWORD);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('heading', { name: 'Account' })).toBeVisible();
await use(page);
},
});
Import this custom test in tests that need signedInPage. Store credentials in your CI secret store or local environment; do not commit real credentials. For expensive authentication or shared services, verify the fixture scope and data isolation before reusing state across tests.
7. Handle frames, tabs, downloads, and authentication deliberately
- Frames: use a frame locator, then locate within that frame. Confirm the frame is actually part of the intended flow.
- Tabs and popups: subscribe to the popup event before the action that opens it, as in the example above.
- Downloads: wait for the download event around the click that starts it, then save or inspect the resulting file according to the test’s needs.
- Authentication: decide whether each test logs in independently or uses a deliberately created storage state. Protect stored credentials and state files.
- Dialogs: register a dialog handler when a test must accept or dismiss a browser dialog; otherwise the dialog can block the interaction.
- Uploads: use Playwright’s file input APIs and ensure fixtures provide stable test files.
- Network dependencies: decide whether the migration should use real services, controlled test data, or route interception. Keep mocked behavior distinct from end-to-end coverage of the real service.
These cases deserve a representative migration test if they appear widely in the suite. Check the relevant Playwright API documentation for the exact language binding and current method signatures.
8. Expand by feature area and compare behavior
After the representative test passes locally, migrate related tests in small batches. Within each batch:
- Move setup, actions, and assertions while preserving the Selenium test’s intent.
- Replace selectors based on meaning and uniqueness, not just syntax.
- Map each wait to the condition it proves.
- Port page objects and helpers only where they make the new suite clearer.
- Run the affected tests, inspect failures, and compare the user journey and assertion coverage with the original.
- Remove the old test only after the new test covers the required behavior and the team’s migration criteria are met.
Maintain a migration checklist by feature and note any intentionally changed behavior. That makes it easier to distinguish an expected change from a regression.
9. Validate parallel execution and test data
Playwright Test runs test files in parallel by default; tests within a file run in order by default. Workers are separate operating-system processes and cannot share in-memory state. A suite that assumes one shared account, ordered side effects across files, or a global mutable fixture may fail when parallelized.
First run with one worker to establish behavior, then validate that tests can run independently before increasing concurrency. Give each worker or test isolated accounts and records where needed; make output paths unique. Use a worker index or test ID to partition data only when the backing system supports that strategy. The official parallelism guide explains worker behavior and isolation patterns.
// Start conservatively while migrating.
npx playwright test --workers=1
// Increase only after validating test and data independence.
npx playwright test --workers=4
Concurrency can shorten elapsed time when the environment has capacity, but it can also increase resource contention and expose data collisions. Measure it in your own CI environment; no universal speedup follows from changing frameworks.
10. Move the suite into CI and preserve diagnostics
Move CI after the local representative test and feature batches are stable. Confirm the CI operating system, browser matrix, network access, secrets, test data, artifacts, retry policy, and reporting requirements. Playwright supports Chromium, Firefox, and WebKit on Windows, Linux, and macOS, locally or in CI, but that does not make an existing Selenium Grid configuration a drop-in replacement. Assess remote execution and infrastructure needs separately.
A minimal Linux CI sequence using npm is:
npm ci
npx playwright install --with-deps
npx playwright test
Keep the lockfile so CI installs the same package version as development, and install the matching browser binaries. Configure projects for the browser coverage you actually need. Begin with a conservative worker count; Playwright’s CI documentation recommends one worker for stability and reproducibility, with parallelism or sharding as a deliberate capacity choice. Keep reports, traces, screenshots, and logs available for failed runs according to your team’s retention policy. See Playwright CI guidance.
11. Common migration failures and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| “strict mode violation” or locator matched more than one element | The action needs one target, but the locator matches duplicates. | Use a more specific accessible name or scope the locator to a row, dialog, or form. Avoid arbitrary positional selection unless order is part of the test contract. |
| Element not found or assertion timed out | Wrong page state, selector, test data, or environment; the expected condition may never occur. | Check the URL and page state, inspect the locator and test data, and use a retrying assertion for the actual expected condition. |
| Click reports that an element is not actionable | The element is hidden, moving, disabled, covered, or does not receive events. | Check the UI state and overlay behavior. Wait for the intended state with an assertion; avoid force-clicking to conceal a real interaction problem. |
| Tests pass alone but fail in the full suite | Shared mutable test data, ordering dependency, leaked state, or resource contention. | Run with one worker to diagnose, remove dependencies between tests, isolate accounts and records, and increase workers only after validation. |
| Browser launch fails in CI | Browser binaries or operating-system dependencies are missing, or the environment cannot download them. | Install browsers matching the package and required system dependencies. Check proxy and certificate configuration where relevant; use DEBUG=pw:browser to inspect launch logs. |
| Works locally but fails only in CI | Environment, permissions, secrets, network access, browser version, time limits, or available resources differ. | Compare environment configuration, preserve artifacts, inspect traces and logs, and reproduce with the CI browser and configuration where possible. |
| A test waits forever or fails after replacing a sleep | The replacement waits for a condition unrelated to the original race or business event. | Identify the condition the test needs and wait for that observable signal with a bounded timeout. Actionability checks do not indicate that a background job completed. |
| TypeScript or module import errors | The example’s module system, file extension, or runner setup does not match the project. | Use explicit Playwright Test imports and align the test file and TypeScript configuration with the project’s module setup. |
12. Performance, reliability, and cost considerations
- Runtime: total time depends on test design, browser count, worker count, application speed, and CI capacity. Benchmark your suite before and after under comparable conditions rather than assuming a fixed improvement.
- Reliability: actionability checks and retrying assertions handle common timing variation, but they cannot correct unstable data, a broken application, or a wait for the wrong event. Preserve meaningful synchronization and isolate tests.
- Browser coverage: Chromium, Firefox, and WebKit are available Playwright targets. Choose projects that match supported customer environments and the team’s risk priorities.
- CI cost: browser downloads, operating-system dependencies, workers, sharding, and artifact retention all use CI resources. Start with required coverage and increase concurrency based on measured capacity and runtime.
- Migration cost: the largest work is often framework lifecycle, selectors, test data, custom waits, and infrastructure—not replacing individual method names. Scope feature batches and retain behavior coverage while old and new suites overlap.
Or skip the browser setup
If the migration work includes capturing pages for QA evidence or visual references, ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns a PNG, JPEG, WebP, or PDF. Its clean-shot flow accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. An MCP server gives AI agents tools for screenshots, page information, and PDF capture.
Example cURL request (see the ScreenshotNeo API documentation for configuration and response details):
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free. Every feature is on every plan. Sign up for 1,000 free screenshots a month, with no card.
Migration checklist
- Inventory the existing suite, lifecycle, waits, browser matrix, data, CI, and diagnostics.
- Choose the Playwright language API and runner deliberately.
- Port and validate one representative end-to-end test.
- Rewrite locators around user intent and uniqueness.
- Replace waits only when an action or assertion proves the same condition.
- Rebuild lifecycle around explicit isolation and suitable fixture scopes.
- Migrate feature by feature while comparing behavior and coverage.
- Validate test-data independence before raising worker counts.
- Install matching browsers in CI and verify reports and failure artifacts.
FAQ
Can I migrate Selenium tests without changing the programming language?
Potentially. Playwright has language APIs beyond Node.js, but Playwright Test and its fixture examples are Node.js-specific. Confirm the current API and runner for your language before choosing the migration path.
Do I have to delete my page objects?
No. Keep page objects that clarify behavior, and adapt them to async methods and Playwright locators. The fixture model can supply the page or other dependencies they need.
Does Playwright eliminate every explicit wait?
No. Built-in actionability and retrying assertions cover particular UI conditions. Keep a bounded wait when the test depends on a distinct business, backend, or external event.
Should the migrated suite run in parallel immediately?
No. First check that tests do not rely on shared mutable state or execution order. Start with one worker while diagnosing migration issues, then increase concurrency when data and environment independence are established.
Is Playwright a drop-in replacement for Selenium Grid?
Do not assume so. Compare the existing remote execution architecture, browser and operating-system requirements, authentication, network access, CI capacity, and artifact workflow with the team’s Playwright setup.


