Images

C#
using Nullean.Snapshots.Images;

byte[] png = await page.ScreenshotAsync();
png.Should().MatchImageSnapshot();

using var stream = File.OpenRead(path);
stream.Should().MatchImageSnapshot(new ImageSnapshotOptions { MaxDifferentRatio = 0.001 });

MatchImageSnapshot is in Nullean.Snapshots.Images.Assertions; Nullean.Snapshots.Images holds the format, the PNG codec and the comparison, with no AwesomeAssertions, so the browser package and the native snap tool can compare screenshots.

Image snapshots are PNG files, stored exactly as given and compared by their pixels, so they hold up when the same picture is rendered on another machine:

  • Pixels, not bytes. Both images are decoded to RGBA and every metadata chunk is ignored, so a different encoder, compression level or tEXt/gAMA/iCCP chunk never fails a test. A match never rewrites the verified file.
  • Differences measured as people see them. Each pixel's difference is a distance in YIQ colour space, which weighs brightness the way the eye does. A pixel differs when that distance exceeds Threshold.
  • Anti-aliasing left out. Font and edge rendering differ between operating systems and graphics stacks mostly in the grey pixels along edges. A differing pixel that sits between a darker and a brighter neighbour, each in a flat area in both images, is classed as anti-aliasing and not counted. An edge that moves one pixel therefore passes; one that moves two fails.

On a mismatch the message gives the count, the share and the regions that differ, and points at the received image and a diff image:

Text
Image did not match snapshot: tests/App.Tests/.snapshots/ChartTests/Draws.verified.png
1,204 of 480,000 pixels differ (0.25%), 0 allowed; 36 anti-aliased pixel(s) left out
2 difference(s):
x 120-184, y 40-58 (65x19): 1,180 pixel(s)
x 610-613, y 402-405 (4x4): 24 pixel(s)
Received: .artifacts/.snapshots/received/App.Tests/ChartTests/Draws.received.png
Diff:     .artifacts/.snapshots/received/App.Tests/ChartTests/Draws.received-diff.png

The diff image is the verified image faded to grey, with differing pixels in red and anti-aliasing in yellow. Both images sit in the received folder, so a CI job that uploads artifacts/ keeps them. A different width or height fails without a pixel comparison and without a diff image.

Option Default Means
Threshold 0.1 how different a pixel may look, from 0 (any change) to 1 (black equals white)
IncludeAntiAliasing false count anti-aliasing pixels as differences
MaxDifferentPixels 0 how many pixels may differ
MaxDifferentRatio 0 the share of pixels that may differ; with both set, the larger allowance applies

Start with the defaults. Raise MaxDifferentRatio for a rendering that is noisy by nature (gradients, video frames); raise Threshold only for colour drift, as it also hides real colour changes. Each is a trade between noise and what the test can catch.

Tolerance absorbs rasteriser noise, but not a different font: a fallback font reflows the text and moves everything after it. Keep renders comparable at the source:

  • Render in a pinned container image (the Playwright image for its version) locally and in CI.
  • Ship the fonts the page uses with the test instead of relying on system fonts.
  • Fix the viewport and use deviceScaleFactor: 1.
  • In headless Chromium, --disable-gpu and --font-render-hinting=none remove two sources of drift.
  • Turn off animations, transitions and the text caret before taking the screenshot.

The package has its own PNG decoder, in managed code with no dependencies, so every machine decodes a file to the same pixels. It reads every colour type, bit depth and interlacing the PNG specification allows, including palettes and tRNS transparency; 16-bit samples are reduced to their high byte. Colour management (gAMA, cHRM, sRGB, iCCP) is not applied. Images up to 8192 × 8192 pixels are accepted. Other formats are not: convert them to PNG first. A value that is not a valid PNG fails the assertion before anything is written.

Git detects PNG files as binary, but a rule such as *.verified.* text overrides that and lets git rewrite line endings inside them. If your .gitattributes has one, mark images binary after it:

Text
*.verified.png binary

snap treats image snapshots as binary: snap accept copies them byte for byte, and snap diff and snap review show their paths, sizes and the diff image instead of a line diff.

ImageSnapshotOptions.OutlineRegions draws a blue rectangle, two pixels thick and three pixels clear, around each region of the diff image where the images differ, so a change on a busy page is easy to find. It is off by default, so a diff image stays the faded image with the differences in red and anti-aliasing in yellow. At most 100 regions are boxed. Comparing two sites turns it on for its screenshots.

ImageSnapshotOptions.ZoomOnChanges also writes a zoomed diff, {name}.zoom.png: the diff image cropped to all the changed places with 32 pixels around them, at its own size, so every pixel is a pixel of the screenshot and none is enlarged. Nothing is written when that crop would cover more than half the image, where the diff image already shows it. Off by default; a site comparison turns it on.