Playwright toHaveScreenshot vs jest-image-snapshot
This page is part of the tool comparisons section of the visual regression guide, which covers choosing between self-hosted and cloud visual testing.
The specific question: both Playwright’s toHaveScreenshot and jest-image-snapshot are self-hosted, store baselines in the repository, and diff with pixelmatch. What actually differs, and which should a component library use?
Problem statement
The two are frequently presented as alternatives, which obscures the real distinction: toHaveScreenshot is an assertion and a capture mechanism, while jest-image-snapshot is an assertion only. It compares whatever image buffer you give it and has no opinion about where that buffer came from.
The symptom of getting this wrong is a project that installs jest-image-snapshot and then discovers it still needs Playwright or Puppeteer to produce the screenshot — at which point it is running two tools where one would have done.
Root cause
They were built for different starting points. Playwright’s matcher assumes you are already in a Playwright test with a page, and gives you capture, comparison, retries, traces and an HTML report as one integrated thing.
jest-image-snapshot assumes you already have an image and are already in Jest. That is genuinely useful when the image comes from somewhere other than a browser — a canvas rendering, a PDF page, a server-side rasteriser — and redundant when it comes from a browser you are already driving.
Minimal reproduction
// Playwright: capture and assertion in one call
await expect(page.locator('#storybook-root')).toHaveScreenshot('card.png', {
maxDiffPixelRatio: 0.002,
animations: 'disabled',
});
// jest-image-snapshot: you supply the image
import { toMatchImageSnapshot } from 'jest-image-snapshot';
expect.extend({ toMatchImageSnapshot });
const image = await page.screenshot(); // still needs a browser driver
expect(image).toMatchImageSnapshot({
failureThreshold: 0.002,
failureThresholdType: 'percent',
});
Step-by-step comparison
1. Compare what each brings to a component library
For a component library driven by Playwright or the Storybook test-runner, toHaveScreenshot is the smaller setup: no extra dependency, and retries, traces and the HTML report come with it.
2. Compare the threshold models
// Playwright: three independent knobs
await expect(el).toHaveScreenshot('x.png', {
threshold: 0.2, // per-pixel colour distance, 0–1
maxDiffPixels: 400, // absolute count
maxDiffPixelRatio: 0.002, // fraction of the image
});
// jest-image-snapshot: one threshold, with a type
expect(image).toMatchImageSnapshot({
failureThreshold: 0.002,
failureThresholdType: 'percent', // or 'pixel'
comparisonMethod: 'ssim', // structural comparison, if wanted
});
The practical difference is that Playwright separates per-pixel tolerance from whole-image budget, which is what lets a component absorb anti-aliasing noise while still failing on a real shift. jest-image-snapshot compensates by offering SSIM, which absorbs that noise structurally instead.
3. Compare what happens on failure
# Playwright writes three images and an HTML report entry
test-results/card-chromium/card-expected.png
test-results/card-chromium/card-actual.png
test-results/card-chromium/card-diff.png
# jest-image-snapshot writes a composite
__image_snapshots__/__diff_output__/card-diff.png
Playwright’s separate expected, actual and diff images are easier to review; the composite is more compact.
4. Compare how each handles a browser matrix
Playwright projects give per-browser baselines and per-browser results for free. Under jest-image-snapshot the browser is something your own code chose, so distinguishing baselines by engine means building the path yourself with customSnapshotIdentifier.
5. Decide by what already runs
If Playwright is already in the stack, toHaveScreenshot costs nothing to adopt and removes a dependency. If the images do not come from a browser at all, jest-image-snapshot is the only one of the two that applies.
Verification
Both detect the same regression at the same magnitude, because both are pixelmatch underneath at their defaults. Any difference in verdict comes from the threshold model rather than from detection ability — which is worth confirming on your own components before switching, by running both against a deliberate one-pixel shift.
Edge cases and caveats
-
Mixing both in one repository. Workable, and a source of confusion: two baseline directories, two update commands, two threshold vocabularies. If both are genuinely needed, keep them in clearly separate tiers.
-
jest-image-snapshotunder Vitest. It works viaexpect.extend, but the update flag differs from Jest’s, and CI detection is based on theCIenvironment variable — which means a local run withCI=truerefuses to write new baselines, exactly as intended and often surprising. -
Playwright’s implicit retry.
toHaveScreenshotretries the capture until it is stable or the timeout expires, which masks mild non-determinism. Helpful in practice, and worth knowing when a story looks stable under Playwright and flaky elsewhere.
Migrating between them
Teams usually move in one direction — from jest-image-snapshot plus a browser driver to Playwright’s matcher — because it removes a dependency and a framework. The migration is mechanical, and the one thing that cannot carry over is the baselines themselves.
Baselines must be regenerated, not moved. The two tools capture through different code paths, and even at the same viewport and scale factor the resulting PNGs differ in encoding and often in a handful of pixels. Copying the old images into the new path produces a suite that fails everywhere on its first run for reasons that have nothing to do with the components.
Thresholds must be re-derived. A failureThreshold of 0.002 as a percentage is not the same policy as maxDiffPixelRatio: 0.002 combined with a per-pixel threshold, because the second lets anti-aliasing noise be absorbed before the ratio is computed. Re-measure the noise floor after the switch rather than porting the number.
Do it in one pull request that changes nothing else. Every baseline in the suite is about to be rewritten, so the only way the change is reviewable is if that rewrite is the sole explanation for every diff in it — the same one-cause property that makes any large baseline change reviewable.
Keep the story ids identical. If the baseline filenames stay derived from the same story ids, the new directory maps one-to-one onto the old, which makes it possible to spot-check that nothing was dropped in the move.
The migration is worth doing when Playwright is already in the stack for other reasons. It is not worth doing to chase a marginal difference in comparison quality, because at their defaults there is not one.
FAQ
Is one more accurate than the other?
No — at their defaults both use pixelmatch, so the same change produces the same pixel count. What differs is how that count is turned into a verdict: Playwright’s separate per-pixel and whole-image thresholds, against a single threshold with a type. Neither model is more correct, and both can express the same policy with different numbers.
Which should a new component library choose?
Whichever matches the runner already in place, and if there is none, Playwright — because the visual tier needs a real browser anyway, and choosing the tool that supplies one avoids running a second framework purely to produce images.
Can jest-image-snapshot compare something that is not a screenshot?
Yes, and that is where it is genuinely irreplaceable. It compares any PNG buffer, so a canvas export, a server-rendered chart, a rasterised PDF page or an image-processing pipeline’s output can all be baselined with it. Playwright’s matcher is tied to a page, so none of those are within its reach.
Do the two produce different baseline file sizes?
Slightly, because they encode PNGs through different libraries, and the difference is not worth optimising for. What does matter for size is the capture: pinning the device scale factor to 1 and scoping the shot to the component rather than the page changes file sizes by an order of magnitude, and that lever is available in both tools equally.
Related
- Tool Comparisons — the parent section on choosing a visual testing tool
- Jest Snapshots vs Playwright Visual Diff vs Chromatic — the wider three-way comparison this narrows
- Pixel Diff Algorithms — the comparator each tool uses underneath
- Tolerance Thresholds — the options each exposes for setting a budget