Playwright logoPlaywrightv1.61INTERMEDIATE

Playwright

Complete reference for Playwright — the cross-browser end-to-end testing framework by Microsoft for reliable web automation

10 min read
playwrighttestinge2eend-to-endautomationbrowserchromiumfirefoxwebkitcross-browser
Loading your progress

Installation & Setup

Installing Playwright and creating your first project

Initialize a new Playwright test project with all dependencies

bash
# Create new project
npm init playwright@latest

# Install in existing project
npm install -D @playwright/test

# Install browsers
npx playwright install
💡 The init command scaffolds config, example tests, and GitHub Actions workflow
⚡ Use --with-deps on CI to install system-level browser dependencies automatically
📌 Playwright bundles its own browsers — no need for separate Chrome/Firefox installs
🟢 Supports Chromium, Firefox, and WebKit out of the box
installsetup

Set up playwright.config.ts with projects, reporters, and options

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

export default defineConfig({
  testDir: './tests',
  timeout: 30000,
  retries: process.env.CI ? 2 : 0,
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
});
💡 The webServer option auto-starts your dev server before running tests
⚡ Use fullyParallel: true to run tests in parallel across files and within files
📌 forbidOnly prevents accidental test.only commits from passing in CI
🟢 Device emulation is built-in — just spread from the devices object
configsetup

Writing Tests

Test structure, hooks, and organizing test suites

Write tests with test blocks, navigation, and assertions

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

test('has correct title', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveTitle(/My App/);
});

test('navigates to about page', async ({ page }) => {
  await page.goto('/');
  await page.getByRole('link', { name: 'About' }).click();
  await expect(page).toHaveURL('/about');
});
💡 Each test gets a fresh browser context — no state leaks between tests
⚡ Playwright auto-waits for elements before interacting — no manual waits needed
📌 Use test.skip() with a condition to skip tests based on platform or environment
🟢 The page fixture is the most common — it provides a fresh page for each test
teststructure

Organize tests with describe blocks and lifecycle hooks

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

test.describe('Dashboard', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/dashboard');
  });

  test('shows welcome message', async ({ page }) => {
    await expect(page.getByText('Welcome')).toBeVisible();
  });

  test('displays stats', async ({ page }) => {
    await expect(page.getByTestId('stats')).toBeVisible();
  });
});
💡 beforeEach/afterEach run for every test — use for navigation and cleanup
⚡ beforeAll/afterAll run once per describe block — ideal for database seeding
📌 Use describe.configure({ mode: "serial" }) when tests depend on each other
🟢 Describe blocks can be nested for better organization of large test suites
hooksdescribeorganize

Locators

Finding elements on the page with built-in locator strategies

Use role-based, text, and semantic locators to find elements

typescript
// By role (preferred — accessible and resilient)
page.getByRole('button', { name: 'Submit' })
page.getByRole('heading', { name: 'Welcome' })
page.getByRole('link', { name: 'Home' })

// By label, placeholder, text
page.getByLabel('Email')
page.getByPlaceholder('Search...')
page.getByText('Hello World')

// By test ID
page.getByTestId('submit-btn')
💡 Prefer getByRole over CSS selectors — it mirrors how users and assistive tech see the page
⚡ Locators auto-wait and auto-retry — no need for explicit waitFor calls
📌 Use filter() to narrow down results from broad locators like getByRole("listitem")
🟢 getByTestId uses data-testid by default — customizable in playwright.config.ts
locatorsselectors

Actions & Interactions

Clicking, typing, selecting, and interacting with page elements

Common Actions

Click, type, select, check, and interact with elements

typescript
// Click
await page.getByRole('button', { name: 'Save' }).click();

// Type into input
await page.getByLabel('Name').fill('John Doe');

// Select dropdown option
await page.getByLabel('Country').selectOption('us');

// Check/uncheck
await page.getByLabel('Remember me').check();
💡 fill() clears the input before typing — use pressSequentially() for char-by-char input
⚡ All actions auto-wait for the element to be visible and enabled before acting
📌 Use setInputFiles([]) to clear a file upload, and pass arrays for multiple files
🟢 Keyboard shortcuts work with page.keyboard.press() using modifier+key syntax
actionsclicktypefill

Assertions

Web-first assertions that auto-wait and auto-retry

Assert on page state, element visibility, text content, and attributes

typescript
// Page assertions
await expect(page).toHaveTitle(/Dashboard/);
await expect(page).toHaveURL('/dashboard');

// Visibility
await expect(page.getByText('Welcome')).toBeVisible();
await expect(page.getByText('Loading')).toBeHidden();

// Text content
await expect(page.locator('h1')).toHaveText('Hello');
await expect(page.locator('p')).toContainText('world');
💡 All expect assertions auto-retry until the condition is met or timeout expires
⚡ Use soft assertions when you want to collect all failures without stopping the test
📌 Screenshot assertions auto-generate golden files on first run — commit them to git
🟢 Default assertion timeout is 5s — override per-assertion with { timeout: ms }
assertionsexpect

Emulation & Devices

