Playwright Headless Mode Failure Debug: Causes, Examples, and Troubleshooting Guide

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:

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.

HeadlessHeaded
Browser UI is hiddenBrowser window is visible
Common in CI/CDCommon for local debugging
Faster for automationEasier to observe
Lower visual overheadUseful for troubleshooting
Standard automated executionUseful 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:

  1. Different viewport dimensions
  2. Responsive UI changes
  3. Animation timing
  4. Element visibility
  5. Font rendering
  6. Browser dependencies
  7. Authentication state
  8. Network timing
  9. CI CPU/memory limitations
  10. 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:

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:

  1. Confirm the same Playwright version.
  2. Confirm browser installation.
  3. Check CI environment variables.
  4. Check API connectivity.
  5. Check authentication.
  6. Check viewport dimensions.
  7. Enable screenshots.
  8. Enable videos.
  9. Enable traces.
  10. 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

MistakeBetter Solution
Assuming headless is brokenCompare environments
Debugging only with headed modeCapture CI evidence
Using arbitrary sleepsUse assertions
Ignoring viewportConfigure it explicitly
Ignoring fontsUse consistent environments
Ignoring failed requestsAdd network logging
Ignoring authenticationValidate login state
Running too many CI workersReduce parallelism
Updating visual baselines blindlyReview 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.

Leave a Comment

Your email address will not be published. Required fields are marked *