Playwright: Debugging, Tracing & Codegen

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

A failing browser test is only useful if you can see why it failed. Playwright ships four complementary answers: tools that let you watch and step through a test while you write it (UI Mode, the Inspector, the VS Code extension), a recording of everything that happened that you can open later (the trace), a recorder that writes the first draft of a test for you (codegen) and reporters that turn a run into a terminal log, an HTML page, a JSON or JUnit file, or an annotation on a pull request. This page covers all of them. It is written for Playwright 1.64.1 (Node.js) and was checked on 25 October 2026; the Python, Java and .NET bindings expose the same tools, and their examples elsewhere in this section were run at 1.63 (see Playwright with Python, Java and .NET). Every command-line and TypeScript example below was run against the Bookshelf application in headless Chromium, and the output excerpts are real. The tools that need a window (UI Mode, the Inspector, the VS Code debugger, the interactive part of codegen) were documented but not run, because the machine that produced these examples had no display to drive; each such place says so.

The examples assume the configuration and fixtures described in Configuration, Fixtures & Projects; retries and workers are covered in Parallelism, Sharding & Retries, and screenshots and videos next to traces in Screenshots, Videos & Traces.

The debug loop

Most debugging sessions follow the same cycle, whichever tool you pick. A test fails (locally or on CI), you decide whether you already have a recording of the failure, read it, change the test or the application and run again. The tools differ in how early they let you intervene: the Inspector and UI Mode stop a live run, the trace is a recording you read afterwards, and DEBUG=pw:api writes a log you can search.

flowchart TD A["Test fails locally or on CI"] --> B{"Trace or HTML report available?"} B -- "Yes" --> C["Open it: show-report, show-trace or npx playwright trace"] B -- "No" --> D["Reproduce with --trace on, --debug or UI Mode"] D --> C C --> E{"Cause visible?"} E -- "No" --> F["Pause in the Inspector or add DEBUG=pw:api logs"] F --> E E -- "Yes" --> G["Fix the locator, wait or application"] G --> H["Rerun the single test with --last-failed"] H --> I{"Passes?"} I -- "No" --> A I -- "Yes" --> J["Keep trace on-first-retry in CI"]

The example project

Two files drive the examples. The configuration records a trace only for failing runs, runs one worker (every test resets the one shared Bookshelf server) and reads the application address from the environment:

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

export default defineConfig({
  testDir: '.',
  outputDir: './test-results',
  testIgnore: process.env.SHOW_FAILURES ? [] : ['**/failing/**'],
  workers: 1, // every test resets the one shared Bookshelf server
  retries: process.env.RETRIES ? Number(process.env.RETRIES) : 0,
  reporter: [['list']],
  use: {
    baseURL: process.env.BASE_URL,
    testIdAttribute: 'data-testid',
    trace: 'retain-on-failure',
  },
  projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
});

The spec has a few passing tests and one that is deliberately flaky: it asserts the wrong title on the first attempt and the right one on the retry. That gives every reporter and trace mode something to show.

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

test.beforeEach(async ({ request }) => {
  await request.post('/api/test/reset');
});

test('search narrows the catalogue', async ({ page }) => {
  await test.step('open the catalogue', async () => {
    await page.goto('/books');
    await expect(page.getByTestId('result-count')).toHaveText('24 books found');
  });
  await test.step('search for a title', async () => {
    await page.getByTestId('search-input').fill('quiet');
    await page.getByRole('button', { name: 'Search' }).click();
    await expect(page.getByTestId('book-title')).toHaveText('The Quiet Compiler');
  });
});

test('a reader can post a review', async ({ page }) => {
  await page.goto('/signin');
  await page.getByLabel('Username').fill('reader');
  await page.getByLabel('Password').fill(process.env.BOOKSHELF_READER_PASSWORD ?? 'reader-local-only');
  await page.getByTestId('signin-submit').click();
  await page.goto('/books/3');
  await page.getByLabel('Rating').selectOption('5');
  await page.getByLabel('Your review').fill('Worth every page of it.');
  await page.getByTestId('review-submit').click();
  await expect(page.getByTestId('review').filter({ hasText: 'Worth every page' })).toHaveAttribute('data-state', 'saved');
});

test('the theme toggle switches to dark', async ({ page }) => {
  await page.goto('/');
  await page.getByTestId('theme-toggle').click();
  await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
});

// Fails on the first attempt and passes on the retry, to show flaky handling and on-first-retry traces.
test('a flaky check passes on retry', async ({ page }) => {
  await page.goto('/books/1');
  const expected = test.info().retry === 0 ? 'Not the real title' : 'The Quiet Compiler';
  await expect(page.getByRole('heading', { level: 1 })).toHaveText(expected, { timeout: 1_000 });
});

The runs below use BASE_URL=http://localhost:3917 (a Bookshelf started with PORT=3917 node server.js) and RETRIES=1. The reader password is read from BOOKSHELF_READER_PASSWORD; the fallback is the application’s local-only default.

UI Mode

UI Mode is the quickest way to develop a test: a window with the test tree on the left, a watch mode, and a time-travel view of every run. Start it with the --ui flag:

npx playwright test --ui

UI Mode lists every test file in a sidebar. You run everything, one file, one describe block or one test with the triangle next to its name, and filter by text, by @tag, by project or by status (passed, failed, skipped). If your suite uses project dependencies, run the setup tests first by hand: UI Mode does not take setup projects into account on its own. The panels for a selected run are the same ones the trace viewer has:

Panel What it shows

Timeline

A coloured strip of the run. Hover to preview the page at that moment; drag to select a range, which filters the actions, console and network panels to that range.

Actions

Every locator and action with its duration, with Before and After DOM snapshots. The snapshot can be popped out into its own window to open the browser developer tools on it.

Pick locator

Hover over the snapshot to see the locator Playwright would write, edit it in the locator playground and copy it.

