Skip to main content

Playwright Cheatsheet for JavaScript & TypeScript

A comprehensive reference for Playwright scripting, testing, and browser gestures, with copy-ready code snippets.

Automation Quick Start

Install Playwright and local browsers

BashCopy
npm i playwright
npx playwright install --with-deps
npm i -D typescript tsx # optional

Create a script file

BashCopy
touch src/example.js # with JS
touch src/example.ts # with TS

Write a basic script

TypeScriptCopy
import * as pw from 'playwright';

const browser = await pw.chromium.launch();
const page = await browser.newPage();
await page.goto('https://www.browsersolver.com');
await browser.close();

Generate code from user behavior

BashCopy
npx playwright codegen

Run your script

BashCopy
node src/example.js # with JS
npx tsx src/example.ts # with TS

Launch Browsers Locally

Launch Chromium

TypeScriptCopy
const chromium = await pw.chromium.launch();

Launch Firefox

TypeScriptCopy
const firefox = await pw.firefox.launch();

Launch Webkit

TypeScriptCopy
const webkit = await pw.webkit.launch();

Persistent User Data

Browser contexts are normally flushed. But you can optionally store context between sessions.

Launch browser with persist user data

TypeScriptCopy
const userDataDir = './userData';
const context = await pw.chromium
  .launchPersistentContext(userDataDir);

Configure persistent context options

TypeScriptCopy
// accepts all options from Browser.newContext()
const context = await pw.chromium
  .launchPersistentContext(userDataDir, {
    acceptDownloads: true,
    ignoreHTTPSErrors: true,
  });

Chrome Browser Variants

Install Chrome

BashCopy
npx playwright install --with-deps chrome

Install Chrome Beta

BashCopy
npx playwright install --with-deps chrome-beta

Launch Chrome

TypeScriptCopy
const chrome = await pw.chromium.launch({
  channel: 'chrome',
});

Launch Chrome Beta

TypeScriptCopy
const chrome = await pw.chromium.launch({
  channel: 'chrome-beta',
});

Launch Chrome Dev

TypeScriptCopy
const chrome = await pw.chromium.launch({
  channel: 'chrome-dev',
  executablePath: '/path/to/chrome-dev',
});

Launch Chrome Canary

TypeScriptCopy
const chrome = await pw.chromium.launch({
  channel: 'chrome-canary',
  executablePath: '/path/to/chrome-canary',
});

Edge Browser Variants

Playwright supports all four Edge variants, but you must download Edge Canary manually.

Install Edge

BashCopy
npx playwright install --with-deps edge

Install Edge Beta

BashCopy
npx playwright install --with-deps edge-beta

Install Edge Dev

BashCopy
npx playwright install --with-deps msedge-dev

Launch Edge

TypeScriptCopy
const browser = await pw.chromium.launch({
  channel: 'msedge',
});

Launch Edge Beta

TypeScriptCopy
const browser = await pw.chromium.launch({
  channel: 'msedge-beta',
});

Launch Edge Dev

TypeScriptCopy
const browser = await pw.chromium.launch({
  channel: 'msedge-dev',
});

Launch Edge Canary

TypeScriptCopy
const browser = await pw.chromium.launch({
  channel: 'msedge-canary',
});

Firefox Browser Variants

Playwright supports all four Firefox variants, but you must download Firefox Dev manually.

Install Firefox

BashCopy
npx playwright install --with-deps firefox

Install Firefox Beta

BashCopy
npx playwright install --with-deps firefox-beta

Install Firefox Nightly

BashCopy
npx playwright install --with-deps firefox-asan

Launch Firefox

TypeScriptCopy
const firefox = await pw.firefox.launch();

Launch Firefox Beta

TypeScriptCopy
const chrome = await pw.firefox.launch({
  channel: 'firefox-beta',
});

Launch Firefox Dev

TypeScriptCopy
const chrome = await pw.firefox.launch({
  channel: 'firefox-beta', // yes, this is correct
  executablePath: '/path/to/firefox-dev',
});

Launch Firefox Nightly

TypeScriptCopy
const chrome = await pw.firefox.launch({
  channel: 'firefox-asan',
});

Contexts (aka User Sessions)

Create new context

TypeScriptCopy
const context = await browser.newContext();

Create new context with custom options

TypeScriptCopy
const context = await browser.newContext({
  bypassCSP: true,
  colorScheme: 'dark',
  deviceScaleFactor: 1,
  permissions: ['geolocation'],
  // etc.
});

List all browser's contexts

TypeScriptCopy
const contexts = browser.contexts();

Get the current page's context

TypeScriptCopy
const context = page.context();

Close context

TypeScriptCopy
await context.close();

Close context with a reason

TypeScriptCopy
await context.close({reason: 'success'});

Pages

Create new page in context

TypeScriptCopy
const page = await context.newPage();

Create new page in new context

TypeScriptCopy
const page = await browser.newPage();

Create new page in new context with custom options

TypeScriptCopy
const page = await browser.newPage({
  bypassCSP: true,
  colorScheme: 'dark',
  deviceScaleFactor: 1,
  permissions: ['geolocation'],
  // etc.
});

