Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Jest does not automatically open a real browser. Its documented default environment is node. Select jsdom when your tests need browser-like DOM APIs, but remember that jsdom emulates the DOM and does not render pixels or calculate layout. For navigation, JavaScript execution in an actual browser, browser-specific behavior, or visual checks, connect Jest to Puppeteer (or use a browser-oriented workflow such as Playwright).

The practical decision is simple: test application logic in jsdom, and reserve real-browser suites for behavior that emulation cannot represent. The sections below show both setups, their boundaries, CI considerations, and a browser-free screenshot option.

What “headless” means in a Jest test

“Headless” describes a browser running without a visible window. Jest itself is a test runner and assertion framework; it is not a browser. A Jest process can run in Node, in jsdom, or alongside a separately launched browser controlled by Puppeteer.

Approach What executes Use it for Main limitation
Jest + node JavaScript in Node Pure functions, services, and server-side modules No browser globals such as window or document
Jest + jsdom Application code against an emulated DOM Component logic, DOM events, accessibility-oriented queries, and integration checks No visual rendering or layout engine
Jest + Puppeteer Tests connected to a real browser page Navigation, browser APIs, rendering-dependent behavior, and end-to-end flows Browser startup and lifecycle are heavier; page-evaluated code has a coverage caveat
Playwright browser workflow Tests in browser automation Projects that want Playwright’s browser tooling The material here establishes the headless-shell installation option, not a universal speed or stability ranking

Jest’s current environment documentation (version 30.5) identifies node as the default and jsdom as the browser-like alternative. Each test suite receives its own environment instance, with setup and teardown performed for that suite.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose the smallest environment that answers the question

Use jsdom for DOM and application logic

Choose jsdom when the question is, “Does this code manipulate the DOM correctly?” Examples include rendering a component, dispatching a click, validating form state, or checking that a client-side router changes the document. These tests normally start quickly because no browser binary is launched.

Use a real browser for browser behavior

Use Puppeteer or another browser automation tool when the result depends on actual navigation, layout, browser-only APIs, script loading, security behavior, or interactions that an emulated DOM cannot reproduce. A passing jsdom test is not evidence that Chrome, Firefox, or another browser will paint the same page.

Use a screenshot service when you need repeatable captures

For a generated image or PDF rather than an in-process assertion, ScreenshotNeo is the first service to try: it removes common consent UI before capture, bills only clean shots, and has the lowest paid entry plan.

Configure Jest with jsdom

Install the test dependencies

npm install --save-dev jest jest-environment-jsdom

Keep the Jest version and the environment package compatible with your project. The environment package is explicit in modern Jest installations, so declaring it avoids a missing-environment error.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Set jsdom for the whole project

Create jest.config.js:

/** @type {import('jest').Config} */
module.exports = {
  testEnvironment: 'jsdom',
  testEnvironmentOptions: {
    url: 'https://app.example.test/account',
    userAgent: 'jest-jsdom-test'
  }
};

The configured URL becomes window.location and affects how relative URLs resolve. A user-agent option is useful when application code branches on that value. Add only options your tests actually require; a realistic URL is usually more valuable than a production-looking user agent.

Select jsdom for one file

Leave the project default unchanged and put this docblock before imports in a specific test file:

/**
 * @jest-environment jsdom
 */

test('updates the greeting', () => {
  document.body.innerHTML = '<button id="hello">Hello</button>';
  const button = document.querySelector('#hello');
  expect(button).not.toBeNull();
  button.textContent = 'Clicked';
  expect(button.textContent).toBe('Clicked');
});

The docblock must be at the top of the file so Jest can select the environment before evaluating the test module. A file that does not request jsdom uses the project default.

Write a DOM-level test

function mountCounter(root) {
  let count = 0;
  root.innerHTML = '<button data-action="increment">0</button>';
  const button = root.querySelector('[data-action="increment"]');
  button.addEventListener('click', () => {
    count += 1;
    button.textContent = String(count);
  });
}

test('increments the counter', () => {
  document.body.innerHTML = '<main id="root"></main>';
  mountCounter(document.querySelector('#root'));
  document.querySelector('button').click();
  expect(document.querySelector('button').textContent).toBe('1');
});

