Jesse A. Green / notebook

Playwright — User Guide

Generated May 2026 from microsoft/playwright at 5790370

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

Playwright is Microsoft's open-source Node.js library for reliable, cross-browser end-to-end testing and browser automation. A single API drives Chromium, Firefox, and WebKit — the engines behind Chrome, Edge, Firefox, and Safari — so tests written once run everywhere. Auto-wait, network interception, mobile emulation, tracing, and code generation come built in.

This guide covers everything you need to install, configure, write tests, automate browsers, and troubleshoot Playwright v1.60.0-next on Node.js 18+. For architecture internals, security hardening, deployment patterns, and full configuration reference, see the Admin Guide.


1. Quick Start

Get a full Playwright project running in under two minutes.

1.1 Create a New Project

npm init playwright@latest

The initializer prompts for TypeScript or JavaScript, a tests folder name, GitHub Actions setup, and whether to install browsers. Accept the defaults for the fastest path.

Expected output:

Initializing NPM project (npm init -y)…
Installing Playwright Test (npm install -D @playwright/test)…
Writing playwright.config.ts
Creating tests/example.spec.ts
Downloading browsers…
✔ Success! Created a Playwright Test project

1.2 Run the Example Test

npx playwright test

Expected output:

Running 3 tests using 2 workers

  ✓ [chromium] › tests/example.spec.ts:3:1 › has title (2.1s)
  ✓ [firefox] › tests/example.spec.ts:3:1 › has title (3.4s)
  ✓ [webkit] › tests/example.spec.ts:3:1 › has title (2.8s)

  3 passed (8.2s)

1.3 View the HTML Report

npx playwright show-report

A browser tab opens with an interactive report. Drill into any test for traces, screenshots, and step timings.

Tip

Run npx playwright test --ui to launch UI Mode — a visual test runner with live-reload, time-travel debugging, and a watch mode that re-runs tests on file change. It is the best way to author and debug tests.

1.4 Generate Tests with Codegen

npx playwright codegen https://example.com

Two windows open: a browser for you to interact with, and an inspector that records every action as test code. Copy the generated code into your test file when done.

1.5 Minimal Test File

// tests/homepage.spec.ts
import { test, expect } from '@playwright/test';

test('homepage loads correctly', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page).toHaveTitle(/Example/);
  await expect(page.locator('h1')).toBeVisible();
});

Save this as tests/homepage.spec.ts and run npx playwright test tests/homepage.spec.ts.


2. Installation

2.1 Requirements

Requirement Minimum
Node.js 18 or higher
OS Windows 10+, macOS 12+, Ubuntu 20.04+ (Debian 11+, other distros vary)
Disk ~500 MB per browser

2.2 Install via npm

npm init -D @playwright/test

This installs the @playwright/test package. Then install browsers:

npx playwright install

Expected output:

Downloading Chromium 131.0.6778.33 (playwright build v1154)
Downloading Firefox 133.0 (playwright build v1466)
Downloading WebKit 18.2 (playwright build v2104)

2.3 Install Specific Browsers

npx playwright install chromium
npx playwright install firefox
npx playwright install webkit

2.4 Install System Dependencies

On Linux, browsers need system libraries. Install them automatically:

npx playwright install-deps

Or for a specific browser:

npx playwright install-deps chromium

Note

On CI systems (Ubuntu), always run npx playwright install --with-deps to install both browsers and OS-level libraries in one step. This is the recommended CI installation command.

2.5 Install Options Reference

Option Description
--with-deps Install OS dependencies alongside browsers
--dry-run Show what would be installed without doing it
--list List installed and available browsers
--force Force re-download even if already installed
--only-shell Install only the shell/launcher, not the full browser
--no-shell Skip shell/launcher installation

2.6 Uninstall Browsers

npx playwright uninstall          # remove browsers for current version
npx playwright uninstall --all    # remove ALL Playwright browsers

2.7 Docker Installation

For CI environments, use the official Playwright Docker image:

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

2.8 Using Playwright Library (Without Test Runner)

If you only need browser automation without the test framework:

npm install playwright

Or for the minimal core without bundled browsers:

npm install playwright-core
import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
await page.screenshot({ path: 'screenshot.png' });
await browser.close();

3. Configuration

Playwright uses a configuration file — playwright.config.ts (or .js) — at the project root. Every setting has a sensible default; you only override what you need.

3.1 Minimal Configuration

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
});

3.2 Full Working Configuration

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

export default defineConfig({
  testDir: './tests',
  timeout: 30_000,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : '50%',
  reporter: process.env.CI ? 'dot' : 'list',
  fullyParallel: true,

  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    locale: 'en-US',
    colorScheme: 'light',
    viewport: { width: 1280, height: 720 },
    testIdAttribute: 'data-testid',
    actionTimeout: 10_000,
    navigationTimeout: 30_000,
  },

  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 13'] } },
  ],

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

3.3 Configuration Defaults