Page Frames

Web pages are made up of frames. The main frame is the viewport, which can contain numerous nested `iframe` elements.

Get parent frame for current page

TypeScriptCopy
const frame = page.mainFrame();

Get frame by `name` attribute

TypeScriptCopy
const frame = page.frame({
  name: /^footer-ad$/, // or exact string match
});

Get frame by `url` attribute

TypeScriptCopy
const frame = page.frame({
  url: //footer-ad.html$/, // or exact string match
});

Get all frames for current page

TypeScriptCopy
const frames = page.frames();

Frame Locators

Use frame locators to find elements within a specific frame. Normal locators stop at the frame boundary.

Create frame locator from CSS selector

TypeScriptCopy
const $frame = page.frameLocator('#soundcloud-embed');

Create frame locator from locator

TypeScriptCopy
const $frame = page.locator('#soundcloud-embed');
const $frameLoc = $frame.frameLocator(':scope');

Locate Elements

Contract-based Locators

These helpers target elements using properties unlikely to change. Use whenever possible for readability and durability.

Select element by role

TypeScriptCopy
const $alert = await page.getByRole('alert');

Select element by label

TypeScriptCopy
const $input = await page.getByLabel('Username');

Select element by placeholder

TypeScriptCopy
const $input = await page.getByPlaceholder('Search');

Select element by title

TypeScriptCopy
const $el = await page.getByTitle('Welcome');

Select element by alt text

TypeScriptCopy
const $image = await page.getByAltText('Logo');

Select element by text

TypeScriptCopy
const $button = await page.getByText('Submit');

Select element by test id

TypeScriptCopy
const $button = await page.getByTestId('submit-button');

Role-based Selectors

These locators support ARIA roles, states, and properties. Very useful for shorthand selection of most DOM elements.

Select element by role

TypeScriptCopy
const $alert = await page.getByRole('alert');
const $heading = await page.getByRole('heading');
const $button = await page.getByRole('button');
const $link = await page.getByRole('link');
// etc.

Select element by accessible name

TypeScriptCopy
const $button = await page.getByRole('button', { 
  name: /(submit|save)/i, // or string
  exact: true, // default: false
});

Select elements by `checked` state

TypeScriptCopy
const $checkbox = await page.getByRole('checkbox', { 
  checked: true, // or false
});

Select elements by `selected` state

TypeScriptCopy
const $option = await page.getByRole('option', { 
  selected: true, // or false
});

Select elements by `expanded` state

TypeScriptCopy
const $menu = await page.getByRole('menu', { 
  expanded: true, // or false
});

Select elements by `disabled` state

TypeScriptCopy
const $input = await page.getByRole('textbox', { 
  disabled: true, // or false
});

Select elements by depth level

TypeScriptCopy
const $heading = await page.getByRole('heading', { 
  level: 2, // etc.
});

Match hidden elements with ARIA locators

TypeScriptCopy
const $alert = await page.getByRole('alert', { 
  includeHidden: true, // default: false
});

CSS Selectors

Playwright supports all CSS selectors using `document.querySelector()`.

Select elements by CSS selector

TypeScriptCopy
const $icon = await page.locator('button > svg[width]');

Select elements by tag name

TypeScriptCopy
const $header = await page.locator('header');

Select elements by tag attribute

TypeScriptCopy
const $absLinks = await page.locator('[href^="https://"]');

Select elements by CSS class

TypeScriptCopy
const $buttons = await page.locator('.btn');

Select elements by CSS id

TypeScriptCopy
const $captcha = await page.locator('#captcha');

XPath Selectors

Playwright supports all XPath selectors using `document.evaluate()`. However, XPath selectors are brittle and highly discouraged.

Select element by XPath selector

TypeScriptCopy
const $button = await page.locator('//button[text()="Submit"]');

Browser Actions

Get browser type

TypeScriptCopy
const browserType = browser.browserType();

Get browser version

TypeScriptCopy
const version = await browser.version();

Check if browser is connected

TypeScriptCopy
const isConnected = browser.isConnected();

Close browser (force)

TypeScriptCopy
await browser.close();

Close browser (gentle)

TypeScriptCopy
await Promise.all(
  browser.contexts()
    .map((context) => context.close()),
);
await browser.close();

Close browser with a reason

TypeScriptCopy
await browser.close({reason: 'success'});

Listen for browser disconnection event

TypeScriptCopy
browser.on('disconnected', (browser) => {});

Network Traffic

Intercept network traffic on the `page` or `context` objects using any method listed below. Prefer `page` for narrow targeting.

Send `fetch` request

TypeScriptCopy
const res = await page.request.fetch(
  'https://www.browsersolver.com',
  {method: 'GET'},
);

Flush network traffic cache

TypeScriptCopy
await page.request.dispose();

Set default HTTP headers on all requests

TypeScriptCopy
await page.setExtraHTTPHeaders({
  'X-Agent': 'production-test-bot',
});

Wait for Network Traffic

Wait for request matching test

