Debugging Playwright with PWDEBUG and the Inspector
You hit “run” on a Playwright test, it goes red, and the terminal hands you a stack trace that tells you almost nothing about what the browser was actually doing. The fastest way out of that loop is not more console.log calls; it is stepping through the run live. This guide covers Playwright PWDEBUG inspector debugging end to end, including the PWDEBUG environment variable, the Inspector window, page.pause(), the locator playground, and how to combine all of them with the UI mode and trace viewer for a complete debugging toolkit.
๐ญ Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.
Contents
What PWDEBUG Actually Does
The PWDEBUG environment variable is a single switch that flips Playwright into a fully interactive debugging session. When you set it, three things happen at once: the browser launches in headed mode so you can see the page, the Playwright Inspector window opens next to it, and every default timeout is disabled so the run does not abort while you are reading the screen. That last part is the most underrated feature; without it, your 30-second action timeout would fire while you are still figuring out what went wrong.
You launch a debugging session by prefixing your normal test command with the variable. On macOS and Linux it looks like this, and the Inspector pops open paused on the very first action.
// Run a single spec under the Inspector (macOS / Linux)
// PWDEBUG=1 npx playwright test tests/login.spec.ts
// Run only one test by title, which is the common debugging case
// PWDEBUG=1 npx playwright test -g "user can log in"
// On Windows PowerShell, set the variable first:
// $env:PWDEBUG=1
// npx playwright test tests/login.spec.ts
// The test file itself needs no changes at all:
import { test, expect } from '@playwright/test';
test('user can log in', async ({ page }) => {
await page.goto('https://example.com/login');
await page.getByLabel('Email').fill('jane@acme.io');
await page.getByLabel('Password').fill('s3cret');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
The value you assign barely matters. PWDEBUG=1 is the documented form, but PWDEBUG=console additionally exposes a playwright helper object inside the browser DevTools console so you can experiment with selectors against the live page. Setting PWDEBUG=0 is treated as “on” too, which surprises people, so to turn it off you must unset the variable rather than set it to zero.
Driving the Playwright Inspector Window
The Inspector is the panel that appears beside the browser. At the top sits a toolbar with the controls that make stepping through a test feel like using a real debugger. Each control maps to a precise behavior you will use constantly once it becomes muscle memory.
| Control | What it does | When to reach for it |
|---|---|---|
| Step over (F10) | Runs the next single action, then pauses again | Walking line by line through a failing flow |
| Resume (F8) | Runs continuously until the next page.pause() or the end | Skipping ahead past code you trust |
| Record | Generates Playwright code from your manual clicks | Adding new steps without leaving the session |
| Pick locator | Click an element to get a suggested resilient locator | Fixing a broken or brittle selector |
| Explore / playground | Type a locator and highlight all matches live | Verifying a selector resolves to one element |
As you step, the Inspector highlights the current line of your test source and, crucially, shows the actionability log for the pending action. If a click is stuck, the log tells you exactly why: the element is not visible, not stable, covered by another node, or still detached from the DOM. This is the single most valuable readout in the whole tool, because most “flaky” Playwright failures are really actionability problems hiding behind an auto-wait.
Pausing Exactly Where You Need With page.pause()
Setting PWDEBUG pauses on the first action, but often you already know the first twenty steps are fine and you only care about line 21. Instead of stepping through everything, drop a page.pause() call right before the suspect step. When the runner reaches it, execution halts and the Inspector opens at exactly that point with all prior state intact.
import { test, expect } from '@playwright/test';
test('checkout total updates after applying coupon', async ({ page }) => {
await page.goto('https://shop.example.com/cart');
await page.getByRole('button', { name: 'Proceed to checkout' }).click();
// Everything above is known-good; freeze the run right here.
await page.pause();
await page.getByLabel('Coupon code').fill('SAVE20');
await page.getByRole('button', { name: 'Apply' }).click();
await expect(page.getByTestId('order-total')).toHaveText('$80.00');
});
A nice property of page.pause() is that it is a no-op in normal headless runs unless the Inspector is open, so a stray pause will simply be ignored in CI rather than hanging your pipeline. Even so, treat it like a debugger breakpoint: keep it on a debugging branch and never commit it to main. To enforce that, many teams add a lint rule that flags page.pause( in committed code.
Once paused, you are not limited to the Inspector buttons. Open the browser’s own DevTools and run expressions against the page, or use the Inspector’s locator box to test a selector before you write it into your spec. This tight feedback loop is the heart of effective Playwright PWDEBUG inspector debugging: change a locator, see it highlight, resume, and confirm the fix in seconds.
The Locator Playground: Fixing Broken Selectors Fast
Most real test failures come down to a selector that no longer matches, matches nothing, or matches several elements. The Inspector’s “Pick locator” button turns this into a point-and-click exercise. Click it, hover over the element you want, and Playwright proposes a locator that prefers role and accessible name over fragile CSS paths. You then paste that suggestion straight into your test.
You can also verify a locator programmatically while debugging. The highlight() method draws the same overlay the Inspector uses, and count() tells you how many nodes a locator resolves to, which instantly reveals strict-mode violations.
import { test, expect } from '@playwright/test';
test('debug a brittle selector', async ({ page }) => {
await page.goto('https://shop.example.com/products');
// Visually highlight what a locator matches during a paused run.
await page.getByRole('listitem').first().highlight();
// Count matches to catch strict-mode "resolved to N elements" errors.
const itemCount = await page.getByRole('listitem').count();
console.log('List items found:', itemCount);
// Assert an exact count before acting on the list.
await expect(page.getByRole('listitem')).toHaveCount(itemCount);
await page.pause(); // inspect, then experiment in the locator box
});
When a locator returns more than one element and you call an action like click(), Playwright throws a strict-mode error rather than guessing. The Inspector surfaces this clearly, and the fix is usually to narrow the locator with .filter(), .first(), getByRole with an accessible name, or a chained locator(). Testing those refinements live in the playground is far faster than editing the file and re-running the whole suite.
PWDEBUG Versus UI Mode and the Trace Viewer
The Inspector is not the only debugging surface Playwright ships. UI mode (npx playwright test --ui) gives you a watch-mode time-travel experience with a DOM snapshot for every step, and the trace viewer (npx playwright show-trace) lets you post-mortem a failure that already happened, even one from CI. Knowing which tool fits the situation saves a lot of time.
| Tool | How to start | Best for |
|---|---|---|
| PWDEBUG / Inspector | PWDEBUG=1 npx playwright test | Live stepping and fixing selectors interactively |
page.pause() | Add the call, run headed | Jumping to one exact line mid-test |
| UI mode | npx playwright test --ui | Watch mode, time travel, picking tests to run |
| Trace viewer | npx playwright show-trace trace.zip | Post-mortem of a CI failure you cannot reproduce |
--debug flag | npx playwright test --debug | Shorthand that sets PWDEBUG for you |
In practice the --debug CLI flag is the most convenient entry point because it sets PWDEBUG=1 under the hood, runs headed, and opens the Inspector, all without you exporting an environment variable. Use it for a single spec, and switch to UI mode when you want to iterate across many tests with watch-on-save behavior.
// playwright.config.ts โ capture a trace so you can debug CI failures later
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
// Record a trace on the first retry; open it with show-trace.
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
retries: process.env.CI ? 2 : 0,
});
With this config, a test that fails in CI produces a trace.zip you can download and replay locally with npx playwright show-trace trace.zip. The trace viewer shows the same actionability logs, DOM snapshots, and network calls you would see live, which is the closest thing to a recorded Inspector session for failures you cannot reproduce on your own machine.
๐ Level Up Your Playwright
From locators to CI pipelines โ build a production-grade Playwright + TypeScript framework step by step.
A Practical Debugging Workflow
Putting the pieces together, here is a repeatable loop that turns a red test into a green one without random guessing.
- Reproduce the failure once normally and read the error message and actionability log it prints.
- If the cause is unclear, add
await page.pause()just before the failing line and run withnpx playwright test --debug -g "test name". - Step over actions in the Inspector and watch the page; stop when the UI diverges from what your assertion expects.
- Use “Pick locator” or the playground to confirm the selector still matches a single element.
- Fix the locator or add an explicit
expect(...).toBeVisible()wait, resume, and confirm the test passes. - Remove the
page.pause()call and run the spec headless to verify the fix holds without the Inspector.
This workflow scales from a single broken assertion to a deeply flaky end-to-end journey. The key insight is that the Inspector removes guesswork: instead of inferring what the browser did from logs, you watch it happen and read the engine’s own explanation of why an action could not proceed.
Conclusion
Mastering Playwright PWDEBUG inspector debugging changes how you fix tests: you stop sprinkling log statements and start watching the run unfold step by step. Reach for PWDEBUG=1 or the --debug flag to open the Inspector, drop a page.pause() to land on the exact failing line, and lean on the locator playground to repair brittle selectors in seconds. Pair the live Inspector with the trace viewer for CI failures you cannot reproduce, and you will have a complete, no-guesswork debugging toolkit that keeps your suite stable as your app grows.
FAQ
What is the difference between PWDEBUG=1 and the –debug flag?
They do the same core thing: both run the browser headed, open the Playwright Inspector, and disable timeouts. The --debug flag is simply a CLI shortcut that sets PWDEBUG=1 for you, so you do not have to export an environment variable. Use --debug for convenience on a single command; use the raw PWDEBUG variable when you want to combine it with other environment settings or run a non-test script under the Inspector.
Why does my test still time out even with PWDEBUG set?
Setting PWDEBUG disables Playwright’s default action and navigation timeouts so the Inspector can pause indefinitely, but it does not override an explicit timeout you passed yourself, such as click({ timeout: 5000 }) or a custom test.setTimeout(). If a step still aborts, search your test and config for a hard-coded timeout and remove or raise it while debugging. The overall test timeout is also relaxed under PWDEBUG, so a lingering timeout almost always comes from your own code.
Can I use the Inspector to fix a selector without editing my code first?
Yes. Once paused, click “Pick locator” in the Inspector toolbar and hover over the target element to get a suggested role-based locator, or type a candidate selector into the locator playground to see every match highlighted live on the page. This lets you validate a fix before touching your source. When the selector resolves to exactly one element, copy it into your test, resume the run, and confirm the assertion passes.
๐ Master Playwright End to End
Join hundreds of SDETs building real automation frameworks. Lifetime access, hands-on projects, and a job-ready portfolio.
