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.
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.
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 |
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.
Related
Permalink to "Related"- Testing Library role queries — unit-level structure checks
- Grid keyboard navigation with Playwright — behaviour checks
- Chrome DevTools accessibility tree — investigating diffs