TypeScriptCopy
const req = await page.waitForEvent('request', (req) => {
  return req.method() === 'PUT' && 
    req.headers()['content-type'] === 'application/json';
});

Wait for response matching test

TypeScriptCopy
const res = await page.waitForEvent('response', (res) => {
  return res.status() === 201 && 
    Array.isArray(await res.json());
});

Wait for page request by URL

TypeScriptCopy
const req = await page.waitForRequest(/browsersolver.com/);

Wait for page response by URL

TypeScriptCopy
const res = await page.waitForResponse(/browsersolver.com/);

Network Events

Listen for new network requests

TypeScriptCopy
page.on('request', (req) => {});

Listen for successful network requests

TypeScriptCopy
page.on('requestfinished', (req) => {});

Listen for failed network requests

TypeScriptCopy
page.on('requestfailed', (req) => {});

Listen for network responses

TypeScriptCopy
page.on('response', (res) => {});

Listen for new websocket requests (`page` only)

TypeScriptCopy
page.on('websocket', (ws) => {});

Intercept Network Traffic

Note: Playwright can't currently intercept traffic to webworkers.

Route all requests through handler

TypeScriptCopy
await page.route('**/*', (route) => {
  route.continue();
});

Route requests matching glob

TypeScriptCopy
await page.route('**/*.png', (route) => {
  route.continue();
});

Route requests matching regex

TypeScriptCopy
await page.route(/.json$/i, (route) => {
  route.continue();
});

Route request only once

TypeScriptCopy
await page.route('**/*.png', (route) => {
  route.continue();
}, {times: 1}); // or any number

Remove all handlers from route

TypeScriptCopy
await page.unroute('**/*.png');

Remove all network routes immediately

TypeScriptCopy
await page.unrouteAll();

Transforming Network Traffic

All routed requests must be handled using either `.continue()`, `.abort()`, or `.fulfill()`.

Allow routed request to proceed

TypeScriptCopy
await page.route('**/*', (route) => {
  route.continue();
});

Modify request before allowing to proceed

TypeScriptCopy
await page.route('**/*', (route, req) => {
  route.continue({
    method: 'GET',
    url: 'http://localhost:8080/test',
    headers: {
      ...req.headers(),
      'X-Test': 'true',
    },
    postData: JSON.stringify({
      ...req.postDataJSON(),
      test: true,
    }),
  });
});

Proceed with request, but intercept response

TypeScriptCopy
await page.route('**/*', (route) => {
  const response = await route.fetch();
  
  route.fulfill({
    response,
    json: {
      ...await response.json(),
      test: true,
    },
  })
});

Fulfill routed request with custom response

TypeScriptCopy
await page.route('**/*', (route) => {
  route.fulfill({
    status: 404,
    json: {message: 'not found'},
  });
});

Fulfill routed request with local file

TypeScriptCopy
await page.route('**/*.png', (route) => {
  route.fulfill({
    path: './1-pixel.png',
  });
});

Abort routed request

TypeScriptCopy
await page.route('**/*', (route) => {
  route.abort();
});

Abort routed request with custom error

TypeScriptCopy
await page.route('**/*', (route) => {
  route.abort('connectionrefused'); // see docs for values
});

HAR Replay Network Traffic

HAR (HTTP Archive format) records network traffic to replay later. Use for exact reproduction of test conditions or in advanced workflows.

Respond using HAR, aborting unknown requests

TypeScriptCopy
await page.routeFromHAR('./recorded.har');

Respond using HAR, allowing unknown requests

TypeScriptCopy
await page.routeFromHAR('./recorded.har', {
  notFound: 'fallback',
});

Respond using HAR, for requests matching pattern

TypeScriptCopy
await page.routeFromHAR('./recorded.har', {
  url: /.png$/i,
});

Record HAR file using network traffic

TypeScriptCopy
await page.routeFromHAR('./recorded.har', {
  update: true,
});

Image Generation

Take screenshots of entire pages or specific elements. Most configuration options work in both cases.

Screenshot current viewport

TypeScriptCopy
await page.screenshot();

Screenshot entire page

TypeScriptCopy
await page.screenshot({fullPage: true});

Screenshot specific element

TypeScriptCopy
const $element = page.locator('h1');
await $element.screenshot();

Resize viewport before screenshot

TypeScriptCopy
await page.setViewportSize({
  width: 2000, 
  height: 1000,
});
await page.screenshot();

Screenshot custom HTML/CSS content

TypeScriptCopy
await page.setContent(`
<html>
  <head>
    <style>
      body { background-color: red; }
    </style>
  </head>

  <body>
    <h1>Hello, World!</h1>
  </body>
</html>
`);
await page.screenshot();

Apply custom stylesheet during screenshot

TypeScriptCopy
await page.screenshot({
  style: './path/to/screenshot.css',
});

Hide specific elements in screenshot

TypeScriptCopy
await page.screenshot({
  mask: [
    page.locator('input'),
    page.getByRole('button'),
    page.locator('.sensitive'),
  ],
});

Clip screenshot to specific region

