[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat(experimental): support aria snapshot (#9668)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Ari Perkkiö <ari.perkkio@gmail.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Vladimir <sleuths.slews0s@icloud.com>

authored by

Hiroshi Ogawa
Claude Opus 4.6 (1M context)
Ari Perkkiö
Codex
Vladimir
and committed by
GitHub
(Apr 9, 2026, 9:23 AM +0200) d4fbb5cc b77de968

+4067 -170
+3
.github/workflows/ci.yml
··· 120 120 - name: Test Examples 121 121 run: pnpm run test:examples 122 122 123 + - name: Unit Test UI 124 + run: pnpm -C packages/ui test:ui 125 + 123 126 - uses: actions/upload-artifact@v7 124 127 if: ${{ !cancelled() }} 125 128 with:
+5
docs/.vitepress/config.ts
··· 806 806 link: '/guide/browser/trace-view', 807 807 docFooterText: 'Trace View | Browser Mode', 808 808 }, 809 + { 810 + text: 'ARIA Snapshots', 811 + link: '/guide/browser/aria-snapshots', 812 + docFooterText: 'ARIA Snapshots | Browser Mode', 813 + }, 809 814 ], 810 815 }, 811 816 {
+37
docs/api/expect.md
··· 1010 1010 1011 1011 The same as [`toMatchInlineSnapshot`](#tomatchinlinesnapshot), but expects the same value as [`toThrow`](#tothrow). 1012 1012 1013 + ## toMatchAriaSnapshot <Version type="experimental">4.1.4</Version> <Experimental /> {#tomatcharisnapshot} 1014 + 1015 + - **Type:** `() => void` 1016 + 1017 + 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. 1018 + 1019 + ```ts 1020 + import { expect, test } from 'vitest' 1021 + 1022 + test('navigation accessibility', () => { 1023 + document.body.innerHTML = ` 1024 + <nav aria-label="Actions"> 1025 + <button>Save</button> 1026 + <button>Cancel</button> 1027 + </nav> 1028 + ` 1029 + expect(document.querySelector('nav')).toMatchAriaSnapshot() 1030 + }) 1031 + ``` 1032 + 1033 + ## toMatchAriaInlineSnapshot <Version type="experimental">4.1.4</Version> <Experimental /> {#tomatchariainlinesnapshot} 1034 + 1035 + - **Type:** `(snapshot?: string) => void` 1036 + 1037 + 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. 1038 + 1039 + ```ts 1040 + import { expect, test } from 'vitest' 1041 + 1042 + test('user profile', () => { 1043 + expect(document.body).toMatchAriaInlineSnapshot(` 1044 + - heading "Dashboard" [level=1] 1045 + - button /User \\d+/: Profile 1046 + `) 1047 + }) 1048 + ``` 1049 + 1013 1050 ## toHaveBeenCalled 1014 1051 1015 1052 - **Type:** `() => Awaitable<void>`
+218
docs/guide/snapshot.md
··· 124 124 125 125 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). 126 126 127 + ## ARIA Snapshots <Badge type="warning">experimental</Badge> <Version>4.1.4</Version> 128 + 129 + ARIA snapshots capture the accessibility tree of a DOM element and compare it against a stored template. Based on [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. 130 + 131 + For example, given this HTML: 132 + 133 + ```html 134 + <nav aria-label="Main"> 135 + <a href="/">Home</a> 136 + <a href="/about">About</a> 137 + </nav> 138 + ``` 139 + 140 + You can assert its accessibility tree: 141 + 142 + ```ts 143 + import { expect, test } from 'vitest' 144 + import { page } from 'vitest/browser' 145 + 146 + test('navigation structure', async () => { 147 + await expect.element(page.getByRole('navigation')).toMatchAriaInlineSnapshot(` 148 + - navigation "Main": 149 + - link "Home": 150 + - /url: / 151 + - link "About": 152 + - /url: /about 153 + `) 154 + }) 155 + ``` 156 + 157 + See the dedicated [ARIA Snapshots guide](/guide/browser/aria-snapshots) for syntax details, retry behavior in Browser Mode, and file vs. inline snapshot examples. See [`toMatchAriaSnapshot`](/api/expect#tomatcharisnapshot) and [`toMatchAriaInlineSnapshot`](/api/expect#tomatchariainlinesnapshot) for the full API reference. 158 + 127 159 ## Custom Serializer 128 160 129 161 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. ··· 298 330 ::: tip 299 331 See [Extending Matchers](/guide/extending-matchers) for more on `expect.extend` and custom matcher conventions. 300 332 ::: 333 + 334 + ## Custom Snapshot Domain <Badge type="warning">experimental</Badge> <Version>4.1.4</Version> {#custom-snapshot-domain} 335 + 336 + 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 for a custom matcher, including how to capture a value, render it, parse a stored snapshot, and match them semantically. 337 + 338 + ### The adapter interface 339 + 340 + A domain adapter implements four methods and is generic over two types — `Captured` (what the value actually is) and `Expected` (what the stored snapshot parses into): 341 + 342 + ```ts 343 + import type { DomainMatchResult, DomainSnapshotAdapter } from '@vitest/snapshot' 344 + 345 + const myAdapter: DomainSnapshotAdapter<Captured, Expected> = { 346 + name: 'my-domain', 347 + 348 + // Extract structured data from the received value 349 + capture(received: unknown): Captured { /* ... */ }, 350 + 351 + // Render captured data as the snapshot string (what gets stored) 352 + render(captured: Captured): string { /* ... */ }, 353 + 354 + // Parse a stored snapshot string into a structured expected value 355 + parseExpected(input: string): Expected { /* ... */ }, 356 + 357 + // Compare captured vs expected, return pass/fail and resolved output 358 + match(captured: Captured, expected: Expected): DomainMatchResult { /* ... */ }, 359 + } 360 + ``` 361 + 362 + #### `DomainMatchResult` 363 + 364 + The `match` method returns a `DomainMatchResult` with two optional string fields beyond `pass`: 365 + 366 + - **`resolved`** — the captured value viewed through the template's lens. Where the template uses patterns (e.g. regexes) or omits details, the resolved string adopts those patterns. Where the template doesn't match, it uses literal captured values. This serves as both the actual side of diffs and the value written on `--update`. When omitted, falls back to `render(capture(received))`. 367 + 368 + - **`expected`** — the stored template re-rendered as a string. Used as the expected side of diffs. When omitted, falls back to the raw snapshot string from the snap file or inline snapshot. 369 + 370 + :::details Why are `Captured` and `Expected` separate types? 371 + 372 + When a snapshot is first generated, `render(captured)` produces a plain string that gets stored. But once stored, the user can **hand-edit** it — replacing literals with regex patterns, relaxing assertions, or adding domain-specific query syntax. After editing, `parseExpected(input)` parses this modified string into a type that is _richer_ than what `capture` produces. 373 + 374 + For example, in the [key-value adapter](#example-key-value-adapter) below, `Captured` values are always `string`, but `Expected` values can be `string | RegExp`: 375 + 376 + ```ts 377 + type KVCaptured = Record<string, string> 378 + type KVExpected = Record<string, string | RegExp> 379 + ``` 380 + 381 + This asymmetry is what makes `--update` work correctly: `match` returns a `resolved` string that updates changed literal parts while **preserving** the user's hand-edited patterns. If both sides were the same type, there would be no way to distinguish "what the value actually is" from "what the user chose to assert" — and every update would overwrite the user's patterns. 382 + 383 + ::: 384 + 385 + ### Build a matcher from the adapter 386 + 387 + Register a custom matcher with `expect.extend(...)` and call the snapshot composables from `vitest`: 388 + 389 + ```ts [setup.ts] 390 + import { expect, Snaphsots } from 'vitest' 391 + 392 + expect.extend({ 393 + toMatchMyDomainSnapshot(received: unknown) { 394 + return Snaphsots.toMatchDomainSnapshot.call(this, myAdapter, received) 395 + }, 396 + toMatchMyDomainInlineSnapshot(received: unknown, inlineSnapshot?: string) { 397 + return Snaphsots.toMatchDomainInlineSnapshot.call( 398 + this, 399 + myAdapter, 400 + received, 401 + inlineSnapshot, 402 + ) 403 + }, 404 + }) 405 + ``` 406 + 407 + Then use your matcher in tests: 408 + 409 + ```ts 410 + expect(value).toMatchMyDomainSnapshot() 411 + expect(value).toMatchMyDomainInlineSnapshot(`key=value`) 412 + ``` 413 + 414 + ### Example: key-value adapter 415 + 416 + A minimal adapter that stores objects as `key=value` lines, with regex pattern and subset key match support ([full source](https://github.com/vitest-dev/vitest/blob/main/test/snapshots/test/fixtures/domain/basic.ts)): 417 + 418 + ```ts [kv-adapter.ts] 419 + import type { DomainMatchResult, DomainSnapshotAdapter } from '@vitest/snapshot' 420 + 421 + type KVCaptured = Record<string, string> 422 + type KVExpected = Record<string, string | RegExp> 423 + 424 + function renderKV(obj: Record<string, unknown>) { 425 + return `\n${Object.entries(obj).map(([k, v]) => `${k}=${v}`).join('\n')}\n` 426 + } 427 + 428 + export const kvAdapter: DomainSnapshotAdapter<KVCaptured, KVExpected> = { 429 + name: 'kv', 430 + 431 + capture(received: unknown): KVCaptured { 432 + if (received && typeof received === 'object') { 433 + return Object.fromEntries( 434 + Object.entries(received).map(([k, v]) => [k, String(v)]), 435 + ) 436 + } 437 + throw new TypeError('kv adapter expects a plain object') 438 + }, 439 + 440 + render(captured: KVCaptured): string { 441 + return renderKV(captured) 442 + }, 443 + 444 + parseExpected(input: string): KVExpected { 445 + const entries = input.trim().split('\n').map((line) => { 446 + const eq = line.indexOf('=') 447 + const key = line.slice(0, eq) 448 + const raw = line.slice(eq + 1) 449 + const value = (raw.startsWith('/') && raw.endsWith('/') && raw.length > 1) 450 + ? new RegExp(raw.slice(1, -1)) 451 + : raw 452 + return [key, value] 453 + }) 454 + return Object.fromEntries(entries) 455 + }, 456 + 457 + match(captured: KVCaptured, expected: KVExpected): DomainMatchResult { 458 + const resolvedLines: string[] = [] 459 + let pass = true 460 + 461 + for (const [key, actualValue] of Object.entries(captured)) { 462 + const expectedValue = expected[key] 463 + 464 + // non-asserted keys are skipped (works as subset match) 465 + if (typeof expectedValue === 'undefined') { 466 + continue 467 + } 468 + 469 + // preserve matched pattern for normalized diff and partial update 470 + if (expectedValue instanceof RegExp && expectedValue.test(actualValue)) { 471 + resolvedLines.push(`${key}=/${expectedValue.source}/`) 472 + continue 473 + } 474 + 475 + resolvedLines.push(`${key}=${actualValue}`) 476 + pass &&= actualValue === expectedValue 477 + } 478 + 479 + return { 480 + pass, 481 + message: pass ? undefined : 'KV entries do not match', 482 + resolved: `\n${resolvedLines.join('\n')}\n`, 483 + expected: `\n${renderKV(expected)}\n`, 484 + } 485 + }, 486 + } 487 + ``` 488 + 489 + ```ts [setup.ts] 490 + import { expect, Snapshots } from 'vitest' 491 + import { kvAdapter } from './kv-adapter' 492 + 493 + expect.extend({ 494 + toMatchKvSnapshot(received: unknown) { 495 + return Snapshots.toMatchDomainSnapshot.call(this, kvAdapter, received) 496 + }, 497 + toMatchKvInlineSnapshot(received: unknown, inlineSnapshot?: string) { 498 + return Snapshots.toMatchDomainInlineSnapshot.call(this, kvAdapter, received, inlineSnapshot) 499 + }, 500 + }) 501 + ``` 502 + 503 + ```ts [example.test.ts] 504 + import { expect, test } from 'vitest' 505 + 506 + test('user data', () => { 507 + const user = { name: 'Alice', score: '42' } 508 + expect(user).toMatchKvSnapshot() 509 + }) 510 + 511 + test('user data inline', () => { 512 + const user = { name: 'Alice', age: 100, score: '42' } 513 + expect(user).toMatchKvInlineSnapshot(` 514 + name=Alice 515 + score=/\\d+/ 516 + `) 517 + }) 518 + ``` 301 519 302 520 ## Difference from Jest 303 521
+62
packages/browser/jest-dom.d.ts
··· 721 721 name?: string, 722 722 options?: ScreenshotMatcherOptions<ComparatorName>, 723 723 ): Promise<R> 724 + 725 + /** 726 + * @experimental 727 + * @description 728 + * Captures the accessibility tree of an element and compares it against a stored 729 + * snapshot file. The snapshot uses a YAML-like format describing roles, names, and 730 + * states of the accessibility tree. 731 + * 732 + * In Browser Mode, `expect.element()` automatically retries until the tree matches 733 + * or the timeout is reached. On first run or with `--update`, the snapshot is 734 + * written immediately. 735 + * 736 + * @example 737 + * <nav aria-label="Main"> 738 + * <a href="/">Home</a> 739 + * <a href="/about">About</a> 740 + * </nav> 741 + * 742 + * await expect.element(page.getByRole('navigation')).toMatchAriaSnapshot() 743 + * 744 + * // Generates a snapshot entry like: 745 + * // - navigation "Main": 746 + * // - link "Home": 747 + * // - /url: / 748 + * // - link "About": 749 + * // - /url: /about 750 + * 751 + * @see https://vitest.dev/guide/browser/aria-snapshots 752 + * @see https://vitest.dev/api/expect#tomatchariasnapshot 753 + */ 754 + toMatchAriaSnapshot: () => void 755 + /** 756 + * @experimental 757 + * @description 758 + * Captures the accessibility tree of an element and compares it against an inline 759 + * snapshot stored directly in the test file. The snapshot uses a YAML-like format 760 + * describing roles, names, and states of the accessibility tree. 761 + * 762 + * Supports partial matching (only listed nodes are checked) and regex patterns 763 + * for flexible assertions. Hand-edited regex patterns are preserved on `--update`. 764 + * 765 + * In Browser Mode, `expect.element()` automatically retries until the tree matches 766 + * or the timeout is reached. 767 + * 768 + * @example 769 + * <form aria-label="Log In"> 770 + * <input aria-label="Email" /> 771 + * <input aria-label="Password" type="password" /> 772 + * <button>Submit</button> 773 + * </form> 774 + * 775 + * await expect.element(page.getByRole('form')).toMatchAriaInlineSnapshot(` 776 + * - form "Log In": 777 + * - textbox "Email" 778 + * - textbox "Password" 779 + * - button "Submit" 780 + * `) 781 + * 782 + * @see https://vitest.dev/guide/browser/aria-snapshots 783 + * @see https://vitest.dev/api/expect#tomatchariaInlinesnapshot 784 + */ 785 + toMatchAriaInlineSnapshot: (inlineSnapshot?: string) => void 724 786 }
+1
packages/ui/.gitignore
··· 1 + __screenshots__
+1 -1
packages/ui/package.json
··· 47 47 "dev:client": "vite", 48 48 "dev": "rollup -c --watch --watch.include 'node/**'", 49 49 "dev:ui": "pnpm run --stream '/^(dev|dev:client)$/'", 50 - "test:ui": "vitest --browser" 50 + "test:ui": "vitest" 51 51 }, 52 52 "peerDependencies": { 53 53 "vitest": "workspace:*"
+4 -1
packages/ui/vitest.config.ts
··· 11 11 }, 12 12 test: { 13 13 browser: { 14 - provider: playwright(), 14 + enabled: true, 15 + provider: playwright({ 16 + actionTimeout: 5000, 17 + }), 15 18 instances: [{ browser: 'chromium' }], 16 19 }, 17 20 },
+1 -1
test/snapshots/README.md
··· 1 1 # Snapshot Tests 2 2 3 - This directory contains integration tests for Vitest's snapshot functionality. It uses a meta-testing approach where integration tests programmatically run fixture tests to validate snapshot behavior. 3 + This directory contains integration tests for Vitest's snapshot functionality. It uses a meta-testing approach where integration tests programmatically run fixture tests to validate snapshot behavior, such as, snapshot update, snapshot error formatting, summary reporting, obsolete snapshots handling, etc. 4 4 5 5 ```bash 6 6 # Run all integration tests
+11 -7
test/test-utils/index.ts
··· 16 16 import fs from 'node:fs' 17 17 import { Readable, Writable } from 'node:stream' 18 18 import { fileURLToPath, pathToFileURL } from 'node:url' 19 - import { inspect } from 'node:util' 19 + import { inspect, stripVTControlCharacters } from 'node:util' 20 20 import { dirname, relative, resolve } from 'pathe' 21 21 import { x } from 'tinyexec' 22 22 import * as tinyrainbow from 'tinyrainbow' ··· 243 243 get results() { 244 244 return ctx?.state.getTestModules() || [] 245 245 }, 246 - errorTree(options?: { project?: boolean; stackTrace?: boolean }) { 246 + errorTree(options?: { project?: boolean; stackTrace?: boolean; diff?: boolean }) { 247 247 const modules = ctx?.state.getTestModules() || [] 248 248 const tree = options?.project 249 249 ? buildErrorProjectTree(modules, options) ··· 572 572 } 573 573 } 574 574 575 - export function buildErrorTree(testModules: TestModule[], options?: { stackTrace?: boolean }) { 575 + export function buildErrorTree(testModules: TestModule[], options?: { stackTrace?: boolean; diff?: boolean }) { 576 576 const root = testModules[0]?.project.config.root 577 577 578 - function mapError(e: { message: string; stacks?: { file: string; line: number; column: number; method: string }[] }) { 578 + function mapError(e: { message: string; diff?: string; stacks?: { file: string; line: number; column: number; method: string }[] }) { 579 + let message = e.message 580 + if (options?.diff && e.diff) { 581 + message = [message, stripVTControlCharacters(e.diff)].join('\n') 582 + } 579 583 if (options?.stackTrace) { 580 584 const stacks = (e.stacks || []).map((s) => { 581 585 const loc = `${relative(root, s.file)}:${s.line}:${s.column}` 582 586 return s.method ? ` at ${s.method} (${loc})` : ` at ${loc}` 583 587 }) 584 - return [e.message, ...stacks].join('\n') 588 + message = [message, ...stacks].join('\n') 585 589 } 586 - return e.message 590 + return message 587 591 } 588 592 589 593 return buildTestTree( ··· 675 679 return projectTree 676 680 } 677 681 678 - export function buildErrorProjectTree(testModules: TestModule[], options?: { stackTrace?: boolean }) { 682 + export function buildErrorProjectTree(testModules: TestModule[], options?: { stackTrace?: boolean; diff?: boolean }) { 679 683 const projectTree: Record<string, Record<string, any>> = {} 680 684 681 685 for (const testModule of testModules) {
+475
docs/guide/browser/aria-snapshots.md
··· 1 + --- 2 + title: ARIA Snapshots | Guide 3 + outline: deep 4 + --- 5 + 6 + # ARIA Snapshots <Badge type="warning">experimental</Badge> <Version>4.1.4</Version> 7 + 8 + 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. 9 + 10 + Given this HTML: 11 + 12 + ```html 13 + <nav aria-label="Main"> 14 + <a href="/">Home</a> 15 + <a href="/about">About</a> 16 + </nav> 17 + ``` 18 + 19 + You can assert its accessibility tree: 20 + 21 + ```ts 22 + await expect.element(page.getByRole('navigation')).toMatchAriaInlineSnapshot(` 23 + - navigation "Main": 24 + - link "Home": 25 + - /url: / 26 + - link "About": 27 + - /url: /about 28 + `) 29 + ``` 30 + 31 + 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. 32 + 33 + ## Snapshot Workflow 34 + 35 + 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. 36 + 37 + See the main [Snapshot guide](/guide/snapshot) for the general snapshot workflow, update behavior, and review guidelines. 38 + 39 + ## Basic Usage 40 + 41 + Given a page with this HTML: 42 + 43 + ```html 44 + <form aria-label="Log In"> 45 + <input aria-label="Email" /> 46 + <input aria-label="Password" type="password" /> 47 + <button>Submit</button> 48 + </form> 49 + ``` 50 + 51 + ### File Snapshots 52 + 53 + Use `toMatchAriaSnapshot()` to store the snapshot in a `.snap` file alongside your test: 54 + 55 + ```ts [basic.test.ts] 56 + import { expect, test } from 'vitest' 57 + 58 + test('login form', async () => { 59 + await expect.element(page.getByRole('form')).toMatchAriaSnapshot() 60 + }) 61 + ``` 62 + 63 + On first run, Vitest generates a snapshot file entry: 64 + 65 + ```js [__snapshots__/basic.test.ts.snap] 66 + // Vitest Snapshot ... 67 + 68 + exports[`login form 1`] = ` 69 + - form "Log In": 70 + - textbox "Email" 71 + - textbox "Password" 72 + - button "Submit" 73 + ` 74 + ``` 75 + 76 + ### Inline Snapshots 77 + 78 + Use `toMatchAriaInlineSnapshot()` to store the snapshot directly in the test file: 79 + 80 + ```ts 81 + import { expect, test } from 'vitest' 82 + 83 + test('login form', async () => { 84 + await expect.element(page.getByRole('form')).toMatchAriaInlineSnapshot(` 85 + - form "Log In": 86 + - textbox "Email" 87 + - textbox "Password" 88 + - button "Submit" 89 + `) 90 + }) 91 + ``` 92 + 93 + ## Browser Mode Retry Behavior 94 + 95 + 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. 96 + 97 + ```ts 98 + await expect.element(page.getByRole('form')).toMatchAriaInlineSnapshot(` 99 + - form "Log In": 100 + - textbox "Email" 101 + - textbox "Password" 102 + - button "Submit" 103 + `) 104 + ``` 105 + 106 + On first run or with `--update`, the stable result is written as the new snapshot. 107 + 108 + 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. 109 + 110 + ## Preserving Hand-Edited Patterns 111 + 112 + 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. 113 + 114 + ### Example 115 + 116 + **Step 1.** Your shopping cart page renders this HTML: 117 + 118 + ```html 119 + <h1>Your Cart</h1> 120 + <ul aria-label="Cart Items"> 121 + <li>Wireless Headphones — $79.99</li> 122 + </ul> 123 + <button>Checkout</button> 124 + ``` 125 + 126 + You run your test for the first time with `--update`. Vitest generates the snapshot: 127 + 128 + ```yaml 129 + - heading "Your Cart" [level=1] 130 + - list "Cart Items": 131 + - listitem: Wireless Headphones — $79.99 132 + - button "Checkout" 133 + ``` 134 + 135 + **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: 136 + 137 + ```yaml 138 + - heading "Your Cart" [level=1] 139 + - list "Cart Items": 140 + - listitem: /.+ — \$\d+\.\d+/ 141 + - button "Checkout" 142 + ``` 143 + 144 + **Step 3.** Later, a developer renames the button from "Checkout" to "Place Order". Running `--update` updates that literal but preserves your regex patterns: 145 + 146 + ```yaml 147 + - heading "Your Cart" [level=1] 148 + - list "Cart Items": 149 + - listitem: /.+ — \$\d+\.\d+/ 150 + - button "Place Order" 👈 New snapshot updated with new string 151 + ``` 152 + 153 + 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". 154 + 155 + ## Snapshot Format 156 + 157 + ARIA snapshots use a YAML-like syntax. Each line represents a node in the accessibility tree. 158 + 159 + ::: info 160 + 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. 161 + 162 + Captured text is also whitespace-normalized before it is rendered into the snapshot. Newlines, `<br>` line breaks, tabs, and repeated whitespace collapse to single spaces, so multi-line DOM text is emitted as a single-line snapshot value. 163 + ::: 164 + 165 + Each accessible element in the tree is represented as a YAML node: 166 + 167 + ```yaml 168 + - role "name" [attribute=value] 169 + ``` 170 + 171 + - `role`: The ARIA role of the element, such as `heading`, `list`, `listitem`, or `button` 172 + - `"name"`: The [accessible name](https://w3c.github.io/accname/), when present. Quoted strings match exact values, and `/patterns/` match regular expressions 173 + - `[attribute=value]`: Accessibility states and properties such as `checked`, `disabled`, `expanded`, `level`, `pressed`, or `selected` 174 + 175 + These values come from ARIA attributes and the browser's accessibility tree, including semantics inferred from native HTML elements. 176 + 177 + 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. 178 + 179 + ### Roles and Accessible Names 180 + 181 + For example: 182 + 183 + ```html 184 + <button>Submit</button> 185 + <h1>Welcome</h1> 186 + <a href="/">Home</a> 187 + <input aria-label="Email" /> 188 + ``` 189 + 190 + ```yaml 191 + - button "Submit" 192 + - heading "Welcome" [level=1] 193 + - link "Home" 194 + - textbox "Email" 195 + ``` 196 + 197 + 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. 198 + 199 + For a closer look at how names are computed, see [Accessible Name and Description Computation](https://w3c.github.io/accname/). 200 + 201 + Some content appears in the snapshot as a text node instead of a role-based element: 202 + 203 + ```html 204 + <span>Hello world</span> 205 + ``` 206 + 207 + ```yaml 208 + - text: Hello world 209 + ``` 210 + 211 + Text values are always serialized on a single line after whitespace normalization. For example: 212 + 213 + ```html 214 + <p> 215 + Line 1 216 + Line 2<br />Line 3 217 + Line 4 218 + </p> 219 + ``` 220 + 221 + ```yaml 222 + - paragraph: Line 1 Line 2 Line 3 Line 4 223 + ``` 224 + 225 + ### Children 226 + 227 + Child elements appear nested under their parent: 228 + 229 + ```html 230 + <ul> 231 + <li>First</li> 232 + <li>Second</li> 233 + <li>Third</li> 234 + </ul> 235 + ``` 236 + 237 + ```yaml 238 + - list: 239 + - listitem: First 240 + - listitem: Second 241 + - listitem: Third 242 + ``` 243 + 244 + If the parent has an accessible name, the snapshot includes it before the nested children: 245 + 246 + ```html 247 + <nav aria-label="Main"> 248 + <a href="/">Home</a> 249 + <a href="/about">About</a> 250 + </nav> 251 + ``` 252 + 253 + ```yaml 254 + - navigation "Main": 255 + - link "Home" 256 + - link "About" 257 + ``` 258 + 259 + If an element only contains a single text child and has no other properties, the text is rendered inline: 260 + 261 + ```html 262 + <p>Hello world</p> 263 + ``` 264 + 265 + ```yaml 266 + - paragraph: Hello world 267 + ``` 268 + 269 + ### Attributes 270 + 271 + ARIA states and properties appear in brackets: 272 + 273 + | HTML | Snapshot | 274 + | ---------------------------------------------------------------------- | ----------------------------------------- | 275 + | `<input type="checkbox" checked aria-label="Agree">` | `- checkbox "Agree" [checked]` | 276 + | `<input type="checkbox" aria-checked="mixed" aria-label="Select all">` | `- checkbox "Select all" [checked=mixed]` | 277 + | `<button aria-disabled="true">Submit</button>` | `- button "Submit" [disabled]` | 278 + | `<button aria-expanded="true">Menu</button>` | `- button "Menu" [expanded]` | 279 + | `<h2>Title</h2>` | `- heading "Title" [level=2]` | 280 + | `<button aria-pressed="true">Bold</button>` | `- button "Bold" [pressed]` | 281 + | `<button aria-pressed="mixed">Bold</button>` | `- button "Bold" [pressed=mixed]` | 282 + | `<option selected>English</option>` | `- option "English" [selected]` | 283 + 284 + Attributes only appear when they are active. A button that is not disabled simply has no `[disabled]` attribute — there is no `[disabled=false]`. 285 + 286 + ### Pseudo-Attributes 287 + 288 + Some DOM properties that aren't part of ARIA but are useful for testing are exposed with a `/` prefix: 289 + 290 + #### `/url:` 291 + 292 + Links include their URL: 293 + 294 + ```html 295 + <a href="/">Home</a> 296 + ``` 297 + 298 + ```yaml 299 + - link "Home": 300 + - /url: / 301 + ``` 302 + 303 + #### `/placeholder:` 304 + 305 + Textboxes can include their placeholder text: 306 + 307 + ```html 308 + <input aria-label="Email" placeholder="user@example.com" /> 309 + ``` 310 + 311 + ```yaml 312 + - textbox "Email": 313 + - /placeholder: user@example.com 314 + ``` 315 + 316 + ::: tip When does `/placeholder:` appear? 317 + 318 + 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 `<label>`, the browser uses the placeholder as the accessible name. In that case, the placeholder information is already in the name and is not duplicated. 319 + 320 + - When placeholder is the accessible name: 321 + 322 + ```html 323 + <input placeholder="Search" /> 324 + ``` 325 + 326 + ```yaml 327 + - textbox "Search" 328 + ``` 329 + 330 + - When placeholder differs from the accessible name: 331 + 332 + ```html 333 + <input placeholder="Search" aria-label="Search products" /> 334 + ``` 335 + 336 + ```yaml 337 + - textbox "Search products": 338 + - /placeholder: Search 339 + ``` 340 + 341 + ::: 342 + 343 + ## Matching 344 + 345 + ### Regular Expressions 346 + 347 + Use regex patterns to match names flexibly: 348 + 349 + ```html 350 + <h1>Welcome, Alice</h1> 351 + <a href="https://example.com/profile/123">Profile</a> 352 + ``` 353 + 354 + ```yaml 355 + - heading /Welcome, .*/ 356 + - link "Profile": 357 + - /url: /https:\/\/example\.com\/.*/ 358 + ``` 359 + 360 + Regex also works in pseudo-attribute values: 361 + 362 + ```html 363 + <input aria-label="Search" placeholder="Type to search..." /> 364 + ``` 365 + 366 + ```yaml 367 + - textbox "Search": 368 + - /placeholder: /Type .*/ 369 + ``` 370 + 371 + ::: warning Escaping backslashes in regex patterns 372 + Snapshots are stored as JavaScript strings — in backtick-delimited template literals for inline snapshots and in `.snap` files. Because of this, backslashes need to be **doubled** when you hand-edit a snapshot to add a regex pattern. 373 + 374 + For example, to match one or more digits with `\d+`: 375 + 376 + ```ts 377 + // ✅ Correct — double backslash 378 + await expect.element(button).toMatchAriaInlineSnapshot(` 379 + - button: /item \\d+/ 380 + `) 381 + 382 + // ❌ Wrong — single backslash is consumed by JS, regex sees "d+" instead of "\d+" 383 + await expect.element(button).toMatchAriaInlineSnapshot(` 384 + - button: /item \d+/ 385 + `) 386 + ``` 387 + 388 + This applies to both inline snapshots and `.snap` files. When Vitest **auto-generates** or **updates** a snapshot, escaping is handled automatically — you only need to worry about this when hand-editing regex patterns. 389 + ::: 390 + 391 + ### Child Matching 392 + 393 + The `/children` directive controls how a node's children are compared against the template. There are three modes: 394 + 395 + #### Partial Matching (default) 396 + 397 + By default (no `/children` directive), templates use **contain** semantics — extra children in the actual tree are allowed as long as all template children appear as an ordered subsequence. This is the same as `/children: contain`. 398 + 399 + ```html 400 + <main> 401 + <h1>Welcome</h1> 402 + <p>Some intro text</p> 403 + <button>Get Started</button> 404 + </main> 405 + ``` 406 + 407 + ```ts 408 + // This passes — the template children are a subset of the actual children 409 + await expect.element(page.getByRole('main')).toMatchAriaInlineSnapshot(` 410 + - main: 411 + - heading "Welcome" [level=1] 412 + `) 413 + ``` 414 + 415 + This is useful for focused, resilient tests that don't break when unrelated content is added. 416 + 417 + #### Exact Matching (`/children: equal`) 418 + 419 + Requires that the node's immediate children match the template exactly — same count, same order. No extra children are allowed at this level. 420 + 421 + ```html 422 + <ul aria-label="Features"> 423 + <li>Feature A</li> 424 + <li>Feature B</li> 425 + <li>Feature C</li> 426 + </ul> 427 + ``` 428 + 429 + ```ts 430 + // This FAILS — the list has 3 items but the template only lists 2 431 + await expect.element(page.getByRole('list')).toMatchAriaInlineSnapshot(` 432 + - list "Features": 433 + - /children: equal 434 + - listitem: Feature A 435 + - listitem: Feature B 436 + `) 437 + ``` 438 + 439 + ```ts 440 + // This PASSES — all 3 items are listed 441 + await expect.element(page.getByRole('list')).toMatchAriaInlineSnapshot(` 442 + - list "Features": 443 + - /children: equal 444 + - listitem: Feature A 445 + - listitem: Feature B 446 + - listitem: Feature C 447 + `) 448 + ``` 449 + 450 + The strict matching only applies at the level where `/children` is placed. Descendants of each `listitem` still use the default contain semantics. 451 + 452 + #### Deep Exact Matching (`/children: deep-equal`) 453 + 454 + Like `equal`, but the strict matching **propagates to all descendants**. Every level of nesting must match exactly — same count, same order, no extra nodes at any depth. 455 + 456 + ```ts 457 + await expect.element(page.getByRole('navigation')).toMatchAriaInlineSnapshot(` 458 + - navigation "Main": 459 + - /children: deep-equal 460 + - link "Home": 461 + - /url: / 462 + - link "About": 463 + - /url: /about 464 + `) 465 + ``` 466 + 467 + With `deep-equal`, every child of each `link` must also match exactly. If a link had an extra child node not listed in the template, the assertion would fail. 468 + 469 + #### Comparison 470 + 471 + | Mode | Directive | Behavior | 472 + | --- | --- | --- | 473 + | Partial | _(default)_ or `/children: contain` | Template children are an ordered subsequence — extra actual children are ignored | 474 + | Exact | `/children: equal` | Immediate children must match exactly; descendants still use partial matching | 475 + | Deep exact | `/children: deep-equal` | All children at every depth must match exactly |
+10
packages/expect/src/jest-extend.ts
··· 142 142 softWrapper, 143 143 ) 144 144 145 + // `expect.poll()` inspects the installed Chai assertion method, 146 + // so copy the internal marker from the original matcher function. 147 + // this is only for domain snapshot matchers for now. 148 + if ((expectAssertion as any).__vitest_poll_takeover__) { 149 + const addedMethod = (c.Assertion.prototype as any)[expectAssertionName] 150 + Object.defineProperty(addedMethod, '__vitest_poll_takeover__', { 151 + value: true, 152 + }) 153 + } 154 + 145 155 class CustomMatcher extends AsymmetricMatcher<[unknown, ...unknown[]]> { 146 156 constructor(inverse = false, ...sample: [unknown, ...unknown[]]) { 147 157 super(sample, inverse)
+221
packages/snapshot/src/client.ts
··· 1 + import type { DomainSnapshotAdapter } from './domain' 1 2 import type { RawSnapshotInfo } from './port/rawSnapshot' 2 3 import type { SnapshotResult, SnapshotStateOptions } from './types' 3 4 import SnapshotState from './port/state' ··· 49 50 errorMessage?: string 50 51 rawSnapshot?: RawSnapshotInfo 51 52 assertionName?: string 53 + } 54 + 55 + interface AssertDomainOptions extends Omit<AssertOptions, 'received'> { 56 + received: unknown 57 + adapter: DomainSnapshotAdapter<any, any> 58 + } 59 + 60 + interface AssertDomainPollOptions extends Omit<AssertDomainOptions, 'received'> { 61 + poll: () => Promise<unknown> | unknown 62 + timeout?: number 63 + interval?: number 52 64 } 53 65 54 66 /** Same shape as expect.extend custom matcher result (SyncExpectationResult from @vitest/expect) */ ··· 199 211 } 200 212 } 201 213 214 + matchDomain(options: AssertDomainOptions): MatchResult { 215 + const { 216 + received, 217 + filepath, 218 + name, 219 + testId = name, 220 + message, 221 + adapter, 222 + isInline = false, 223 + inlineSnapshot, 224 + error, 225 + } = options 226 + 227 + if (!filepath) { 228 + throw new Error('Snapshot cannot be used outside of test') 229 + } 230 + 231 + const captured = adapter.capture(received) 232 + const rendered = adapter.render(captured) 233 + 234 + const snapshotState = this.getSnapshotState(filepath) 235 + const testName = [name, ...(message ? [message] : [])].join(' > ') 236 + 237 + const expectedSnapshot = snapshotState.probeExpectedSnapshot({ 238 + testName, 239 + testId, 240 + isInline, 241 + inlineSnapshot, 242 + }) 243 + expectedSnapshot.markAsChecked() 244 + const matchResult = expectedSnapshot.data 245 + ? adapter.match(captured, adapter.parseExpected(expectedSnapshot.data)) 246 + : undefined 247 + const { actual, expected, key, pass } = snapshotState.processDomainSnapshot({ 248 + testId, 249 + received: rendered, 250 + expectedSnapshot, 251 + matchResult, 252 + isInline, 253 + error, 254 + assertionName: options.assertionName, 255 + }) 256 + 257 + return { 258 + pass, 259 + message: () => `Snapshot \`${key}\` mismatched`, 260 + actual: actual?.trim(), 261 + expected: expected?.trim(), 262 + } 263 + } 264 + 265 + async pollMatchDomain(options: AssertDomainPollOptions): Promise<MatchResult> { 266 + const { 267 + poll, 268 + filepath, 269 + name, 270 + testId = name, 271 + message, 272 + adapter, 273 + isInline = false, 274 + inlineSnapshot, 275 + error, 276 + timeout = 1000, 277 + interval = 50, 278 + } = options 279 + 280 + if (!filepath) { 281 + throw new Error('Snapshot cannot be used outside of test') 282 + } 283 + 284 + const snapshotState = this.getSnapshotState(filepath) 285 + const testName = [name, ...(message ? [message] : [])].join(' > ') 286 + 287 + const expectedSnapshot = snapshotState.probeExpectedSnapshot({ 288 + testName, 289 + testId, 290 + isInline, 291 + inlineSnapshot, 292 + }) 293 + 294 + const reference = expectedSnapshot.data && snapshotState.snapshotUpdateState !== 'all' 295 + ? adapter.parseExpected(expectedSnapshot.data) 296 + : undefined 297 + const timedOut = timeout > 0 298 + ? new Promise<void>(r => setTimeout(r, timeout)) 299 + : undefined 300 + const stableResult = await getStableSnapshot({ 301 + adapter, 302 + poll, 303 + interval, 304 + timedOut, 305 + match: reference 306 + ? captured => adapter.match(captured, reference).pass 307 + : undefined, 308 + }) 309 + 310 + expectedSnapshot.markAsChecked() 311 + 312 + if (!stableResult?.rendered) { 313 + // the original caller `expect.poll` later manipulates error via `throwWithCause`, 314 + // so here we can directly throw `lastPollError` if exists. 315 + if (stableResult?.lastPollError) { 316 + throw stableResult.lastPollError 317 + } 318 + return { 319 + pass: false, 320 + message: () => `poll() did not produce a stable snapshot within the timeout`, 321 + } 322 + } 323 + 324 + // TODO: should `all` mode ignore parse error? 325 + // Sielently hiding the error and creating snaphsot full scratch isn't good either. 326 + // Users can fix or purge the broken snapshot manually and that decision affects how domain snapshot gets updated. 327 + const matchResult = expectedSnapshot.data 328 + ? adapter.match(stableResult.captured, adapter.parseExpected(expectedSnapshot.data)) 329 + : undefined 330 + const { actual, expected, key, pass } = snapshotState.processDomainSnapshot({ 331 + testId, 332 + received: stableResult.rendered, 333 + expectedSnapshot, 334 + matchResult, 335 + isInline, 336 + error, 337 + assertionName: options.assertionName, 338 + }) 339 + 340 + return { 341 + pass, 342 + message: () => `Snapshot \`${key}\` mismatched`, 343 + actual: actual?.trim(), 344 + expected: expected?.trim(), 345 + } 346 + } 347 + 202 348 async assertRaw(options: AssertOptions): Promise<void> { 203 349 if (!options.rawSnapshot) { 204 350 throw new Error('Raw snapshot is required') ··· 231 377 clear(): void { 232 378 this.snapshotStateMap.clear() 233 379 } 380 + } 381 + 382 + /** 383 + * Polls repeatedly until the value reaches a stable state. 384 + * 385 + * Compares consecutive rendered outputs from the current session — 386 + * when two consecutive polls produce the same rendered string, 387 + * the value is considered stable. 388 + * 389 + * Every `await` (poll call, interval delay) races against `timedOut` 390 + * so that hanging polls and delays are interrupted. 391 + */ 392 + async function getStableSnapshot( 393 + { adapter, poll, interval, timedOut, match }: { 394 + adapter: DomainSnapshotAdapter<any, any> 395 + poll: () => Promise<unknown> | unknown 396 + interval: number 397 + timedOut?: Promise<void> 398 + match?: (captured: unknown) => boolean 399 + }, 400 + ) { 401 + let lastRendered: string | undefined 402 + let lastPollError: unknown 403 + let lastStable: { captured: unknown; rendered: string } | undefined 404 + 405 + while (true) { 406 + try { 407 + const pollResult = await raceWith(Promise.resolve(poll()), timedOut) 408 + if (!pollResult.ok) { 409 + break 410 + } 411 + const captured = adapter.capture(pollResult.value) 412 + const rendered = adapter.render(captured) 413 + if (lastRendered !== undefined && rendered === lastRendered) { 414 + lastStable = { captured, rendered } 415 + if (!match || match(captured)) { 416 + break 417 + } 418 + } 419 + else { 420 + lastRendered = rendered 421 + lastStable = undefined 422 + } 423 + } 424 + catch (pollError) { 425 + // poll() threw — reset stability baseline and retry 426 + lastRendered = undefined 427 + lastStable = undefined 428 + lastPollError = pollError 429 + } 430 + const delayed = await raceWith( 431 + new Promise<void>(r => setTimeout(r, interval)), 432 + timedOut, 433 + ) 434 + if (!delayed.ok) { 435 + break 436 + } 437 + } 438 + 439 + return { ...lastStable, lastPollError } 440 + } 441 + 442 + /** Type-safe `Promise.race` — tells you which promise won. */ 443 + function raceWith<A, B>( 444 + promise: Promise<A>, 445 + other?: Promise<B>, 446 + ): Promise<{ ok: true; value: A } | { ok: false; value: B }> { 447 + const left = promise.then(value => ({ ok: true as const, value })) 448 + if (!other) { 449 + return left 450 + } 451 + return Promise.race([ 452 + left, 453 + other.then(value => ({ ok: false as const, value })), 454 + ]) 234 455 }
+39
packages/snapshot/src/domain.ts
··· 1 + export interface DomainMatchResult { 2 + pass: boolean 3 + message?: string 4 + /** 5 + * The captured value viewed through the template's lens. 6 + * 7 + * Where the template uses patterns (e.g. regexes) or omits details, 8 + * the resolved string adopts those patterns. Where the template doesn't 9 + * match, the resolved string uses literal captured values instead. 10 + * 11 + * Used for two purposes: 12 + * - **Diff display** (actual side): compared against `expected` 13 + * so the diff highlights only genuine mismatches, not pattern-vs-literal noise. 14 + * - **Snapshot update** (`--update`): written as the new snapshot content, 15 + * preserving user-edited patterns from matched regions while incorporating 16 + * actual values for mismatched regions. 17 + * 18 + * When omitted, falls back to `render(capture(received))` (the raw rendered value). 19 + */ 20 + resolved?: string 21 + /** 22 + * The stored template re-rendered as a string, representing what the user 23 + * originally wrote or last saved. 24 + * 25 + * Used as the expected side in diff display. 26 + * 27 + * When omitted, falls back to the raw snapshot string from the snap file 28 + * or inline snapshot. 29 + */ 30 + expected?: string 31 + } 32 + 33 + export interface DomainSnapshotAdapter<Captured = unknown, Expected = unknown> { 34 + name: string 35 + capture: (received: unknown) => Captured 36 + render: (captured: Captured) => string 37 + parseExpected: (input: string) => Expected 38 + match: (captured: Captured, expected: Expected) => DomainMatchResult 39 + }
+5
packages/snapshot/src/index.ts
··· 1 1 export { SnapshotClient } from './client' 2 2 export type { MatchResult } from './client' 3 3 4 + export type { 5 + DomainMatchResult, 6 + DomainSnapshotAdapter, 7 + } from './domain' 8 + 4 9 export { stripSnapshotIndentation } from './port/inlineSnapshot' 5 10 export { addSerializer, getSerializers } from './port/plugins' 6 11 export { default as SnapshotState } from './port/state'
+278
test/browser/specs/aria-snapshot.test.ts
··· 1 + import fs, { readFileSync } from 'node:fs' 2 + import { join } from 'node:path' 3 + import { expect, test } from 'vitest' 4 + import { rolldownVersion } from 'vitest/node' 5 + import { editFile } from '../../test-utils' 6 + import { instances, runBrowserTests } from './utils' 7 + 8 + const dir = join(import.meta.dirname, '../fixtures/aria-snapshot') 9 + 10 + function extractInlineSnaphsots(code: string) { 11 + const matches = Array.from( 12 + code.matchAll(/\.toMatchAriaInlineSnapshot\(\s*`[\s\S]*?`\s*\)/g), 13 + ) 14 + const snapshots = matches.map((match) => { 15 + const end = match.index! + match[0].length 16 + const start = code.lastIndexOf('expect', match.index) 17 + if (start === -1) { 18 + throw new Error(`Failed to extract inline snapshot: no expect found for match ${match[0]}`) 19 + } 20 + return code.slice(start, end) 21 + }) 22 + return `\n${snapshots.join('\n\n')}\n` 23 + } 24 + 25 + test.for(instances.map(i => i.browser))('aria snapshot %s', async (browser) => { 26 + const testFile = join(dir, 'basic.test.ts') 27 + const snapshotFile = join(dir, '__snapshots__/basic.test.ts.snap') 28 + 29 + // clean slate — remove file snapshots and clear inline snapshots 30 + fs.rmSync(join(dir, '__snapshots__'), { recursive: true, force: true }) 31 + editFile(testFile, s => 32 + s.replace(/toMatchAriaInlineSnapshot\(`[^`]*`\)/g, 'toMatchAriaInlineSnapshot()')) 33 + 34 + // run with update: new — creates snapshots from scratch 35 + let result = await runBrowserTests({ 36 + root: './fixtures/aria-snapshot', 37 + project: [browser], 38 + update: 'new', 39 + }) 40 + expect(result.stderr).toMatchInlineSnapshot(`""`) 41 + expect(result.errorTree()).toMatchInlineSnapshot(` 42 + { 43 + "basic.test.ts": { 44 + "expect.element aria once": "passed", 45 + "expect.element aria retry": "passed", 46 + "poll aria once": "passed", 47 + "toMatchAriaInlineSnapshot simple": "passed", 48 + "toMatchAriaSnapshot simple": "passed", 49 + }, 50 + } 51 + `) 52 + expect(readFileSync(snapshotFile, 'utf-8')).toMatchInlineSnapshot(` 53 + "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 54 + 55 + exports[\`toMatchAriaSnapshot simple 1\`] = \` 56 + - main: 57 + - heading "Dashboard" [level=1] 58 + - navigation "Actions": 59 + - button "Save" 60 + - button "Cancel" 61 + \`; 62 + " 63 + `) 64 + expect(extractInlineSnaphsots(readFileSync(testFile, 'utf-8'))).toMatchInlineSnapshot(` 65 + " 66 + expect(document.body).toMatchAriaInlineSnapshot(\` 67 + - paragraph: Original 68 + - button "1234": Pattern 69 + \`) 70 + 71 + expect.poll(async () => { 72 + document.body.innerHTML = \`<p>poll once</p>\` 73 + return document.body 74 + }).toMatchAriaInlineSnapshot(\`- paragraph: poll once\`) 75 + 76 + expect.element(page.getByTestId('body')).toMatchAriaInlineSnapshot(\` 77 + - heading "Hello" [level=1] 78 + - paragraph: World 79 + \`) 80 + 81 + expect.element(page.getByTestId('body'), { interval: 20 }) 82 + .toMatchAriaInlineSnapshot(\` 83 + - heading "Hello" [level=1] 84 + - paragraph: World 85 + \`) 86 + " 87 + `) 88 + 89 + // run with update: none — all should pass 90 + const result2 = await runBrowserTests({ 91 + root: './fixtures/aria-snapshot', 92 + project: [browser], 93 + update: 'none', 94 + }) 95 + expect(result2.stderr).toEqual(result.stderr) 96 + expect(result2.errorTree()).toEqual(result.errorTree()) 97 + 98 + // edit snapshots to add regex patterns, run with none — should still pass 99 + editFile(snapshotFile, s => s.replace(`navigation "Actions"`, 'navigation /A\\\\w+/')) 100 + editFile(testFile, s => s.replace(`- button "1234"`, '- button /\\\\d+/')) 101 + 102 + const result3 = await runBrowserTests({ 103 + root: './fixtures/aria-snapshot', 104 + project: [browser], 105 + update: 'none', 106 + }) 107 + expect(result3.stderr).toEqual(result.stderr) 108 + expect(result3.errorTree()).toEqual(result.errorTree()) 109 + 110 + // edit test HTML to break a literal match, run with none — should fail 111 + editFile(testFile, s => s.replace(`aria-label="Actions"`, `aria-label="EDITED"`)) 112 + editFile(testFile, s => s.replace('<p>Original</p>', '<p>Changed</p>')) 113 + 114 + result = await runBrowserTests({ 115 + root: './fixtures/aria-snapshot', 116 + project: [browser], 117 + update: 'none', 118 + }) 119 + if (browser === 'webkit') { 120 + if (rolldownVersion) { 121 + expect(result.errorTree({ stackTrace: true, diff: true })).toMatchInlineSnapshot(` 122 + { 123 + "basic.test.ts": { 124 + "expect.element aria once": "passed", 125 + "expect.element aria retry": "passed", 126 + "poll aria once": "passed", 127 + "toMatchAriaInlineSnapshot simple": [ 128 + "Snapshot \`toMatchAriaInlineSnapshot simple 1\` mismatched 129 + - Expected 130 + + Received 131 + 132 + - - paragraph: Original 133 + + - paragraph: Changed 134 + - button /\\d+/: Pattern 135 + at basic.test.ts:22:50", 136 + ], 137 + "toMatchAriaSnapshot simple": [ 138 + "Snapshot \`toMatchAriaSnapshot simple 1\` mismatched 139 + - Expected 140 + + Received 141 + 142 + - main: 143 + - heading "Dashboard" [level=1] 144 + - - navigation /A\\w+/: 145 + + - navigation "EDITED": 146 + - button "Save" 147 + - button "Cancel" 148 + at basic.test.ts:14:24", 149 + ], 150 + }, 151 + } 152 + `) 153 + } 154 + else { 155 + expect(result.errorTree({ stackTrace: true, diff: true })).toMatchInlineSnapshot(` 156 + { 157 + "basic.test.ts": { 158 + "expect.element aria once": "passed", 159 + "expect.element aria retry": "passed", 160 + "poll aria once": "passed", 161 + "toMatchAriaInlineSnapshot simple": [ 162 + "Snapshot \`toMatchAriaInlineSnapshot simple 1\` mismatched 163 + - Expected 164 + + Received 165 + 166 + - - paragraph: Original 167 + + - paragraph: Changed 168 + - button /\\d+/: Pattern 169 + at basic.test.ts:22:50", 170 + ], 171 + "toMatchAriaSnapshot simple": [ 172 + "Snapshot \`toMatchAriaSnapshot simple 1\` mismatched 173 + - Expected 174 + + Received 175 + 176 + - main: 177 + - heading "Dashboard" [level=1] 178 + - - navigation /A\\w+/: 179 + + - navigation "EDITED": 180 + - button "Save" 181 + - button "Cancel" 182 + at basic.test.ts:14:44", 183 + ], 184 + }, 185 + } 186 + `) 187 + } 188 + } 189 + else { 190 + expect(result.errorTree({ stackTrace: true, diff: true })).toMatchInlineSnapshot(` 191 + { 192 + "basic.test.ts": { 193 + "expect.element aria once": "passed", 194 + "expect.element aria retry": "passed", 195 + "poll aria once": "passed", 196 + "toMatchAriaInlineSnapshot simple": [ 197 + "Snapshot \`toMatchAriaInlineSnapshot simple 1\` mismatched 198 + - Expected 199 + + Received 200 + 201 + - - paragraph: Original 202 + + - paragraph: Changed 203 + - button /\\d+/: Pattern 204 + at basic.test.ts:22:24", 205 + ], 206 + "toMatchAriaSnapshot simple": [ 207 + "Snapshot \`toMatchAriaSnapshot simple 1\` mismatched 208 + - Expected 209 + + Received 210 + 211 + - main: 212 + - heading "Dashboard" [level=1] 213 + - - navigation /A\\w+/: 214 + + - navigation "EDITED": 215 + - button "Save" 216 + - button "Cancel" 217 + at basic.test.ts:14:24", 218 + ], 219 + }, 220 + } 221 + `) 222 + } 223 + 224 + // run with update: all — should pass, preserve regex, update mismatched literal 225 + result = await runBrowserTests({ 226 + root: './fixtures/aria-snapshot', 227 + project: [browser], 228 + update: 'all', 229 + }) 230 + expect(result.stderr).toMatchInlineSnapshot(`""`) 231 + expect(result.errorTree()).toMatchInlineSnapshot(` 232 + { 233 + "basic.test.ts": { 234 + "expect.element aria once": "passed", 235 + "expect.element aria retry": "passed", 236 + "poll aria once": "passed", 237 + "toMatchAriaInlineSnapshot simple": "passed", 238 + "toMatchAriaSnapshot simple": "passed", 239 + }, 240 + } 241 + `) 242 + expect(readFileSync(snapshotFile, 'utf-8')).toMatchInlineSnapshot(` 243 + "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 244 + 245 + exports[\`toMatchAriaSnapshot simple 1\`] = \` 246 + - main: 247 + - heading "Dashboard" [level=1] 248 + - navigation "EDITED": 249 + - button "Save" 250 + - button "Cancel" 251 + \`; 252 + " 253 + `) 254 + expect(extractInlineSnaphsots(readFileSync(testFile, 'utf-8'))).toMatchInlineSnapshot(` 255 + " 256 + expect(document.body).toMatchAriaInlineSnapshot(\` 257 + - paragraph: Changed 258 + - button /\\\\d+/: Pattern 259 + \`) 260 + 261 + expect.poll(async () => { 262 + document.body.innerHTML = \`<p>poll once</p>\` 263 + return document.body 264 + }).toMatchAriaInlineSnapshot(\`- paragraph: poll once\`) 265 + 266 + expect.element(page.getByTestId('body')).toMatchAriaInlineSnapshot(\` 267 + - heading "Hello" [level=1] 268 + - paragraph: World 269 + \`) 270 + 271 + expect.element(page.getByTestId('body'), { interval: 20 }) 272 + .toMatchAriaInlineSnapshot(\` 273 + - heading "Hello" [level=1] 274 + - paragraph: World 275 + \`) 276 + " 277 + `) 278 + })
+323
test/snapshots/test/domain-aria-inline.test.ts
··· 1 + import { join } from 'node:path' 2 + import { playwright } from '@vitest/browser-playwright' 3 + import { expect, test } from 'vitest' 4 + import { editFile, runInlineTests, runVitest } from '../../test-utils' 5 + import { readInlineSnapshots } from './utils' 6 + 7 + test('aria inline snapshot', async () => { 8 + const root = join(import.meta.dirname, 'fixtures/domain-aria-inline') 9 + const testFile = join(root, 'basic.test.ts') 10 + 11 + // purge inline snapshots to empty strings, restore test values 12 + editFile(testFile, s => s 13 + .replace(/toMatchAriaInlineSnapshot\(`[^`]*`/g, 'toMatchAriaInlineSnapshot(')) 14 + 15 + // create snapshots from scratch 16 + let result = await runVitest({ root, update: 'new' }) 17 + expect(result.stderr).toMatchInlineSnapshot(`""`) 18 + expect(result.errorTree()).toMatchInlineSnapshot(` 19 + Object { 20 + "basic.test.ts": Object { 21 + "semantic match with regex in snapshot": "passed", 22 + "simple heading": "passed", 23 + }, 24 + } 25 + `) 26 + expect(readInlineSnapshots(testFile)).toMatchInlineSnapshot(` 27 + " 28 + expect(document.body).toMatchAriaInlineSnapshot(\` 29 + - heading "Hello World" [level=1] 30 + - paragraph: Some description 31 + \`) 32 + 33 + expect(document.body).toMatchAriaInlineSnapshot(\` 34 + - paragraph: Original 35 + - button "1234": Pattern 36 + \`) 37 + " 38 + `) 39 + expect(result.ctx?.snapshot.summary).toMatchInlineSnapshot(` 40 + Object { 41 + "added": 2, 42 + "didUpdate": false, 43 + "failure": false, 44 + "filesAdded": 1, 45 + "filesRemoved": 0, 46 + "filesRemovedList": Array [], 47 + "filesUnmatched": 0, 48 + "filesUpdated": 0, 49 + "matched": 0, 50 + "total": 2, 51 + "unchecked": 0, 52 + "uncheckedKeysByFile": Array [], 53 + "unmatched": 0, 54 + "updated": 0, 55 + } 56 + `) 57 + 58 + // hand-edit inline snapshot to introduce regex pattern 59 + // "1234" -> /\\d+/ 60 + editFile(testFile, s => s 61 + .replace(`- button "1234"`, '- button /\\\\d+/')) 62 + 63 + // run without update — regex matches, all pass 64 + result = await runVitest({ root, update: 'none' }) 65 + expect(result.stderr).toMatchInlineSnapshot(`""`) 66 + expect(result.errorTree()).toMatchInlineSnapshot(` 67 + Object { 68 + "basic.test.ts": Object { 69 + "semantic match with regex in snapshot": "passed", 70 + "simple heading": "passed", 71 + }, 72 + } 73 + `) 74 + 75 + // edit test 76 + editFile(testFile, s => s 77 + .replace('<p>Original</p>', '<p>Changed</p>') 78 + .replace(`aria-label="1234"`, `aria-label="9999"`)) 79 + 80 + // run without update — literal mismatch causes failure 81 + result = await runVitest({ root, update: 'none' }) 82 + expect(result.stderr).toMatchInlineSnapshot(` 83 + " 84 + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ 85 + 86 + FAIL |chromium| basic.test.ts > semantic match with regex in snapshot 87 + Error: Snapshot \`semantic match with regex in snapshot 1\` mismatched 88 + 89 + Failure screenshot: 90 + - test/fixtures/domain-aria-inline/__screenshots__/basic.test.ts/semantic-match-with-regex-in-snapshot-1.png 91 + 92 + - Expected 93 + + Received 94 + 95 + - - paragraph: Original 96 + + - paragraph: Changed 97 + - button /\\d+/: Pattern 98 + 99 + ❯ basic.test.ts:19:24 100 + 17| <button aria-label="9999">Pattern</button> 101 + 18| \` 102 + 19| expect(document.body).toMatchAriaInlineSnapshot(\` 103 + | ^ 104 + 20| - paragraph: Original 105 + 21| - button /\\\\d+/: Pattern 106 + 107 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 108 + 109 + " 110 + `) 111 + expect(result.errorTree()).toMatchInlineSnapshot(` 112 + Object { 113 + "basic.test.ts": Object { 114 + "semantic match with regex in snapshot": Array [ 115 + "Snapshot \`semantic match with regex in snapshot 1\` mismatched", 116 + ], 117 + "simple heading": "passed", 118 + }, 119 + } 120 + `) 121 + 122 + // run with update — should overwrite inline snapshot 123 + result = await runVitest({ root, update: 'all' }) 124 + expect(result.stderr).toMatchInlineSnapshot(`""`) 125 + expect(result.errorTree()).toMatchInlineSnapshot(` 126 + Object { 127 + "basic.test.ts": Object { 128 + "semantic match with regex in snapshot": "passed", 129 + "simple heading": "passed", 130 + }, 131 + } 132 + `) 133 + 134 + // verify inline snapshot in source was rewritten correctly 135 + expect(readInlineSnapshots(testFile)).toMatchInlineSnapshot(` 136 + " 137 + expect(document.body).toMatchAriaInlineSnapshot(\` 138 + - heading "Hello World" [level=1] 139 + - paragraph: Some description 140 + \`) 141 + 142 + expect(document.body).toMatchAriaInlineSnapshot(\` 143 + - paragraph: Changed 144 + - button /\\\\d+/: Pattern 145 + \`) 146 + " 147 + `) 148 + }) 149 + 150 + test('domain multiple inline at same location - success', async () => { 151 + const result = await runInlineTests({ 152 + 'basic.test.ts': ` 153 + import { expect, test } from 'vitest'; 154 + 155 + test('basic', () => { 156 + for (let i = 0; i < 3; i++) { 157 + document.body.innerHTML = "<p>OK</p>"; 158 + expect(document.body).toMatchAriaInlineSnapshot(); 159 + } 160 + }); 161 + `, 162 + }, { 163 + browser: { 164 + enabled: true, 165 + headless: true, 166 + provider: playwright(), 167 + instances: [ 168 + { 169 + browser: 'chromium', 170 + }, 171 + ], 172 + }, 173 + update: 'new', 174 + }) 175 + expect(result.stderr).toMatchInlineSnapshot(`""`) 176 + expect(result.errorTree()).toMatchInlineSnapshot(` 177 + Object { 178 + "basic.test.ts": Object { 179 + "basic": "passed", 180 + }, 181 + } 182 + `) 183 + expect(result.fs.readFile('basic.test.ts')).toMatchInlineSnapshot(` 184 + " 185 + import { expect, test } from 'vitest'; 186 + 187 + test('basic', () => { 188 + for (let i = 0; i < 3; i++) { 189 + document.body.innerHTML = "<p>OK</p>"; 190 + expect(document.body).toMatchAriaInlineSnapshot(\`- paragraph: OK\`); 191 + } 192 + }); 193 + " 194 + `) 195 + }) 196 + 197 + test('domain multiple inline at same location - fail', async () => { 198 + const result = await runInlineTests({ 199 + 'basic.test.ts': ` 200 + import { expect, test } from 'vitest'; 201 + 202 + test('basic', () => { 203 + for (let i = 0; i < 3; i++) { 204 + document.body.innerHTML = "<p>count - " + i + "</p>"; 205 + expect(document.body).toMatchAriaInlineSnapshot(); 206 + } 207 + }); 208 + `, 209 + }, { 210 + browser: { 211 + enabled: true, 212 + headless: true, 213 + screenshotFailures: false, 214 + provider: playwright(), 215 + instances: [ 216 + { 217 + browser: 'chromium', 218 + }, 219 + ], 220 + }, 221 + update: 'new', 222 + }) 223 + expect(result.stderr).toMatchInlineSnapshot(` 224 + " 225 + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ 226 + 227 + FAIL |chromium| basic.test.ts > basic 228 + Error: toMatchAriaInlineSnapshot with different snapshots cannot be called at the same location 229 + 230 + - Expected 231 + + Received 232 + 233 + 234 + - - paragraph: count - 0 235 + + - paragraph: count - 1 236 + 237 + 238 + ❯ basic.test.ts:7:26 239 + 5| for (let i = 0; i < 3; i++) { 240 + 6| document.body.innerHTML = "<p>count - " + i + "</p>"; 241 + 7| expect(document.body).toMatchAriaInlineSnapshot(); 242 + | ^ 243 + 8| } 244 + 9| }); 245 + 246 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 247 + 248 + " 249 + `) 250 + expect(result.errorTree()).toMatchInlineSnapshot(` 251 + Object { 252 + "basic.test.ts": Object { 253 + "basic": Array [ 254 + "toMatchAriaInlineSnapshot with different snapshots cannot be called at the same location", 255 + ], 256 + }, 257 + } 258 + `) 259 + expect(result.fs.readFile('basic.test.ts')).toMatchInlineSnapshot(` 260 + " 261 + import { expect, test } from 'vitest'; 262 + 263 + test('basic', () => { 264 + for (let i = 0; i < 3; i++) { 265 + document.body.innerHTML = "<p>count - " + i + "</p>"; 266 + expect(document.body).toMatchAriaInlineSnapshot(); 267 + } 268 + }); 269 + " 270 + `) 271 + }) 272 + 273 + test('template parse error', async () => { 274 + const result = await runInlineTests({ 275 + 'basic.test.ts': ` 276 + import { expect, test } from 'vitest'; 277 + 278 + test('basic', () => { 279 + expect(document.body).toMatchAriaInlineSnapshot(\`x: y\`); 280 + }); 281 + `, 282 + }, { 283 + browser: { 284 + enabled: true, 285 + headless: true, 286 + screenshotFailures: false, 287 + provider: playwright(), 288 + instances: [ 289 + { 290 + browser: 'chromium', 291 + }, 292 + ], 293 + }, 294 + update: 'none', 295 + }) 296 + expect(result.stderr).toMatchInlineSnapshot(` 297 + " 298 + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ 299 + 300 + FAIL |chromium| basic.test.ts > basic 301 + Error: Aria snapshot must be a YAML sequence, elements starting with " -" 302 + ❯ basic.test.ts:5:24 303 + 3| 304 + 4| test('basic', () => { 305 + 5| expect(document.body).toMatchAriaInlineSnapshot(\`x: y\`); 306 + | ^ 307 + 6| }); 308 + 7| 309 + 310 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 311 + 312 + " 313 + `) 314 + expect(result.errorTree()).toMatchInlineSnapshot(` 315 + Object { 316 + "basic.test.ts": Object { 317 + "basic": Array [ 318 + "Aria snapshot must be a YAML sequence, elements starting with " -"", 319 + ], 320 + }, 321 + } 322 + `) 323 + })
+160
test/snapshots/test/domain-aria.test.ts
··· 1 + import fs, { readFileSync } from 'node:fs' 2 + import { join } from 'node:path' 3 + import { expect, test } from 'vitest' 4 + import { editFile, runVitest } from '../../test-utils' 5 + 6 + test('aria snapshot', async () => { 7 + const root = join(import.meta.dirname, 'fixtures/domain-aria') 8 + const testFile = join(root, 'basic.test.ts') 9 + const snapshotFile = join(root, '__snapshots__/basic.test.ts.snap') 10 + 11 + // clean slate 12 + fs.rmSync(join(root, '__snapshots__'), { recursive: true, force: true }) 13 + 14 + // create snapshots from scratch — literal rendered values 15 + let result = await runVitest({ root, update: 'new' }) 16 + expect(result.stderr).toMatchInlineSnapshot(`""`) 17 + expect(result.errorTree()).toMatchInlineSnapshot(` 18 + Object { 19 + "basic.test.ts": Object { 20 + "semantic match with regex in snapshot": "passed", 21 + "simple heading and paragraph": "passed", 22 + }, 23 + } 24 + `) 25 + expect(readFileSync(snapshotFile, 'utf-8')).toMatchInlineSnapshot(` 26 + "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 27 + 28 + exports[\`semantic match with regex in snapshot 1\`] = \` 29 + - paragraph: Original 30 + - button "1234": Pattern 31 + \`; 32 + 33 + exports[\`simple heading and paragraph 1\`] = \` 34 + - heading "Hello World" [level=1] 35 + - paragraph: Some description 36 + \`; 37 + " 38 + `) 39 + expect(result.ctx?.snapshot.summary).toMatchInlineSnapshot(` 40 + Object { 41 + "added": 2, 42 + "didUpdate": false, 43 + "failure": false, 44 + "filesAdded": 1, 45 + "filesRemoved": 0, 46 + "filesRemovedList": Array [], 47 + "filesUnmatched": 0, 48 + "filesUpdated": 0, 49 + "matched": 0, 50 + "total": 2, 51 + "unchecked": 0, 52 + "uncheckedKeysByFile": Array [], 53 + "unmatched": 0, 54 + "updated": 0, 55 + } 56 + `) 57 + 58 + // hand-edit snapshot to introduce regex patterns for "semantic match" test 59 + editFile(snapshotFile, s => s 60 + .replace(`- button "1234"`, `- button /\\\\d+/`)) 61 + 62 + // re-run without update — regex pattern matches, all pass, snapshot unchanged 63 + result = await runVitest({ root, update: 'none' }) 64 + expect(result.stderr).toMatchInlineSnapshot(`""`) 65 + expect(result.errorTree()).toMatchInlineSnapshot(` 66 + Object { 67 + "basic.test.ts": Object { 68 + "semantic match with regex in snapshot": "passed", 69 + "simple heading and paragraph": "passed", 70 + }, 71 + } 72 + `) 73 + expect(readFileSync(snapshotFile, 'utf-8')).toMatchInlineSnapshot(` 74 + "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 75 + 76 + exports[\`semantic match with regex in snapshot 1\`] = \` 77 + - paragraph: Original 78 + - button /\\\\d+/: Pattern 79 + \`; 80 + 81 + exports[\`simple heading and paragraph 1\`] = \` 82 + - heading "Hello World" [level=1] 83 + - paragraph: Some description 84 + \`; 85 + " 86 + `) 87 + 88 + // edit test 89 + editFile(testFile, s => s 90 + .replace('<p>Original</p>', '<p>Changed</p>') 91 + .replace(`aria-label="1234"`, `aria-label="9999"`)) 92 + 93 + // run without update — literal mismatch causes failure 94 + result = await runVitest({ root, update: 'none' }) 95 + expect(result.stderr).toMatchInlineSnapshot(` 96 + " 97 + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ 98 + 99 + FAIL |chromium| basic.test.ts > semantic match with regex in snapshot 100 + Error: Snapshot \`semantic match with regex in snapshot 1\` mismatched 101 + 102 + Failure screenshot: 103 + - test/fixtures/domain-aria/__screenshots__/basic.test.ts/semantic-match-with-regex-in-snapshot-1.png 104 + 105 + - Expected 106 + + Received 107 + 108 + - - paragraph: Original 109 + + - paragraph: Changed 110 + - button /\\d+/: Pattern 111 + 112 + ❯ basic.test.ts:20:24 113 + 18| <button aria-label="9999">Pattern</button> 114 + 19| \` 115 + 20| expect(document.body).toMatchAriaSnapshot() 116 + | ^ 117 + 21| }) 118 + 22| 119 + 120 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 121 + 122 + " 123 + `) 124 + expect(result.errorTree()).toMatchInlineSnapshot(` 125 + Object { 126 + "basic.test.ts": Object { 127 + "semantic match with regex in snapshot": Array [ 128 + "Snapshot \`semantic match with regex in snapshot 1\` mismatched", 129 + ], 130 + "simple heading and paragraph": "passed", 131 + }, 132 + } 133 + `) 134 + 135 + // run with update 136 + result = await runVitest({ root, update: 'all' }) 137 + expect(result.stderr).toMatchInlineSnapshot(`""`) 138 + expect(result.errorTree()).toMatchInlineSnapshot(` 139 + Object { 140 + "basic.test.ts": Object { 141 + "semantic match with regex in snapshot": "passed", 142 + "simple heading and paragraph": "passed", 143 + }, 144 + } 145 + `) 146 + expect(readFileSync(snapshotFile, 'utf-8')).toMatchInlineSnapshot(` 147 + "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 148 + 149 + exports[\`semantic match with regex in snapshot 1\`] = \` 150 + - paragraph: Changed 151 + - button /\\\\d+/: Pattern 152 + \`; 153 + 154 + exports[\`simple heading and paragraph 1\`] = \` 155 + - heading "Hello World" [level=1] 156 + - paragraph: Some description 157 + \`; 158 + " 159 + `) 160 + })
+132
test/snapshots/test/domain-inline.test.ts
··· 1 + import { join } from 'node:path' 2 + import { expect, test } from 'vitest' 3 + import { editFile, runVitest } from '../../test-utils' 4 + import { readInlineSnapshots } from './utils' 5 + 6 + test('domain inline snapshot', async () => { 7 + const root = join(import.meta.dirname, 'fixtures/domain-inline') 8 + const testFile = join(root, 'basic.test.ts') 9 + 10 + // purge inline snapshots to empty strings, restore test values 11 + editFile(testFile, s => s 12 + .replace(/toMatchKvInlineSnapshot\(`[^`]*`/g, 'toMatchKvInlineSnapshot(')) 13 + 14 + // create snapshots from scratch 15 + let result = await runVitest({ root, update: 'new' }) 16 + expect(result.stderr).toMatchInlineSnapshot(`""`) 17 + expect(result.errorTree()).toMatchInlineSnapshot(` 18 + Object { 19 + "basic.test.ts": Object { 20 + "all literal": "passed", 21 + "with regex": "passed", 22 + }, 23 + } 24 + `) 25 + expect(readInlineSnapshots(testFile)).toMatchInlineSnapshot(` 26 + " 27 + expect({ name: 'alice', age: '30' }).toMatchKvInlineSnapshot(\` 28 + name=alice 29 + age=30 30 + \`) 31 + 32 + expect({ name: 'bob', score: '999', status: 'active' }).toMatchKvInlineSnapshot(\` 33 + name=bob 34 + score=999 35 + status=active 36 + \`) 37 + " 38 + `) 39 + 40 + // hand-edit inline snapshot to introduce regex pattern 41 + // score=999 -> score=/\\d+/ 42 + editFile(testFile, s => s 43 + .replace('score=999', 'score=/\\\\d+/')) 44 + 45 + // run without update — regex matches, all pass 46 + result = await runVitest({ root, update: 'none' }) 47 + expect(result.stderr).toMatchInlineSnapshot(`""`) 48 + expect(result.errorTree()).toMatchInlineSnapshot(` 49 + Object { 50 + "basic.test.ts": Object { 51 + "all literal": "passed", 52 + "with regex": "passed", 53 + }, 54 + } 55 + `) 56 + 57 + // edit test values: score '999' -> '42' (regex still matches), 58 + // status 'active' -> 'inactive' (literal mismatch) 59 + editFile(testFile, s => s 60 + .replace(`score: '999'`, `score: '42'`) 61 + .replace(`status: 'active'`, `status: 'inactive'`)) 62 + 63 + // run without update — status mismatch causes failure 64 + result = await runVitest({ root, update: 'none' }) 65 + expect(result.stderr).toMatchInlineSnapshot(` 66 + " 67 + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ 68 + 69 + FAIL basic.test.ts > with regex 70 + Error: Snapshot \`with regex 1\` mismatched 71 + 72 + - Expected 73 + + Received 74 + 75 + name=bob 76 + score=/\\d+/ 77 + - status=active 78 + + status=inactive 79 + 80 + ❯ basic.test.ts:12:60 81 + 10| 82 + 11| test('with regex', () => { 83 + 12| expect({ name: 'bob', score: '42', status: 'inactive' }).toMatchKvIn… 84 + | ^ 85 + 13| name=bob 86 + 14| score=/\\\\d+/ 87 + 88 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 89 + 90 + " 91 + `) 92 + expect(result.errorTree()).toMatchInlineSnapshot(` 93 + Object { 94 + "basic.test.ts": Object { 95 + "all literal": "passed", 96 + "with regex": Array [ 97 + "Snapshot \`with regex 1\` mismatched", 98 + ], 99 + }, 100 + } 101 + `) 102 + 103 + // run with update — should preserve score regex (matched), 104 + // overwrite status with literal (didn't match) 105 + result = await runVitest({ root, update: 'all' }) 106 + expect(result.stderr).toMatchInlineSnapshot(`""`) 107 + expect(result.errorTree()).toMatchInlineSnapshot(` 108 + Object { 109 + "basic.test.ts": Object { 110 + "all literal": "passed", 111 + "with regex": "passed", 112 + }, 113 + } 114 + `) 115 + 116 + // verify inline snapshot in source was rewritten correctly 117 + // score regex preserved, status updated to 'inactive' 118 + expect(readInlineSnapshots(testFile)).toMatchInlineSnapshot(` 119 + " 120 + expect({ name: 'alice', age: '30' }).toMatchKvInlineSnapshot(\` 121 + name=alice 122 + age=30 123 + \`) 124 + 125 + expect({ name: 'bob', score: '42', status: 'inactive' }).toMatchKvInlineSnapshot(\` 126 + name=bob 127 + score=/\\\\d+/ 128 + status=inactive 129 + \`) 130 + " 131 + `) 132 + })
+435
test/snapshots/test/domain-poll-inline.test.ts
··· 1 + import { join } from 'node:path' 2 + import { expect, test } from 'vitest' 3 + import { editFile, runInlineTests, runVitest } from '../../test-utils' 4 + import { readInlineSnapshots } from './utils' 5 + 6 + test('domain inline snapshot with poll', async () => { 7 + const root = join(import.meta.dirname, 'fixtures/domain-poll-inline') 8 + const testFile = join(root, 'basic.test.ts') 9 + 10 + // purge inline snapshots to empty strings 11 + editFile(testFile, s => s 12 + .replace(/toMatchKvInlineSnapshot\(`[^`]*`/g, 'toMatchKvInlineSnapshot(')) 13 + 14 + // --- create snapshots (update: new) --- 15 + let result = await runVitest({ root, update: 'new' }) 16 + expect(result.stderr).toMatchInlineSnapshot(`""`) 17 + expect(result.errorTree()).toMatchInlineSnapshot(` 18 + Object { 19 + "basic.test.ts": Object { 20 + "multiple poll snapshots": "passed", 21 + "non-poll alongside poll": "passed", 22 + "stable": "passed", 23 + "throw then stable": "passed", 24 + "unstable then stable": "passed", 25 + }, 26 + } 27 + `) 28 + expect(readInlineSnapshots(testFile)).toMatchInlineSnapshot(` 29 + " 30 + expect.poll(() => { 31 + trial++ 32 + return { name: 'a', age: '23' } 33 + }, { interval: 10 }).toMatchKvInlineSnapshot(\` 34 + name=a 35 + age=23 36 + \`) 37 + 38 + expect.poll(() => { 39 + trial++ 40 + if (trial <= 3) { 41 + throw new Error(\`Fail at \${trial}\`) 42 + } 43 + return { name: 'b', age: '23' } 44 + }, { interval: 10 }).toMatchKvInlineSnapshot(\` 45 + name=b 46 + age=23 47 + \`) 48 + 49 + expect.poll(() => { 50 + trial++ 51 + if (trial <= 3) return { status: 'loading', trial } // unstable 52 + return { status: 'done' } // then stable 53 + }, { interval: 10 }).toMatchKvInlineSnapshot(\`status=done\`) 54 + 55 + expect.poll(() => { 56 + return { x: '1' } 57 + }, { interval: 10 }).toMatchKvInlineSnapshot(\`x=1\`) 58 + 59 + expect.poll(() => { 60 + return { y: '2' } 61 + }, { interval: 10 }).toMatchKvInlineSnapshot(\`y=2\`) 62 + 63 + expect({ static: 'value' }).toMatchKvInlineSnapshot(\`static=value\`) 64 + 65 + expect.poll(() => { 66 + return { polled: 'value' } 67 + }, { interval: 10 }).toMatchKvInlineSnapshot(\`polled=value\`) 68 + 69 + expect({ another: 'static' }).toMatchKvInlineSnapshot(\`another=static\`) 70 + " 71 + `) 72 + 73 + // --- re-run unchanged (update: none) --- 74 + result = await runVitest({ root, update: 'none' }) 75 + expect(result.stderr).toMatchInlineSnapshot(`""`) 76 + expect(result.errorTree()).toMatchInlineSnapshot(` 77 + Object { 78 + "basic.test.ts": Object { 79 + "multiple poll snapshots": "passed", 80 + "non-poll alongside poll": "passed", 81 + "stable": "passed", 82 + "throw then stable": "passed", 83 + "unstable then stable": "passed", 84 + }, 85 + } 86 + `) 87 + 88 + // --- mismatch — stable on wrong value --- 89 + editFile(testFile, s => s 90 + .replace('name=a\n', 'name=a-changed\n')) 91 + 92 + result = await runVitest({ root, update: 'none' }) 93 + expect(result.stderr).toMatchInlineSnapshot(` 94 + " 95 + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ 96 + 97 + FAIL basic.test.ts > stable 98 + Error: Snapshot \`stable 1\` mismatched 99 + 100 + - Expected 101 + + Received 102 + 103 + - name=a-changed 104 + + name=a 105 + age=23 106 + 107 + ❯ basic.test.ts:9:24 108 + 7| trial++ 109 + 8| return { name: 'a', age: '23' } 110 + 9| }, { interval: 10 }).toMatchKvInlineSnapshot(\` 111 + | ^ 112 + 10| name=a-changed 113 + 11| age=23 114 + 115 + Caused by: Error: Matcher did not succeed in time. 116 + ❯ basic.test.ts:6:3 117 + 118 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 119 + 120 + " 121 + `) 122 + expect(result.errorTree()).toMatchInlineSnapshot(` 123 + Object { 124 + "basic.test.ts": Object { 125 + "multiple poll snapshots": "passed", 126 + "non-poll alongside poll": "passed", 127 + "stable": Array [ 128 + "Snapshot \`stable 1\` mismatched", 129 + ], 130 + "throw then stable": "passed", 131 + "unstable then stable": "passed", 132 + }, 133 + } 134 + `) 135 + 136 + // --- update mode (update: all) --- 137 + result = await runVitest({ root, update: 'all' }) 138 + expect(result.stderr).toMatchInlineSnapshot(`""`) 139 + expect(result.errorTree()).toMatchInlineSnapshot(` 140 + Object { 141 + "basic.test.ts": Object { 142 + "multiple poll snapshots": "passed", 143 + "non-poll alongside poll": "passed", 144 + "stable": "passed", 145 + "throw then stable": "passed", 146 + "unstable then stable": "passed", 147 + }, 148 + } 149 + `) 150 + expect(readInlineSnapshots(testFile)).toMatchInlineSnapshot(` 151 + " 152 + expect.poll(() => { 153 + trial++ 154 + return { name: 'a', age: '23' } 155 + }, { interval: 10 }).toMatchKvInlineSnapshot(\` 156 + name=a 157 + age=23 158 + \`) 159 + 160 + expect.poll(() => { 161 + trial++ 162 + if (trial <= 3) { 163 + throw new Error(\`Fail at \${trial}\`) 164 + } 165 + return { name: 'b', age: '23' } 166 + }, { interval: 10 }).toMatchKvInlineSnapshot(\` 167 + name=b 168 + age=23 169 + \`) 170 + 171 + expect.poll(() => { 172 + trial++ 173 + if (trial <= 3) return { status: 'loading', trial } // unstable 174 + return { status: 'done' } // then stable 175 + }, { interval: 10 }).toMatchKvInlineSnapshot(\`status=done\`) 176 + 177 + expect.poll(() => { 178 + return { x: '1' } 179 + }, { interval: 10 }).toMatchKvInlineSnapshot(\`x=1\`) 180 + 181 + expect.poll(() => { 182 + return { y: '2' } 183 + }, { interval: 10 }).toMatchKvInlineSnapshot(\`y=2\`) 184 + 185 + expect({ static: 'value' }).toMatchKvInlineSnapshot(\`static=value\`) 186 + 187 + expect.poll(() => { 188 + return { polled: 'value' } 189 + }, { interval: 10 }).toMatchKvInlineSnapshot(\`polled=value\`) 190 + 191 + expect({ another: 'static' }).toMatchKvInlineSnapshot(\`another=static\`) 192 + " 193 + `) 194 + 195 + // --- pattern-preserving update --- 196 + editFile(testFile, s => s 197 + .replace('name=a\n', 'name=/\\\\w/\n')) 198 + 199 + result = await runVitest({ root, update: 'all' }) 200 + expect(result.stderr).toMatchInlineSnapshot(`""`) 201 + expect(result.errorTree()).toMatchInlineSnapshot(` 202 + Object { 203 + "basic.test.ts": Object { 204 + "multiple poll snapshots": "passed", 205 + "non-poll alongside poll": "passed", 206 + "stable": "passed", 207 + "throw then stable": "passed", 208 + "unstable then stable": "passed", 209 + }, 210 + } 211 + `) 212 + expect(readInlineSnapshots(testFile)).toMatchInlineSnapshot(` 213 + " 214 + expect.poll(() => { 215 + trial++ 216 + return { name: 'a', age: '23' } 217 + }, { interval: 10 }).toMatchKvInlineSnapshot(\` 218 + name=/\\\\w/ 219 + age=23 220 + \`) 221 + 222 + expect.poll(() => { 223 + trial++ 224 + if (trial <= 3) { 225 + throw new Error(\`Fail at \${trial}\`) 226 + } 227 + return { name: 'b', age: '23' } 228 + }, { interval: 10 }).toMatchKvInlineSnapshot(\` 229 + name=b 230 + age=23 231 + \`) 232 + 233 + expect.poll(() => { 234 + trial++ 235 + if (trial <= 3) return { status: 'loading', trial } // unstable 236 + return { status: 'done' } // then stable 237 + }, { interval: 10 }).toMatchKvInlineSnapshot(\`status=done\`) 238 + 239 + expect.poll(() => { 240 + return { x: '1' } 241 + }, { interval: 10 }).toMatchKvInlineSnapshot(\`x=1\`) 242 + 243 + expect.poll(() => { 244 + return { y: '2' } 245 + }, { interval: 10 }).toMatchKvInlineSnapshot(\`y=2\`) 246 + 247 + expect({ static: 'value' }).toMatchKvInlineSnapshot(\`static=value\`) 248 + 249 + expect.poll(() => { 250 + return { polled: 'value' } 251 + }, { interval: 10 }).toMatchKvInlineSnapshot(\`polled=value\`) 252 + 253 + expect({ another: 'static' }).toMatchKvInlineSnapshot(\`another=static\`) 254 + " 255 + `) 256 + }) 257 + 258 + test('poll until stable match when "none"', async () => { 259 + const result = await runInlineTests({ 260 + 'basic.test.ts': ` 261 + import { expect, test } from 'vitest' 262 + import '../test/fixtures/domain/basic-extend' 263 + 264 + test('stable wrong then right', async () => { 265 + let trial = 0 266 + await expect.poll(() => { 267 + trial++ 268 + if (trial <= 4) return { phase: 'pending' } 269 + return { phase: 'complete' } 270 + }, { interval: 10 }).toMatchKvInlineSnapshot(\` 271 + phase=complete 272 + \`) 273 + expect(trial).toBe(6) 274 + }) 275 + `, 276 + }, { 277 + update: 'none', 278 + }) 279 + expect(result.stderr).toMatchInlineSnapshot(`""`) 280 + expect(result.errorTree()).toMatchInlineSnapshot(` 281 + Object { 282 + "basic.test.ts": Object { 283 + "stable wrong then right": "passed", 284 + }, 285 + } 286 + `) 287 + }) 288 + 289 + test('poll until stable when "all"', async () => { 290 + const result = await runInlineTests({ 291 + 'basic.test.ts': ` 292 + import { expect, test } from 'vitest' 293 + import '../test/fixtures/domain/basic-extend' 294 + 295 + test('stable wrong then right', async () => { 296 + let trial = 0 297 + await expect.poll(() => { 298 + trial++ 299 + if (trial <= 4) return { phase: 'pending' } 300 + return { phase: 'complete' } 301 + }, { interval: 10 }).toMatchKvInlineSnapshot(\` 302 + phase=complete 303 + \`) 304 + expect(trial).toBe(2) 305 + }) 306 + `, 307 + }, { 308 + update: 'all', 309 + }) 310 + expect(result.stderr).toMatchInlineSnapshot(`""`) 311 + expect(result.errorTree()).toMatchInlineSnapshot(` 312 + Object { 313 + "basic.test.ts": Object { 314 + "stable wrong then right": "passed", 315 + }, 316 + } 317 + `) 318 + expect(result.fs.readFile('basic.test.ts')).toMatchInlineSnapshot(` 319 + " 320 + import { expect, test } from 'vitest' 321 + import '../test/fixtures/domain/basic-extend' 322 + 323 + test('stable wrong then right', async () => { 324 + let trial = 0 325 + await expect.poll(() => { 326 + trial++ 327 + if (trial <= 4) return { phase: 'pending' } 328 + return { phase: 'complete' } 329 + }, { interval: 10 }).toMatchKvInlineSnapshot(\`phase=pending\`) 330 + expect(trial).toBe(2) 331 + }) 332 + " 333 + `) 334 + }) 335 + 336 + test('errors', async () => { 337 + const result = await runInlineTests({ 338 + 'basic.test.ts': ` 339 + import { expect, test } from 'vitest' 340 + import '../test/fixtures/domain/basic-extend' 341 + 342 + test('unstable', async () => { 343 + let trial = 0 344 + await expect.poll(() => { 345 + trial++ 346 + return { name: 'x', counter: String(trial) } 347 + }, { timeout: 100, interval: 10 }).toMatchKvInlineSnapshot(\`\`) 348 + }) 349 + 350 + test('hanging', async () => { 351 + let trial = 0 352 + await expect.poll(() => { 353 + trial++ 354 + return new Promise(() => {}) 355 + }, { timeout: 100, interval: 10 }).toMatchKvInlineSnapshot(\`\`) 356 + }) 357 + 358 + test('throwing', async () => { 359 + let trial = 0 360 + await expect.poll(() => { 361 + trial++ 362 + throw new Error("ALWAYS_THROWS") 363 + }, { timeout: 100, interval: 10 }).toMatchKvInlineSnapshot(\`\`) 364 + }) 365 + `, 366 + }, { 367 + update: 'all', 368 + }) 369 + expect(result.stderr).toMatchInlineSnapshot(` 370 + " 371 + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 3 ⎯⎯⎯⎯⎯⎯⎯ 372 + 373 + FAIL basic.test.ts > unstable 374 + Error: poll() did not produce a stable snapshot within the timeout 375 + ❯ basic.test.ts:10:38 376 + 8| trial++ 377 + 9| return { name: 'x', counter: String(trial) } 378 + 10| }, { timeout: 100, interval: 10 }).toMatchKvInlineSnapshot(\`\`) 379 + | ^ 380 + 11| }) 381 + 12| 382 + 383 + Caused by: Error: Matcher did not succeed in time. 384 + ❯ basic.test.ts:7:3 385 + 386 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/3]⎯ 387 + 388 + FAIL basic.test.ts > hanging 389 + Error: poll() did not produce a stable snapshot within the timeout 390 + ❯ basic.test.ts:18:38 391 + 16| trial++ 392 + 17| return new Promise(() => {}) 393 + 18| }, { timeout: 100, interval: 10 }).toMatchKvInlineSnapshot(\`\`) 394 + | ^ 395 + 19| }) 396 + 20| 397 + 398 + Caused by: Error: Matcher did not succeed in time. 399 + ❯ basic.test.ts:15:3 400 + 401 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/3]⎯ 402 + 403 + FAIL basic.test.ts > throwing 404 + Error: ALWAYS_THROWS 405 + ❯ basic.test.ts:26:38 406 + 24| trial++ 407 + 25| throw new Error("ALWAYS_THROWS") 408 + 26| }, { timeout: 100, interval: 10 }).toMatchKvInlineSnapshot(\`\`) 409 + | ^ 410 + 27| }) 411 + 28| 412 + 413 + Caused by: Error: Matcher did not succeed in time. 414 + ❯ basic.test.ts:23:3 415 + 416 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/3]⎯ 417 + 418 + " 419 + `) 420 + expect(result.errorTree()).toMatchInlineSnapshot(` 421 + Object { 422 + "basic.test.ts": Object { 423 + "hanging": Array [ 424 + "poll() did not produce a stable snapshot within the timeout", 425 + ], 426 + "throwing": Array [ 427 + "ALWAYS_THROWS", 428 + ], 429 + "unstable": Array [ 430 + "poll() did not produce a stable snapshot within the timeout", 431 + ], 432 + }, 433 + } 434 + `) 435 + })
+436
test/snapshots/test/domain-poll.test.ts
··· 1 + /** 2 + * pollAssertDomain behavior matrix 3 + * 4 + * Stability requirement: poll must produce two consecutive identical renders. 5 + * When a reference exists (and not `update: all`), the stable value must also 6 + * pass `adapter.match` — otherwise the baseline resets and polling continues 7 + * through intermediate stable states. 8 + * 9 + * Poll behavior | no reference | has reference (match) | has reference (mismatch) 10 + * -----------------------|----------------------|------------------------|------------------------- 11 + * stable immediately | new: pass (creates) | none/new/all: pass | none/new: fail (diff) 12 + * | none: fail (missing) | | all: pass (rewrites) 13 + * | all: pass (creates) | | 14 + * throw then stable | same as above | pass (throw resets | same as above 15 + * | | baseline, re-polls) | 16 + * transitional → stable | same as above | pass (rides through | same as above 17 + * | | intermediate states) | 18 + * stable wrong → right | n/a (no ref to | pass (match rejects | n/a 19 + * | reject against) | wrong, rides through)| 20 + * never stabilizes | fail (unstable) | fail (unstable) | fail (unstable) 21 + * always throws | fail (unstable, | fail (unstable, | fail (unstable, 22 + * | cause: poll error) | cause: poll error) | cause: poll error) 23 + * poll hangs | fail (unstable) | fail (unstable) | fail (unstable) 24 + */ 25 + import fs, { readFileSync } from 'node:fs' 26 + import { join } from 'node:path' 27 + import { expect, test } from 'vitest' 28 + import { editFile, runInlineTests, runVitest } from '../../test-utils' 29 + 30 + test('domain snapshot with poll', async () => { 31 + const root = join(import.meta.dirname, 'fixtures/domain-poll') 32 + const snapshotFile = join(root, '__snapshots__/basic.test.ts.snap') 33 + 34 + // clean slate 35 + fs.rmSync(join(root, '__snapshots__'), { recursive: true, force: true }) 36 + 37 + // --- create snapshots (update: new) --- 38 + let result = await runVitest({ root, update: 'new' }) 39 + expect(result.stderr).toMatchInlineSnapshot(`""`) 40 + expect(result.errorTree()).toMatchInlineSnapshot(` 41 + Object { 42 + "basic.test.ts": Object { 43 + "multiple poll snapshots": "passed", 44 + "non-poll alongside poll": "passed", 45 + "stable": "passed", 46 + "throw then stable": "passed", 47 + "unstable then stable": "passed", 48 + }, 49 + } 50 + `) 51 + expect(readFileSync(snapshotFile, 'utf-8')).toMatchInlineSnapshot(` 52 + "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 53 + 54 + exports[\`multiple poll snapshots 1\`] = \` 55 + x=1 56 + \`; 57 + 58 + exports[\`multiple poll snapshots 2\`] = \` 59 + y=2 60 + \`; 61 + 62 + exports[\`non-poll alongside poll 1\`] = \` 63 + static=value 64 + \`; 65 + 66 + exports[\`non-poll alongside poll 2\`] = \` 67 + polled=value 68 + \`; 69 + 70 + exports[\`non-poll alongside poll 3\`] = \` 71 + another=static 72 + \`; 73 + 74 + exports[\`stable 1\`] = \` 75 + name=a 76 + age=23 77 + \`; 78 + 79 + exports[\`throw then stable 1\`] = \` 80 + name=b 81 + age=23 82 + \`; 83 + 84 + exports[\`unstable then stable 1\`] = \` 85 + status=done 86 + \`; 87 + " 88 + `) 89 + 90 + // --- re-run unchanged (update: none) --- 91 + const result2 = await runVitest({ root, update: 'none' }) 92 + expect(result2.stderr).toBe(result.stderr) 93 + expect(result2.errorTree()).toEqual(result.errorTree()) 94 + 95 + // --- mismatch — stable on wrong value --- 96 + // Edit reference so "stable" poll stabilizes but doesn't match 97 + // Should produce a mismatch error with diff, not "unstable" error 98 + editFile(snapshotFile, s => s 99 + .replace('name=a\n', 'name=a-changed\n')) 100 + 101 + result = await runVitest({ root, update: 'none' }) 102 + expect(result.stderr).toMatchInlineSnapshot(` 103 + " 104 + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ 105 + 106 + FAIL basic.test.ts > stable 107 + Error: Snapshot \`stable 1\` mismatched 108 + 109 + - Expected 110 + + Received 111 + 112 + - name=a-changed 113 + + name=a 114 + age=23 115 + 116 + ❯ basic.test.ts:9:24 117 + 7| trial++ 118 + 8| return { name: 'a', age: '23' } 119 + 9| }, { interval: 10 }).toMatchKvSnapshot() 120 + | ^ 121 + 10| expect(trial).toBe(2) 122 + 11| }) 123 + 124 + Caused by: Error: Matcher did not succeed in time. 125 + ❯ basic.test.ts:6:3 126 + 127 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 128 + 129 + " 130 + `) 131 + expect(result.errorTree()).toMatchInlineSnapshot(` 132 + Object { 133 + "basic.test.ts": Object { 134 + "multiple poll snapshots": "passed", 135 + "non-poll alongside poll": "passed", 136 + "stable": Array [ 137 + "Snapshot \`stable 1\` mismatched", 138 + ], 139 + "throw then stable": "passed", 140 + "unstable then stable": "passed", 141 + }, 142 + } 143 + `) 144 + 145 + // --- update mode (update: all) --- 146 + // Rewrite snapshots with current stable values 147 + result = await runVitest({ root, update: 'all' }) 148 + expect(result.stderr).toMatchInlineSnapshot(`""`) 149 + expect(result.errorTree()).toMatchInlineSnapshot(` 150 + Object { 151 + "basic.test.ts": Object { 152 + "multiple poll snapshots": "passed", 153 + "non-poll alongside poll": "passed", 154 + "stable": "passed", 155 + "throw then stable": "passed", 156 + "unstable then stable": "passed", 157 + }, 158 + } 159 + `) 160 + expect(readFileSync(snapshotFile, 'utf-8')).toMatchInlineSnapshot(` 161 + "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 162 + 163 + exports[\`multiple poll snapshots 1\`] = \` 164 + x=1 165 + \`; 166 + 167 + exports[\`multiple poll snapshots 2\`] = \` 168 + y=2 169 + \`; 170 + 171 + exports[\`non-poll alongside poll 1\`] = \` 172 + static=value 173 + \`; 174 + 175 + exports[\`non-poll alongside poll 2\`] = \` 176 + polled=value 177 + \`; 178 + 179 + exports[\`non-poll alongside poll 3\`] = \` 180 + another=static 181 + \`; 182 + 183 + exports[\`stable 1\`] = \` 184 + name=a 185 + age=23 186 + \`; 187 + 188 + exports[\`throw then stable 1\`] = \` 189 + name=b 190 + age=23 191 + \`; 192 + 193 + exports[\`unstable then stable 1\`] = \` 194 + status=done 195 + \`; 196 + " 197 + `) 198 + 199 + // --- pattern-preserving update --- 200 + // Inject regex pattern into snapshot, verify --update preserves it 201 + editFile(snapshotFile, s => s 202 + .replace('name=a\n', 'name=/\\\\w/\n')) 203 + 204 + result = await runVitest({ root, update: 'all' }) 205 + expect(result.stderr).toMatchInlineSnapshot(`""`) 206 + expect(result.errorTree()).toMatchInlineSnapshot(` 207 + Object { 208 + "basic.test.ts": Object { 209 + "multiple poll snapshots": "passed", 210 + "non-poll alongside poll": "passed", 211 + "stable": "passed", 212 + "throw then stable": "passed", 213 + "unstable then stable": "passed", 214 + }, 215 + } 216 + `) 217 + expect(readFileSync(snapshotFile, 'utf-8')).toMatchInlineSnapshot(` 218 + "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 219 + 220 + exports[\`multiple poll snapshots 1\`] = \` 221 + x=1 222 + \`; 223 + 224 + exports[\`multiple poll snapshots 2\`] = \` 225 + y=2 226 + \`; 227 + 228 + exports[\`non-poll alongside poll 1\`] = \` 229 + static=value 230 + \`; 231 + 232 + exports[\`non-poll alongside poll 2\`] = \` 233 + polled=value 234 + \`; 235 + 236 + exports[\`non-poll alongside poll 3\`] = \` 237 + another=static 238 + \`; 239 + 240 + exports[\`stable 1\`] = \` 241 + name=/\\\\w/ 242 + age=23 243 + \`; 244 + 245 + exports[\`throw then stable 1\`] = \` 246 + name=b 247 + age=23 248 + \`; 249 + 250 + exports[\`unstable then stable 1\`] = \` 251 + status=done 252 + \`; 253 + " 254 + `) 255 + }) 256 + 257 + test('poll until stable match when "none"', async () => { 258 + const result = await runInlineTests({ 259 + '__snapshots__/basic.test.ts.snap': `\ 260 + // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 261 + 262 + exports[\`stable wrong then right 1\`] = \` 263 + phase=complete 264 + \`; 265 + `, 266 + 'basic.test.ts': ` 267 + import { expect, test } from 'vitest' 268 + import '../test/fixtures/domain/basic-extend' 269 + 270 + test('stable wrong then right', async () => { 271 + let trial = 0 272 + await expect.poll(() => { 273 + trial++ 274 + if (trial <= 4) return { phase: 'pending' } 275 + return { phase: 'complete' } 276 + }, { interval: 10 }).toMatchKvSnapshot() 277 + expect(trial).toBe(6) 278 + }) 279 + `, 280 + }, { 281 + update: 'none', 282 + }) 283 + expect(result.stderr).toMatchInlineSnapshot(`""`) 284 + expect(result.errorTree()).toMatchInlineSnapshot(` 285 + Object { 286 + "basic.test.ts": Object { 287 + "stable wrong then right": "passed", 288 + }, 289 + } 290 + `) 291 + }) 292 + 293 + test('poll until stable when "all"', async () => { 294 + const result = await runInlineTests({ 295 + '__snapshots__/basic.test.ts.snap': `\ 296 + // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 297 + 298 + exports[\`stable wrong then right 1\`] = \` 299 + phase=complete 300 + \`; 301 + `, 302 + 'basic.test.ts': ` 303 + import { expect, test } from 'vitest' 304 + import '../test/fixtures/domain/basic-extend' 305 + 306 + test('stable wrong then right', async () => { 307 + let trial = 0 308 + await expect.poll(() => { 309 + trial++ 310 + if (trial <= 4) return { phase: 'pending' } 311 + return { phase: 'complete' } 312 + }, { interval: 10 }).toMatchKvSnapshot() 313 + expect(trial).toBe(2) 314 + }) 315 + `, 316 + }, { 317 + update: 'all', 318 + }) 319 + expect(result.stderr).toMatchInlineSnapshot(`""`) 320 + expect(result.errorTree()).toMatchInlineSnapshot(` 321 + Object { 322 + "basic.test.ts": Object { 323 + "stable wrong then right": "passed", 324 + }, 325 + } 326 + `) 327 + expect(result.fs.readFile('__snapshots__/basic.test.ts.snap')).toMatchInlineSnapshot(` 328 + "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 329 + 330 + exports[\`stable wrong then right 1\`] = \` 331 + phase=pending 332 + \`; 333 + " 334 + `) 335 + }) 336 + 337 + test('errors', async () => { 338 + const result = await runInlineTests({ 339 + 'basic.test.ts': ` 340 + import { expect, test } from 'vitest' 341 + import '../test/fixtures/domain/basic-extend' 342 + 343 + test('unstable', async () => { 344 + let trial = 0 345 + await expect.poll(() => { 346 + trial++ 347 + return { name: 'x', counter: String(trial) } 348 + }, { timeout: 100, interval: 10 }).toMatchKvSnapshot() 349 + }) 350 + 351 + test('hanging', async () => { 352 + let trial = 0 353 + await expect.poll(() => { 354 + trial++ 355 + return new Promise(() => {}) 356 + }, { timeout: 100, interval: 10 }).toMatchKvSnapshot() 357 + }) 358 + 359 + test('throwing', async () => { 360 + let trial = 0 361 + await expect.poll(() => { 362 + trial++ 363 + throw new Error("ALWAYS_THROWS") 364 + }, { timeout: 100, interval: 10 }).toMatchKvSnapshot() 365 + }) 366 + `, 367 + }, { 368 + update: 'all', 369 + }) 370 + expect(result.stderr).toMatchInlineSnapshot(` 371 + " 372 + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 3 ⎯⎯⎯⎯⎯⎯⎯ 373 + 374 + FAIL basic.test.ts > unstable 375 + Error: poll() did not produce a stable snapshot within the timeout 376 + ❯ basic.test.ts:10:38 377 + 8| trial++ 378 + 9| return { name: 'x', counter: String(trial) } 379 + 10| }, { timeout: 100, interval: 10 }).toMatchKvSnapshot() 380 + | ^ 381 + 11| }) 382 + 12| 383 + 384 + Caused by: Error: Matcher did not succeed in time. 385 + ❯ basic.test.ts:7:3 386 + 387 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/3]⎯ 388 + 389 + FAIL basic.test.ts > hanging 390 + Error: poll() did not produce a stable snapshot within the timeout 391 + ❯ basic.test.ts:18:38 392 + 16| trial++ 393 + 17| return new Promise(() => {}) 394 + 18| }, { timeout: 100, interval: 10 }).toMatchKvSnapshot() 395 + | ^ 396 + 19| }) 397 + 20| 398 + 399 + Caused by: Error: Matcher did not succeed in time. 400 + ❯ basic.test.ts:15:3 401 + 402 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/3]⎯ 403 + 404 + FAIL basic.test.ts > throwing 405 + Error: ALWAYS_THROWS 406 + ❯ basic.test.ts:26:38 407 + 24| trial++ 408 + 25| throw new Error("ALWAYS_THROWS") 409 + 26| }, { timeout: 100, interval: 10 }).toMatchKvSnapshot() 410 + | ^ 411 + 27| }) 412 + 28| 413 + 414 + Caused by: Error: Matcher did not succeed in time. 415 + ❯ basic.test.ts:23:3 416 + 417 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/3]⎯ 418 + 419 + " 420 + `) 421 + expect(result.errorTree()).toMatchInlineSnapshot(` 422 + Object { 423 + "basic.test.ts": Object { 424 + "hanging": Array [ 425 + "poll() did not produce a stable snapshot within the timeout", 426 + ], 427 + "throwing": Array [ 428 + "ALWAYS_THROWS", 429 + ], 430 + "unstable": Array [ 431 + "poll() did not produce a stable snapshot within the timeout", 432 + ], 433 + }, 434 + } 435 + `) 436 + })
+202
test/snapshots/test/domain.test.ts
··· 1 + import fs, { readFileSync } from 'node:fs' 2 + import { join } from 'node:path' 3 + import { expect, test } from 'vitest' 4 + import { editFile, runVitest } from '../../test-utils' 5 + 6 + test('domain snapshot', async () => { 7 + const root = join(import.meta.dirname, 'fixtures/domain') 8 + const testFile = join(root, 'basic.test.ts') 9 + const snapshotFile = join(root, '__snapshots__/basic.test.ts.snap') 10 + 11 + // clean slate 12 + fs.rmSync(join(root, '__snapshots__'), { recursive: true, force: true }) 13 + 14 + // create snapshots from scratch — literal rendered values 15 + let result = await runVitest({ root, update: 'new' }) 16 + expect(result.stderr).toMatchInlineSnapshot(`""`) 17 + expect(result.errorTree()).toMatchInlineSnapshot(` 18 + Object { 19 + "basic.test.ts": Object { 20 + "all literal": "passed", 21 + "with regex": "passed", 22 + }, 23 + } 24 + `) 25 + expect(readFileSync(snapshotFile, 'utf-8')).toMatchInlineSnapshot(` 26 + "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 27 + 28 + exports[\`all literal 1\`] = \` 29 + name=alice 30 + age=30 31 + \`; 32 + 33 + exports[\`with regex 1\`] = \` 34 + name=bob 35 + age=24 36 + score=999 37 + status=active 38 + \`; 39 + " 40 + `) 41 + 42 + // hand-edit snapshot 43 + editFile(snapshotFile, s => s 44 + // match any numbers match for score 45 + .replace('score=999', 'score=/\\\\d+/') 46 + .replace('age=24\n', '')) 47 + 48 + // re-run without update 49 + result = await runVitest({ root, update: 'none' }) 50 + expect(result.stderr).toMatchInlineSnapshot(`""`) 51 + expect(result.errorTree()).toMatchInlineSnapshot(` 52 + Object { 53 + "basic.test.ts": Object { 54 + "all literal": "passed", 55 + "with regex": "passed", 56 + }, 57 + } 58 + `) 59 + expect(readFileSync(snapshotFile, 'utf-8')).toMatchInlineSnapshot(` 60 + "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 61 + 62 + exports[\`all literal 1\`] = \` 63 + name=alice 64 + age=30 65 + \`; 66 + 67 + exports[\`with regex 1\`] = \` 68 + name=bob 69 + score=/\\\\d+/ 70 + status=active 71 + \`; 72 + " 73 + `) 74 + 75 + // edit test 76 + editFile(testFile, s => s 77 + .replace(`score: '999'`, `score: '42'`) 78 + .replace(`status: 'active'`, `status: 'inactive'`)) 79 + 80 + // run without update 81 + // (note that `age` and `score` is not in diff) 82 + result = await runVitest({ root, update: 'none' }) 83 + expect(result.stderr).toMatchInlineSnapshot(` 84 + " 85 + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ 86 + 87 + FAIL basic.test.ts > with regex 88 + Error: Snapshot \`with regex 1\` mismatched 89 + 90 + - Expected 91 + + Received 92 + 93 + name=bob 94 + score=/\\d+/ 95 + - status=active 96 + + status=inactive 97 + 98 + ❯ basic.test.ts:9:71 99 + 7| 100 + 8| test('with regex', () => { 101 + 9| expect({ name: 'bob', age: '24', score: '42', status: 'inactive' }).… 102 + | ^ 103 + 10| }) 104 + 11| 105 + 106 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 107 + 108 + " 109 + `) 110 + expect(result.errorTree()).toMatchInlineSnapshot(` 111 + Object { 112 + "basic.test.ts": Object { 113 + "all literal": "passed", 114 + "with regex": Array [ 115 + "Snapshot \`with regex 1\` mismatched", 116 + ], 117 + }, 118 + } 119 + `) 120 + 121 + // run with update — should preserve score regex (matched), 122 + // overwrite status with literal (didn't match) 123 + result = await runVitest({ root, update: 'all' }) 124 + expect(result.stderr).toMatchInlineSnapshot(`""`) 125 + expect(result.errorTree()).toMatchInlineSnapshot(` 126 + Object { 127 + "basic.test.ts": Object { 128 + "all literal": "passed", 129 + "with regex": "passed", 130 + }, 131 + } 132 + `) 133 + // NOTE 134 + // score regex matched '42' -> preserved as /\d+/ 135 + // status literal 'active' != 'inactive' -> overwritten with literal 136 + expect(readFileSync(snapshotFile, 'utf-8')).toMatchInlineSnapshot(` 137 + "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 138 + 139 + exports[\`all literal 1\`] = \` 140 + name=alice 141 + age=30 142 + \`; 143 + 144 + exports[\`with regex 1\`] = \` 145 + name=bob 146 + score=/\\\\d+/ 147 + status=inactive 148 + \`; 149 + " 150 + `) 151 + }) 152 + 153 + test('domain parseExpected error', async () => { 154 + const root = join(import.meta.dirname, 'fixtures/domain-error') 155 + const result = await runVitest({ root, update: 'none' }) 156 + expect(result.stderr).toMatchInlineSnapshot(` 157 + " 158 + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯ 159 + 160 + FAIL basic.test.ts > file 161 + Error: Invalid KV Format: 'file-broken' 162 + ❯ ../domain/basic.ts:34:15 163 + 32| const eq = line.indexOf('=') 164 + 33| if (eq === -1) { 165 + 34| throw new Error(\`Invalid KV Format: '\${line}'\`) 166 + | ^ 167 + 35| } 168 + 36| const key = line.slice(0, eq) 169 + ❯ Object.parseExpected ../domain/basic.ts:31:46 170 + ❯ Object.toMatchKvSnapshot ../domain/basic-extend.ts:16:44 171 + 172 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯ 173 + 174 + FAIL basic.test.ts > inline 175 + Error: Invalid KV Format: 'inine-broken' 176 + ❯ ../domain/basic.ts:34:15 177 + 32| const eq = line.indexOf('=') 178 + 33| if (eq === -1) { 179 + 34| throw new Error(\`Invalid KV Format: '\${line}'\`) 180 + | ^ 181 + 35| } 182 + 36| const key = line.slice(0, eq) 183 + ❯ Object.parseExpected ../domain/basic.ts:31:46 184 + ❯ Object.toMatchKvInlineSnapshot ../domain/basic-extend.ts:22:50 185 + 186 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯ 187 + 188 + " 189 + `) 190 + expect(result.errorTree()).toMatchInlineSnapshot(` 191 + Object { 192 + "basic.test.ts": Object { 193 + "file": Array [ 194 + "Invalid KV Format: 'file-broken'", 195 + ], 196 + "inline": Array [ 197 + "Invalid KV Format: 'inine-broken'", 198 + ], 199 + }, 200 + } 201 + `) 202 + })
+4 -4
test/snapshots/test/inline-multiple-calls.test.ts
··· 542 542 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/6]⎯ 543 543 544 544 FAIL each.test.ts > toThrowErrorMatchingInlineSnapshot world 545 - Error: toMatchInlineSnapshot with different snapshots cannot be called at the same location 545 + Error: toThrowErrorMatchingInlineSnapshot with different snapshots cannot be called at the same location 546 546 547 547 Expected: "[Error: length = 3]" 548 548 Received: "[Error: length = 5]" ··· 582 582 "Snapshot \`toThrowErrorMatchingInlineSnapshot hey 1\` mismatched", 583 583 ], 584 584 "toThrowErrorMatchingInlineSnapshot world": Array [ 585 - "toMatchInlineSnapshot with different snapshots cannot be called at the same location", 585 + "toThrowErrorMatchingInlineSnapshot with different snapshots cannot be called at the same location", 586 586 ], 587 587 }, 588 588 } ··· 645 645 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/3]⎯ 646 646 647 647 FAIL each.test.ts > toThrowErrorMatchingInlineSnapshot world 648 - Error: toMatchInlineSnapshot with different snapshots cannot be called at the same location 648 + Error: toThrowErrorMatchingInlineSnapshot with different snapshots cannot be called at the same location 649 649 650 650 Expected: "[Error: length = 3]" 651 651 Received: "[Error: length = 5]" ··· 679 679 ], 680 680 "toThrowErrorMatchingInlineSnapshot hey": "passed", 681 681 "toThrowErrorMatchingInlineSnapshot world": Array [ 682 - "toMatchInlineSnapshot with different snapshots cannot be called at the same location", 682 + "toThrowErrorMatchingInlineSnapshot with different snapshots cannot be called at the same location", 683 683 ], 684 684 }, 685 685 }
+6
test/snapshots/test/utils.ts
··· 1 + import fs from 'node:fs' 2 + 3 + export function readInlineSnapshots(file: string) { 4 + return extractInlineSnaphsots(fs.readFileSync(file, 'utf-8')) 5 + } 6 + 1 7 export function extractInlineSnaphsots(code: string) { 2 8 const matches = Array.from( 3 9 code.matchAll(/\.toMatch(\w*)InlineSnapshot\(\s*`[\s\S]*?`\s*\)/g),
+236 -133
packages/snapshot/src/port/state.ts
··· 8 8 import type { OptionsReceived as PrettyFormatOptions } from '@vitest/pretty-format' 9 9 import type { ParsedStack } from '@vitest/utils' 10 10 import type { 11 + ProcessDomainSnapshotOptions, 11 12 SnapshotData, 12 13 SnapshotEnvironment, 13 14 SnapshotMatchOptions, ··· 48 49 } 49 50 50 51 type ParsedStackPosition = Pick<ParsedStack, 'file' | 'line' | 'column'> 52 + 53 + export interface ExpectedSnapshot { 54 + key: string 55 + count: number 56 + data?: string 57 + markAsChecked: () => void 58 + } 51 59 52 60 function isSameStackPosition(x: ParsedStackPosition, y: ParsedStackPosition) { 53 61 return x.file === y.file && x.column === y.column && x.line === y.line ··· 119 127 return new SnapshotState(testFilePath, snapshotPath, content, options) 120 128 } 121 129 130 + get snapshotUpdateState(): SnapshotUpdateState { 131 + return this._updateSnapshot 132 + } 133 + 122 134 get environment(): SnapshotEnvironment { 123 135 return this._environment 124 136 } ··· 166 178 ) 167 179 if (promiseIndex !== -1) { 168 180 return stacks[promiseIndex + 3] 181 + } 182 + 183 + // support poll + inline snapshot 184 + const pollChainIndex = stacks.findIndex(i => 185 + i.method.match(/__VITEST_POLL_CHAIN__/), 186 + ) 187 + if (pollChainIndex !== -1) { 188 + return stacks[pollChainIndex + 1] 169 189 } 170 190 171 191 // inline snapshot function can be named __INLINE_SNAPSHOT_OFFSET_<n>__ ··· 216 236 } 217 237 else { 218 238 this._snapshotData[key] = receivedSerialized 239 + } 240 + } 241 + 242 + private _resolveKey(testId: string, testName: string, key?: string): { key: string; count: number } { 243 + this._counters.increment(testName) 244 + const count = this._counters.get(testName) 245 + if (!key) { 246 + key = testNameToKey(testName, count) 247 + } 248 + this._testIdToKeys.get(testId).push(key) 249 + return { key, count } 250 + } 251 + 252 + private _resolveInlineStack(options: { 253 + testId: string 254 + snapshot: string 255 + assertionName: string 256 + error: Error 257 + }): ParsedStack { 258 + const { testId, snapshot, assertionName, error } = options 259 + const stacks = parseErrorStacktrace( 260 + error, 261 + { ignoreStackEntries: [] }, 262 + ) 263 + const _stack = this._inferInlineSnapshotStack(stacks) 264 + if (!_stack) { 265 + const message = stacks.map(s => 266 + ` ${s.file}:${s.line}:${s.column}${s.method ? ` (${s.method})` : ''}`, 267 + ).join('\n') 268 + throw new Error( 269 + `@vitest/snapshot: Couldn't infer stack frame for inline snapshot.\n${message}`, 270 + ) 271 + } 272 + const stack = this.environment.processStackTrace?.(_stack) || _stack 273 + // removing 1 column, because source map points to the wrong 274 + // location for js files, but `column-1` points to the same in both js/ts 275 + // https://github.com/vitejs/vite/issues/8657 276 + stack.column-- 277 + 278 + // reject multiple inline snapshots at the same location if snapshot is different 279 + const snapshotsWithSameStack = this._inlineSnapshotStacks.filter(s => isSameStackPosition(s, stack)) 280 + if (snapshotsWithSameStack.length > 0) { 281 + // ensure only one snapshot will be written at the same location 282 + this._inlineSnapshots = this._inlineSnapshots.filter(s => !isSameStackPosition(s, stack)) 283 + 284 + const differentSnapshot = snapshotsWithSameStack.find(s => s.snapshot !== snapshot) 285 + if (differentSnapshot) { 286 + throw Object.assign( 287 + new Error( 288 + `${assertionName} with different snapshots cannot be called at the same location`, 289 + ), 290 + { 291 + actual: snapshot, 292 + expected: differentSnapshot.snapshot, 293 + }, 294 + ) 295 + } 296 + } 297 + this._inlineSnapshotStacks.push({ ...stack, testId, snapshot }) 298 + return stack 299 + } 300 + 301 + private _reconcile(opts: { 302 + testId: string 303 + key: string 304 + count: number 305 + pass: boolean 306 + hasSnapshot: boolean 307 + snapshotIsPersisted: boolean 308 + addValue: string 309 + actualDisplay: string 310 + expectedDisplay?: string 311 + stack?: ParsedStack 312 + rawSnapshot?: RawSnapshotInfo 313 + assertionName?: string 314 + }): SnapshotReturnOptions { 315 + // These are the conditions on when to write snapshots: 316 + // * There's no snapshot file in a non-CI environment. 317 + // * There is a snapshot file and we decided to update the snapshot. 318 + // * There is a snapshot file, but it doesn't have this snapshot. 319 + // These are the conditions on when not to write snapshots: 320 + // * The update flag is set to 'none'. 321 + // * There's no snapshot file or a file without this snapshot on a CI environment. 322 + if ( 323 + (opts.hasSnapshot && this._updateSnapshot === 'all') 324 + || ((!opts.hasSnapshot || !opts.snapshotIsPersisted) 325 + && (this._updateSnapshot === 'new' || this._updateSnapshot === 'all')) 326 + ) { 327 + if (this._updateSnapshot === 'all') { 328 + if (!opts.pass) { 329 + if (opts.hasSnapshot) { 330 + this.updated.increment(opts.testId) 331 + } 332 + else { 333 + this.added.increment(opts.testId) 334 + } 335 + this._addSnapshot(opts.key, opts.addValue, { 336 + stack: opts.stack, 337 + testId: opts.testId, 338 + rawSnapshot: opts.rawSnapshot, 339 + assertionName: opts.assertionName, 340 + }) 341 + } 342 + else { 343 + this.matched.increment(opts.testId) 344 + } 345 + } 346 + else { 347 + this._addSnapshot(opts.key, opts.addValue, { 348 + stack: opts.stack, 349 + testId: opts.testId, 350 + rawSnapshot: opts.rawSnapshot, 351 + assertionName: opts.assertionName, 352 + }) 353 + this.added.increment(opts.testId) 354 + } 355 + 356 + return { 357 + actual: '', 358 + count: opts.count, 359 + expected: '', 360 + key: opts.key, 361 + pass: true, 362 + } 363 + } 364 + else { 365 + if (!opts.pass) { 366 + this.unmatched.increment(opts.testId) 367 + return { 368 + actual: opts.actualDisplay, 369 + count: opts.count, 370 + expected: opts.expectedDisplay, 371 + key: opts.key, 372 + pass: false, 373 + } 374 + } 375 + else { 376 + this.matched.increment(opts.testId) 377 + return { 378 + actual: '', 379 + count: opts.count, 380 + expected: '', 381 + key: opts.key, 382 + pass: true, 383 + } 384 + } 219 385 } 220 386 } 221 387 ··· 279 445 280 446 probeExpectedSnapshot( 281 447 options: Pick<SnapshotMatchOptions, 'testName' | 'testId' | 'isInline' | 'inlineSnapshot'>, 282 - ): { 283 - data?: string 284 - markAsChecked: () => void 285 - } { 448 + ): ExpectedSnapshot { 286 449 const count = this._counters.get(options.testName) + 1 287 450 const key = testNameToKey(options.testName, count) 288 451 return { 452 + key, 453 + count, 289 454 data: options?.isInline ? options.inlineSnapshot : this._snapshotData[key], 290 455 markAsChecked: () => { 291 456 this._counters.increment(options.testName) ··· 306 471 rawSnapshot, 307 472 assertionName, 308 473 }: SnapshotMatchOptions): SnapshotReturnOptions { 309 - // this also increments counter for inline snapshots. maybe we shouldn't? 310 - this._counters.increment(testName) 311 - const count = this._counters.get(testName) 312 - 313 - if (!key) { 314 - key = testNameToKey(testName, count) 315 - } 316 - this._testIdToKeys.get(testId).push(key) 474 + const resolved = this._resolveKey(testId, testName, key) 475 + key = resolved.key 476 + const count = resolved.count 317 477 318 478 // Do not mark the snapshot as "checked" if the snapshot is inline and 319 479 // there's an external snapshot. This way the external snapshot can be ··· 356 516 || (rawSnapshot && rawSnapshot.content != null) 357 517 358 518 if (pass && !isInline && !rawSnapshot) { 359 - // Executing a snapshot file as JavaScript and writing the strings back 360 - // when other snapshots have changed loses the proper escaping for some 361 - // characters. Since we check every snapshot in every test, use the newly 362 - // generated formatted string. 363 - // Note that this is only relevant when a snapshot is added and the dirty 364 - // flag is set. 519 + // When the file is re-saved (because other snapshots changed), the JS 520 + // round-trip can lose proper escaping. Refresh in-memory data with the 521 + // freshly serialized string so the file is written correctly. 522 + // _reconcile does not write _snapshotData on pass, so this is the only 523 + // place it gets refreshed. Domain snapshots skip this because the stored 524 + // value may contain match patterns that differ from the received output. 365 525 this._snapshotData[key] = receivedSerialized 366 526 } 367 527 368 - // find call site of toMatchInlineSnapshot 369 - let stack: ParsedStack | undefined 370 - if (isInline) { 371 - const stacks = parseErrorStacktrace( 372 - error || new Error('snapshot'), 373 - { ignoreStackEntries: [] }, 374 - ) 375 - const _stack = this._inferInlineSnapshotStack(stacks) 376 - if (!_stack) { 377 - throw new Error( 378 - `@vitest/snapshot: Couldn't infer stack frame for inline snapshot.\n${JSON.stringify( 379 - stacks, 380 - )}`, 381 - ) 382 - } 383 - stack = this.environment.processStackTrace?.(_stack) || _stack 384 - // removing 1 column, because source map points to the wrong 385 - // location for js files, but `column-1` points to the same in both js/ts 386 - // https://github.com/vitejs/vite/issues/8657 387 - stack.column-- 388 - 389 - // reject multiple inline snapshots at the same location if snapshot is different 390 - const snapshotsWithSameStack = this._inlineSnapshotStacks.filter(s => isSameStackPosition(s, stack!)) 391 - if (snapshotsWithSameStack.length > 0) { 392 - // ensure only one snapshot will be written at the same location 393 - this._inlineSnapshots = this._inlineSnapshots.filter(s => !isSameStackPosition(s, stack!)) 394 - 395 - const differentSnapshot = snapshotsWithSameStack.find(s => s.snapshot !== receivedSerialized) 396 - if (differentSnapshot) { 397 - throw Object.assign( 398 - new Error( 399 - 'toMatchInlineSnapshot with different snapshots cannot be called at the same location', 400 - ), 401 - { 402 - actual: receivedSerialized, 403 - expected: differentSnapshot.snapshot, 404 - }, 405 - ) 406 - } 407 - } 408 - this._inlineSnapshotStacks.push({ ...stack, testId, snapshot: receivedSerialized }) 409 - } 410 - 411 - // These are the conditions on when to write snapshots: 412 - // * There's no snapshot file in a non-CI environment. 413 - // * There is a snapshot file and we decided to update the snapshot. 414 - // * There is a snapshot file, but it doesn't have this snapshot. 415 - // These are the conditions on when not to write snapshots: 416 - // * The update flag is set to 'none'. 417 - // * There's no snapshot file or a file without this snapshot on a CI environment. 418 - if ( 419 - (hasSnapshot && this._updateSnapshot === 'all') 420 - || ((!hasSnapshot || !snapshotIsPersisted) 421 - && (this._updateSnapshot === 'new' || this._updateSnapshot === 'all')) 422 - ) { 423 - if (this._updateSnapshot === 'all') { 424 - if (!pass) { 425 - if (hasSnapshot) { 426 - this.updated.increment(testId) 427 - } 428 - else { 429 - this.added.increment(testId) 430 - } 431 - 432 - this._addSnapshot(key, receivedSerialized, { 433 - stack, 434 - testId, 435 - rawSnapshot, 436 - assertionName, 437 - }) 438 - } 439 - else { 440 - this.matched.increment(testId) 441 - } 442 - } 443 - else { 444 - this._addSnapshot(key, receivedSerialized, { 445 - stack, 528 + const stack = isInline 529 + ? this._resolveInlineStack({ 446 530 testId, 447 - rawSnapshot, 448 - assertionName, 531 + snapshot: receivedSerialized, 532 + assertionName: assertionName || 'toMatchInlineSnapshot', 533 + error: error || new Error('snapshot'), 449 534 }) 450 - this.added.increment(testId) 451 - } 535 + : undefined 452 536 453 - return { 454 - actual: '', 455 - count, 456 - expected: '', 457 - key, 458 - pass: true, 459 - } 460 - } 461 - else { 462 - if (!pass) { 463 - this.unmatched.increment(testId) 464 - return { 465 - actual: rawSnapshot ? receivedSerialized : removeExtraLineBreaks(receivedSerialized), 466 - count, 467 - expected: 468 - expectedTrimmed !== undefined 469 - ? rawSnapshot ? expectedTrimmed : removeExtraLineBreaks(expectedTrimmed) 470 - : undefined, 471 - key, 472 - pass: false, 473 - } 474 - } 475 - else { 476 - this.matched.increment(testId) 477 - return { 478 - actual: '', 479 - count, 480 - expected: '', 481 - key, 482 - pass: true, 483 - } 484 - } 485 - } 537 + return this._reconcile({ 538 + testId, 539 + key, 540 + count, 541 + pass, 542 + hasSnapshot, 543 + snapshotIsPersisted: !!snapshotIsPersisted, 544 + addValue: receivedSerialized, 545 + actualDisplay: rawSnapshot ? receivedSerialized : removeExtraLineBreaks(receivedSerialized), 546 + expectedDisplay: expectedTrimmed !== undefined 547 + ? rawSnapshot ? expectedTrimmed : removeExtraLineBreaks(expectedTrimmed) 548 + : undefined, 549 + stack, 550 + rawSnapshot, 551 + assertionName, 552 + }) 553 + } 554 + 555 + processDomainSnapshot({ 556 + testId, 557 + received, 558 + expectedSnapshot, 559 + matchResult, 560 + isInline, 561 + error, 562 + assertionName, 563 + }: ProcessDomainSnapshotOptions): SnapshotReturnOptions { 564 + const stack = isInline 565 + ? this._resolveInlineStack({ 566 + testId, 567 + snapshot: received, 568 + assertionName: assertionName!, 569 + error: error || new Error('STACK_TRACE_ERROR'), 570 + }) 571 + : undefined 572 + const actualResolved = matchResult?.resolved ?? received 573 + const expectedResolved = matchResult?.expected ?? expectedSnapshot.data 574 + return this._reconcile({ 575 + testId, 576 + key: expectedSnapshot.key, 577 + count: expectedSnapshot.count, 578 + pass: matchResult?.pass ?? false, 579 + hasSnapshot: !!expectedSnapshot.data, 580 + snapshotIsPersisted: isInline ? true : this._fileExists, 581 + addValue: actualResolved, 582 + actualDisplay: removeExtraLineBreaks(actualResolved), 583 + expectedDisplay: expectedResolved !== undefined 584 + ? removeExtraLineBreaks(expectedResolved) 585 + : undefined, 586 + stack, 587 + assertionName, 588 + }) 486 589 } 487 590 488 591 async pack(): Promise<SnapshotResult> {
+14
packages/snapshot/src/types/index.ts
··· 2 2 OptionsReceived as PrettyFormatOptions, 3 3 Plugin as PrettyFormatPlugin, 4 4 } from '@vitest/pretty-format' 5 + import type { DomainMatchResult } from '../domain' 5 6 import type { RawSnapshotInfo } from '../port/rawSnapshot' 7 + import type { ExpectedSnapshot } from '../port/state' 6 8 import type { 7 9 SnapshotEnvironment, 8 10 SnapshotEnvironmentOptions, ··· 33 35 error?: Error 34 36 rawSnapshot?: RawSnapshotInfo 35 37 assertionName?: string 38 + } 39 + 40 + export interface ProcessDomainSnapshotOptions { 41 + testId: string 42 + received: string 43 + expectedSnapshot: ExpectedSnapshot 44 + matchResult?: DomainMatchResult 45 + isInline?: boolean 46 + // assertionName and error are crucial 47 + // for finding assertion callsite by probing stacktrace 48 + assertionName?: string 49 + error?: Error 36 50 } 37 51 38 52 export interface SnapshotResult {
+63
test/browser/fixtures/aria-snapshot/basic.test.ts
··· 1 + import { expect, test, } from 'vitest' 2 + import { server, page } from 'vitest/browser' 3 + 4 + test('toMatchAriaSnapshot simple', () => { 5 + document.body.innerHTML = ` 6 + <main> 7 + <h1>Dashboard</h1> 8 + <nav aria-label="Actions"> 9 + <button>Save</button> 10 + <button>Cancel</button> 11 + </nav> 12 + </main> 13 + ` 14 + expect(document.body).toMatchAriaSnapshot() 15 + }) 16 + 17 + test('toMatchAriaInlineSnapshot simple', () => { 18 + document.body.innerHTML = ` 19 + <p>Original</p> 20 + <button aria-label="1234">Pattern</button> 21 + ` 22 + expect(document.body).toMatchAriaInlineSnapshot(` 23 + - paragraph: Original 24 + - button "1234": Pattern 25 + `) 26 + }) 27 + 28 + test('poll aria once', async () => { 29 + await expect.poll(async () => { 30 + document.body.innerHTML = `<p>poll once</p>` 31 + return document.body 32 + }).toMatchAriaInlineSnapshot(`- paragraph: poll once`) 33 + }) 34 + 35 + test('expect.element aria once', async () => { 36 + document.body.innerHTML = ` 37 + <h1>Hello</h1> 38 + <p>World</p> 39 + ` 40 + document.body.setAttribute('data-testid', 'body') 41 + await expect.element(page.getByTestId('body')).toMatchAriaInlineSnapshot(` 42 + - heading "Hello" [level=1] 43 + - paragraph: World 44 + `) 45 + }) 46 + 47 + test('expect.element aria retry', async () => { 48 + document.body.innerHTML = ` 49 + <h1>Hello</h1> 50 + ` 51 + document.body.setAttribute('data-testid', 'body') 52 + setTimeout(() => { 53 + document.body.innerHTML = ` 54 + <h1>Hello</h1> 55 + <p>World</p> 56 + ` 57 + }, 10) 58 + await expect.element(page.getByTestId('body'), { interval: 20 }) 59 + .toMatchAriaInlineSnapshot(` 60 + - heading "Hello" [level=1] 61 + - paragraph: World 62 + `) 63 + })
+14
test/browser/fixtures/aria-snapshot/vitest.config.ts
··· 1 + import { fileURLToPath } from 'node:url' 2 + import { defineConfig } from 'vitest/config' 3 + import { instances, provider } from '../../settings' 4 + 5 + export default defineConfig({ 6 + cacheDir: fileURLToPath(new URL('./node_modules/.vite', import.meta.url)), 7 + test: { 8 + browser: { 9 + enabled: true, 10 + provider, 11 + instances, 12 + }, 13 + }, 14 + })
+75
packages/browser/src/client/tester/aria.ts
··· 1 + import type { MatchersObject } from '@vitest/expect' 2 + import type { DomainMatchResult, DomainSnapshotAdapter } from '@vitest/snapshot' 3 + import type { 4 + AriaNode, 5 + AriaTemplateNode, 6 + } from 'ivya/aria' 7 + import { 8 + generateAriaTree, 9 + matchAriaTree, 10 + parseAriaTemplate, 11 + renderAriaTemplate, 12 + renderAriaTree, 13 + } from 'ivya/aria' 14 + import { Snapshots } from 'vitest' 15 + 16 + const ariaSnapshotAdapter: DomainSnapshotAdapter<AriaNode, AriaTemplateNode> = { 17 + name: 'aria', 18 + 19 + capture(received) { 20 + if (received instanceof Element) { 21 + return generateAriaTree(received) 22 + } 23 + throw new TypeError('aria adapter expects an Element') 24 + }, 25 + 26 + render(captured) { 27 + return wrapNewlines(renderAriaTree(captured)) 28 + }, 29 + 30 + parseExpected(input) { 31 + // increase limit so that yaml parse error can reach `toMatchAriaSnapshot` callsite in user test files 32 + const limit = Error.stackTraceLimit 33 + Error.stackTraceLimit = limit + 20 34 + try { 35 + return parseAriaTemplate(input.trim()) 36 + } 37 + finally { 38 + Error.stackTraceLimit = limit 39 + } 40 + }, 41 + 42 + match(captured, expected): DomainMatchResult { 43 + const r = matchAriaTree(captured, expected) 44 + if (r.pass) { 45 + return { pass: true } 46 + } 47 + return { 48 + pass: false, 49 + message: 'Accessibility tree does not match expected template', 50 + resolved: wrapNewlines(r.resolved), 51 + expected: wrapNewlines(renderAriaTemplate(expected)), 52 + } 53 + }, 54 + } 55 + 56 + // ensure newlines for diff/snapshot readability 57 + function wrapNewlines(s: string) { 58 + return `\n${s}\n` 59 + } 60 + 61 + export const ariaMatchers: MatchersObject = { 62 + toMatchAriaSnapshot(actual: unknown) { 63 + return Snapshots.toMatchDomainSnapshot.call(this, ariaSnapshotAdapter, actual) 64 + }, 65 + toMatchAriaInlineSnapshot(actual: Element, inlineSnapshot?: string) { 66 + return Snapshots.toMatchDomainInlineSnapshot.call(this, ariaSnapshotAdapter, actual, inlineSnapshot) 67 + }, 68 + } 69 + 70 + // internal flag to allow expect.poll for snapshot matchers 71 + for (const matcher of Object.values(ariaMatchers)) { 72 + Object.assign(matcher, { 73 + __vitest_poll_takeover__: true, 74 + }) 75 + }
+2
packages/browser/src/client/tester/expect-element.ts
··· 3 3 import { chai, expect } from 'vitest' 4 4 import { getType } from 'vitest/internal/browser' 5 5 import { getBrowserState, getWorkerState } from '../utils' 6 + import { ariaMatchers } from './aria' 6 7 import { matchers } from './expect' 7 8 import { processTimeoutOptions } from './tester-utils' 8 9 ··· 83 84 } 84 85 85 86 expect.extend(matchers) 87 + expect.extend(ariaMatchers) 86 88 expect.element = element
+1 -1
packages/ui/client/components/views/ViewReport.spec.ts
··· 60 60 } 61 61 fileWithTextStacks.file = fileWithTextStacks 62 62 63 - describe('ViewReport', () => { 63 + describe.todo('ViewReport', () => { 64 64 describe('RunnerTestFile where stacks are in text', () => { 65 65 beforeEach(() => { 66 66 render(ViewReport, {
+48 -5
packages/vitest/src/integrations/chai/poll.ts
··· 60 60 poll: true, 61 61 }) as Assertion 62 62 fn = fn.bind(assertion) 63 + // injected so that domain snapshot can take over poll implementation. 64 + chai.util.flag(assertion, '_poll.fn', fn) 65 + chai.util.flag(assertion, '_poll.timeout', timeout) 66 + chai.util.flag(assertion, '_poll.interval', interval) 63 67 const test = chai.util.flag(assertion, 'vitest-test') as Test | undefined 64 68 if (!test) { 65 69 throw new Error('expect.poll() must be called inside a test') ··· 82 86 ) 83 87 } 84 88 85 - return function (this: any, ...args: any[]) { 89 + // Core poll stack-trace trick: 90 + // 1. capture STACK_TRACE_ERROR here before entering the async poll loop 91 + // 2. when the matcher eventually fails, rethrow via throwWithCause() 92 + // so the final error keeps this earlier stack 93 + // 94 + // For example, when user writes: 95 + // await expect.poll(...).toBeSomething() 96 + // STACK_TRACE_ERROR.stack would look like 97 + // at ...(more internal stacks)... 98 + // at __VITEST_POLL_CHAIN__ .../packages/vitest/dist/... 99 + // at .../my-file.test.ts:12:3 (this points to `toBeSomething()` callsite in user test file) 100 + // Vitest later filters out internal stacks from `vitest/dist`, so the reported errors correctly 101 + // points to the user callsite for poll assertion errors. 102 + // 103 + // Inline snapshots piggyback on the same idea. We pass 104 + // STACK_TRACE_ERROR through `chai.util.flag(assertion, 'error', ...)`. 105 + // Inline snapshot assertion access the same error stack for 106 + // extracting inline snapshot location to validate and update new snapshots. 107 + return function __VITEST_POLL_CHAIN__(this: any, ...args: any[]) { 86 108 const STACK_TRACE_ERROR = new Error('STACK_TRACE_ERROR') 87 109 const promise = async () => { 110 + chai.util.flag(assertion, '_name', key) 111 + chai.util.flag(assertion, 'error', STACK_TRACE_ERROR) 112 + 113 + const onSettled = chai.util.flag(assertion, '_poll.onSettled') as Function | undefined 114 + 115 + // We use `matcher.__vitest_poll_takeover__` flag 116 + // to let domain snapshot matchers take over polling logic. 117 + // this is not public API yet. 118 + // Need to use `getOwnPropertyDescriptor` since otherwise chai proxy breaks. 119 + const pollTakeover = Object.getOwnPropertyDescriptor( 120 + assertionFunction, 121 + '__vitest_poll_takeover__', 122 + )?.value 123 + if (pollTakeover) { 124 + try { 125 + const output = await assertionFunction.call(assertion, ...args) 126 + await onSettled?.({ assertion, status: 'pass' }) 127 + return output 128 + } 129 + catch (err) { 130 + await onSettled?.({ assertion, status: 'fail' }) 131 + throwWithCause(err, STACK_TRACE_ERROR) 132 + } 133 + } 134 + 88 135 const { setTimeout, clearTimeout } = getSafeTimers() 89 136 90 137 let executionPhase: 'fn' | 'assertion' = 'fn' ··· 93 140 const timerId = setTimeout(() => { 94 141 hasTimedOut = true 95 142 }, timeout) 96 - 97 - chai.util.flag(assertion, '_name', key) 98 - 99 - const onSettled = chai.util.flag(assertion, '_poll.onSettled') as Function | undefined 100 143 101 144 try { 102 145 while (true) {
+93 -1
packages/vitest/src/integrations/snapshot/chai.ts
··· 1 - import type { AsyncExpectationResult, ChaiPlugin, MatcherState, SyncExpectationResult } from '@vitest/expect' 1 + import type { AsyncExpectationResult, ChaiPlugin, ExpectationResult, MatcherState, SyncExpectationResult } from '@vitest/expect' 2 2 import type { Test } from '@vitest/runner' 3 + import type { DomainSnapshotAdapter } from '@vitest/snapshot' 3 4 import { chai, createAssertionMessage, equals, iterableEquality, recordAsyncExpect, subsetEquality, wrapAssertion } from '@vitest/expect' 4 5 import { getNames } from '@vitest/runner/utils' 5 6 import { ··· 175 176 }), 176 177 ) 177 178 utils.addMethod(chai.expect, 'addSnapshotSerializer', addSerializer) 179 + } 180 + 181 + function toMatchDomainSnapshotImpl(opts: { 182 + assertion: Chai.Assertion 183 + adapter: DomainSnapshotAdapter<any, any> 184 + received: unknown 185 + isInline?: boolean 186 + inlineSnapshot?: string 187 + hint?: string 188 + }): ExpectationResult { 189 + const { assertion } = opts 190 + validateAssertion(assertion) 191 + const assertionName = getAssertionName(assertion) 192 + const test = getTest(assertion) 193 + 194 + let { inlineSnapshot } = opts 195 + if (inlineSnapshot) { 196 + inlineSnapshot = stripSnapshotIndentation(inlineSnapshot) 197 + } 198 + 199 + const pollFn = chai.util.flag(assertion, '_poll.fn') as (() => Promise<unknown> | unknown) | undefined 200 + if (pollFn) { 201 + return getSnapshotClient().pollMatchDomain({ 202 + poll: pollFn, 203 + adapter: opts.adapter, 204 + message: opts.hint, 205 + isInline: opts.isInline, 206 + errorMessage: chai.util.flag(assertion, 'message'), 207 + timeout: chai.util.flag(assertion, '_poll.timeout') as number | undefined, 208 + interval: chai.util.flag(assertion, '_poll.interval') as number | undefined, 209 + assertionName, 210 + inlineSnapshot, 211 + error: chai.util.flag(assertion, 'error'), 212 + ...getTestNames(test), 213 + }) 214 + } 215 + 216 + return getSnapshotClient().matchDomain({ 217 + received: opts.received, 218 + adapter: opts.adapter, 219 + message: opts.hint, 220 + isInline: opts.isInline, 221 + errorMessage: chai.util.flag(assertion, 'message'), 222 + assertionName, 223 + inlineSnapshot, 224 + error: chai.util.flag(assertion, 'error'), 225 + ...getTestNames(test), 226 + }) 178 227 } 179 228 180 229 // toMatchSnapshot(propertiesOrHint?, hint?) ··· 372 421 received, 373 422 filepath, 374 423 hint, 424 + }) 425 + }, 426 + 427 + /** 428 + * Composable for building custom domain-based snapshot matchers via `expect.extend`. 429 + * 430 + * Call this from a matcher and pass the domain adapter that defines capture, 431 + * rendering, parsing, and semantic matching behavior. 432 + * 433 + * @experimental 434 + */ 435 + toMatchDomainSnapshot( 436 + this: MatcherState, 437 + domain: DomainSnapshotAdapter<any, any>, 438 + received: unknown, 439 + ): ExpectationResult { 440 + return toMatchDomainSnapshotImpl({ 441 + assertion: this.assertion, 442 + adapter: domain, 443 + received, 444 + }) 445 + }, 446 + 447 + /** 448 + * Composable for building custom domain-based inline snapshot matchers via `expect.extend`. 449 + * 450 + * Call this from a matcher and pass the domain adapter that defines capture, 451 + * rendering, parsing, and semantic matching behavior. 452 + * 453 + * @experimental 454 + */ 455 + toMatchDomainInlineSnapshot( 456 + this: MatcherState, 457 + domain: DomainSnapshotAdapter<any, any>, 458 + received: unknown, 459 + inlineSnapshot?: string, 460 + ): ExpectationResult { 461 + return toMatchDomainSnapshotImpl({ 462 + assertion: this.assertion, 463 + adapter: domain, 464 + received, 465 + isInline: true, 466 + inlineSnapshot, 375 467 }) 376 468 }, 377 469 }
+9
test/browser/fixtures/aria-snapshot/__snapshots__/basic.test.ts.snap
··· 1 + // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 + 3 + exports[`toMatchAriaSnapshot simple 1`] = ` 4 + - main: 5 + - heading "Dashboard" [level=1] 6 + - navigation "Actions": 7 + - button "Save" 8 + - button "Cancel" 9 + `;
+23
test/snapshots/test/fixtures/domain-aria-inline/basic.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + 3 + test('simple heading', () => { 4 + document.body.innerHTML = ` 5 + <h1>Hello World</h1> 6 + <p>Some description</p> 7 + ` 8 + expect(document.body).toMatchAriaInlineSnapshot(` 9 + - heading "Hello World" [level=1] 10 + - paragraph: Some description 11 + `) 12 + }) 13 + 14 + test('semantic match with regex in snapshot', () => { 15 + document.body.innerHTML = ` 16 + <p>Original</p> 17 + <button aria-label="1234">Pattern</button> 18 + ` 19 + expect(document.body).toMatchAriaInlineSnapshot(` 20 + - paragraph: Original 21 + - button "1234": Pattern 22 + `) 23 + })
+17
test/snapshots/test/fixtures/domain-aria-inline/vitest.config.ts
··· 1 + import { defineConfig } from "vitest/config" 2 + import { playwright } from "@vitest/browser-playwright" 3 + 4 + export default defineConfig({ 5 + test: { 6 + browser: { 7 + enabled: true, 8 + headless: true, 9 + provider: playwright(), 10 + instances: [ 11 + { 12 + browser: "chromium", 13 + } 14 + ] 15 + } 16 + } 17 + })
+1
test/snapshots/test/fixtures/domain-aria/.gitignore
··· 1 + __snapshots__/
+21
test/snapshots/test/fixtures/domain-aria/basic.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + 3 + test('simple heading and paragraph', () => { 4 + document.body.innerHTML = ` 5 + <h1>Hello World</h1> 6 + <p> 7 + Some 8 + <br/> 9 + description 10 + </p> 11 + ` 12 + expect(document.body).toMatchAriaSnapshot() 13 + }) 14 + 15 + test('semantic match with regex in snapshot', () => { 16 + document.body.innerHTML = ` 17 + <p>Original</p> 18 + <button aria-label="1234">Pattern</button> 19 + ` 20 + expect(document.body).toMatchAriaSnapshot() 21 + })
+17
test/snapshots/test/fixtures/domain-aria/vitest.config.ts
··· 1 + import { defineConfig } from "vitest/config" 2 + import { playwright } from "@vitest/browser-playwright" 3 + 4 + export default defineConfig({ 5 + test: { 6 + browser: { 7 + enabled: true, 8 + headless: true, 9 + provider: playwright(), 10 + instances: [ 11 + { 12 + browser: "chromium", 13 + } 14 + ] 15 + } 16 + } 17 + })
+13
test/snapshots/test/fixtures/domain-error/basic.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import "../domain/basic-extend" 3 + 4 + test('file', () => { 5 + expect({ name: 'alice', age: '30' }).toMatchKvSnapshot() 6 + }) 7 + 8 + test('inline', () => { 9 + expect({ name: 'alice', age: '30' }).toMatchKvInlineSnapshot(` 10 + name=bob 11 + inine-broken 12 + `) 13 + })
+17
test/snapshots/test/fixtures/domain-inline/basic.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import "../domain/basic-extend" 3 + 4 + test('all literal', () => { 5 + expect({ name: 'alice', age: '30' }).toMatchKvInlineSnapshot(` 6 + name=alice 7 + age=30 8 + `) 9 + }) 10 + 11 + test('with regex', () => { 12 + expect({ name: 'bob', score: '999', status: 'active' }).toMatchKvInlineSnapshot(` 13 + name=bob 14 + score=999 15 + status=active 16 + `) 17 + })
+1
test/snapshots/test/fixtures/domain-poll-inline/.gitignore
··· 1 + __snapshots__/
+71
test/snapshots/test/fixtures/domain-poll-inline/basic.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import "../domain/basic-extend" 3 + 4 + test('stable', async () => { 5 + let trial = 0 6 + await expect.poll(() => { 7 + trial++ 8 + return { name: 'a', age: '23' } 9 + }, { interval: 10 }).toMatchKvInlineSnapshot(` 10 + name=a 11 + age=23 12 + `) 13 + expect(trial).toBe(2) 14 + }) 15 + 16 + test('throw then stable', async () => { 17 + let trial = 0 18 + await expect.poll(() => { 19 + trial++ 20 + if (trial <= 3) { 21 + throw new Error(`Fail at ${trial}`) 22 + } 23 + return { name: 'b', age: '23' } 24 + }, { interval: 10 }).toMatchKvInlineSnapshot(` 25 + name=b 26 + age=23 27 + `) 28 + expect(trial).toBe(5) 29 + }) 30 + 31 + test('unstable then stable', async () => { 32 + let trial = 0 33 + await expect.poll(() => { 34 + trial++ 35 + if (trial <= 3) return { status: 'loading', trial } // unstable 36 + return { status: 'done' } // then stable 37 + }, { interval: 10 }).toMatchKvInlineSnapshot(` 38 + status=done 39 + `) 40 + expect(trial).toBe(5) 41 + }) 42 + 43 + test('multiple poll snapshots', async () => { 44 + await expect.poll(() => { 45 + return { x: '1' } 46 + }, { interval: 10 }).toMatchKvInlineSnapshot(` 47 + x=1 48 + `) 49 + 50 + await expect.poll(() => { 51 + return { y: '2' } 52 + }, { interval: 10 }).toMatchKvInlineSnapshot(` 53 + y=2 54 + `) 55 + }) 56 + 57 + test('non-poll alongside poll', async () => { 58 + expect({ static: 'value' }).toMatchKvInlineSnapshot(` 59 + static=value 60 + `) 61 + 62 + await expect.poll(() => { 63 + return { polled: 'value' } 64 + }, { interval: 10 }).toMatchKvInlineSnapshot(` 65 + polled=value 66 + `) 67 + 68 + expect({ another: 'static' }).toMatchKvInlineSnapshot(` 69 + another=static 70 + `) 71 + })
+1
test/snapshots/test/fixtures/domain-poll/.gitignore
··· 1 + __snapshots__/
+52
test/snapshots/test/fixtures/domain-poll/basic.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import "../domain/basic-extend" 3 + 4 + test('stable', async () => { 5 + let trial = 0 6 + await expect.poll(() => { 7 + trial++ 8 + return { name: 'a', age: '23' } 9 + }, { interval: 10 }).toMatchKvSnapshot() 10 + expect(trial).toBe(2) 11 + }) 12 + 13 + test('throw then stable', async () => { 14 + let trial = 0 15 + await expect.poll(() => { 16 + trial++ 17 + if (trial <= 3) { 18 + throw new Error(`Fail at ${trial}`) 19 + } 20 + return { name: 'b', age: '23' } 21 + }, { interval: 10 }).toMatchKvSnapshot() 22 + expect(trial).toBe(5) 23 + }) 24 + 25 + test('unstable then stable', async () => { 26 + let trial = 0 27 + await expect.poll(() => { 28 + trial++ 29 + if (trial <= 3) return { status: 'loading', trial } // unstable 30 + return { status: 'done' } // then stable 31 + }, { interval: 10 }).toMatchKvSnapshot() 32 + expect(trial).toBe(5) 33 + }) 34 + 35 + test('multiple poll snapshots', async () => { 36 + await expect.poll(() => { 37 + return { x: '1' } 38 + }, { interval: 10 }).toMatchKvSnapshot() 39 + 40 + await expect.poll(() => { 41 + return { y: '2' } 42 + }, { interval: 10 }).toMatchKvSnapshot() 43 + }) 44 + 45 + test('non-poll alongside poll', async () => { 46 + expect({ static: 'value' }).toMatchKvSnapshot() 47 + await expect.poll(() => { 48 + return { polled: 'value' } 49 + }, { interval: 10 }).toMatchKvSnapshot() 50 + 51 + expect({ another: 'static' }).toMatchKvSnapshot() 52 + })
+1
test/snapshots/test/fixtures/domain/.gitignore
··· 1 + __snapshots__/
+33
test/snapshots/test/fixtures/domain/basic-extend.ts
··· 1 + import { expect, Snapshots } from "vitest" 2 + import type { MatchersObject } from "@vitest/expect" 3 + import { kvAdapter } from "./basic" 4 + 5 + interface CustomMatchers<R = unknown> { 6 + toMatchKvSnapshot: () => R 7 + toMatchKvInlineSnapshot: (snapshot?: string) => R 8 + } 9 + 10 + declare module 'vitest' { 11 + interface Assertion<T = any> extends CustomMatchers<T> {} 12 + } 13 + 14 + const matchers: MatchersObject = { 15 + toMatchKvSnapshot(actual: unknown) { 16 + return Snapshots.toMatchDomainSnapshot.call(this, kvAdapter, actual) 17 + }, 18 + toMatchKvInlineSnapshot( 19 + actual: unknown, 20 + inlineSnapshot?: string, 21 + ) { 22 + return Snapshots.toMatchDomainInlineSnapshot.call(this, kvAdapter, actual, inlineSnapshot) 23 + }, 24 + } 25 + 26 + // internal flag to allow expect.poll for snapshot matchers 27 + for (const matcher of Object.values(matchers)) { 28 + Object.assign(matcher, { 29 + __vitest_poll_takeover__: true, 30 + }) 31 + } 32 + 33 + expect.extend(matchers)
+10
test/snapshots/test/fixtures/domain/basic.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import "./basic-extend" 3 + 4 + test('all literal', () => { 5 + expect({ name: 'alice', age: '30' }).toMatchKvSnapshot() 6 + }) 7 + 8 + test('with regex', () => { 9 + expect({ name: 'bob', age: '24', score: '999', status: 'active' }).toMatchKvSnapshot() 10 + })
+75
test/snapshots/test/fixtures/domain/basic.ts
··· 1 + import type { DomainMatchResult, DomainSnapshotAdapter } from '@vitest/snapshot' 2 + 3 + // Key-value domain adapter: each snapshot is multiple lines of `key=value`. 4 + // Values can be literal strings or `/regex/` patterns in the stored snapshot. 5 + // On match, each line is checked independently — regex lines use RegExp.test(). 6 + // On partial match, `mergedExpected` preserves regex for matched lines 7 + // and substitutes literal rendered values for unmatched lines. 8 + 9 + type KVCaptured = Record<string, string> 10 + type KVExpected = Record<string, string | RegExp> 11 + 12 + function renderKV(obj: Record<string, unknown>) { 13 + return `\n${Object.entries(obj).map(([k, v]) => `${k}=${v}`).join('\n')}\n` 14 + } 15 + 16 + export const kvAdapter: DomainSnapshotAdapter<KVCaptured, KVExpected> = { 17 + name: 'kv', 18 + 19 + capture(received: unknown): KVCaptured { 20 + if (received && typeof received === 'object') { 21 + return Object.fromEntries(Object.entries(received).map(([k, v]) => [k, String(v)])) 22 + } 23 + throw new TypeError('kv adapter expects a plain object') 24 + }, 25 + 26 + render(captured: KVCaptured): string { 27 + return renderKV(captured) 28 + }, 29 + 30 + parseExpected(input: string): KVExpected { 31 + const entries = input.trim().split('\n').map((line) => { 32 + const eq = line.indexOf('=') 33 + if (eq === -1) { 34 + throw new Error(`Invalid KV Format: '${line}'`) 35 + } 36 + const key = line.slice(0, eq) 37 + const raw = line.slice(eq + 1) 38 + const value = (raw.startsWith('/') && raw.endsWith('/') && raw.length > 1) 39 + ? new RegExp(raw.slice(1, -1)) 40 + : raw 41 + return [key, value] 42 + }) 43 + return Object.fromEntries(entries) 44 + }, 45 + 46 + match(captured: KVCaptured, expected: KVExpected): DomainMatchResult { 47 + const resolvedLines: string[] = [] 48 + let pass = true 49 + 50 + for (const [key, actualValue] of Object.entries(captured)) { 51 + const expectedValue = expected[key] 52 + 53 + // non asserted keys are skipped (works as subset match) 54 + if (typeof expectedValue === 'undefined') { 55 + continue; 56 + } 57 + 58 + // preserve matched pattern for normalized error diff and partial update 59 + if (expectedValue instanceof RegExp && expectedValue.test(actualValue)) { 60 + resolvedLines.push(`${key}=/${expectedValue.source}/`) 61 + continue 62 + } 63 + 64 + resolvedLines.push(`${key}=${actualValue}`) 65 + pass &&= actualValue === expectedValue 66 + } 67 + 68 + return { 69 + pass, 70 + message: pass ? undefined : 'KV entries do not match', 71 + resolved: `\n${resolvedLines.join('\n')}\n`, 72 + expected: `\n${renderKV(expected)}\n`, 73 + } 74 + }, 75 + }
+21 -6
packages/ui/client/components/artifacts/visual-regression/SmallTabs.spec.ts
··· 1 - import { faker } from '@faker-js/faker' 2 1 import { describe, expect, it } from 'vitest' 3 2 import { userEvent } from 'vitest/browser' 4 3 import { defineComponent, h } from 'vue' ··· 14 13 SmallTabs, 15 14 null, 16 15 { 17 - default: () => Array.from({ length: children }, () => h( 16 + default: () => Array.from({ length: children }, (_, i) => h( 18 17 SmallTabsPane, 19 - { title: faker.lorem.word() }, 20 - () => faker.lorem.words(2), 18 + { title: `title-${i}` }, 19 + () => `content-${i}`, 21 20 )), 22 21 }, 23 22 ), ··· 26 25 27 26 describe('SmallTabs', () => { 28 27 it('has accessible elements', async () => { 29 - render(createSmallTabs(2)) 28 + const result = render(createSmallTabs(2)) 30 29 31 30 // a tablist with two elements inside 32 31 const tablist = page.getByRole('tablist') ··· 35 34 const secondTab = tabs.last() 36 35 37 36 await expect.element(tablist).toBeInTheDocument() 37 + await expect.element(result.locator).toMatchAriaInlineSnapshot(` 38 + - tablist: 39 + - tab "title-0" [selected] 40 + - tab "title-1" 41 + - tabpanel "title-0": content-0 42 + `) 38 43 expect(tabs.all()).toHaveLength(2) 39 44 40 45 await expect.element(firstTab).toHaveAttribute('aria-selected', 'true') ··· 73 78 it('opens one panel at a time', async () => { 74 79 const tabsLimit = 5 75 80 76 - render(createSmallTabs(tabsLimit)) 81 + const result = render(createSmallTabs(tabsLimit)) 77 82 78 83 const tabs = page.getByRole('tablist').getByRole('tab') 79 84 const panels = page.getByRole('tabpanel', { includeHidden: true }) ··· 90 95 page.getByRole('tabpanel'), 91 96 ).toBe(activePanel.element()) 92 97 } 98 + 99 + expect(result.container).toMatchAriaInlineSnapshot(` 100 + - tablist: 101 + - tab "title-0" 102 + - tab "title-1" 103 + - tab "title-2" 104 + - tab "title-3" 105 + - tab "title-4" [selected] 106 + - tabpanel "title-4": content-4 107 + `) 93 108 }) 94 109 })
+63 -10
packages/ui/client/components/artifacts/visual-regression/VisualRegression.spec.ts
··· 30 30 it('renders content with no attachments', async () => { 31 31 const messageContent = faker.lorem.words(5) 32 32 33 - render(VisualRegression, { 33 + const result = render(VisualRegression, { 34 34 props: { 35 35 regression: { 36 36 type: 'internal:toMatchScreenshot', ··· 49 49 await expect.element(article.getByRole('paragraph')) 50 50 .toHaveTextContent(messageContent) 51 51 await expect.element(article.getByRole('tablist')).toHaveTextContent('') 52 + 53 + expect(result.container).toMatchAriaInlineSnapshot(` 54 + - article: 55 + - heading "Visual Regression" [level=1] 56 + - paragraph: /.*/ 57 + - tablist 58 + `) 52 59 }) 53 60 54 61 it('renders diff tab', async () => { 55 - render(VisualRegression, { 62 + const result = render(VisualRegression, { 56 63 props: { 57 64 regression: { 58 65 type: 'internal:toMatchScreenshot', 59 66 kind: 'visual-regression', 60 - message: faker.lorem.words(5), 67 + message: 'Happy testing!', 61 68 attachments: [diff], 62 69 } satisfies VisualRegressionArtifact, 63 70 }, ··· 67 74 .toHaveTextContent('Diff') 68 75 await expect.element(page.getByRole('tabpanel').getByRole('img')) 69 76 .toBeInTheDocument() 77 + await expect.element(result.locator).toMatchAriaInlineSnapshot(` 78 + - article: 79 + - heading "Visual Regression" [level=1] 80 + - paragraph: Happy testing! 81 + - tablist: 82 + - tab "Diff" [selected] 83 + - tabpanel "Diff": 84 + - link: 85 + - /url: /.*/ 86 + - img 87 + `) 70 88 }) 71 89 72 90 it('renders reference tab', async () => { ··· 106 124 }) 107 125 108 126 it('renders reference, actual, and slider tabs', async () => { 109 - render(VisualRegression, { 127 + const result = render(VisualRegression, { 110 128 props: { 111 129 regression: { 112 130 type: 'internal:toMatchScreenshot', ··· 121 139 const tabs = tablist.getByRole('tab') 122 140 123 141 await expect.element(tablist).toBeInTheDocument() 142 + expect(result.container).toMatchAriaInlineSnapshot(` 143 + - article: 144 + - heading "Visual Regression" [level=1] 145 + - paragraph: /.*/ 146 + - tablist: 147 + - tab "Reference" [selected] 148 + - tab "Actual" 149 + - tab "Slider" 150 + - tabpanel "Reference": 151 + - link: 152 + - /url: /.*/ 153 + - img 154 + `) 124 155 125 156 expect(tabs.all()).toHaveLength(3) 126 157 await expect.element(tabs.nth(0)).toHaveTextContent('Reference') ··· 129 160 130 161 await userEvent.click(tabs.nth(2)) 131 162 132 - await expect.element( 133 - page.getByLabelText( 134 - 'Image comparison slider showing reference and actual screenshots', 135 - ), 136 - ).toBeInTheDocument() 163 + await expect.element(tablist.getByRole('tab', { selected: true })).toHaveTextContent('Slider') 164 + expect(result.container).toMatchAriaInlineSnapshot(` 165 + - article: 166 + - heading "Visual Regression" [level=1] 167 + - paragraph: /.*/ 168 + - tablist: 169 + - tab "Reference" 170 + - tab "Actual" 171 + - tab "Slider" [selected] 172 + - tabpanel "Slider": 173 + - slider "Adjust slider to compare reference and actual images": "50" 174 + - status: Showing 50% reference, 50% actual 175 + `) 137 176 }) 138 177 139 178 it('renders diff, reference, actual, and slider tabs', async () => { 140 - render(VisualRegression, { 179 + const result = render(VisualRegression, { 141 180 props: { 142 181 regression: { 143 182 type: 'internal:toMatchScreenshot', ··· 158 197 await expect.element(tabs.nth(1)).toHaveTextContent('Reference') 159 198 await expect.element(tabs.nth(2)).toHaveTextContent('Actual') 160 199 await expect.element(tabs.nth(3)).toHaveTextContent('Slider') 200 + expect(result.container).toMatchAriaInlineSnapshot(` 201 + - article: 202 + - heading "Visual Regression" [level=1] 203 + - paragraph: /.*/ 204 + - tablist: 205 + - tab "Diff" [selected] 206 + - tab "Reference" 207 + - tab "Actual" 208 + - tab "Slider" 209 + - tabpanel "Diff": 210 + - link: 211 + - /url: /.*/ 212 + - img 213 + `) 161 214 }) 162 215 })
+6
test/snapshots/test/fixtures/domain-error/__snapshots__/basic.test.ts.snap
··· 1 + // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 + 3 + exports[`file 1`] = ` 4 + name=alice 5 + file-broken 6 + `;