This verifies event wiring and state transitions without claiming anything about pixels, fonts, or responsive layout.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Know what jsdom cannot prove

jsdom is an emulation environment. Its project documentation states that it does not render visual content or implement layout. Consequently, assertions about computed geometry, line wrapping, paint order, real font metrics, screenshots, and CSS breakpoints are not browser tests. A property such as offsetWidth may remain zero or otherwise fail to represent what a user sees.

Jest and jsdom can expose a pretendToBeVisual option. It changes visibility hints and enables animation-frame APIs such as requestAnimationFrame; it does not turn jsdom into a rendering browser. Use it only when code needs those timing APIs, not as a substitute for visual testing.

Run a real browser while keeping Jest

Use the documented Puppeteer preset

The Jest Puppeteer guide documents a preset that handles browser setup for you. Install the packages, then configure the preset:

npm install --save-dev jest jest-puppeteer puppeteer
// jest.config.js
module.exports = {
  preset: 'jest-puppeteer',
  testTimeout: 30000
};

A test can use the page supplied by the preset:

test('loads the home page in a browser', async () => {
  await page.goto('https://example.com', { waitUntil: 'networkidle0' });
  await expect(page.title()).resolves.toMatch(/Example/i);
  await expect(page.$eval('h1', element => element.textContent)).resolves.toContain('Example');
});

Choose an explicit wait condition that matches the application. Waiting for network idle can be inappropriate for pages with long-lived analytics or WebSocket connections; in that case wait for a selector that proves the UI is ready.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use custom global setup, environment, and teardown

The same guide describes a custom arrangement when the preset does not fit: global setup launches a browser, a custom test environment connects each suite to that browser, and global teardown closes it. Keep the browser process separate from individual tests so suites do not repeatedly launch it, and create or reset a page per test when isolation matters.

// global-setup.js
const puppeteer = require('puppeteer');

module.exports = async globalConfig => {
  const browser = await puppeteer.launch({ headless: true });
  globalConfig.__BROWSER__ = browser;
};

In a production project, pass the browser handle through the environment mechanism supported by your Jest version, expose a page to tests, and close the browser in global teardown. The exact environment hooks are version-sensitive; verify them against the Jest and Puppeteer versions installed in your project rather than copying an older example unchanged.

Account for the coverage boundary

The Jest integration guide warns that coverage is not generated for functions executed outside Jest through page.$eval, page.$$eval, or page.evaluate. Keep important business logic in modules imported by the Jest process and use page evaluation for browser-only glue. If a coverage threshold fails unexpectedly, inspect whether the code ran in the page context instead of inside Jest.

Where Playwright fits

Playwright is a separate browser-automation workflow rather than a jsdom environment. Its browser documentation describes installing a headless shell for CI when a full headed browser is unnecessary. That option can reduce the browser package footprint for CI jobs that only run headlessly. The available material does not establish that Playwright is universally faster, more stable, or broader in browser coverage than a Puppeteer-plus-Jest setup, so choose based on your project’s existing tooling and required browsers.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Make headless suites reliable in CI

Control readiness explicitly

  • Wait for a selector representing usable UI instead of sleeping for an arbitrary number of milliseconds.
  • Use a deterministic test URL and seed data before navigation.
  • Disable animations or shorten them in test CSS when they create timing races.
  • Capture the URL, console output, and a screenshot or HTML artifact when a browser test fails.

Isolate state

Reset cookies, local storage, and server-side fixtures between tests. A shared page is faster to write but can leak authentication or DOM state; a fresh page per test is easier to reason about. Keep Jest’s own fake timers away from code that depends on browser navigation timers unless you deliberately coordinate them.

Manage parallelism and resources

Jest may run files in parallel. A single browser process can serve several pages, but concurrent tests must not mutate the same account or database records. Cap workers in constrained CI runners, and always close pages and browsers in teardown so a failed test cannot leave orphaned processes.

Separate functional and visual evidence

Use Jest assertions for state and behavior. Run screenshot or PDF capture as a separate step when you need a visual artifact; pixel output is sensitive to fonts, viewport, device scale, and network resources, so make those inputs explicit.

Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF output, while the service accepts the page as a visitor first: cookie and consent banners are handled and more than 60 known consent platforms, newsletter popups, and chat widgets can be removed. Each step can be disabled when you need the original page state.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Read the parameter reference in the ScreenshotNeo documentation. A minimal cURL capture is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The equivalent Python request is:

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
const fs = require('node:fs');
fs.writeFileSync('shot.webp', Buffer.from(await res.arrayBuffer()));

