DS DevShelfHub Projects · AI tools
Cheatsheets / Playwright
Cheatsheet · Dev tooling

Playwright Cheatsheet: Locators, Assertions, Fixtures and Traces

By DevShelfHub

Locators, auto-wait, assertions, fixtures, traces, network mocking, parallelism — the modern end-to-end testing surface in TS + Python.

108 items 8 min Locators Auto-wait Traces

Start hereQuick start · 6 you’ll reach for daily

Run all testsnpx playwright test
Locatorpage.getByRole("button", "Pay")
Web-first assertexpect(loc).toBeVisible()
Mock APIpage.route(url, route => route.fulfill())
Trace viewernpx playwright show-trace
Recordnpx playwright codegen url

Target versions · paceVersions

Targets: @playwright/test ≥ 1.45 playwright (py) ≥ 1.45 node ≥ 18

Playwright ships a new minor every ~6 weeks — browsers bundled and pinned, no Selenium-style version drift. Test isolation runs each test in a fresh BrowserContext (cookies / storage / cache cleared). Web-first assertions auto-retry until expect.timeout — never wrap them in setTimeout. UI Mode (--ui) and the Trace Viewer cover ~90% of debugging. This sheet pins to 1.45+.

Install · run · recordSetup

bash
# Node / TypeScript — the canonical path
npm init playwright@latest
# → installs @playwright/test + Chromium, Firefox, WebKit binaries
# → scaffolds tests/, playwright.config.ts, .github/workflows/playwright.yml

npx playwright test                          # all browsers, all projects
npx playwright test --ui                     # interactive Trace + watch mode
npx playwright test login.spec.ts --headed   # see the browser
npx playwright test -g "checkout"            # filter by title (regex)
npx playwright test --workers=4              # parallel processes
npx playwright codegen example.com           # record a test from clicks

# Python — pytest plugin
pip install pytest-playwright
playwright install                           # download browser binaries
pytest --browser chromium --headed

# Reports
npx playwright show-report                   # open last HTML report
npx playwright show-trace trace.zip          # open a captured trace

Where things liveCommon imports

import { test, expect } from '@playwright/test'Test runner entry. Preferred over the bare playwright library.
import { defineConfig, devices } from '@playwright/test'Config + device descriptors (iPhone, Pixel, Desktop Chrome...).
import { chromium, firefox, webkit } from 'playwright'Library-mode launchers — for scripts / scrapers, not tests.
import { request } from '@playwright/test'API-only request context. Skip the browser entirely.
from playwright.sync_api import sync_playwright, expectPython sync API.
from playwright.async_api import async_playwrightPython async API.

Auto-wait · strict-by-defaultLocators

Recommended (a11y-first)

page.getByRole('button', { name: 'Pay' })By ARIA role + accessible name. Preferred.
page.getByLabel('Email')Form input by its <label>.
page.getByPlaceholder('you@x.com')Input by placeholder.
page.getByText('Sign in', { exact: true })Visible text.
page.getByAltText('Logo')Image by alt.
page.getByTitle('Settings')Element by title attribute.
page.getByTestId('checkout-cta')By data-testid. Configure attribute via testIdAttribute.

Fallback / refinement

page.locator('css=.product:visible')CSS — explicit engine prefix.
page.locator('text=Pay now')Built-in text engine.
page.locator('xpath=//button[@aria-pressed]')XPath — last resort.
loc.filter({ hasText: 'Pro' })Narrow a list by inner text.
loc.filter({ has: page.getByRole('link') })Narrow by descendant matching another locator.
loc.nth(0) / loc.first() / loc.last()Pick from a multi-match.
loc.or(loc2) / loc.and(loc2)Boolean combine (1.30+).
page.frameLocator('iframe[name=stripe]').getByLabel('Card number')Cross into an iframe.

click · fill · type · hoverActions

