A blast-radius report for a snapshot run, measured against the baseline.
Where mix excessibility checks each snapshot in isolation, a review
asks a sharper question: what did this change actually do? It diffs
every current snapshot against its baseline (the known-good/main
state) and, per view, reports:
- the rendered regions that changed (via
Excessibility.SnapshotDiff) - the accessibility findings this change newly introduced —
axe-core violations and
Excessibility.LiveViewRulesviolations present now but not in the baseline (a finding-delta) - a risk tier —
:auto,:review, or:block
With content_diff: true, content that changed without an aria-live
announcement is also flagged. That signal compares rendered text, so it
is only meaningful when the baseline and current snapshots rendered the
same fixture data — with the usual CI shape (baseline and current
from two independent mix test runs) it mostly reports fixture drift,
which is why it is off by default.
axe-core runs through the configured :scanner_mod (a browser scan of
each side of the pair); disable it with axe: false. When a scan fails
(e.g. Playwright isn't installed) the review still runs on the LiveView
rules alone and says so in the report's :warnings.
The tier is a transparent heuristic over the new findings; a smarter
judge can be layered on top of the same report. Because it diffs against
the baseline rather than two git refs, it runs from an ordinary
mix test + baseline, with no worktree gymnastics.
Summary
Functions
Run the configured judge over each change in a report.
Review every current snapshot that has a baseline, returning a report.
Review a single view's baseline-vs-current snapshot pair.
Review a list of {view, baseline_html, current_html} tuples.
The risk tier for a list of findings (accessibility and/or behavioral).
Types
@type change() :: %{ view: String.t(), regions: [Excessibility.SnapshotDiff.region()], region_count: non_neg_integer(), findings: [map()], behavioral: [Excessibility.Review.Behavioral.finding()], warnings: [String.t()], tier: tier() }
@type report() :: %{ changes: [change()], behavioral: [Excessibility.Review.Behavioral.finding()], warnings: [String.t()], summary: %{ auto: non_neg_integer(), review: non_neg_integer(), block: non_neg_integer() } }
@type tier() :: :auto | :review | :block
Functions
Run the configured judge over each change in a report.
Attaches the judge's verdict to every change, replaces the change's
tier with the judged tier, and recomputes the summary. Run-level
behavioral findings are handed to the judge as context
(:run_behavioral) but are not attributed to any view — they
stay at the report level, where mix excessibility.review prints
them and gates the exit code on them. A view's tier only reflects
what that view introduced.
Review every current snapshot that has a baseline, returning a report.
Reads snapshots from the configured output path (html_snapshots/ vs
baseline/). Only views that actually changed (regions or findings)
appear in :changes.
Review a single view's baseline-vs-current snapshot pair.
Review a list of {view, baseline_html, current_html} tuples.
Unchanged views (no regions and no findings) are dropped. Returns a report with per-view changes and a tier summary.
The risk tier for a list of findings (accessibility and/or behavioral).
Driven by the worst severity: a new critical/serious finding is :block,
any other finding is :review, none is :auto. Regions are reported for
context but don't escalate on their own.