ScreenshotNeo

BlogHow-to

Puppeteer Screenshot Testing with Jest and Image Snapshots

Compare Puppeteer screenshots with Jest and jest-image-snapshot. Set up stable tests, review image diffs, and manage baselines safely.

By the ScreenshotNeo team4 October 20266 min read

Use Puppeteer to render a page and capture its screenshot, Jest to run the test, and jest-image-snapshot to compare the screenshot buffer with a stored image. The first run creates a baseline; later runs fail when the rendered page differs beyond your configured tolerance. Review each diff before updating a baseline.

Jest’s ordinary snapshots serialize values as text. Screenshot-based visual regression tests compare rendered images, so they catch layout, color, typography, and other visual changes that a serialized object snapshot cannot show. The two approaches cover different needs and can be used together. See Jest’s snapshot testing documentation.

1. Install and configure the image matcher

The jest-image-snapshot README documents Jest peer dependencies from version 20 through 29. Check the package metadata and your lockfile before adopting it, especially if your project uses a newer Jest release.

npm install --save-dev jest puppeteer jest-image-snapshot

Register the matcher once in a Jest setup file:

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

expect.extend({ toMatchImageSnapshot });

For example, configure Jest to load that file and use Node’s supported test environment:

// jest.config.js
module.exports = {
  testEnvironment: 'node',
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  testMatch: ['<rootDir>/**/*.visual.test.js'],
};

If your project already has a Jest setup file, add the matcher registration there instead. Keep browser startup and server startup in the test or your existing test infrastructure so failures do not leave processes running.

2. Write a runnable Puppeteer visual test

This example expects your application to be available at http://127.0.0.1:3000 before Jest starts. It fixes the viewport, waits for a meaningful page condition, disables motion, captures the page, and closes the browser even when an assertion fails.

// home.visual.test.js
const puppeteer = require('puppeteer');

describe('home page visual appearance', () => {
  let browser;

  beforeAll(async () => {
    browser = await puppeteer.launch({ headless: true });
  });

  afterAll(async () => {
    if (browser) await browser.close();
  });

  it('matches the reviewed desktop appearance', async () => {
    const page = await browser.newPage();
    try {
      await page.setViewport({ width: 1365, height: 900, deviceScaleFactor: 1 });
      await page.goto('http://127.0.0.1:3000', { waitUntil: 'domcontentloaded' });
      await page.waitForSelector('main');

      // Stabilize motion before capturing. Keep application-specific setup
      // (fixtures, login, locale, and data) deterministic as well.
      await page.addStyleTag({
        content: '*, *::before, *::after { animation: none !important; transition: none !important; caret-color: transparent !important; }',
      });
      await page.evaluate(() => document.fonts.ready);

      const image = await page.screenshot({ fullPage: true });
      expect(image).toMatchImageSnapshot({
        customSnapshotIdentifier: 'home-desktop',
      });
    } finally {
      await page.close();
    }
  });
});

Run the test with your project’s Jest command, for example:

npx jest home.visual.test.js

The example assumes the app server and required test data are already available. In a real project, use your existing server lifecycle hooks or CI job to start the app, wait until it is ready, and stop it afterward. Set an explicit page readiness condition that matches the content under test. A fixed sleep may conceal a slow or failed page and makes the test slower on every run.

3. Create and review image baselines

On the first successful run, the matcher writes an expected image under __image_snapshots__ by default. Commit that baseline with the test. Subsequent runs compare the newly captured buffer with the committed image and produce diagnostics for differences.

  1. Run the test once in the intended environment to create the baseline.
  2. Inspect the baseline image and confirm it represents the expected page state.
  3. Commit the test and baseline together so local runs and CI compare against the same reference.
  4. When a test fails, inspect the expected image, received image, and diff output before deciding what changed.
  5. Update only the affected baseline after confirming the new appearance is intentional.

Jest recommends committing snapshots alongside the code and tests they cover. Do not accept an image update just to make CI pass: a baseline can record a bug as easily as a correct design change.

4. Choose what the screenshot covers

Full page or viewport

page.screenshot({ fullPage: true }) captures the full document, including content below the fold. A default screenshot captures the visible viewport. Use full-page captures for long-page layout and viewport captures when the tested state is defined by a particular screen size. Full-page images can be large and can expose more dynamic content, so they may require more stabilization.

One element

Capture a component when the test is about a focused visual unit and the rest of the page adds noise:

const card = await page.waitForSelector('[data-testid="pricing-card"]');
const image = await card.screenshot();
expect(image).toMatchImageSnapshot({ customSnapshotIdentifier: 'pricing-card' });

Prefer stable test IDs or selectors over fragile positional selectors. Make sure the element is visible and fully rendered before capture.

Remove or control dynamic content

Dates, rotating promotions, user-specific details, ads, and remote content can cause irrelevant diffs. Prefer fixed fixtures and deterministic responses. If a region is outside the test’s purpose, remove or mask it before capture; do not mask content whose behavior the test is intended to protect.

await page.evaluate(() => {
  document.querySelectorAll('[data-visual-test-noise]').forEach((element) => element.remove());
});

5. Configure comparison sensitivity

The matcher documents pixel-by-pixel comparison with pixelmatch as its default and supports SSIM as an alternative. It exposes both per-pixel sensitivity and an overall failure threshold. Its README lists defaults of 0.01 for the pixel threshold and 0 for the allowed differing-pixel proportion; these are library defaults, not universal recommendations.

