Playwright Cheat Sheet

Complete quick reference for Playwright - the fast, reliable end-to-end testing and browser automation framework by Microsoft. Chromium, Firefox, and WebKit with a single API.

≈ 10 min read
Microsoft · Apache 2.0
Last updated: April 14, 2026

What Is Playwright?

Fast, reliable end-to-end testing and browser automation across Chromium, Firefox, and WebKit

The Core Idea

Playwright is a Microsoft-maintained open-source framework (Apache 2.0) for end-to-end testing and browser automation. A single API drives Chromium, Firefox, and WebKit across Linux, macOS, and Windows - headed or headless. It is built around auto-waiting (actions wait for elements to be actionable), web-first assertions (retry until conditions are met), and full test isolation via fresh browser contexts per test.

Library vs Test Runner

Playwright Library

The low-level automation API. Controls Browser, BrowserContext, Page, and Locator directly. Use for scripting, scraping, or AI-agent workflows.

import { chromium } from 'playwright';
const browser = await chromium.launch();
Playwright Test (recommended)

Full test framework built on the Library. Adds fixtures, parallelism, HTML reporter, tracing, UI mode, and config. Use for all testing scenarios.

import { test, expect } from '@playwright/test';
test('page title', async ({ page }) => { ... });

Core Strengths

Auto-waiting - actions wait for elements to be actionable before proceeding. No manual sleeps.
Web-first assertions - expect() retries until conditions are met or timeout.
Resilient locators - role-based and semantic selectors, not fragile CSS paths.
Full isolation - fresh BrowserContext per test, zero state leakage.
Built-in parallelism - parallel workers and cross-browser sharding out-of-the-box.
Tracing - timeline, DOM snapshots, network, and console logs per test run.
Cross-platform - Linux, macOS, Windows. Headed and headless modes.

Supported Browsers

Chromium

Chrome and Edge (stable + dev channels). Also covers Chrome for Testing via channel: 'chrome'.

Firefox

Playwright-patched Firefox build for full CDP protocol support.

WebKit

Apple's browser engine (Safari). Cross-platform - test Safari behaviour on Linux/Windows CI.

Key Terminology

Fixture
A dependency injected into tests by Playwright Test (e.g. page, context, browser). Scoped and automatically cleaned up.
Project
A named browser/config combination in playwright.config.ts. Run your suite across multiple browsers via projects.
storageState
Serialised cookies + localStorage snapshot. Used to reuse authenticated sessions across tests without re-logging in.
Trace
A zip archive per test containing timeline, DOM snapshots, network, console. Open with npx playwright show-trace.

Core Concepts

Browser · BrowserContext · Page · Locator - the four primitives and how they compose

Object Hierarchy & Lifecycle

1
Browser

A running browser instance. Launch via chromium.launch().

newContext()
contexts()
version()
close()
2
BrowserContext

Isolated incognito-like environment. Own cookies, localStorage, permissions, emulation. Near-zero overhead.

newPage()
storageState()
addCookies()
close()
3
Page

A single browser tab. Shares context storage. Entry point for most actions.

goto(url)
getByRole()
fill() / click()
screenshot()
4
Locator

A resilient element reference. Auto-waits for actionability. Preferred over raw selectors.

click()
fill(text)
filter()
nth(n)
// Playwright Library - full lifecycle (close context before browser to flush artifacts)
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ baseURL: 'https://example.com' });
const page    = await context.newPage();

await page.goto('/');
await page.getByRole('button', { name: 'Sign in' }).click();

await context.close();  // flush traces / videos / HARs
await browser.close();

BrowserContext - Isolation Model

Each context is a fully isolated browsing session. Playwright Test automatically creates a fresh context per test - preventing any state leakage and enabling safe parallelism without test ordering constraints.

Context options
browser.newContext({
  baseURL: 'https://example.com',
  storageState: 'auth.json',    // reuse auth
  viewport: { width: 1280, height: 720 },
  locale: 'en-GB',
  permissions: ['geolocation'],
  recordVideo: { dir: 'videos/' },
})
Best practice: one context per test. Reuse across pages within the same test. Close context before browser to ensure videos and traces are flushed to disk.

