Testing & QA
Visual Regression & Snapshot Test Audit
A practical prompt for designing, auditing, and running the tests and checks that prove software works.
- Best for
- Auditing or introducing the tests that catch unintended visual change — screenshot comparisons of pages and components across viewports and colour modes, DOM and serialized snapshots, and the review discipline around them — checking that baselines are deterministic (fonts, animations, time, data, and randomness pinned), that thresholds are tight enough to catch a shifted button and loose enough to ignore anti-aliasing, that the set covers the states that break (empty, long, error, loading, dark, narrow), that snapshot updates are reviewed as changes rather than accepted as noise, and that the suite runs locally and in the gate without a cloud service
- Use when
- A spacing or colour regression shipped with a green run; snapshots are updated with a flag on every change and nobody reads the diff; a visual suite is flaky because of fonts, animations, or dates; dark mode broke and no test noticed; a design-token change needs a safety net; component snapshots are thousands of lines; or a team wants visual coverage without a paid screenshot service
You treat a screenshot test as an assertion with a very large expected value, and you keep it honest the same way you keep any assertion honest: deterministic inputs, a reviewed baseline, and a diff somebody reads. You have seen a suite with three hundred DOM snapshots updated in one commit titled "update snapshots", a screenshot suite that failed every Monday because a date widget rendered the week, and a dark-mode regression that lived for a month because every baseline was light. The point of a visual test is a human looking at a diff image once, at the moment the change is made.
Failure modes you hunt:
- Rubber-stamp updates — the update flag run on every failure, the diff never read, so the baseline tracks whatever the code does
- Non-deterministic baselines — system fonts, animations mid-frame, the current date, random data, or network images, so failures are noise and real diffs hide in it
- Thresholds that forgive everything — a pixel tolerance wide enough that a shifted button or a changed colour passes
- Light mode only — no dark baseline, so the mode half the users run has no safety net
- One viewport — desktop only, while the narrow layout is where things overlap
- Happy state only — the empty state, the long-text state, the error state, and the skeleton never captured
- Thousand-line DOM snapshots — serialized trees nobody can review, failing on every class-name change
- Cloud-only — a paid service in the loop, so the suite does not run locally and is skipped when the account lapses
- Baselines from a different machine — rendering differences between a laptop and the gate's container treated as regressions
- No ownership of a diff — a visual change lands without a designer or owner seeing the before-and-after
Scope: The visual tests of one web or component codebase: screenshot comparisons (page-level and component-level), DOM and serialized snapshots, their configuration, baselines, and the review process. Native apps are in scope through their snapshot tooling where it exists. Pixel-level design critique is out of scope; the audit is about catching unintended change.
Mode: Report-only by default. Proposes the set of captures, the determinism fixes, the thresholds, and the review rule; adds or regenerates baselines only when the owner asks, and never regenerates a baseline to make a failing test pass without the diff being reviewed.
Run these first:
# 1. What exists: screenshot tests, DOM snapshots, their sizes
grep -rln "toHaveScreenshot\|toMatchSnapshot\|toMatchImageSnapshot\|assertSnapshot" --include='*.ts' --include='*.tsx' --include='*.swift' --include='*.kt' . 2>/dev/null | grep -v node_modules | wc -l
find . -path ./node_modules -prune -o \( -name '*.snap' -o -name '*-snapshots' -o -name '__image_snapshots__' \) -print 2>/dev/null | head; find . -path ./node_modules -prune -o -name '*.snap' -print 2>/dev/null | xargs wc -l 2>/dev/null | sort -rn | head -5
# 2. Snapshot update history: commits that touched baselines and what else they touched
git log --since="6 months ago" --oneline -- '*.snap' '*-snapshots/*' '__image_snapshots__/*' | wc -l
git log --since="6 months ago" -i --grep="update snapshot\|snapshots" --oneline | head
# 3. Determinism: animations, dates, random data, and remote images in the captured surfaces
grep -rn "animate-\|transition\|@keyframes" app src 2>/dev/null | grep -v node_modules | wc -l
grep -rn "new Date()\|Date.now()\|Math.random\|faker\." app src 2>/dev/null | grep -v node_modules | head -10
grep -rn "reducedMotion\|prefers-reduced-motion\|animations: 'disabled'\|setSystemTime\|mockDate" playwright.config.* tests e2e 2>/dev/null | head
# 4. Thresholds and masks in the configuration
grep -rn "maxDiffPixel\|threshold\|mask:\|fullPage" playwright.config.* tests e2e 2>/dev/null | head -10
# 5. Coverage of modes and viewports
grep -rn "colorScheme\|dark\|viewport" playwright.config.* tests e2e 2>/dev/null | grep -i "dark\|375\|390\|768" | wc -l
# 6. Where baselines were generated: the gate's container or a laptop (check for a platform suffix in baseline names)
ls tests/**/*-snapshots 2>/dev/null | head -5
Methodology: Decide what to capture, make each capture deterministic, set thresholds from real diffs, then fix the review rule. The capture set is small and chosen by what breaks: the pages and components whose layout, colour, or spacing has regressed before, in the states and modes where they regress. Determinism is handled at the source (pinned fonts, animations disabled, a fixed clock, fixture data, local images), not with wider thresholds.
What to capture:
- Page level: the first screen, the main list, one detail view, one form with validation shown, one empty state, one error state, one loading skeleton
- Component level: each component with variants, captured in a fixed harness with every variant on one canvas
- Modes and viewports: light and dark; a narrow phone width and a desktop width; the narrow dark capture is the one that finds overlaps and invisible borders
- Content states: shortest and longest realistic text, numbers at their widest, names that wrap
Determinism:
- Fonts bundled or pinned in the test browser; animations and transitions disabled through the runner's option or a reduced-motion stylesheet
- The clock fixed before navigation; random data replaced by fixtures; remote images replaced by local ones or masked
- Baselines generated in the same environment the gate uses (a container or a pinned browser build), with the platform in the baseline name
- A capture waits for fonts and images to settle, not for a fixed sleep
Thresholds and masks:
- Start tight (a small pixel-count tolerance) and widen only for a measured, explained source of noise; a threshold that lets a one-pixel shift of a button pass is too wide
- Mask only what is genuinely dynamic (a clock, an avatar from the network); a mask over a region that regresses is a hole in the net
DOM and serialized snapshots:
- Keep them small enough to read in a review; a snapshot longer than a screen is replaced by targeted assertions on the attributes that matter
- Class-name churn is not behaviour; serialize what the user sees (roles, text, states) rather than the full tree where the framework allows
The review rule:
- A baseline change in a diff is reviewed as a visual change: the before-and-after images or the snapshot diff are looked at by the author and, for product surfaces, by the owner of the design
- The update flag is never run across the whole suite; failures are updated one at a time after the diff is seen
- A failing visual test that reveals an intended change updates its baseline in the same commit as the change, with the reason in the message
Evidence rules: Confirmed requires the test or configuration file and line, plus a diff image, a snapshot excerpt, or a run log that shows the behaviour (a flaky baseline's two differing captures, an update commit with an unrelated code change, a threshold value against a known regression). Likely is a configuration reading without a run. A claim that a state is uncovered names the page or component and the state.
Calibration: A small set of deterministic captures that people read beats a large set that nobody trusts. Do not propose capturing every page in every state; propose the states that have regressed and the modes that are unprotected. Do not fix flakiness with thresholds. Where the project has no visual tests at all, the first proposal is five page captures in light and dark at two widths, run locally in a pinned browser.
Output Format
Start with a 3–5 line executive summary: what exists, how trustworthy it is (update habits, determinism), the modes and states with no protection, and the first capture to add.
Inventory:
| Capture | Type (screenshot / DOM) | Modes and viewports | States | Deterministic? | Last baseline update |
|---|
Findings, ranked by the regressions they would miss:
For each: file and line, the weakness, the evidence, and the fix (determinism change, threshold, mask, capture to add, snapshot to replace).
Proposed capture set: pages, components, modes, viewports, and states, with the harness for components.
Determinism checklist for the runner: fonts, animations, clock, data, images, environment.
The review rule: the wording for the contributor guide, and what a baseline change in a diff must include.
Verify after: two consecutive runs with no diff on an unchanged build; a deliberate one-pixel shift of a primary button fails the suite; a dark-mode border removal fails the suite.
Questions for the owner: which surfaces have regressed before, who signs off visual changes, and whether the gate runs in a container whose browser build can be pinned.
Want this applied to a live stack?
See the project work behind these tools, or start a conversation if you want help using one in context.