TypeScriptCopy
await page.screenshot({
  clip: {x: 0, y: 0, width: 100, height: 100},
});

Save screenshot to file

TypeScriptCopy
await page.screenshot({
  path: './screenshot.png',
});

Save screenshot as JPEG

TypeScriptCopy
await page.screenshot({
  type: 'jpeg',
  quality: 80, // 0-100
});

Tracing

Record context trace

TypeScriptCopy
await context.tracing.start();

Record context trace with custom prefix

TypeScriptCopy
await context.tracing.start({
  name: 'checkout-process',
});

Record screenshots for trace

TypeScriptCopy
await context.tracing.start({
  screenshots: true,
});

Record snapshots of all actions for trace

TypeScriptCopy
await context.tracing.start({
  snapshots: true,
});

Include source files in trace

TypeScriptCopy
await context.tracing.start({
  sources: true,
});

Stop recording context trace

TypeScriptCopy
const trace = await context.tracing.stop();

Save context trace to specific file

TypeScriptCopy
await context.tracing.stop({
  path: './trace.json', 
  // default: browser.launch({tracesDir})
});

Debugging

Launch browser with custom logger

TypeScriptCopy
const browser = await pw.chromium.launch({
  logger: {
    isEnabled: (name, sev) => sev === 'error',
    log: (name, sev, msg, args) => console.debug(name, msg),
  },
});

Launch browser with slow motion

TypeScriptCopy
const browser = await pw.chromium.launch({
  slowMo: 100, // ms delay between actions
});

Launch browser in headed mode

TypeScriptCopy
const browser = await pw.chromium.launch({
  headless: false,
});

Open browser DevTools automatically

TypeScriptCopy
const browser = await pw.chromium.launch({
  devtools: true,
});

Browser Servers

Connect to a remote Playwright browser server

TypeScriptCopy
const browser = await pw.chromium.connect(
  process.env.BROWSER_WS_ENDPOINT!,
);

Connect to remote browser server over CDP

TypeScriptCopy
const browser = await pw.chromium.connectOverCDP(
  'http://localhost:9222',
);

Launch a local browser server

TypeScriptCopy
const browserServer = await pw.chromium.launchServer();
const wsEndpoint = browserServer.wsEndpoint();

Connect to local browser server

TypeScriptCopy
const browser = await pw.chromium.connect(wsEndpoint);

Close browser server

TypeScriptCopy
await browserServer.close();

Testing Quick Start

Get started without any configuration required.

Install Playwright for testing

BashCopy
npm i -D @playwright/test
npx playwright install
npm i -D typescript # with TS

Create a test file

BashCopy
touch tests/example.spec.js # with JS
touch tests/example.spec.ts # with TS

Write a basic test

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

test('has brand in <title>', async ({ page }) => {
  await page.goto('https://www.browsersolver.com/');
  await expect(page).toHaveTitle(/BrowserSolver/);
});

Generate code from user behavior

BashCopy
npx playwright codegen

Run your tests

BashCopy
npx playwright test

Run your tests in UI mode

BashCopy
npx playwright test --ui

Show test results in browser

BashCopy
npx playwright show-report

Writing Tests

Write a basic test

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

test('has brand in <title>', async ({ page }) => {
  await page.goto('https://www.browsersolver.com/');
  await expect(page).toHaveTitle(/BrowserSolver/);
});

Attach screenshot to a test

TypeScriptCopy
test('with screenshot', async ({page}) => {
  await test.info().attach('screenshot', {
    contentType: 'image/png',
    body: await page.screenshot(),
  });
});

Test Groups

Test groups cluster tests for shared configuration and organized reporting.

Create a test group

TypeScriptCopy
test.describe('group', async () => {
  test('test', async () => {});
});

Create nested test groups

TypeScriptCopy
test.describe('group', async () => {
  test.describe('subgroup', async () => {
    test('test', async () => {});
  });
});

Test Steps

Test steps improve report readability. They can also be reused across multiple tests.

Break a test into steps

TypeScriptCopy
test('long test', async () => {
  await step('step 1', async () => {});
  await step('step 2', async () => {});
  await step('step 3', async () => {});
});

Create reusable test steps

TypeScriptCopy
async function login(user: string, pass: string) {
  return test.step('login', async ({page}) => {
    await page.fill('input[name="username"]', user);
    await page.fill('input[name="password"]', pass);
    await page.click('button[type="submit"]');
  });
}

test('login, then interact', async () => {
  await login('user', 'pass');
});

Test Fixtures

Use Playwright's built-in fixtures to save time on setup and teardown.

Access `page` fixture in test

TypeScriptCopy
test('test', async ({page}) => {
  // `page` is exclusive to this test
  await page.goto('https://www.browsersolver.com');
});

Access `context` fixture in test

TypeScriptCopy
test('test', async ({context}) => {
  // `context` is exclusive to this test
  const page = await context.newPage();
});

Access `browser` fixture in test

TypeScriptCopy
test('test', async ({browser}) => {
  // `browser` is shared across worker thread
  const context = await browser.newContext();
});

Access `request` fixture in test