playwright.config.ts

Centralised configuration for all test behaviour. Defines projects (browser targets), timeouts, retries, reporters, and base URL.

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  retries: 2,               // retry on CI
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',  // capture on failure
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit',   use: { ...devices['Desktop Safari'] } },
    { name: 'Mobile Safari', use: { ...devices['iPhone 13'] } },
  ],
});

Locators

Resilient element selection - auto-waits for actionability, retries on DOM changes

Preferred - User-Facing Locators

These mirror how users and assistive technology perceive the page. Resilient to DOM structure changes.

getByRole() - most preferred
page.getByRole('button', { name: 'Submit' })
page.getByRole('heading', { level: 1 })
page.getByRole('checkbox', { checked: true })
page.getByRole('link', { name: 'Home' })

Uses ARIA role + accessible name. Covers buttons, links, headings, inputs, lists, and more.

getByLabel()
page.getByLabel('Password')
page.getByLabel('Email address')

Locates form controls by their associated label text.

getByPlaceholder()
page.getByPlaceholder('Search…')
getByText()
page.getByText('Welcome back')
page.getByText(/welcome/i)  // regex

Matches by visible text content. Supports strings and regexes.

getByTestId()
// HTML: data-testid="submit-btn"
page.getByTestId('submit-btn')

Matches data-testid attribute. Attribute name configurable in config.

getByAltText() · getByTitle()
page.getByAltText('Company logo')
page.getByTitle('Delete item')

Chaining & Filtering

Narrow down matches by chaining and filtering locators.

// Filter by text within a list item
page.getByRole('listitem')
  .filter({ hasText: 'Product 2' })
  .getByRole('button', { name: 'Add to cart' })
  .click();

// Filter by child element
page.getByRole('listitem')
  .filter({ has: page.getByRole('img') });

// Pick the nth match
page.getByRole('listitem').nth(2);
page.getByRole('listitem').first();
page.getByRole('listitem').last();

// Scope to a container
const nav = page.getByRole('navigation');
nav.getByRole('link', { name: 'Home' }).click();
Locators are lazy - they don't query the DOM until an action is performed. Re-query automatically on retries.

CSS / XPath (use sparingly)

Only use when user-facing locators aren't possible. These are fragile to DOM restructuring.

// CSS selector
page.locator('.submit-button')
page.locator('button[type="submit"]')

// XPath (last resort)
page.locator('xpath=//button[@id="submit"]')

// Prefer: generate with Codegen instead
$ npx playwright codegen https://example.com
Avoid CSS/XPath unless no semantic alternative exists. They break easily when the UI structure changes.

Assertions

Web-first assertions - retry automatically until the condition is met or the timeout expires

Web-First Assertions - Always Use These

Playwright's expect() assertions are asynchronous and retrying. They poll until the condition is true or a timeout is reached. Never use isVisible() / textContent() in assertions - those are point-in-time and won't retry.

// ✅ Web-first - retries automatically
await expect(page.getByText('Welcome'))
  .toBeVisible();

await expect(page.getByRole('button'))
  .toBeEnabled();

await expect(page)
  .toHaveURL('/dashboard');

await expect(page)
  .toHaveTitle(/Dashboard/);
// ❌ Non-retrying - avoid in assertions
const visible = await page
  .getByText('Welcome')
  .isVisible();   // point-in-time
expect(visible).toBe(true);  // no retry!

// ❌ Also avoid
const text = await page
  .getByRole('heading')
  .textContent();  // no retry

Locator Assertions

const btn = page.getByRole('button');

// Visibility
await expect(btn).toBeVisible();
await expect(btn).toBeHidden();

// State
await expect(btn).toBeEnabled();
await expect(btn).toBeDisabled();
await expect(btn).toBeFocused();
await expect(btn).toBeChecked();   // checkbox

