# `Excessibility.SnapshotDiff`
[🔗](https://github.com/lessthanseventy/excessibility/blob/v0.18.1/lib/excessibility/snapshot_diff.ex#L1)

Cross-snapshot diffing: compare two HTML snapshots of the same view and
report the regions whose rendered content changed.

This is a capability no single-snapshot tool (axe-core included) can
provide: by comparing a *before* and *after* of the same template it can
detect that visible content changed. The first consumer is the
accessibility check in `live_region_findings/3` — LiveView patches the
DOM without a page load, so content that updates outside an `aria-live`
region (or `role="alert"|"status"|"log"`, or `<output>`) is never
announced to screen-reader users (WCAG 2.1 SC 4.1.3, Status Messages).

`diff/3` itself is content-agnostic and returns the structural delta, so
it doubles as a general-purpose semantic DOM diff for other callers.

## Diff strategy

Both snapshots are parsed and walked in parallel from `<body>` down,
matching element children positionally by tag. The change is localized
to the **deepest element whose subtree text differs but whose children
still line up** — i.e. the smallest stable container that owns the
change. When the child structure itself diverges (rows added/removed,
tags differ), that parent is reported as the changed region. Text is
whitespace-normalized, so reflow/indentation differences are ignored.

# `region`

```elixir
@type region() :: %{
  change: :changed,
  selector: String.t(),
  tag: String.t(),
  old_text: String.t(),
  new_text: String.t(),
  announced: boolean(),
  element: String.t()
}
```

A single changed region produced by `diff/3`.

# `diff`

```elixir
@spec diff(String.t(), String.t(), keyword()) :: [region()]
```

Diff two HTML snapshots and return the list of changed regions.

Each region is the deepest stable container whose normalized text
content differs between `old_html` and `new_html`. Returns `[]` when the
documents are content-equivalent (whitespace aside) or either fails to
parse.

# `live_region_findings`

```elixir
@spec live_region_findings(String.t(), String.t(), keyword()) :: [
  Excessibility.LiveViewRules.Rule.finding()
]
```

Return accessibility findings for content that changed outside any live
region, in the `t:Excessibility.LiveViewRules.Rule.finding/0` shape.

A change is considered *announced* (and therefore not flagged) when the
changed region or any of its ancestors is an `aria-live` region (a value
other than `"off"`), carries `role="alert"|"status"|"log"`, or is an
`<output>` element.

# `scan_files`

```elixir
@spec scan_files(
  [Path.t()],
  keyword()
) :: [{Path.t(), Excessibility.LiveViewRules.Rule.finding()}]
```

Diff snapshot *files* on disk, grouped by the test that produced them.

Auto-captured snapshots embed a `Test:`/`Sequence:` metadata comment;
this groups files by test, orders them by sequence, and diffs each
consecutive pair. Returns `{file, finding}` tuples where `file` is the
later snapshot of the pair the finding came from.

Snapshots without that metadata (the default `Module_line.html` naming
carries no reliable test boundary) are skipped, so this is inert unless
capture metadata is present.

# `scan_sequence`

```elixir
@spec scan_sequence(
  [String.t()],
  keyword()
) :: [Excessibility.LiveViewRules.Rule.finding()]
```

Run `live_region_findings/3` over each consecutive pair in a list of
snapshots captured for the same test, and concatenate the findings.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