Source, Call, Log

The line of the test that issued the action, the parameters and strictness of the call, and the actionability log (scrolled into view, visible, enabled, stable).

Errors, Console, Network

The failure with its code frame, console output from the page and from the test, and every request with headers and bodies.

Attachments, Metadata

Screenshot diffs for visual tests (with a slider to compare expected and actual) and facts about the run such as browser, viewport and duration.

The eye icon next to a test turns on watch mode: the test re-runs whenever its source changes, so the edit-run-look cycle needs no terminal. UI Mode records a trace of every test automatically, whatever the trace option says, and can show only the tests affected by source changes (added in 1.59).

UI Mode normally opens its own window. To use it from a container or a remote machine, serve it over HTTP instead:

npx playwright test --ui-host=0.0.0.0
npx playwright test --ui-port=8080 --ui-host=0.0.0.0

Binding to 0.0.0.0 makes UI Mode, with its traces and any passwords or tokens in them, reachable from other machines on the network; in GitHub Codespaces the forwarded port is private to your account by default. UI Mode was not run for this page because it needs a window to open and click in.

The Playwright Inspector

The Inspector is a separate window that steps through a test action by action while the browser is visible. It has a toolbar to resume, pause and step over, the test code with the current action highlighted, a Pick locator button with a live-editable locator field, and the actionability log of the paused action.

Way to start it Effect

npx playwright test --debug

Opens the Inspector and runs tests one at a time. It is a shortcut for PWDEBUG=1 plus --timeout=0 --max-failures=1 --headed --workers=1: headed browsers, no test timeout, one worker, stop at the first failure.

npx playwright test example.spec.ts:10 --debug

Debug the single test on line 10, in every project of the configuration.

npx playwright test --project=webkit --debug

Debug on one project (browser or device profile). Combine with a file and line number to debug one test in one browser.

PWDEBUG=1 npx playwright test

The environment-variable form of --debug, for scripts, and for Playwright libraries that have no --debug flag.

PWDEBUG=console npx playwright test

Does not open the Inspector; it only exposes a playwright object in the browser developer tools console (see below). PWDEBUG=0 or PWDEBUG=false turns debug mode off.

npx playwright test --debug=cli

Added in 1.59 for coding agents: it prints a session name and waits for playwright-cli attach <session>, so a debugging session can be driven from a terminal instead of a window.

To stop at a particular line instead of stepping from the start, add page.pause() to the test. Run with --debug, press Resume and the test runs until it reaches the pause.

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

test('inspect the catalogue before asserting', async ({ page }) => {
  await page.goto('/books');
  await page.pause(); // opens the Inspector at this point when running headed or with --debug
  await expect(page.getByTestId('book-card')).toHaveCount(6);
});

Run headless, page.pause() does nothing: a two-line test with a page.pause() call passed in headless Chromium without stopping. That is convenient (a forgotten pause does not hang CI) and also the reason the call only helps in a headed run (--headed, --debug or headless: false). The Inspector itself was not run here because it needs a window.

Live locator editing. While paused, the field next to Pick locator shows the locator of the current action. Edit it and the matching elements are highlighted in the page, with a count, so a strictness violation (more than one match) is visible before you change the code. Pick locator lets you hover over the page and prefers role, text and test-id locators, adding filters when several elements match.

Actionability log. Paused on a click, the log already lists the checks Playwright ran: locator resolved, element visible, enabled, stable, receives events, scrolled into view. An action that cannot pass its checks stays marked as pending, which points at the real cause of a timeout.

Browser developer tools and the console API

With PWDEBUG=console and a page.pause() in the test, Playwright adds a playwright object to the page’s developer-tools console. It uses the real selector engine, so you can try locators without touching the test:

Call Does

playwright.$(selector)

The first element matching the Playwright selector.

playwright.$$(selector)

All matching elements.

playwright.inspect(selector)

Reveals the element in the Elements panel.

playwright.locator(selector)

Creates a locator and shows its matching elements.

playwright.selector(element)

Generates a selector for an element, for example playwright.selector($0) for the element selected in the Elements panel.

In WebKit, opening the WebKit Inspector during the run stops the Playwright script from executing further and resets the configured user agent and device emulation, so use the Playwright Inspector there.

Headed mode and slow motion

Playwright is headless by default. --headed (or use: { headless: false }) shows the browser, and in library mode chromium.launch({ headless: false, slowMo: 100 }) slows every operation by 100 ms so you can follow along. In the test runner, slowMo is set through launchOptions in use. Headed runs are for local debugging; on CI, a trace gives you the same information without a display.

Verbose logs with DEBUG=pw:api

Set the DEBUG environment variable to pw:api to log every Playwright call with its result and the timing since the last line. It is the cheapest way to see where time goes, or what the runner actually did, with no GUI at all:

DEBUG=pw:api npx playwright test -c ex-debugging/playwright.config.ts --reporter=list -g theme
  pw:api => apiRequestContext.post started +2ms
  pw:api → POST http://localhost:3917/api/test/reset +6ms
  pw:api ← 200 OK +3ms
  pw:api <= apiRequestContext.post succeeded +1ms
  pw:api => browserType.launch started +1ms
  pw:api <= browserType.launch succeeded +87ms
  pw:api => browserContext.newPage started +1ms
  pw:api <= browserContext.newPage succeeded +51ms
  pw:api => page.goto started +0ms
  pw:api navigating to "http://localhost:3917/", waiting until "load" +1ms
  pw:api   "commit" event fired +4ms
  pw:api   "domcontentloaded" event fired +13ms
  pw:api   "load" event fired +0ms
  pw:api <= page.goto succeeded +0ms

(The excerpt drops the request and response header lines the log also prints.) DEBUG=pw:api also covers API requests, so a flaky request.post shows up with its status. On Windows PowerShell set $env:DEBUG="pw:api" first; in cmd use set DEBUG=pw:api.

