cheat sheet

@playwright/test

Package-level reference for @playwright/test and playwright on npm — install variants, browser-binary download, fixtures, traces, and alternatives.

@playwright/test

What it is

Playwright is Microsoft's cross-browser end-to-end testing framework. There are two npm packages:

  • @playwright/test — the test runner bundle. Use this for E2E tests. It re-exports the browser API and adds fixtures, parallelism, retries, reporters, and the trace viewer.
  • playwright — the core browser-automation library (no test runner). Use this when you embed browser automation inside a non-test runtime (scraping, screenshot services, AI agents driving a browser).

For test work, always pick @playwright/test. It controls real Chromium, Firefox, and WebKit (Safari) builds — including mobile-emulation profiles — and produces a single trace per failed test that captures DOM, network, console, and screenshots for time-travel debugging.

Install

bash
# Scaffold a new test project (recommended)
npm init playwright@latest
# or
pnpm create playwright

Output: writes playwright.config.ts, tests/example.spec.ts, installs @playwright/test, and runs playwright install to download browser binaries.

bash
# Add to an existing project
npm install -D @playwright/test
pnpm add -D @playwright/test
yarn add -D @playwright/test
bun add -d @playwright/test

# Then download the browsers (~250 MB)
npx playwright install
npx playwright install --with-deps    # Linux: also install required system libs
npx playwright install chromium       # single browser only

Output: browsers extracted to ~/.cache/ms-playwright/ (Linux/macOS) or %USERPROFILE%\AppData\Local\ms-playwright\ (Windows).

bash
# The core library (no test runner — for agents, scrapers, etc.)
npm install playwright

Output: exposes chromium, firefox, webkit — no test/expect/fixtures.

bash
# Day-to-day
npx playwright test                     # run all tests
npx playwright test --ui                # interactive UI mode
npx playwright test --headed            # show the browser
npx playwright show-report              # open the last HTML report
npx playwright codegen example.com      # record actions → generate selectors

Output: test results to stdout + HTML report at playwright-report/index.html (open with show-report).

Versioning & Node support

  • Current major line is 1.x — Playwright has stayed on a single major line since 1.0 (early 2020). Breaking changes ship in minor bumps (1.40, 1.45, …) with deprecation cycles.
  • Requires Node 18+ for the recent releases; the floor moves up every few minors.
  • Both @playwright/test and playwright ship CJS + ESM.
  • Always a dev dependency for tests. The browser binaries are external — npx playwright install is a separate step on a fresh machine and in CI.
  • The npm package version and the browser-binary version are locked: playwright@1.46.0 always pulls a specific Chromium build. Upgrading one without the other is the most common CI break.

Package metadata

  • Maintainer: Microsoft (Playwright team)
  • Project home: github.com/microsoft/playwright
  • Docs: playwright.dev
  • npm (test runner): npmjs.com/package/@playwright/test
  • npm (core lib): npmjs.com/package/playwright
  • License: Apache-2.0
  • First released: January 2020
  • Downloads: millions per week — now the leading new E2E choice over Cypress.

Peer dependencies & extras

No formal npm peers — the "extras" are external binaries and side-cars:

SurfaceNotes
Browser binariesInstalled via npx playwright install to ~/.cache/ms-playwright/. ~250 MB for all three; ~80 MB for just Chromium. Pin in CI via Docker (mcr.microsoft.com/playwright).
@playwright/experimental-ct-react / …-vue / …-svelteComponent testing — mount and test components without a full browser app shell. Marked experimental but widely used.
playwright-coreThe minimal driver — pulled in as a dep by both playwright and @playwright/test. Don't install directly.
playwright-chromium / playwright-firefox / playwright-webkitSingle-browser bundles. Older pattern — most projects use the main package and pick browsers in config.
axe-playwright / @axe-core/playwrighta11y testing fixtures.
mcr.microsoft.com/playwright:vX.Y.Z-nobleOfficial Docker image — matches the npm release. The canonical way to run on CI without flaky binary downloads.

Alternatives