Setting Default
timeout 30000 ms
workers 50% of CPU cores
testMatch **/*.@(spec|test).?(c|m)[jt]s?(x)
viewport 1280 × 720
locale en-US
colorScheme light
screenshot off
video off
trace off
testIdAttribute data-testid
reporter list (dot on CI)

3.4 Projects — Multi-Browser and Multi-Config

Projects let you run the same tests under different configurations:

projects: [
  {
    name: 'setup',
    testMatch: /global\.setup\.ts/,
  },
  {
    name: 'chromium',
    use: { ...devices['Desktop Chrome'] },
    dependencies: ['setup'],
  },
  {
    name: 'mobile',
    use: { ...devices['Pixel 5'] },
    dependencies: ['setup'],
  },
],

Use dependencies to run setup projects (like authentication) before test projects.

3.5 Web Server Configuration

Playwright can start your app before tests and shut it down after:

webServer: {
  command: 'npm run start',
  url: 'http://localhost:3000',
  reuseExistingServer: !process.env.CI,
  timeout: 120_000,
},

Multiple servers:

webServer: [
  { command: 'npm run start:api', port: 4000 },
  { command: 'npm run start:web', port: 3000 },
],

3.6 Environment Variables

Variable Purpose
PLAYWRIGHT_TEST_BASE_URL Override baseURL without changing config
PWDEBUG Set to 1 to launch Playwright Inspector for debugging
PWPAUSE Pause at end of test execution
CI Auto-detected; changes default reporter to dot
PLAYWRIGHT_HTML_TITLE Custom title for the HTML report
PLAYWRIGHT_DASHBOARD Enable dashboard features

Important

When PWDEBUG=1 is set, Playwright launches in headed mode with the Inspector attached, timeouts are disabled, and the test pauses at each await for manual stepping. This is the primary interactive debugging workflow.

3.7 Reporters

Configure one or more reporters to control test output:

reporter: [
  ['list'],
  ['html', { open: 'never' }],
  ['junit', { outputFile: 'results.xml' }],
],
Reporter Output Best For
list Step-by-step in terminal Local development
line Single updating line Compact terminal output
dot Dots for pass/fail CI (default when CI is set)
html Interactive HTML report Post-run analysis
json JSON to stdout or file Programmatic processing
junit JUnit XML CI systems (Jenkins, GitLab)
blob Binary blob Sharding, cross-machine merge
github GitHub Actions annotations GitHub CI
null No output Suppressing output

3.8 Global Setup and Teardown

// playwright.config.ts
export default defineConfig({
  globalSetup: require.resolve('./global-setup'),
  globalTeardown: require.resolve('./global-teardown'),
});
// global-setup.ts
import { chromium, FullConfig } from '@playwright/test';

async function globalSetup(config: FullConfig) {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('http://localhost:3000/login');
  await page.fill('#username', 'admin');
  await page.fill('#password', 'password');
  await page.click('button[type="submit"]');
  await page.context().storageState({ path: './auth.json' });
  await browser.close();
}
export default globalSetup;

4. Core Usage

4.1 Running Tests

Run All Tests

npx playwright test

Run a Specific File

npx playwright test tests/login.spec.ts

Run Tests Matching a Name

npx playwright test -g "login"

Run on a Specific Browser

npx playwright test --project=chromium
npx playwright test --project=firefox
npx playwright test --project=webkit

Run in Headed Mode (See the Browser)

npx playwright test --headed

Run a Single Test in Debug Mode

npx playwright test --debug tests/login.spec.ts

Run with Specific Workers

npx playwright test --workers=4
npx playwright test --workers=1    # serial execution

Run Tests with Tags

npx playwright test --grep @smoke
npx playwright test --grep-invert @slow

4.2 Test Runner CLI Reference

Flag Description
--headed Run in headed (visible) mode
--debug Launch Inspector, pause on first action
--ui Open UI Mode for interactive development
-g, --grep <regex> Filter tests by title
--grep-invert <regex> Exclude tests by title
--project <name> Run only the named project(s)
--workers <count> Number of parallel workers
--retries <count> Retry failed tests N times
--reporter <name> Override reporter
--output <dir> Test artifacts directory
--timeout <ms> Override test timeout
--max-failures <N> Stop after N failures
--shard <current/total> Shard tests (e.g., 1/3)
--repeat-each <N> Repeat each test N times
--forbid-only Fail if test.only exists (CI guard)
--update-snapshots Update visual comparison baselines
--pass-with-no-tests Don't fail on empty test suite
--last-failed Re-run only previously failed tests
-c, --config <file> Path to config file

4.3 Writing Tests

Basic Test Structure

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

test.describe('Feature: Shopping Cart', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/shop');
  });

  test('adds item to cart', async ({ page }) => {
    await page.click('[data-testid="add-to-cart"]');
    await expect(page.locator('.cart-count')).toHaveText('1');
  });

  test('removes item from cart', async ({ page }) => {
    await page.click('[data-testid="add-to-cart"]');
    await page.click('[data-testid="remove-from-cart"]');
    await expect(page.locator('.cart-count')).toHaveText('0');
  });
});

Test Annotations

test('feature behind flag', async ({ page }) => {
  test.skip(process.env.FEATURE_FLAG !== 'true', 'Feature not enabled');
  // ...
});

test('known flaky on webkit', async ({ page, browserName }) => {
  test.fixme(browserName === 'webkit', 'WebKit rendering issue');
});

test.slow('large data import', async ({ page }) => {
  // timeout tripled automatically
});

test.only('debug this test', async ({ page }) => {
  // only this test runs (remove before commit)
});

Tagging Tests

test('login @smoke @auth', async ({ page }) => {
  // ...
});

test('complex report @slow', async ({ page }) => {
  // ...
});

Run tagged tests: npx playwright test --grep @smoke

4.4 Locators

Locators are the preferred way to find elements. They auto-wait and auto-retry.

// By role (best for accessibility)
page.getByRole('button', { name: 'Submit' });
page.getByRole('heading', { name: 'Welcome' });
page.getByRole('link', { name: 'Sign Up' });
page.getByRole('textbox', { name: 'Email' });
page.getByRole('checkbox', { name: 'Remember me' });

// By label text
page.getByLabel('Username');
page.getByLabel('Password');

// By placeholder
page.getByPlaceholder('Search...');

// By visible text
page.getByText('Welcome back');
page.getByText(/welcome/i);  // regex

// By alt text (images)
page.getByAltText('Company Logo');

// By title attribute
page.getByTitle('Close dialog');

// By test ID (configurable via testIdAttribute)
page.getByTestId('submit-button');

CSS and XPath (When Needed)

page.locator('css=button.primary');
page.locator('#login-form input[type="email"]');
page.locator('xpath=//button[@aria-label="Close"]');

Locator Filtering and Chaining

// Filter by text
page.getByRole('listitem').filter({ hasText: 'Product' });

// Filter by child locator
page.getByRole('listitem').filter({
  has: page.getByRole('button', { name: 'Buy' }),
});

// Nth item
page.getByRole('listitem').nth(2);
page.getByRole('listitem').first();
page.getByRole('listitem').last();

4.5 Actions

All actions auto-wait for the element to be actionable (visible, enabled, stable).

// Click
await page.click('button#submit');
await page.dblclick('.card');
await page.click('button', { button: 'right' });

// Fill form fields (clears first)
await page.fill('#email', '[email removed]');
await page.fill('#password', 's3cure!');

// Type character by character (for autocomplete testing)
await page.type('#search', 'playwright', { delay: 100 });

// Press keyboard keys
await page.press('#search', 'Enter');
await page.keyboard.press('Control+A');

// Select dropdown
await page.selectOption('select#country', 'US');
await page.selectOption('select#colors', ['red', 'blue']);

// Checkbox and radio
await page.check('#agree-terms');
await page.uncheck('#newsletter');

// File upload
await page.setInputFiles('input[type="file"]', 'path/to/file.pdf');
await page.setInputFiles('input[type="file"]', ['file1.jpg', 'file2.jpg']);

// Hover
await page.hover('.tooltip-trigger');

// Drag and drop
await page.dragAndDrop('#source', '#target');

// Focus
await page.focus('#input-field');

4.6 Assertions

Playwright assertions auto-retry until the condition is met or the timeout expires.

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

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

// Visibility
await expect(page.locator('.alert')).toBeVisible();
await expect(page.locator('.spinner')).toBeHidden();
await expect(page.locator('.modal')).not.toBeVisible();

// Text content
await expect(page.locator('h1')).toHaveText('Welcome');
await expect(page.locator('h1')).toContainText('Welc');
await expect(page.locator('.items')).toHaveCount(5);

// Input values
await expect(page.locator('#email')).toHaveValue('[email removed]');
await expect(page.locator('#name')).toBeEmpty();

// Attributes and CSS
await expect(page.locator('button')).toHaveAttribute('disabled', '');
await expect(page.locator('button')).toBeEnabled();
await expect(page.locator('button')).toBeDisabled();
await expect(page.locator('.item')).toHaveClass(/selected/);
await expect(page.locator('.box')).toHaveCSS('color', 'rgb(255, 0, 0)');

// Checked state
await expect(page.locator('#checkbox')).toBeChecked();
await expect(page.locator('#checkbox')).not.toBeChecked();

// Attached to DOM
await expect(page.locator('.dynamic')).toBeAttached();

// Screenshot comparison
await expect(page).toHaveScreenshot('homepage.png');
await expect(page.locator('.chart')).toHaveScreenshot('chart.png', {
  maxDiffPixels: 100,
});

Tip

Always prefer Playwright's auto-retrying expect assertions over manual if checks or raw textContent() calls. Auto-retrying assertions handle timing naturally and eliminate flakiness from elements that haven't rendered yet.

4.7 Navigation

// Navigate
await page.goto('https://example.com');
await page.goto('/dashboard');  // uses baseURL from config

// Wait for navigation
await page.waitForURL('**/dashboard');
await page.waitForLoadState('networkidle');