TypeScriptCopy
test('test', async ({request}) => {
  // `request` is exclusive to this test
  await request.get('https://api.browsersolver.com');
});

Custom Test Fixtures

Create fixtures to share behavior and improve readability.

Create fixture with setup and teardown

TypeScriptCopy
import {test as base, type BrowserContext} from '@playwright/test';

type TestFixtures = {
  authContext: BrowserContext;
};

export const test = base.extend<TestFixtures>({
  authContext: async ({browser}, use) => {
    // setup before each test
    const authContext = await browser.newContext({
      storageState: 'auth.json',
    });

    // run test
    await use(authContext);
    
    // teardown after each test
    await authContext.close();
  },
});

Override fixture implementation

TypeScriptCopy
export const test = base.extend({
  page: [async ({page}, use) => {
    page.addInitScript('tests/init.js');
    await use(page);
  }],
});

Combine multiple custom fixture assignments

TypeScriptCopy
import {test, mergeTests} from '@playwright/test';

const test1 = test.extend({});
const test2 = test.extend({});

export const test = mergeTests(test1, test2);

Share fixture across all tests in worker

TypeScriptCopy
export const test = base.extend<{}, WorkerFixtures>({
  authContext: [async ({browser}, use) => {
    const authContext = await browser.newContext({
      storageState: 'auth.json',
    });

    await use(authContext);
    await authContext.close();
  }, {scope: 'worker'}],
});

Test Runner Configuration

Create a config file

BashCopy
touch playwright.config.ts

Write basic config

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

export default defineConfig({
  testMatch: 'tests/**/*.spec.{ts,tsx}',
});

Minimize CLI output

TypeScriptCopy
export default defineConfig({
  quiet: !!process.env.CI,
});

Configure Exit Criteria

Retry failed tests before marking as failed

TypeScriptCopy
export default defineConfig({
  retries: 3, // default 0
});

Repeat all tests before marking as passed

TypeScriptCopy
export default defineConfig({
  repeatEach: 3, // default 1
});

Fail individual tests if they exceed timeout

TypeScriptCopy
export default defineConfig({
  timeout: 1000 * 30,
});

Fail test suite if exceeds timeout

TypeScriptCopy
export default defineConfig({
  globalTimeout: 1000 * 60 * 60,
});

Fail test suite early, after N failures

TypeScriptCopy
export default defineConfig({
  maxFailures: 10, // default 0
});

Output Files

Select test results output path

TypeScriptCopy
export default defineConfig({
  outputDir: './.test/results',
});

Only preserve test results on failure

TypeScriptCopy
export default defineConfig({
  preserveOutput: 'failures-only',
});

Test Environment Options

Configure your test environment, browser, and emulated device.

Connect tests to a remote browser server

TypeScriptCopy
export default defineConfig({
  use: {
    connectOptions: {
      wsEndpoint: process.env.BROWSER_WS_ENDPOINT!,
    },
  },
});

Configure Behavior

Show browser window during tests

TypeScriptCopy
export default defineConfig({
  use: {
    headless: false,
  },
});

Screenshot tests automatically

TypeScriptCopy
export default defineConfig({
  use: {
    screenshot: 'on', // or 'only-on-failure' | 'off'
  },
});

Record video of tests automatically

TypeScriptCopy
export default defineConfig({
  use: {
    video: 'on', // or 'retain-on-failure' | 'on-first-retry' | 'off'
  },
});

Record trace data for tests automatically

TypeScriptCopy
export default defineConfig({
  use: {
    trace: 'on', // or 'retain-on-failure' | 'on-first-retry' | 'on-all-retries' | 'off'
  },
});

Set delay between user actions

TypeScriptCopy
export default defineConfig({
  use: {
    actionTimeout: 1000 * 3, // default: 0
  },
});

Enable or disable JavaScript

TypeScriptCopy
export default defineConfig({
  use: {
    javaScriptEnabled: false, // default: true
  },
});

Grant custom browser permissions automatically

TypeScriptCopy
export default defineConfig({
  use: {
    permissions: ['geolocation', 'notifications'],
  },
});

Configure Network Traffic

Enable relative URLs with custom base URL

TypeScriptCopy
export default defineConfig({
  use: {
    // Allows tests to use relative URLs
    // e.g. `page.goto('/login');`
    baseURL: isDev ? 
      'https://localhost:8080' :
      'https://www.example.com',
  },
});

Set custom HTTP credentials

TypeScriptCopy
export default defineConfig({
  use: {
    httpCredentials: {
      username: 'user',
      password: 'pass',
    },
  },
});

Send custom default HTTP headers with requests

TypeScriptCopy
export default defineConfig({
  use: {
    extraHTTPHeaders: {
      'X-My-Header': 'value',
    },
  },
});

Route all traffic through a proxy server

TypeScriptCopy
export default defineConfig({
  use: {
    proxy: {
      server: 'http://localhost:8080',
      username: 'user',
      password: 'pass',
      bypass: 'browsersolver.com, .example.com',
    },
  },
});

Ignore HTTPS errors

