Snapshot
Learn Snapshot by video from Vue SchoolSnapshot tests are a very useful tool whenever you want to make sure the output of your functions does not change unexpectedly.
When using snapshot, Vitest will take a snapshot of the given value, then compare it to a reference snapshot file stored alongside the test. The test will fail if the two snapshots do not match: either the change is unexpected, or the reference snapshot needs to be updated to the new version of the result.
Use Snapshots
To snapshot a value, you can use the toMatchSnapshot() from expect() API:
import { expect, it } from 'vitest'
it('toUpperCase', () => {
const result = toUpperCase('foobar')
expect(result).toMatchSnapshot()
})The first time this test is run, Vitest creates a snapshot file that looks like this:
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports['toUpperCase 1'] = '"FOOBAR"'The snapshot artifact should be committed alongside code changes, and reviewed as part of your code review process. On subsequent test runs, Vitest will compare the rendered output with the previous snapshot. If they match, the test will pass. If they don't match, either the test runner found a bug in your code that should be fixed, or the implementation has changed and the snapshot needs to be updated.
WARNING
When using Snapshots with async concurrent tests, expect from the local Test Context must be used to ensure the right test is detected.
Inline Snapshots
Similarly, you can use the toMatchInlineSnapshot() to store the snapshot inline within the test file.
import { expect, it } from 'vitest'
it('toUpperCase', () => {
const result = toUpperCase('foobar')
expect(result).toMatchInlineSnapshot()
})Instead of creating a snapshot file, Vitest will modify the test file directly to update the snapshot as a string:
import { expect, it } from 'vitest'
it('toUpperCase', () => {
const result = toUpperCase('foobar')
expect(result).toMatchInlineSnapshot('"FOOBAR"')
})This allows you to see the expected output directly without jumping across different files.
WARNING
When using Snapshots with async concurrent tests, expect from the local Test Context must be used to ensure the right test is detected.
Updating Snapshots
When the received value doesn't match the snapshot, the test fails and shows you the difference between them. When the snapshot change is expected, you may want to update the snapshot from the current state.
In watch mode, you can press the u key in the terminal to update the failed snapshot directly.
Or you can use the --update or -u flag in the CLI to make Vitest update snapshots.
vitest -uCI behavior
By default, Vitest does not write snapshots in CI (process.env.CI is truthy) and any snapshot mismatches, missing snapshots, and obsolete snapshots fail the run. See update for the details.
An obsolete snapshot is a snapshot entry (or snapshot file) that no longer matches any collected test. This usually happens after removing or renaming tests.
File Snapshots
When calling toMatchSnapshot(), we store all snapshots in a formatted snap file. That means we need to escape some characters (namely the double-quote " and backtick `) in the snapshot string. Meanwhile, you might lose the syntax highlighting for the snapshot content (if they are in some language).
In light of this, we introduced toMatchFileSnapshot() to explicitly match against a file. This allows you to assign any file extension to the snapshot file, and makes them more readable.
import { expect, it } from 'vitest'
it('render basic', async () => {
const result = renderHTML(h('div', { class: 'foo' }))
await expect(result).toMatchFileSnapshot('./test/basic.output.html')
})It will compare with the content of ./test/basic.output.html. And can be written back with the --update flag.
Visual Snapshots
For visual regression testing of UI components and pages, Vitest provides built-in support through browser mode with the toMatchScreenshot() assertion:
import { expect, test } from 'vitest'
import { page } from 'vitest/browser'
test('button looks correct', async () => {
const button = page.getByRole('button')
await expect(button).toMatchScreenshot('primary-button')
})This captures screenshots and compares them against reference images to detect unintended visual changes. Learn more in the Visual Regression Testing guide.
ARIA Snapshots
ARIA snapshots capture the accessibility tree of a DOM element and compare it against a stored template. Based on Playwright's ARIA snapshots, they provide a semantic alternative to visual regression testing — asserting structure and meaning rather than pixels.
For example, given this HTML:
<nav aria-label="Main">
<a href="/">Home</a>
<a href="/about">About</a>
</nav>You can assert its accessibility tree:
import { expect, test } from 'vitest'
import { page } from 'vitest/browser'
test('navigation structure', async () => {
await expect.element(page.getByRole('navigation')).toMatchAriaInlineSnapshot(`
- navigation "Main":
- link "Home":
- /url: /
- link "About":
- /url: /about
`)
})See the dedicated ARIA Snapshots guide for syntax details, retry behavior in Browser Mode, and file vs. inline snapshot examples. See toMatchAriaSnapshot and toMatchAriaInlineSnapshot for the full API reference.
Custom Serializer
You can add your own logic to alter how your snapshots are serialized. Like Jest, Vitest has default serializers for built-in JavaScript types, HTML elements, ImmutableJS and for React elements.
You can explicitly add custom serializer by using expect.addSnapshotSerializer API.
expect.addSnapshotSerializer({
serialize(val, config, indentation, depth, refs, printer) {
// `printer` is a function that serializes a value using existing plugins.
return `Pretty foo: ${printer(
val.foo,
config,
indentation,
depth,
refs,
)}`
},
test(val) {
return val && Object.prototype.hasOwnProperty.call(val, 'foo')
},
})We also support snapshotSerializers option to implicitly add custom serializers.
import { SnapshotSerializer } from 'vitest'
export default {
serialize(val, config, indentation, depth, refs, printer) {
// `printer` is a function that serializes a value using existing plugins.
return `Pretty foo: ${printer(
val.foo,
config,
indentation,
depth,
refs,
)}`
},
test(val) {
return val && Object.prototype.hasOwnProperty.call(val, 'foo')
},
} satisfies SnapshotSerializerimport { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
snapshotSerializers: ['path/to/custom-serializer.ts'],
},
})After adding a test like this:
test('foo snapshot test', () => {
const bar = {
foo: {
x: 1,
y: 2,
},
}
expect(bar).toMatchSnapshot()
})You will get the following snapshot:
Pretty foo: Object {
"x": 1,
"y": 2,
}We are using Jest's pretty-format for serializing snapshots. You can read more about it here: pretty-format.
Custom Snapshot Matchers experimental 4.1.3+
You can build custom snapshot matchers using composable functions exported from vitest. These let you transform values before snapshotting while preserving full snapshot lifecycle support (creation, update, inline rewriting).
import { expect, test, toMatchFileSnapshot, toMatchInlineSnapshot, toMatchSnapshot } from 'vitest'
expect.extend({
toMatchTrimmedSnapshot(received: string, length: number) {
return toMatchSnapshot.call(this, received.slice(0, length))
},
toMatchTrimmedInlineSnapshot(received: string, inlineSnapshot?: string) {
return toMatchInlineSnapshot.call(this, received.slice(0, 10), inlineSnapshot)
},
async toMatchTrimmedFileSnapshot(received: string, file: string) {
return toMatchFileSnapshot.call(this, received.slice(0, 10), file)
},
})
test('file snapshot', () => {
expect('extra long string oh my gerd').toMatchTrimmedSnapshot(10)
})
test('inline snapshot', () => {
expect('extra long string oh my gerd').toMatchTrimmedInlineSnapshot()
})
test('raw file snapshot', async () => {
await expect('extra long string oh my gerd').toMatchTrimmedFileSnapshot('./raw-file.txt')
})The composables return { pass, message } so you can further customize the error:
expect.extend({
toMatchTrimmedSnapshot(received: string, length: number) {
const result = toMatchSnapshot.call(this, received.slice(0, length))
return { ...result, message: () => `Trimmed snapshot failed: ${result.message()}` }
},
})WARNING
For inline snapshot matchers, the snapshot argument must be the last parameter (or second-to-last when using property matchers). Vitest rewrites the last string argument in the source code, so custom arguments before the snapshot work, but custom arguments after it are not supported.
TIP
File snapshot matchers must be async — toMatchFileSnapshot returns a Promise. Remember to await the result in the matcher and in your test.
For TypeScript, extend the Assertion interface:
import 'vitest'
declare module 'vitest' {
interface Assertion<T = any> {
toMatchTrimmedSnapshot: (length: number) => T
toMatchTrimmedInlineSnapshot: (inlineSnapshot?: string) => T
toMatchTrimmedFileSnapshot: (file: string) => Promise<T>
}
}TIP
See Extending Matchers for more on expect.extend and custom matcher conventions.
Custom Snapshot Domain experimental
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.
The adapter interface
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):
import type { DomainMatchResult, DomainSnapshotAdapter } from '@vitest/snapshot'
const myAdapter: DomainSnapshotAdapter<Captured, Expected> = {
name: 'my-domain',
// Extract structured data from the received value
capture(received: unknown): Captured { /* ... */ },
// Render captured data as the snapshot string (what gets stored)
render(captured: Captured): string { /* ... */ },
// Parse a stored snapshot string into a structured expected value
parseExpected(input: string): Expected { /* ... */ },
// Compare captured vs expected, return pass/fail and resolved output
match(captured: Captured, expected: Expected): DomainMatchResult { /* ... */ },
}DomainMatchResult
The match method returns a DomainMatchResult with two optional string fields beyond pass:
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 torender(capture(received)).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.
Why are Captured and Expected separate types?
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.
For example, in the key-value adapter below, Captured values are always string, but Expected values can be string | RegExp:
type KVCaptured = Record<string, string>
type KVExpected = Record<string, string | RegExp>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.
Build a matcher from the adapter
Register a custom matcher with expect.extend(...) and call the snapshot composables from vitest:
import { expect, toMatchDomainInlineSnapshot, toMatchDomainSnapshot } from 'vitest'
expect.extend({
toMatchMyDomainSnapshot(received: unknown) {
return toMatchDomainSnapshot.call(this, myAdapter, received)
},
toMatchMyDomainInlineSnapshot(received: unknown, inlineSnapshot?: string) {
return toMatchDomainInlineSnapshot.call(
this,
myAdapter,
received,
inlineSnapshot,
)
},
})Then use your matcher in tests:
expect(value).toMatchMyDomainSnapshot()
expect(value).toMatchMyDomainInlineSnapshot(`key=value`)Example: key-value adapter
A minimal adapter that stores objects as key=value lines, with regex pattern and subset key match support (full source):
import type { DomainMatchResult, DomainSnapshotAdapter } from '@vitest/snapshot'
type KVCaptured = Record<string, string>
type KVExpected = Record<string, string | RegExp>
function renderKV(obj: Record<string, unknown>) {
return `\n${Object.entries(obj).map(([k, v]) => `${k}=${v}`).join('\n')}\n`
}
export const kvAdapter: DomainSnapshotAdapter<KVCaptured, KVExpected> = {
name: 'kv',
capture(received: unknown): KVCaptured {
if (received && typeof received === 'object') {
return Object.fromEntries(
Object.entries(received).map(([k, v]) => [k, String(v)]),
)
}
throw new TypeError('kv adapter expects a plain object')
},
render(captured: KVCaptured): string {
return renderKV(captured)
},
parseExpected(input: string): KVExpected {
const entries = input.trim().split('\n').map((line) => {
const eq = line.indexOf('=')
const key = line.slice(0, eq)
const raw = line.slice(eq + 1)
const value = (raw.startsWith('/') && raw.endsWith('/') && raw.length > 1)
? new RegExp(raw.slice(1, -1))
: raw
return [key, value]
})
return Object.fromEntries(entries)
},
match(captured: KVCaptured, expected: KVExpected): DomainMatchResult {
const resolvedLines: string[] = []
let pass = true
for (const [key, actualValue] of Object.entries(captured)) {
const expectedValue = expected[key]
// non-asserted keys are skipped (works as subset match)
if (typeof expectedValue === 'undefined') {
continue
}
// preserve matched pattern for normalized diff and partial update
if (expectedValue instanceof RegExp && expectedValue.test(actualValue)) {
resolvedLines.push(`${key}=/${expectedValue.source}/`)
continue
}
resolvedLines.push(`${key}=${actualValue}`)
pass &&= actualValue === expectedValue
}
return {
pass,
message: pass ? undefined : 'KV entries do not match',
resolved: `\n${resolvedLines.join('\n')}\n`,
expected: `\n${renderKV(expected)}\n`,
}
},
}import { expect, toMatchDomainInlineSnapshot, toMatchDomainSnapshot } from 'vitest'
import { kvAdapter } from './kv-adapter'
expect.extend({
toMatchKvSnapshot(received: unknown) {
return toMatchDomainSnapshot.call(this, kvAdapter, received)
},
toMatchKvInlineSnapshot(received: unknown, inlineSnapshot?: string) {
return toMatchDomainInlineSnapshot.call(this, kvAdapter, received, inlineSnapshot)
},
})import { expect, test } from 'vitest'
test('user data', () => {
const user = { name: 'Alice', score: '42' }
expect(user).toMatchKvSnapshot()
})
test('user data inline', () => {
const user = { name: 'Alice', age: 100, score: '42' }
expect(user).toMatchKvInlineSnapshot(`
name=Alice
score=/\\d+/
`)
})Difference from Jest
Vitest provides an almost compatible Snapshot feature with Jest's with a few exceptions:
1. Comment header in the snapshot file is different
- // Jest Snapshot v1, https://goo.gl/fbAQLP
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.htmlThis does not really affect the functionality but might affect your commit diff when migrating from Jest.
2. printBasicPrototype is default to false
Both Jest and Vitest's snapshots are powered by pretty-format. In Vitest we set printBasicPrototype default to false to provide a cleaner snapshot output, while in Jest <29.0.0 it's true by default.
import { expect, test } from 'vitest'
test('snapshot', () => {
const bar = [
{
foo: 'bar',
},
]
// in Jest
expect(bar).toMatchInlineSnapshot(`
Array [
Object {
"foo": "bar",
},
]
`)
// in Vitest
expect(bar).toMatchInlineSnapshot(`
[
{
"foo": "bar",
},
]
`)
})We believe this is a more reasonable default for readability and overall DX. If you still prefer Jest's behavior, you can change your config:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
snapshotFormat: {
printBasicPrototype: true,
},
},
})3. Chevron > is used as a separator instead of colon : for custom messages
Vitest uses chevron > as a separator instead of colon : for readability, when a custom message is passed during creation of a snapshot file.
For the following example test code:
test('toThrowErrorMatchingSnapshot', () => {
expect(() => {
throw new Error('error')
}).toThrowErrorMatchingSnapshot('hint')
})In Jest, the snapshot will be:
exports[`toThrowErrorMatchingSnapshot: hint 1`] = `"error"`;In Vitest, the equivalent snapshot will be:
exports[`toThrowErrorMatchingSnapshot > hint 1`] = `[Error: error]`;4. default Error snapshot is different for toThrowErrorMatchingSnapshot and toThrowErrorMatchingInlineSnapshot
import { expect, test } from 'vitest'
test('snapshot', () => {
// in Jest and Vitest
expect(new Error('error')).toMatchInlineSnapshot(`[Error: error]`)
// Jest snapshots `Error.message` for `Error` instance
// Vitest prints the same value as toMatchInlineSnapshot
expect(() => {
throw new Error('error')
}).toThrowErrorMatchingInlineSnapshot(`"error"`)
}).toThrowErrorMatchingInlineSnapshot(`[Error: error]`)
})