// Back and forward
await page.goBack();
await page.goForward();

// Reload
await page.reload();

4.8 Fixtures

Fixtures provide isolated resources for each test. Playwright's built-in fixtures give you a fresh page and context per test.

Built-in Test Fixtures

Fixture Scope Description
page Test Isolated page instance
context Test Browser context (cookies, storage)
browser Worker Shared browser instance
browserName Worker chromium, firefox, or webkit
playwright Worker Playwright instance
request Test API request context

Custom Fixtures

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

type MyFixtures = {
  adminPage: Page;
  userPage: Page;
};

const test = base.extend<MyFixtures>({
  adminPage: async ({ browser }, use) => {
    const context = await browser.newContext({
      storageState: './admin-auth.json',
    });
    const page = await context.newPage();
    await use(page);
    await context.close();
  },

  userPage: async ({ browser }, use) => {
    const context = await browser.newContext({
      storageState: './user-auth.json',
    });
    const page = await context.newPage();
    await use(page);
    await context.close();
  },
});

test('admin sees dashboard', async ({ adminPage }) => {
  await adminPage.goto('/admin');
  await expect(adminPage.locator('h1')).toHaveText('Admin Dashboard');
});

Fixture Configuration via Config

Many fixtures are configurable directly in playwright.config.ts:

use: {
  acceptDownloads: true,
  bypassCSP: false,
  colorScheme: 'dark',
  geolocation: { latitude: 40.7128, longitude: -74.0060 },
  hasTouch: false,
  httpCredentials: { username: 'admin', password: 'pass' },
  ignoreHTTPSErrors: true,
  isMobile: false,
  javaScriptEnabled: true,
  locale: 'en-US',
  offline: false,
  permissions: ['geolocation'],
  timezoneId: 'America/New_York',
  userAgent: 'Custom UA String',
  viewport: { width: 1920, height: 1080 },
  serviceWorkers: 'block',
  baseURL: 'http://localhost:3000',
  actionTimeout: 15_000,
  navigationTimeout: 30_000,
  testIdAttribute: 'data-testid',
},

4.9 Page Object Model

Organize tests with page objects for maintainability:

