Playwright architecture, selector reliability, and advanced interaction patterns.

Debugging with Playwright Inspector and UI Mode

The Trace Viewer explains a failure after it happened; the Inspector and UI mode let you debug a test while it runs. When you are authoring a flow or chasing a failure that only appears with a real browser open, you want to pause execution, step action by action, and try selectors against the live page until one sticks. Playwright ships two interactive tools for this: the Inspector, a step debugger driven by PWDEBUG=1 or --debug and page.pause(), and UI mode (--ui), a watch-mode dashboard with a built-in timeline, locator picker, and per-action snapshots. This walkthrough shows when to reach for each. It sits under Trace Viewer & Debugging, part of the Debugging & Test Observability guide.

Interactive debugging tools and their entry points Two debugging surfaces — the Inspector and UI mode — with the commands that launch them and the live page they drive. PWDEBUG=1 / --debug page.pause() --ui (watch mode) Inspector step debugger UI mode timeline + watch Live page locator picker + DOM
Inspector and UI mode are two entry points to the same goal: drive a live page, step actions, and build selectors interactively.

Root cause: post-mortem debugging is slow for authoring

A trace is ideal for a failure you cannot reproduce, but it is a poor fit while you are still writing a flow. When you do not yet know the right selector, or you want to try an action and immediately see the result, the edit-run-read-trace loop is too slow. Interactive debugging collapses it: you pause the browser at a known point, experiment against the real DOM, copy a working locator straight out of the picker, and continue.

The cost is not the run itself but everything wrapped around it. A cold run boots a browser, replays authentication, and walks the whole flow before it reaches the line you care about, and a trace only answers questions you thought to ask before the process exited. Holding the browser open inverts that: the state is live, so a bad guess costs one click instead of one full run. The two tools below cover the spectrum — the Inspector for line-by-line stepping inside a running test, UI mode for a watch-driven dashboard you keep open across edits.

Minimal reproducible example

Drop a page.pause() into a flow you are building. When the test runs under a headed debug session, execution stops at that line and the Inspector opens.

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

test('build the settings flow', async ({ page }) => {
  await page.goto('/settings');
  // Execution halts here; the Inspector opens so you can step and try locators.
  await page.pause();
  await page.getByRole('button', { name: 'Save' }).click();
  await expect(page.getByText('Saved')).toBeVisible();
});

Choosing between the Inspector, UI mode, and the Trace Viewer

Three surfaces overlap enough to be confusing, and picking the wrong one wastes an afternoon. The Inspector is a single-test step debugger: it attaches to one running spec, freezes it, and hands you the live page. Its strength is authoring — a browser sits at exactly the state you care about, so you can try a locator against the real DOM without restarting anything. Its weakness is scale, because it knows nothing about the other four hundred specs in the project.

UI mode inverts that. It lists the whole project, runs any subset headed, and records a timeline of every action with before and after snapshots. Because it watches the filesystem, the run-read-edit cycle shrinks to a single save. What it will not do is stop mid-action and let you poke at the page by hand; for that you still drop a page.pause() and take the Inspector.

The Trace Viewer covers the case neither handles: a failure that already happened somewhere you were not, which in practice means CI. It reads a trace.zip from a run nobody watched, so analyzing a failure from its recorded trace is the remote story while the Inspector and UI mode own the local one. When the question is about traffic rather than DOM state, the same evidence is available in the network and console tabs of a trace.

Which debugging surface fits which situation A matrix rating the Inspector, UI mode and the Trace Viewer against authoring a flow, debugging locally, and diagnosing a failure in CI. Tool Authoring a flow Debugging locally Failure in CI Inspector Best fit Workable Unavailable UI mode Best fit Best fit Unavailable Trace Viewer Too slow Workable Best fit
Interactive tools win where a browser can stay open; the Trace Viewer wins where nobody was watching the run.