Emulate mobile devices, viewports, geolocation, locale, and color schemes

Test on mobile devices, custom viewports, and different screen configurations

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

export default defineConfig({
  projects: [
    { name: 'Desktop', use: { viewport: { width: 1280, height: 720 } } },
    { name: 'Mobile', use: { ...devices['iPhone 13'] } },
    { name: 'Tablet', use: { ...devices['iPad Pro 11'] } },
  ],
});
💡 Playwright ships with 100+ device profiles — use devices["Device Name"] to emulate any of them
⚡ Device profiles include viewport, userAgent, deviceScaleFactor, isMobile, and hasTouch
📌 Use page.setViewportSize() mid-test to verify responsive breakpoints dynamically
🟢 Each project runs your entire test suite — great for cross-device coverage in CI
emulationdevicesviewportmobile

Emulate user locale, timezone, geolocation, permissions, and color scheme

typescript
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    locale: 'de-DE',
    timezoneId: 'Europe/Berlin',
    colorScheme: 'dark',
    geolocation: { latitude: 52.52, longitude: 13.405 },
    permissions: ['geolocation'],
  },
});
💡 test.use() overrides config-level settings for all tests in the current file
⚡ emulateMedia() lets you switch color scheme mid-test without reloading
📌 permissions must be granted explicitly — geolocation, notifications, clipboard, etc.
🟢 Locale affects Intl APIs, date formatting, and number display in the browser context
localetimezonegeolocationdark-mode

Visual & Snapshot Testing

Compare screenshots and snapshots against golden baselines

Capture and compare page or element screenshots against baselines

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

test('homepage visual check', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot();
});

test('component visual check', async ({ page }) => {
  await page.goto('/dashboard');
  await expect(page.getByTestId('chart')).toHaveScreenshot();
});
💡 Golden files auto-generate on first run — commit them to git for CI comparisons
⚡ Use mask to hide dynamic content like timestamps or avatars that change between runs
📌 Run npx playwright test --update-snapshots to regenerate baselines after intentional changes
🟢 Screenshots are browser-specific — each project gets its own snapshot folder
screenshotvisualsnapshotcomparison

Parallelism & Sharding

Run tests in parallel across workers and shard across CI machines

Configure parallel workers and split test suites across CI machines

typescript
import { defineConfig } from '@playwright/test';

export default defineConfig({
  fullyParallel: true,          // Parallelize within files too
  workers: process.env.CI ? 1 : undefined, // Default: half CPU cores
});

// Shard from CLI
// npx playwright test --shard=1/4
// npx playwright test --shard=2/4
💡 fullyParallel: true runs tests within a single file in parallel — not just across files
⚡ Sharding splits the full test suite across CI machines — combine with GitHub Actions matrix
📌 Use describe.configure({ mode: "serial" }) when tests in a block depend on each other
🟢 Default worker count is half your CPU cores — set workers: 1 on CI to reduce flakiness
parallelshardingworkersci

Retries & Timeouts

Configure test retries for flaky tests and set timeout boundaries

Automatically retry failed tests to handle flaky behavior

typescript
import { defineConfig } from '@playwright/test';

export default defineConfig({
  retries: process.env.CI ? 2 : 0,  // Retry twice on CI
});
💡 Retries re-run the entire test including beforeEach hooks — each retry gets a clean state
⚡ Traces with "on-first-retry" only record when a test fails and retries — saving disk space
📌 Use testInfo.retry to detect retries and add conditional cleanup or logging
🟢 Set retries: 0 locally for fast feedback, retries: 2 on CI for stability
retriesflaky

Set timeouts for tests, actions, navigation, and assertions

typescript
import { defineConfig } from '@playwright/test';

export default defineConfig({
  timeout: 30_000,               // Per-test timeout (30s)
  expect: { timeout: 5_000 },   // Assertion timeout (5s)
  use: {
    actionTimeout: 10_000,       // Click/fill/etc timeout
    navigationTimeout: 30_000,   // page.goto timeout
  },
});
💡 test.slow() triples the timeout — use for known slow tests instead of hardcoding a number
⚡ The 4 timeout layers: test > action > navigation > expect — each independently configurable
📌 Per-assertion timeout overrides expect.timeout — useful for elements that load asynchronously
🟢 Default test timeout is 30s — increase for E2E flows, decrease for unit-style tests
timeoutslowconfiguration

Navigation & Waiting

Page navigation, waiting strategies, and auto-wait behavior

Navigation

Navigate between pages and wait for load states

typescript
// Navigate to URL
await page.goto('/products');

// Wait for navigation after click
await page.getByRole('link', { name: 'Products' }).click();
await page.waitForURL('/products');

// Go back/forward
await page.goBack();
await page.goForward();
💡 Playwright auto-waits for navigation to complete — manual waits are rarely needed
⚡ Use waitForResponse to synchronize on API calls triggered by user actions
📌 Avoid waitForTimeout (sleep) — prefer waiting on specific conditions like URL or element state
🟢 waitUntil: "networkidle" waits for no network requests for 500ms — useful for SPAs
navigationwaitgoto

