Debugging: UI Mode, Trace Viewer, Codegen
On this page
Sooner or later a test fails and the message alone does not explain why. Playwright ships four debugging tools, and each fits a different moment: UI mode while you write tests, the Inspector when you need to stop at a line, the trace viewer for failures you did not watch happen (above all on CI), and codegen when you need to discover how to reach an element.
UI mode: the everyday tool#
npx playwright test --ui
UI mode opens a desktop window with your test tree on the left. For whichever test you run, it shows:
- an action timeline: every
goto,click,filland assertion, each with a before/after DOM snapshot you can inspect with the browser's element tools; - the source line for the selected action, plus console messages, network requests and errors;
- a Pick locator button: hover over the snapshot and it shows the locator it would use, which you can copy straight into your test;
- watch mode (the eye icon next to a test or file): the test re-runs every time you save.
Because the snapshots are real DOM, not screenshots, you can inspect an element at the exact moment an assertion failed and see, for example, that the text was "2 item left" rather than "2 items left".
UI mode runs the same projects as the command line, so you can filter to chromium while iterating and run all browsers before committing.
The Inspector: step through a test#
When you want to stop a test at a particular line and poke at the live browser, use debug mode:
npx playwright test tests/todo.spec.ts:14 --debug
The :14 suffix runs only the test declared on that line. --debug is a shortcut for headed mode, one worker, no timeout, and the Playwright Inspector window. The Inspector pauses before each action, highlights the target element in the browser and lets you step forward one action at a time, or edit a locator and see live what it matches.
To stop at a specific point instead of stepping from the beginning, put a breakpoint in the code:
test('completes one todo', async ({ page }) => {
await page.goto('https://demo.playwright.dev/todomvc/');
await page.pause(); // execution stops here when run with --debug or --headed
// ...
});
page.pause() only does something in a headed browser, but remove it before committing anyway; a forgotten breakpoint is confusing for the next person. Setting the environment variable PWDEBUG=1 has the same effect as --debug and is useful when tests are launched by another script.
If you use VS Code, the official Playwright extension gives you the same powers inside the editor: run or debug a single test from the gutter, set normal breakpoints, and pick locators.
Trace viewer: debugging failures after the fact#
UI mode and the Inspector need you at the keyboard. Failures on CI happen while nobody is watching, and that is what traces are for. A trace is a zip file containing every action, DOM snapshots before and after each one, network traffic, console output and the test source.
The generated config already records one when a test is retried:
use: {
trace: 'on-first-retry',
},
The options are 'off', 'on', 'on-first-retry', 'on-all-retries' and 'retain-on-failure'. Recording on every test ('on') is heavy and rarely needed. If you do not use retries, 'retain-on-failure' records every test but keeps the trace only when the test fails.
To produce a trace locally on demand, override the mode from the command line:
npx playwright test --trace on
npx playwright show-report
In the HTML report, open a test and click its trace. You can also open a trace file directly:
npx playwright show-trace test-results/todo-adds-a-todo-item-chromium/trace.zip
or drag the zip onto trace.playwright.dev, which processes the file in your browser rather than uploading it. That is the quickest way to open a trace a colleague attached to a bug.
How to read one efficiently:
- Start at the failed action (marked red) and read the error and the action log, which says what Playwright was waiting for.
- Look at the Before snapshot of that action. Is the element there? Is something covering it?
- Step back through earlier actions to find where the page diverged from what the test expected.
- Check the Network tab for failed or slow API calls around that moment; many "UI" failures are a backend returning a 500.
Screenshots and video can be captured too, with the same retention logic:
use: {
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
All of these land in test-results/ and are linked from the HTML report, which is why chapter 7 uploads that report from CI.
Your CI runs with retries: 0. Which trace setting gives you a trace for every failing test without keeping traces for passing ones?
Without retries, 'on-first-retry' never records anything. 'retain-on-failure' records each test and discards the trace when it passes; 'on' keeps them all.
Make traces readable with test.step#
Long tests produce long, flat action lists. test.step groups actions under a name that appears in the report, in UI mode and in the trace:
import { test, expect } from '@playwright/test';
test('completes a todo', async ({ page }) => {
await test.step('add two todos', async () => {
await page.goto('https://demo.playwright.dev/todomvc/');
const input = page.getByPlaceholder('What needs to be done?');
await input.fill('Buy milk');
await input.press('Enter');
await input.fill('Pay rent');
await input.press('Enter');
});
await test.step('complete one', async () => {
await page.getByTestId('todo-item').filter({ hasText: 'Pay rent' })
.getByRole('checkbox', { name: 'Toggle Todo' }).check();
await expect(page.getByTestId('todo-count')).toHaveText('1 item left');
});
});
A failure now reads "complete one" rather than "locator.check at line 14".
Codegen: discover selectors by clicking#
npx playwright codegen https://demo.playwright.dev/todomvc/
Codegen opens a browser plus a window that writes test code as you interact. It prefers role, label and test-id locators, and its toolbar can insert assertions (visibility, text, value) for the element you click. Useful flags:
npx playwright codegen --device="iPhone 13" https://example.com # emulate a device
npx playwright codegen --save-storage=auth.json https://example.com # save login state when you close it
npx playwright codegen --load-storage=auth.json https://example.com # start already logged in
npx playwright codegen --target=python https://example.com # generate Python instead
Treat recorded code as a draft. It has no notion of which values are test data, it records every incidental click, and it does not know which waits matter. Our article on the Playwright recorder goes through cleaning a recording into a maintainable test.
Codegen recorded getByRole('link', { name: 'Order #4821' }). The order number comes from whatever data the test environment holds today. What should you do with it?
Recorded code has no idea which values are test data. A role locator is only stable if its name is; XPath would be stable in neither sense.
A debugging routine that works#
- Read the error. Playwright's messages usually name the locator and the condition that was not met.
- Reproduce in UI mode with the failing project selected.
- If it only fails on CI, download the report artifact and open the trace.
- If it passes on retry, it is flaky: compare the failing attempt's trace with the passing one and find the state you did not wait for.
When tests run on a remote grid, you get additional artifacts per session, such as a video of the whole screen and the browser's logs; our guide to debugging Playwright tests on TestingBot lists them.