Want to migrate an existing Selenium test suite to Playwright, but not sure where to start or how the syntax actually changes? This article isn’t a “which one should I choose” comparison โ it’s written for engineers who already have Selenium code, covering a conversion table for locators, waits, and common operations, along with a practical migration strategy.
Migration isn’t an all-or-nothing choice between “rewrite everything” and “stay put.” With a conversion table close at hand, you can migrate gradually.
๐ Who This Article Is For
- Anyone already writing test code in Selenium who’s considering a move to Playwright
- Anyone who wants a clear reference for how locators and waits map between the two
- Anyone who needs to migrate a large test suite while keeping risk manageable
- Anyone who wants to know the common pitfalls of migration ahead of time
โ What You’ll Learn
- A reference table mapping Selenium’s core APIs to their Playwright equivalents
- The trade-offs between a phased migration and a big-bang rewrite
- Common issues that come up during migration and how to handle them
๐ค About the Author
Written by Yoshi, a QA and test automation engineer with over 15 years of hands-on experience, including leading a real-world migration of a Selenium-based test suite to Playwright and accumulating decision criteria and gotchas along the way.
๐ Key Takeaways
- The larger the test suite, the more a phased migration reduces risk compared to a big-bang rewrite
- Explicit
WebDriverWaitcalls become largely unnecessary in Playwright, since Auto-waiting is built in - Once you have the locator mapping down, most of the conversion becomes mechanical
Migrating an existing Selenium-based test suite to Playwright tends to surface challenges like these:
- There are too many tests to realistically rewrite all at once
- Looking up how
By.IDorBy.XPATHtranslates to Playwright, over and over, gets tedious - There’s a large amount of
WebDriverWait-based waiting logic, and it’s unclear how to handle it - There’s no clear plan for how to handle the period where Selenium and Playwright tests coexist
This article walks through a conversion table and migration strategy that address each of these.
Where This Article Fits
Information about which tool to choose โ Selenium vs. Playwright โ is covered elsewhere. This article focuses on the practical stage: how to actually rewrite code you’ve already written in Selenium. If you’re still deciding which tool to use, we recommend reading a comparison article first.
โ Migration Strategy: Big-Bang or Phased?
There are broadly two approaches to migration.
| Strategy | Overview | Good fit for |
|---|---|---|
| Big-bang migration | Rewrite every test to Playwright at once | A small test count, or a scope that can genuinely be completed in a short window |
| Phased migration | Migrate feature by feature or module by module, with both coexisting for a while | A large test count, or a project that needs to keep shipping releases |
โก Locator Conversion Table
Selenium’s By-based locators generally map onto page.locator() or a dedicated Playwright method.
| Selenium | Playwright (equivalent) |
|---|---|
driver.find_element(By.ID, "email") | page.locator("#email") |
driver.find_element(By.CSS_SELECTOR, "button.submit") | page.locator("button.submit") |
driver.find_element(By.CLASS_NAME, "btn-primary") | page.locator(".btn-primary") |
driver.find_element(By.XPATH, "//button[@id='x']") | page.locator("//button[@id='x']") (see the code example below) |
driver.find_element(By.NAME, "email") | page.locator("[name='email']") |
driver.find_element(By.LINK_TEXT, "View details") | page.get_by_role("link", name="View details") |
| (no direct Selenium equivalent) | page.get_by_role() / get_by_test_id(), etc. (Playwright’s recommended locators) |
# Both of these work
page.locator("//button[@id='submit']")
page.locator("xpath=//button[@id='submit']")// is automatically detected as XPath, so you can pass it straight into page.locator(). Explicitly prefixing with xpath= is also supported, and some teams prefer it to make the distinction from a CSS selector visually clear.get_by_role(), get_by_test_id() โ for even better readability and maintainability, if you have the time.โข Replacing Wait Logic
Of everything involved in migrating from Selenium, this is where the code shrinks the most.
# โ Selenium: requires an explicit wait until the element is actionable
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
wait = WebDriverWait(driver, 10)
element = wait.until(EC.element_to_be_clickable((By.ID, "submit")))
element.click()# โ
Playwright: Auto-waiting is built in, so an explicit wait is generally unnecessary
page.locator("#submit").click() Spots in the Selenium code where time.sleep() was used as a stand-in for WebDriverWait can often be removed entirely in Playwright.
# โ Selenium: waiting was papered over with time.sleep()
driver.find_element(By.ID, "open-modal").click()
time.sleep(2) # waiting an arbitrary number of seconds for the modal animation
driver.find_element(By.ID, "modal-confirm").click()# โ
Playwright: time.sleep() disappears entirely
page.locator("#open-modal").click()
page.locator("#modal-confirm").click() # Actionability checks handle the wait automaticallyexpect().to_be_visible() or page.expect_response(). “Playwright means you never need to wait for anything” isn’t an accurate takeaway.| Selenium | Playwright (equivalent) |
|---|---|
EC.presence_of_element_located | Usually no explicit wait needed (Auto-waiting handles it) |
EC.visibility_of_element_located | expect(locator).to_be_visible() |
| (no equivalent โ often stood in for with sleep) | page.expect_response() (waits for a specific API response) |
โฃ Common Operations Conversion Table
Here’s a mapping for other commonly used operations.
| Selenium | Playwright |
|---|---|
webdriver.Chrome() | playwright.chromium.launch() (or the browser fixture with pytest-playwright) |
driver.get(url) | page.goto(url) |
element.send_keys("text") | locator.fill("text") |
element.click() | locator.click() |
element.text | locator.text_content() or locator.inner_text() |
Select(element).select_by_visible_text("x") | locator.select_option(label="x") |
driver.switch_to.frame(x) | page.frame_locator("iframe selector") |
driver.execute_script(js) | page.evaluate(js) |
driver.get_screenshot_as_png() | page.screenshot() |
driver.current_url | page.url |
driver.quit() | browser.close() |
Example: Rewriting a Login Flow
# โ Selenium version
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
driver.get("https://example.com/login")
wait = WebDriverWait(driver, 10)
email = wait.until(EC.presence_of_element_located((By.ID, "email")))
email.send_keys("test@example.com")
driver.find_element(By.ID, "password").send_keys("Password123")
driver.find_element(By.ID, "submit").click()# โ
Playwright version
page.goto("https://example.com/login")
page.locator("#email").fill("test@example.com")
page.locator("#password").fill("Password123")
page.locator("#submit").click()Common Mistakes Seen in Practice
โ Leaving WebDriverWait-style logic in the Playwright code
Rushing through the migration and mechanically converting syntax while carrying over Selenium’s mental model of waiting is a common trap. Understand what Auto-waiting actually covers, and remove wait code that’s no longer needed.
โก Forgetting to update CI configuration during the coexistence period
While Selenium and Playwright tests coexist, it’s common to miss adding the Playwright browser install step (playwright install) to CI, or to underestimate the added runtime. Revisit your CI configuration early in the migration.
โข Starting without a clear priority order and stalling out halfway
Starting with “whatever’s easiest first” can mean low-priority tests get migrated while the important tests stay on Selenium indefinitely. Deciding up front to prioritize business-critical features, or the tests that break most often, is worth doing at the start.
โฃ Proceeding without aligning the team first
If one person makes locator and design decisions unilaterally, the Playwright code other team members write later can end up inconsistent in style. Agree on a migration policy โ recommended locators, directory structure โ as a team before starting.
๐ Related Articles
FAQ
Q. How long should I budget for a migration?
This varies enormously depending on your test count, complexity, and team size, so there’s no single answer. A practical approach is to migrate a small feature first, measure the actual time per test, and extrapolate from there to your full test count.
Q. Can Selenium and Playwright coexist in the same repository?
Yes. Both can be listed in requirements.txt, and pytest can run Selenium and Playwright tests within the same test session. Keeping them in separate directories โ tests/selenium/ and tests/playwright/ โ makes it immediately clear which tool a given test uses and makes migration progress easy to track. For CI, the common approach is either separate parallel jobs or running both test commands sequentially within one job. Just be aware that total runtime will temporarily increase during the migration period.
Q. Can I reuse my existing Page Object Model design?
The overall concept carries over, but the locator definitions and method bodies inside each class need to be rewritten for Playwright. Since the “one class per page” structure doesn’t change, your directory layout can often stay as-is.
Q. Is parallel execution in CI, like Selenium Grid provides, possible with Playwright too?
Yes. Playwright supports parallel execution across multiple browser contexts, and combining it with a plugin like pytest-xdist gives you parallelization at the pytest level as well. In many cases, you won’t need to build out dedicated infrastructure the way Selenium Grid requires.
Q. Should I also refactor the test code itself as part of the migration?
Doing everything at once risks dragging out the migration itself. We’d recommend completing the migration first with a mechanical, behavior-preserving rewrite, and treating refactoring as a separate task to tackle afterward.
๐ Summary
- For a large test suite, choose a phased migration so you can keep shipping releases without interruption
- Locator conversion is often just a matter of reusing your existing CSS/XPath as-is โ most of it is mechanical
- Most of WebDriverWait becomes unnecessary thanks to Auto-waiting, though waiting on asynchronous processes may still be needed in some cases
- Agree on a migration policy as a team first, then set priorities and start
Migrating from Selenium to Playwright is, for the most part, a mechanical process once you have a conversion table in hand. Start with a single test file and try rewriting it while following along with the table above.