// pages/login-page.ts
import { Page, Locator, expect } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.getByLabel('Email');
    this.passwordInput = page.getByLabel('Password');
    this.submitButton = page.getByRole('button', { name: 'Sign In' });
    this.errorMessage = page.locator('.error-message');
  }

  async goto() {
    await this.page.goto('/login');
  }

  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }

  async expectError(message: string) {
    await expect(this.errorMessage).toHaveText(message);
  }
}
// tests/login.spec.ts
import { test } from '@playwright/test';
import { LoginPage } from '../pages/login-page';

test('shows error for bad credentials', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('[email removed]', 'wrong');
  await loginPage.expectError('Invalid credentials');
});

5. Common Tasks

5.1 Authentication and Storage State

Reuse authentication across tests by saving and loading browser storage state.

Save Authentication State (Setup Project)

// auth.setup.ts
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('[email removed]');
  await page.getByLabel('Password').fill('password123');
  await page.getByRole('button', { name: 'Sign In' }).click();

  await page.waitForURL('/dashboard');
  await page.context().storageState({ path: authFile });
});

Use Saved State in Config

// playwright.config.ts
projects: [
  { name: 'setup', testMatch: /.*\.setup\.ts/ },
  {
    name: 'chromium',
    use: {
      ...devices['Desktop Chrome'],
      storageState: 'playwright/.auth/user.json',
    },
    dependencies: ['setup'],
  },
],

Note

Add playwright/.auth/ to your .gitignore to avoid committing session tokens.

5.2 API Testing

Playwright includes a built-in API testing client — no additional packages needed.

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

test('API: create and fetch user', async ({ request }) => {
  // POST
  const createResponse = await request.post('/api/users', {
    data: {
      name: 'Jane Doe',
      email: '[email removed]',
    },
  });
  expect(createResponse.ok()).toBeTruthy();
  const user = await createResponse.json();
  expect(user.name).toBe('Jane Doe');

  // GET
  const getResponse = await request.get(`/api/users/${user.id}`);
  expect(getResponse.ok()).toBeTruthy();
  const fetched = await getResponse.json();
  expect(fetched.email).toBe('[email removed]');

  // DELETE
  const deleteResponse = await request.delete(`/api/users/${user.id}`);
  expect(deleteResponse.status()).toBe(204);
});

5.3 Network Interception and Mocking

Mock API Responses

test('shows mocked data', async ({ page }) => {
  await page.route('**/api/products', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([
        { id: 1, name: 'Mock Product', price: 9.99 },
      ]),
    });
  });

  await page.goto('/products');
  await expect(page.locator('.product-name')).toHaveText('Mock Product');
});

Modify Responses

test('modify API response', async ({ page }) => {
  await page.route('**/api/user/profile', async (route) => {
    const response = await route.fetch();
    const json = await response.json();
    json.name = 'Modified Name';
    await route.fulfill({ response, body: JSON.stringify(json) });
  });

  await page.goto('/profile');
});

Block Resources

test('block images for speed', async ({ page }) => {
  await page.route('**/*.{png,jpg,jpeg,gif,svg}', (route) => route.abort());
  await page.goto('/gallery');
});

HAR Recording and Replay

# Record network traffic
npx playwright open --save-har=traffic.har https://example.com
test('replay from HAR', async ({ page }) => {
  await page.routeFromHAR('traffic.har', { update: false });
  await page.goto('https://example.com');
});

5.4 Screenshots and Visual Comparison

Capture Screenshots

// Full page
await page.screenshot({ path: 'screenshots/full.png', fullPage: true });

// Specific element
await page.locator('.chart').screenshot({ path: 'screenshots/chart.png' });

// Clip a region
await page.screenshot({
  path: 'screenshots/header.png',
  clip: { x: 0, y: 0, width: 1280, height: 200 },
});

Visual Regression Testing

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

test('component visual', async ({ page }) => {
  await page.goto('/components');
  await expect(page.locator('.card')).toHaveScreenshot('card.png', {
    maxDiffPixelRatio: 0.01,
  });
});

Update baselines after intentional changes:

npx playwright test --update-snapshots

CLI Screenshot

npx playwright screenshot https://example.com screenshot.png --full-page
npx playwright screenshot https://example.com hero.png --wait-for-selector="h1"

5.5 Video Recording

Enable video per project in config:

use: {
  video: 'retain-on-failure',   // or 'on', 'off', 'on-first-retry'
},

Videos are saved to test-results/ and embedded in the HTML report.

5.6 Tracing

Traces capture a complete timeline of actions, DOM snapshots, network requests, and console logs.

Enable in Config

use: {
  trace: 'on-first-retry',  // or 'on', 'off', 'retain-on-failure'
},

Capture Manually in a Test

test('traced workflow', async ({ page, context }) => {
  await context.tracing.start({ screenshots: true, snapshots: true });

  await page.goto('/checkout');
  await page.fill('#card', '4242424242424242');
  await page.click('#pay');

  await context.tracing.stop({ path: 'trace.zip' });
});

View a Trace

npx playwright show-trace trace.zip

The Trace Viewer opens in a browser with a timeline, DOM snapshots at each step, network log, and console output. You can step through actions and inspect the page state at any point.

5.7 PDF Generation (Chromium Only)

const page = await browser.newPage();
await page.goto('https://example.com/report');
await page.pdf({
  path: 'report.pdf',
  format: 'A4',
  printBackground: true,
});

CLI shortcut:

npx playwright pdf https://example.com/report report.pdf --paper-format=A4

5.8 File Downloads

test('download report', async ({ page }) => {
  const downloadPromise = page.waitForEvent('download');
  await page.click('a#download-report');
  const download = await downloadPromise;

  expect(download.suggestedFilename()).toBe('report.csv');
  await download.saveAs('./downloads/report.csv');
});

