How to Run Tests in Headless Mode with Chrome
Run Chrome tests without a visible window using the CLI, Puppeteer, or Selenium, with CI tips, capture flags, troubleshooting, and ScreenshotNeo.
Chrome headless mode runs the browser without opening a visible window. Add --headless when starting Chrome directly, set headless: true in Puppeteer, or add the --headless Chrome argument in Selenium.
Chrome’s unified Headless mode uses the same browser implementation as regular Chrome. It is the right default for end-to-end tests, visual checks, and extension testing. If you need the smaller legacy runtime, install the separate chrome-headless-shell binary; do not use the obsolete --headless=old switch.
1. Start Chrome directly in headless mode
The basic Linux command is:
google-chrome --headless
Binary names differ by operating system. Chrome’s documented forms include:
# Linux
chrome --headless https://example.com
# macOS
open -a "Google Chrome" --args --headless https://example.com
# Windows (Command Prompt)
start chrome --headless https://example.com
For a test runner, the command normally starts Chrome with a remote debugging endpoint or is launched by Puppeteer or Selenium. The direct CLI is especially useful for smoke checks and artifact capture.
Capture the DOM, a screenshot, or a PDF
# Serialize the DOM after Chrome parses the page and runs scripts
chrome --headless --dump-dom https://example.com
# Capture a viewport screenshot
chrome --headless \
--screenshot \
--window-size=412,892 \
https://example.com
# Create a PDF
chrome --headless \
--print-to-pdf=output.pdf \
https://example.com
--dump-dom is not the same as downloading raw HTML: Chrome parses the document and executes page scripts before serializing it. --screenshot writes screenshot.png in the current directory unless you provide an output path. --window-size=WIDTH,HEIGHT controls the viewport.
Control waiting and virtual time
chrome --headless \
--timeout=10000 \
--virtual-time-budget=5000 \
--screenshot \
--window-size=1440,900 \
https://example.com
--timeout=MSlimits how long Chrome waits before proceeding with DOM, screenshot, or PDF capture.--virtual-time-budget=MSadvances timer-driven page code, which can make animated or time-dependent captures more repeatable.- These flags do not replace application-specific synchronization. A test should still wait for the selector, network request, or state that proves the page is ready.
For chrome:// URLs, add --allow-chrome-scheme-url (available from Chrome 123):
chrome --headless --allow-chrome-scheme-url \
--dump-dom chrome://gpu
2. Run headless tests with Puppeteer
Puppeteer launches unified Headless mode by default when you pass headless: true. The following test navigates to a page, checks its title, and saves a screenshot.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
const title = await page.title();
if (title !== 'Example Domain') {
throw new Error(`Unexpected title: ${title}`);
}
await page.screenshot({ path: 'example.png', fullPage: true });
console.log('pass');
} finally {
await browser.close();
}
Use headless: false while debugging locally so you can see the browser. Use headless: 'shell' only when you intentionally want the separate Headless Shell behavior and do not need all Chrome features.
Wait for the application, not just navigation
await page.goto('https://app.example.com/dashboard', {
waitUntil: 'domcontentloaded'
});
await page.waitForSelector('[data-testid="dashboard-ready"]', {
visible: true,
timeout: 15000
});
const heading = await page.locator('h1').innerText();
if (heading !== 'Dashboard') throw new Error('Dashboard did not render');
Choose a readiness condition that represents the user-visible state. networkidle2 can be unsuitable for applications that keep analytics, WebSocket, or polling connections open.
Test an extension in unified Headless
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({
headless: 'new',
args: [
'--disable-extensions-except=/absolute/path/to/extension',
'--load-extension=/absolute/path/to/extension'
]
});
try {
const page = await browser.newPage();
await page.goto('https://example.com');
// Assert extension behavior here.
} finally {
await browser.close();
}
Chrome’s extension guidance uses the new unified mode. The old Headless implementation did not support loading extensions.
3. Run headless tests with Selenium
Selenium adds the same Chrome command-line argument through its browser options. This JavaScript example uses the WebDriver bindings:
import { Builder } from 'selenium-webdriver';
import chrome from 'selenium-webdriver/chrome.js';
const options = new chrome.Options();
options.addArguments('--headless');
options.addArguments('--window-size=1440,900');
const driver = await new Builder()
.forBrowser('chrome')
.setChromeOptions(options)
.build();
try {
await driver.get('https://example.com');
const title = await driver.getTitle();
if (title !== 'Example Domain') {
throw new Error(`Unexpected title: ${title}`);
}
} finally {
await driver.quit();
}
Option-builder names vary by Selenium language binding. The important operation is equivalent to options.addArguments('--headless'). Keep Chrome, ChromeDriver, and the Selenium binding compatible according to the current Selenium documentation.
Python Selenium example
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_argument('--headless')
options.add_argument('--window-size=1440,900')
driver = webdriver.Chrome(options=options)
try:
driver.get('https://example.com')
assert driver.title == 'Example Domain'
finally:
driver.quit()
4. Choose unified Headless or Headless Shell
| Mode | Use it when | Important detail |
|---|---|---|
| Unified Headless | End-to-end tests, visual fidelity, browser extensions, and features that must match normal Chrome | --headless, --headless=new, or the framework’s normal headless setting |
| Headless Shell | Screenshotting or scraping where a lighter runtime is more important than full Chrome behavior | Install and invoke the separate chrome-headless-shell binary |
Since Chrome 112, the new mode has shared the regular Chrome codebase. Since Chrome 132, --headless=old reports an error; it no longer selects a built-in legacy mode.
5. Make headless tests reliable in CI
- Pin the browser environment. Record the Chrome version and use a CI image whose browser and driver are compatible with your automation library.
- Set an explicit viewport. Responsive breakpoints, screenshots, and layout assertions depend on viewport width and height.
- Wait for a meaningful state. Prefer a stable selector, completed request, or application-ready marker over a fixed sleep.
- Save artifacts on failure. Capture a screenshot, serialized DOM, browser console output, and test logs when an assertion fails.
- Close every browser. Put
browser.close(),driver.quit(), or the equivalent in afinallyblock. - Isolate test data. Use independent accounts, storage directories, and test records when tests run concurrently.
Chrome also supports virtual headless screens. For multi-display scenarios, configure screen information with the DevTools Protocol, including Emulation.addScreen; Puppeteer exposes the corresponding capabilities.
6. Useful Chrome flags for capture tests
| Flag | Purpose | Typical use |
|---|---|---|
--headless |
Run without a visible browser window | All unattended test runs |
--dump-dom |
Print the post-script serialized DOM | Debugging rendered markup |
--screenshot |
Write a PNG screenshot | Visual regression and failure artifacts |
--window-size=WIDTH,HEIGHT |
Set viewport dimensions | Responsive layout checks |
--print-to-pdf |
Write a PDF | Document rendering checks |
--no-pdf-header-footer |
Remove PDF headers and footers | Clean PDF comparison |
--timeout=MS |
Cap capture waiting time | Prevent a stuck page from hanging a capture |
--virtual-time-budget=MS |
Advance timer-driven page code | Repeatable animation or clock-dependent output |
--allow-chrome-scheme-url |
Permit chrome:// navigation |
Inspect Chrome-internal pages |
Older Chrome versions may use --print-to-pdf-no-header instead of --no-pdf-header-footer. Verify the flag against the Chrome version installed in your runner.
7. Troubleshooting headless Chrome
Chrome says the flag is unknown or refuses to start
Cause: An outdated Chrome binary, an incorrect executable name, or the removed --headless=old value.
Fix: Check the binary with chrome --version, use --headless or --headless=new, and install chrome-headless-shell separately if you need the shell.
The test passes locally but fails in CI
Cause: Different Chrome versions, fonts, viewport dimensions, timezone, locale, network access, or application data.
Fix: Pin the CI image, set viewport and locale explicitly, log the browser version, and save a failure screenshot and DOM.
The screenshot is blank or captured before content appears
Cause: The script waited only for navigation while the application rendered asynchronously.
Fix: Wait for a page-specific ready selector or completed API response. Use --timeout as an upper bound, not as the readiness condition.
networkidle never completes
Cause: Analytics, polling, WebSockets, or service workers keep network activity open.
Fix: Wait for a deterministic selector or application event instead of network idleness.
The layout differs from visible Chrome
Cause: A different viewport, device scale factor, font set, media preference, or legacy Headless implementation.
Fix: Use unified Headless, configure the viewport and scale factor explicitly, and install the same fonts used by the reference environment.
Selenium cannot create a session
Cause: ChromeDriver and Chrome are incompatible, or the runner cannot find the executable.
Fix: Confirm both versions, configure the Chrome binary path when necessary, and use the current Selenium driver-management guidance.
An extension does not load
Cause: The old Headless implementation does not support extension loading.
Fix: Run unified Headless with --headless=new and provide absolute extension paths.
8. Performance, reliability, and cost considerations
- Reuse a browser process when safe. Creating one browser per assertion adds startup overhead. Reuse the browser and create isolated pages or contexts, while keeping tests independent.
- Limit parallelism to the runner. Too many simultaneous pages compete for CPU, memory, file descriptors, and network bandwidth. Increase workers gradually and watch for timeouts.
- Block unnecessary resources in test fixtures. Images, third-party analytics, and ads can make tests slower, but only block them when the behavior under test does not depend on them.
- Use deterministic data. Stable API fixtures and seeded records reduce retries and make screenshots comparable.
- Retry selectively. A retry can hide a real race. Record the first failure and retry only known transient infrastructure errors.
- Budget capture work. Full-page screenshots and PDFs require more layout and memory work than a viewport screenshot. Capture them only where the test needs them.
9. Or skip the browser setup
If your goal is dependable website screenshots rather than browser test assertions, ScreenshotNeo provides a single HTTP request. See the ScreenshotNeo API documentation for the full option set.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Before capture, ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server includes take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 screenshots.
Create a free ScreenshotNeo account and get 1,000 screenshots each month with no card.
10. FAQ
What is the shortest way to enable Chrome Headless?
Use chrome --headless https://example.com from the command line, or the equivalent framework setting.
Should new projects use --headless=new or --headless?
Both select unified Headless in current Chrome. --headless is the simplest default; --headless=new makes the mode explicit in extension-focused setups.
Does Headless change the page’s JavaScript behavior?
Unified Headless uses the real Chrome implementation, but the environment can still differ through viewport, fonts, permissions, display configuration, and timing. Control those inputs when tests compare output.
Can I use the Chrome CLI for full interaction tests?
The CLI is suited to DOM, screenshot, and PDF capture. Use Puppeteer or Selenium for clicks, typing, assertions, network interception, and multi-step workflows.
When should I use Headless Shell?
Use the separate shell binary when its reduced feature set is sufficient and a lighter runtime suits the workload. Use unified Headless for browser-fidelity and extension tests.


