ARIA Snapshot Testing with Playwright

Permalink to "ARIA Snapshot Testing with Playwright"

Most accessibility regressions are not dramatic. A refactor wraps a cell’s content in a new component and the cell’s name becomes empty; a library upgrade turns aria-sort into a data- attribute; a header loses its scope. Axe catches some of these; many slip through because the result is still valid ARIA, just wrong. Playwright’s ARIA snapshots record the accessibility tree of a region as readable YAML — roles, names, states — and fail when it changes. For data grids, whose value depends on precise structure, they are an efficient safety net.

This guide belongs to accessibility tree & API inspection.

Spec reference

Permalink to "Spec reference"
  • Playwright toMatchAriaSnapshot (v1.49+) — compares a locator’s accessibility tree with a YAML template. Templates list roles with names in quotes, nested by indentation, with attributes like [checked], [expanded], [selected], [level=2]. Names can be regular expressions (/Showing \d+ rows/).
  • Partial matching — by default, a template matches if the listed nodes exist in that order; unlisted nodes are allowed. Strict children matching is opt-in.
  • Relevant criteria: SC 1.3.1, SC 4.1.2 (A), and SC 4.1.3 (AA) for status text.
Unit assertions versus ARIA snapshots Comparison: individual unit assertions check a few attributes and miss structural regressions; an ARIA snapshot captures the whole computed structure of a region in readable form. Unit assertions versus ARIA snapshots✗ Individual assertionsChecks attributes you thought ofMisses wrapper changesBrittle to markup changesHard to review intent✓ ARIA snapshotCaptures roles, names and states of the regionDetects new or missing nodesRobust: based on the accessibility treeYAML diff shows the user-facing change

When to use — and when not to

Permalink to "When to use — and when not to"

Snapshot the regions whose structure matters most: the grid header row and a representative body row, the toolbar, pagination and status messages, in each important state (default, sorted, filtered, row expanded, editing).

Do not snapshot entire pages with dynamic content; diffs become noisy and reviewers stop reading them. Do not use snapshots instead of keyboard and screen reader testing — they verify structure, not behaviour or announcements.

The misapplication: regenerating snapshots on every failure with --update-snapshots without reading the diff, which turns the safety net into a rubber stamp.

An ARIA snapshot of a sorted grid Hierarchy of the snapshot template: grid named Invoices containing a header row with column headers (Amount marked sorted descending), and a body row with a row header and cells; plus a status message with a pattern. An ARIA snapshot of a sorted gridgrid "Invoices"the region under testrow (header)columnheaders in ordercolumnheader "Invoice"columnheader "Amount" [sort=descending]sort state capturedrow "INV-1042 …"first body rowrowheader "INV-1042"gridcell "1,240.00"value as namestatus "/Sorted by Amount, descending/"regex for dynamic text
Readable structure: reviewers can see exactly what screen readers get.

Annotated code example

Permalink to "Annotated code example"
import { test, expect } from '@playwright/test';

test('invoice grid structure when sorted', async ({ page }) => {
  await page.goto('/invoices?fixture=small');
  await page.getByRole('button', { name: 'Amount' }).click();

  await expect(page.getByRole('grid', { name: 'Invoices' })).toMatchAriaSnapshot(`
    - grid "Invoices":
      - rowgroup:
        - row:
          - columnheader "Invoice"
          - columnheader "Customer"
          - columnheader "Amount"
      - rowgroup:
        - row /INV-1042/:
          - rowheader "INV-1042"
          - gridcell "Lund & Co"
          - gridcell "1,240.00"
  `);

  // Status message captured with a pattern (SC 4.1.3)
  await expect(page.getByRole('status')).toMatchAriaSnapshot(`
    - status: /Sorted by Amount, descending/
  `);

  // Sort state is an attribute snapshots may not show in your Playwright version: assert it directly
  await expect(page.getByRole('columnheader', { name: 'Amount' })).toHaveAttribute('aria-sort', 'descending');
});

Generate the first template with npx playwright test --update-snapshots or the codegen “Assert snapshot” tool, then edit it down to the nodes that matter and replace volatile values with regular expressions.

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"

Snapshots do not press keys; combine them with keyboard steps and focus assertions:

Step Assertion
Tab into grid await expect(page.getByRole('gridcell').first()).toBeFocused()
ArrowDown Snapshot unchanged; focus on next row’s cell
Enter on expandable row Snapshot shows [expanded] and child rows
Escape in editor Snapshot shows cell value restored
What ARIA snapshots catch Matrix of regression types — lost cell names, changed roles, missing headers, wrong expanded state, wrong live region text, focus order bugs — against whether snapshots, axe and focus assertions catch them. What ARIA snapshots catchARIA snapshotaxe-coreFocus assertionsLost cell namesYesSometimesNoChanged rolesYesIf invalidNoMissing headersYesTables onlyNoWrong expanded stateYesNoNoWrong status textYesNoNoFocus order bugsNoNoYes
Snapshots fill the gap between axe and behaviour tests.

Integration context

Permalink to "Integration context"

Snapshots complement testing library role queries for grids at the unit level and testing grid keyboard navigation with Playwright for behaviour. When a snapshot changes unexpectedly, investigate with inspecting the accessibility tree in Chrome DevTools.

Gotchas

Permalink to "Gotchas"

Virtualized rows. Only rendered rows appear. Use small fixtures or snapshot the first rows only.

Locale and time. Dates and numbers change with locale and time zone; fix both in the test configuration or use patterns.

Attribute coverage. Snapshots include common states (checked, expanded, selected, pressed, level, disabled) but not every ARIA attribute; assert others directly.

Common mistakes

Permalink to "Common mistakes"
  • Snapshotting whole pages.
  • Blindly updating snapshots.
  • Using live data instead of fixtures.
  • Treating snapshots as behaviour tests.

Framework notes

Permalink to "Framework notes"

ARIA snapshots work with any framework because they read the browser’s tree. In component testing (Playwright CT for React, Vue or Svelte), snapshot mounted components directly. Storybook users can run Playwright against story URLs to snapshot each story’s region.

// Playwright component test (React)
const component = await mount(<InvoiceGrid rows={fixture} />);
await expect(component.getByRole('grid')).toMatchAriaSnapshot(`- grid "Invoices"`);

Worked example

Permalink to "Worked example"

A team migrated its grid from one component library to another. The migration passed all unit tests and axe checks. ARIA snapshots, added a month earlier, failed on 14 states: row headers had become plain cells, the status region was gone, and expanded rows no longer exposed aria-expanded. The diffs showed exactly what had changed, and the team restored the semantics in custom renderers before the release instead of discovering them through user complaints.

Writing good templates

Permalink to "Writing good templates"

Good templates are short and intentional. Include the nodes a user depends on — the grid name, header cells in order, the first row’s header and a couple of cells, toolbar buttons with names, the status message — and leave out everything else. Replace values that change with patterns, and use fixtures so values that should not change do not. A reviewer should be able to read the template and say what a screen reader user gets from the region. If they cannot, the template is too big.

Testing checklist

Permalink to "Testing checklist"

Design system notes

Permalink to "Design system notes"

Publish canonical ARIA snapshot templates in component documentation. Product teams can reuse them as tests, and reviewers can compare product output with the reference.

FAQ

Permalink to "FAQ"
Are ARIA snapshots stable?

More stable than DOM snapshots, because they ignore wrappers and classes. Use patterns for dynamic text.

Do they replace axe?

No. Axe checks rules; snapshots check that structure matches expectations. Use both.

How big should a snapshot be?

Small enough to review: a header row, one or two body rows and nearby controls.

Can I snapshot live regions?

Yes — the status element’s text appears in the snapshot. Timing of announcements still needs separate testing.

What Playwright version is needed?

toMatchAriaSnapshot was introduced in Playwright 1.49; later versions added features. Use a current version.

Can Cypress do this?

Not natively; cypress-axe covers rules, and Testing Library queries cover names and roles.

How do snapshots handle translations?

Run snapshot tests in one locale, or keep templates per locale for the languages you ship. Patterns for numbers and dates avoid most locale churn.

Where should snapshot templates live?

Inline in the test for small regions, or in separate files Playwright manages for larger ones. Inline templates are easier to review in pull requests.

Permalink to "Related"

← Back to Accessibility Tree & API Inspection