loc.click({ button: 'right', clickCount: 2 })Auto-waits for actionability, then clicks.
loc.dblclick() / loc.tap()Double-click / mobile tap.
loc.fill('hello')Clear + type. Preferred over type() for forms.
loc.pressSequentially('hello', { delay: 50 })Per-key dispatch (when you need keydown handlers to fire).
loc.press('Enter') / page.keyboard.press('Shift+Tab')Single key / modifier combo.
loc.check() / loc.uncheck() / loc.setChecked(true)Checkboxes + radios.
loc.selectOption('us') / selectOption(['a','b'])Native <select>.
loc.setInputFiles('./photo.png')File upload (real or virtual file).
loc.hover() / loc.focus() / loc.blur()Pointer + focus states.
loc.dragTo(target)High-level drag-and-drop.
page.goto('/path', { waitUntil: 'domcontentloaded' })Navigate. waitUntil: load | domcontentloaded | networkidle | commit.
page.evaluate(([sel]) => document.querySelector(sel).textContent, ['#x'])Run JS in page context.

Web-first · auto-retryAssertions

await expect(loc).toBeVisible()Retries until the element is visible or timeout. Preferred over isVisible().
toBeHidden() / toBeAttached() / toBeEnabled() / toBeDisabled()State assertions.
toHaveText('Welcome', { useInnerText: true })Exact text match (string or regex). Array compares the list.
toContainText('Welcome')Substring match.
toHaveValue('foo')Input value.
toHaveAttribute('disabled', '') / toHaveClass(/active/)Attribute / class assertions.
toHaveCount(3)List length.
toHaveURL('/checkout') / toHaveTitle('Pay')Page-level assertions.
toHaveScreenshot('home.png', { maxDiffPixelRatio: 0.02 })Visual snapshot. Auto-updates on first run.
expect.soft(loc).toBeVisible()Soft assertion — test keeps going, fails at end.
expect.poll(async () => await api.get(), { timeout: 5000 }).toEqual(...)Poll any async function until it matches.
Never wrap a Playwright assertion in page.waitForTimeout(). Web-first assertions are the wait — arbitrary sleeps cause flake.

playwright.config.tsConfig

defineConfig({ testDir, projects, use, webServer })Top-level config shape.
use.baseURLPrefix for relative page.goto('/x'). Preferred over hard-coded URLs.
use.trace: 'on' | 'on-first-retry' | 'retain-on-failure'Capture network + DOM steps for the Trace Viewer.
use.screenshot: 'only-on-failure'PNG attached to the report.
use.video: 'retain-on-failure'WebM video.
use.actionTimeout / navigationTimeoutPer-action vs per-navigation budgets.
use.storageState: './state.json'Pre-seed cookies / localStorage. Use to skip login per test.
projects: [{ name, use: devices['iPhone 15'] }]Run the same suite across browsers / devices.
webServer: { command, url, reuseExistingServer }Spin up your app before tests, tear down after.
retries: 2 / fullyParallel: trueCI defaults.
reporter: 'html' | 'list' | 'github' | 'json'Output format. Multiple allowed.
javascript
// playwright.config.ts — the knobs you'll touch most
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? '50%' : undefined,

  use: {
    baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
    headless: true,
    trace: 'on-first-retry',        // capture network/DOM only on retries
    video: 'retain-on-failure',
    screenshot: 'only-on-failure',
    actionTimeout: 10_000,
    navigationTimeout: 30_000,
    locale: 'en-US',
    timezoneId: 'Europe/London',
    viewport: { width: 1280, height: 800 },
  },

  // Multi-browser fanout — each becomes its own report group
  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 15'] } },
  ],

  // Boot the app before tests
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

test.extend · auth stateFixtures