Setting or choice What it controls How to decide
Pixel threshold How different a pixel’s color can be before it counts as changed. Adjust only after inspecting noise patterns in representative pages.
Failure threshold How much of the image may differ before the assertion fails. Keep tight for high-signal regions; broader tolerance can hide real changes.
Comparison method Pixel comparison or structural similarity (SSIM). Choose based on the changes you need to detect and review actual output.
Snapshot identifier and directory Names and stores baselines. Use predictable identifiers and a stable, version-controlled location.
Diff diagnostics How expected, received, and difference artifacts are produced. Retain enough output for reviewers and CI debugging.

Configuration option names and supported values can vary by package version. Consult the jest-image-snapshot README for the version in your lockfile. Do not raise thresholds simply until tests turn green: a permissive setting can miss a genuine regression.

6. Make browser rendering repeatable

  • Viewport and scale: Set width, height, and device scale factor explicitly. Capture the same region each run.
  • Fonts: Wait for document.fonts.ready when the page uses web fonts; ensure the environment can load the same font files.
  • Data and identity: Use fixed fixtures, stable accounts, and a known locale and timezone. Avoid production data that changes between runs.
  • Animations: Disable or finish animations and transitions where they are irrelevant. Avoid globally disabling motion if motion itself is under test.
  • Network: Stub volatile third-party requests or serve deterministic fixtures when those responses are not part of the test.
  • Operating environment: Keep browser and OS rendering consistent between developer machines and CI. A container can help provide parity; the Think Company example uses Docker, but it is an implementation choice rather than a requirement.
  • Readiness: Wait for a selector or application-specific ready signal. networkidle can be unsuitable for pages with long polling or persistent connections.

7. Run locally and in CI

Keep the browser version, dependencies, fonts, viewport, and test data stable between local and CI runs. Install dependencies from the lockfile. Start the app with a predictable configuration and wait for its ready endpoint before invoking Jest. In CI, preserve failure artifacts such as the received image and diff so a reviewer can diagnose a failure without reproducing it immediately.

Separate a rendering failure from an application startup failure. If navigation times out or the readiness selector never appears, report that as a page setup/readiness problem rather than updating an image baseline. Close each page and browser in cleanup hooks, and give the test runner a timeout suited to browser startup and the page load.

8. Troubleshooting

Symptom Likely cause Fix
Matcher is undefined The setup file did not load, or expect.extend was omitted. Check setupFilesAfterEnv, the setup path, and the matcher registration.
Install reports a Jest peer dependency conflict The selected Jest version is outside the README’s stated >=20 and <=29 range. Check package metadata and lockfile compatibility; select a compatible combination or verify an updated package release before upgrading.
Browser fails to launch in CI Browser installation, OS libraries, or runner configuration differs from local setup. Use the project’s documented Puppeteer installation approach and align the CI image and dependencies with the local environment.
Test times out waiting for navigation or a selector The server is not ready, the route failed, or the readiness condition is wrong. Start the app before Jest, verify the URL and response, and wait for a condition that actually indicates the page is ready.
Diffs appear on every run Uncontrolled data, fonts, viewport, animation, timezone, or network responses vary. Stabilize those inputs and compare in a consistent environment before relaxing thresholds.
Screenshot is blank or incomplete Capture happened before content rendered, or the page navigated to an error state. Assert the expected route and content, await fonts and app readiness, and inspect the received image.
Baseline update hides a regression Snapshots were updated without reviewing the visual change. Revert the update, inspect expected/received/diff artifacts, and update only after the intended change is confirmed.
Images differ across machines Browser, operating system, fonts, or device scale differs. Standardize those inputs; consider a consistent container for local and CI rendering.

9. Performance, reliability, and cost

Browser startup and page rendering usually dominate the work, so reuse a browser process across tests where your test architecture safely permits it, while using isolated pages and deterministic state. Avoid repeatedly capturing enormous full-page images when a component capture answers the test question. Keep readiness waits specific so a slow third-party request does not stall every test.

Reliability comes from controlling the inputs and reviewing diffs, not from choosing a permissive threshold. Visual tests can be noisy when the browser environment or page data changes, and threshold tuning trades sensitivity for fewer false alarms. The core tools listed here are software dependencies; actual CI cost depends on your browser runtime, test volume, and infrastructure, so no universal cost figure applies.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One GET request captures a URL, while the Puppeteer/Jest workflow above is useful when you need to assert against a version-controlled visual baseline inside your test suite.

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

See the ScreenshotNeo API documentation for request options. Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed, and response headers report the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.

Sign up free for 1,000 screenshots a month, with no card required.

FAQ

Can I use Jest’s regular snapshots and image snapshots in one project?

Yes. Use text snapshots for serialized values and image snapshots for rendered appearance; they cover different kinds of change.

Should every visual test capture the entire page?

No. Capture the viewport for screen-specific states, the full page for document-level layout, or one element for a focused component.

What threshold should I use?

There is no universal threshold. Start with the matcher defaults, inspect representative diffs, then tune for your pages while checking that meaningful changes still fail.

Can I run this with Jest 30?

The cited README states a peer range through Jest 29. Verify package metadata and compatibility for your chosen release rather than assuming support.

Sources