TypeScriptCopy
export default defineConfig({
  use: {
    ignoreHTTPSErrors: process.env.NODE_ENV === 'development',
  },
});

Configure Browsers

Emulate specific browsers quickly

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

export default defineConfig({
  projects: [{
    name: 'Desktop Chrome',
    use: {
      ...devices['Desktop Chrome'],
    },
  }, {
    name: 'Mobile Chrome',
    use: {
      ...devices['Mobile Chrome'],
    },
  }],
});

Select browser to use for tests

TypeScriptCopy
export default defineConfig({
  use: {
    browserName: 'firefox', // or 'webkit' | 'chromium'
  },
});

Set user preferred color scheme

TypeScriptCopy
export default defineConfig({
  use: {
    colorScheme: 'dark', // or 'light' | 'no-preference'
  },
});

Set browser viewport size

TypeScriptCopy
export default defineConfig({
  use: {
    viewport: { width: 1920, height: 1080 },
  },
});

Configure Locale

Emulate custom locale

TypeScriptCopy
export default defineConfig({
  use: {
    locale: 'en-US',
  },
});

Emulate custom timezone

TypeScriptCopy
export default defineConfig({
  use: {
    timezoneId: 'America/New_York',
  },
});

Emulate custom geolocation

TypeScriptCopy
export default defineConfig({
  use: {
    geolocation: {
      latitude: 40.730610, 
      longitude: -73.935242,
      accuracy: 11.08,
    },
  },
});

Test Parallelization

By default, Playwright executes test files in parallel, but it executes a file's tests in order.

Run test files in parallel, tests sequentially

TypeScriptCopy
export default defineConfig({
  fullyParallel: false, // default
});

Run all tests in all files in parallel

TypeScriptCopy
export default defineConfig({
  fullyParallel: true, // default: false
});

Set number of parallel workers

TypeScriptCopy
export default defineConfig({
  workers: 3, // default: 50% logical CPUs
});

Disable all parallelism (forcefully)

TypeScriptCopy
export default defineConfig({
  workers: 1,
});

Test File & Group Parallelism

Note that parallelizing tests within a file prohibits tests from sharing state. Use wisely.

Run a single file's tests in parallel

TypeScriptCopy
test.describe.configure({
  mode: 'parallel', // default 'serial'
});

test('test', async () => {});

Run a test group's tests in parallel

TypeScriptCopy
test.describe('group', async () => {
  test.describe.configure({
    mode: 'parallel', // default 'serial'
  });
  
  test('test', async () => {});
});

Page Assertions

Assert page has specific title

TypeScriptCopy
await expect(page).toHaveTitle('My Page Title');
await expect(page).toHaveTitle(/My Page/);

Assert page has specific URL

