How to compare full-page screenshots in Puppeteer tests
Capture full pages with Puppeteer, compare them with reviewed image baselines, and keep visual regression tests stable in CI.
A full-page screenshot test has three parts: render the page in a controlled state, capture it with Puppeteer using fullPage: true, and compare the resulting image with a reviewed baseline. Puppeteer captures the image; a visual regression matcher such as jest-image-snapshot or a lower-level library such as pixelmatch compares it. A reported difference shows that the rendered image changed; review the baseline, current image, and diff before deciding whether the change is a defect or an intentional update.
1. Capture the full page with Puppeteer
The Puppeteer screenshots guide uses Page.screenshot() and demonstrates navigation with waitUntil: 'networkidle2'. The ScreenshotOptions reference defines fullPage as a boolean that defaults to false; set it to true to capture the full page. PNG is the default image type.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setViewport({
width: 1365,
height: 900,
deviceScaleFactor: 1,
});
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
Install Puppeteer in a Node project with npm install --save-dev puppeteer. This capture-only script writes page.png to the current directory. It does not compare the image with anything; comparison is a separate test step.
networkidle2 is a useful navigation wait, not a guarantee that the page is visually stable. Animations, clocks, randomized content, late application updates, and external feeds can still change what appears in the screenshot. Wait for the specific application state your test needs, such as a ready selector or an app-provided signal.
2. Compare screenshots with a reviewed baseline
For a Jest project, jest-image-snapshot supplies a matcher and baseline workflow. Install it with npm install --save-dev jest-image-snapshot. The package documents Jest versions 20 through 29 as peer dependencies, so check compatibility with your project’s Jest version before adopting it. Register the matcher once in a Jest setup file:
// test/setup.js
import { expect } from '@jest/globals';
import { toMatchImageSnapshot } from 'jest-image-snapshot';
expect.extend({ toMatchImageSnapshot });
Configure that file as a Jest setup file according to your Jest configuration. Then capture and compare in a test:
// test/page.visual.test.js
import puppeteer from 'puppeteer';
describe('example page visual appearance', () => {
let browser;
beforeAll(async () => {
browser = await puppeteer.launch();
});
afterAll(async () => {
await browser?.close();
});
test('matches the reviewed full-page baseline', async () => {
const page = await browser.newPage();
await page.setViewport({
width: 1365,
height: 900,
deviceScaleFactor: 1,
});
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.waitForSelector('main');
const image = await page.screenshot({ fullPage: true, type: 'png' });
expect(image).toMatchImageSnapshot({
customSnapshotIdentifier: 'example-page-full-page',
});
await page.close();
});
});
Run the test once to create its candidate baseline. Review the generated expected image and commit it only after deciding it represents the intended UI. When a later run fails, inspect the old image, received image, and generated diff. Update the baseline only after reviewing the visual change; do not treat a failing comparison as proof of a regression.
3. Choose and tune the comparison policy
jest-image-snapshot uses pixelmatch by default and also documents an SSIM comparison option. Pixel-level comparison is useful for detecting local color and edge changes. Structural similarity compares image structure differently and may be preferable when small rendering variation is expected. The package describes its SSIM support as experimental, so verify its current behavior before making it part of a critical test policy.
There are two distinct tolerance decisions: the per-pixel sensitivity and the overall amount of difference that fails the test. In jest-image-snapshot, these are exposed separately as customDiffConfig.threshold and failureThreshold with failureThresholdType. A larger tolerance can hide a small real defect; a stricter tolerance can flag harmless rendering noise. There is no universal threshold that suits every page.
expect(image).toMatchImageSnapshot({
comparisonMethod: 'pixelmatch',
customDiffConfig: { threshold: 0.01 },
failureThreshold: 0.2,
failureThresholdType: 'percent',
});
The values above illustrate how to configure the two settings; they are not a universal recommendation. Start with a policy appropriate to the visual risk, inspect actual diff artifacts, then tune in response to identified noise. The package also documents options such as customSnapshotIdentifier, diffDirection, onlyDiff, noColors, blur, and updatePassedSnapshot. Use identifiers when test names are insufficiently distinctive, diff layout options to make artifacts easier to inspect, and automatic update behavior only when its effect is understood by the team.
For a custom comparison pipeline, pixelmatch is a lower-level pixel comparison library. It accepts image data and can return a differing-pixel count and a diff image. You then own image decoding, dimension handling, threshold policy, and baseline storage. Choose a Jest matcher for an integrated baseline workflow; choose pixelmatch directly when the project needs to control those pieces itself.
4. Make full-page captures repeatable
- Fix the rendering environment: use the same browser version, viewport width and height, device scale factor, locale, timezone, and fonts in baseline and current runs when they affect output.
- Control page data: use stable fixtures and mock volatile responses such as timestamps, rotating offers, user-specific content, or third-party feeds.
- Wait for meaningful readiness: combine an appropriate navigation wait with a page-specific selector or application-ready condition. A network-idle event alone does not ensure all visual work is complete.
- Control motion: disable or freeze animations and transitions when they are not under test. Freeze clocks or seeded random inputs when they affect rendered content.
- Keep capture options consistent: use the same screenshot type,
fullPagevalue, viewport, background behavior, and clipping options for both baseline and current images. - Review and retain artifacts: preserve the baseline, received screenshot, and diff in local or CI output so failures can be diagnosed. The Jest matcher documents uploading diagnostic artifacts for ephemeral CI environments.
Full-page images can be very tall. Long pages may contain lazy-loaded images that only appear after scrolling; if those sections matter, ensure they have loaded before the capture and apply the same procedure to both baseline and current runs. Pages with infinite scrolling need an explicit stopping condition, because there may be no finite full-page height to capture reliably.
5. Handle failures and edge cases
| Symptom | Likely cause | Fix |
|---|---|---|
| Screenshot is only viewport-sized | fullPage was omitted or set to false. |
Set fullPage: true and keep that option consistent for baseline and current capture. |
| Test times out during navigation | The site keeps connections open or never reaches the selected wait condition. | Choose a navigation wait that fits the page, then wait for a specific readiness selector or app signal. Do not assume a more permissive wait means the page is visually ready. |
| Intermittent diffs in unchanged pages | Dynamic content, animations, fonts, viewport differences, or external resources vary between runs. | Stabilize the relevant inputs, use the same browser and viewport, wait for required fonts/content, and inspect the diff before increasing tolerance. |
| Entire image differs or dimensions mismatch | Layout dimensions, responsive breakpoint, content length, or device scale factor changed. | Compare viewport and scale settings, then determine whether the height or layout change is intentional. Do not mask a meaningful page-length change. |
| Jest says the matcher is undefined | The setup file did not execute or the matcher was not registered in the active Jest environment. | Confirm the setup file is configured and that it calls expect.extend before the test runs. |
| Baseline update produces a large change | A real redesign, changed test data, rendering environment drift, or accidental broad update. | Inspect baseline, received image, and diff; verify environment and data; update only the reviewed snapshots. |
| CI has no useful diff artifacts | The runner discards its filesystem after the job. | Configure artifact retention or upload failed-test images to durable storage, following your CI’s artifact mechanism. |
For Jest’s own serialized snapshots, use its snapshot review workflow; image baselines are a separate visual comparison mechanism. The Jest documentation distinguishes serialized value snapshots from visual regression tools that compare screenshots.
6. Performance, reliability, and cost
Screenshot time depends on page loading, rendering, image dimensions, and the comparison work. Full-page captures create larger images than viewport captures, and decoding and comparing those images consumes more memory and CPU. Keep the captured page and test set scoped to the visual risks you care about; use deterministic fixtures to avoid spending CI time rerunning noisy failures. No universal runtime or resource benchmark applies to all pages.
Reliability comes from controlling the page and environment, retaining diagnostic images, and requiring review before baseline changes. A screenshot test is evidence about the rendered state under its configured conditions; it does not establish behavior at other viewports or with other data. To cover responsive layouts, capture distinct viewport configurations with distinct baselines.
With Puppeteer, cost is the compute and CI time for running a browser and storing or transferring artifacts. There is no per-screenshot API charge for this local workflow. Consider artifact retention and parallelism as project-specific resource choices.
7. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns an image or PDF, so it can replace browser installation and capture code when you need a screenshot. It does not replace a baseline comparison library: use the returned image as an input to your chosen review or comparison workflow.
For a direct call, see the ScreenshotNeo API documentation:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
Cookie banners, newsletter popups, and chat widgets are removed before the shot, and each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers report the page verdict and billing status. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for AI agents. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Every feature is on every plan. Create a free ScreenshotNeo account to get started.
FAQ
Does Puppeteer compare screenshots by itself?
No. Puppeteer captures the image. A matcher or image-diff library compares it with a baseline.
Does full-page mean every page on a site?
No. It means the full document for the page currently open in that browser tab.
Should every visual difference fail the test?
That is a project policy decision. Choose a deliberate tolerance and inspect diffs so harmless rendering noise and meaningful changes are handled appropriately.
Can I use Jest’s ordinary snapshot matcher for screenshots?
Ordinary Jest snapshots serialize values. Use an image matcher or image comparison library for screenshot pixels.