{ page, browser, context, request }Built-in test fixtures — injected by destructuring.
test.extend<{ admin: Page }>({ admin: async ({ browser }, use) => ... })Define a custom fixture. Preferred over helper functions.
test.beforeAll / afterAll / beforeEach / afterEachLifecycle hooks. All hooks share state at the suite level.
test.use({ storageState: 'auth.json' })Reuse a logged-in session.
setup project + dependenciesRun a special "login" project once, then dependent projects reuse the state file.
test.step('login', async () => { ... })Group actions — nicer trace viewer + report tree.
test.skip() / test.fixme() / test.fail()Mark tests for runners + report grouping.
test.describe.parallel.serialForce serial order within a describe block.

Intercept · mock · observeNetwork & mocking

page.route('**/api/x', route => route.fulfill({ body }))Stub a response.
route.continue({ headers, postData })Forward with modifications.
route.abort('blockedbyclient')Kill the request.
page.unroute(pattern)Drop a previously installed route.
page.routeFromHAR('flow.har', { update: false })Replay recorded traffic.
page.waitForResponse(/.*\/checkout/)Deterministic wait for a specific response.
page.waitForRequest(...)Same for requests — useful in fire-and-forget flows.
request.newContext({ baseURL, extraHTTPHeaders })API-only context — no browser, just HTTP.
api.post('/login', { data: {...} })Seed state via API before opening the page.
javascript
// Intercept + stub a JSON API
await page.route('**/api/users/*', async route => {
  await route.fulfill({
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify({ id: 42, name: 'Ana', plan: 'pro' }),
  });
});

// Modify an in-flight request (e.g. inject a header)
await page.route('**/api/**', async route => {
  const headers = { ...route.request().headers(), 'x-trace-id': 'abc' };
  await route.continue({ headers });
});

// Block a 3rd-party tracker entirely
await page.route('**/analytics.js', route => route.abort());

// Listen, don't intercept — read response bodies
page.on('response', async resp => {
  if (resp.url().includes('/api/orders'))
    console.log(resp.status(), await resp.json());
});

// Wait for a specific response (deterministic — beats waitForTimeout)
const [resp] = await Promise.all([
  page.waitForResponse(r => r.url().endsWith('/checkout') && r.status() === 200),
  page.click('button:has-text("Pay")'),
]);
expect((await resp.json()).orderId).toBeTruthy();

UI Mode · trace viewerTraces & debugging

npx playwright test --uiInteractive watch + time-travel debugger. Preferred dev workflow.
npx playwright test --debugStep through with the Playwright Inspector. Pauses at start.
await page.pause()Inline breakpoint that opens the inspector.
PWDEBUG=1 npx playwright testEnv-var equivalent.
npx playwright codegen example.comRecord clicks into a test file.
npx playwright show-trace trace.zipInspect a CI failure trace locally.
trace: 'on-first-retry'Cheap default — traces only when a test re-runs.
page.screenshot({ path, fullPage: true })Ad-hoc screenshot.
await page.video()?.path()Video file path for the current test (when video is on).

Workers · shards · shardingParallelism & CI

npx playwright test --workers=4Parallel worker processes. Default = half the CPUs.
npx playwright test --shard=2/4Split the suite across 4 CI machines.
fullyParallel: trueRun tests within a file in parallel too.
test.describe.configure({ mode: 'serial' })Force serial order in one describe (when tests share state).
retries: process.env.CI ? 2 : 0Retry flaky tests on CI only.
reporter: [['html', { open: 'never' }], ['github']]HTML artifact + inline GitHub annotations.
testInfo.attachments.push({ name, body, contentType })Attach custom artifacts to the report.
npx playwright test --grep-invert @flakyTag-style exclusion (uses test titles).

Checkout flowEnd-to-end · Login · cart · pay

A complete TypeScript test — reuses a saved auth state, drives the UI, deterministically waits for the checkout API.

javascript
// Full login → cart → checkout flow with auto-wait + storage state reuse.
import { test, expect } from '@playwright/test';

// Reuse a logged-in session across tests
test.use({ storageState: 'storage/user.json' });

