Skip to content
Draft
Show file tree
Hide file tree
Changes from 88 commits
Commits
Show all changes
120 commits
Select commit Hold shift + click to select a range
5d3c2ab
feat: aria snapshot
hi-ogawa Feb 15, 2026
986b451
feat: toMatchDomainInlineSnapshot
hi-ogawa Feb 15, 2026
5c9458d
feat: toMatchDomainSnapshot
hi-ogawa Feb 15, 2026
9e53df4
wip
hi-ogawa Feb 15, 2026
d965f3f
chore: design doc
hi-ogawa Feb 15, 2026
4df273d
Merge branch 'main' into feat-aria-snapshot
hi-ogawa Mar 11, 2026
54696d7
wip: revert slop
hi-ogawa Mar 11, 2026
1a2cdb2
test: wip
hi-ogawa Mar 11, 2026
0066bc0
wip: comment out more slop
hi-ogawa Mar 11, 2026
099cdd3
wip: custom match
hi-ogawa Mar 11, 2026
36a25a9
wip: aria
hi-ogawa Mar 11, 2026
011712e
wip: aria prototype
hi-ogawa Mar 11, 2026
9ab69f6
wip: aria domain
hi-ogawa Mar 11, 2026
832e4e1
wip: more aria
hi-ogawa Mar 11, 2026
3fccafe
docs: design
hi-ogawa Mar 11, 2026
e99d081
wip: docs
hi-ogawa Mar 11, 2026
477e642
test/snapshots/README.md
hi-ogawa Mar 11, 2026
83f9240
chore: cleanup
hi-ogawa Mar 11, 2026
0a75d63
test: split
hi-ogawa Mar 11, 2026
9158aad
test: gitignore
hi-ogawa Mar 11, 2026
dadbbe1
test: wip
hi-ogawa Mar 11, 2026
4716df1
test: wip
hi-ogawa Mar 11, 2026
7767628
test: wip
hi-ogawa Mar 11, 2026
f7e15ae
test: wip
hi-ogawa Mar 11, 2026
8cd7617
feat: partial update
hi-ogawa Mar 11, 2026
b183e4c
test: diff
hi-ogawa Mar 11, 2026
77282a9
design docs
hi-ogawa Mar 11, 2026
38e0cb1
refactor: remove slop `DomainMatchResult.mismatches`
hi-ogawa Mar 11, 2026
055f912
test: aria
hi-ogawa Mar 11, 2026
afe6d6a
design docs: inline
hi-ogawa Mar 11, 2026
0e1aa40
wip: inline
hi-ogawa Mar 11, 2026
2726322
test: inline e2e
hi-ogawa Mar 11, 2026
cc089ba
design docs: aria
hi-ogawa Mar 11, 2026
fc97217
feat: toMatchAriaSnapshot
hi-ogawa Mar 11, 2026
d481ef7
refactor: remove slop
hi-ogawa Mar 11, 2026
0e41364
test: more
hi-ogawa Mar 11, 2026
ce20c13
test: simplify
hi-ogawa Mar 11, 2026
009ec90
test: domain-aria-inline
hi-ogawa Mar 11, 2026
6208a2a
wip: aria better match
hi-ogawa Mar 11, 2026
259dbf2
feat: aria better match
hi-ogawa Mar 11, 2026
b6897de
test: rename
hi-ogawa Mar 11, 2026
ae5c302
test: update
hi-ogawa Mar 11, 2026
62670e9
test: update
hi-ogawa Mar 11, 2026
d62e9fd
chore: lint
hi-ogawa Mar 11, 2026
b4c123d
design docs: poll + snapshot via domain model
hi-ogawa Mar 11, 2026
580db2e
design docs: more on poll + snapshot
hi-ogawa Mar 11, 2026
6ebeaf5
wip: poll + domain snapshot
hi-ogawa Mar 11, 2026
b730f3a
wip: more on poll + snapshot
hi-ogawa Mar 11, 2026
3a214b9
test: more poll snapshot
hi-ogawa Mar 11, 2026
2d3fad9
wip: poll snapshot
hi-ogawa Mar 11, 2026
b78777e
wip: poll snapshot
hi-ogawa Mar 11, 2026
79b9748
wip: poll snapshot with timeout
hi-ogawa Mar 11, 2026
437ff9c
test: more
hi-ogawa Mar 11, 2026
700db56
chore: cleanup
hi-ogawa Mar 11, 2026
fea46b3
test: move code
hi-ogawa Mar 11, 2026
0ad314f
test: update snapshot
hi-ogawa Mar 12, 2026
f348173
test: browser mode + aria
hi-ogawa Mar 12, 2026
eb312df
refactor: consolidate four domain snapshots
hi-ogawa Mar 12, 2026
65533b4
test: wip poll + snapshot inline
hi-ogawa Mar 12, 2026
2eddd71
fix: poll + domain inline snapshot
hi-ogawa Mar 12, 2026
a52b717
test: poll domain inline snapshot e2e
hi-ogawa Mar 12, 2026
0396ce2
test: browser mode + aria
hi-ogawa Mar 12, 2026
ec5b402
fix: webkit curse
hi-ogawa Mar 12, 2026
059b421
chore: jsdoc experimental
hi-ogawa Mar 12, 2026
5d5bab4
refactor: remove slop
hi-ogawa Mar 12, 2026
a70ffe8
docs: aria snapshot
hi-ogawa Mar 12, 2026
2fcb89f
docs: experimental domain snapshot
hi-ogawa Mar 12, 2026
392b143
Merge branch 'main' into feat-aria-snapshot
hi-ogawa Mar 12, 2026
f3c40d3
docs: tweak
hi-ogawa Mar 12, 2026
9766a9a
refactor: remove slop
hi-ogawa Mar 12, 2026
7b8da57
chore: remove slop
hi-ogawa Mar 12, 2026
2dbaa4f
refactor: remove slop
hi-ogawa Mar 12, 2026
1bf5d5f
docs: update
hi-ogawa Mar 12, 2026
4cf4112
test: more coverage
hi-ogawa Mar 12, 2026
4df4fa5
fix: more aria
hi-ogawa Mar 12, 2026
f1b1242
test: more snapshot
hi-ogawa Mar 12, 2026
f7493c7
test: more aria
hi-ogawa Mar 12, 2026
6fb0517
chore: remove ai docs
hi-ogawa Mar 12, 2026
f2eae4c
fix: better aria match
hi-ogawa Mar 12, 2026
9b696b0
test: organize
hi-ogawa Mar 12, 2026
7aeccb8
fix: better aria udpate
hi-ogawa Mar 12, 2026
dfb90e3
test: more
hi-ogawa Mar 12, 2026
2027604
fix: aria psheudo attribute (link url, input placeholder)
hi-ogawa Mar 12, 2026
cd3fa2e
test: pseudo attribute with children
hi-ogawa Mar 12, 2026
4dbfa4a
chore: cleanup
hi-ogawa Mar 12, 2026
a55a1dc
chore: todo
hi-ogawa Mar 12, 2026
e9e8082
fix: inline aria inside loop
hi-ogawa Mar 12, 2026
ca0abbc
refactor: consolidate SnapshotState.match/matchDomain
hi-ogawa Mar 12, 2026
c38d008
chore: rename
hi-ogawa Mar 12, 2026
3cc7204
fix: replace Date.now with performance.now for measuring deadline
hi-ogawa Mar 12, 2026
4ae35b0
chore: move code
hi-ogawa Mar 12, 2026
2b47c4d
test: ws endpoint implies headless
hi-ogawa Mar 12, 2026
e24dbee
test: wip browser mode
hi-ogawa Mar 12, 2026
027213d
test: cleanup
hi-ogawa Mar 12, 2026
4ee88c7
fix: fix poll inline snapshot on firefox
hi-ogawa Mar 12, 2026
5afece7
test: full browser test
hi-ogawa Mar 12, 2026
1e0cc18
test: fix errorProjectTree
hi-ogawa Mar 12, 2026
d13a9fa
test: rolldown
hi-ogawa Mar 13, 2026
66dcfa5
test: snapshot for win
hi-ogawa Mar 13, 2026
1018ba9
test: more snapshot
hi-ogawa Mar 13, 2026
c3d48d5
test: rename
hi-ogawa Mar 13, 2026
4cd6546
test: consolidate
hi-ogawa Mar 13, 2026
48e9383
test: consolidate more
hi-ogawa Mar 13, 2026
f400313
test: more
hi-ogawa Mar 13, 2026
3328d9e
test: more
hi-ogawa Mar 13, 2026
7a9f42e
test: tweak
hi-ogawa Mar 13, 2026
0a7fde5
test: consolidate
hi-ogawa Mar 13, 2026
992e9e2
test: more
hi-ogawa Mar 13, 2026
bf6603d
test: more
hi-ogawa Mar 13, 2026
b040f37
Merge branch 'main' into feat-aria-snapshot
hi-ogawa Mar 13, 2026
b00490a
chore: comment
hi-ogawa Mar 13, 2026
666b46b
chore: more aria plan
hi-ogawa Mar 13, 2026
fa99722
chore: unused
hi-ogawa Mar 13, 2026
192d5aa
docs: aria.md
hi-ogawa Mar 13, 2026
de348ab
docs: wip
hi-ogawa Mar 13, 2026
26d1db7
docs: wip
hi-ogawa Mar 13, 2026
d921fae
docs: polish
hi-ogawa Mar 13, 2026
1b90efe
docs: wip
hi-ogawa Mar 13, 2026
f8414fe
docs: don't teach aria spec itself
hi-ogawa Mar 13, 2026
e25e29b
docs: refine aria snapshots guide
hi-ogawa Mar 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions docs/api/expect.md
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,75 @@ 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 {#tomatcharisnapshot}

- **Type:** `() => void`

Captures the accessibility tree of a DOM element and compares it against a stored snapshot. Inspired by [Playwright's aria snapshots](https://playwright.dev/docs/aria-snapshots).

The snapshot uses a YAML-like format describing the accessible roles, names, and states of the element tree.

```ts
import { expect, test } from 'vitest'

test('navigation accessibility', () => {
document.body.innerHTML = `
<nav aria-label="Actions">
<button>Save</button>
<button>Cancel</button>
</nav>
`
expect(document.querySelector('nav')).toMatchAriaSnapshot()
})
```

On first run, Vitest generates a snapshot entry like:

```
- navigation "Actions":
- button: Save
- button: Cancel
```

See the [Aria Snapshots guide](/guide/snapshot#aria-snapshots) for more details.

## toMatchAriaInlineSnapshot {#tomatchariaInlinesnapshot}

- **Type:** `(snapshot?: string) => void`

Same as [`toMatchAriaSnapshot`](#tomatcharisnapshot), but stores the snapshot inline in the test file.

```ts
import { expect, test } from 'vitest'

test('user profile', () => {
expect(document.body).toMatchAriaInlineSnapshot(`
- heading "Dashboard" [level=1]
- button /User \\d+/: Profile
`)
})
```

## toMatchDomainSnapshot <Badge type="warning">experimental</Badge> {#tomatchdomainsnapshot}

- **Type:** `(domain: string, hint?: string) => void`

Matches a value against a stored snapshot using a registered [domain snapshot adapter](/guide/snapshot#custom-snapshot-domain). The `domain` argument is the adapter's `name`.

```ts
expect(value).toMatchDomainSnapshot('my-domain')
```

## toMatchDomainInlineSnapshot <Badge type="warning">experimental</Badge> {#tomatchdomaininlinesnapshot}

- **Type:** `(snapshot: string, domain: string, hint?: string) => void`

Same as [`toMatchDomainSnapshot`](#tomatchdomainsnapshot), but stores the snapshot inline in the test file.

```ts
expect(value).toMatchDomainInlineSnapshot(`...`, `my-domain`)
```

## toHaveBeenCalled

- **Type:** `() => Awaitable<void>`
Expand Down Expand Up @@ -2116,6 +2185,19 @@ If you are adding custom serializers, you should call this method inside [`setup
If you previously used Vue CLI with Jest, you might want to install [jest-serializer-vue](https://npmx.dev/package/jest-serializer-vue). Otherwise, your snapshots will be wrapped in a string, which cases `"` to be escaped.
:::

## expect.addSnapshotDomain <Badge type="warning">experimental</Badge> {#expect-addsnapshotdomain}

- **Type:** `(adapter: DomainSnapshotAdapter) => void`

Registers a [domain snapshot adapter](/guide/snapshot#custom-snapshot-domain) for use with `toMatchDomainSnapshot` and `toMatchDomainInlineSnapshot`. Call this in [`setupFiles`](/config/setupfiles).

```ts
import { expect } from 'vitest'
import { kvAdapter } from './kv-adapter'

expect.addSnapshotDomain(kvAdapter)
```

## expect.extend

- **Type:** `(matchers: MatchersObject) => void`
Expand Down
200 changes: 200 additions & 0 deletions docs/guide/snapshot.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,64 @@ test('button looks correct', async () => {

This captures screenshots and compares them against reference images to detect unintended visual changes. Learn more in the [Visual Regression Testing guide](/guide/browser/visual-regression-testing).

## Aria Snapshots

Aria snapshots capture the accessibility tree of a DOM element and compare it against a stored template. Inspired by [Playwright's aria snapshots](https://playwright.dev/docs/aria-snapshots), they provide a semantic alternative to visual regression testing — asserting structure and meaning rather than pixels.

- Works in jsdom, happy-dom, and [Browser Mode](/guide/browser/)
- Supports regex patterns in names and text (`/User \d+/`)
- Hand-edited patterns survive `--update` on partial match

### File snapshots

```ts
import { expect, test } from 'vitest'

test('navigation structure', () => {
const nav = document.querySelector('nav')
expect(nav).toMatchAriaSnapshot()
})
```

On first run, Vitest generates a snapshot file entry like:

```
- navigation "Actions":
- button: Save
- button: Cancel
```

### Inline snapshots

```ts
import { expect, test } from 'vitest'

test('navigation structure', () => {
expect(document.body).toMatchAriaInlineSnapshot(`
- navigation "Actions":
- button: Save
- button: Cancel
`)
})
```

### Browser Mode

In [Browser Mode](/guide/browser/), use `expect.element()` to automatically retry until the DOM accessibility tree matches the snapshot:

```ts
await expect.element(page.getByRole('navigation')).toMatchAriaInlineSnapshot(`
- button: Save
- button: Cancel
`)
```

The matcher re-queries the element and re-captures the accessibility tree on each attempt until it matches or the timeout is reached.

Retry only applies when comparing against an existing snapshot. On first run (snapshot creation) or with `--update`, the matcher captures once and writes immediately — no timeout wait.

See [`toMatchAriaSnapshot`](/api/expect#tomatcharisnapshot) and [`toMatchAriaInlineSnapshot`](/api/expect#tomatchariaInlinesnapshot) for the full API reference.

## Custom Serializer

You can add your own logic to alter how your snapshots are serialized. Like Jest, Vitest has default serializers for built-in JavaScript types, HTML elements, ImmutableJS and for React elements.
Expand Down Expand Up @@ -200,6 +258,148 @@ Pretty foo: Object {

We are using Jest's `pretty-format` for serializing snapshots. You can read more about it here: [pretty-format](https://github.com/facebook/jest/blob/main/packages/pretty-format/README.md#serialize).

## Custom Snapshot Domain <Badge type="warning">experimental</Badge> {#custom-snapshot-domain}

Custom serializers control how values are _rendered_ into snapshot strings, but comparison is still string equality. A **domain snapshot adapter** goes further — it owns the entire comparison pipeline: how to capture a value, render it, parse a stored snapshot, and match them semantically.

This is useful when snapshot comparison needs to be smarter than `===`. For example, the built-in [aria snapshots](#aria-snapshots) use a domain adapter to support regex patterns and preserve hand-edited patterns when running with `--update` — the adapter's `match` method can return a `mergedExpected` string so only the changed literal parts are overwritten.

### The adapter interface

A domain adapter implements four methods:

```ts
import type { DomainSnapshotAdapter } from '@vitest/snapshot'

const myAdapter: DomainSnapshotAdapter<Captured, Expected> = {
name: 'my-domain',

// Extract structured data from the received value
capture(received) { /* ... */ },

// Render captured data as the snapshot string (what gets stored)
render(captured) { /* ... */ },

// Parse a stored snapshot string into a structured expected value
parseExpected(input) { /* ... */ },

// Compare captured vs expected, return pass/fail and diff info
match(captured, expected) { /* ... */ },
}
```

### Registration

Register an adapter in your test setup file:

```ts [setup.ts]
import { expect } from 'vitest'

expect.addSnapshotDomain(myAdapter)
```

Then use it in tests via [`toMatchDomainSnapshot`](/api/expect#tomatchdomainsnapshot) or [`toMatchDomainInlineSnapshot`](/api/expect#tomatchdomaininlinesnapshot):

```ts
expect(value).toMatchDomainSnapshot('my-domain')
expect(value).toMatchDomainInlineSnapshot(`key=value`, 'my-domain')
```

### Example: key-value adapter

A minimal adapter that stores objects as `key=value` lines, with regex pattern support ([full source](https://github.com/vitest-dev/vitest/blob/main/test/snapshots/test/fixtures/domain/basic.ts)):

```ts [kv-adapter.ts]
import type {
DomainMatchResult,
DomainSnapshotAdapter,
} from '@vitest/snapshot'

interface KVCaptured {
entries: { key: string; value: string }[]
}

interface KVExpected {
entries: { key: string; value: string | RegExp }[]
}

export const kvAdapter: DomainSnapshotAdapter<KVCaptured, KVExpected> = {
name: 'kv',

capture(received) {
const entries = Object.entries(received as Record<string, string>)
.map(([key, value]) => ({ key, value: String(value) }))
return { entries }
},

render(captured) {
return captured.entries.map(e => `${e.key}=${e.value}`).join('\n')
},

parseExpected(input) {
const entries = input.trim().split('\n').map((line) => {
const eq = line.indexOf('=')
const key = line.slice(0, eq)
const raw = line.slice(eq + 1)
const value = (raw.startsWith('/') && raw.endsWith('/'))
? new RegExp(raw.slice(1, -1))
: raw
return { key, value }
})
return { entries }
},

match(captured, expected): DomainMatchResult {
let allPass = true
for (let i = 0; i < captured.entries.length; i++) {
const cap = captured.entries[i]
const exp = expected.entries[i]
if (!exp) {
allPass = false
}
else if (exp.value instanceof RegExp) {
if (!exp.value.test(cap.value)) allPass = false
}
else if (cap.value !== exp.value) {
allPass = false
}
}
return {
pass: allPass,
// Optionally return mergedExpected, actual, expected for diffs
// and pattern-preserving updates
// actual: "...",
// expected: "...",
// mergedExpected: "...",
}
},
}
```

```ts [setup.ts]
import { expect } from 'vitest'
import { kvAdapter } from './kv-adapter'

expect.addSnapshotDomain(kvAdapter)
```

```ts [example.test.ts]
import { expect, test } from 'vitest'

test('user data', () => {
const user = { name: 'Alice', score: '42' }
expect(user).toMatchDomainSnapshot('kv')
})

test('user data inline', () => {
const user = { name: 'Alice', score: '42' }
expect(user).toMatchDomainInlineSnapshot(`
name=Alice
score=/\\d+/
`, 'kv')
})
```

## Difference from Jest

Vitest provides an almost compatible Snapshot feature with [Jest's](https://jestjs.io/docs/snapshot-testing) with a few exceptions:
Expand Down
4 changes: 4 additions & 0 deletions packages/snapshot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./aria": {
"types": "./dist/aria.d.ts",
"default": "./dist/aria.js"
},
"./environment": {
"types": "./dist/environment.d.ts",
"default": "./dist/environment.js"
Expand Down
1 change: 1 addition & 0 deletions packages/snapshot/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const external = [

const entries = {
index: 'src/index.ts',
aria: 'src/aria.ts',
environment: 'src/environment.ts',
manager: 'src/manager.ts',
}
Expand Down
Loading
Loading