Q29 of 37 · Selenium
How do you debug a Selenium test that fails only in headless mode?
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-endevents or useelement.is(:not(.animating))patterns. - GPU / WebGL — required for some 3D / canvas content.
--use-gl=swiftshaderis 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-streamif 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.