TestRelic AI
Go to App
CLI

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-swap

tr 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 toMatchVisualBaseline records these; native toHaveScreenshot attaches 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.jsonl before 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.json carries 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 to synced: 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 the mcp:visual scope (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

Was this page helpful?

On this page