Visual and snapshot regression
Catches what assertions cannot: layout that broke, a component that vanished, a theme that inverted. Also the category most prone to becoming noise, so the real lesson is threshold tuning and review discipline.
Visual regression testing compares a screenshot of your interface against an approved one and flags the differences.
Snapshot testing is the same idea applied to serialised output rather than pixels. A stored copy of rendered markup or a data structure, compared on later runs.
Both catch a class of defect nothing else does. A layout collapse on one viewport. Text overflowing its container. An element that disappeared because a CSS change removed it. Nobody writes an assertion for those, because nobody thinks of them.
Both also produce false positives at a rate that determines whether the practice survives. The whole skill is scoping and noise control.
The terms you will hear
- Baseline. The approved image or snapshot that future runs are compared against.
- Diff. The visual difference between the baseline and the current run.
- Threshold. How much difference is tolerated before a test fails.
- Masking. Excluding regions that legitimately change, so they cannot cause a diff.
- Approval. Accepting a diff as the new baseline. The step where discipline is won or lost.
- Flaky diff. A difference caused by rendering variation rather than a real change.
Where the noise comes from
Understanding this is most of the practice.
- Fonts and rendering. The same page renders differently on macOS and Linux, so run in a container and always the same one.
- Dynamic content. Dates, names, avatars, order numbers. Freeze them or mask them, which is the determinism problem again.
- Animation and transitions. Capture after they finish, or disable them for the run.
- Lazy loading and images. Wait for the network to settle, or the screenshot catches a half-loaded page.
- Scrollbars and viewport chrome. Small, constant, and enough to fail a strict threshold.
How to scope it so it survives
- Pick a handful of targets. The components with real layout risk and the two or three pages that matter commercially. Not every page.
- Two viewports, not six. One mobile, one desktop. Add more only when a defect demonstrates the need.
- Prefer component-level over full-page. A component diff is small and obvious, and it pairs with the component tests you already have. A full page diff is often one pixel of reflow.
- Freeze the data. Fixed dates, fixed names, seeded content. Then mask anything left that moves.
- Disable animation for the run, and wait for fonts and images before capturing.
- Set a threshold deliberately and record why. Zero tolerance produces noise, and a loose threshold hides real shifts.
- Require a named approver. Approval is a decision, so it needs an owner, exactly like accepting a known issue.
A worked setup
For example, here is one narrow, surviving setup.
BEFORE full-page screenshots of 34 pages, 6 viewports, 204 comparisons
diffs per pull request 11 to 30
time to review nobody did. approve-all was normal.
real defects caught in 4 months 1
verdict: expensive noise
AFTER scoped deliberately
COMPONENTS (9, one viewport each, in a Linux container)
gift card field, basket summary, price block, order line,
payment method list, alert banner, empty basket, nav, footer
PAGES (3, two viewports: 390px and 1280px)
checkout, order confirmation, gift card balance
TOTAL 15 comparisons
NOISE CONTROL
fonts loaded and network idle before capture
animations disabled via reduced-motion
frozen clock, so dates are constant
masked: avatar images, order reference, the promo strip
threshold: 0.2 percent of pixels, agreed and written down
APPROVAL RULE
a diff is approved by the person who made the change, and the
approval comment says what changed and why.
AFTER SIX WEEKS
diffs per pull request 0 to 2
real defects caught 4
- price block overflowed at 390px with a 4-digit total
- alert banner lost its icon after a dependency bump
- footer collapsed on iOS Safari only
- confirmation page lost the payment lines entirely after a
refactor. no other test noticed, because the API was fine.
approvals reviewed all of them, because there were fewThe last defect is the argument for the practice. The API returned the payment lines correctly, every API test passed, and the page simply stopped rendering them. Only a picture catches that.
How to show you know it
- A scoped list with a reason. Nine components and three pages beats thirty-four pages nobody reviews.
- A noise budget. "Zero to two diffs per pull request." It is the number that predicts whether the practice lives.
- A defect only a picture could catch. The missing payment lines is the canonical example.
- A masking list. Showing which regions you excluded, and why, demonstrates you understand the failure mode.
Questions
Do we need a paid service?
Not to start. Playwright and other frameworks have screenshot comparison built in, and a container plus a baseline folder is enough. Paid services help most with review workflow and cross-browser rendering at scale.
Is snapshot testing of markup worth it?
In moderation, and never as the only assertion. It is a change detector. Somebody has to have verified the first snapshot, and a snapshot that includes a bug protects the bug.
How do I handle intentional redesigns?
Approve in bulk, deliberately, as one change with a note. That is the one time approve-all is correct, and it should be a decision rather than a habit.
What if our rendering differs between machines?
Always capture in the same containerised environment. Comparing a macOS baseline against a Linux CI run produces permanent noise and is the most common reason teams abandon this.