ScreenshotNeo

BlogEngineering

Visual Regression Testing with Jest

Learn how to compare rendered UI screenshots in Jest, Playwright, and CI, with baselines, diffs, troubleshooting, and ScreenshotNeo.

By the ScreenshotNeo team1 October 20267 min read

Direct answer: Jest’s built-in snapshot testing does not take screenshots. It serializes values such as component output and compares the resulting text. Visual regression testing compares browser-rendered pixels. To do it with Jest, render the UI, capture an image, and use an image matcher such as jest-image-snapshot. For end-to-end browser tests, Playwright’s toHaveScreenshot is another direct option. Chromatic can add hosted visual review to Playwright tests.

The workflow is the same whichever tool you choose: capture representative states, compare each capture with a reviewed baseline, inspect the diff when a check fails, and update the baseline only after confirming that the visual change is intentional.

Jest snapshots versus visual regression

Test type What it compares What it can detect
Jest toMatchSnapshot() Serialized text or JavaScript values Changed component output, props, and generated markup
Image matcher in Jest A rendered image against a stored image Pixel-level changes after you render the component or page
Playwright toHaveScreenshot() Page or element screenshots Browser-rendered layout, styles, fonts, and state
Chromatic for Playwright Captured UI states in a hosted comparison workflow Visual changes reviewed in the cloud

Jest’s documentation describes serialized snapshot testing separately from screenshot-based visual regression. A passing text snapshot therefore does not prove that the rendered page still looks correct. See the Jest snapshot testing documentation.

Approach 1: image comparisons in Jest

This approach keeps assertions in Jest. You supply the rendering step, then pass the resulting image buffer to toMatchImageSnapshot(). The jest-image-snapshot README documents Jest versions 20 through 29 as its peer dependency range. Check the installed version in your project before adopting it.

1. Install the matcher and a renderer

npm install --save-dev jest jest-image-snapshot
npm install --save-dev @testing-library/react @testing-library/jest-dom

Your renderer can be React Testing Library, a server-side renderer, or a browser automation helper. A component-only renderer is useful for isolated states; a real browser is needed when fonts, layout engines, CSS, animations, or browser APIs affect the pixels.

2. Register the matcher

// test/setup.js
const { toMatchImageSnapshot } = require('jest-image-snapshot');

expect.extend({ toMatchImageSnapshot });
// jest.config.js
module.exports = {
  setupFilesAfterEnv: ['<rootDir>/test/setup.js'],
  testEnvironment: 'jsdom'
};

3. Capture and compare an image

The exact capture API depends on your renderer. The following example assumes a helper that returns a PNG buffer for a deterministic component state.

// visual/Button.visual.test.js
const { renderButtonToPng } = require('./renderButtonToPng');

test('primary button matches its visual baseline', async () => {
  const image = await renderButtonToPng({
    label: 'Checkout',
    variant: 'primary',
    disabled: false
  });

  expect(image).toMatchImageSnapshot({
    customSnapshotIdentifier: 'button-primary-enabled',
    failureThreshold: 0.001,
    failureThresholdType: 'percent'
  });
});

Run the test once to create the baseline, then run it again during development:

npx jest visual/Button.visual.test.js

Commit the generated baseline files with the test. When a comparison fails, inspect the received image and diff image produced by the matcher. Update the baseline only after reviewing the intended change.

Useful matcher controls

  • customSnapshotIdentifier gives a stable name when test titles or file paths may change.
  • failureThreshold and failureThresholdType let you define a tolerated pixel difference. Keep thresholds narrow enough to catch real regressions.
  • Use a fixed viewport, font set, color scheme, locale, timezone, and test data. Otherwise the image can change for reasons unrelated to your code.
  • Disable animations and transitions, freeze clocks, and wait for images and asynchronous content before capturing.
  • Keep volatile content such as timestamps, random IDs, rotating ads, and live counters out of the capture or replace it with deterministic fixtures.

Approach 2: browser screenshots with Playwright

When the pixels that matter are produced by a real browser, Playwright’s test runner provides toHaveScreenshot assertions for pages and elements. See the Playwright screenshot assertions documentation.

Install and configure

npm init playwright@latest
// tests/home.visual.spec.js
import { test, expect } from '@playwright/test';

test('home page matches the desktop baseline', async ({ page }) => {
  await page.goto('http://localhost:3000/');
  await page.evaluate(() => {
    document.querySelectorAll('*').forEach((el) => {
      el.style.animation = 'none';
      el.style.transition = 'none';
    });
  });
  await expect(page).toHaveScreenshot('home-desktop.png', {
    fullPage: true,
    animations: 'disabled',
    caret: 'hide'
  });
});

