Excessibility.Review (Excessibility v0.18.1)

Copy Markdown View Source

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.LiveViewRules violations 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

change()

@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()
}

report()

@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()
  }
}

tier()

@type tier() :: :auto | :review | :block

Functions

judge_changes(report, opts \\ [])

@spec judge_changes(
  report(),
  keyword()
) :: report()

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(opts \\ [])

@spec review(keyword()) :: report()

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_pair(view, baseline_html, current_html, opts \\ [])

@spec review_pair(String.t(), String.t(), String.t(), keyword()) :: change()

Review a single view's baseline-vs-current snapshot pair.

review_pairs(pairs, opts \\ [])

@spec review_pairs(
  [{String.t(), String.t(), String.t()}],
  keyword()
) :: report()

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.

tier(findings)

@spec tier([map()]) :: tier()

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.