ToolTrade-off
cypressMature, friendly DX, time-travel debugger. Limited to Chromium-family browsers (WebKit experimental). Same-tab execution model is restrictive (no cross-origin without workarounds). Playwright is the default new choice.
puppeteerGoogle's Chromium-only library. Now mostly used for scripted automation (PDF generation, scraping). Playwright is a superset for testing.
selenium-webdriverThe grandfather. W3C standard, polyglot. Slower, flakier, much more code to write. Use when you need W3C compatibility or a non-Node language.
webdriverioSelenium-on-rails with a friendly test DSL. Supports both WebDriver and Chrome DevTools Protocol. Smaller user base than Playwright today.
testcafeNo external driver — runs tests via a proxy in any browser. Less code to wire up; slower and less precise than Playwright.
nightwatchSelenium-based, declarative. Niche today.

Common gotchas

  1. Browser-binary version locks. @playwright/test@1.46.0 only knows how to drive the Chromium build it downloads. Upgrading the npm package without re-running npx playwright install in CI causes "Executable doesn't exist at …" failures. The fix in CI is to derive the install version from package.json and cache by that key.
  2. test.describe vs test.beforeEach scoping. Hooks attached at the file top level apply to every describe block in that file. Hooks inside a describe only apply to its tests. Moving a hook out of a block silently changes its scope.
  3. page.goto waiting strategies. The default is load, which fires when the page's load event resolves — often before your SPA is ready. Pass { waitUntil: "networkidle" } or assert a visible element afterward; relying on load causes flaky waits.
  4. Auto-waiting is real. Playwright's locators wait up to actionTimeout for the element to be visible, enabled, and stable. Adding await page.waitForTimeout(500) "just in case" is an anti-pattern — it hides real races and slows CI.
  5. Trace size on CI. trace: "on" captures every test (~5–50 MB each). CI runs of hundreds of tests balloon. Use trace: "retain-on-failure" or trace: "on-first-retry" for prod CI; reserve on for local debugging.
  6. request context is separate from the browser. page.request and the top-level request fixture share auth state with the page; an independently-created apiRequestContext does not. Use the page-bound one when you want a request to inherit cookies.
  7. Globs in testMatch are repo-relative, not config-relative. Defining testMatch: "tests/**/*.spec.ts" in a config nested inside a monorepo workspace still resolves from the config-file directory — but testDir doesn't compose with it the way you'd think. Use one or the other, not both, to avoid empty test sets.

Real-world recipes

The patterns that come up on every realistic E2E suite — auth state reuse, network mocking, projects, and trace debugging.

Auth state — log in once, reuse everywhere

Logging in via UI for every test is slow and brittle. Run a setup project that logs in once, saves storage state, then have every other test load it.

typescript
// auth.setup.ts
import { test as setup } from "@playwright/test";

setup("authenticate", async ({ page }) => {
  await page.goto("https://example.com/login");
  await page.getByLabel("Email").fill("alice@example.com");
  await page.getByLabel("Password").fill(process.env.TEST_PASSWORD!);
  await page.getByRole("button", { name: "Log in" }).click();
  await page.waitForURL("**/dashboard");
  await page.context().storageState({ path: "playwright/.auth/user.json" });
});
typescript
// playwright.config.ts
export default defineConfig({
  projects: [
    { name: "setup", testMatch: /.*\.setup\.ts/ },
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"], storageState: "playwright/.auth/user.json" },
      dependencies: ["setup"],
    },
  ],
});

Add playwright/.auth/ to .gitignore.

Network mocking via page.route

typescript
import { test, expect } from "@playwright/test";

test("renders mock users", async ({ page }) => {
  await page.route("**/api/users", (route) =>
    route.fulfill({ json: [{ id: 1, name: "Alice Dev" }] })
  );

  await page.goto("/users");
  await expect(page.getByText("Alice Dev")).toBeVisible();
});

For more elaborate mocks, MSW + Playwright integration provides handler-based interception that mirrors what the unit tests use.

Parallel sharding in CI

yaml
# .github/workflows/e2e.yml
strategy:
  fail-fast: false
  matrix:
    shard: [1/4, 2/4, 3/4, 4/4]
steps:
  - uses: actions/setup-node@v4
  - run: npm ci
  - run: npx playwright install --with-deps chromium
  - run: npx playwright test --shard=${{ matrix.shard }}
  - uses: actions/upload-artifact@v4
    if: always()
    with:
      name: trace-${{ matrix.shard }}
      path: test-results/

Each shard runs ~25% of tests in parallel; total wall-clock = slowest shard.

Component testing

typescript
// Counter.spec.tsx — experimental component mode
import { test, expect } from "@playwright/experimental-ct-react";
import { Counter } from "./Counter";