Step-by-step fix

  1. Launch the Inspector. Run the test with PWDEBUG=1 npx playwright test settings.spec.ts or npx playwright test settings.spec.ts --debug. Both force a headed browser and open the Inspector docked beside it, paused before the first action. PWDEBUG=1 also disables action timeouts so the session never expires while you think.
  2. Step through actions. Use the Inspector's Step Over button to execute one Playwright call at a time. Each step highlights the target element on the live page and logs the resolved locator, so you watch the test drive the browser action by action.
  3. Pin a precise breakpoint with page.pause(). Instead of stepping from the top, insert await page.pause() at the exact line you care about. Running under debug halts there, letting you skip the setup and land on the problem step directly.
  4. Build selectors with the locator picker. Click "Pick locator" in the Inspector (or in UI mode), then hover and click elements on the live page. Playwright generates a resilient locator — preferring getByRole, getByLabel, and getByText — that you copy straight into the test, following the guidance in Reliable Selector Strategies for Playwright. The picker also shows the match count live, which is the quickest way to catch an ambiguous query before it becomes a strict mode violation.
  5. Switch to UI mode for watch-driven work. Run npx playwright test --ui to open the UI mode dashboard. Pick a test to run it headed with a live timeline; toggle watch mode so the test re-runs automatically every time you save the spec, giving instant feedback while you iterate.
  6. Inspect any action in the UI timeline. In UI mode, click an action on the timeline to see its before/after DOM snapshot, the source line, and the network and console panels — the same surfaces as a trace, but for the run you just triggered, so you confirm a fix without leaving the dashboard.
  7. Remove debug hooks before committing. Delete every page.pause() and avoid committing PWDEBUG-dependent code. A page.pause() left in a spec hangs CI forever because nothing clicks Resume; treat removing it as part of the fix.

Those seven steps are really one small state machine repeated until the flow is right. A debug session spends its life in four states: running headed, paused at a breakpoint, stepping or picking while paused, and resumed toward the next action. Knowing which state you are in tells you which controls are live — the picker and the DOM console only work while the test is paused, and Step Over silently resumes and re-pauses the run rather than freezing it.

States of a paused Inspector session A four-state cycle showing a headed run reaching a pause, stepping and picking locators while paused, then resuming toward the next action. Test running headed browser Paused Inspector attached Stepping and picking locators Resumed to next action page.pause() reached step or pick locator copied in resume run
The picker and the live DOM are only reachable in the paused state; Resume returns the session to the running state at the next action.

UI mode is also the fastest place to confirm a config change behaves. The settings it honors — projects, base URL, timeouts — come from Playwright Config & Fixtures, so a quick UI run validates them against a real browser.

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

test('verified flow after picking locators', async ({ page }) => {
  await page.goto('/settings');
  // Locator copied from the picker — role-based and resilient.
  await page.getByRole('switch', { name: 'Email notifications' }).click();
  await page.getByRole('button', { name: 'Save' }).click();
  await expect(page.getByText('Saved')).toBeVisible();
});

The watch-mode feedback loop

What actually changes the feel of writing tests is the length of the loop, not the features on the dashboard. In the classic cycle you edit a spec, launch a full run from the terminal, wait for a browser to boot and fixtures to build, open the artifact it produced, and only then read what happened. Four moves, and three of them are dead time you spend staring at a progress line.

With --ui and watch enabled, saving the file is the entire trigger. The watcher notices the write, re-runs only the tests currently selected, and repaints the timeline in place while keeping your scroll position and the action you had open, so the difference between the previous run and this one is visible at a glance. Two habits keep that loop tight. First, select a single project before you start, because running the same spec across Chromium, Firefox, and WebKit triples the wait for feedback you only need once. Second, lean on the failed-only filter so a re-run surfaces the one assertion still red instead of a wall of green.

Watch mode also honors test-level scoping, so a temporary test.only keeps re-runs down to one spec while you iterate — delete it before committing for exactly the same reason you delete page.pause(). Expensive fixtures rebuild on every watch run, so if setup dominates the loop, move it to a worker-scoped fixture using the patterns in Setting Up Global Fixtures for Parallel Tests.

