Browser Mode
Run your Jest tests in a real browser using Playwright, with access to native browser APIs like window, document, and the full DOM โ no simulations.
Why Browser Mode?โ
The Simulation Caveatโ
Testing JavaScript programs in simulated environments such as jsdom or happy-dom has simplified test setup and provided an easy-to-use API. However, these tools only simulate a browser environment โ not an actual browser โ which can result in discrepancies between test and production behavior. False positives and negatives occur.
To achieve the highest confidence, it's crucial to test in a real browser. Browser Mode runs tests natively in Chromium, Firefox, or WebKit so you can trust that your tests reflect real user behavior.
What Real Browsers Give Youโ
- Event handling: Disabled buttons don't fire click events in real browsers โ
jsdomfires them anyway - CSS: Layout, computed styles, and media queries work correctly
- Web APIs:
IntersectionObserver,ResizeObserver,fetch,WebSocket,Canvas, Web Animations, and more work natively - Security: CORS, CSP, and other security policies are enforced
- Rendering: Font rendering, viewport behavior, and responsive layouts are real
Drawbacksโ
- Longer initialization: Browser Mode spins up Vite and a browser process, adding startup time compared to in-process environments
- ESM only: Test files are served as ES modules via Vite โ CommonJS
require()is not available - Early development: Some features (module mocking, coverage) are not yet supported
Installationโ
Install Playwright as a peer dependency:
- npm
- Yarn
- pnpm
- Bun
npm install --save-dev playwright
yarn add --dev playwright
pnpm add --save-dev playwright
bun add --dev playwright
Install browser binaries:
npx playwright install chromium
To test in Firefox or WebKit, install those as well:
npx playwright install firefox webkit
Quick Startโ
Activate browser mode by adding the browserMode field to your Jest configuration:
- JavaScript
- TypeScript
const {defineConfig} = require('jest');
module.exports = defineConfig({
browserMode: {
enabled: true,
provider: 'playwright',
name: 'chromium',
headless: true,
},
});
import {defineConfig} from 'jest';
export default defineConfig({
browserMode: {
enabled: true,
provider: 'playwright',
name: 'chromium',
headless: true,
},
});
For full configuration options, see Browser Mode Configuration.
Writing Testsโ
Tests use explicit imports from @jest/globals:
import {describe, expect, it} from '@jest/globals';
describe('DOM operations', () => {
it('creates and queries elements', () => {
const div = document.createElement('div');
div.textContent = 'Hello Browser';
div.dataset.testid = 'greeting';
document.body.append(div);
const found = document.querySelector('[data-testid="greeting"]');
expect(found.textContent).toBe('Hello Browser');
});
it('handles events correctly', () => {
const button = document.createElement('button');
let clicked = false;
button.addEventListener('click', () => {
clicked = true;
});
button.click();
expect(clicked).toBe(true);
});
});
Run tests:
npx jest
Fake Timersโ
jest.useFakeTimers() works in browser mode, patching setTimeout, setInterval, clearTimeout, clearInterval, and Date.now:
import {expect, it, jest} from '@jest/globals';
it('advances timers', () => {
jest.useFakeTimers();
let called = false;
setTimeout(() => {
called = true;
}, 1000);
jest.advanceTimersByTime(1000);
expect(called).toBe(true);
jest.useRealTimers();
});
Lifecycle Hooksโ
beforeAll, afterAll, beforeEach, and afterEach work as expected with proper scope inheritance in nested describe blocks.
Parametric Testsโ
it.each and describe.each are supported:
import {expect, it} from '@jest/globals';
it.each([
[1, 1, 2],
[2, 3, 5],
[10, 20, 30],
])('adds %d + %d = %d', (a, b, expected) => {
expect(a + b).toBe(expected);
});
How It Worksโ
Jest (Node.js)
โโโ Vite dev server (serves tests + deps as ESM)
โโโ WebSocket server (birpc โ bidirectional RPC)
โโโ Playwright (launches real browser)
โ
โผ
Browser
โโโ @jest/globals (virtual module via Vite plugin)
โ โโโ describe / it / test / expect
โ โโโ jest.fn() / jest.useFakeTimers()
โ โโโ page / userEvent
โ โโโ toMatchScreenshot
โโโ Test files (native ESM via Vite)
โโโ Results โ birpc โ Jest โ terminal output
- Jest detects
browserMode.enabledand activates the browser runner - Vite starts as a dev server, serving test files and dependencies as native ES modules
- Playwright launches the specified browser and navigates to a Jest entry page
- The entry page loads
@jest/globalsas a Vite virtual module containing the test framework, assertion library, and a birpc client - The browser connects back to Node via WebSocket
- Node instructs the browser to import and run each test file
- Results flow back through birpc to Jest's standard reporting pipeline
Supported Browsersโ
| Browser | Engine | Provider |
|---|---|---|
| Chromium | Blink/V8 | Playwright |
| Firefox | Gecko | Playwright |
| WebKit | WebKit/JSC | Playwright |
Limitationsโ
No jest.mock() Module Mocking (Yet)โ
Module mocking via jest.mock() is not yet supported in browser mode. This requires AST-level transforms to hoist mock calls and intercept module requests โ planned for a future release.
jest.fn() and jest.spyOn() work normally since they operate on runtime objects.
No Runner Options (Yet)โ
Unlike Node environment runner-level options (testTimeout, retry, sequence, maxConcurrency, bail, custom runner class), Jest browser mode does not yet support configuring these per-runner. Test timeout and retry behavior use Jest's global configuration but are not individually tunable for browser tests.
No Code Coverage (Yet)โ
Code coverage collection is not yet supported in browser mode.
Thread-Blocking Dialogsโ
alert(), confirm(), and prompt() block the browser thread and cannot be used. Mock them if your code calls these APIs:
window.alert = jest.fn();
window.confirm = jest.fn(() => true);
ESM Onlyโ
Test files are served as ES modules via Vite. Use import/export syntax. CommonJS require() is not available in the browser.
Spying on Module Exportsโ
Browser mode uses native ESM โ module namespace objects are sealed and can't be reconfigured. You cannot call jest.spyOn on an imported module's exports directly:
import * as utils from './utils.js';
jest.spyOn(utils, 'helper'); // โ throws โ module namespace is sealed
Instead, spy on object methods or use dependency injection patterns.
Limitationsโ
jest.mock() Hoisting Not Supportedโ
In Node-based Jest, jest.mock() calls are automatically hoisted to the top of the file by babel-jest. In browser mode, test files are served as native ES modules via Vite and this hoisting transform is not yet implemented.
This means:
jest.mock('./module')will not work as expected โ imports resolve beforejest.mock()executes- Manual mocks (
__mocks__/directory) are not supported
Workaround: Use dependency injection, pass dependencies as function arguments, or mock at the object/method level:
// โ
Works โ mock methods on imported objects
import {api} from './api.js';
api.fetch = jest.fn();
// โ
Works โ dependency injection
function createService(fetcher = fetch) {
/* ... */
}
No Node.js APIs in Test Filesโ
Browser tests run in an actual browser context. Node.js built-in modules (fs, path, child_process, etc.) are not available. Use custom commands to perform server-side operations from your tests.
No Snapshot Testingโ
toMatchSnapshot() and toMatchInlineSnapshot() are not supported in browser mode. Use explicit assertions instead.
Thread/Worker Isolationโ
Each test file runs in an isolated iframe. There is no shared state between test files. Unlike Node-based Jest, --runInBand has no effect on browser tests โ they always run in parallel iframes managed by the orchestrator.
Limited jest.config Optionsโ
Many Jest configuration options are designed for Node.js and are ignored in browser mode, including:
transformโ Vite handles all transformsmoduleNameMapperโ use Viteresolve.aliasinsteadtestEnvironmentโ always browserglobalsโ use Vitedefineinstead