Playwright is an open-source browser automation framework developed by Microsoft that enables developers to write reliable end-to-end tests for modern web applications. Released in 2020, Playwright has quickly become one of the most popular alternatives to Selenium and Puppeteer.
Definition: What is Playwright?#
Playwright is a Node.js library that provides a unified API (application programming interface) to automate Chromium, Firefox, and WebKit browsers with a single codebase. Unlike traditional testing tools, Playwright was built from the ground up to handle the challenges of modern web applications, including single-page applications (SPAs), dynamic content, and complex user interactions.
Key Characteristics#
- Cross-browser support: Test on Chromium, Firefox, and WebKit (Safari)
- Auto-wait mechanism: Automatically waits for elements to be ready before performing actions
- Network interception: Mock and modify network requests
- Multi-context: Run tests in parallel across multiple browser contexts
- Modern architecture: Built starting in 2020 by engineers from the original Puppeteer team, who left Google for Microsoft to fix limitations they couldn't address in Puppeteer's Chromium-only design
How Playwright Talks to the Browser#
Playwright does not simulate keystrokes and clicks from outside the browser. Your test code acts as a client to a Playwright driver process, and that driver speaks to each browser engine through a low-level debugging protocol, the same kind of channel a browser's own developer tools use.
For Chromium, that protocol is the Chrome DevTools Protocol (CDP). Firefox and WebKit don't natively speak CDP, so the Playwright team maintains its own patched builds of both engines with an equivalent protocol layer added: a custom Firefox protocol (named Juggler) and a WebKit-specific one. That direct patch-and-maintain relationship with all three rendering engines, not just Chromium, is why the same API produces consistent behavior across browsers instead of three separate integrations bolted together. For the full setup and config walkthrough, see our complete Playwright guide.
Core Features of Playwright#
1. True Cross-Browser Testing#
Playwright supports all major browser engines:
import { chromium, firefox, webkit } from "@playwright/test";// Test on Chromiumconst browser = await chromium.launch();// Test on Firefoxconst browser = await firefox.launch();// Test on WebKit (Safari)const browser = await webkit.launch();
import { chromium, firefox, webkit } from "@playwright/test";// Test on Chromiumconst browser = await chromium.launch();// Test on Firefoxconst browser = await firefox.launch();// Test on WebKit (Safari)const browser = await webkit.launch();
This means you can write tests once and run them across all browsers, ensuring consistent behavior for all your users.
2. Automatic Waiting#
One of Playwright's most powerful features is its built-in auto-wait mechanism. You don't need to add manual sleep() or wait() calls:
// Playwright automatically waits for the button to be:// - Attached to DOM (document object model)// - Visible// - Stable (not animating)// - Enabledawait page.getByTestId("submit").click();
// Playwright automatically waits for the button to be:// - Attached to DOM (document object model)// - Visible// - Stable (not animating)// - Enabledawait page.getByTestId("submit").click();
This eliminates the most common source of flaky tests: race conditions.
3. Network Interception and Mocking#
Playwright allows you to intercept and modify network requests, making it easy to test edge cases:
// Mock API responsesawait page.route("**/api/users", (route) => {route.fulfill({status: 200,body: JSON.stringify([{ id: 1, name: "Test User" }]),});});
// Mock API responsesawait page.route("**/api/users", (route) => {route.fulfill({status: 200,body: JSON.stringify([{ id: 1, name: "Test User" }]),});});
4. Mobile Emulation#
Test your responsive designs by emulating mobile devices:
const iPhone = devices["iPhone 13"];const context = await browser.newContext({...iPhone,});
const iPhone = devices["iPhone 13"];const context = await browser.newContext({...iPhone,});
5. Screenshots and Videos#
Capture visual evidence of test failures:
// Take a screenshotawait page.screenshot({ path: "screenshot.png" });// Record videoconst context = await browser.newContext({recordVideo: { dir: "videos/" },});
// Take a screenshotawait page.screenshot({ path: "screenshot.png" });// Record videoconst context = await browser.newContext({recordVideo: { dir: "videos/" },});
When to Use Playwright#
Playwright is ideal for:
1. End-to-End Testing#
Testing complete user workflows across multiple pages and interactions.
Example use case: Testing an e-commerce checkout flow from product selection to payment confirmation.
2. Cross-Browser Compatibility Testing#
Ensuring your application works consistently across Chrome, Firefox, and Safari.
Example use case: Verifying that a complex dashboard renders correctly on all browsers.
3. API testing#
Testing backend APIs alongside UI tests.
const response = await page.request.get("https://api.example.com/users");expect(response.status()).toBe(200);
const response = await page.request.get("https://api.example.com/users");expect(response.status()).toBe(200);
4. Visual Regression Testing#
Detecting unintended visual changes in your application.
Example use case: Ensuring a design system update doesn't break existing components.
Playwright vs Alternatives#
Playwright vs Selenium#
| Feature | Playwright | Selenium |
|---|---|---|
| Setup | Simple, batteries-included | Requires driver management |
| Auto-wait | Built-in | Manual waits required |
| Speed | Faster (direct browser protocol) | Slower (WebDriver protocol) |
| Modern features | Network mocking, tracing | Limited |
| Browser support | Chromium, Firefox, WebKit | All browsers + mobile |
When to choose Playwright: Modern web apps, need speed and reliability
When to choose Selenium: Legacy browser support (IE11), existing Selenium infrastructure
Playwright vs Puppeteer#
| Feature | Playwright | Puppeteer |
|---|---|---|
| Browser support | Chromium, Firefox, WebKit | Chromium only |
| Maintained by | Microsoft | |
| Test runner | Built-in (@playwright/test) | Requires third-party (Jest, Mocha) |
| Auto-wait | More robust | Basic |
When to choose Playwright: Need cross-browser support, prefer all-in-one solution
When to choose Puppeteer: Chrome-only automation, existing Puppeteer expertise
Playwright vs Cypress#
| Feature | Playwright | Cypress |
|---|---|---|
| Architecture | Runs outside browser | Runs inside browser |
| Multi-tab support | Yes | Limited |
| Network control | Full control | Good, but limited |
| Speed | Very fast | Fast |
| Learning curve | Moderate | Easy |
When to choose Playwright: Complex scenarios, need full browser control
When to choose Cypress: Simpler projects, prefer developer-friendly DX
Limitations to Know About#
Playwright automates web browsers, not native mobile or desktop applications; testing a native iOS/Android app needs a tool like Appium instead. The three browser binaries it downloads on install add real weight to a CI image, which matters if you're optimizing container size or cold-start time. Its WebKit is Playwright's own build of the engine, close to Safari but not Safari itself, so it approximates Safari behavior well without being a perfect substitute for testing on an actual macOS or iOS device. And the open-source core has no built-in test dashboard, historical trend view, or cross-run flaky-test detection; teams that want those add a paid layer or their own tooling on top.
Getting Started with Playwright#
Installation#
npm init playwright@latest
npm init playwright@latest
This command:
- Installs Playwright and browsers
- Creates example tests
- Sets up configuration
- Adds npm scripts
Your First Test#
import { test, expect } from "@playwright/test";test("homepage has title", async ({ page }) => {await page.goto("https://playwright.dev/");// Expect a title "to contain" a substringawait expect(page).toHaveTitle(/Playwright/);});
import { test, expect } from "@playwright/test";test("homepage has title", async ({ page }) => {await page.goto("https://playwright.dev/");// Expect a title "to contain" a substringawait expect(page).toHaveTitle(/Playwright/);});
Running Tests#
# Run all testsnpx playwright test# Run in headed mode (see the browser)npx playwright test --headed# Run specific test filenpx playwright test tests/example.spec.ts
# Run all testsnpx playwright test# Run in headed mode (see the browser)npx playwright test --headed# Run specific test filenpx playwright test tests/example.spec.ts
Real-World Use Cases#
1. E-commerce Testing#
test("complete purchase flow", async ({ page }) => {await page.goto("/products");await page.getByTestId("add-to-cart").click();await page.getByTestId("checkout").click();await page.getByTestId("email").fill(process.env.TEST_EMAIL ?? "");await page.getByTestId("card").fill(process.env.TEST_CARD ?? "");await page.getByTestId("place-order").click();await expect(page.getByTestId("success-message")).toBeVisible();});
test("complete purchase flow", async ({ page }) => {await page.goto("/products");await page.getByTestId("add-to-cart").click();await page.getByTestId("checkout").click();await page.getByTestId("email").fill(process.env.TEST_EMAIL ?? "");await page.getByTestId("card").fill(process.env.TEST_CARD ?? "");await page.getByTestId("place-order").click();await expect(page.getByTestId("success-message")).toBeVisible();});
2. Form Validation Testing#
test("validates email format", async ({ page }) => {await page.goto("/signup");await page.getByTestId("email").fill("invalid-email");await page.getByTestId("submit").click();await expect(page.getByTestId("error")).toContainText("Invalid email");});
test("validates email format", async ({ page }) => {await page.goto("/signup");await page.getByTestId("email").fill("invalid-email");await page.getByTestId("submit").click();await expect(page.getByTestId("error")).toContainText("Invalid email");});
3. Authentication Testing#
test("login with valid credentials", async ({ page }) => {await page.goto("/login");await page.getByTestId("username").fill(process.env.TEST_USERNAME ?? "");await page.getByTestId("password").fill(process.env.TEST_PASSWORD ?? "");await page.getByTestId("submit").click();await expect(page).toHaveURL("/dashboard");});
test("login with valid credentials", async ({ page }) => {await page.goto("/login");await page.getByTestId("username").fill(process.env.TEST_USERNAME ?? "");await page.getByTestId("password").fill(process.env.TEST_PASSWORD ?? "");await page.getByTestId("submit").click();await expect(page).toHaveURL("/dashboard");});
Best Practices#
- Use
data-testidandgetByTestIdfor stable, maintainable selectors. In your app, add test IDs to key elements:
<input data-testid="username" type="text" /><input data-testid="password" type="password" /><button data-testid="submit" type="submit">Sign in</button>
<input data-testid="username" type="text" /><input data-testid="password" type="password" /><button data-testid="submit" type="submit">Sign in</button>
In Playwright, use the method:
await page.getByTestId("username").fill(username);await page.getByTestId("password").fill(password);await page.getByTestId("submit").click();
await page.getByTestId("username").fill(username);await page.getByTestId("password").fill(password);await page.getByTestId("submit").click();
- Leverage auto-waiting instead of manual waits:
// Goodawait page.click("button");// Avoidawait page.waitForTimeout(1000);await page.click("button");
// Goodawait page.click("button");// Avoidawait page.waitForTimeout(1000);await page.click("button");
- Run tests in parallel for speed:
// playwright.config.tsexport default {workers: 4, // Run 4 tests in parallel};
// playwright.config.tsexport default {workers: 4, // Run 4 tests in parallel};
Conclusion#
Playwright's cross-browser support and auto-wait mechanism make it a strong choice for modern web testing. For more depth, see our complete Playwright guide. Writing and maintaining that Playwright suite by hand is still real work as your app changes; ObserveOne's Autopilot generates and self-heals Playwright tests from a URL, so the suite keeps up with the app instead of falling behind it.