Tip

Set acceptDownloads: true in config or fixture options to allow downloads. It is true by default with @playwright/test fixtures.

5.9 File Uploads

test('upload avatar', async ({ page }) => {
  await page.goto('/settings');

  // Single file
  await page.setInputFiles('input[type="file"]', 'fixtures/avatar.png');

  // Multiple files
  await page.setInputFiles('input[type="file"]', [
    'fixtures/doc1.pdf',
    'fixtures/doc2.pdf',
  ]);

  // Clear selection
  await page.setInputFiles('input[type="file"]', []);
});

5.10 Handling Dialogs (alert, confirm, prompt)

test('handles confirm dialog', async ({ page }) => {
  page.on('dialog', async (dialog) => {
    expect(dialog.type()).toBe('confirm');
    expect(dialog.message()).toBe('Delete this item?');
    await dialog.accept();
  });

  await page.click('#delete-button');
});

test('dismisses prompt', async ({ page }) => {
  page.on('dialog', (dialog) => dialog.dismiss());
  await page.click('#rename-button');
});

5.11 iFrames

test('interact with iframe', async ({ page }) => {
  const frame = page.frameLocator('#payment-iframe');
  await frame.locator('#card-number').fill('4242424242424242');
  await frame.locator('#submit').click();
});

5.12 Multiple Pages and Tabs

test('handle popup', async ({ page }) => {
  const popupPromise = page.waitForEvent('popup');
  await page.click('a[target="_blank"]');
  const popup = await popupPromise;

  await popup.waitForLoadState();
  await expect(popup).toHaveTitle(/New Tab/);
});

5.13 Mobile Emulation

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

// In config
projects: [
  {
    name: 'mobile-safari',
    use: {
      ...devices['iPhone 13'],
    },
  },
  {
    name: 'tablet',
    use: {
      ...devices['iPad Pro 11'],
    },
  },
],

Or per test:

test('mobile layout', async ({ browser }) => {
  const context = await browser.newContext({
    ...devices['Pixel 5'],
  });
  const page = await context.newPage();
  await page.goto('/');
  // test mobile-specific behavior
  await context.close();
});

5.14 Geolocation, Timezone, and Locale

// Config-level
use: {
  geolocation: { latitude: 48.8566, longitude: 2.3522 },
  permissions: ['geolocation'],
  timezoneId: 'Europe/Paris',
  locale: 'fr-FR',
},
// Per test
test('paris location', async ({ browser }) => {
  const context = await browser.newContext({
    geolocation: { latitude: 48.8566, longitude: 2.3522 },
    permissions: ['geolocation'],
    locale: 'fr-FR',
    timezoneId: 'Europe/Paris',
  });
  const page = await context.newPage();
  await page.goto('/store-locator');
  await context.close();
});

5.15 Dark Mode and Color Scheme

use: {
  colorScheme: 'dark',
},

Or toggle mid-test:

test('toggle dark mode', async ({ page }) => {
  await page.emulateMedia({ colorScheme: 'dark' });
  await expect(page.locator('body')).toHaveCSS(
    'background-color', 'rgb(0, 0, 0)'
  );

  await page.emulateMedia({ colorScheme: 'light' });
  await expect(page.locator('body')).toHaveCSS(
    'background-color', 'rgb(255, 255, 255)'
  );
});

5.16 Parallel Execution and Sharding

By default, Playwright runs test files in parallel with 50% of CPU cores as workers.

// Make all tests in a file run in parallel (not just across files)
test.describe.configure({ mode: 'parallel' });

For CI sharding across machines:

# Machine 1
npx playwright test --shard=1/3

# Machine 2
npx playwright test --shard=2/3

# Machine 3
npx playwright test --shard=3/3

Merge Sharded Reports

npx playwright merge-reports ./blob-reports --reporter=html

5.17 Test Retries

// playwright.config.ts
export default defineConfig({
  retries: process.env.CI ? 2 : 0,
});

In tests, detect retry:

test('may need retry', async ({ page }, testInfo) => {
  if (testInfo.retry) {
    await page.goto('/clear-cache');
  }
  // ...
});

5.18 Parameterized Tests

const users = [
  { role: 'admin', expectDashboard: true },
  { role: 'viewer', expectDashboard: false },
  { role: 'editor', expectDashboard: true },
];

for (const { role, expectDashboard } of users) {
  test(`${role} sees correct dashboard`, async ({ page }) => {
    await page.goto(`/login?role=${role}`);
    if (expectDashboard) {
      await expect(page.locator('#dashboard')).toBeVisible();
    } else {
      await expect(page.locator('#dashboard')).not.toBeVisible();
    }
  });
}

5.19 Code Generation with Codegen

Record interactions as test code automatically:

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

# Save to file
npx playwright codegen -o tests/recorded.spec.ts https://example.com

# Target a different language
npx playwright codegen --target python https://example.com
npx playwright codegen --target java https://example.com
npx playwright codegen --target csharp https://example.com

# Emulate a device
npx playwright codegen --device="iPhone 13" https://example.com

# Set viewport
npx playwright codegen --viewport-size=800,600 https://example.com

# Custom test ID attribute
npx playwright codegen --test-id-attribute=data-cy https://example.com

Available --target languages:

Target Output
javascript Playwright library script
playwright-test @playwright/test test file (default)
python Python sync script
python-async Python async script
python-pytest pytest-playwright test
csharp C# script
csharp-mstest C# MSTest
csharp-nunit C# NUnit
java Java script
java-junit Java JUnit test

5.20 Browser Open and Inspect

Open a browser manually for exploration:

# Default browser (Chromium)
npx playwright open https://example.com

