ScreenshotNeo

BlogHow-to

How to Use Puppeteer Screenshots for Visual Regression Testing in CI

Capture pages with Puppeteer, compare them with reviewed baselines, and make CI screenshot failures reproducible and useful.

By the ScreenshotNeo team4 October 202610 min read

Short answer: Puppeteer captures the page; a separate comparison tool checks that screenshot against an approved baseline. In CI, use a predictable browser environment, wait for a meaningful page state, save screenshots and diffs when comparisons fail, and update baselines only after review. Puppeteer does not provide baseline management or a visual regression assertion by itself.

This guide uses Node.js, Puppeteer, Jest, and jest-image-snapshot as one concrete comparison setup. The capture and comparison are separate steps, so you can replace Jest or the matcher without changing the basic Puppeteer workflow. See the Puppeteer screenshot guide and the matcher’s current documentation for version-specific details.

1. Understand the workflow

A visual regression test renders a route or component, captures an image, then compares it to a baseline created from an approved UI state.

  1. Start the application with stable test data.
  2. Open the target route with Puppeteer under the same rendering conditions used to create the baseline.
  3. Wait for the UI state and assets the test needs.
  4. Capture the page or a selected element.
  5. Compare the image with a reviewed baseline using a distinct matcher.
  6. On mismatch, retain the actual image and diff as CI artifacts for review.
  7. Update the baseline only when the UI change is intended and reviewed.

Playwright Test has its own integrated screenshot assertions, but those are Playwright features. For Puppeteer, select and configure a separate comparator. The Playwright visual comparison guide is useful operational guidance on rendering consistency; it does not make Playwright assertions part of Puppeteer.

2. Install the tools

In an existing Node project, install Puppeteer, Jest, and the image matcher:

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

Puppeteer manages a compatible browser installation as part of its normal install. In restricted CI environments, ensure the install step is allowed to download the browser, or follow Puppeteer’s current configuration instructions for using an existing browser. Pin dependency versions with your lockfile so local and CI installs resolve the same versions.

Add a test script to package.json:

{
  "scripts": {
    "test:visual": "jest --runInBand"
  }
}

This example assumes the application is already available at http://127.0.0.1:3000 when the test runs. Start it with your project’s normal CI command before invoking Jest.

3. Capture and compare a page

Create visual.test.js. The example registers the matcher, creates one browser for the test file, fixes the viewport and device scale, waits for a page-specific ready marker, and captures a PNG buffer. Adapt the route, selector, and test data to your app.

const puppeteer = require('puppeteer');
const { toMatchImageSnapshot } = require('jest-image-snapshot');

expect.extend({ toMatchImageSnapshot });

let browser;

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

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

test('dashboard visual baseline', async () => {
  const page = await browser.newPage();
  await page.setViewport({ width: 1365, height: 900, deviceScaleFactor: 1 });

  try {
    await page.goto('http://127.0.0.1:3000/dashboard', {
      waitUntil: 'domcontentloaded',
      timeout: 30000,
    });

    // Prefer an app-specific readiness signal over an arbitrary sleep.
    await page.waitForSelector('[data-testid="dashboard-ready"]', {
      visible: true,
      timeout: 15000,
    });

    // Fonts and images can affect layout and pixels. Wait for the assets
    // required by this page before taking the screenshot.
    await page.evaluate(async () => {
      if (document.fonts && document.fonts.ready) await document.fonts.ready;
      const images = Array.from(document.images);
      await Promise.all(images.map((image) => {
        if (image.complete) return Promise.resolve();
        return new Promise((resolve) => {
          image.addEventListener('load', resolve, { once: true });
          image.addEventListener('error', resolve, { once: true });
        });
      }));
    });

    const screenshot = await page.screenshot({
      type: 'png',
      fullPage: true,
      animations: 'disabled',
    });

    expect(screenshot).toMatchImageSnapshot({
      customSnapshotIdentifier: 'dashboard',
    });
  } finally {
    await page.close();
  }
}, 60000);

Run it locally with npm run test:visual. On the first approved run, the matcher creates a baseline. Later runs compare the captured image with that baseline. Inspect the matcher’s output and documentation to confirm baseline paths, update behavior, and supported threshold options for the version you install.