// Content
await expect(btn).toHaveText('Submit');
await expect(btn).toContainText('Save');
await expect(btn).toHaveValue('hello'); // input
await expect(btn).toHaveAttribute('aria-label', 'Close');
await expect(btn).toHaveClass('active');
await expect(btn).toHaveCount(3);     // list length

Page Assertions, Soft & Timeouts

// Page-level assertions
await expect(page).toHaveURL('https://example.com/login');
await expect(page).toHaveURL(/dashboard/);
await expect(page).toHaveTitle('My App');

// Custom timeout (ms) - override default 5000ms
await expect(page.getByText('Loading…'))
  .toBeHidden({ timeout: 10_000 });

// Soft assertions - don't stop the test on failure
await expect.soft(page.getByTestId('status'))
  .toHaveText('Active');
// test continues; all soft failures reported at end

// Negate any assertion
await expect(btn).not.toBeVisible();
Default assertion timeout: 5000ms. Set globally via expect.timeout in config. Action timeout (for click, fill etc.) defaults to 30000ms.

Network Mocking & API Testing

Intercept and mock network requests with page.route() - great for isolating UI tests from third-party APIs.

// Mock a fetch/XHR request
await page.route('**/api/users', route =>
  route.fulfill({
    status: 200,
    body: JSON.stringify([{ name: 'Alice' }]),
  })
);

// Block analytics/trackers
await page.route('**/*.{png,jpg}', r => r.abort());

Test APIs directly with request fixture - no browser needed, but shares auth state.

test('create user', async ({ request }) => {
  const res = await request.post('/api/users', {
    data: { name: 'Alice' },
  });
  await expect(res).toBeOK();
  await expect(res).toBeOK();
});

Tooling & CLI

Codegen, Trace Viewer, UI Mode, VS Code extension, and essential CLI commands

Essential CLI Commands

# Scaffold a new project
npm init playwright@latest

# Install browser binaries
npx playwright install
npx playwright install chromium  # one browser
npx playwright install --with-deps # incl. OS deps

# Run tests
npx playwright test
npx playwright test --headed
npx playwright test --project=chromium
npx playwright test tests/login.spec.ts
npx playwright test --grep "login"
# UI Mode (recommended for local dev)
npx playwright test --ui

# Codegen - record interactions
npx playwright codegen https://example.com

# Show HTML report
npx playwright show-report

# Open Trace Viewer
npx playwright show-trace trace.zip

# Sharding for CI
npx playwright test --shard=1/3
npx playwright test --shard=2/3

Codegen - Record & Generate

Playwright Codegen opens a browser and records your interactions, generating resilient locator-based test code in real time. The Inspector panel lets you pick elements and see suggested locators.

# Generate TypeScript test
npx playwright codegen https://example.com

# Generate for a specific language
npx playwright codegen --target=python https://example.com

# Save to file
npx playwright codegen -o tests/login.spec.ts https://example.com
Suggests getByRole, getByLabel, and getByTestId over CSS
Also available directly in the VS Code extension

Trace Viewer & UI Mode

Trace Viewer

A rich GUI for post-mortem debugging. Shows a step-by-step timeline of every action with DOM snapshots before/after, network requests, console logs, and screenshots.

Recommended: enable only on retry (trace: 'on-first-retry') to avoid CI overhead. Never enable trace: 'on' for every run.
UI Mode (--ui)

Interactive watch mode for local development. Re-runs tests on file change, shows time-travel debugging, and lets you filter and isolate test runs. The fastest way to iterate on tests.

VS Code Extension

The official Playwright VS Code extension (ID: ms-playwright.playwright) integrates the full toolchain into the editor.

Run and debug individual tests or full suites from the Testing panel
Pick locators in a live browser and auto-insert into code
Record new tests with Codegen directly from the editor
Step-through debugging with breakpoints in test code

CI & Parallelism

GitHub Actions (auto-generated)
npx playwright install --with-deps  # install OS deps
npx playwright test
Sharding across CI runners
# Split into 3 shards across parallel jobs
npx playwright test --shard=1/3
npx playwright test --shard=2/3
npx playwright test --shard=3/3
Tests run in parallel by default within a worker. Use workers config to set concurrency. Retries are separate from parallelism.

