Q29 of 37 · Selenium

How do you debug a Selenium test that fails only in headless mode?

SeleniumSeniorseleniumheadlessdebuggingcisenior

Short answer

Short answer: Reproduce locally with `--headless=new`, take screenshots and HTML dumps at each step, capture browser console + network logs, and compare a headed run side-by-side. The mismatches are usually viewport size, missing fonts, or animations that headed UI hides.

Detail

Tests passing headed but failing headless is one of the classic Selenium debugging headaches. The investigation has a few reliable angles.

1. Reproduce locally — same OS, same Chrome version, same flags as CI. --headless=new (the new headless mode in Chrome 109+) behaves much closer to headed than the old --headless did, and many "headless-only failures" disappear by switching to it.

2. Capture state at every step — not just at failure:

@AfterMethod
public void capture(ITestResult r) {
    if (r.getStatus() == ITestResult.FAILURE) {
        captureScreenshot(r.getName());
        savePageSource(r.getName());
        dumpBrowserLogs(r.getName());
    }
}

Page source + console logs catch JS errors that don't surface visually.

3. Common causes, ranked by frequency:

  • Viewport size: headless defaults can be 800×600 — far smaller than the developer's monitor. Sticky headers cover elements, mobile media queries trigger. Set --window-size=1920,1080.
  • Fonts missing — text-based locators that depend on rendering (rare, but happens with custom font fallbacks).
  • Animations — headed users wait through transitions; headless can race past. Wait for transition-end events or use element.is(:not(.animating)) patterns.
  • GPU / WebGL — required for some 3D / canvas content. --use-gl=swiftshader is the workaround.
  • Time zone / locale — CI containers run UTC; date-dependent UI breaks if the dev was on local time.
  • Notifications / clipboard / camera permissions — disabled differently in headless. --use-fake-ui-for-media-stream if tests touch media.

4. Side-by-side comparison: take the same screenshots in both modes and diff them. The visual difference often points straight at the cause — a modal cut off, a button in a different place, a viewport-dependent breakpoint.

5. Fall back to video — Chromium DevTools' Page.captureScreenshot in a polling loop, or a recorder like ffmpeg against Xvfb, gives you a movie of the headless run. Often makes the cause obvious in seconds.

The senior signal: structured debugging (reproduce, capture, compare) rather than guessing — and naming the most common causes on first ask.

// WHAT INTERVIEWERS LOOK FOR

A debugging process (reproduce, capture state, side-by-side compare), naming 3+ common causes (viewport, animations, fonts), and awareness that --headless=new matters.

// COMMON PITFALL

Adding random Thread.sleep until the test passes — it sometimes 'fixes' the symptom but leaves the suite slow and the underlying flake unsolved. Diagnose the actual cause.