Network Mocking & Interception

Intercept, mock, and modify network requests and responses

Intercept API calls and return custom data without hitting the server

typescript
// Mock an API endpoint
await page.route('**/api/users', async (route) => {
  await route.fulfill({
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify([{ id: 1, name: 'John' }]),
  });
});
await page.goto('/users');
await expect(page.getByText('John')).toBeVisible();
💡 Use route.fulfill({ json }) as a shorthand — Playwright sets content-type automatically
⚡ route.fetch() lets you intercept the real response and modify it before returning
📌 Route patterns use glob syntax — ** matches any path segment, * matches within a segment
🟢 Mock routes before page.goto() to ensure requests are intercepted from the start
mocknetworkrouteapi

API Testing

Test REST APIs directly without browser UI using the request fixture

API Requests

Send HTTP requests directly to test your API endpoints

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

test('can create and fetch user', async ({ request }) => {
  const res = await request.post('/api/users', {
    data: { name: 'John', email: 'john@test.com' },
  });
  expect(res.ok()).toBeTruthy();

  const users = await request.get('/api/users');
  expect(await users.json()).toContainEqual(
    expect.objectContaining({ name: 'John' })
  );
});
💡 The request fixture uses baseURL from config — no need for full URLs
⚡ Combine API calls with UI testing to set up state quickly without clicking through forms
📌 The request fixture shares cookies/auth state with the browser context
🟢 Use API testing for fast backend validation without the overhead of browser rendering
apirequesthttp

Fixtures & Page Object Model

Custom fixtures for reusable setup and the Page Object pattern

Custom Fixtures

Create reusable test fixtures for shared setup logic

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

// Define custom fixture
const test = base.extend<{ todoPage: TodoPage }>({
  todoPage: async ({ page }, use) => {
    const todoPage = new TodoPage(page);
    await todoPage.goto();
    await use(todoPage);
  },
});

test('can add todo', async ({ todoPage }) => {
  await todoPage.addTodo('Buy milk');
  await todoPage.expectTodoVisible('Buy milk');
});
💡 Page Objects encapsulate page interactions — tests read like user stories
⚡ Custom fixtures handle setup and teardown automatically for every test that uses them
📌 Export your extended test and expect from a shared file so all tests use the same fixtures
🟢 The use() callback marks where the test runs — code after it is teardown logic
fixturespage-objectpom

Authentication & Storage State

Reuse authentication state across tests for faster execution

Log in once and share the session across multiple tests

typescript
// auth.setup.ts — runs once before all tests
import { test as setup, expect } from '@playwright/test';

const authFile = 'playwright/.auth/user.json';

setup('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('user@test.com');
  await page.getByLabel('Password').fill('password');
  await page.getByRole('button', { name: 'Log in' }).click();
  await page.waitForURL('/dashboard');
  await page.context().storageState({ path: authFile });
});
💡 storageState saves cookies and localStorage — tests start already logged in
⚡ The setup project runs once, then all dependent projects reuse the saved auth state
📌 Add playwright/.auth/ to .gitignore — auth state files contain session tokens
🟢 Use dependencies in config to ensure setup runs before authenticated test projects
authloginstorage-state

Debugging & Trace Viewer

Debug failing tests with UI mode, trace viewer, and screenshots

Debugging Tools

Use UI mode, trace viewer, and headed mode to debug tests

bash
# Run in UI mode (interactive debugging)
npx playwright test --ui

# Run with browser visible
npx playwright test --headed

# Run with trace recording
npx playwright test --trace on

# View trace file
npx playwright show-trace trace.zip
💡 UI mode is the best debugging experience — shows DOM snapshots at every step
⚡ Use page.pause() in your test code to open the Inspector at a specific point
📌 Traces capture screenshots, DOM snapshots, network, and console logs for every action
🟢 Upload trace.zip to trace.playwright.dev to share with teammates — no install needed
debugtraceui-mode

Running Tests & CI

Run tests locally and in continuous integration pipelines

CLI Commands

Common commands for running and filtering tests

bash
# Run all tests
npx playwright test

# Run specific file
npx playwright test tests/login.spec.ts

# Run tests matching name
npx playwright test --grep "submit form"

# Run in specific project/browser
npx playwright test --project=chromium
💡 Use codegen to record browser interactions and generate test code automatically
⚡ --workers=1 forces serial execution — useful for debugging flaky tests
📌 Use --update-snapshots after intentional UI changes to refresh golden screenshots
🟢 Combine --grep with --project to run specific tests in specific browsers
clirunci

Run Playwright tests in a GitHub Actions workflow

yaml
# .github/workflows/playwright.yml
name: Playwright Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test
      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: playwright-report/
💡 --with-deps installs system libraries needed by browsers on the CI runner
⚡ Upload the HTML report as an artifact to debug failures without re-running
📌 Use if: ${{ !cancelled() }} so the report uploads even when tests fail
🟢 The init command generates this workflow automatically — customize as needed
cigithub-actionsworkflow