Choose readiness deliberately

The Puppeteer guide demonstrates navigation with waitUntil: 'networkidle2'. That is useful for pages whose network becomes quiet, but it is not a universal readiness condition. Analytics, polling, streaming, and long-lived requests can prevent network idle; a page can also reach network idle before a client-side render is complete. Prefer a visible app-specific marker that means the state under test is ready. Wait for fonts and relevant images if they affect the captured region.

4. Capture only the relevant area

Use a full-page capture when page composition is the requirement. Use an element capture when the test concerns a component or region; keep the baseline scope identical between runs. Puppeteer documents both Page.screenshot() and ElementHandle.screenshot(). Element screenshots scroll a hidden element into view by default.

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

Other useful capture decisions include:

  • fullPage: true captures the full document rather than only the viewport. Very long pages can produce large images and amplify unrelated changes far below the area of interest.
  • clip captures a specified rectangle when a precise region is needed. Keep its coordinates and dimensions stable.
  • type selects PNG, JPEG, or WebP where supported by the installed Puppeteer version. PNG is generally a straightforward choice for comparison because it avoids lossy compression differences.
  • omitBackground can make the page background transparent where the capture format and comparison pipeline support it.
  • animations: 'disabled' disables animations for the capture. Also consider hiding or stabilizing caret, cursor, video, and time-dependent content when those are not what the test is intended to assert.

Check the current ScreenshotOptions API for the installed Puppeteer version before relying on an option. The screenshot API returns image data; with base64 encoding the documented return type is a string, otherwise it can return a Uint8Array. Passing a buffer directly to the matcher avoids a file read in the test itself.

5. Keep rendering conditions stable in CI

Pixel comparisons are sensitive to more than application code. Playwright’s visual comparison documentation warns that output can vary with host OS, browser version, settings, hardware, power source, headless mode, and other factors. Apply the practical lesson to your Puppeteer setup: create and compare baselines in the same controlled environment wherever possible.

  • Use the same CI image or operating system for baseline generation and normal CI runs.
  • Keep Puppeteer and its browser version fixed through the lockfile and build environment.
  • Set viewport width, height, and device scale explicitly.
  • Use deterministic fixtures, account state, locale, timezone, and test data.
  • Disable or freeze clocks, rotating content, random values, and animations when they are outside the assertion’s scope.
  • Make font availability consistent; missing or substituted fonts can change line breaks and layout.
  • Avoid concurrent tests that mutate shared data or use the same baseline identifier.

Do not promise identical rendering across unrelated machines. If you intentionally compare multiple operating systems or browser versions, maintain appropriately separated baselines and review them as distinct environments.

6. Set comparison tolerance and baseline policy

Choose the comparator’s tolerance according to its own documented semantics. jest-image-snapshot is one separate Jest matcher; it supports comparison configuration described in its repository. Its options and defaults can change, so check the installed version’s documentation. Do not copy Playwright Test’s maxDiffPixels setting into a Puppeteer matcher unless that matcher independently documents the same option.

A useful policy is to start with a strict comparison, inspect actual mismatch images, and adjust only when there is understood rendering noise. A loose tolerance can hide real regressions; an excessively strict threshold can fail on harmless rasterization differences. Compare the images at the same dimensions and ensure the matcher is using the intended algorithm and threshold.

Treat baseline updates as code changes. A reviewer should inspect the expected image change alongside the corresponding UI change. Playwright documents an explicit snapshot update flow for Playwright Test; a Puppeteer stack should use the equivalent controlled update workflow provided by its matcher and repository. Avoid automatically accepting every CI-generated mismatch.

7. Make failures useful in CI

Run the app, then invoke the visual test command in the same job. CI configuration differs by provider, so the provider-neutral requirements are:

  1. Install dependencies from the lockfile and install or make available Puppeteer’s browser.
  2. Start the app with deterministic test data and wait for its health or readiness endpoint.
  3. Run npm run test:visual and preserve the exit status.
  4. When a test fails, upload the actual screenshot, generated diff, and relevant test logs as CI artifacts using your provider’s artifact feature.
  5. Keep reference baselines under version control or in the chosen matcher’s managed baseline location so reviewers can see deliberate changes.