Responses identify whether a capture was clean, cached, failed, blank, or blocked through the X-Page-Verdict and X-Billed headers. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing; only clean shots are billed.

Rank #4
The Web Testing Handbook
  • Used Book in Good Condition

For test pipelines, relevant options include full-page capture with lazy images loaded; one-element capture by CSS selector; dark mode; 12 device presets or a custom viewport; retina scale; PDF paper size, margins, landscape mode, and page ranges; HTML/CSS-to-image; custom CSS and JavaScript; pre-capture clicks; hidden selectors; waits for a selector, delay, or network idle; blocking ads, trackers, requests, or resource types; custom headers, cookies, user agents, Authorization, timezone, and geolocation; transparent backgrounds; image resizing; a chosen cache TTL; signed public-image links; asynchronous jobs with signed webhooks; bulk capture of up to 100 URLs per call; a usage API; and an OpenAPI specification. Parameter names used by other screenshot APIs also work, which can simplify migration.

An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients, so an AI agent can request captures without you building browser lifecycle code.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Plan Included shots per month Price
Free 1,000 $0, no card
Starter 3,000 $5
Growth 15,000 $15
Pro 60,000 $39
Scale 250,000 $99
Business 1,000,000 $249

Every feature is available on every plan, and yearly billing gives two months free. Start with 1,000 free screenshots a month with no card; paid plans start at $5 for 3,000 shots.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting common failures

“document is not defined”

Cause: The suite is running in Jest’s default node environment. Fix: Set testEnvironment: 'jsdom' or add the @jest-environment jsdom docblock, and ensure jest-environment-jsdom is installed.

Layout assertions always return zero or nonsense

Cause: jsdom has no layout engine. Fix: move that assertion to a real-browser suite and control viewport, fonts, and device scale there.

Relative links point to the wrong host

Cause: jsdom’s default URL is not your application URL. Fix: set testEnvironmentOptions.url to the origin and path your code expects.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Puppeteer cannot launch in CI

Cause: missing browser dependencies, an incompatible executable, or sandbox restrictions. Fix: install the browser dependencies required by your runner, use the supported headless mode for that Puppeteer version, and inspect the launch error before adding flags that weaken isolation.

The browser test hangs

Cause: a page waits forever for a network-idle condition, selector, or unresolved promise. Fix: set a finite Jest timeout, wait for a concrete readiness signal, and log pending navigation or console errors.

Coverage is missing for code that clearly ran

Cause: the code ran inside page.evaluate, page.$eval, or page.$$eval. Fix: test the underlying module in Jest as well, and treat the browser suite as an integration check for the page boundary.

The screenshot is a consent dialog or chat window

Cause: the capture happened before cleanup or used a tool without consent handling. Fix: configure waits and hide selectors yourself, or use ScreenshotNeo’s pre-capture consent handling and popup/chat removal. Check X-Page-Verdict and X-Billed to distinguish a clean result from a failed or non-billable one.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently asked questions

Can jsdom detect CSP, TLS, or service-worker defects?

No. Those depend on browser and network behavior. Keep jsdom tests for logic and use a real browser and controlled deployment environment for such defects.

Should visual screenshot checks replace Jest assertions?

No. A screenshot can reveal visual regressions, while Jest assertions explain state and behavior. Use each for the evidence it can actually provide.

Does a headless browser make a test deterministic automatically?

No. Network timing, data, fonts, animations, and shared state can still vary. Determinism comes from controlled inputs, explicit readiness checks, and cleanup.

Frequently Asked Questions

Can jsdom detect CSP, TLS, or service-worker defects?

No. Those depend on browser and network behavior, so test them in a real browser and controlled deployment environment.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Should visual screenshot checks replace Jest assertions?

No. Screenshots expose visual changes; Jest assertions provide precise state and behavior checks.

Does a headless browser make a test deterministic automatically?

No. Control data, network timing, fonts, animations, readiness checks, and cleanup.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.