test("increments", async ({ mount }) => {
  const component = await mount(<Counter initial={5} />);
  await component.getByRole("button", { name: "+" }).click();
  await expect(component.getByText("6")).toBeVisible();
});

Faster than full-app E2E but slower than Vitest + @testing-library — use for components with real browser behaviour (drag-and-drop, IntersectionObserver, Canvas).

Trace debugging

bash
npx playwright show-trace test-results/checkout-failed/trace.zip

Output: Launches the trace viewer UI in a new browser window (no terminal output beyond a brief listening-on-port line).

Opens a time-traveller showing every action, network request, DOM snapshot, and console log. The single most valuable Playwright feature for flaky-test triage.

Production deployment

Playwright is a dev/CI tool — but the binary management is the deployment story.

Docker image — version pinning

dockerfile
FROM mcr.microsoft.com/playwright:v1.50.0-noble
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npx", "playwright", "test"]

Match the npm package version exactly. Mismatched versions = "Executable doesn't exist at /ms-playwright/chromium-…/chrome".

Browser binary cache in CI

yaml
- name: Cache browsers
  uses: actions/cache@v4
  with:
    path: ~/.cache/ms-playwright
    key: pw-${{ runner.os }}-${{ hashFiles('package-lock.json') }}

- name: Install browsers if not cached
  run: npx playwright install --with-deps chromium

Cache hit saves ~30-90s per CI run.

Trace + video upload

typescript
// playwright.config.ts
export default defineConfig({
  use: {
    trace: "retain-on-failure",
    video: "retain-on-failure",
    screenshot: "only-on-failure",
  },
});

Then upload test-results/ and playwright-report/ as CI artefacts. Failure analysis becomes trace-driven, not log-spelunking.

Performance tuning

E2E tests are slow by nature — every keystroke goes through a real browser. The knobs:

Fixture reuse across tests

test.use({ storageState }) reuses one logged-in session; faster than logging in per test.

Parallelism

Default: one worker per CPU. Tune in config:

typescript
workers: process.env.CI ? "50%" : "100%",
fullyParallel: true,

fullyParallel: true runs files AND tests-in-file in parallel — biggest single speedup.

actionTimeout and navigationTimeout

typescript
use: { actionTimeout: 10_000, navigationTimeout: 30_000 }

Setting too low → flaky; too high → tests hang on real failures. Tune per-environment (CI is slower than local).

Avoid waitForTimeout

page.waitForTimeout(500) is the universal anti-pattern. Replace with assertions:

typescript
// Bad
await page.click("#submit");
await page.waitForTimeout(500);
expect(await page.url()).toContain("/dashboard");

// Good — Playwright auto-waits for navigation
await page.click("#submit");
await expect(page).toHaveURL(/\/dashboard/);

Trace recording cost

trace: "on" adds ~15-30% test runtime. Use retain-on-failure or on-first-retry for prod CI; on only for local debugging.

ESM/CJS interop & bundling

Playwright ships dual ESM + CJS via conditional exports. The test file's module system is determined by tsconfig.json (module: "esnext") or package.json ("type": "module").

typescript
// ESM
import { test, expect } from "@playwright/test";

// CJS (still supported)
const { test, expect } = require("@playwright/test");

The config file follows the same rules — playwright.config.ts works in both modes via Playwright's built-in TS handling.

Version migration guide

Playwright stays on 1.x since 2020 — breaking changes ship in minor bumps with deprecation cycles.

HighlightVersion
expect matcher API stabilised1.18
UI mode1.32
Component testing GA1.39
storageState on context1.45
test.step.skip1.50

Common pitfalls when upgrading

  • Browser binary mismatch. Every playwright minor pulls a different Chromium build. npm update without npx playwright install produces "Executable doesn't exist".
  • locator.evaluate deprecation — older page.$eval / page.$$eval still work but are discouraged. Migrate to locator-based assertions for auto-wait.
  • API mode changesrequest fixture got a separate playwright.config.ts use.baseURL for API tests in 1.40+. Old code that constructed URLs manually may need updating.

