How to Debug Playwright and Puppeteer Tests
Debug browser tests by narrowing the failure, inspecting what ran, and collecting the right evidence in Playwright or Puppeteer—including CI-only failures.
To debug Playwright and Puppeteer tests, first reduce the failure to one test and identify where the suspected fault runs: in the test runner or Node.js, in page JavaScript, in the browser process, or in the CI environment. Then make the run observable and collect evidence that fits that fault. Playwright’s Inspector, UI Mode, and test traces serve different purposes; Puppeteer’s headed mode, DevTools, Node inspector, and browser traces are separate tools with different scopes.
A screenshot can show the final page, but it usually cannot explain the sequence that led to a failure. Use logs, locator state, snapshots, network activity, and traces to reconstruct the sequence. This guide covers both frameworks without treating their commands or artifacts as interchangeable.
1. Reduce the failure to a useful reproduction
Start with the smallest run that still fails. A smaller run cuts unrelated output and makes it easier to compare a passing and failing attempt.
Playwright: run one test, file, or browser project
# Run the suite in interactive debug mode
npx playwright test --debug
# Run one test file
npx playwright test example.spec.ts
# Run a test at a particular line
npx playwright test example.spec.ts:10
# Compare a specific configured browser project
npx playwright test example.spec.ts --project=chromium
Playwright’s CLI accepts a file path and line number to filter tests. Use --project when the failure may depend on the browser project. A test that passes in Chromium but fails in another configured browser is a different debugging problem from a test that fails intermittently in every project. See the [Playwright command-line reference](https://playwright.dev/docs/test-cli).
Puppeteer: reduce the script
Puppeteer is commonly used from a Node.js script, so isolate the sequence that fails: navigation, page setup, action, and assertion. Keep the URL, input data, and launch settings fixed while investigating. If the script is part of a larger test suite, invoke only the failing test through that suite’s runner. Puppeteer itself does not use Playwright Test’s CLI or project-selection model.
2. Make the browser behavior visible
A visible browser run helps answer whether the page is in the state your test expects at each step. It is evidence, not proof: a headed run can change timing and may hide a race that occurs in headless CI.
Playwright Inspector and UI Mode
npx playwright test --debug
This starts the Playwright Inspector and a headed browser. Step through actions, inspect locator matches, use the locator picker, and review actionability information. To stop at a particular point in a test, add await page.pause():
import { test, expect } from '@playwright/test';
test('checkout button advances to payment', async ({ page }) => {
await page.goto('https://example.com/checkout');
await page.pause();
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByText('Payment details')).toBeVisible();
});
For an interactive view of test steps and errors, run UI Mode:
npx playwright test --ui
UI Mode can help inspect errors, logs, network requests, DOM snapshots, and locators. Use it when the terminal stack trace does not reveal enough context. The Inspector is useful for stepping through an action sequence; UI Mode is useful for browsing a test run and its surrounding evidence. See [Debug Tests](https://playwright.dev/docs/debug) and [Running and debugging tests](https://playwright.dev/docs/running-tests).
Puppeteer headed mode and slow motion
Launch a headed browser and slow actions down when you need to watch the interaction:
const browser = await puppeteer.launch({ headless: false, slowMo: 250 });
const page = await browser.newPage();
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
await page.goto('https://example.com');
slowMo adds a delay between Puppeteer operations; it can make a sequence easier to observe, but it can also alter timing. If the failure disappears, investigate timing, readiness, and race conditions instead of assuming it is fixed. Puppeteer’s debugging guide documents headed execution, slow motion, console forwarding, and other debugging approaches at [Debugging](https://pptr.dev/guides/debugging).
3. Check locator matches and action preconditions
Before changing a timeout, confirm that the test is targeting the intended element and that the element is ready for the action.
Playwright locator checks
In the Inspector, use the locator picker or edit the locator live. Review the actionability log to see whether a target matched and whether it was visible, enabled, stable, or still waiting. Prefer locators tied to user-facing semantics when they uniquely identify the intended control:
const continueButton = page.getByRole('button', { name: 'Continue' });
console.log('Matches:', await continueButton.count());
await continueButton.click();
A count greater than one means the locator is ambiguous for this interaction. A count of zero may indicate a wrong locator, a page-state assumption, or content that has not loaded. Check the DOM snapshot at the time of failure rather than only inspecting the page after it has changed.
Puppeteer locator behavior
Puppeteer’s locator API waits for elements and checks action preconditions as described in its [page interactions guide](https://pptr.dev/guides/page-interactions). Do not assume every lower-level selector method waits or retries the same way. When changing from a locator to a lower-level method, verify that the page has reached the needed state before acting. A selector that finds an element is not by itself evidence that the element is visible, enabled, stable, or ready to receive input.
4. Collect evidence that matches the failure
Playwright traces for a failing test
A trace can include an action timeline, DOM snapshots, network activity, and logs. It is especially useful when a failure appears in CI but is hard to reproduce locally. Open a saved trace with:
npx playwright show-trace trace.zip
For CI, configure Playwright Test to record a trace on the first retry, so a failing test can produce evidence without tracing every test on every run:
import { defineConfig } from '@playwright/test';
export default defineConfig({
retries: 1,
use: {
trace: 'on-first-retry',
},
});
Choose a trace policy that fits the suite. Tracing every test can add performance overhead. Playwright’s best-practices guidance recommends traces for CI failures and discusses this trade-off in [Best Practices](https://playwright.dev/docs/best-practices). The test-runner trace configuration includes runner context such as assertions; the lower-level [Tracing API](https://playwright.dev/docs/api/class-tracing) does not record test assertions in the same way.
Puppeteer traces and page console
Forward page console messages to Node to find errors raised by page JavaScript:
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
page.on('pageerror', error => console.error('PAGE ERROR:', error));
Puppeteer can also record a browser trace for inspection in Chrome DevTools or a timeline viewer:
await page.tracing.start({ path: 'trace.json' });
await page.goto('https://example.com');
// Perform the actions relevant to the failure here.
await page.tracing.stop();
This is a browser trace for timeline inspection, not the same artifact as a Playwright Test trace. See the [Puppeteer Tracing class](https://pptr.dev/api/puppeteer.tracing).
Do not rely on a final screenshot alone when the suspected fault is about ordering, requests, or a transient DOM state. A screenshot is a point-in-time view; a trace or logs can show how the browser reached it.
5. Identify which execution context is failing
Puppeteer debugging is easier when you first distinguish Node.js script code, JavaScript running in the page, and the browser process. Each needs different inspection tools.
Node.js test or script code
Put debugger at the suspected line in the Node script and launch Node with its inspector paused at startup:
node --inspect-brk test.js
Connect a Node inspector to step through the script. This is the right place to inspect variables and control flow in the test process.
Page JavaScript
Open browser DevTools and put a debugger statement inside the page code being evaluated. For example:
const browser = await puppeteer.launch({ headless: false, devtools: true });
const page = await browser.newPage();
await page.evaluate(() => {
debugger;
// Inspect page-side state in browser DevTools.
return document.readyState;
});
Node’s inspector and browser DevTools inspect different JavaScript contexts. A breakpoint in one will not automatically pause the other.
Browser launch or protocol behavior
For browser process output, use Puppeteer’s dumpio launch option. For Puppeteer’s documented protocol debug logging, use its debug environment variable:
DEBUG=pw:browser npx playwright test
DEBUG=pw:api npx playwright test
Those commands are Playwright logging examples. Puppeteer’s guide documents dumpio: true and NODE_DEBUG="puppeteer:*" for Puppeteer debugging:
const browser = await puppeteer.launch({ dumpio: true });
NODE_DEBUG="puppeteer:*" node test.js
Debug output can contain sensitive information. Review logs before sharing them, especially if requests include authorization headers, cookies, or private page data. Consult the framework’s current guidance in [Puppeteer Debugging](https://pptr.dev/guides/debugging).
6. Diagnose CI-only failures
A test that passes locally and fails in CI needs a reproducible comparison, not just a longer timeout. Start by collecting a trace on failure, then compare:
- The browser project and browser version used locally and in CI.
- The test configuration, including retries, timeouts, and launch settings.
- Environment variables, test data, authentication state, and available services.
- Console output, failed network requests, and the last successful test action.
- Whether the failure is intermittent, tied to a particular worker, or repeatable.
Playwright’s CI guidance notes that headed Linux execution requires Xvfb. If a CI job is configured to run headed, confirm that its display environment is available. A headed local success does not establish that a headless CI run has the same timing or environment. See [Playwright Continuous Integration](https://playwright.dev/docs/ci).
For Puppeteer, preserve the same evidence across a local reproduction and CI run: console and page errors, browser output, and a browser trace when useful. The cited Puppeteer debugging guide describes general tools rather than a CI-specific tracing policy.
7. Common errors and fixes
| Symptom | Likely cause | What to check or change |
|---|---|---|
| Playwright test times out waiting for a locator | The locator does not match, is ambiguous, or the page never reached the expected state. | Inspect the trace or snapshot at the failure, check the locator match count, and verify the preceding navigation or action. |
| Playwright debug run works but CI still fails | The visible local run differs in timing, browser, display setup, or environment. | Record a trace on retry in CI and compare browser project, configuration, logs, and network requests. |
| A Playwright trace file is missing | The selected trace policy may only record on retry, and the failure was not retried or artifacts were not retained. | Check retry settings and CI artifact collection; reproduce under the configured trace policy. |
| Puppeteer action runs before the page is ready | A lower-level selector or action path may not wait for the same conditions as a locator. | Use locator behavior where appropriate and verify the documented preconditions for the method in use. |
| Page JavaScript error is absent from Node output | Page console and page errors are not being forwarded. | Register console and pageerror listeners before the failing action. |
| Node breakpoint does not stop in page code | Node and page JavaScript run in separate execution contexts. | Use Node’s inspector for the script and browser DevTools for page code. |
| Headed browser fails to launch in Linux CI | A display server may be missing. | Use the documented CI setup; headed Linux execution requires Xvfb. |
| Logs are too noisy to diagnose | Debug logging is enabled too broadly or the run includes unrelated tests. | Filter to the failing test first, then enable the relevant API, browser, or protocol logging. |
8. Performance, reliability, and cost considerations
- Keep expensive evidence failure-focused. Playwright traces add overhead, so a retry-focused trace policy is a practical CI starting point. Avoid enabling full traces for every test without a reason.
- Do not treat slow motion as a reliability fix. It changes timing and can mask races. Use it to observe, then address the missing wait condition or unstable assumption the evidence reveals.
- Preserve useful artifacts. A trace is only useful if the CI system retains it and the team can retrieve it. Keep artifact retention aligned with how long failures take to investigate.
- Protect secrets in logs. Browser and protocol logs may reveal request data. Limit access and redact sensitive material before sharing.
- There is no universal timeout adjustment. Increasing a timeout can be appropriate for a known slow operation, but it does not fix a wrong locator, failed request, or page error. Determine which condition is taking time first.
9. Or skip the browser setup
If the debugging task is to capture a page for visual inspection, a browser screenshot API can return the image without you configuring a browser runner. [ScreenshotNeo](https://screenshotneo.com) is a website screenshot API and MCP server from Yorker Media. One GET request returns a PNG, JPEG, WebP, or PDF; its [API documentation](https://screenshotneo.com/docs/) covers the available options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie banners are accepted and removed before capture, along with known newsletter popups and chat widgets; each cleanup step can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Response headers report the page verdict and billing status.
- An MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs.
- The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for 1,000 free screenshots a month, with no card required.
10. Short FAQ
Should I use Playwright or Puppeteer debugging tools for a Puppeteer script?
Use Puppeteer’s own debugging workflow for a Puppeteer script. Playwright Inspector, UI Mode, and Playwright Test traces apply to Playwright Test runs; their commands and artifacts do not transfer directly.
Can a screenshot prove why a test failed?
Usually not on its own. It records one visible state, while a trace, logs, snapshots, and network evidence can show the sequence and conditions leading to it.
Should I always run tests headed when debugging?
No. Headed execution helps you observe interactions, but it can alter timing and does not necessarily reproduce headless CI behavior. Capture evidence in the environment where the failure occurs.
What is the first thing to do when only one browser project fails?
Reproduce that project in isolation, then compare its locator state, browser behavior, network activity, and configuration against a passing project.


