|

ARIA Snapshot Testing in Playwright with toMatchAriaSnapshot

Pixel-based screenshot tests break the instant a font renders one pixel differently, yet they tell you nothing about whether a screen reader can actually use your page. Playwright ARIA snapshot testing solves both problems at once: instead of comparing images, you assert against the accessibility tree your component exposes. In this guide you will learn how toMatchAriaSnapshot works, how to write and update YAML snapshots, how to match dynamic text with regex, and how to fit ARIA snapshots into a real TypeScript test suite.

Contents

What Is an ARIA Snapshot?

An ARIA snapshot is a YAML representation of the accessibility tree for a given element and its descendants. Every browser builds an accessibility tree from your DOM — the same structure assistive technologies like VoiceOver and NVDA consume. Playwright serializes a slice of that tree into readable YAML where each line is a node described by its ARIA role and its accessible name.

Here is what a snapshot of a small navigation region looks like. Notice it captures roles and names, not CSS, colors, or exact markup:

- banner:
  - heading "The Testing Academy" [level=1]
  - navigation:
    - link "Home"
    - link "Courses"
    - link "Blog"
  - button "Sign in"

Because the snapshot only encodes semantics, it is resilient to refactors. You can swap a <div> for a <section>, change class names, or reorder unrelated styling, and the test stays green — as long as the roles and names that users and assistive tech rely on remain intact. The moment a heading loses its text or a button stops being announced as a button, the snapshot fails. That is exactly the regression you want to catch.

Your First toMatchAriaSnapshot Test

The assertion lives on a locator: await expect(locator).toMatchAriaSnapshot(expected). The simplest form passes the expected YAML inline as a string. Playwright then resolves the locator, computes its current ARIA tree, and compares the two structurally.

import { test, expect } from '@playwright/test';

test('navbar exposes the right accessibility tree', async ({ page }) => {
  await page.goto('https://scrolltest.com/');

  await expect(page.getByRole('banner')).toMatchAriaSnapshot(`
    - banner:
      - heading "The Testing Academy" [level=1]
      - navigation:
        - link "Home"
        - link "Courses"
        - link "Blog"
      - button "Sign in"
  `);
});

A few rules make this far more pleasant to work with than image snapshots:

  • The match is a subset match by default for properties — extra attributes on a node do not fail the test, but missing roles and names do.
  • Child order matters. If the YAML lists link "Home" before link "Courses", the DOM must expose them in that order.
  • You do not have to describe the entire subtree. You can assert just the nodes you care about and Playwright will still verify they appear in the right structural position.
  • It is a web-first assertion, so it auto-retries until the tree matches or the timeout expires — no manual waits for async content.

Generating and Updating Snapshots Automatically

Hand-writing YAML for a complex component is tedious. Playwright will write it for you. Add an empty toMatchAriaSnapshot() call (or run with the update flag) and Playwright fills in the expected tree from the live page.

import { test, expect } from '@playwright/test';

test('capture the product card tree', async ({ page }) => {
  await page.goto('https://scrolltest.com/courses');

  // Run once with --update-snapshots and Playwright fills the YAML in.
  await expect(page.getByRole('article').first()).toMatchAriaSnapshot();
});

Run the suite with the update flag to populate or refresh the expected value:

npx playwright test product-card.spec.ts --update-snapshots

By default the generated YAML is written back inline into your test file. If you prefer the snapshot to live in a separate, version-controlled file, point the assertion at one with the name option. Playwright stores it under your snapshot directory as a .aria.yml file, which keeps large trees out of your test source and makes diffs in code review crisp.

test('checkout summary matches stored snapshot', async ({ page }) => {
  await page.goto('https://scrolltest.com/checkout');

  await expect(page.getByRole('region', { name: 'Order summary' }))
    .toMatchAriaSnapshot({ name: 'order-summary.aria.yml' });
});

One important habit: always read the generated YAML before committing it. The update flag captures whatever the page currently exposes, including any accessibility bugs that already exist. Treat the first generation as a draft you review, not gospel.

Matching Dynamic Content with Regex and Special Tokens

Real pages contain values that change every run — cart totals, timestamps, user names, item counts. Hard-coding them makes snapshots flaky. Playwright lets you embed regular expressions directly in the accessible-name slot using the /pattern/ syntax, so you match the shape of the text instead of its exact value.

import { test, expect } from '@playwright/test';

test('cart totals match regardless of exact amount', async ({ page }) => {
  await page.goto('https://scrolltest.com/cart');

  await expect(page.getByRole('region', { name: 'Cart' })).toMatchAriaSnapshot(`
    - region "Cart":
      - listitem:
        - text /Playwright Masterclass/
        - text /\\$\\d+\\.\\d{2}/
      - text /Total: \\$\\d+\\.\\d{2}/
      - button /Checkout \\(\\d+ items?\\)/
  `);
});

Beyond regex, the YAML grammar supports several tokens that express intent precisely. These let you assert on structure and ARIA state without pinning down volatile details:

