Selenium to Playwright Migration: Practical Tips Beyond 1:1 Translation
The biggest mistake in a Selenium to Playwright migration is translating your existing tests line by line. Playwright has a different architecture that requires rethinking, not just rewriting.
“Generates standard Playwright files you can inspect, modify, and run in any CI pipeline.”
Assrt SDK
1. Why 1:1 translation is the wrong approach
When teams migrate from Selenium to Playwright, the natural instinct is to translate each Selenium command to its Playwright equivalent. driver.findElement(By.css('.btn')) becomes page.locator('.btn'). Explicit waits get replaced with Playwright timeouts. Page object classes get copied with method signatures unchanged. The tests run, they pass, and the migration is declared complete.
The problem is that this approach carries forward every architectural decision from Selenium, including the bad ones. Selenium tests are full of explicit waits because the framework does not handle timing automatically. They use CSS selectors because Selenium's support for semantic locators is limited. They wrap everything in page objects because element lookups are expensive and need to be managed carefully.
Playwright has fundamentally different characteristics. Auto-wait eliminates the need for explicit waits. Role-based locators eliminate the fragility of CSS selectors. Locator objects are lazy and lightweight, so the overhead that justified page objects in Selenium does not exist. A 1:1 translation preserves Selenium patterns that work against Playwright's strengths.
The better approach is to treat migration as an opportunity to rewrite your test architecture. Keep the test scenarios (what you are testing) but redesign the implementation (how you test it). This takes more time upfront but produces a test suite that is faster, more reliable, and easier to maintain.
2. Eliminating explicit waits with auto-wait
In a typical Selenium test suite, 20% to 30% of the code is wait logic. Explicit waits, fluent waits, expected conditions, custom wait wrappers, and retry loops. This code exists because Selenium does not know when the page is ready for interaction. The developer must anticipate every timing scenario and code for it manually.
Playwright's auto-wait replaces all of this. When you call page.getByRole('button', { name: 'Submit' }).click() , Playwright automatically waits for the button to be attached to the DOM, visible, stable (not animating), enabled, and not obscured by other elements. Only then does it click. If the button takes 3 seconds to appear after a page load, Playwright waits 3 seconds. If it takes 100 milliseconds, Playwright waits 100 milliseconds.
During migration, the first step is to remove all explicit waits from your translated tests. Delete every WebDriverWait, ExpectedConditions, and Thread.sleep equivalent. Run the tests. Most will pass without any waits because Playwright handles the timing. The few that fail will need investigation, but the failure messages will tell you exactly what condition was not met.
The remaining cases where you need explicit waits in Playwright are rare and specific: waiting for a network response to complete, waiting for a URL change, or waiting for a custom JavaScript condition. These are expressed cleanly with page.waitForResponse(), page.waitForURL(), or page.waitForFunction(). Each waits for a specific, meaningful condition rather than an arbitrary timeout.
3. Locator chaining and the new selector model
Selenium's element model is imperative: you find an element, get a reference, and interact with the reference. If the element changes between finding and interacting, you get a stale element exception. This forces a pattern of find-then-act that requires careful management of element lifetimes.
Playwright's locator model is declarative. A locator describes how to find an element, but it does not actually find the element until you perform an action. You can create a locator at the top of a test, and it will find the current matching element each time you use it. There is no stale element problem because there is no stored element reference.
Locator chaining lets you compose locators to target specific elements within a context. For example, page.getByRole('dialog').getByRole('button', { name: 'Confirm' }) targets the "Confirm" button specifically within a dialog, ignoring any other "Confirm" buttons on the page. This is more readable and resilient than CSS selectors like .modal .btn-confirm.
During migration, replace CSS selectors with semantic locators wherever possible. Use getByRole for interactive elements, getByLabel for form fields, getByText for content verification, and getByTestId only as a fallback. Playwright's codegen tool can help by showing you the recommended locator for any element you click on.
4. Evolving page objects for Playwright
Page objects in Selenium serve multiple purposes: they encapsulate element lookups, provide wait logic, and offer a high-level API for page interactions. In Playwright, the first two purposes are handled by the framework itself (lazy locators and auto-wait), leaving page objects with only the third purpose: providing a domain-specific API.
This means Playwright page objects should be much thinner than Selenium page objects. A Selenium page object for a login page might have 50 lines: element locators, wait methods, retry logic, and interaction methods. A Playwright equivalent might have 15 lines: just the interaction methods, with locators defined inline using semantic selectors.
Some teams abandon page objects entirely in Playwright and use plain test functions with locators created on the fly. This works well for smaller test suites. For larger suites, lightweight page objects that group related actions (login, fill form, submit order) still provide value by reducing duplication and improving readability.
Tools like Assrt take a different approach entirely. Instead of requiring you to build and maintain page objects, Assrt discovers your application's pages and interactions automatically and generates complete test files. When the application changes, Assrt regenerates or updates the affected tests. This eliminates the page object maintenance burden that drives much of the cost in traditional test automation.
5. A phased migration strategy that reduces risk
Do not attempt to migrate your entire Selenium suite at once. A phased approach lets you validate the new architecture before committing fully and maintains test coverage throughout the migration.
Phase 1: Set up the Playwright infrastructure alongside Selenium. Install Playwright, configure the test runner, set up CI integration, and write 3 to 5 new tests for your most critical user journeys. These tests should use idiomatic Playwright (semantic locators, no explicit waits, trace viewer enabled). Run both suites in CI. This phase proves that Playwright works in your environment and establishes the patterns your team will follow.
Phase 2: Stop writing new Selenium tests. All new test development uses Playwright. Continue running the existing Selenium suite for coverage, but new features only get Playwright tests. This prevents the Selenium suite from growing while the Playwright suite catches up.
Phase 3: Migrate existing Selenium tests by priority. Start with the tests that fail most often (they benefit most from Playwright's auto-wait and better debugging). Then migrate tests for critical paths. Leave stable, rarely-failing Selenium tests for last, or consider whether they even need to be migrated (if they are reliable and cover low-risk areas, the cost of migration may not be justified).
Phase 4: Decommission Selenium. Once the Playwright suite covers everything the Selenium suite covered (or you have decided that certain low-value tests do not need to be carried forward), remove the Selenium dependencies, CI jobs, and infrastructure. Celebrate the reduction in maintenance burden.
Ready to automate your testing?
Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.