When first setting up Playwright for test automation, you may encounter a frustrating scenario - your tests run smoothly in headful mode, but fail unexpectedly when run headlessly.
What gives? This article will explore the main culprits behind this headless/headful discrepancy, with actionable fixes to help your tests achieve consistency regardless of run mode.
1. Async Code Issues
JavaScript executed in headless mode handles promises and async code differently than headful. Code that seems to work fine when visible can expose race conditions or timing issues when run headlessly.
The fix: Audit use of
// Avoid this antipattern that makes tests flaky
await page.click('button');
await page.waitForNavigation();
// Better approach:
await page.click('button');
const [response] = await Promise.all([
page.waitForNavigation(),
page.waitForResponse()
]);
2. POPUP Windows Create Headaches
By default, Playwright suppresses new tab/window opens when running headlessly. What works fine for visible browser windows can therefore break in headless mode.
The fix: Override the default context setting for ignoring HTTPS errors to handle popups.
// Have context handle browser popups instead of suppressing
const context = await browser.newContext({
ignoreHTTPSErrors: true,
acceptDownloads: true
});
3. Page Visibility Differences
Certain DOM elements may have subtle differences in behavior based on whether they are currently visible on a loaded page.
The fix: Use more precise selectors and assertions that do not depend on visible state.
// Fragile check dependent on visibility
await expect(page.getByText('Submit').toBeVisible();
// More robust validation
const submitButton = await page.$('#submitButton');
await expect(submitButton).toBeDefined();
4. Environment-Specific Issues
Depending on your test environment, factors like mouse/keyboard input, screen size, or installed fonts could lead to inconsistencies between headful and headless execution.
The fix: Standardize environment conditions where possible using
await context.overridePermissions(
"https://example.com",
["clipboard-read"]
);
const browser = await chromium.launchPersistentContext(userDataDir, {
headless: false,
args: [
'--window-size=1280,720'
]
});
These tweaks help make your test logic and environment more resilient to the key differences between headless and headful modes.
Additional Strategies for Smoother Playwright Tests
Beyond addressing what causes headless vs headful discrepancies, here are some best practices that will serve you well in Playwright:
Key Takeaways: No More "Works in Headful, Fails Headless"
With Playwright offering the performance of headless execution plus the environment visibility of headful mode, you don't need to compromise on test reliability.
Focus on the fixes and strategies outlined here - like managing async code, handling popups properly, and standardizing environment conditions. This will help bulletproof your tests to run consistently and provide value, whether executed visibly or invisibly.
The end result? Faster and more robust browser automation without surprises between headful and headless test runs.