mirror of
https://github.com/zebbern/claude-code-guide.git
synced 2026-09-19 05:23:28 +08:00
978 lines
45 KiB
Markdown
978 lines
45 KiB
Markdown
# 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<WebElement> 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
|