Migrating from Selenium to Playwright
By TBS Tech Services QA team. Published
Migrating from Selenium or Cypress to Playwright works best in stages. Decide whether the move is worth it, group your tests by risk, migrate one batch at a time, run old and new side by side, and prove each new test fails when the product is broken. Only then retire the original.
This guide is a working outline. We will expand it with worked examples.
Should you migrate at all?
Not always. A stable, fast suite your team trusts may not be worth the cost of moving. Migration pays off when tests are slow or flaky, when the old tool limits what you can test, or when maintaining the suite takes more time than the tests save. Decide with evidence, not fashion.
Gather the evidence first:
- How long a full run takes
- How many tests fail and then pass on a rerun
- Hours spent each month keeping the suite working
- What you can’t test today
- Who understands the suite well enough to maintain it
How do you plan the migration?
Start by auditing the suite. List every test, note what it protects, and group tests by business risk. Delete tests that no longer matter. Then migrate in batches, highest risk first, so the most valuable protection moves early and you can stop at any point with a working suite.
- Audit the suite and group tests by risk.
- Set up a Playwright project with your CI running it from day one.
- Migrate one batch of tests.
- Run old and new side by side.
- Prove the new tests catch the same defects.
- Retire the old tests, then repeat with the next batch.
How do Selenium and Cypress ideas map to Playwright?
Most concepts have a direct equivalent, but the habits differ. Playwright waits for elements automatically, so explicit waits mostly disappear. Locators describe what a user sees, such as a button name, instead of fragile paths. Network stubbing, tabs and iframes are built in. Plan to rewrite the tests, not translate them line by line.
Finding an element and clicking it:
// Selenium (Java style): driver.findElement(By.cssSelector("#login")).click();
await page.getByRole('button', { name: 'Sign in' }).click()
Waiting for something to appear:
// Selenium WebDriverWait or Cypress cy.wait aren’t needed.
await expect(page.getByText('Welcome back')).toBeVisible()
Stubbing a network call:
// Cypress: cy.intercept('/api/orders', [])
await page.route('**/api/orders', (route) => route.fulfill({ json: [] }))
Custom commands and page objects usually become fixtures and small helper functions.
How do you prove the new tests catch the same defects?
Break the app on purpose. Remove a validation, change a price or hide a button in a test branch, then check that both the old and the new test fail. A migrated test that stays green when the product is broken isn’t protecting anything and must be rewritten before the original is retired.
What mistakes should you avoid?
The common ones are migrating everything at once, copying old waits and sleeps, keeping fragile selectors, hiding flakiness behind retries and switching off the old suite too early. Each one recreates the problems you were trying to leave behind, only in a newer tool.
For help planning a move, see Selenium and Cypress migration and Playwright in CI/CD.