Visual testing
Catch unintended visual change. Rendered UI is deterministic enough that a pixel
which moved without an intended cause is worth seeing — a card that lost its shadow,
a table that overflows on tablet, a button that drifted but still works. This skill
screenshots each screen and diffs it against a baseline captured when the UI was
last correct.
Two halves, kept separate on purpose:
- Capture is agent-driven. You open the thing under test in whatever browser or
device tool you already have and save a PNG. No headless browser is bundled or
installed — capture rides on the tooling that already exists (a connected browser,
a Playwright/Appium session, a running app's automation).
- Diff is browser-free.
scripts/visual-diff.mjs shells out to reg-cli
(resolved local-first, else npx --yes reg-cli@latest, so nothing is added to
package.json) to compare, manage baselines, and write a report.
Workflow
- Render the thing under test.
- A generated static page → build it, then open the HTML from disk (
file://);
the pages are self-contained, no server needed.
- A running app → drive it to the screen/state with the automation you already
use (Playwright for web, the device tooling for mobile). This is the same
navigation a functional check performs — the visual diff is an extra assertion,
not a separate run.
- Capture each screen into a
current/ directory, one PNG per variant, named
by the scheme below. See references/capturing.md for driving each source and
controlling the noise that makes diffs flaky.
- Diff against the baseline:
node scripts/visual-diff.mjs --current <current/> --baseline <baselines/> --diff <out/>
Exit 0 = no change; exit 1 = something changed (report at <out/>/report.html).
- Review the report. An intended change (a redesign, a shipped restyle) →
promote it:
node scripts/visual-diff.mjs --current <current/> --baseline <baselines/> --diff <out/> --update
then commit the updated baselines. An unintended change → a finding: fix the
cause (or file the defect with the diff image); do not update the baseline.
A green diff is not a passing test. It proves the screen is unchanged since the
baseline — nothing about whether that baseline was ever correct or whether the
feature works. Use it to hold a reviewed UI steady, alongside the checks that judge
correctness, never instead of them.
Naming (this is what pairs a shot with its baseline)
reg-cli matches current/<name>.png to baselines/<name>.png by filename, so
names must be stable and describe exactly one variant. Pick a scheme that fits what
you're testing and keep it lowercase and stable; examples:
| Context |
Filename |
| design mock (mobile) |
screen-<flow>-<screenId>-<state>-<device>.png |
| design mock (web) |
screen-<flow>-<screenId>-<state>-web-<style>-<bp>.png |
| running web app |
web-<screen>-<state>-<viewport>.png |
| running mobile app |
mobile-<screen>-<state>-<device>.png |
| flow-map / poster |
flow-<flowId>.png |
Renaming a file breaks its baseline pairing — reg-cli then reports one deleted +
one new, which is the signal to re-approve, not a bug.
Keep the baseline set honest
Screens × states × sizes/styles is a combinatorial trap. Default coverage: the
screens actually touched, in their default state, at the size/device set you target,
in one representative variant. Add more only where a real regression risk lives.
Every variant is a baseline someone re-approves on every intended change — a giant
set nobody re-approves is worse than a focused one people actually look at.
Files
| Path |
What it is |
scripts/visual-diff.mjs |
The reg-cli wrapper: build args, local-or-npx, read the JSON report, summary + exit code. --update promotes current → baseline. |
references/capturing.md |
Driving each source (static page / running app), the naming scheme, and taming the noise that makes diffs flaky. |
references/baselines.md |
Where baselines live, the promote/approve flow, what to commit, reading the report. |
1---2name: visual-testing3description: Use when rendered UI needs visual regression checking — "did the UI change", "visual test", "screenshot diff", "compare against the baseline", "catch layout regressions", or a visual assertion in a test run. Works on any HTML you can render — a generated static page (design mock, report, flow-map poster) opened from disk, or a running app driven by your existing browser/device automation. Screenshots each screen, diffs it against a committed baseline with reg-cli (MIT, no cloud), and reports what changed as an HTML report plus a pass/fail summary. Capture rides on whatever browser or device tool you already have — no separate headless engine is installed. NOT a functional or correctness check (a diff proves pixels moved, not that anything is right), and a red diff is a prompt to look, not an automatic verdict.4---56# Visual testing78Catch **unintended** visual change. Rendered UI is deterministic enough that a pixel9which moved without an intended cause is worth seeing — a card that lost its shadow,10a table that overflows on tablet, a button that drifted but still works. This skill11screenshots each screen and diffs it against a baseline captured when the UI was12last correct.1314Two halves, kept separate on purpose:1516- **Capture is agent-driven.** You open the thing under test in whatever browser or17 device tool you already have and save a PNG. No headless browser is bundled or18 installed — capture rides on the tooling that already exists (a connected browser,19 a Playwright/Appium session, a running app's automation).20- **Diff is browser-free.** `scripts/visual-diff.mjs` shells out to `reg-cli`21 (resolved local-first, else `npx --yes reg-cli@latest`, so nothing is added to22 `package.json`) to compare, manage baselines, and write a report.2324## Workflow25261. **Render the thing under test.**27 - A generated static page → build it, then open the HTML from disk (`file://`);28 the pages are self-contained, no server needed.29 - A running app → drive it to the screen/state with the automation you already30 use (Playwright for web, the device tooling for mobile). This is the same31 navigation a functional check performs — the visual diff is an extra assertion,32 not a separate run.332. **Capture** each screen into a `current/` directory, one PNG per variant, named34 by the scheme below. See `references/capturing.md` for driving each source and35 controlling the noise that makes diffs flaky.363. **Diff** against the baseline:37 ```bash38 node scripts/visual-diff.mjs --current <current/> --baseline <baselines/> --diff <out/>39 ```40 Exit `0` = no change; exit `1` = something changed (report at `<out/>/report.html`).414. **Review the report.** An **intended** change (a redesign, a shipped restyle) →42 promote it:43 `node scripts/visual-diff.mjs --current <current/> --baseline <baselines/> --diff <out/> --update`44 then commit the updated baselines. An **unintended** change → a finding: fix the45 cause (or file the defect with the diff image); do not update the baseline.4647**A green diff is not a passing test.** It proves the screen is unchanged since the48baseline — nothing about whether that baseline was ever correct or whether the49feature works. Use it to hold a *reviewed* UI steady, alongside the checks that judge50correctness, never instead of them.5152## Naming (this is what pairs a shot with its baseline)5354`reg-cli` matches `current/<name>.png` to `baselines/<name>.png` by filename, so55names must be stable and describe exactly one variant. Pick a scheme that fits what56you're testing and keep it lowercase and stable; examples:5758| Context | Filename |59|---|---|60| design mock (mobile) | `screen-<flow>-<screenId>-<state>-<device>.png` |61| design mock (web) | `screen-<flow>-<screenId>-<state>-web-<style>-<bp>.png` |62| running web app | `web-<screen>-<state>-<viewport>.png` |63| running mobile app | `mobile-<screen>-<state>-<device>.png` |64| flow-map / poster | `flow-<flowId>.png` |6566Renaming a file breaks its baseline pairing — reg-cli then reports one *deleted* +67one *new*, which is the signal to re-approve, not a bug.6869## Keep the baseline set honest7071Screens × states × sizes/styles is a combinatorial trap. Default coverage: the72screens actually touched, in their default state, at the size/device set you target,73in one representative variant. Add more only where a real regression risk lives.74Every variant is a baseline someone re-approves on every intended change — a giant75set nobody re-approves is worse than a focused one people actually look at.7677## Files7879| Path | What it is |80|---|---|81| `scripts/visual-diff.mjs` | The reg-cli wrapper: build args, local-or-npx, read the JSON report, summary + exit code. `--update` promotes current → baseline. |82| `references/capturing.md` | Driving each source (static page / running app), the naming scheme, and taming the noise that makes diffs flaky. |83| `references/baselines.md` | Where baselines live, the promote/approve flow, what to commit, reading the report. |