Languages & Installation

The same Browser/Context/Page/Locator API across JavaScript, Python, Java, and .NET

All 4 Languages Share the Same Core API

JS / TypeScript
Node.js · Primary language
npm init playwright@latest
Package: @playwright/test
playwright.dev/docs/intro
Python
pytest plugin recommended
pip install playwright
playwright install
Plugin: pytest-playwright
playwright.dev/python/docs/intro
Java
JUnit / TestNG recommended
<!-- Maven -->
<dependency>
  <groupId>com.microsoft.playwright</groupId>
  <artifactId>playwright</artifactId>
</dependency>
playwright.dev/java/docs/intro
.NET (C#)
MSTest · NUnit · xUnit
dotnet add package Microsoft.Playwright
pwsh bin/Debug/net8.0/playwright.ps1 install
playwright.dev/dotnet/docs/intro
Important: after installing the package, always run npx playwright install (or equivalent) to download browser binaries. They are not bundled in the package.
T

TypeScript - First Test

import { test, expect } from '@playwright/test';

test('homepage has title', async ({ page }) => {
  await page.goto('https://playwright.dev/');

  // Expect title to contain "Playwright"
  await expect(page).toHaveTitle(/Playwright/);
});

test('get started link', async ({ page }) => {
  await page.goto('https://playwright.dev/');

  // Click the get started link
  await page.getByRole('link', { name: 'Get started' }).click();

  // Assert navigation
  await expect(page).toHaveURL(/intro/);
});
P

Python - First Test (pytest)

import re
from playwright.sync_api import Page, expect

def test_homepage_title(page: Page):
    page.goto("https://playwright.dev/")
    expect(page).to_have_title(re.compile("Playwright"))

def test_get_started(page: Page):
    page.goto("https://playwright.dev/")

    # Click the link
    page.get_by_role("link", name="Get started").click()

    # Assert URL
    expect(page).to_have_url(re.compile(r".*intro"))
Run with: pytest --browser chromium

Best Practices

Official guidance from playwright.dev/docs/best-practices - the rules that prevent the most common failures

Do's & Don'ts at a Glance

✅ Do❌ Don't
Use getByRole, getByLabel, getByTestIdUse raw CSS/XPath selectors as first choice
Use web-first await expect(locator).toBeVisible()Use await locator.isVisible() in assertions
Keep each test independent - fresh context per testShare state between tests or assume test order
Reuse auth via storageState in a setup projectLog in through the UI in every single test
Mock third-party APIs with page.route()Test against live third-party services in CI
Set trace: 'on-first-retry' in CI configEnable traces on every test run (slow CI)
Close context before browser to flush artifactsClose only the browser and lose videos/traces
Keep @playwright/test updated regularlyPin to old versions - browser patches lag behind

Authentication State Reuse

Log in once in a setup project, save the session to a file, then reuse it across all tests - no UI login overhead per test.

// global-setup.ts - log in once
const page = await browser.newPage();
await page.goto('/login');
await page.getByLabel('Email').fill('user@test.com');
await page.getByLabel('Password').fill('secret');
await page.getByRole('button', { name: 'Login' }).click();
await page.context().storageState({ path: 'auth.json' });

// playwright.config.ts - apply to all tests
use: { storageState: 'auth.json' }

Parallelism & Performance

Tests run in parallel by default across worker processes. Each worker gets its own browser context.
Run tests in parallel within a file: test.describe.configure({ mode: 'parallel' })
Set fullyParallel: true in config to parallel all tests across all files.
Use --shard=N/total to distribute across CI runners.
Only install browsers you need for CI: npx playwright install chromium
Avoid page.waitForTimeout() - use assertions and auto-waiting instead.

Official Resources

Primary sources - docs, API references, GitHub, and release notes

Official Documentation

GitHub Repository

Core Repo (Apache 2.0)
github.com/microsoft/playwright
Repo structure
/packages/ - library, test runner, CLI
/docs/src/ - documentation source
/browser_patches/ - browser patches
/examples/ - code examples