# Migrating from Selenium to Playwright > **When to use**: When converting a Selenium/WebDriver test suite to Playwright. Covers Java/Python/C# Selenium idioms mapped to TypeScript and JavaScript Playwright equivalents. > **Prerequisites**: [core/locators.md](../core/locators.md), [core/assertions-and-waiting.md](../core/assertions-and-waiting.md) ## Key Mindset Shifts Before translating any code, internalize these six changes. They are not syntax swaps -- they are fundamental differences in how Playwright works. ### 1. No More Explicit Waits Selenium requires `WebDriverWait` and `ExpectedConditions` on nearly every interaction. Playwright auto-waits on every action (`click`, `fill`, `check`, `selectOption`) and every web-first assertion (`expect(locator).toBeVisible()`). Delete all your waits. They are not needed. ``` Selenium: WebDriverWait + ExpectedConditions.visibilityOfElementLocated(...) Playwright: nothing — auto-waiting is built into every action and assertion ``` ### 2. No WebDriver Protocol Overhead Selenium sends every command over the WebDriver (W3C) HTTP protocol -- each click, each find, each assertion is a round-trip HTTP request. Playwright communicates over CDP (Chromium), the DevTools protocol (Firefox), or native WebKit protocol. This is a persistent bidirectional connection, not request-response. Tests are faster by default. ### 3. No Driver Management Selenium requires matching browser drivers (`chromedriver`, `geckodriver`) to browser versions. Version mismatches are a constant source of CI failures. Playwright bundles browser binaries. One command installs everything: ```bash npx playwright install ``` No `WebDriverManager`. No `chromedriver` path. No version matrix. ### 4. No Implicit vs Explicit Wait Confusion Selenium has implicit waits (global, apply to all `findElement` calls), explicit waits (`WebDriverWait`), and `Thread.sleep()` / `time.sleep()`. Teams mix these, creating unpredictable timing behavior. Playwright has one mechanism: auto-waiting. Every action waits for the element to be actionable. Every web-first assertion retries until timeout. There is nothing to configure. ### 5. Locators Are Lazy and Auto-Retry In Selenium, `findElement()` immediately queries the DOM and returns a `WebElement` reference. If the DOM changes, that reference is stale and throws `StaleElementReferenceException`. In Playwright, `page.locator()` returns a lazy locator that re-queries the DOM on every action. There is no stale element. There is no `StaleElementReferenceException`. Ever. ### 6. Built-In Test Runner Selenium is a browser automation library, not a test framework. You need JUnit, TestNG, pytest, or Mocha on top. Playwright Test is a full test runner with parallel execution, retries, fixtures, HTML reports, trace viewer, and UI mode. No assembly required. ## API Mapping Table Every Selenium API and its direct Playwright equivalent. The "Notes" column calls out behavior differences. ### Navigation | Selenium WebDriver | Playwright | Notes | | ----------------------------- | ------------------------ | -------------------------------------------------------------------------- | | `driver.get(url)` | `await page.goto(url)` | Playwright waits for `load` event by default; configurable via `waitUntil` | | `driver.navigate().to(url)` | `await page.goto(url)` | Same as above | | `driver.navigate().back()` | `await page.goBack()` | Waits for navigation to complete | | `driver.navigate().forward()` | `await page.goForward()` | Waits for navigation to complete | | `driver.navigate().refresh()` | `await page.reload()` | Waits for `load` event | | `driver.getCurrentUrl()` | `page.url()` | Synchronous in Playwright -- no await needed | | `driver.getTitle()` | `await page.title()` | Or use `await expect(page).toHaveTitle('...')` for assertion | ### Element Location | Selenium WebDriver | Playwright | Notes | | --------------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------- | | `driver.findElement(By.id("x"))` | `page.locator('#x')` or `page.getByTestId('x')` | Prefer `getByRole()` or `getByTestId()` over ID selectors | | `driver.findElement(By.css("x"))` | `page.locator('x')` | CSS selectors work, but prefer semantic locators | | `driver.findElement(By.xpath("x"))` | `page.locator('xpath=x')` | Works but avoid XPath; use `getByRole()`, `getByLabel()`, `getByText()` | | `driver.findElement(By.name("x"))` | `page.locator('[name="x"]')` | Or `page.getByLabel()` if the field has a label | | `driver.findElement(By.linkText("x"))` | `page.getByRole('link', { name: 'x' })` | Role-based is more resilient | | `driver.findElement(By.partialLinkText("x"))` | `page.getByRole('link', { name: /x/ })` | Regex for partial match | | `driver.findElement(By.className("x"))` | `page.locator('.x')` | Class selectors are fragile; prefer semantic locators | | `driver.findElement(By.tagName("x"))` | `page.locator('x')` | Rarely useful alone; combine with role or text | | `driver.findElements(By.css("x"))` | `await page.locator('x').all()` | Returns array of locators; or use `toHaveCount()` to assert count | ### Element Interaction | Selenium WebDriver | Playwright | Notes | | ---------------------------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `element.click()` | `await locator.click()` | Auto-waits for element to be visible, stable, enabled, and unobscured | | `element.sendKeys("text")` | `await locator.fill("text")` | **Behavior change**: `fill()` clears existing text first, then sets the value. Use `pressSequentially()` to type character by character. | | `element.sendKeys(Keys.ENTER)` | `await locator.press('Enter')` | Or `await page.keyboard.press('Enter')` | | `element.clear()` | `await locator.clear()` | Or `await locator.fill('')` | | `element.submit()` | `await locator.press('Enter')` | No direct equivalent; click the submit button or press Enter | | `new Select(element).selectByVisibleText("x")` | `await locator.selectOption({ label: 'x' })` | Also supports `{ value: 'x' }` and `{ index: 0 }` | | `element.isDisplayed()` | `await expect(locator).toBeVisible()` | Use assertion form -- it auto-retries. `locator.isVisible()` does not retry. | | `element.isEnabled()` | `await expect(locator).toBeEnabled()` | Use assertion form for reliability | | `element.isSelected()` | `await expect(locator).toBeChecked()` | For checkboxes and radio buttons | | `element.getText()` | `await locator.textContent()` | Or prefer `await expect(locator).toHaveText('...')` which auto-retries | | `element.getAttribute("x")` | `await locator.getAttribute('x')` | Or `await expect(locator).toHaveAttribute('x', 'value')` | | `element.getCssValue("x")` | `await expect(locator).toHaveCSS('x', 'value')` | Use assertion form; computed values only | ### Waits and Conditions | Selenium WebDriver | Playwright | Notes | | ------------------------------------------------------ | --------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | `WebDriverWait(driver, 10).until(...)` | Not needed | Playwright auto-waits on all actions and assertions | | `ExpectedConditions.visibilityOfElementLocated(...)` | `await expect(locator).toBeVisible()` | Built into every action; use assertion only when you need to verify visibility explicitly | | `ExpectedConditions.elementToBeClickable(...)` | Not needed | `click()` auto-waits for clickability | | `ExpectedConditions.presenceOfElementLocated(...)` | `await expect(locator).toBeAttached()` | Rarely needed; most actions wait for attachment automatically | | `ExpectedConditions.invisibilityOfElementLocated(...)` | `await expect(locator).not.toBeVisible()` | Auto-retries until element disappears | | `ExpectedConditions.textToBePresentInElement(...)` | `await expect(locator).toHaveText('...')` | Auto-retries until text matches | | `ExpectedConditions.titleIs("x")` | `await expect(page).toHaveTitle('x')` | Auto-retries | | `ExpectedConditions.urlContains("x")` | `await expect(page).toHaveURL(/x/)` | Or `await page.waitForURL('**/x')` | | `ExpectedConditions.alertIsPresent()` | `page.on('dialog', ...)` or `page.waitForEvent('dialog')` | Register handler before the action that triggers the dialog | | `Thread.sleep(5000)` / `time.sleep(5)` | Never | Delete it. Use auto-waiting assertions instead. | | `driver.manage().timeouts().implicitlyWait(10)` | Not needed | No implicit waits in Playwright -- auto-waiting handles everything | ### Frames and Windows | Selenium WebDriver | Playwright | Notes | | ------------------------------------ | ------------------------------------------ | --------------------------------------------------------------- | | `driver.switchTo().frame("name")` | `page.frameLocator('iframe[name="name"]')` | No context switching; chain locators directly into the frame | | `driver.switchTo().frame(element)` | `page.frameLocator('iframe#id')` | Target the iframe by any CSS selector | | `driver.switchTo().defaultContent()` | Not needed | No frame switching in Playwright; each `frameLocator` is scoped | | `driver.switchTo().parentFrame()` | Not needed | No frame switching to undo | | `driver.switchTo().window(handle)` | `context.pages()` | Access all pages in the context by index | | `driver.getWindowHandle()` | Not needed | Use `page` references directly | | `driver.getWindowHandles()` | `context.pages()` | Returns array of all open pages | | New window/tab opened by click | `page.waitForEvent('popup')` | Register before the click; returns the new `Page` object | ### Browser and Context | Selenium WebDriver | Playwright | Notes | | ----------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------ | | `driver.manage().window().setSize(w, h)` | `await page.setViewportSize({ width: w, height: h })` | Sets the viewport, not the OS window | | `driver.manage().window().maximize()` | Configure in `playwright.config` `use.viewport` | Or pass `--headed` with large viewport | | `driver.manage().addCookie(cookie)` | `await context.addCookies([cookie])` | Takes an array; operates on the browser context | | `driver.manage().getCookieNamed("x")` | `await context.cookies()` then filter | Returns all cookies; filter in JS | | `driver.manage().deleteAllCookies()` | `await context.clearCookies()` | Clears all cookies in the context | | `driver.executeScript("return ...")` | `await page.evaluate(() => { ... })` | Full access to browser JS context; supports return values | | `driver.executeAsyncScript(...)` | `await page.evaluate(async () => { ... })` | `evaluate` supports async functions natively | | `driver.getScreenshotAs(OutputType.FILE)` | `await page.screenshot({ path: 'shot.png' })` | Also supports `fullPage: true`, element screenshots via `locator.screenshot()` | | `driver.quit()` | Handled automatically | Playwright Test manages browser lifecycle. No manual cleanup. | ### Actions Class | Selenium WebDriver | Playwright | Notes | | ------------------------------------------------------------------------------- | ----------------------------------------------- | ----------------------------------------- | | `new Actions(driver).moveToElement(el).perform()` | `await locator.hover()` | Single method, auto-waits | | `new Actions(driver).doubleClick(el).perform()` | `await locator.dblclick()` | Single method, auto-waits | | `new Actions(driver).contextClick(el).perform()` | `await locator.click({ button: 'right' })` | Right-click option | | `new Actions(driver).dragAndDrop(src, tgt).perform()` | `await source.dragTo(target)` | Both are locators | | `new Actions(driver).keyDown(Keys.SHIFT).click(el).keyUp(Keys.SHIFT).perform()` | `await locator.click({ modifiers: ['Shift'] })` | Modifier keys as option | | `new Actions(driver).sendKeys(Keys.chord(Keys.CONTROL, "a")).perform()` | `await page.keyboard.press('Control+a')` | Keyboard API for global shortcuts | | `new Actions(driver).moveByOffset(x, y).perform()` | `await page.mouse.move(x, y)` | Raw mouse API for canvas/map interactions | ## Before/After Examples ### Example 1: Login Test The most common Selenium test. Notice the complete absence of explicit waits in the Playwright version. **Selenium (Java)** ```java import org.openqa.selenium.*; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.support.ui.WebDriverWait; import org.openqa.selenium.support.ui.ExpectedConditions; import org.junit.jupiter.api.*; public class LoginTest { WebDriver driver; @BeforeEach void setUp() { System.setProperty("webdriver.chrome.driver", "/path/to/chromedriver"); driver = new ChromeDriver(); driver.manage().window().maximize(); driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10)); } @AfterEach void tearDown() { if (driver != null) driver.quit(); } @Test void userCanLogIn() { driver.get("https://myapp.com/login"); WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); WebElement emailField = wait.until( ExpectedConditions.visibilityOfElementLocated(By.id("email")) ); emailField.clear(); emailField.sendKeys("user@example.com"); WebElement passwordField = driver.findElement(By.id("password")); passwordField.clear(); passwordField.sendKeys("s3cure!Pass"); WebElement loginButton = wait.until( ExpectedConditions.elementToBeClickable(By.cssSelector("button[type='submit']")) ); loginButton.click(); wait.until(ExpectedConditions.urlContains("/dashboard")); WebElement heading = wait.until( ExpectedConditions.visibilityOfElementLocated(By.tagName("h1")) ); Assertions.assertEquals("Dashboard", heading.getText()); } } ``` **Playwright (TypeScript)** ```typescript import { test, expect } from "@playwright/test" test("user can log in", async ({ page }) => { await page.goto("/login") await page.getByLabel("Email").fill("user@example.com") await page.getByLabel("Password").fill("s3cure!Pass") await page.getByRole("button", { name: "Sign In" }).click() await page.waitForURL("/dashboard") await expect(page.getByRole("heading", { level: 1 })).toHaveText("Dashboard") }) ``` **Playwright (JavaScript)** ```javascript const { test, expect } = require("@playwright/test") test("user can log in", async ({ page }) => { await page.goto("/login") await page.getByLabel("Email").fill("user@example.com") await page.getByLabel("Password").fill("s3cure!Pass") await page.getByRole("button", { name: "Sign In" }).click() await page.waitForURL("/dashboard") await expect(page.getByRole("heading", { level: 1 })).toHaveText("Dashboard") }) ``` **What changed**: 45 lines of Java with explicit waits, driver management, and element references became 9 lines of Playwright. No setup, no teardown, no waits, no driver path. The `fill()` call replaces `clear()` + `sendKeys()`. Semantic locators (`getByLabel`, `getByRole`) replace brittle `By.id` and `By.cssSelector`. --- ### Example 2: Search and Verify Results Demonstrates replacing `findElements()`, explicit waits for result count, and text assertions. **Selenium (Java)** ```java @Test void searchReturnsResults() { driver.get("https://myapp.com/products"); WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); WebElement searchBox = wait.until( ExpectedConditions.visibilityOfElementLocated(By.cssSelector("input[placeholder='Search products']")) ); searchBox.clear(); searchBox.sendKeys("wireless headphones"); searchBox.sendKeys(Keys.ENTER); wait.until(ExpectedConditions.numberOfElementsToBeMoreThan( By.cssSelector(".product-card"), 0 )); List results = driver.findElements(By.cssSelector(".product-card")); Assertions.assertTrue(results.size() >= 3); String firstTitle = results.get(0).findElement(By.cssSelector(".product-title")).getText(); Assertions.assertTrue(firstTitle.toLowerCase().contains("wireless")); } ``` **Playwright (TypeScript)** ```typescript import { test, expect } from "@playwright/test" test("search returns results", async ({ page }) => { await page.goto("/products") await page.getByPlaceholder("Search products").fill("wireless headphones") await page.getByPlaceholder("Search products").press("Enter") const results = page.getByTestId("product-card") await expect(results).toHaveCount(3, { timeout: 10_000 }) await expect(results.first().getByTestId("product-title")).toContainText(/wireless/i) }) ``` **Playwright (JavaScript)** ```javascript const { test, expect } = require("@playwright/test") test("search returns results", async ({ page }) => { await page.goto("/products") await page.getByPlaceholder("Search products").fill("wireless headphones") await page.getByPlaceholder("Search products").press("Enter") const results = page.getByTestId("product-card") await expect(results).toHaveCount(3, { timeout: 10_000 }) await expect(results.first().getByTestId("product-title")).toContainText(/wireless/i) }) ``` **What changed**: No `WebDriverWait` for element visibility. No `clear()` before `sendKeys()`. No `findElements()` returning a stale list. The Playwright `toHaveCount()` auto-retries until the results appear. The regex assertion on `toContainText` replaces manual `getText()` + `toLowerCase()` + `contains()`. --- ### Example 3: Working with Iframes Selenium's frame switching is stateful and error-prone. Playwright's `frameLocator` is scoped and stateless. **Selenium (Java)** ```java @Test void fillPaymentFormInIframe() { driver.get("https://myapp.com/checkout"); WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); // Switch into the payment iframe wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt( By.cssSelector("iframe#payment-frame") )); // Now inside the iframe context WebElement cardNumber = wait.until( ExpectedConditions.visibilityOfElementLocated(By.id("card-number")) ); cardNumber.sendKeys("4242424242424242"); driver.findElement(By.id("expiry")).sendKeys("12/28"); driver.findElement(By.id("cvc")).sendKeys("123"); // Switch back to main content before interacting with the page driver.switchTo().defaultContent(); driver.findElement(By.id("place-order")).click(); wait.until(ExpectedConditions.visibilityOfElementLocated( By.cssSelector(".confirmation-message") )); } ``` **Playwright (TypeScript)** ```typescript import { test, expect } from "@playwright/test" test("fill payment form in iframe", async ({ page }) => { await page.goto("/checkout") // No switching — just scope into the frame const paymentFrame = page.frameLocator("#payment-frame") await paymentFrame.getByLabel("Card number").fill("4242424242424242") await paymentFrame.getByLabel("Expiry").fill("12/28") await paymentFrame.getByLabel("CVC").fill("123") // No switching back — main page locators still work await page.getByRole("button", { name: "Place order" }).click() await expect(page.getByText("Order confirmed")).toBeVisible() }) ``` **Playwright (JavaScript)** ```javascript const { test, expect } = require("@playwright/test") test("fill payment form in iframe", async ({ page }) => { await page.goto("/checkout") const paymentFrame = page.frameLocator("#payment-frame") await paymentFrame.getByLabel("Card number").fill("4242424242424242") await paymentFrame.getByLabel("Expiry").fill("12/28") await paymentFrame.getByLabel("CVC").fill("123") await page.getByRole("button", { name: "Place order" }).click() await expect(page.getByText("Order confirmed")).toBeVisible() }) ``` **What changed**: No `switchTo().frame()`. No `switchTo().defaultContent()`. No risk of forgetting to switch back. Playwright's `frameLocator` scopes into the iframe without changing the driver's global state. You can interact with the main page and the iframe in any order. --- ### Example 4: Handling Popups and New Windows Selenium window handle management is notoriously fragile. Playwright makes it declarative. **Selenium (Java)** ```java @Test void handlePopupWindow() { driver.get("https://myapp.com/settings"); String originalWindow = driver.getWindowHandle(); WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); driver.findElement(By.linkText("Connect OAuth Provider")).click(); // Wait for new window to appear wait.until(ExpectedConditions.numberOfWindowsToBe(2)); // Find and switch to the new window for (String handle : driver.getWindowHandles()) { if (!handle.equals(originalWindow)) { driver.switchTo().window(handle); break; } } // Interact with the popup wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("authorize-btn"))); driver.findElement(By.id("authorize-btn")).click(); // Wait for popup to close and switch back wait.until(ExpectedConditions.numberOfWindowsToBe(1)); driver.switchTo().window(originalWindow); wait.until(ExpectedConditions.visibilityOfElementLocated( By.cssSelector(".connection-status.success") )); } ``` **Playwright (TypeScript)** ```typescript import { test, expect } from "@playwright/test" test("handle popup window", async ({ page }) => { await page.goto("/settings") // Register listener BEFORE the click that opens the popup const popupPromise = page.waitForEvent("popup") await page.getByRole("link", { name: "Connect OAuth Provider" }).click() const popup = await popupPromise // Interact with the popup — it's just another Page object await popup.getByRole("button", { name: "Authorize" }).click() // Popup closes automatically; verify result on original page await expect(page.getByText("Connected successfully")).toBeVisible() }) ``` **Playwright (JavaScript)** ```javascript const { test, expect } = require("@playwright/test") test("handle popup window", async ({ page }) => { await page.goto("/settings") const popupPromise = page.waitForEvent("popup") await page.getByRole("link", { name: "Connect OAuth Provider" }).click() const popup = await popupPromise await popup.getByRole("button", { name: "Authorize" }).click() await expect(page.getByText("Connected successfully")).toBeVisible() }) ``` **What changed**: No window handle iteration. No `switchTo()`. No tracking original vs new window. The popup is a `Page` object you interact with directly. When it closes, you just continue using the original `page`. The `waitForEvent('popup')` pattern is declarative and race-condition-free. --- ### Example 5: Drag and Drop with Actions Selenium's `Actions` class requires chaining and `perform()`. Playwright has direct methods. **Selenium (Java)** ```java @Test void dragAndDropTask() { driver.get("https://myapp.com/kanban"); WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); WebElement sourceCard = wait.until( ExpectedConditions.visibilityOfElementLocated( By.xpath("//div[@class='card' and contains(text(),'Fix login bug')]") ) ); WebElement targetColumn = driver.findElement( By.xpath("//div[@class='column' and .//h2[text()='Done']]") ); Actions actions = new Actions(driver); actions.dragAndDrop(sourceCard, targetColumn).perform(); // Verify the card moved WebElement doneColumn = driver.findElement( By.xpath("//div[@class='column' and .//h2[text()='Done']]") ); WebElement movedCard = doneColumn.findElement( By.xpath(".//div[@class='card' and contains(text(),'Fix login bug')]") ); Assertions.assertTrue(movedCard.isDisplayed()); } ``` **Playwright (TypeScript)** ```typescript import { test, expect } from "@playwright/test" test("drag and drop task to done column", async ({ page }) => { await page.goto("/kanban") const card = page.getByText("Fix login bug") const doneColumn = page.getByRole("heading", { name: "Done" }).locator("..") await card.dragTo(doneColumn) // Verify card is now inside the Done column await expect(doneColumn.getByText("Fix login bug")).toBeVisible() }) ``` **Playwright (JavaScript)** ```javascript const { test, expect } = require("@playwright/test") test("drag and drop task to done column", async ({ page }) => { await page.goto("/kanban") const card = page.getByText("Fix login bug") const doneColumn = page.getByRole("heading", { name: "Done" }).locator("..") await card.dragTo(doneColumn) await expect(doneColumn.getByText("Fix login bug")).toBeVisible() }) ``` **What changed**: No `Actions` class. No `perform()`. No XPath for finding elements. `dragTo()` is a single method call on a locator. The verification uses scoped locators instead of XPath ancestor traversal. ## Migration Steps A practical, ordered checklist for converting a Selenium suite to Playwright. ### Step 1: Set Up Playwright Alongside Selenium Do not rip out Selenium on day one. Run both in parallel. ```bash # Install Playwright in your existing project npm init playwright@latest # This creates: # - playwright.config.ts (or .js) # - tests/ directory # - package.json dependencies ``` Configure `baseURL` in `playwright.config` to match your Selenium target: **TypeScript** ```typescript // playwright.config.ts import { defineConfig } from "@playwright/test" export default defineConfig({ testDir: "./tests-playwright", // separate from Selenium tests use: { baseURL: "https://staging.myapp.com", trace: "on-first-retry", }, retries: process.env.CI ? 2 : 0, }) ``` **JavaScript** ```javascript // playwright.config.js const { defineConfig } = require("@playwright/test") module.exports = defineConfig({ testDir: "./tests-playwright", use: { baseURL: "https://staging.myapp.com", trace: "on-first-retry", }, retries: process.env.CI ? 2 : 0, }) ``` ### Step 2: Convert Page Objects First If you have Selenium page objects, convert them to Playwright page objects. The pattern is similar but simpler. **Selenium Page Object (Java)** ```java public class LoginPage { private WebDriver driver; private WebDriverWait wait; private By emailField = By.id("email"); private By passwordField = By.id("password"); private By loginButton = By.cssSelector("button[type='submit']"); private By errorMessage = By.cssSelector(".error-message"); public LoginPage(WebDriver driver) { this.driver = driver; this.wait = new WebDriverWait(driver, Duration.ofSeconds(10)); } public void login(String email, String password) { wait.until(ExpectedConditions.visibilityOfElementLocated(emailField)); driver.findElement(emailField).clear(); driver.findElement(emailField).sendKeys(email); driver.findElement(passwordField).clear(); driver.findElement(passwordField).sendKeys(password); driver.findElement(loginButton).click(); } public String getErrorMessage() { return wait.until( ExpectedConditions.visibilityOfElementLocated(errorMessage) ).getText(); } } ``` **Playwright Page Object (TypeScript)** ```typescript // page-objects/login-page.ts import { type Page, type Locator, expect } from "@playwright/test" export class LoginPage { private readonly emailField: Locator private readonly passwordField: Locator private readonly loginButton: Locator private readonly errorMessage: Locator constructor(private readonly page: Page) { this.emailField = page.getByLabel("Email") this.passwordField = page.getByLabel("Password") this.loginButton = page.getByRole("button", { name: "Sign In" }) this.errorMessage = page.getByRole("alert") } async login(email: string, password: string) { await this.emailField.fill(email) await this.passwordField.fill(password) await this.loginButton.click() } async expectError(message: string) { await expect(this.errorMessage).toHaveText(message) } } ``` **Playwright Page Object (JavaScript)** ```javascript // page-objects/login-page.js const { expect } = require("@playwright/test") class LoginPage { constructor(page) { this.page = page this.emailField = page.getByLabel("Email") this.passwordField = page.getByLabel("Password") this.loginButton = page.getByRole("button", { name: "Sign In" }) this.errorMessage = page.getByRole("alert") } async login(email, password) { await this.emailField.fill(email) await this.passwordField.fill(password) await this.loginButton.click() } async expectError(message) { await expect(this.errorMessage).toHaveText(message) } } module.exports = { LoginPage } ``` Key differences in page objects: - Locators are defined in the constructor, not as `By` objects -- they are lazy and never go stale - No `WebDriverWait` anywhere - No `clear()` before `fill()` - Assertions can live inside the page object (`expectError`) because they auto-retry ### Step 3: Convert Tests by Priority Start with your most valuable and most flaky tests. Convert them in order: 1. **Smoke tests** -- highest value, run on every deploy 2. **Flaky tests** -- Playwright's auto-waiting eliminates most flakiness 3. **Slow tests** -- Playwright's parallelism and faster protocol make these faster 4. **Everything else** -- bulk conversion of the remaining suite For each test, apply the API mapping table above. The mechanical translation is: 1. Remove all `WebDriverWait` and `ExpectedConditions` 2. Replace `findElement(By.xxx)` with semantic locators (`getByRole`, `getByLabel`, `getByText`) 3. Replace `sendKeys` with `fill` (or `pressSequentially` for character-by-character input) 4. Replace `Assert` / `assertEquals` with `expect(locator).toHaveText()` / `toBeVisible()` / etc. 5. Remove `setUp` and `tearDown` -- Playwright Test handles browser lifecycle 6. Remove `Thread.sleep()` / `time.sleep()` entirely ### Step 4: Replace Test Infrastructure | Selenium Infrastructure | Playwright Equivalent | | ------------------------- | -------------------------------------------------------------------------------------- | | Selenium Grid / Hub | `npx playwright test --shard=1/4` (built-in sharding) | | BrowserStack / SauceLabs | Often unnecessary; Playwright runs 3 browsers locally. Use for Safari on CI if needed. | | WebDriverManager | `npx playwright install` (one command, all browsers) | | TestNG XML suites | `playwright.config` `projects` array | | JUnit @Tag / pytest marks | `test.describe()` grouping + `--grep` filtering | | Allure / ExtentReports | Built-in HTML reporter (`npx playwright show-report`) + trace viewer | | Screenshot on failure | Built-in: `use: { screenshot: 'only-on-failure' }` | | Video recording | Built-in: `use: { video: 'on-first-retry' }` | ### Step 5: Remove Selenium Once all tests pass in Playwright and have run green in CI for at least two weeks: 1. Delete Selenium test files 2. Remove Selenium dependencies (`selenium-webdriver`, `chromedriver`, `geckodriver`) 3. Remove Selenium Grid infrastructure 4. Update CI pipelines to run only Playwright 5. Remove `setUp` / `tearDown` boilerplate classes ## Common Gotchas ### Gotcha 1: `findElement` Throws, `locator()` Does Not In Selenium, `driver.findElement(By.id("missing"))` throws `NoSuchElementException` immediately. In Playwright, `page.locator('#missing')` returns a locator object without querying the DOM. It only throws when you perform an action on it and the element does not appear within the timeout. ```typescript // This does NOT throw — locators are lazy const missing = page.locator("#does-not-exist") // This throws after timeout — because the action waits and the element never appears await missing.click() // TimeoutError after actionTimeout ``` **Impact**: If your Selenium tests use try-catch around `findElement` to check element existence, replace with assertions: ```typescript // Selenium pattern (do not replicate) // try { driver.findElement(By.id("error")); fail(); } catch (NoSuchElementException e) { /* expected */ } // Playwright equivalent await expect(page.locator("#error")).not.toBeVisible() ``` ### Gotcha 2: `sendKeys` Appends, `fill` Replaces Selenium's `sendKeys("text")` appends to the existing value. If the field already contains "hello" and you `sendKeys("world")`, you get "helloworld". Playwright's `fill("text")` clears the field first, then sets the value. You get "text". ```typescript // Selenium behavior: appends // element.sendKeys("world"); // field: "helloworld" // Playwright behavior: replaces await locator.fill("world") // field: "world" // To replicate Selenium's append behavior: await locator.pressSequentially("world") // types each character, appending to existing value ``` ### Gotcha 3: No StaleElementReferenceException In Selenium, storing a `WebElement` reference and using it after the DOM changes throws `StaleElementReferenceException`. This is the single most common source of Selenium test flakiness. ```java // Selenium — this can throw StaleElementReferenceException WebElement button = driver.findElement(By.id("submit")); // ... some action causes the DOM to re-render ... button.click(); // BOOM — StaleElementReferenceException ``` In Playwright, locators re-query the DOM on every action. Store locators freely. ```typescript // Playwright — this always works const button = page.getByRole("button", { name: "Submit" }) // ... some action causes the DOM to re-render ... await button.click() // Works — re-queries the DOM automatically ``` ### Gotcha 4: No Global Driver State Selenium has a single `driver` instance with global state: the current frame, the current window, implicit wait timeout. Calling `switchTo().frame()` changes state for all subsequent calls. Forgetting to `switchTo().defaultContent()` causes every following `findElement` to fail. Playwright has no global state. `page.frameLocator()` returns a scoped object. `context.pages()` gives you all pages. Nothing changes the "current" context. ```typescript // Selenium mental model: "Where am I now?" // driver.switchTo().frame("payment"); // I'm in the payment frame // driver.findElement(...); // This looks in the payment frame // driver.switchTo().defaultContent(); // Now I'm back in the main page // ... forget this line and everything breaks // Playwright mental model: "I always say exactly where I'm looking" const paymentFrame = page.frameLocator("#payment") await paymentFrame.getByLabel("Card").fill("4242...") // scoped to frame await page.getByRole("button", { name: "Pay" }).click() // main page — no switching ``` ### Gotcha 5: Assertions Must Be Awaited In Selenium (Java), assertions are synchronous: `assertEquals("Dashboard", heading.getText())`. In Playwright, web-first assertions are async and must be awaited: ```typescript // WRONG — assertion runs detached, test may pass before it resolves expect(page.getByRole("heading")).toHaveText("Dashboard") // missing await! // CORRECT await expect(page.getByRole("heading")).toHaveText("Dashboard") ``` Missing `await` is the number one Playwright beginner mistake. Your linter should flag this. Enable `@typescript-eslint/no-floating-promises` or the Playwright ESLint plugin. ### Gotcha 6: Parallel by Default Selenium tests typically run sequentially (one browser, one thread). Playwright Test runs test files in parallel by default. This means: - Tests must be isolated -- no shared state between test files - Each test gets its own browser context (fresh cookies, storage, session) - Database fixtures must not collide between parallel tests If your Selenium suite depends on execution order, you must fix that before migrating. See [core/test-organization.md](../core/test-organization.md) for isolation strategies. ## What's Better in Playwright These are not just syntax differences. These are capabilities that Selenium does not have. ### Auto-Waiting Everywhere Every action, every assertion, every navigation auto-waits. You write zero wait code. This alone eliminates 60-80% of Selenium test flakiness. ### Trace Viewer When a test fails in CI, Playwright captures a trace: screenshots at every step, DOM snapshots, network requests, console logs. Open it with `npx playwright show-report` and step through the exact failure. Selenium has nothing comparable. ```typescript // playwright.config.ts — enable traces on first retry export default defineConfig({ use: { trace: "on-first-retry", }, }) ``` ### Built-In Parallel Execution Playwright Test runs test files in parallel with zero configuration. No Selenium Grid. No TestNG parallel suite XML. No pytest-xdist. ```bash # Run all tests in parallel (default) npx playwright test # Shard across CI machines npx playwright test --shard=1/4 # machine 1 npx playwright test --shard=2/4 # machine 2 ``` ### Multiple Browsers, One API The same test runs on Chromium, Firefox, and WebKit without code changes. Configure in `playwright.config`: ```typescript export default defineConfig({ projects: [ { name: "chromium", use: { ...devices["Desktop Chrome"] } }, { name: "firefox", use: { ...devices["Desktop Firefox"] } }, { name: "webkit", use: { ...devices["Desktop Safari"] } }, ], }) ``` Selenium requires separate driver binaries and often browser-specific workarounds. ### Network Interception Playwright can intercept, modify, and mock network requests natively. Selenium cannot. ```typescript // Mock an API response — impossible in Selenium await page.route("**/api/users", (route) => { route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([{ name: "Mock User" }]), }) }) ``` ### Browser Context Isolation Each test gets a fresh browser context (like an incognito window). Cookies, localStorage, and sessions are isolated between tests without restarting the browser. Selenium requires `driver.quit()` and `new ChromeDriver()` for true isolation. ### Test Fixtures Playwright's fixture system provides dependency injection for test setup and teardown. Fixtures guarantee cleanup even if a test crashes. Selenium relies on `@BeforeEach` / `@AfterEach` which skip teardown on hard failures. ```typescript // Custom fixture for authenticated user — reusable across all tests import { test as base } from "@playwright/test" export const test = base.extend({ authenticatedPage: async ({ page }, use) => { await page.goto("/login") await page.getByLabel("Email").fill("admin@example.com") await page.getByLabel("Password").fill("password") await page.getByRole("button", { name: "Sign In" }).click() await page.waitForURL("/dashboard") await use(page) // Cleanup runs automatically, even on crash }, }) ``` ### Codegen Generate tests by recording browser interactions: ```bash npx playwright codegen https://myapp.com ``` This opens a browser and records your clicks, fills, and navigations as Playwright test code. No equivalent exists in Selenium. ### UI Mode Debug tests visually with time-travel: ```bash npx playwright test --ui ``` Step forward and backward through each action, see the DOM state, network requests, and console logs at every point. Selenium's closest equivalent is manual `Thread.sleep()` and screenshot debugging. ## Related - [core/locators.md](../core/locators.md) -- locator strategy priority and patterns - [core/assertions-and-waiting.md](../core/assertions-and-waiting.md) -- auto-waiting and web-first assertions in depth - [core/configuration.md](../core/configuration.md) -- setting up `playwright.config` - [core/page-object-model.md](../core/page-object-model.md) -- page object patterns for Playwright - [core/fixtures-and-hooks.md](../core/fixtures-and-hooks.md) -- fixtures system for test setup and isolation - [core/test-organization.md](../core/test-organization.md) -- organizing tests for parallel execution - [migration/from-cypress.md](from-cypress.md) -- migrating from Cypress instead