test('checkout flow', async ({ page }) => {
  await page.goto('/shop');                              // baseURL prepends

  // Add to cart
  await page.getByRole('button', { name: 'Add Mug' }).click();
  await expect(page.getByTestId('cart-count')).toHaveText('1');

  // Open cart drawer + go to checkout
  await page.getByRole('navigation').getByRole('link', { name: 'Cart' }).click();
  await page.getByRole('button', { name: /checkout/i }).click();

  // Fill the form by labels (a11y-friendly)
  await page.getByLabel('Card number').fill('4242 4242 4242 4242');
  await page.getByLabel('Expiry').fill('12/29');
  await page.getByLabel('CVC').fill('123');

  // Wait for the success response — deterministic
  const [resp] = await Promise.all([
    page.waitForResponse(r => r.url().endsWith('/checkout') && r.ok()),
    page.getByRole('button', { name: 'Pay' }).click(),
  ]);
  expect((await resp.json()).status).toBe('paid');

  // Web-first assertion — auto-retries until visible or test times out
  await expect(page.getByText('Thank you for your order')).toBeVisible();
});

Best practiceGood to know

Prefer role-based locators — they double as a11y tests. getByRole('button', { name: 'Pay' }) matches what assistive tech sees. CSS selectors break on every redesign; role + name survive.
Use storageState to skip login. Run a one-time setup project that logs in and saves cookies / localStorage to a JSON file. Every other project loads that state — no per-test login, no flake on auth boundary.
Lean on the trace viewer over console.log. trace: 'on-first-retry' captures the full DOM, network, console, and time-travel snapshots on flake. The viewer beats reading logs and is free in CI.

Common trapsWatch out for

Don’t sprinkle waitForTimeout() calls. Fixed sleeps are the #1 source of flake. Replace them with expect(loc).toBeVisible(), waitForResponse, or expect.poll.
Locator returns can be undefined; assertions on them must be awaited. expect(loc).toBeVisible() without await resolves to undefined and your test always "passes". The ESLint plugin eslint-plugin-playwright catches this.
Iframes need frameLocator, not page.locator. Stripe Elements, payment SDKs, embedded widgets — all live in iframes. page.frameLocator('iframe[name=...]').getByLabel(...) crosses the boundary; the bare locator silently times out.

Go deeperSee also

Playwright FAQ

What is Playwright?

Playwright is an end-to-end browser testing framework from Microsoft that controls Chromium, Firefox, and WebKit with a single API. It supports TypeScript, JavaScript, Python, Java, and C#, auto-waits for elements to be actionable before interacting, and includes built-in tools for tracing, video recording, and network mocking.

How do Playwright locators work?

Locators are lazy handles that describe how to find an element. Unlike older selectors that resolve immediately, locators retry automatically on every action and assertion until the element is stable. Prefer role-based locators like page.get_by_role('button', name='Submit') over CSS selectors, as they are more resilient to DOM changes and reflect how assistive technologies see the page.

What is auto-wait in Playwright?

Auto-wait means Playwright automatically waits for an element to meet an actionability condition before performing an action. For click(), it waits for the element to be visible, enabled, and stable. For fill(), it additionally waits for the element to be editable. This eliminates most manual sleep() and waitForSelector() calls needed in older frameworks like Selenium.

How do I mock network requests in Playwright?

Use page.route(url_pattern, handler) to intercept and respond to matching requests. The handler receives a Route object; call route.fulfill(status=200, json={...}) to return a mock response or route.abort() to simulate a network error. Use page.route on specific API endpoints in tests to avoid real network calls and make tests deterministic.

How do I debug failing Playwright tests?

Run tests with --headed to watch the browser, or --debug to open the Playwright Inspector for step-by-step execution. Enable traces with trace='on-first-retry' in playwright.config and open the resulting trace.zip in playwright show-trace to replay the full test with DOM snapshots, network logs, and action timeline. The VS Code extension also provides an integrated debugger.