Token / SyntaxMeaningExample
role "name"Node with an exact accessible namebutton "Submit"
role /regex/Accessible name matched by a regular expressiontext /\$\d+\.\d{2}/
[level=2]Heading level assertionheading "FAQ" [level=2]
[checked] / [expanded]ARIA state on the nodecheckbox "Agree" [checked]
[disabled]Element is disabled in the treebutton "Pay" [disabled]
/children: equal/Require an exact child list, no extras allowedSee strict-match section

Asserting interactive state

Because ARIA snapshots understand state, you can verify accessibility-critical behavior such as a toggle button announcing aria-expanded correctly, or a disabled pay button being announced as disabled. This is something a screenshot can never assert reliably.

test('accordion toggles expanded state accessibly', async ({ page }) => {
  await page.goto('https://scrolltest.com/faq');
  const trigger = page.getByRole('button', { name: 'Refund policy' });

  await expect(page.getByRole('region', { name: 'FAQ' })).toMatchAriaSnapshot(`
    - region "FAQ":
      - button "Refund policy" [expanded=false]
  `);

  await trigger.click();

  await expect(page.getByRole('region', { name: 'FAQ' })).toMatchAriaSnapshot(`
    - region "FAQ":
      - button "Refund policy" [expanded=true]
      - text /We offer a 30-day/
  `);
});

ARIA Snapshots vs Other Playwright Assertions

ARIA snapshots are not a replacement for every assertion — they are the right tool when you care about the overall semantic structure of a region. The table below positions them against the alternatives you already use.

ApproachComparesBest forWeakness
toHaveScreenshotRendered pixelsVisual styling, layout, themingBrittle across fonts, OS, GPU; ignores semantics
toMatchAriaSnapshotAccessibility tree (roles + names)Semantic structure, a11y regressions, refactor safetyDoes not catch pure visual/CSS changes
toHaveText / toContainTextText content of one locatorSingle-value content checksNo structural or role context
getByRole(...).toBeVisible()Presence of one elementSpot checks on a single controlVerbose for many nodes; no tree shape

A pragmatic split: use toMatchAriaSnapshot to lock down the shape and labels of whole components, keep a small number of toHaveScreenshot tests for purely visual concerns, and reserve getByRole spot assertions for the precise control a given test exercises.

Strict Matching and Common Pitfalls

By default an ARIA snapshot allows extra children that you did not list. That is forgiving, but sometimes you want to guarantee a list contains exactly the items you expect — for example, that a menu has no leftover or duplicated options after a filter. Add the /children: equal/ directive to a container to enforce an exact child set.

test('filtered menu contains exactly these options', async ({ page }) => {
  await page.goto('https://scrolltest.com/menu');
  await page.getByRole('searchbox').fill('play');

  await expect(page.getByRole('listbox')).toMatchAriaSnapshot(`
    - listbox:
      - /children: equal
      - option "Playwright Basics"
      - option "Playwright Advanced"
  `);
});

As you adopt ARIA snapshot testing, watch out for these recurring traps:

  • Snapshotting too large a region. A snapshot of page.locator('body') is noisy and breaks on unrelated changes. Scope each assertion to a meaningful landmark such as a banner, navigation, or named region.
  • Committing accessibility bugs. The update flag captures missing names and wrong roles too. If a button serializes as button with no name, fix the markup — do not bake the empty name into your snapshot.
  • Forgetting the YAML is order-sensitive for children. Re-ordering DOM nodes is a real, intentional-or-not change, and the snapshot will flag it.
  • Escaping regex inside template literals. Inside a JavaScript template string, backslashes must be doubled or moved into a separate .aria.yml file where raw regex reads cleanly.
  • Treating it as a visual test. A color or spacing change will not fail an ARIA snapshot — pair it with a screenshot test if visual fidelity matters.

Conclusion

Playwright ARIA snapshot testing gives you assertions that are simultaneously more stable than screenshots and more meaningful than one-off text checks. By comparing the accessibility tree with toMatchAriaSnapshot, your tests verify the exact contract that screen-reader users depend on, survive cosmetic refactors, and surface real regressions the moment a role or name drifts. Start by snapshotting one important landmark per page, generate the YAML with --update-snapshots, review it like code, and reach for regex and state tokens to keep dynamic content stable. Done well, ARIA snapshots become the backbone of a fast, accessibility-first regression suite.

FAQ

Is toMatchAriaSnapshot a visual or accessibility test?

It is an accessibility-structure test, not a visual one. It compares the browser’s accessibility tree — ARIA roles, accessible names, and states — rather than rendered pixels. CSS, color, and spacing changes will not fail it, so pair it with toHaveScreenshot when you also need to guard the visual appearance.

How do I update an ARIA snapshot after intentional UI changes?

Run your tests with the update flag: npx playwright test --update-snapshots. Playwright regenerates the expected YAML inline in your spec, or in the linked .aria.yml file when you used the name option. Always review the regenerated YAML in your diff before committing, since the update captures whatever the page currently exposes — including any accessibility bugs.

Can I match dynamic text like prices or timestamps?

Yes. Use the /regex/ syntax in the accessible-name slot, for example text /\$\d+\.\d{2}/ to match any currency amount. This asserts the shape of the content instead of its exact value, which keeps snapshots stable across runs with changing totals, dates, or user names. Remember to double-escape backslashes inside JavaScript template literals.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.