Visual regression
Review what changed on screen in the last run with tr visual — which element caused it, then accept or reject the new baseline from the terminal, recorded locally and synced to the platform when the run was uploaded.
tr visual reads the local .testrelic-report your last Playwright run wrote and tells you
what changed on screen — not as a diff image, which a terminal cannot show, but as the
element that caused it: div.card.accent moved · restyled — and 10 elements shifted below it. When the change is intended, tr visual accept makes this run's render the new
committed baseline; when it is not, tr visual reject records that the baseline stands.
No login and no upload are needed to read a report. Decisions are always recorded beside the report, and synced to TestRelic Cloud when the run went there.
What it reads
The report written by @testrelic/playwright-analytics 2.16.0 or later — every
toHaveScreenshot / toMatchVisualBaseline comparison, with the DOM attribution from the
Visual Testing guide. accept needs 2.16.2 or
later, which records where each committed baseline lives.
Usage
tr visual # every comparison in the report (the default: list)
tr visual show drift-target # one comparison in full
tr visual open # the report, in a browser, for the pixels
tr visual accept drift-target --note "the accent card restyle is intended"
tr visual reject structural-swaptr visual looks for .testrelic-report under the current directory (up to three levels
down, so it finds test-results/.testrelic-report); --path points it elsewhere.
list
One line per comparison, grouped by spec file, most changed first:
visual.spec.ts
✗ drift-target 16.04% 80,858 px 11 elements
div.card.accent moved · restyled — and 10 elements shifted below it
✗ structural-swap 0.22% 1,098 px 3 elements
div.banner restyled · attrs
✓ release-health-dashboard matched its baseline
✓ dashboard-with-ticker matched its baseline
2 changed · 2 matched · 0 baseline written · 0 size mismatch- ✗ changed — the render differs from its baseline; the ratio and pixel count are Playwright's, the cause line is the element the DOM comparison ranked first.
- ✓ matched — a comparison that passed. Only
toMatchVisualBaselinerecords these; nativetoHaveScreenshotattaches nothing when images match. - ⚠ baseline written — no baseline existed, so this run's render became one. Nothing was compared, so it is never printed as a match.
- size mismatch — the two images have different dimensions; the comparator cannot draw a diff across them, and there is nothing to accept.
A retried test is listed once, with its final attempt's verdict.
The command exits 1 when any comparison changed, so it can gate CI. Add
--fail-on-review to also exit 1 for a size mismatch or a baseline that was only written —
the two cases where the run compared nothing it could judge.
show
Everything the DOM comparison found for one comparison, the edit separated from what it shifted:
drift-target
failed · toHaveScreenshot · 16.04% · 80,858 px
visual.spec.ts · drifting card is caught
WHAT ACTUALLY CHANGED
div.card.accent moved · restyled 7.9%
background-color rgb(3, 183, 156) → rgb(224, 108, 79)
margin-top (initial) → 24px
MOVED OR RESIZED ONLY · 10
Nothing about these changed except their position.
div.grid resized 28.9%
div.bar moved 19.9%
…A comparison with no DOM capture (a suite on Playwright before 1.51, or DOM comparison turned off) shows its pixel figures and says so, rather than claiming nothing moved.
Naming a comparison
show, accept and reject take the snapshot name as list prints it. When two spec
files use the same name, qualify it — visual.spec.ts::drift-target — or give the test id
from --json. An ambiguous name is refused with the candidates listed, never guessed.
accept and reject
accept copies this run's actual render over the committed baseline file — the one
Playwright reads next time, which the report names in baselinePath — and nothing else.
reject changes no files. Both:
- refuse what cannot be decided — a comparison that passed, a baseline that was only written, a size mismatch — so a decision is never recorded only to be turned away later;
- record the decision locally, always, as one line appended to
<report>/visual-decisions.jsonlbefore anything touches the network, so a decision cannot vanish if the sync fails; - sync it to TestRelic Cloud when this run was uploaded there — the report's
summary.jsoncarries the run id and the endpoint it went to, and the endpoint has to be the one this CLI is pointed at (Authentication). The line is then flipped tosynced: true. Any other outcome keeps the local record and prints one line saying why it stayed local — including the two worth naming: a platform that predates the visual endpoints, and an MCP token without themcp:visualscope (recording a decision is a write; a token minted to read dashboards cannot make one).
accept also promotes the committed <name>.dom.json beside the image when the project keeps
one. The SDK diffs the next run's DOM against that file, so moving only the pixels would report
the change you just accepted on every run after it — on a comparison that passed.
Accepted visual.spec.ts::drift-target
promoted tests/visual.spec.ts-snapshots/drift-target-win32.png
from test-results/artifacts/2026-09-14T18-04-14/…/visual/drift-target--d743c8/actual.png
recorded locally in test-results/.testrelic-report/visual-decisions.jsonl — the run was
uploaded to http://localhost:3011/api/v1, and this CLI is pointed at
https://platform.testrelic.ai/api/v1--note travels with the decision, locally and on the platform. After accepting, commit
the changed baseline as you would after --update-snapshots — the difference is that this
one records who accepted what, and why.
Where the committed file is looked for
baselinePath is relative to the directory of your Playwright config. The CLI resolves it
under --path (or the current directory) and then under each parent of the report
directory, so a monorepo works from its top or from the package. A path that would leave
the project, or a report older than SDK 2.16.2 that has no baselinePath, is refused rather
than guessed from a snapshot-path template.
open
Opens the run's HTML report in a browser — where each comparison shows baseline, this run, a
wipe slider and Playwright's pixel diff — for the part of the review a terminal cannot do.
A report written without the HTML page is refused with the npx testrelic serve command
that serves it.
--json
Every subcommand emits JSON with --json: the comparisons with their status, ratio, pixel
count, element changes and test id — the shape an agent or a script reads.
In Studio and in the cloud
The same comparisons appear in Studio, on a Visual tab beside Steps, Console and Network in a test's stack — with the changed elements' boxes drawn over the render, and Accept / Reject for a run that was uploaded. A report opened from disk shows the comparison without decision buttons: there is no server to record one.
On the platform, an uploaded run has the same Visual tab in its session, and the standalone Visual Regression workspace for triaging every comparison in a session. A decision made in either place, or synced from the CLI, is the one every surface shows.
Next steps
→ Visual Testing · Artifacts & headless output · Authentication
Artifacts & headless output
How the CLI renders dashboards, reports, and test plans in the terminal, plus the artifacts subcommand and the print-mode output formats for scripts and CI.
TestRelic CLI configuration
Configure the CLI's backend URL, theme, statusline, and keybindings, and learn where session cookies, tokens, and MCP config files live on disk.