ScreenshotNeo

BlogHow-to

How to Debug Websites in a Headless Browser

Debug headless browser failures with Playwright Inspector, traces, logs, network evidence, and a repeatable CI workflow.

By the ScreenshotNeo team1 October 20266 min read

Short answer: reproduce one failing action, then inspect the evidence around that moment: the locator and DOM snapshot, actionability log, browser and test console, network requests, and a screenshot or trace. Use Playwright Inspector for an interactive local failure, a headed run when you need to see rendering, and Trace Viewer when the failure happened in CI. Keep the original headless run as the final check.

Playwright runs browsers headless by default. The workflow below shows how to debug a page without guessing at timing or changing several variables at once.

1. Read the failure before changing the environment

  1. Record the assertion message, expected value, received value, call log, and source line.
  2. Choose one failing test and one action. A narrow reproduction makes page state and request sequences easier to correlate.
  3. Note the browser, viewport, URL, test data, and whether the failure is local or in CI.

2. Reproduce interactively with Playwright Inspector

Run one test in debug mode:

npx playwright test tests/checkout.spec.ts:24 --debug

The Inspector opens a headed browser and lets you step through actions, edit or pick locators, and view actionability logs. A locator that fails because an element is hidden, moving, covered, or not yet attached is different from a selector that never matches. See the Playwright debugging guide.

Make a normal launch visible

import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: false, slowMo: 150 });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.pause();
await browser.close();

headless: false changes rendering and interaction conditions. A visible run helps observation but does not prove that a headless or CI failure is fixed. Re-run with the original launch settings.

3. Capture a trace for failures you cannot watch live

Tracing preserves a time-ordered record for inspection after a run. It includes action details, source locations, DOM snapshots, errors, console messages, network requests, and screenshots when recorded. Playwright documents traces as useful for CI failures.

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

test('checkout', async ({ page }) => {
  await page.goto('https://example.com/checkout');
  await page.locator('[data-testid="pay"]').click();
  await expect(page.getByRole('heading', { name: 'Receipt' })).toBeVisible();
});
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({ use: { trace: 'retain-on-failure' } });
npx playwright show-trace path/to/trace.zip

In Trace Viewer, step through the action timeline. For the failed action, compare the DOM snapshot before and after it, read the action log, then check matching console and network events. A screenshot proves what was visible; the snapshot, console, and request details help explain why.

4. Correlate page, browser, and network evidence

Symptom Evidence Direction
Locator or click fails Actionability log, locator, DOM snapshot Selector drift, hidden or covered element, wrong frame, or timing
Page looks wrong Before/after snapshots and screenshots Layout state, viewport, fonts, flags, or rendering race
Data or assets are missing Requests, responses, console output Blocked request, authorization, CORS, or server error
Browser stalls or will not launch API and browser launch logs Executable, dependency, sandbox, or resource problem
Only CI fails Trace from the failing job Environment, timing, data, or network difference

Do not infer a root cause from one screenshot or timeout. Correlate the failed action with state and requests at the same timestamp.

5. Turn on verbose Playwright logs

DEBUG=pw:api npx playwright test tests/checkout.spec.ts:24

For an early browser launch problem, Playwright’s CI guidance documents:

DEBUG=pw:browser npx playwright test tests/checkout.spec.ts:24

Debug namespaces and commands can change with framework versions. Check the installed version’s official documentation before copying old launch flags.

6. Add targeted diagnostics to a test

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

test('diagnostic capture', async ({ page }, testInfo) => {
  page.on('console', msg => console.log(`[console:${msg.type()}] ${msg.text()}`));
  page.on('pageerror', error => console.error('[pageerror]', error));
  page.on('requestfailed', request => console.error('[requestfailed]', request.url(), request.failure()?.errorText));
  page.on('response', response => { if (response.status() >= 400) console.error('[http]', response.status(), response.url()); });
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
  await page.screenshot({ path: testInfo.outputPath('state.png'), fullPage: true });
});