Debugging in VS Code

The Playwright extension for VS Code brings the same tools into the editor:

  • Failures are shown in the editor with the expected and received values and the full call log.

  • Show Browser keeps a browser open between runs; click a locator in the code and it is highlighted in the page (the extension also tells you when several elements match), and edit the locator to see the highlight change live.

  • Pick locator in the testing sidebar copies the best locator for the element you click, with the same role-text-test-id preference as the Inspector.

  • A red dot in the gutter sets a breakpoint; running the test with the Debug Test action stops there and gives you the usual step, resume and variables view.

  • Choosing Run Test with Show Browser checked, instead of Debug Test, keeps the browser session reusable so you can open Chrome DevTools on it.

  • The default debug profile is Chromium; Select Default Profile on the debug icon switches the browser (any project in your configuration) used for debugging.

  • Record new and Record at cursor start the code generator (see below) and write into the file at the cursor.

As of Chrome 136 the default Chrome user data directory cannot be driven by automation, so a debug profile that reuses your daily browser profile needs a separate user data directory. The extension was not run for this page; the behaviour is taken from the official guides.

Traces

A trace is a zip archive with everything needed to replay a run after the fact: every action with its parameters and log, screenshots of the page, DOM snapshots before, during and after each action, the console, every network request with bodies, the source of the test and its errors. It is the tool for failures you cannot reproduce on your machine, because the CI job that failed can attach it and you can read it as if you had been watching.

Recording a trace with the test runner

Set trace in use (or pass --trace on the command line). Playwright creates one trace per test run and keeps it according to the mode. "First run" is the initial attempt of a test and "retries" are the attempts caused by retries:

Mode Records and keeps

'off'

No trace (the default).

'on'

Records and keeps a trace for every run. Heavy; use it only while debugging locally.

'on-first-retry'

Records and keeps a trace only for the first retry of a test. The recommended CI setting: passing tests pay nothing and a failure leaves a trace on the retry.

'on-all-retries'

Records and keeps a trace for every retry.

'retain-on-failure'

Records every run, keeps only the traces of failed runs, even if a later retry passes. Works without retries.

'retain-on-first-failure'

Records only the first run of each test (not retries) and keeps it if it failed (added in 1.43).

'retain-on-failure-and-retries'

Records every run and keeps the traces of failed runs and of retries, so you can compare a passing trace with a failing one of a flaky test (added in 1.59).

Which runs end up with a trace file is easy to verify. The deliberately broken test in failing/broken.spec.ts fails on every attempt, with one retry allowed. After each run, the output directory contains:

--trace retain-on-first-failure
  failing-broken-a-broken-expectation-chromium:        error-context.md  trace.zip
  failing-broken-a-broken-expectation-chromium-retry1: error-context.md

--trace retain-on-failure
  failing-broken-a-broken-expectation-chromium:        error-context.md  trace.zip
  failing-broken-a-broken-expectation-chromium-retry1: error-context.md  trace.zip

--trace on-all-retries
  failing-broken-a-broken-expectation-chromium:        error-context.md
  failing-broken-a-broken-expectation-chromium-retry1: error-context.md  trace.zip

A passing test with --trace on leaves a trace.zip in its directory as well. On the command line, npx playwright test --trace on forces the mode for one run and wins over the configuration; locally, UI Mode records traces automatically, so trace: 'on-first-retry' is mainly a CI setting. The trace of a failed test is also attached to the test result, so the list reporter prints the show-trace command to open it and the HTML report links it.

For more control, trace accepts an object. The mode takes any of the values above and the other keys choose what is captured:

Key Default Meaning

mode

none

One of the modes in the table above.

screenshots

true

Records a screencast and renders it as the film strip in the timeline.

snapshots

true

true means DOM snapshots; an object { dom, aria, screen } chooses what to capture on every action: the DOM snapshot (with network activity), an aria snapshot of the page, or a plain screenshot (aria and screen added in 1.63).

sources

true

Includes the test source files so the viewer can show the code of each action.

attachments

true

Includes the test’s attachments in the trace.

A configuration that keeps richer traces for failures only (it extends the base config shown earlier):

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

export default defineConfig(base, {
  use: {
    trace: {
      mode: 'retain-on-failure',
      screenshots: true,
      snapshots: { dom: true, aria: true, screen: true },
      sources: true,
      attachments: true,
    },
  },
});

With this configuration the failing test’s trace.zip contained aria/ entries (one JSON aria snapshot per action phase), screenshots/ and screencast/ images, the test source under src/ and the attachments, in addition to the usual trace.trace and trace.network files. The extra snapshots make the archive larger, so enable them where you need them, not everywhere.

The context.tracing API

Outside the test runner (library mode, or a test that manages its own context), use context.tracing. Its main methods are:

Method Purpose

tracing.start(options)

Starts recording. Options: screenshots, snapshots (true or { dom, aria, screen }), sources, title (the name shown in the viewer), name (prefix for intermediate files) and live (write an unarchived file that updates in real time, added in 1.59).

tracing.stop({ path })

Stops recording and writes the zip to path; without a path the recording is discarded.

tracing.startChunk() and tracing.stopChunk({ path })

Split one tracing session into several trace files on the same context.

tracing.group(name) and tracing.groupEnd()

Group the calls in between under a heading in the viewer; inside tests prefer test.step.

tracing.startHar(path) and tracing.stopHar()

Record a HAR file for the context (added in 1.60).

The runner already traces every context it creates when trace is on, and starting a second recording on such a context throws tracing.start: Tracing has been already started. A test that wants to drive tracing itself therefore turns the runner’s tracing off for that file:

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

// The runner already traces every context it creates when `trace` is on, and tracing.start() throws
// "Tracing has been already started" in that case. Turn the runner's tracing off for this file.
test.use({ trace: 'off' });