Trace-driven loop versus the UI mode watch loop A two-lane timeline comparing edit, full run, open trace and read against save, watch re-run and inspect step. elapsed time Trace loop Edit Full run Open trace Read + edit UI watch loop Save Watch re-run Inspect step loop repeats in seconds
The watch loop removes the launch and artifact-opening phases entirely, which is where most of the wall-clock time in the trace loop goes.

Troubleshooting variants

The Inspector never opens

You ran headless. PWDEBUG=1 and --debug force headed mode, but if you override with --headed=false or a config headless: true that takes precedence in some setups, the Inspector cannot attach. Drop the override and rerun. On a remote or containerized CI box there is no display, so interactive debugging belongs on a local machine — for headless environments use traces instead.

page.pause() seems to do nothing

page.pause() only opens the Inspector in a headed run. Under a plain npx playwright test (headless) it resolves immediately. Run with --debug or PWDEBUG=1 to make the pause interactive, or use UI mode where you can step without code changes.

UI mode does not pick up my new test

The file watcher may not be tracking the path, or the test is filtered out by a project or grep. Confirm the test appears in the UI mode tree, clear any active filter, and check that the spec matches your testMatch glob. Saving the file should trigger a re-run when watch mode is on.

Watch mode re-runs far more than I changed

The watcher follows the import graph, not just the file you saved. Editing a shared helper or a base page object invalidates every spec that imports it, so a one-line change can queue the whole project. That is correct behaviour, but it destroys the loop. Narrow the selection in the dashboard to the specs you are working on, or keep shared code in the layered structure described in Structuring Large Projects with the Page Object Model so an edit touches a smaller blast radius.

Timeouts fire while I am reading the page

Only PWDEBUG=1 disables action and navigation timeouts; a plain --ui run still enforces whatever you configured. If a paused inspection keeps tripping an expectation, raise the limits temporarily or debug with PWDEBUG=1 instead, and keep the committed values tuned per the advice in Configuring Retries and Timeouts for Stable CI.

Verification

Confirm interactive debugging did its job in four ways. First, the locator you copied from the picker resolves to exactly one element — run the finished test and watch it pass headed in UI mode. Second, run the same spec headless (npx playwright test settings.spec.ts) to prove it does not depend on a debug session. Third, grep the codebase for page.pause( before committing to guarantee no interactive breakpoint reaches CI, where it would hang the job indefinitely.

Fourth, run the spec a few times in a row with --repeat-each=5 before you call it done. A flow that only passes when a human was watching usually depended on the extra milliseconds a paused session bought it, and that dependency shows up as an intermittent failure later; the diagnosis path for that class of problem is covered in Detecting and Fixing Flaky Playwright Tests. If the repeat run is clean but CI still fails, capture evidence there rather than guessing, using screenshots and video on failure.

Frequently Asked Questions

What is the difference between the Inspector and UI mode?

The Inspector is a step debugger that attaches to a single running test via PWDEBUG=1, --debug, or page.pause(), letting you execute one action at a time. UI mode (--ui) is a standalone dashboard for browsing, running, and watching the whole suite, with a per-action timeline and snapshots. Use the Inspector to step through one test, UI mode to iterate across many.

Will PWDEBUG or page.pause() break my CI?

Yes if they reach CI. A page.pause() left in a spec halts the run forever because no human clicks Resume, and PWDEBUG forces headed mode that headless CI cannot provide. Remove pauses and keep debug flags out of committed code; rely on trace: 'on-first-retry' for CI evidence instead.

How do I get a good selector without writing it by hand?

Use the locator picker in the Inspector or UI mode. Click "Pick locator," hover the element on the live page, and Playwright generates a resilient locator favoring role, label, and text. Copy it directly into your test, which keeps selectors aligned with the accessibility-first approach recommended for stable suites.

Does UI mode respect my playwright.config projects and fixtures?

It does. UI mode loads the same config as the CLI, so projects appear as selectable filters, use options such as base URL and viewport apply exactly as they would in a normal run, and both test-scoped and worker-scoped fixtures execute normally. Global setup runs too, which is worth remembering when a watch re-run feels slower than expected — the setup work is being repeated, not cached.

Back to overview