From 234e0c0aca17fffe41580878605ef8911b8e75ad Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 9 Apr 2026 15:53:34 +0900 Subject: [PATCH 1/4] docs(snapshot): gotchas for custom async inline snaphsot matcher (#10107) Co-authored-by: Claude Sonnet 4.6 --- guide/snapshot.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/guide/snapshot.md b/guide/snapshot.md index 91ed8628..434abb8f 100644 --- a/guide/snapshot.md +++ b/guide/snapshot.md @@ -261,6 +261,26 @@ For inline snapshot matchers, the snapshot argument must be the last parameter ( File snapshot matchers must be `async` — `toMatchFileSnapshot` returns a `Promise`. Remember to `await` the result in the matcher and in your test. ::: +::: warning +When custom inline snapshot matcher is aynchronous, Vitest cannot automatically infer the call location for inline snapshot rewriting. You must capture the call site by setting the `'error'` flag on the chai assertion object: + +```ts +import { expect, chai, Snapshots } from 'vitest' + +const { toMatchInlineSnapshot } = Snapshots + +expect.extend({ + async toMatchTransformedInlineSnapshot(received: string, inlineSnapshot?: string) { + // capture call site synchronously at the top of matcher implementation + chai.util.flag(this.assertion, 'error', new Error()) + const transformed = await transform(received) + return toMatchInlineSnapshot.call(this, transformed, inlineSnapshot) + }, +}) +``` + +::: + For TypeScript, extend the `Assertion` interface: ```ts From 66e8626c531b6401fc0ef6dec583c739ba1e7a76 Mon Sep 17 00:00:00 2001 From: yugo innami <58389827+nami8824@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:56:57 +0900 Subject: [PATCH 2/4] feat(reporter): add filterMeta option to json reporter (#10078) Co-authored-by: Vladimir Sheremet --- guide/reporters.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/guide/reporters.md b/guide/reporters.md index 95612f07..72a86146 100644 --- a/guide/reporters.md +++ b/guide/reporters.md @@ -417,6 +417,20 @@ Example of a JSON report: Since Vitest 3, the JSON reporter includes coverage information in `coverageMap` if coverage is enabled. ::: +The `meta` field in each assertion result can be filtered via the `filterMeta` reporter option. It receives the key and value of each field and should return a falsy value to exclude the field from the report: + +```ts +export default defineConfig({ + test: { + reporters: [ + ['json', { + filterMeta: (key, value) => key !== 'internalField', + }] + ] + }, +}) +``` + ### HTML Reporter Generates an HTML file to view test results through an interactive [GUI](/guide/ui). After the file has been generated, Vitest will keep a local development server running and provide a link to view the report in a browser. From 5755951bae9c1f29ead80a0df48c81f2d0a46248 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 9 Apr 2026 16:23:59 +0900 Subject: [PATCH 3/4] feat(experimental): support aria snapshot (#9668) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Ari Perkkiö Co-authored-by: Codex Co-authored-by: Vladimir --- .vitepress/config.ts | 5 + api/expect.md | 37 +++ guide/browser/aria-snapshots.md | 475 ++++++++++++++++++++++++++++++++ guide/snapshot.md | 218 +++++++++++++++ 4 files changed, 735 insertions(+) create mode 100644 guide/browser/aria-snapshots.md diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 96505b69..67c88513 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -806,6 +806,11 @@ export default ({ mode }: { mode: string }) => { link: '/guide/browser/trace-view', docFooterText: 'Trace View | Browser Mode', }, + { + text: 'ARIA Snapshots', + link: '/guide/browser/aria-snapshots', + docFooterText: 'ARIA Snapshots | Browser Mode', + }, ], }, { diff --git a/api/expect.md b/api/expect.md index c62de312..48374392 100644 --- a/api/expect.md +++ b/api/expect.md @@ -1010,6 +1010,43 @@ The same as [`toMatchSnapshot`](#tomatchsnapshot), but expects the same value as The same as [`toMatchInlineSnapshot`](#tomatchinlinesnapshot), but expects the same value as [`toThrow`](#tothrow). +## toMatchAriaSnapshot 4.1.4 {#tomatcharisnapshot} + +- **Type:** `() => void` + +Captures the accessibility tree of a DOM element and generate a snapshot file or compares it against a stored snapshot. See the [ARIA Snapshots guide](/guide/browser/aria-snapshots) for more details. + +```ts +import { expect, test } from 'vitest' + +test('navigation accessibility', () => { + document.body.innerHTML = ` + + ` + expect(document.querySelector('nav')).toMatchAriaSnapshot() +}) +``` + +## toMatchAriaInlineSnapshot 4.1.4 {#tomatchariainlinesnapshot} + +- **Type:** `(snapshot?: string) => void` + +Same as [`toMatchAriaSnapshot`](#tomatcharisnapshot), but stores the snapshot inline in the test file. See the [ARIA Snapshots guide](/guide/browser/aria-snapshots) for more details. + +```ts +import { expect, test } from 'vitest' + +test('user profile', () => { + expect(document.body).toMatchAriaInlineSnapshot(` + - heading "Dashboard" [level=1] + - button /User \\d+/: Profile + `) +}) +``` + ## toHaveBeenCalled - **Type:** `() => Awaitable` diff --git a/guide/browser/aria-snapshots.md b/guide/browser/aria-snapshots.md new file mode 100644 index 00000000..d4dee825 --- /dev/null +++ b/guide/browser/aria-snapshots.md @@ -0,0 +1,475 @@ +--- +title: ARIA Snapshots | Guide +outline: deep +--- + +# ARIA Snapshots experimental 4.1.4 + +ARIA snapshots let you test the accessibility structure of your pages. Instead of asserting against raw HTML or visual output, you assert against the accessibility tree — the same structure that screen readers and other assistive technologies use. + +Given this HTML: + +```html + +``` + +You can assert its accessibility tree: + +```ts +await expect.element(page.getByRole('navigation')).toMatchAriaInlineSnapshot(` + - navigation "Main": + - link "Home": + - /url: / + - link "About": + - /url: /about +`) +``` + +This catches accessibility regressions: missing labels, broken roles, incorrect heading levels, and more — things that DOM snapshots would miss. Even if the underlying HTML structure changes, the assertion would not fail as long as content matches semantically. + +## Snapshot Workflow + +ARIA snapshots use the same Vitest snapshot workflow as other snapshot assertions. File snapshots, inline snapshots, `--update` / `-u`, watch mode updates, and CI snapshot behavior all work the same way. + +See the main [Snapshot guide](/guide/snapshot) for the general snapshot workflow, update behavior, and review guidelines. + +## Basic Usage + +Given a page with this HTML: + +```html +
+ + + +
+``` + +### File Snapshots + +Use `toMatchAriaSnapshot()` to store the snapshot in a `.snap` file alongside your test: + +```ts [basic.test.ts] +import { expect, test } from 'vitest' + +test('login form', async () => { + await expect.element(page.getByRole('form')).toMatchAriaSnapshot() +}) +``` + +On first run, Vitest generates a snapshot file entry: + +```js [__snapshots__/basic.test.ts.snap] +// Vitest Snapshot ... + +exports[`login form 1`] = ` +- form "Log In": + - textbox "Email" + - textbox "Password" + - button "Submit" +` +``` + +### Inline Snapshots + +Use `toMatchAriaInlineSnapshot()` to store the snapshot directly in the test file: + +```ts +import { expect, test } from 'vitest' + +test('login form', async () => { + await expect.element(page.getByRole('form')).toMatchAriaInlineSnapshot(` + - form "Log In": + - textbox "Email" + - textbox "Password" + - button "Submit" + `) +}) +``` + +## Browser Mode Retry Behavior + +In [Browser Mode](/guide/browser/), `expect.element()` polls the DOM and waits for the accessibility tree to **stabilize** before evaluating the result. On each poll, the matcher re-queries the element and re-captures the accessibility tree. The snapshot is considered stable when two consecutive polls produce the same output. + +```ts +await expect.element(page.getByRole('form')).toMatchAriaInlineSnapshot(` + - form "Log In": + - textbox "Email" + - textbox "Password" + - button "Submit" +`) +``` + +On first run or with `--update`, the stable result is written as the new snapshot. + +When an existing snapshot is present, the matcher also checks whether the stable result matches. If it does not, polling resets and continues — giving the DOM time to reach the expected state. This handles cases like animations, async rendering, or delayed state updates where the tree may briefly stabilize in an intermediate state before settling into its final form. + +## Preserving Hand-Edited Patterns + +When you hand-edit a snapshot to use regex patterns, those patterns survive `--update`. Only the literal parts that changed are overwritten. This lets you write flexible assertions that don't break when content changes. + +### Example + +**Step 1.** Your shopping cart page renders this HTML: + +```html +

Your Cart

+
    +
  • Wireless Headphones — $79.99
  • +
+ +``` + +You run your test for the first time with `--update`. Vitest generates the snapshot: + +```yaml +- heading "Your Cart" [level=1] +- list "Cart Items": + - listitem: Wireless Headphones — $79.99 +- button "Checkout" +``` + +**Step 2.** The item names and prices are seeded test data that may change. You hand-edit those lines to regex patterns, but keep the stable structure as literals: + +```yaml +- heading "Your Cart" [level=1] +- list "Cart Items": + - listitem: /.+ — \$\d+\.\d+/ +- button "Checkout" +``` + +**Step 3.** Later, a developer renames the button from "Checkout" to "Place Order". Running `--update` updates that literal but preserves your regex patterns: + +```yaml +- heading "Your Cart" [level=1] +- list "Cart Items": + - listitem: /.+ — \$\d+\.\d+/ +- button "Place Order" 👈 New snapshot updated with new string +``` + +The regex patterns you wrote in step 2 are preserved because they still match the actual content. Only the mismatched literal "Checkout" was updated to "Place Order". + +## Snapshot Format + +ARIA snapshots use a YAML-like syntax. Each line represents a node in the accessibility tree. + +::: info +ARIA snapshot templates use a **subset of YAML** syntax. Only the features needed for accessibility trees are supported: scalar values, nested mappings via indentation, and sequences (`- item`). Advanced YAML features like anchors, tags, flow collections, and multi-line scalars are not supported. + +Captured text is also whitespace-normalized before it is rendered into the snapshot. Newlines, `
` line breaks, tabs, and repeated whitespace collapse to single spaces, so multi-line DOM text is emitted as a single-line snapshot value. +::: + +Each accessible element in the tree is represented as a YAML node: + +```yaml +- role "name" [attribute=value] +``` + +- `role`: The ARIA role of the element, such as `heading`, `list`, `listitem`, or `button` +- `"name"`: The [accessible name](https://w3c.github.io/accname/), when present. Quoted strings match exact values, and `/patterns/` match regular expressions +- `[attribute=value]`: Accessibility states and properties such as `checked`, `disabled`, `expanded`, `level`, `pressed`, or `selected` + +These values come from ARIA attributes and the browser's accessibility tree, including semantics inferred from native HTML elements. + +Because ARIA snapshots reflect the browser's accessibility tree, content excluded from that tree, such as `aria-hidden="true"` or `display: none`, does not appear in the snapshot. + +### Roles and Accessible Names + +For example: + +```html + +

Welcome

+Home + +``` + +```yaml +- button "Submit" +- heading "Welcome" [level=1] +- link "Home" +- textbox "Email" +``` + +The role usually comes from the element's native semantics, though it can also be defined with ARIA. The accessible name is computed from text content, associated labels, `aria-label`, `aria-labelledby`, and related naming rules. + +For a closer look at how names are computed, see [Accessible Name and Description Computation](https://w3c.github.io/accname/). + +Some content appears in the snapshot as a text node instead of a role-based element: + +```html +Hello world +``` + +```yaml +- text: Hello world +``` + +Text values are always serialized on a single line after whitespace normalization. For example: + +```html +

+Line 1 +Line 2
Line 3 +Line 4 +

+``` + +```yaml +- paragraph: Line 1 Line 2 Line 3 Line 4 +``` + +### Children + +Child elements appear nested under their parent: + +```html +
    +
  • First
  • +
  • Second
  • +
  • Third
  • +
+``` + +```yaml +- list: + - listitem: First + - listitem: Second + - listitem: Third +``` + +If the parent has an accessible name, the snapshot includes it before the nested children: + +```html + +``` + +```yaml +- navigation "Main": + - link "Home" + - link "About" +``` + +If an element only contains a single text child and has no other properties, the text is rendered inline: + +```html +

Hello world

+``` + +```yaml +- paragraph: Hello world +``` + +### Attributes + +ARIA states and properties appear in brackets: + +| HTML | Snapshot | +| ---------------------------------------------------------------------- | ----------------------------------------- | +| `` | `- checkbox "Agree" [checked]` | +| `` | `- checkbox "Select all" [checked=mixed]` | +| `` | `- button "Submit" [disabled]` | +| `` | `- button "Menu" [expanded]` | +| `

Title

` | `- heading "Title" [level=2]` | +| `` | `- button "Bold" [pressed]` | +| `` | `- button "Bold" [pressed=mixed]` | +| `` | `- option "English" [selected]` | + +Attributes only appear when they are active. A button that is not disabled simply has no `[disabled]` attribute — there is no `[disabled=false]`. + +### Pseudo-Attributes + +Some DOM properties that aren't part of ARIA but are useful for testing are exposed with a `/` prefix: + +#### `/url:` + +Links include their URL: + +```html +Home +``` + +```yaml +- link "Home": + - /url: / +``` + +#### `/placeholder:` + +Textboxes can include their placeholder text: + +```html + +``` + +```yaml +- textbox "Email": + - /placeholder: user@example.com +``` + +::: tip When does `/placeholder:` appear? + +The `/placeholder:` pseudo-attribute only appears when the placeholder text is **different from the accessible name**. When an input has a placeholder but no `aria-label` or associated `