Security considerations

  1. Auth state files leak credentials. playwright/.auth/user.json contains session cookies. .gitignore it; never commit. In CI, generate fresh state per run.
  2. Traces capture secrets. trace: "on" records every network request body — including auth tokens. Filter via await page.route to strip sensitive headers, or use trace: "retain-on-failure" to avoid keeping passing-test traces.
  3. request context shares state with the browser if used via page.request. An independently-created apiRequestContext does not — use the right one for the threat model.
  4. --debug opens an Inspector with full DOM/network access. Never enable in production CI.
  5. Browser binaries downloaded over HTTP — Playwright fetches from playwright.azureedge.net over HTTPS, but supply chain attacks are theoretical. Pin Docker image versions (mcr.microsoft.com/playwright:v1.50.0-noble) for reproducible installs.

Testing strategies

Test pyramid

  • Unit (Vitest) — function-level, 100s-1000s of tests, milliseconds each.
  • Integration (Vitest + jsdom) — component-level, 10s-100s of tests, seconds total.
  • Component (Playwright CT) — component-level in real browser, 10s of tests.
  • E2E (Playwright) — full user flows, 10s of tests, minutes total.

Put 80% of effort below the E2E layer; reserve Playwright for high-confidence user journeys (sign-up, checkout, primary nav).

Visual regression

typescript
import { test, expect } from "@playwright/test";

test("home page visual", async ({ page }) => {
  await page.goto("/");
  await expect(page).toHaveScreenshot("home.png", { maxDiffPixelRatio: 0.01 });
});

First run writes the baseline; subsequent runs diff. Baselines per browser × OS — store under tests/__screenshots__/<test>-<browser>-<os>.png.

Multi-browser projects

typescript
projects: [
  { name: "chromium", use: devices["Desktop Chrome"] },
  { name: "firefox", use: devices["Desktop Firefox"] },
  { name: "webkit", use: devices["Desktop Safari"] },
  { name: "mobile-chrome", use: devices["Pixel 5"] },
  { name: "mobile-safari", use: devices["iPhone 14"] },
],

npx playwright test --project=chromium to scope.

Configuration patterns

Base URL + environment switching

typescript
export default defineConfig({
  use: {
    baseURL: process.env.PW_BASE_URL ?? "http://localhost:3000",
    trace: "retain-on-failure",
  },
  webServer: {
    command: "npm run dev",
    url: "http://localhost:3000",
    reuseExistingServer: !process.env.CI,
  },
});

webServer boots your app before running tests; in CI it must start fresh, locally it reuses if already up.

Per-project trace settings

typescript
projects: [
  { name: "smoke", use: { trace: "on" }, retries: 0 },
  { name: "full", use: { trace: "retain-on-failure" }, retries: 2 },
],

Troubleshooting common errors

  • browserType.launch: Executable doesn't exist at /ms-playwright/... — run npx playwright install. Binary mismatch with npm version.
  • Timeout 30000ms exceeded — element selector didn't resolve. Open trace; check whether the element ever rendered, or whether a previous step failed silently.
  • Page closed mid-test — usually a navigation triggered an alert or popup. Add page.on("dialog", d => d.accept()).
  • storageState is invalid — auth setup project failed; check for hidden cookies/CSRF tokens that weren't captured.
  • Tests pass locally, flake in CI — almost always (a) CI is slower → bump actionTimeout, or (b) shared state → re-check fixture scopes.

Ecosystem integrations

ToolWhat it adds
@axe-core/playwrightAccessibility scanning fixtures
@playwright/experimental-ct-react / …-vue / …-svelteComponent testing
mswService-worker network mocking (preferred over per-test page.route)
allure-playwrightAllure-formatted reports
playwright-test-coverageV8 coverage from Playwright runs
Microsoft VS Code extensionTest browser, codegen, trace viewer inside the IDE
playwright-bddCucumber-style BDD on top of Playwright
eslint-plugin-playwrightLint rules — no-conditional-in-test, no-skipped-test, etc.

When NOT to use this

  • Pure unit tests. Vitest is 100× faster. Reserve Playwright for flows that need a real browser.
  • API-only tests. Use Vitest + supertest, or Playwright's request fixture without a page. Don't spin up a browser to test JSON.
  • Cross-language polyglot test suite. Selenium has more language bindings; Playwright is Node-centric (Python, .NET, Java bindings exist but lag).
  • W3C WebDriver compatibility required. Some enterprise compliance frameworks require WebDriver. Playwright uses CDP-flavoured protocols; Selenium is the safe pick there.
  • Tests on real iOS / Android devices. Playwright emulates mobile via desktop browsers. Real-device testing → Appium, BrowserStack, Sauce Labs.

See also