# Specific browser
npx playwright cr https://example.com    # Chromium
npx playwright ff https://example.com    # Firefox
npx playwright wk https://example.com    # WebKit

# With emulation
npx playwright open --device="iPhone 13" https://example.com
npx playwright open --color-scheme=dark https://example.com
npx playwright open --timezone="Asia/Tokyo" https://example.com
npx playwright open --geolocation="35.6762,139.6503" https://example.com

5.21 Waiting Strategies

Playwright auto-waits for elements before acting on them, but sometimes you need explicit waits:

// Wait for a specific element
await page.waitForSelector('.results-loaded');

// Wait for navigation
await page.waitForURL('**/success');

// Wait for load state
await page.waitForLoadState('networkidle');

// Wait for a response
const response = await page.waitForResponse('**/api/data');

// Wait for a request
const request = await page.waitForRequest('**/api/submit');

// Wait for a function to return truthy
await page.waitForFunction(() => document.title.includes('Ready'));

// Explicit timeout
await page.waitForTimeout(1000);  // avoid in tests; prefer auto-wait

Warning

Avoid page.waitForTimeout() in tests. It introduces artificial delays and makes tests slow and fragile. Use locator assertions (await expect(locator).toBeVisible()) or event-based waits instead.

5.22 Console and Error Capture

test('capture console errors', async ({ page }) => {
  const errors: string[] = [];
  page.on('console', (msg) => {
    if (msg.type() === 'error') errors.push(msg.text());
  });

  page.on('pageerror', (error) => {
    errors.push(error.message);
  });

  await page.goto('/');

  expect(errors).toEqual([]);
});

5.23 Accessibility Testing

test('page is accessible', async ({ page }) => {
  await page.goto('/');

  // Check for accessibility violations using @axe-core/playwright
  // npm install -D @axe-core/playwright
  const { checkA11y } = require('@axe-core/playwright');

  const accessibilityScanResults = await new AxeBuilder({ page })
    .analyze();

  expect(accessibilityScanResults.violations).toEqual([]);
});

5.24 Component Testing

Playwright supports component testing for React and Vue applications.

npm install -D @playwright/ct-react    # React 18+
npm install -D @playwright/ct-react17  # React 17
npm install -D @playwright/ct-vue      # Vue
// src/Button.spec.tsx
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './Button';

test('renders with text', async ({ mount }) => {
  const component = await mount(<Button>Click me</Button>);
  await expect(component).toContainText('Click me');
});

test('handles click', async ({ mount }) => {
  let clicked = false;
  const component = await mount(
    <Button onClick={() => (clicked = true)}>Click</Button>
  );
  await component.click();
  expect(clicked).toBeTruthy();
});

5.25 Running a Playwright Server

For remote browser connections:

npx playwright run-server --port=3001 --host=0.0.0.0
Option Description
--port Port to listen on
--host Host to bind to
--path WebSocket path prefix
--max-clients Maximum concurrent connections
--mode Server mode
--artifacts-dir Directory for artifacts

Connect from a client:

import { chromium } from 'playwright';

const browser = await chromium.connect('ws://server-host:3001');
const page = await browser.newPage();
await page.goto('https://example.com');

5.26 Storage State for Multi-User Scenarios

test('admin and user interaction', async ({ browser }) => {
  // Admin context
  const adminContext = await browser.newContext({
    storageState: 'playwright/.auth/admin.json',
  });
  const adminPage = await adminContext.newPage();

  // User context
  const userContext = await browser.newContext({
    storageState: 'playwright/.auth/user.json',
  });
  const userPage = await userContext.newPage();

  // Admin creates content
  await adminPage.goto('/admin/posts');
  await adminPage.click('#create-post');
  await adminPage.fill('#title', 'New Announcement');
  await adminPage.click('#publish');

  // User sees it
  await userPage.goto('/posts');
  await expect(userPage.locator('.post-title')).toContainText('New Announcement');

  await adminContext.close();
  await userContext.close();
});

5.27 Handling HTTP Credentials and Proxies

// Basic auth
use: {
  httpCredentials: {
    username: 'admin',
    password: 'secret',
  },
},

// Proxy
use: {
  proxy: {
    server: 'http://proxy.example.com:8080',
    username: 'proxyuser',
    password: 'proxypass',
  },
},

CLI with proxy:

npx playwright open --proxy-server=http://proxy:8080 https://example.com

5.28 Block Service Workers

// In config
use: {
  serviceWorkers: 'block',
},

Or via CLI:

npx playwright open --block-service-workers https://example.com

5.29 Custom Test ID Attribute

If your app uses a custom attribute for test selectors (e.g., data-cy for Cypress migration):

// playwright.config.ts
use: {
  testIdAttribute: 'data-cy',
},

Now page.getByTestId('submit') looks for [data-cy="submit"] instead of [data-testid="submit"].

5.30 MCP Test Server

Playwright includes an MCP (Model Context Protocol) server for AI-assisted testing:

npx playwright run-test-mcp-server --headless --config=playwright.config.ts
Option Description
--headless Run browsers in headless mode
-c, --config Path to config file
--host Host to bind to
--port Port to listen on

5.31 Test Agents

Initialize test agents for AI-driven testing:

npx playwright init-agents --config=playwright.config.ts --project=chromium
Option Description
-c, --config Path to config file
--project Target project
--prompts Agent prompt configuration

6. Troubleshooting

6.1 Installation Problems

Error: Host system is missing dependencies to run browsers

Cause: Linux system lacks required shared libraries for browser binaries.

Solution:

npx playwright install-deps

Or for a specific browser:

npx playwright install-deps chromium

Prevention: Always use npx playwright install --with-deps on fresh CI images.


Error: browserType.launch: Executable doesn't exist at ...

