Browser Testing
What a browser test can prove that nothing below it can, the three engines that matter, headless versus headed, and the cost model that should shape your suite.
3 min read · updated 19 September 2026
A browser test is the only kind that exercises what the user actually receives: the shipped JavaScript bundle, the applied CSS, the real event loop, the real network stack.
Everything below it — component tests, DOM tests in jsdom — is testing a model of the browser. Usually a good model. Not the thing.
#What only a browser can tell you
- Layout. Whether the button is on screen, overlapped, or pushed below the fold at 320px.
- Applied CSS. jsdom parses stylesheets and applies almost nothing. White text on a white background is invisible to every test except this one.
- Real event sequences. Pointer capture, focus traps, scroll-linked
behaviour,
:focus-visible. - The actual bundle. Whether a tree-shaking change broke a dynamic import, whether a polyfill is missing on the engine you shipped to.
- Timing under real conditions. Layout shift, hydration mismatches, long tasks.
- Engine differences. Date parsing, regex features,
Intloutput — see cross-browser testing.
#The three engines
| Engine | Browsers | Notes |
|---|---|---|
| Chromium (Blink) | Chrome, Edge, Brave, Opera, Electron | Largest share; the default to develop against |
| Gecko | Firefox | Independent implementation; catches spec assumptions |
| WebKit | Safari on macOS and every browser on iOS | The one that bites |
WebKit matters disproportionately because iOS requires it: Chrome on an iPhone is WebKit with a Chrome interface. If any meaningful share of your traffic is iOS, WebKit is not optional.
#Headless
// Playwright: headless by default; headed for debugging.
await chromium.launch({ headless: true });
await chromium.launch({ headless: false, slowMo: 250 }); // watch it happenModern headless Chromium is the same binary in a different mode, so the old "works headed, fails headless" folklore is largely obsolete. What still differs:
- Fonts. A CI container has a different font set, which changes text metrics and therefore layout. This is the main reason visual baselines must be generated in the same container that compares them.
- GPU. Headless usually falls back to software rendering; canvas and WebGL output can differ subtly.
- Media. Codec availability varies by build.
#Viewports and devices
// Emulation: viewport, user agent, device pixel ratio, touch support.
const iPhone = devices['iPhone 15'];
const context = await browser.newContext({ ...iPhone });
// Or just the dimensions that matter to your layout
await page.setViewportSize({ width: 375, height: 812 });Emulation tests your layout at that size. It does not test the device's browser, its font stack, its input model or its memory ceiling. That distinction is where a lot of false confidence comes from — a responsive suite that passes on emulated Mobile Safari is not evidence that the site works on an iPhone.
#Network control
// Fail a dependency the real one will not fail on demand.
await page.route('**/api/rates/**', (route) => route.fulfill({ status: 503 }));
// Cut third-party weight so the suite is fast and deterministic.
await page.route(/analytics|doubleclick|hotjar/, (route) => route.abort());
// Emulate a slow connection (Chromium, via CDP).
const client = await page.context().newCDPSession(page);
await client.send('Network.emulateNetworkConditions', {
offline: false, latency: 400, downloadThroughput: 400_000, uploadThroughput: 200_000
});Blocking third-party scripts is worth doing by default. They are slow, they are outside your control, and they are a recurring source of flakiness that has nothing to do with your application.
#Console and page errors
Most suites ignore these, which wastes a free signal:
test.beforeEach(async ({ page }) => {
page.on('pageerror', (error) => { throw new Error(`Uncaught: ${error.message}`); });
page.on('console', (message) => {
if (message.type() === 'error') throw new Error(`console.error: ${message.text()}`);
});
});An uncaught exception that does not break the assertion still usually means something is broken. Failing on it costs nothing and catches a lot.
#The cost model
A browser test costs roughly a thousand times what a unit test costs, in both wall-clock and maintenance. That ratio should shape the suite:
- Journeys, not permutations. One checkout test, not one per payment method. See end-to-end testing.
- Set up through the API, act through the UI.
- One engine for everything, three for the critical few.
- Capture traces on retry so a CI failure is diagnosable without reproducing it.
#What to run here that is not a journey
Two things are worth a browser and are not user-journey tests:
Accessibility scans. axe-core in a browser catches contrast, missing
labels and ARIA misuse that no other level can see — see
accessibility testing.
Visual regression. A screenshot comparison catches the entire class of defect where the logic is right and the page looks wrong. See screenshots.
Common questions
- Is headless testing different from headed?
- Less than it used to be. Modern headless Chromium is the same binary in a different mode, so behaviour matches closely. Differences that remain tend to involve fonts, GPU-accelerated rendering, media playback and some dialog handling — which is why visual regression baselines should be generated in the same environment that will compare against them.
- How many browsers should my suite run against?
- Run everything on one engine and a small critical subset on the others. Running the full suite on three engines triples the cost and the flakiness for a small increase in signal, because most defects are not engine-specific.
- Do I need real devices?
- For the last mile, yes. Emulating a viewport tests your layout; it does not test the device's browser, its fonts, its input handling or its memory limits. A device cloud for a handful of journeys covers what emulation cannot.
Runnable samples for this page
last test results ↗- TypeScript (Playwright)
browser/tests/platforms/browser-testing
Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.
Was this page useful?
Related topics
- Cross-Browser TestingWhich browsers actually need testing in 2026, what still differs between engines, and a strategy that catches real defects without tripling your pipeline.
- PlaywrightThe default browser automation tool in 2026 — auto-waiting locators, tracing, sharding and fixtures — with the configuration that matters and the mistakes that still cause flakiness.
- Selenium WebDriverThe W3C standard for browser automation — where it still wins, the waiting model that decides whether a suite is stable, and Grid for scale.
- End-to-End TestingWhat belongs in an end-to-end suite and what does not, how many journeys are enough, and the practices that keep a browser suite from becoming the thing everyone ignores.
- ScreenshotsCapturing the screen at the moment of failure, and using screenshot comparison as visual regression testing — configuration, masking and the flakiness to design out.
- Accessibility TestingWhat axe and similar tools genuinely catch, the roughly two-thirds they cannot, and how accessible markup makes your test suite more maintainable at the same time.