test('checkout summary matches its element baseline', async ({ page }) => {
  await page.goto('http://localhost:3000/checkout');
  await page.getByRole('button', { name: 'Show summary' }).click();
  await expect(page.locator('[data-testid="checkout-summary"]'))
    .toHaveScreenshot('checkout-summary.png');
});

Create or update screenshots with the Playwright update flag:

npx playwright test
npx playwright test --update-snapshots

Use separate projects or snapshot paths for Chromium, Firefox, and WebKit if cross-browser rendering matters. A baseline generated on one operating system and browser version should not automatically be treated as identical everywhere.

Choosing the right rendering boundary

  • Component image test: fast and focused; useful for buttons, cards, dialogs, and states that can be rendered deterministically.
  • Page screenshot: validates routing, layout, fonts, responsive behavior, and integration between components.
  • Element screenshot: limits noise when only one region matters, such as a chart or checkout panel.
  • Hosted review: Chromatic documents a Playwright integration that captures UI states and performs visual comparisons in its cloud environment. Review its current setup documentation before adopting it.

Designing reliable baselines

  1. List the states users can see: empty, loading, error, populated, disabled, focused, hovered, dark mode, and responsive breakpoints.
  2. Give each state stable fixtures. Seed database data and mock network responses where live data would move.
  3. Wait for the state you intend to check. A screenshot taken before fonts, images, or client rendering finish creates a misleading baseline.
  4. Control viewport dimensions, device scale, browser version, operating system, locale, timezone, and color scheme.
  5. Review diffs as code changes. Record why a baseline changed in the pull request.
  6. Delete obsolete baselines when routes, components, or states are removed.

Common failures and fixes

Symptom Likely cause Fix
Text snapshot passes but layout is broken Only serialized output was tested Add an image assertion with a browser or image renderer.
Every pixel changes between runs Animations, fonts, viewport, or data are unstable Disable motion, wait for fonts, fix the viewport, and use deterministic fixtures.
Screenshot is blank Capture happened before navigation or rendering completed Wait for the target selector or a stable page state before capturing.
Small anti-aliasing differences fail the test Different browser, OS, GPU, or font rasterization Run in a pinned environment and set a narrowly justified threshold.
Baseline files are missing in CI They were not committed or the snapshot path differs Commit baselines and verify the same project, path, and browser configuration locally and in CI.
Image matcher will not install Jest version is outside the package’s documented peer range Check the installed Jest version against the matcher README, then upgrade, downgrade, or choose Playwright.
Diff is caused by a timestamp or ad Volatile content is included Mock it, hide it, or assert a stable element instead.
Updating snapshots hides a real regression Baselines were updated without review Inspect the diff first and require a human-reviewed change in the pull request.

Performance, reliability, and cost

Image tests are usually cheapest when they render a small component and reuse a controlled environment. Full-page browser captures provide broader coverage but take longer because they start a browser, load resources, and wait for application state. Keep a focused visual suite on every change and run larger cross-browser matrices on the schedule that matches your risk.

Cache dependencies and browser binaries in CI, avoid unnecessary duplicate states, and shard independent browser tests when your CI system supports it. Reliability comes from deterministic inputs more than from a larger pixel tolerance. A high tolerance can hide a real regression; a low tolerance in an uncontrolled environment can create noise.

The direct cost of a self-managed Jest or Playwright setup is your CI compute, artifact storage, and maintenance time. Hosted review can reduce infrastructure work but adds a service dependency and its own plan considerations. Evaluate those trade-offs using current vendor documentation rather than assuming a fixed price or limit.

Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers.

For a page-level visual check, call the API from your CI job and compare the returned file with your reviewed baseline. The ScreenshotNeo documentation lists the available options, including full-page capture, CSS selectors, dark mode, device presets, custom viewports, retina scale, waits, request blocking, headers, cookies, JavaScript, caching, signed links, asynchronous jobs, bulk capture, and usage reporting.

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}`);

An MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf. 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

FAQ

Does toMatchSnapshot() compare screenshots?

No. It compares serialized values. Use an image matcher or a browser screenshot assertion for pixels.

Should I use Jest or Playwright?

Use Jest with an image matcher when you want Jest-centered component tests. Use Playwright when browser behavior and page rendering are part of the requirement.

How often should visual baselines be updated?

Only when the visual change is intentional and the diff has been reviewed. Treat baseline updates as part of the code change.

Can one baseline work on every operating system?

Do not assume that. Fonts, browsers, graphics stacks, and rasterization can differ. Pin the environment or maintain environment-specific baselines.

Can visual tests replace functional tests?

No. Visual assertions verify appearance. Keep interaction, accessibility, unit, and integration tests for behavior and semantics.