Introduction: What Does a Playwright Headless Mode Failure Mean?
A playwright headless mode failure occurs when a Playwright test behaves differently or fails when the browser runs without a visible graphical window.
For example, a test may pass when you run:
npx playwright test –headed
but fail with:
npx playwright test
The default Playwright Test execution is headless.
This difference can be confusing for beginners because the application is the same, but the execution environment may not be identical.
Common reasons include:
- Different viewport assumptions
- Timing and animation differences
- Missing fonts
- Browser dependencies
- Responsive layouts
- Authentication problems
- Network failures
- Incorrect selectors
- CI resource constraints
- Browser launch configuration
A good playwright headless mode failure debug process does not simply switch permanently to headed mode. Instead, use headed mode to understand the failure, then make the headless test deterministic.
What Is Playwright Headless Mode?
Playwright Headless Mode runs a browser without displaying its graphical user interface.
For example:
npx playwright test
This is commonly used in:
- CI/CD pipelines
- Docker containers
- GitHub Actions
- Jenkins
- Azure DevOps
- Automated regression suites
You can explicitly configure headless execution:
import { defineConfig } from ‘@playwright/test’;
export default defineConfig({
use: {
headless: true
}
});
Headless execution is usually faster and requires fewer graphical resources.
Headless vs Headed Mode in Playwright
The main difference is whether the browser window is visible.
| Headless | Headed |
| Browser UI is hidden | Browser window is visible |
| Common in CI/CD | Common for local debugging |
| Faster for automation | Easier to observe |
| Lower visual overhead | Useful for troubleshooting |
| Standard automated execution | Useful with Inspector |
Run headed:
npx playwright test –headed
Run headless:
npx playwright test
You can also use:
npx playwright test –debug
for interactive debugging.
Important
Headed mode should generally be treated as a debugging tool, not the permanent solution to a headless failure.
Why Does Playwright Fail in Headless Mode?
A test can pass in headed mode but fail in headless mode because of environmental or synchronization differences.
Common causes include:
- Different viewport dimensions
- Responsive UI changes
- Animation timing
- Element visibility
- Font rendering
- Browser dependencies
- Authentication state
- Network timing
- CI CPU/memory limitations
- Incorrect assumptions about browser state
The first step in playwright headless mode failure debug is to determine whether the problem is genuinely headless-specific.
Browser, Viewport, Rendering, and Environment Differences
A fixed viewport makes tests more deterministic.
import { defineConfig } from ‘@playwright/test’;
export default defineConfig({
use: {
headless: true,
viewport: {
width: 1280,
height: 720
}
}
});
Without controlled dimensions, a responsive application may render different layouts.
For example:
Desktop:
[Dashboard] [Reports] [Settings]
Small viewport:
[Dashboard]
[Menu]
A locator that works against the desktop navigation may fail against the mobile menu.
Best Practice
Always define a viewport for visual or layout-sensitive tests.
Debugging Selectors That Fail Only in Headless Mode
Problem → Cause → Debugging Step → Fix → Best Practice
Problem
await page.locator(‘.submit-button’).click();
works headed but fails headless.
Cause
The selector may identify a hidden duplicate or a responsive version of the button.
Debugging Step
Inspect the number of matching elements:
const buttons = page.locator(‘.submit-button’);
console.log(
‘Matching buttons:’,
await buttons.count()
);
Check visibility:
console.log(
‘Visible:’,
await buttons.first().isVisible()
);
Fix
Use a semantic locator:
await page.getByRole(‘button’, {
name: ‘Submit’
}).click();
Best Practice
Prefer role, label, placeholder, and test-ID locators over fragile CSS selectors.
Capturing Screenshots During Headless Failures
Screenshots are one of the simplest ways to understand what the headless browser actually rendered.
import { test } from ‘@playwright/test’;
test(‘debug headless page’, async ({ page }) => {
await page.goto(‘https://example.com’);
await page.screenshot({
path: ‘headless-debug.png’,
fullPage: true
});
});
If the test fails in CI, configure failure screenshots:
import { defineConfig } from ‘@playwright/test’;
export default defineConfig({
use: {
screenshot: ‘only-on-failure’
}
});
Then inspect the captured image.
It can reveal:
- Unexpected login page
- Cookie popup
- Missing content
- Mobile layout
- Loading spinner
- Error page
- Modal covering an element
Recording Videos for Headless Debugging
Video can help when the screenshot alone does not show how the failure occurred.
Configure:
import { defineConfig } from ‘@playwright/test’;
export default defineConfig({
use: {
video: ‘retain-on-failure’
}
});
A failed test can then produce a video showing the sequence leading to the failure.
This is especially useful for:
- Animation problems
- Redirects
- Popups
- Unexpected navigation
- Loading states
Using Trace Viewer for Playwright Headless Mode Failure Debug
Tracing is one of the most powerful debugging tools.
Configure:
import { defineConfig } from ‘@playwright/test’;
export default defineConfig({
use: {
trace: ‘retain-on-failure’
}
});
Or run:
npx playwright test –trace=on
Then open the trace:
npx playwright show-trace trace.zip
Trace Viewer helps you inspect:
- Actions
- Screenshots
- DOM snapshots
- Network activity
- Timing
- Console output
When a test fails only in CI, traces can provide evidence that is difficult to obtain interactively.
Debugging Console and Network Failures
A headless browser may reveal application errors that are easy to miss locally.
Add console logging:
page.on(‘console’, message => {
console.log(
`[${message.type()}] ${message.text()}`
);
});
Monitor failed requests:
page.on(‘requestfailed’, request => {
console.log(
‘REQUEST FAILED:’,
request.url(),
request.failure()?.errorText
);
});
Monitor responses:
page.on(‘response’, response => {
if (response.status() >= 400) {
console.log(
‘HTTP ERROR:’,
response.status(),
response.url()
);
}
});
These logs can expose:
- API failures
- Authentication failures
- DNS issues
- Missing resources
- Server errors
- Blocked requests
Debugging Timing, Animation, and Visibility Issues
Problem
A button exists but cannot be clicked headlessly.
Cause
The element may still be moving, covered by an overlay, or waiting for an animation.
Debugging Step
const button = page.getByRole(‘button’, {
name: ‘Submit’
});
console.log(‘Visible:’, await button.isVisible());
console.log(‘Enabled:’, await button.isEnabled());
Fix
Wait for a meaningful state:
import { expect } from ‘@playwright/test’;
await expect(button).toBeVisible();
await expect(button).toBeEnabled();
await button.click();
Best Practice
Do not solve timing issues with:
await page.waitForTimeout(5000);
Use Playwright’s auto-waiting and assertions.
Handling Fonts, Browser Dependencies, and OS Differences
Headless CI failures are sometimes caused by environment differences.
Check:
- Playwright version
- Browser version
- Node.js version
- Installed fonts
- Linux browser dependencies
- OS
- Docker image
- Locale
- Timezone
A missing font can change text dimensions and cause:
- Different wrapping
- Different element positions
- Screenshot mismatches
- Click coordinate changes
For reliable tests, use a controlled environment such as the official Playwright Docker setup.
Debugging Network and Authentication Failures
Authentication failures can look like selector failures.
For example, your test expects:
await expect(
page.getByRole(‘heading’, {
name: ‘Dashboard’
})
).toBeVisible();
But the headless browser is actually on:
/login
Check the current URL:
console.log(‘Current URL:’, page.url());
You can also wait for a login API:
const loginResponse = page.waitForResponse(response =>
response.url().includes(‘/api/login’) &&
response.ok()
);
await page.getByRole(‘button’, {
name: ‘Login’
}).click();
await loginResponse;
Then validate the dashboard.
This is much more reliable than assuming the login completed.
Playwright Headless Failures in CI/CD and Docker
A common issue is:
Playwright headless test failing in CI but passing locally.
Use this troubleshooting sequence:
- Confirm the same Playwright version.
- Confirm browser installation.
- Check CI environment variables.
- Check API connectivity.
- Check authentication.
- Check viewport dimensions.
- Enable screenshots.
- Enable videos.
- Enable traces.
- Reduce parallel workers.
Try:
npx playwright test –workers=1
If the failure disappears, resource contention may be involved.
For Docker, ensure the container includes the browser dependencies required by Playwright.
Real-World Playwright Headless Debugging Examples
Example 1: Headless Button Failure
Problem: Button works headed but fails headless.
Cause: Responsive layout displays a different button.
Debugging Step:
console.log(‘Viewport:’, page.viewportSize());
console.log(
‘Button count:’,
await page.getByRole(‘button’, {
name: ‘Submit’
}).count()
);
Fix:
await page.getByRole(‘button’, {
name: ‘Submit’
}).click();
Configure a stable viewport.
Best Practice: Never assume headed and headless layouts are identical without verifying the viewport.
Example 2: Headless Test Fails in CI
Problem: Local headless test passes, CI fails.
Cause: API request fails in CI.
Debugging Step:
page.on(‘requestfailed’, request => {
console.log(
request.url(),
request.failure()?.errorText
);
});
Fix: Correct the CI environment, credentials, network access, or API configuration.
Best Practice: Treat infrastructure failures separately from application failures.
Example 3: Screenshot Looks Different
Problem: Headless screenshot differs from local screenshot.
Cause: Fonts or browser environment differ.
Debugging Step: Compare:
- Browser version
- OS
- Fonts
- Viewport
- Device scale factor
Fix: Generate and compare baselines in a consistent environment.
Best Practice: Keep visual regression environments reproducible.
Common Mistakes and Solutions
| Mistake | Better Solution |
| Assuming headless is broken | Compare environments |
| Debugging only with headed mode | Capture CI evidence |
| Using arbitrary sleeps | Use assertions |
| Ignoring viewport | Configure it explicitly |
| Ignoring fonts | Use consistent environments |
| Ignoring failed requests | Add network logging |
| Ignoring authentication | Validate login state |
| Running too many CI workers | Reduce parallelism |
| Updating visual baselines blindly | Review screenshot differences |
Playwright Headless Testing Best Practices
Use this checklist:
- Run tests headless in CI.
- Use headed mode for local debugging.
- Keep browser versions consistent.
- Define deterministic viewport sizes.
- Use stable locators.
- Rely on Playwright auto-waiting.
- Avoid arbitrary delays.
- Capture screenshots on failure.
- Retain videos for difficult failures.
- Enable traces in CI.
- Log console and network failures.
- Keep authentication deterministic.
- Use consistent Docker environments.
- Control fonts and browser dependencies.
- Separate application defects from environment defects.
The goal of playwright headless mode failure debug is not to make every test headed. The goal is to make headless execution reliable and reproducible.
Playwright Interview Questions With Answers
Why does Playwright fail in headless mode?
Common causes include viewport differences, timing, animations, fonts, browser dependencies, authentication, network failures, and CI environment differences.
What is the difference between headed and headless Playwright?
Headed mode displays the browser UI and is useful for debugging. Headless mode runs without a visible browser and is commonly used for automated CI/CD execution.
How do you debug a Playwright headless failure?
Use screenshots, videos, traces, console logs, network logging, Inspector, and controlled viewport/browser settings.
Can headed and headless tests behave differently?
Yes. Differences in viewport, rendering, timing, fonts, resources, and environment can affect behavior.
How do you debug a headless test that fails only in CI?
Collect screenshots, traces, videos, console logs, and network failures. Compare browser versions, environment variables, dependencies, viewport, and available resources.
Should you switch all CI tests to headed mode?
Usually no. Headed mode is primarily useful for investigation. CI should normally use headless execution.
FAQs
Why does Playwright fail in headless mode?
A test can fail because headless execution exposes timing, viewport, rendering, dependency, network, authentication, or CI environment differences.
How do I debug Playwright headless mode failures?
Run the same test headed locally, then enable screenshots, videos, traces, console logging, and network logging to determine what differs.
Why does Playwright headless browser not work in Docker?
The container may lack required browser dependencies, fonts, permissions, or compatible system libraries. Use a compatible Playwright Docker image and verify browser installation.
Why does a Playwright test pass headed but fail headless?
The two runs may have different rendering, timing, viewport, authentication, network, or environmental conditions.
Is Playwright headless mode faster?
Generally, headless execution avoids rendering a visible browser window and is well suited to automated CI/CD execution.