test('context.tracing records a chunk of a session', async ({ browser, baseURL }, testInfo) => {
  const context = await browser.newContext({ baseURL });
  await context.tracing.start({
    title: 'Bookshelf search',
    screenshots: true,
    snapshots: { dom: true, aria: true, screen: true },
    sources: true,
  });
  const page = await context.newPage();
  await page.goto('/books');
  await context.tracing.group('Search for a book');
  await page.getByTestId('search-input').fill('quiet');
  await page.getByRole('button', { name: 'Search' }).click();
  await expect(page.getByTestId('book-title')).toHaveText('The Quiet Compiler');
  await context.tracing.groupEnd();
  await context.tracing.stop({ path: testInfo.outputPath('manual-trace.zip') });
  await context.close();
});

The official guidance is to prefer the configuration option in tests, because traces made with context.tracing do not record the test’s expect calls; the manual API is for scripts and for contexts that you create yourself. In this run the trace of the example contained the Search for a book group with the fill, click and a toHaveText expectation under it.

Opening a trace

npx playwright show-trace path/to/trace.zip
npx playwright show-trace https://example.com/trace.zip
npx playwright show-report

show-trace opens the viewer on a local file or a URL, so a trace stored as a CI artifact can be opened without downloading it first. show-report serves the last HTML report, where every test that recorded a trace has a trace icon next to it (and a Traces tab in the detail view). The viewer is also hosted at https://trace.playwright.dev/, a static progressive web app: drop a trace.zip on it or use the Select file button. The page loads the trace entirely in your browser and transmits nothing externally. To open a remote trace there, pass its URL as a query parameter, for example https://trace.playwright.dev/?trace=https://example.com/trace.zip; the usual CORS rules apply to the server that hosts the file.

The viewer shows the same panels as UI Mode (a trace is exactly what UI Mode shows live). For each selected action:

  • Actions, with a Before, Action and After DOM snapshot. The Action snapshot is taken at the moment of the input and highlights the exact point where Playwright clicked, which is the quickest way to see that a click landed on an overlay. The snapshots are live DOM: you can open the browser developer tools on them and inspect HTML and CSS.

  • Source, Call and Log: the line of the test, the call parameters, and the actionability and retry log.

  • Errors, Console and Network: double-click an action to filter console and network to that action, or drag over the timeline to filter to a range.

  • Metadata and Attachments.

  • Display Aria (added in 1.63): when a trace has aria and screen snapshots, the viewer shows the action screenshot next to the aria snapshot, and hovering an aria node highlights it on the screenshot. This is the view to use when the test is written with role locators, because it shows the accessibility tree the locators match against.

The trace command line

Since 1.59, npx playwright trace explores a trace from the terminal, which is useful over SSH, in CI logs and for coding agents that cannot open a window. The installed 1.64.1 lists these subcommands:

Subcommand Purpose

trace open <trace>

Extracts the trace for inspection and prints its metadata. The other subcommands work on the opened trace.

trace close

Removes the extracted data.

trace actions

Lists actions. Options: --grep <pattern> and --errors-only.

trace action <id>

Shows the details of one action: parameters, error and call log.

trace requests

Lists network requests. Options: --grep <pattern>, --method <method>, --status <code> and --failed.

trace request <id>

Shows one request with its headers.

trace console

Lists console messages. Options: --grep, --errors-only, --warnings, --browser and --stdio.

trace errors

Shows errors with their stack traces.

trace snapshot <id>

Prints the DOM snapshot of an action as an aria snapshot. Options: --phase before|action|after, and --serve to serve it on localhost.

trace screenshot <id>

Saves the screencast frame of an action; -o <path> chooses the file.

trace attachments and trace attachment <id>

Lists the trace’s attachments, or extracts one.

trace install-skill

Installs a SKILL.md that teaches an agent to use these commands.

Running them on the trace of the flaky test’s first attempt (the one that failed):

npx playwright trace open ex-debugging/test-results/catalogue-a-flaky-check-passes-on-retry-chromium/trace.zip
npx playwright trace actions
  Playwright:   1.64.1
  Title:        catalogue.spec.ts:38 › a flaky check passes on retry
  Duration:     1.1s
  Actions:      28
  Pages:        1
  Network:      5 requests
  Errors:       1
  Attachments:  1

     # Time       Action                                                  Duration
  ──── ─────────  ─────────────────────────────────────────────────────── ────────
    1. 0:00.000  Before Hooks                                                35ms
    2. 0:00.000    beforeEach hook                                            7ms
    5. 0:00.006      POST                                                     1ms
                     /api/test/reset
   10. 0:00.035  Navigate                                                    14ms
                 /books/1
   11. 0:00.050  Expect "toHaveText"                                         1.0s  ✗
                 getByRole('heading', { level: 1 })
   12. 0:01.054  After Hooks                                                 14ms

(Rows 3, 4 and 6 to 9, the fixture setup lines, were dropped from the excerpt.) The failing action is marked with the cross, and --errors-only lists just that row. Then ask for the action’s details, the requests, and the page at that moment:

npx playwright trace action 11
npx playwright trace requests
npx playwright trace snapshot 11 --phase before
npx playwright trace close
  Expect "toHaveText" getByRole('heading', { level: 1 })

  Time
    start:     0:00.050
    duration:  1.0s

  Parameters
    selector: "internal:role=heading[level=1]"
    expression: "to.have.text"
    expectedText: [{"string":"Not the real title","normalizeWhiteSpace":true}]
    timeout: 1000

  Error
    Error: expect(locator).toHaveText(expected) failed
    Locator:  getByRole('heading', { level: 1 })
    Expected: "Not the real title"
    Received: "The Quiet Compiler"
    Timeout:  1000ms

     # Start     Method   Status   Name        Duration     Size
  ──── ───────── ──────── ──────── ─────────── ────────── ────────
    1. 0:00.006  POST     200      reset            1ms       24
    2. 0:00.037  GET      200      1                2ms     2.2K
    3. 0:00.040  GET      200      app.css          2ms     2.4K
    4. 0:00.040  GET      200      app.js           4ms     3.2K
    5. 0:00.040  GET      200      1.svg            4ms      782