Do not discard the actual capture when a comparison fails. It is the fastest way to tell whether the cause is an intended design change, a timing issue, a missing asset, or a different rendering environment.

8. Common failures and fixes

Symptom Likely cause Fix
Baseline missing or first run fails No approved reference image exists yet, or the matcher expects a different baseline directory. Run the documented baseline-creation flow locally or in the controlled environment, inspect the image, and commit the approved baseline. Confirm matcher paths and update settings.
Passes locally but fails in CI Different OS, browser version, fonts, viewport, device scale, data, or headless rendering conditions. Align the environment and explicit page settings. Use the CI rendering environment for baseline generation when possible.
Navigation times out The app is unavailable, navigation is waiting for a network state that never occurs, or the timeout is too short for the environment. Confirm the server is running and URL is reachable. Use an appropriate navigation wait condition, then wait for a specific app-ready selector. Increase timeout only when normal CI startup requires it.
Ready selector times out The selector changed, the route did not load, the test is unauthenticated, or the element is not visible. Check route and test setup, verify selector in the rendered DOM, and use the state that actually indicates readiness.
Images or fonts are missing Capture happens before assets load, asset requests fail in CI, or fonts are absent from the CI image. Wait for relevant images and document.fonts.ready; inspect failed requests and make fonts available in the same environment.
Flaky diffs around timestamps or banners Dynamic text, animation, rotating content, cookie prompts, chat widgets, or personalized state changes between runs. Seed stable data, freeze or hide content outside the assertion, wait for the intended state, or capture a narrower element. Do not mask content that is itself under test.
Browser fails to launch in CI Browser installation or required system dependencies are unavailable, or sandbox/container settings differ. Follow the current Puppeteer installation and troubleshooting documentation for the CI image. Verify the browser executable and dependencies rather than silently skipping the test.
Every pixel differs Images have different dimensions, a font or browser changed, the wrong baseline was selected, or the page captured a different state. Compare dimensions and metadata first, then inspect environment, route, viewport, and test data before changing tolerance.

9. Performance, reliability, and cost

Visual tests consume time and compute because they launch a browser, load pages, render assets, and compare image data. Keep the suite focused on routes and components whose appearance matters; use element captures when a full-page assertion would include unrelated content. Reuse a browser process across tests in a file where isolation permits, but create and close pages reliably and avoid shared mutable state.

Parallelism can reduce wall-clock time, but it increases resource use and can introduce contention or test-data collisions. Start with stable serial execution, then increase concurrency while checking reliability and memory limits in your CI environment. Do not set arbitrary performance claims: capture time depends on the application, page weight, browser, and CI resources.

Self-hosting with Puppeteer has no per-screenshot API fee, but it uses CI minutes, storage for baselines and artifacts, and engineering time to maintain browser dependencies and test stability. A hosted screenshot API trades browser setup for a service request and plan cost; account for the API’s behavior and billing rules. Keep secrets out of screenshots and logs, and use controlled test accounts for authenticated routes.

Or skip the browser setup

For URL-to-image captures without installing and maintaining Puppeteer in your job, ScreenshotNeo is a screenshot API and MCP server from Yorker Media. One GET request returns a screenshot or PDF. Its API accepts parameters used by other screenshot APIs, which can make switching easier. See the ScreenshotNeo API documentation for request options.

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}`);
  • Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed. Response headers report the page verdict and billing status.
  • An MCP server gives AI agents tools for screenshots, page information, and PDF capture.
  • 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000 screenshots.

Sign up for ScreenshotNeo’s free plan to get 1,000 screenshots a month with no card.

FAQ

Does Puppeteer compare screenshots to a baseline?

No. Puppeteer captures image data. A separate matcher, test framework, or hosted service must manage baselines and decide whether images differ enough to fail.

Should I use a full-page screenshot for every test?

No. Use full-page capture when the whole route is the requirement. For a component or a specific visual region, an element screenshot usually keeps unrelated changes out of the comparison.

Can I use Playwright’s screenshot assertions with Puppeteer?

No. Those assertions belong to Playwright Test. Choose a comparison tool that works with the image data produced by Puppeteer.

When should a baseline change be accepted?

When the UI change is intentional, the proposed image has been inspected, and the baseline update is reviewed with the code change.