Cause: Browsers were not installed, or were installed for a different Playwright version.

Solution:

npx playwright install

Prevention: Add npx playwright install to your CI setup step. Pin your Playwright version in package.json.


Error: Cannot find module '@playwright/test'

Cause: Package not installed or not in the correct node_modules.

Solution:

npm install -D @playwright/test

6.2 Test Execution Problems

Error: Timeout 30000ms exceeded

Cause: Action or assertion took longer than the default 30-second timeout. Common with slow pages, network latency, or elements that never appear.

Solution:

  1. Increase timeout for the specific test:
test('slow operation', async ({ page }) => {
  test.setTimeout(60_000);
  // ...
});
  1. Or increase globally in config:
timeout: 60_000,
use: {
  actionTimeout: 15_000,
  navigationTimeout: 60_000,
},
  1. Investigate whether the element actually appears — use --debug mode to watch.

Error: strict mode violation: locator resolved to N elements

Cause: A locator matched multiple elements when only one was expected.

Solution: Make the locator more specific:

// Too broad
await page.click('button');

// Specific
await page.click('button:has-text("Submit")');
// or
await page.getByRole('button', { name: 'Submit' }).click();

Error: Element is not visible

Cause: Attempting to interact with a hidden element.

Solution:

// Wait for visibility first
await expect(page.locator('#modal')).toBeVisible();
await page.locator('#modal button').click();

// Or force the action (use sparingly)
await page.click('#hidden-button', { force: true });

Error: waiting for selector "..." to be visible

Cause: The element exists in DOM but is not rendered (display: none, visibility: hidden, zero size).

Solution: Check that the element becomes visible before interacting. Use the browser DevTools or Trace Viewer to inspect the DOM state at the time of failure.


Error: page.goto: net::ERR_CONNECTION_REFUSED

Cause: The target URL is not reachable. The dev server may not have started yet.

Solution:

  1. Verify the webServer config starts your app correctly.
  2. Ensure the URL and port match between webServer and baseURL.
  3. Add a longer timeout to webServer:
webServer: {
  command: 'npm start',
  url: 'http://localhost:3000',
  timeout: 120_000,
},

Playwright stuck at about:blank