### Page
- Page Title: The Quiet Compiler - Bookshelf
### Snapshot
- heading "The Quiet Compiler" [level=1] [ref=f1e14]
- paragraph [ref=f1e15]: by Mara Velde

The listing of requests omits the Route column and the snapshot excerpt shows only part of the aria tree. One difference to note: the release notes for 1.59 pass the phase as trace snapshot 9 --name after, but the 1.64.1 command takes --phase before|action|after, as its --help shows; --name failed with an error. Trust npx playwright trace snapshot --help for the installed version. A trace recorded with context.tracing and the aria option opens the same way, and its snapshot output is the aria tree of the page at that action.

Reading a trace: a short routine

  1. Start at the red action in the timeline or trace actions --errors-only and read the error: expected and received values, and the call log with the number of retries.

  2. Look at the Before snapshot: was the element there, covered, disabled, or still loading?

  3. Check Network for failed or slow requests and Console for page errors around the same time.

  4. Use the Action snapshot to see where a click or fill actually landed.

  5. If the test is flaky, compare the trace of the failing attempt with a passing one (retain-on-failure-and-retries keeps both).

Generating tests with codegen

Codegen records your interaction with a page and writes Playwright code for it. Playwright inspects the rendered page and picks the most resilient locator, preferring role, text and test-id locators, and making it more specific when several elements match. Generated code is a first draft: it rarely contains the waits-by-assertion and the page-object structure that a maintainable suite needs (see Test Design & Page Objects), so review and tidy it.

npx playwright codegen localhost:3917/books

The command opens two windows: a browser, where you click and type, and the Playwright Inspector, which shows the code as it is generated. The URL is optional; you can type it into the browser instead. In the Inspector:

  • Record toggles recording. Stop it to use Pick locator, which highlights the locator under the cursor and puts it in a field you can edit and copy.

  • The toolbar generates assertions. Click an icon and then an element on the page: assert visibility adds toBeVisible(), assert text adds a text assertion and assert value checks the current value of an input.

  • Copy copies the code, Clear starts over, and the language selector switches the output among the test runner and the Python, Java and .NET flavours. On the command line, --target does the same.

  • Closing the Inspector window, or stopping the command in the terminal, finishes the session.

An actual recording of the Bookshelf search (click the search box, type "quiet", press Search, then use the three assertion tools) produces code of this shape. This file was written by hand in the style codegen generates it, using the locators and assertions codegen would choose for these elements, and was then run as a normal test (it passed); the recording itself was not run for this page because it needs a display:

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

// Written in the style `npx playwright codegen --test-id-attribute data-testid localhost:3000/books` produces:
// role, label and test id locators, plus the three generated assertion kinds.
test('test', async ({ page }) => {
  await page.goto('/books');
  await page.getByRole('searchbox', { name: 'Search books' }).click();
  await page.getByRole('searchbox', { name: 'Search books' }).fill('quiet');
  await page.getByRole('button', { name: 'Search' }).click();
  await expect(page.getByTestId('book-title')).toBeVisible();                       // assert visibility
  await expect(page.getByTestId('book-title')).toContainText('The Quiet Compiler'); // assert text
  await expect(page.getByRole('searchbox', { name: 'Search books' })).toHaveValue('quiet'); // assert value
});

Pass --test-id-attribute data-testid when the application uses data-testid (Playwright’s default test-id attribute is data-testid, so this matters only when your attribute has another name, as in an application that uses data-test).

Codegen options

npx playwright codegen --help in 1.64.1 lists these options:

Option Effect

-o, --output <file>

Save the generated script to a file.

--target <language>

javascript, playwright-test (the default), python, python-async, python-pytest, csharp, csharp-mstest, csharp-nunit, csharp-xunit, java or java-junit.

-b, --browser <type>

chromium (cr), firefox (ff) or webkit (wk); default chromium.

--channel <channel>

A Chromium distribution channel such as chrome, chrome-beta or msedge-dev.

--device <name>

Emulate a device, for example "iPhone 13": viewport, user agent, touch and device scale factor.

--viewport-size <size>

Viewport in pixels, for example "800,600". The default recording viewport is fixed so tests run under the same conditions as the recording.

--color-scheme <scheme>

light or dark.

--geolocation <lat,lon>, --timezone <tz>, --lang <locale>

Emulate location, time zone and locale, for example --timezone="Europe/Rome" --geolocation="41.890221,12.492348" --lang="it-IT".

--save-storage <file>

At the end of the session, save cookies, localStorage and IndexedDB to a file.

--load-storage <file>

Start from a storage state saved earlier, so you begin signed in.

--user-data-dir <dir>

Use a fixed browser profile. Since Chrome 136 the default profile cannot be used by automation, so create a separate directory.

--http-credentials <user:password>

HTTP Basic authentication. Unlike credentials in the URL they are sent to every origin that asks, and appear in the generated code (the option was added in 1.63).

--proxy-server <proxy>, --proxy-bypass <domains>

Route the session through a proxy and skip it for some domains.

--ignore-https-errors, --user-agent <ua>, --block-service-workers, --timeout <ms>

Accept invalid certificates, override the user agent, block service workers, and set a timeout for actions (there is none by default).

--save-har <file>, --save-har-glob <glob>

Save a HAR of the session, optionally filtered by URL glob.

--test-id-attribute <name>

The attribute used for generated test-id locators.

Examples of the emulation flags from the official guide, for the Bookshelf:

npx playwright codegen --device="iPhone 13" localhost:3917
npx playwright codegen --viewport-size="800,600" localhost:3917
npx playwright codegen --color-scheme=dark localhost:3917
npx playwright codegen --timezone="Europe/Rome" --geolocation="41.890221,12.492348" --lang="it-IT" localhost:3917

Note the viewport option’s name: the guide’s text calls it --viewport, but the command accepts --viewport-size, as the help output above shows.

Recording behind a sign-in

Record the sign-in once, save the storage state, and load it in later sessions so you do not record the login again:

npx playwright codegen localhost:3917/signin --save-storage=auth.json
npx playwright codegen localhost:3917/books --load-storage=auth.json

The first command saves cookies, localStorage and IndexedDB when you close the browser; the second restores them so the recording starts signed in. The file contains session credentials, so keep it out of version control (add it to .gitignore) and delete it when you finish. The same file works as the storageState of a test project, as described in Authentication & Test Isolation. These commands open a browser and so were not run for this page.

Codegen in a custom setup

When the recording needs a context you configure yourself (for example browserContext.route() mocks), start a headed browser from a script and call page.pause(); the paused page opens the same Inspector with the codegen controls:

const { chromium } = require('@playwright/test');

(async () => {
  // Make sure to run headed.
  const browser = await chromium.launch({ headless: false });

  // Setup context however you like.
  const context = await browser.newContext({ /* pass any options */ });
  await context.route('**/*', route => route.continue());

  // Pause the page, and start recording manually.
  const page = await context.newPage();
  await page.pause();
})();

This script needs a display and was not run for this page. Two further sources of generated tests exist: the Record new and Record at cursor buttons in VS Code (which use the same generator and insert the code at your cursor) and, since 1.56, the generator agent described in Playwright Test Agents.

Reporters

A reporter turns a run into output. Choose one with --reporter on the command line or reporter in the configuration, and use several at once with an array. --reporter replaces the configured list, whereas --add-reporter (added in 1.63) appends to it:

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

export default defineConfig({
  // 'github' for GitHub Actions CI to generate annotations, plus a concise 'dot'
  // default 'list' when running locally
  reporter: process.env.CI ? [['github'], ['dot'], ['html', { open: 'never' }]] : [['list']],
});

The default is list locally and dot when the CI environment variable is set. The built-in reporters in 1.64.1 are list, line, dot, json, junit, null, github, html, blob, perfetto and coverage, plus any reporter module you point to. Every built-in console reporter shows the full failure details; they differ in how much they print for passing tests. One behaviour is worth knowing: when none of the configured reporters writes to the terminal (json or junit with an output file, html, blob, perfetto, null), Playwright adds the line reporter (or dot on CI) so you still see progress. A run with --reporter=null printed the line reporter’s output and the failure details; null does not mean silent, it means "no extra reporter" (the exit code still reports failure).

list, line and dot