TypeScriptCopy
await expect(page).toHaveURL('https://example.com/login');
await expect(page).toHaveURL(//login$/);

Element Property Assertions

Assert element is visible

TypeScriptCopy
await expect(locator).toBeVisible();

Assert element is hidden

TypeScriptCopy
await expect(locator).toBeHidden();

Assert element is enabled

TypeScriptCopy
await expect(locator).toBeEnabled();

Assert element is disabled

TypeScriptCopy
await expect(locator).toBeDisabled();

Assert element is checked

TypeScriptCopy
await expect(locator).toBeChecked();

Assert element is focused

TypeScriptCopy
await expect(locator).toBeFocused();

Assert element is in the viewport

TypeScriptCopy
await expect(locator).toBeInViewport();

Assert element has text content

TypeScriptCopy
await expect(locator).toHaveText('Submit');
await expect(locator).toHaveText(/submit/i);

Assert element has attribute

TypeScriptCopy
await expect(locator).toHaveAttribute('href', '/home');

Assert element has class

TypeScriptCopy
await expect(locator).toHaveClass('active');

Assert element has value

TypeScriptCopy
await expect(locator).toHaveValue('Hello');

Assert list has N elements

TypeScriptCopy
await expect(locator).toHaveCount(5);

Assert element has CSS property

TypeScriptCopy
await expect(locator).toHaveCSS('color', 'rgb(255, 0, 0)');

Visual Assertions

Page Screenshot Assertions

Assert page matches screenshot

TypeScriptCopy
await expect(page).toHaveScreenshot();

Assert image matches named screenshot

TypeScriptCopy
// compare to a single image shared across tests
await expect(page).toHaveScreenshot('home-page.png');

Assert page roughly matches screenshot

TypeScriptCopy
await expect(page).toHaveScreenshot({
  // pixel color can vary by 10% in YIQ space
  threshold: 0.1,
  // 10% of pixels can exceed threshold
  maxDiffPixelRatio: 0.1, 
});

Element Screenshot Assertions

Assert element matches screenshot

TypeScriptCopy
await expect(locator).toHaveScreenshot();

Assert element matches named screenshot

TypeScriptCopy
await expect(locator).toHaveScreenshot('home-hero.png');

Test Annotations

.only() Tests

Run only this test

TypeScriptCopy
test.only('test', async () => {});

Run only tests in this group

TypeScriptCopy
test.describe.only('group', async () => {
  test('test', async () => {});
});

.skip() Tests

Skip this test

TypeScriptCopy
test.skip('test', async () => {});

Skip test conditionally

TypeScriptCopy
test('test', async ({page}) => {
  test.skip(process.env.CI === 'true', 'Not running in CI');
});

.fixme() Tests

Mark test as fixme (skipped)

TypeScriptCopy
test.fixme('test', async () => {});

.slow() Tests

Mark test as slow (3x timeout)

TypeScriptCopy
test('test', async () => {
  test.slow();
});

.fail() Tests

Mark test as expected to fail

TypeScriptCopy
test.fail('test', async () => {});

API Testing

Send GET request

TypeScriptCopy
test('get users', async ({request}) => {
  const res = await request.get('https://api.example.com/users');
  expect(res.status()).toBe(200);
  const data = await res.json();
  expect(data).toHaveLength(10);
});

Send POST request

TypeScriptCopy
test('create user', async ({request}) => {
  const res = await request.post('https://api.example.com/users', {
    data: {name: 'Jane Doe', email: 'jane@example.com'},
  });
  expect(res.status()).toBe(201);
});

Send PUT request

TypeScriptCopy
test('update user', async ({request}) => {
  const res = await request.put('https://api.example.com/users/1', {
    data: {name: 'Updated Name'},
    headers: {'Authorization': 'Bearer token'},
  });
  expect(res.status()).toBe(200);
});

Send DELETE request

TypeScriptCopy
test('delete user', async ({request}) => {
  const res = await request.delete('https://api.example.com/users/1');
  expect(res.status()).toBe(204);
});

Parameterize Tests

Parameterize tests across multiple projects

TypeScriptCopy
// playwright.config.ts
export type TestOptions = {
  cat: string;
};

export default defineConfig<TestOptions>({
  projects: [{
    name: 'cartoons',
    use: {cat: 'Garfield'},
  }, {
    name: 'sitcoms',
    use: {cat: 'Smelly Cat'},
  }]
});

// extended-test.ts
import {test} from '@playwright/test';
import type {TestOptions} from 'playwright.config.ts';

export default test.extend<TestOptions>({
  cat: ['Hello Kitty', {option: true}],
});

// profiles.spec.ts
import test from './extended-test.ts';

test('test', async ({page, cat}) => {
  await expect(page).toHaveTitle(cat);
});

Local Web Server

Launch web server during tests

TypeScriptCopy
export default defineConfig({
  webServer: {
    command: 'npm run start',
    url: 'http://localhost:3000',
    reuseExistingServer: true,
  },
  use: {
    baseURL: 'http://localhost:3000',
  },
});

Launch multiple web servers during tests

TypeScriptCopy
export default defineConfig({
  webServer: [{
    command: 'npm run start:app',
    url: 'http://localhost:3000',
  }, {
    command: 'npm run start:api',
    url: 'http://localhost:3030',
  }],
});

Test Reporters

Configure test reporters

TypeScriptCopy
export default defineConfig({
  reporter: [
    ['list'],
    ['json', {outputFile: 'test-results.json'}],
    ['junit', {outputFile: 'test-results.xml'}],
  ],
});

CLI Reporters

Use list reporter (default)

TypeScriptCopy
export default defineConfig({
  reporter: 'list',
});

Use dot reporter

TypeScriptCopy
export default defineConfig({
  reporter: 'dot',
});

Use line reporter

TypeScriptCopy
export default defineConfig({
  reporter: 'line',
});

File Reporters

Use JSON reporter

TypeScriptCopy
export default defineConfig({
  reporter: [['json', {outputFile: 'test-results.json'}]],
});

Use JUnit reporter

TypeScriptCopy
export default defineConfig({
  reporter: [['junit', {outputFile: 'results.xml'}]],
});

Use HTML reporter

TypeScriptCopy
export default defineConfig({
  reporter: [['html', {outputFolder: 'playwright-report'}]],
});

Run Tests via CLI

Run all tests

BashCopy
npx playwright test

Run tests in a specific file

BashCopy
npx playwright test tests/example.spec.ts

Run tests matching pattern

BashCopy
npx playwright test --grep "login"

Run tests in a specific project

BashCopy
npx playwright test --project "Desktop Chrome"

Run tests in headed mode

BashCopy
npx playwright test --headed

Run tests in debug mode

BashCopy
npx playwright test --debug

Run tests with specific number of workers

BashCopy
npx playwright test --workers 4

Run tests and show results in browser

BashCopy
npx playwright test --reporter html
npx playwright show-report

Mouse Control

Move mouse and click at specific coordinates

TypeScriptCopy
await page.mouse.click(100, 100);

Move mouse and click with delay between events

TypeScriptCopy
await page.mouse.click(100, 100, {
  delay: 100,
});

Click element with modifier keys

TypeScriptCopy
await page.locator('button').click({
  modifiers: ['Shift'],
});

Click element with custom X/Y coordinates

TypeScriptCopy
await page.locator('button').click({
  position: {x: 10, y: 10},
});

Move mouse and double-click at specific coordinates

TypeScriptCopy
await page.mouse.dblclick(100, 100);

Double-click element

TypeScriptCopy
await page.locator('button').dblclick();

Scroll mouse wheel vertically

TypeScriptCopy
await page.mouse.wheel(0, 100);

Scroll mouse wheel horizontally

TypeScriptCopy
await page.mouse.wheel(100, 0);

Hover element

TypeScriptCopy
await page.locator('button').hover();

Hover element with custom options

TypeScriptCopy
await page.locator('button').hover({
  modifiers: ['Meta'],
  position: {x: 10, y: 10},
});

Drag and Drop

Playwright supports manual and automatic drag-and-drop behavior. Use the automatic method, if possible.

Drag and drop element (easy method)

TypeScriptCopy
const $source = page.locator('#source');
const $target = page.locator('#target');
await $source.dragTo($target);

Drag and drop with custom X/Y coordinates

TypeScriptCopy
const $source = page.locator('#source');
const $target = page.locator('#target');
await $source.dragTo($target, {
  // relative to top-left corner
  sourcePosition: {x: 10, y: 10},
  targetPosition: {x: 10, y: 10},
});

Drag and drop element (manual method)

TypeScriptCopy
const $source = page.locator('#source');
const $target = page.locator('#target');

await $source.scrollIntoViewIfNeeded();
await $source.hover();
const sourceBox = await $source.boundingBox();
await page.mouse.move(
  sourceBox.x + sourceBox.width / 2, 
  sourceBox.y + sourceBox.height / 2,
);
await page.mouse.down();

await $target.scrollIntoViewIfNeeded();
await $target.hover();
await $target.hover(); // needed for some browsers
const targetBox = await $target.boundingBox();
await page.mouse.move(
  targetBox.x + targetBox.width / 2, 
  targetBox.y + targetBox.height / 2,
);
await page.mouse.up();

Low-level Mouse Events

Dispatch `mousedown` event

TypeScriptCopy
await page.mouse.down();

Dispatch `mouseup` event

TypeScriptCopy
await page.mouse.up();

Dispatch `mousemove` event

TypeScriptCopy
await page.mouse.move(100, 100);

Dispatch `mousemove` event across smooth steps

TypeScriptCopy
await page.mouse.move(100, 100, {
  steps: 5, // default: 1
});

Dispatch mouse events to specific element

TypeScriptCopy
const $button = page.locator('button');

await $button.dispatchEvent('click');
await $button.dispatchEvent('dblclick');
await $button.dispatchEvent('mousedown');
await $button.dispatchEvent('mouseup');
await $button.dispatchEvent('mousemove');

Keyboard Control

Type text with keyboard

TypeScriptCopy
await page.locator('input').focus();
await page.keyboard.type('Hello');

Type text with delay between presses

TypeScriptCopy
await page.keyboard.type('Hello', {
  delay: 100,
});

Press key combination

TypeScriptCopy
await page.keyboard.press('F12');
await page.keyboard.press('Control+c');

Press key combination with delay

TypeScriptCopy
await page.keyboard.press('Control+v', {
  delay: 100,
});

Press key combination in specific element

TypeScriptCopy
await page.locator('textarea').press('Control+Z');

Low-level Keyboard Events

Dispatch `keydown` event

TypeScriptCopy
await page.keyboard.down('Shift');
await page.keyboard.down('a');

Dispatch `keyup` event

TypeScriptCopy
await page.keyboard.up('a');
await page.keyboard.up('Shift');

Dispatch keyboard events on specific element

TypeScriptCopy
const $input = page.locator('input');

await $input.dispatchEvent('keydown', {key: 'a'});
await $input.dispatchEvent('keyup', {key: 'b'});
await $input.dispatchEvent('keypress', {key: 'c'});

Touchscreen Control

Tap screen at specific coordinates

TypeScriptCopy
await page.touchscreen.tap(100, 100);

Low-level Touchscreen Events

Dispatch touchscreen events to specific element

TypeScriptCopy
const $link = page.locator('a');

await $link.dispatchEvent('touchstart');
await $link.dispatchEvent('touchmove');
await $link.dispatchEvent('touchend');
await $link.dispatchEvent('touchcancel');

Events and Listeners

Listen for page load event

TypeScriptCopy
page.on('load', () => {});

Listen for page DOM content loaded event

TypeScriptCopy
page.on('domcontentloaded', () => {});

Listen for page close event

TypeScriptCopy
page.on('close', () => {});

Listen for page crash event

TypeScriptCopy
page.on('crash', () => {});

Listen for page dialog event

TypeScriptCopy
page.on('dialog', async (dialog) => {
  console.log(dialog.message());
  await dialog.accept();
});

Listen for console messages

TypeScriptCopy
page.on('console', (msg) => {
  console.log(msg.type(), msg.text());
});

Remove event listener

TypeScriptCopy
const handler = () => {};
page.on('load', handler);
page.off('load', handler);

Listen for event only once

TypeScriptCopy
page.once('load', () => {});

Wait for an event to be emitted

TypeScriptCopy
const response = await page.waitForEvent('response');