Cause: This is a known issue (GitHub #31050, 50+ comments) where the browser opens but never navigates. Often caused by browser extensions, corrupted profiles, or system-level browser interference.

Solution:

  1. Clear the browser cache: npx playwright clear-cache
  2. Ensure no other Playwright processes are running
  3. Try a different browser: --project=firefox
  4. On Linux, check that --no-sandbox is not needed (Docker/CI)

Tests pass locally but fail on CI

Cause: Timing differences, missing fonts, different screen resolution, or missing system dependencies.

Solution:

  1. Use npx playwright install --with-deps in CI
  2. Set retries: 2 for CI in config
  3. Use trace: 'on-first-retry' to debug CI failures
  4. Set deterministic viewport and locale in config
  5. Avoid networkidle waits — prefer element-based assertions

6.3 Browser-Specific Issues

WebKit WASM breakage (v1.59)

Cause: Known regression in v1.59 (GitHub #40268) where WebAssembly modules fail in WebKit.

Solution: Upgrade to v1.60+ where this is addressed, or skip WebKit for WASM-dependent tests:

test('wasm feature', async ({ page, browserName }) => {
  test.skip(browserName === 'webkit', 'WebKit WASM issue #40268');
  // ...
});

Firefox cannot read response JSON from GET requests

Cause: Known issue with Firefox 146 (GitHub #39160).

Solution: Use response.text() and parse manually as a workaround, or check for Playwright updates that bundle a patched Firefox.


Response.formData() throws TypeError in WebKit

Cause: WebKit 26.4 regression (GitHub #40244).

Solution: Parse the response body manually instead of using formData().


Proxy auth causes redirects to hang in Chromium

Cause: When using proxy authentication without page.route(), Chromium may hang on redirects (GitHub #40194).

Solution: Set proxy credentials in the browser context options rather than relying on HTTP authentication prompts:

const context = await browser.newContext({
  proxy: {
    server: 'http://proxy:8080',
    username: 'user',
    password: 'pass',
  },
});

6.4 Dialog and Popup Issues

page.on('dialog') not working with external URL handlers

Cause: Known limitation (GitHub #11014, 37 comments) where dialogs triggered by external URL scheme handlers (e.g., mailto:, tel:) are not intercepted by Playwright's dialog handler.

Solution: Mock the navigation instead of handling the dialog:

await page.route('**/*', (route) => {
  if (route.request().url().startsWith('mailto:')) {
    return route.abort();
  }
  return route.continue();
});

6.5 TypeScript Issues

Error: Cannot run TypeScript files with "declare"

Cause: TypeScript files using declare keyword (ambient declarations) can conflict with Playwright's test loader (GitHub #38586).

Solution: Move ambient declarations to a separate .d.ts file and ensure tsconfig.json includes it properly:

{
  "compilerOptions": {
    "strict": true
  },
  "include": ["tests/**/*.ts", "types/**/*.d.ts"]
}

6.6 VS Code Extension Issues

VS Code extension showing "No tests have been found"

Cause: The extension may not detect tests if the config file is in a subdirectory, the test file pattern does not match, or the extension version is mismatched (GitHub #40226).

Solution:

  1. Ensure playwright.config.ts is at the workspace root
  2. Verify testDir in config points to the correct directory
  3. Check that test files match the pattern **/*.@(spec|test).?(c|m)[jt]s?(x)
  4. Restart the extension: Ctrl+Shift+P > "Playwright: Reload"
  5. Ensure extension version matches your installed Playwright version

6.7 Performance Problems

Tests are too slow

Solution:

  1. Run in parallel (default): ensure fullyParallel: true is set
  2. Use more workers: --workers=8
  3. Block unnecessary resources:
await page.route('**/*.{png,jpg,css,font}', (route) => route.abort());
  1. Reuse authentication state instead of logging in per test
  2. Use webServer.reuseExistingServer in local development
  3. Avoid waitForLoadState('networkidle') — it waits for 500ms of network silence

Out of memory in CI

Solution:

  1. Reduce workers: --workers=1 or --workers=2
  2. Shard across machines instead of parallelizing on one
  3. Close contexts explicitly in tests
  4. Disable video and trace unless needed

6.8 Debugging Techniques

Debug a Specific Test

PWDEBUG=1 npx playwright test tests/login.spec.ts

The Playwright Inspector opens alongside the browser. Step through actions, inspect locators, and evaluate selectors in real time.

Pause Mid-Test

test('debug this', async ({ page }) => {
  await page.goto('/');
  await page.pause();  // Inspector opens here
  await page.click('#next');
});

Verbose Logging

DEBUG=pw:api npx playwright test

Other debug channels: pw:browser, pw:protocol, pw:channel.

Review Traces After Failure

npx playwright show-trace test-results/my-test-chromium/trace.zip

7. FAQ

Q: Which browsers does Playwright support? Playwright supports Chromium (Chrome, Edge), Firefox, and WebKit (Safari) — the three major browser engines. Each Playwright release bundles specific browser versions that are tested and supported.

Q: Can I use Playwright with JavaScript instead of TypeScript? Yes. Use playwright.config.js instead of .ts and write tests as .spec.js files. TypeScript is optional but recommended for autocompletion and type safety.

Q: How do I run tests on only one browser? Use --project=chromium, --project=firefox, or --project=webkit. These correspond to project names in your config.

Q: What is the difference between playwright and playwright-core? playwright bundles browser download logic and defaults. playwright-core is the bare library without auto-download — intended for library authors or custom browser management. For testing, always use @playwright/test which includes everything.

Q: How do I run tests in headed mode to see the browser? Use npx playwright test --headed. For debugging, npx playwright test --debug opens headed mode with the Inspector.

Q: How do I test multiple viewports or devices? Define multiple projects in your config, each with different devices settings. Playwright includes device descriptors for dozens of phones and tablets.

Q: Can I test a site that requires login? Yes. Use a setup project to authenticate once, save storageState to a JSON file, and load it in subsequent projects. See section 5.1.

Q: How do I handle pop-ups and new tabs? Use page.waitForEvent('popup') to capture the new page object. See section 5.12.

Q: How do I test file downloads? Set acceptDownloads: true (default in test fixtures), then use page.waitForEvent('download'). See section 5.8.

Q: What is the baseURL setting for? It allows you to use relative URLs in page.goto('/path') instead of full URLs. Set it in config or override with the PLAYWRIGHT_TEST_BASE_URL environment variable.

Q: How do I debug tests on CI? Enable trace: 'on-first-retry' in your config. Failed tests produce trace files you can download and view locally with npx playwright show-trace.

Q: How do I test Web Components or Shadow DOM? Playwright pierces Shadow DOM automatically. Locators like page.locator('my-component >> button') work without extra configuration.

Q: Can I run Playwright tests in parallel? Yes, parallelism is on by default. Test files run in parallel across workers (default: 50% of CPU cores). Use test.describe.configure({ mode: 'parallel' }) for intra-file parallelism.

Q: How do I shard tests across CI machines? Use --shard=1/3, --shard=2/3, --shard=3/3 on each machine, then merge results with npx playwright merge-reports.

Q: How do I retry flaky tests? Set retries: 2 in your config. Playwright re-runs failed tests up to that many times. Use trace: 'on-first-retry' to automatically capture traces for debugging the first failure.

Q: How do I test APIs without a browser? Use the request fixture which provides an API client. See section 5.2. No browser is launched for API-only tests using request.

Q: Can I intercept and mock network requests? Yes. Use page.route() to intercept requests and route.fulfill() to return mock responses. See section 5.3.

Q: What file naming convention should I use for tests? The default pattern matches *.spec.ts, *.spec.js, *.test.ts, *.test.js (and their .mjs, .cjs, .mts, .cts, .tsx, .jsx variants). Use .spec.ts by convention.

Q: How do I update snapshot baselines? Run npx playwright test --update-snapshots. This regenerates all screenshot baselines.

Q: How do I skip a test on a specific browser? Use conditional skip inside the test:

test('chromium only feature', async ({ browserName }) => {
  test.skip(browserName !== 'chromium', 'Chromium-only feature');
  // ...
});

Q: Can I use environment variables in tests? Yes. Access them via process.env in config or test files. Use dotenv for .env file loading, or pass them inline: MY_VAR=value npx playwright test.

Q: How do I generate PDF output from a page? Use page.pdf() in Chromium only. See section 5.7. Firefox and WebKit do not support PDF generation.

Q: What is UI Mode? npx playwright test --ui opens an interactive test runner with live-reload, time-travel debugging, and a visual timeline. It is the recommended way to author and debug tests during development.

Q: How do I clear Playwright's cache? Run npx playwright clear-cache. This removes cached browser binaries and test artifacts.

Q: What is the Trace Viewer? A web-based tool that visualizes test execution: DOM snapshots, network traffic, console logs, and action timeline. Open with npx playwright show-trace <trace.zip>.

Q: Can I connect to a remote browser? Yes. Start a server with npx playwright run-server and connect with browser.connect('ws://host:port'). See section 5.25.

Q: How do I test responsive design? Configure multiple projects with different viewport sizes or use the device descriptors (devices['iPhone 13'], devices['iPad Pro 11'], etc.).