list prints one line per test as it finishes, with a tick or cross, the project, file, title and duration; retries appear as separate lines marked (retry #1). The excerpt below is the real output for the example project with one retry:

npx playwright test --reporter=list
Running 5 tests using 1 worker

  ✓  1 [chromium] › ex-debugging/catalogue.spec.ts:7:5 › search narrows the catalogue (252ms)
  ✓  2 [chromium] › ex-debugging/catalogue.spec.ts:19:5 › a reader can post a review (991ms)
  ✓  3 [chromium] › ex-debugging/catalogue.spec.ts:31:5 › the theme toggle switches to dark (99ms)
  ✘  4 [chromium] › ex-debugging/catalogue.spec.ts:38:5 › a flaky check passes on retry (1.1s)
  ✓  5 [chromium] › ex-debugging/catalogue.spec.ts:38:5 › a flaky check passes on retry (retry #1) (109ms)
  ✓  6 [chromium] › ex-debugging/tracing-api.spec.ts:7:5 › context.tracing records a chunk of a session (403ms)

(The failure details are printed after the list.) Its options are printSteps (print every test.step on its own line), printFailuresInline (show the failure right after the failed test instead of at the end) and omitTags (hide tags added to titles); each also has an environment variable (PLAYWRIGHT_LIST_PRINT_STEPS, PLAYWRIGHT_LIST_PRINT_FAILURES_INLINE, PLAYWRIGHT_LIST_OMIT_TAGS).

line is for large suites: it keeps a single, updating line with the last finished test and prints failures as they occur. When not attached to a terminal (as in the capture below) every progress line is printed:

Running 5 tests using 1 worker

[1/5] [chromium] › ex-debugging/catalogue.spec.ts:7:5 › search narrows the catalogue
[2/5] [chromium] › ex-debugging/catalogue.spec.ts:19:5 › a reader can post a review
[3/5] [chromium] › ex-debugging/catalogue.spec.ts:31:5 › the theme toggle switches to dark
[4/5] [chromium] › ex-debugging/catalogue.spec.ts:38:5 › a flaky check passes on retry
[5/5] (retries) [chromium] › ex-debugging/catalogue.spec.ts:38:5 › a flaky check passes on retry (retry #1)

dot prints one character per test run, which keeps CI logs short. The symbols are · passed, F failed, × failed or timed out and will be retried, ± passed on retry (flaky), T timed out and ° skipped. The run above printed:

Running 5 tests using 1 worker
···×±·

  1 flaky
    [chromium] › ex-debugging/catalogue.spec.ts:38:5 › a flaky check passes on retry ───────────────
  4 passed (3.6s)

The cross followed by the plus-minus sign is the flaky test: it failed once and passed on the retry. The summary classifies it as flaky rather than passed or failed, which is exactly the information to watch in CI (see Running in CI/CD). --fail-on-flaky-tests (or failOnFlakyTests in the configuration) turns a flaky run into a failing one.

html

The HTML reporter writes a self-contained report folder. It lists every test by file and project with filters for passed, failed, flaky and skipped, shows each test’s errors, steps, attachments and trace, and, for tests that were retried, lists every attempt. By default it opens in the browser when a test failed (open: 'on-failure'); in CI set open: 'never'.

PLAYWRIGHT_HTML_OPEN=never PLAYWRIGHT_HTML_OUTPUT_DIR=ex-debugging/my-report PLAYWRIGHT_HTML_TITLE="Bookshelf debug run" \
  npx playwright test --reporter=html
npx playwright show-report ex-debugging/my-report

The run wrote index.html, a data folder with attachments and a trace folder with the trace viewer assets to ex-debugging/my-report. show-report serves a report folder (or, since 1.60, a .zip archive of one, such as a CI artifact) on a local port. Options and their environment variables:

Option Environment variable Meaning

outputFolder

PLAYWRIGHT_HTML_OUTPUT_DIR

Where to write the report; default playwright-report.

open

PLAYWRIGHT_HTML_OPEN

always, never or on-failure (default).

host, port

PLAYWRIGHT_HTML_HOST, PLAYWRIGHT_HTML_PORT

Where show-report serves it; port 9323 or a free one.

title

PLAYWRIGHT_HTML_TITLE

Title shown in the report.

attachmentsBaseURL

PLAYWRIGHT_HTML_ATTACHMENTS_BASE_URL

Where the data folder is uploaded when the report and its attachments are stored separately.

noCopyPrompt, noSnippets

PLAYWRIGHT_HTML_NO_COPY_PROMPT, PLAYWRIGHT_HTML_NO_SNIPPETS

Hide the "Copy prompt" button for errors, or the code snippets in the action log.

doNotInlineAssets

PLAYWRIGHT_HTML_DO_NOT_INLINE_ASSETS

Write scripts, styles and data as separate files, for a strict Content Security Policy.

mergeFiles

PLAYWRIGHT_HTML_MERGE_FILES

Group tests by their top-level describe instead of by file.

Treat a report folder like the traces inside it: it can contain page content, request bodies and screenshots, so publish it only where the same people may see the application’s data.

json and junit

json writes one object with the configuration, the suites, specs and per-attempt results, and the aggregate statistics. junit writes JUnit-style XML for build servers that parse it. Both print to standard output unless you set a file:

PLAYWRIGHT_JSON_OUTPUT_NAME=results.json npx playwright test --reporter=json
PLAYWRIGHT_JUNIT_OUTPUT_NAME=results.xml npx playwright test --reporter=junit

Or in the configuration, reporter: [['json', { outputFile: 'results.json' }], ['junit', { outputFile: 'results.xml' }]]. The relative path is resolved against the configuration’s directory. The stats object of the JSON file for the run above reported 4 expected, 0 unexpected, 0 skipped and 1 flaky test in a duration of about 3.8 seconds, and the flaky spec inside the file has status: "flaky" with two results, the first failed (retry 0) and the second passed (retry 1). The head of the JUnit file:

<testsuites id="" name="" tests="5" failures="0" skipped="0" errors="0" time="4.471997">
<testsuite name="catalogue.spec.ts" timestamp="2026-10-25T19:57:43.975Z" hostname="chromium" tests="4" failures="0" skipped="0" time="2.529" errors="0">
<testcase name="search narrows the catalogue" classname="catalogue.spec.ts" time="0.187">
</testcase>
...
<testcase name="a flaky check passes on retry" classname="catalogue.spec.ts" time="1.255">
<system-out>
<![CDATA[
[[ATTACHMENT|test-results/catalogue-a-flaky-check-passes-on-retry-chromium/error-context.md]]

[[ATTACHMENT|test-results/catalogue-a-flaky-check-passes-on-retry-chromium-retry1/trace.zip]]
]]>
</system-out>
</testcase>

A flaky test is reported as passed (it has no failure element) with its attachments in system-out, so a JUnit-based dashboard does not show flakiness; use the JSON or HTML report for that. Since 1.59 the JUnit reporter distinguishes types of errors, so failures that used to be failure elements may now be error elements. The JUnit options are outputFile, stripANSIControlSequences, includeProjectInTestName, omitTags, plus the environment variables PLAYWRIGHT_JUNIT_OUTPUT_DIR, PLAYWRIGHT_JUNIT_OUTPUT_NAME, PLAYWRIGHT_JUNIT_OUTPUT_FILE, PLAYWRIGHT_JUNIT_SUITE_ID and PLAYWRIGHT_JUNIT_SUITE_NAME. For JSON, PLAYWRIGHT_JSON_OUTPUT_DIR, PLAYWRIGHT_JSON_OUTPUT_NAME and PLAYWRIGHT_JSON_OUTPUT_FILE do the same job; the full-file-path variable wins over the other two.

blob

The blob reporter writes a zip with every detail of the run so that reports from several machines can be merged into any other report later. It exists for sharded runs:

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

The run wrote blob-report/report.zip (about 37 KB for this suite) in the package directory. With sharding the file is named report-<hash>-<shard>.zip; the hash comes from --grep, --project and file filters so different invocations do not clash. Options: outputDir (PLAYWRIGHT_BLOB_OUTPUT_DIR; existing content is deleted before writing), fileName (PLAYWRIGHT_BLOB_OUTPUT_NAME) and outputFile (PLAYWRIGHT_BLOB_OUTPUT_FILE, which overrides the other two). When you run the same tests in different environments, set the top-level tag option (for example from a CI variable) so each merged report is labelled. The merge itself, and the CI recipe for it, are in Parallelism, Sharding & Retries; the merge-reports command above was not run on this page.

github, null and perfetto

  • github creates failure annotations on the files of a pull request when it runs in GitHub Actions. Other reporters work there too, but only github annotates; avoid it with a matrix strategy, because the failures multiply. It accepts omitTags (or PLAYWRIGHT_GITHUB_OMIT_TAGS). Run outside GitHub Actions it printed the line reporter’s output, and with GITHUB_ACTIONS=true set locally the output was the same, so the annotation format was not captured here; it is documented from the official page.

  • null produces no report of its own (see the note on added reporters above).

  • perfetto (added in 1.63) writes a Trace Event Format file, test-results/perfetto.json by default, that opens in the Perfetto UI or chrome://tracing: one lane per worker, every test a slice with its hooks, fixtures and steps nested inside. An output file name ending in .gz is compressed on the fly. The run produced a 19 KB file for this suite; it was not opened in the Perfetto UI. Environment variables: PLAYWRIGHT_PERFETTO_OUTPUT_DIR, PLAYWRIGHT_PERFETTO_OUTPUT_NAME and PLAYWRIGHT_PERFETTO_OUTPUT_FILE.

  • coverage appears in the 1.64.1 help text but is not described on the reporters page, so it is not covered here.

Custom reporters

A custom reporter is a class that implements some of the Reporter methods, exported as the default export of a module:

import type {
  FullConfig, FullResult, Reporter, Suite, TestCase, TestResult,
} from '@playwright/test/reporter';

class MyReporter implements Reporter {
  onBegin(config: FullConfig, suite: Suite) {
    console.log(`Starting the run with ${suite.allTests().length} tests`);
  }
  onTestBegin(test: TestCase, result: TestResult) {
    console.log(`Starting test ${test.title}`);
  }
  onTestEnd(test: TestCase, result: TestResult) {
    console.log(`Finished test ${test.title}: ${result.status}`);
  }
  onEnd(result: FullResult) {
    console.log(`Finished the run: ${result.status}`);
  }
}
export default MyReporter;

Use it by path, in the configuration (reporter: './my-reporter.ts') or on the command line. Run alone, it printed:

npx playwright test --reporter=./ex-debugging/my-reporter.ts
Starting the run with 5 tests
Starting test search narrows the catalogue
Finished test search narrows the catalogue: passed
...
Starting test a flaky check passes on retry
Finished test a flaky check passes on retry: failed
Starting test a flaky check passes on retry
Finished test a flaky check passes on retry: passed
Starting test context.tracing records a chunk of a session
Finished test context.tracing records a chunk of a session: passed
Finished the run: passed

Note that onTestEnd fires once per attempt (the flaky test appears twice) and that status is the status of that attempt; the test’s overall outcome is test.outcome(). A custom reporter prints nothing unless it logs, and Playwright assumes it prints to the terminal, so no line reporter is added. To keep the usual console output and add your reporter on top, use --add-reporter:

npx playwright test --reporter=dot --add-reporter=./ex-debugging/my-reporter.ts
Running 5 tests using 1 worker
Starting the run with 5 tests
Starting test search narrows the catalogue
·Finished test search narrows the catalogue: passed
...
×Finished test a flaky check passes on retry: failed
Starting test a flaky check passes on retry
±Finished test a flaky check passes on retry: passed

Other Reporter hooks exist for steps (onStepBegin, onStepEnd), standard output (onStdOut, onStdErr), errors outside tests (onError), printsToStdio() and, since 1.62, a preprocess() hook that runs before onBegin and can mark tests as skipped, excluded, fixed or failing. Steps carry subtitle and params (added in 1.63), which the trace viewer and the HTML report show next to the step title. Community reporters worth reading as examples include the Allure, GitHub Actions, Mail, ReportPortal and Monocart reporters.

Debugging flags at a glance

Flag or variable Use

--trace on

Force a trace for every run of this command, overriding the configuration. Open the result from the HTML report or with show-trace.

--debug or PWDEBUG=1

Inspector, headed, one worker, no timeout, stop at the first failure.

--debug=cli

Debug over the CLI for agents (1.59+).

PWDEBUG=console

playwright helper object in the browser console.

DEBUG=pw:api

Log every Playwright call and its timing.

--timeout <ms>

Per-test timeout (default 30000; 0 is unlimited). Raise it while stepping, not permanently. --debug sets it to 0.

--headed, --ui, --workers=1

Visible browser, UI Mode, serial execution to remove interference from parallel tests.

--max-failures <n>, -x

Stop after the first n failures so you debug one thing at a time.

--last-failed, --only-changed

Re-run only the failures of the last run, or the test files changed since the last commit (--only-changed, added in 1.46).

--retries <n>

Retry failed tests, which also triggers on-first-retry traces.

--project <name>

Narrow the run to one browser or device profile.

The last-failed list lives in .last-run.json under the output directory. A re-run with --last-failed in the example project ran only the failing test and its retry.

What changed recently

  • 1.64: no change to the debugging tools; the baseline for this page.

  • 1.63: aria and screen snapshots in traces (snapshots: { dom, aria, screen }) and the Display Aria mode of the trace viewer; the perfetto reporter; --add-reporter; the omitTags option for list, line, dot, github and junit; codegen --http-credentials; subtitle and params on test.step.

  • 1.62: the HTML reporter’s mergeFiles option can be set from the configuration, and the new reporter.preprocess() hook lets a reporter mark tests as skipped, excluded or expected to fail before the run starts.

  • 1.61: HAR and trace recordings include WebSocket requests; video modes match the trace modes.

  • 1.60: tracing.startHar(); npx playwright show-report accepts a .zip archive.

  • 1.59: the npx playwright trace command line, the retain-on-failure-and-retries trace mode, tracing.start({ live }), npx playwright test --debug=cli, and a UI Mode option that shows only the tests affected by source changes.

  • 1.58 and 1.57: a Speedboard tab (1.57) and a timeline (1.58) in the HTML report for merged runs.

  • 1.56: Test Agents, including a generator that writes tests, and an html reporter option that turns off the Copy prompt button (the button itself, which copies an error with context for an AI assistant, appeared in the HTML report, trace viewer and UI Mode in 1.51).

  • 1.46: --only-changed.

  • 1.43: the retain-on-first-failure trace mode.