Skip to content

feat: add aria snapshot utilities#8

Merged
sheremet-va merged 74 commits intovitest-dev:masterfrom
hi-ogawa:feat-aria
Mar 18, 2026
Merged

feat: add aria snapshot utilities#8
sheremet-va merged 74 commits intovitest-dev:masterfrom
hi-ogawa:feat-aria

Conversation

@hi-ogawa
Copy link
Copy Markdown
Collaborator

@hi-ogawa hi-ogawa commented Mar 13, 2026

This PR add core aria logic required by toMatchAriaSnapshot implemented on vitest-dev/vitest#9668.

API

Here is a rough picture of API.

                  generateAriaTree         renderAriaTree
        Element ──────────────────► AriaNode ──────────────► string (YAML)
                                       │                        │
                                       │                        │ parseAriaTemplate
                                       │                        ▼
                                       │               AriaTemplateNode
                                       │                        │
                                       └────── matchAriaTree ◄──┘
                                                    │
                                                    ▼
                                       { pass, resolved: string } (AriaMatchResult)
                  renderAriaTemplate
  AriaTemplateNode ─────────────────► string (YAML)
generateAriaTree(element: Element): AriaNode
renderAriaTree(node: AriaNode): string
parseAriaTemplate(yaml: string): AriaTemplateNode
renderAriaTemplate(template: AriaTemplateNode): string
matchAriaTree(node: AriaNode, template: AriaTemplateNode): { pass: boolean, resolved: string }

Main code

I've structured code in a following way:

src/aria/
├── folk/           ← Playwright fork (generateAriaTree, renderAriaTree, parseAriaTemplate)
├── match.ts        ← match algorithm (matchAriaTree)
├── template.ts     ← template rendering utilities (renderAriaTemplate)
└── index.ts        ← public exports for Vitest

Part 1. Playwright fork (src/aria/folk/)

Direct copy from Playwright with minimal divergences (marked with DIVERGENCE(playwright), list with grep -rn 'DIVERGENCE(playwright)' src/aria/folk/). Not deeply reviewed — correctness relies on upstream.

Part 2. Match algorithm (src/aria/match.ts)

This implements matchAriaTree which provides parital matching/merging mechanism for given AriaNode and AriaTemplateNode. While playwright's aria snapshot doesn't have such update mechanism (their snapshot update is all or nothing update based on current node), matchAriaTree still relies on exact matchesNode exported from src/aria/folk.

The algorithm, invariants, Playwright comparison, and complexity analysis are documented in the comment at the top of match.ts.

Part 3. Tests (test/aria.test.ts)

Wrote mostly manual brute force style unit tests for above utilities.

Two main testing strategies are:

  • runPipeline: round-trip invariant — render → parse → match → pass: true.
  • match helper with assertInvariant: verifies pass = true ⟺ resolved === expected on every test case.

How to read test cases

Each match(html, template) call returns three views. Example from 'mismatch text and regex match':

# html
<p>Changed</p>
<button aria-label="1234">Pattern</button>

# template
- paragraph: Original
- button /\d+/: Pattern
actual (input DOM rendered as aria tree):
  - paragraph: Changed        ← what the DOM actually has
  - button "1234": Pattern

actualResolved (DOM through template's lens):
  - paragraph: Changed        ← mismatch: literal DOM value
  - button /\d+/: Pattern     ← match: adopts regex from template

pass: false
  • actualResolved is what gets written on --update — note how /\d+/ survives from the template while Changed replaces Original.
  • actualResolved vs template is what the user sees in the diff. In this case, Vitest should be able to format it like:
    - - paragraph: Original    (from template)
    + - paragraph: Changed     (from actualResolved)
      - button /\d+/: Pattern
  • The invariant holds: pass = false and actualResolved !== expected (they differ on the paragraph line).

Integration with vitest

In vitest#9668, these utilities plug into a DomainSnapshotAdapter interface:

export const ariaDomainAdapter: DomainSnapshotAdapter<AriaNode, AriaTemplateRoleNode> = {
  capture(received)     generateAriaTree(received)
  render(captured)      renderAriaTree(captured)
  parseExpected(input)  parseAriaTemplate(input)
  match(captured, expected)  matchAriaTree(...)
}

@hi-ogawa
Copy link
Copy Markdown
Collaborator Author

This is finally ready. I ended up doing multiple iterations of both API and internal algorithm. A full context should be available in a huge comment in match.ts.

Copy link
Copy Markdown
Member

@sheremet-va sheremet-va left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just gonna trust you on this one 😄

The code looks good to me

@sheremet-va sheremet-va merged commit 7fb9aef into vitest-dev:master Mar 18, 2026
4 checks passed
@hi-ogawa hi-ogawa deleted the feat-aria branch March 18, 2026 23:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants