playwright
Browser automation from Microsoft that drives Chromium, Firefox, and WebKit with one API. The playwright package is the library for scripts (scraping, screenshots, PDFs); the companion @playwright/test package adds a full test runner with parallelism, auto-waiting locators, retrying assertions, and trace capture. Each test runs in a fresh browser context, so state never bleeds between tests. It has become the default choice for end-to-end testing in the JavaScript world, and Microsoft now ships an MCP server and CLI on top of it for AI agents.
The current default for browser testing and automation, and it earns that spot: the API is well designed and the tooling around failures (traces, UI mode) is best in class. Budget for the browser-binary weight in CI, and do not reach for it when a plain unit or API test would do.
Use it if
- You are writing end-to-end tests and want auto-waiting and web-first assertions instead of hand-rolled sleeps and flaky waits
- You need real cross-engine coverage: Chromium, Firefox, and WebKit all run headless on Linux, macOS, and Windows
- You are scraping or automating sites that need a real browser: JS rendering, login flows, network interception, PDF generation
- You want built-in test isolation, parallel execution, and trace/video artifacts on failure without wiring up extra tooling
- You are building browser tooling for AI agents; the official MCP server and CLI sit directly on this library
- You only test APIs or pure logic: a browser test suite is the slowest, most infrastructure-heavy kind of test you can add, so keep it for real user flows
- Disk and CI time are tight: playwright downloads its own browser builds (hundreds of MB) on install, and every version bump wants fresh binaries, which makes CI caching a recurring chore
- You need to test real Safari on iOS: WebKit builds approximate Safari but are not Safari, and there is no real mobile device execution, only viewport and user-agent emulation
- Your team is settled on Cypress and the suite works: the migration cost is real and the day-to-day payoff for an existing green suite is small
- You want to automate at scale against sites with serious anti-bot systems; stock Playwright is detectable and stealth is not a supported use case
Setup reality
npm init playwright@latest scaffolds config, an example test, and a CI file, which is genuinely smooth. The catch is browsers: npx playwright install pulls custom Chromium, Firefox, and WebKit builds that live outside node_modules, so CI needs the install step (plus system deps via install --with-deps on Linux) or the official Docker image, and caching them across builds is your problem. Browser builds are version-locked to the library, so upgrades re-download everything. Also know the split: the playwright package has no test runner; for tests you install @playwright/test and should not mix the two in one project.
Patterns
Write and run a basic testwrite-first-test
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/');
await expect(page).toHaveTitle(/Playwright/);
});Tests need @playwright/test, not the playwright package; run with npx playwright test.
Find elements with user-facing locatorslocate-elements
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('Email').fill('ada@example.com');
await page.getByPlaceholder('Search...').fill('printers');
await expect(page.getByTestId('login-form')).toBeVisible();Prefer getByRole over CSS selectors; locators auto-wait and retry, so no manual waitForSelector.
Log in once and reuse the session across testsreuse-auth-state
// in a setup test, after logging in:
await page.context().storageState({ path: 'auth.json' });
// in your test file:
test.use({ storageState: 'auth.json' });Saves cookies and localStorage only; sessionStorage and in-memory tokens do not survive.
Mock an API responsemock-network
await page.route('**/api/users', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Ada' }]),
})
);
await page.goto('/users');Register the route before the navigation that triggers the request, or the real call goes through.
Block images to speed up scrapingblock-resources
await page.route('**/*.{png,jpg,jpeg,webp}', (route) => route.abort());
await page.goto('https://example.com');Blocking assets cuts load time a lot, but some sites lazy-load content based on image events.
Take a screenshot from a scripttake-screenshot
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://playwright.dev/');
await page.screenshot({ path: 'shot.png', fullPage: true });
await browser.close();Always close the browser in scripts; orphaned headless processes pile up fast on servers.
Render a page to PDFgenerate-pdf
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.pdf({ path: 'page.pdf', format: 'A4' });
await browser.close();page.pdf works in headless Chromium only, not Firefox or WebKit.
Emulate a mobile deviceemulate-mobile
import { chromium, devices } from 'playwright';
const browser = await chromium.launch();
const context = await browser.newContext(devices['iPhone 15']);
const page = await context.newPage();
await page.goto('https://playwright.dev/');This emulates viewport, user agent, and touch; it is not a real iOS Safari engine.
Wait for a specific network responsewait-for-response
const responsePromise = page.waitForResponse('**/api/orders');
await page.getByRole('button', { name: 'Load orders' }).click();
const response = await responsePromise;
const data = await response.json();Start waitForResponse before the click; awaiting it only afterwards can miss a fast response.
Capture traces for failed CI runstrace-on-failure
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
retries: process.env.CI ? 2 : 0,
use: { trace: 'on-first-retry' },
});Open the resulting zip with npx playwright show-trace; it replays every action, DOM snapshot, and network call.
Debug a failing test interactivelydebug-tests
npx playwright test --ui # watch mode with time travel
npx playwright test --debug # headed with inspector
npx playwright codegen https://example.com # record actions as codeUI mode is usually faster than sprinkling page.pause() calls through the test.
Install browsers in CIinstall-browsers-ci
npm ci
npx playwright install --with-deps chromiumInstall only the browsers you test to save time and disk; binaries are version-locked, so re-run this after every playwright upgrade.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| cypress | npm | You want an interactive in-browser test-writing experience and accept Chromium-family plus Firefox coverage |
| puppeteer | npm | Chrome-only automation or scraping where you do not need a test runner or cross-engine support |
| selenium-webdriver | npm | You must run against real vendor browser installs or a Selenium Grid you already operate |