Keep listeners focused on the failing test. Logging every successful request on a large page can obscure the event that matters.

7. Debug common headless-only differences

  • Viewport and device: set an explicit viewport and device scale factor. Responsive breakpoints can change which locator exists.
  • Fonts and rendering: wait for document.fonts.ready when text measurement affects layout and ensure CI has required fonts.
  • Animations: disable them for diagnosis, then verify with production timing restored.
  • Frames: inspect page.frames() and target the correct iframe.
  • Authentication: confirm identical storage state, cookies, headers, and flags locally and in CI.
  • Time and locale: set timezone and locale explicitly when date formatting affects assertions.
  • Network readiness: networkidle does not guarantee that application data is ready. Prefer a response assertion or meaningful selector.

8. A repeatable CI procedure

  1. Save the failed test’s trace, video or screenshot artifacts, console output, and framework logs.
  2. Open the trace from the failing job before reproducing locally.
  3. Find the first divergence: an unexpected request, console error, missing DOM node, or actionability failure.
  4. Reproduce with the same browser project, viewport, storage state, and test data.
  5. Make one change, run the focused test headless, then run the wider suite.
  6. Keep trace retention sized to your CI storage budget.

9. Performance, reliability, and cost considerations

  • Use a focused test and line filter while investigating.
  • Trace and video artifacts consume storage; retain them on failure unless every passing run is required.
  • Headed mode and slowMo are observation tools, not performance measurements.
  • Prefer deterministic waits tied to application state. Large fixed delays hide races and slow runs.
  • Record browser version and OS image with CI artifacts.
  • When a third-party dependency is flaky, capture URL, status, and timing before deciding whether a test-owned stub is appropriate.

10. Puppeteer users

Puppeteer has its own official debugging workflow, including headed launches and Node/browser debugging tools. Exact APIs depend on the installed version; follow the Puppeteer debugging guide. The evidence model remains the same: reproduce one action, inspect page state, correlate console and network output, and preserve CI artifacts.

Or skip the browser setup

If your goal is a clean image of a page while diagnosing visual state, ScreenshotNeo provides a single capture request. It accepts the consent banner like a visitor, removes more than 60 known consent platforms plus newsletter popups and chat widgets before the shot, and lets each step be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers report the page verdict and whether the shot was billed. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for AI clients.

See the ScreenshotNeo API docs for all options.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

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)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Every plan includes full-page or CSS-element capture, dark mode, device presets or custom viewport, retina scale, PDF controls, custom CSS and JavaScript, clicks and waits, blocking rules, headers, cookies, user agent, authorization, timezone, geolocation, transparent background, resizing, cache TTL, signed links, async webhooks, bulk capture, usage API, and OpenAPI support. Free includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

Troubleshooting checklist

Error or symptom Cause to check Fix
Timeout exceeded Wrong state, slow dependency, or broad wait Inspect trace and requests; wait for a meaningful selector or response.
Element is not visible Responsive layout, overlay, animation, or wrong frame Use Inspector picker and inspect the snapshot.
Selector matches zero nodes Selector drift or page not reached Check URL, DOM snapshot, and navigation response.
Requests fail in CI Credentials, proxy, DNS, CORS, or blocked third party Compare network and console evidence from CI with local output.
Browser executable missing Browser binaries or OS dependencies absent Install the framework-required version and inspect DEBUG=pw:browser.
Headed passes, headless fails Mode changed timing, viewport, GPU, or environment Use headed mode for observation, then verify under original headless conditions.

FAQ

Should I start with a screenshot or a trace?

Start with a trace when the failure involves an action, timing, or CI. Add a screenshot when visual state is the question.

Does networkidle mean the page is ready?

No. It describes observed network activity. Assert the application state your test needs.

Can a headed success prove a headless bug is fixed?

No. Re-run the same headless project and environment.

What should be retained from CI?

Keep the failing trace, relevant screenshot or video, console and framework logs, browser version, and test configuration.