Skip to content

Commit 5b1021d

Browse files
authoredFeb 6, 2024
feat(config): add snapshotSerializers option (#5092)
1 parent 97c94ed commit 5b1021d

File tree

16 files changed

+166
-8
lines changed

16 files changed

+166
-8
lines changed
 

‎docs/config/index.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -1662,9 +1662,16 @@ Format options for snapshot testing. These options are passed down to [`pretty-f
16621662
::: tip
16631663
Beware that `plugins` field on this object will be ignored.
16641664

1665-
If you need to extend snapshot serializer via pretty-format plugins, please, use [`expect.addSnapshotSerializer`](/api/expect#expect-addsnapshotserializer) API.
1665+
If you need to extend snapshot serializer via pretty-format plugins, please, use [`expect.addSnapshotSerializer`](/api/expect#expect-addsnapshotserializer) API or [snapshotSerializers](#snapshotserializers-1-3-0) option.
16661666
:::
16671667

1668+
### snapshotSerializers<NonProjectOption /> <Badge type="info">1.3.0+</Badge>
1669+
1670+
- **Type:** `string[]`
1671+
- **Default:** `[]`
1672+
1673+
A list of paths to snapshot serializer modules for snapshot testing, useful if you want add custom snapshot serializers. See [Custom Serializer](/guide/snapshot#custom-serializer) for more information.
1674+
16681675
### resolveSnapshotPath<NonProjectOption />
16691676

16701677
- **Type**: `(testPath: string, snapExtension: string) => string`

‎docs/guide/snapshot.md

+34-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ You can learn more in the [`examples/image-snapshot`](https://github.com/vitest-
117117

118118
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.
119119

120-
Example serializer module:
120+
You can explicitly add custom serializer by using [`expect.addSnapshotSerializer`](/api/expect#expect-addsnapshotserializer) API.
121121

122122
```ts
123123
expect.addSnapshotSerializer({
@@ -137,6 +137,39 @@ expect.addSnapshotSerializer({
137137
})
138138
```
139139

140+
We also support [snapshotSerializers](/config/#snapshotserializers-1-3-0) option to implicitly add custom serializers.
141+
142+
```ts
143+
import { SnapshotSerializer } from 'vitest'
144+
145+
export default {
146+
serialize(val, config, indentation, depth, refs, printer) {
147+
// `printer` is a function that serializes a value using existing plugins.
148+
return `Pretty foo: ${printer(
149+
val.foo,
150+
config,
151+
indentation,
152+
depth,
153+
refs,
154+
)}`
155+
},
156+
test(val) {
157+
return val && Object.prototype.hasOwnProperty.call(val, 'foo')
158+
},
159+
} satisfies SnapshotSerializer
160+
```
161+
162+
163+
```ts
164+
import { defineConfig } from 'vite'
165+
166+
export default defineConfig({
167+
test: {
168+
snapshotSerializers: ['path/to/custom-serializer.ts']
169+
},
170+
})
171+
```
172+
140173
After adding a test like this:
141174

142175
```ts

‎packages/browser/src/client/main.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ async function prepareTestEnvironment(config: ResolvedConfig) {
211211
startTests,
212212
setupCommonEnv,
213213
loadDiffConfig,
214+
loadSnapshotSerializers,
214215
takeCoverageInsideWorker,
215216
} = await importId('vitest/browser') as typeof import('vitest/browser')
216217

@@ -228,6 +229,7 @@ async function prepareTestEnvironment(config: ResolvedConfig) {
228229
startTests,
229230
setupCommonEnv,
230231
loadDiffConfig,
232+
loadSnapshotSerializers,
231233
executor,
232234
runner,
233235
}
@@ -244,7 +246,7 @@ async function runTests(paths: string[], config: ResolvedConfig) {
244246
return
245247
}
246248

247-
const { startTests, setupCommonEnv, loadDiffConfig, executor, runner } = preparedData!
249+
const { startTests, setupCommonEnv, loadDiffConfig, loadSnapshotSerializers, executor, runner } = preparedData!
248250

249251
onCancel.then((reason) => {
250252
runner?.onCancel?.(reason)
@@ -254,7 +256,11 @@ async function runTests(paths: string[], config: ResolvedConfig) {
254256
config.snapshotOptions.snapshotEnvironment = new BrowserSnapshotEnvironment()
255257

256258
try {
257-
runner.config.diffOptions = await loadDiffConfig(config, executor as VitestExecutor)
259+
const [diffOptions] = await Promise.all([
260+
loadDiffConfig(config, executor as VitestExecutor),
261+
loadSnapshotSerializers(config, executor as VitestExecutor),
262+
])
263+
runner.config.diffOptions = diffOptions
258264

259265
await setupCommonEnv(config)
260266
const files = paths.map((path) => {

‎packages/snapshot/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type {
1010
SnapshotStateOptions,
1111
SnapshotMatchOptions,
1212
SnapshotResult,
13+
SnapshotSerializer,
1314
UncheckedSnapshot,
1415
SnapshotSummary,
1516
} from './types'

‎packages/snapshot/src/types/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { OptionsReceived as PrettyFormatOptions } from 'pretty-format'
1+
import type { OptionsReceived as PrettyFormatOptions, Plugin as PrettyFormatPlugin } from 'pretty-format'
22
import type { RawSnapshotInfo } from '../port/rawSnapshot'
33
import type { SnapshotEnvironment, SnapshotEnvironmentOptions } from './environment'
44

@@ -7,6 +7,8 @@ export type SnapshotData = Record<string, string>
77

88
export type SnapshotUpdateState = 'all' | 'new' | 'none'
99

10+
export type SnapshotSerializer = PrettyFormatPlugin
11+
1012
export interface SnapshotStateOptions {
1113
updateSnapshot: SnapshotUpdateState
1214
snapshotEnvironment: SnapshotEnvironment

‎packages/vitest/src/browser.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export { startTests, processError } from '@vitest/runner'
2-
export { setupCommonEnv, loadDiffConfig } from './runtime/setup-common'
2+
export { setupCommonEnv, loadDiffConfig, loadSnapshotSerializers } from './runtime/setup-common'
33
export { takeCoverageInsideWorker, stopCoverageInsideWorker, getCoverageProvider, startCoverageInsideWorker } from './integrations/coverage'

‎packages/vitest/src/node/config.ts

+6
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,12 @@ export function resolveConfig(
234234
snapshotEnvironment: null as any,
235235
}
236236

237+
resolved.snapshotSerializers ??= []
238+
resolved.snapshotSerializers = resolved.snapshotSerializers.map(file =>
239+
resolvePath(file, resolved.root),
240+
)
241+
resolved.forceRerunTriggers.push(...resolved.snapshotSerializers)
242+
237243
if (options.resolveSnapshotPath)
238244
delete (resolved as UserConfig).resolveSnapshotPath
239245

‎packages/vitest/src/runtime/runners/index.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { distDir } from '../../paths'
66
import { getWorkerState } from '../../utils/global'
77
import { rpc } from '../rpc'
88
import { takeCoverageInsideWorker } from '../../integrations/coverage'
9-
import { loadDiffConfig } from '../setup-common'
9+
import { loadDiffConfig, loadSnapshotSerializers } from '../setup-common'
1010

1111
const runnersFile = resolve(distDir, 'runners.js')
1212

@@ -38,7 +38,11 @@ export async function resolveTestRunner(config: ResolvedConfig, executor: Vitest
3838
if (!testRunner.importFile)
3939
throw new Error('Runner must implement "importFile" method.')
4040

41-
testRunner.config.diffOptions = await loadDiffConfig(config, executor)
41+
const [diffOptions] = await Promise.all([
42+
loadDiffConfig(config, executor),
43+
loadSnapshotSerializers(config, executor),
44+
])
45+
testRunner.config.diffOptions = diffOptions
4246

4347
// patch some methods, so custom runners don't need to call RPC
4448
const originalOnTaskUpdate = testRunner.onTaskUpdate

‎packages/vitest/src/runtime/setup-common.ts

+22
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { setSafeTimers } from '@vitest/utils'
2+
import { addSerializer } from '@vitest/snapshot'
3+
import type { SnapshotSerializer } from '@vitest/snapshot'
24
import { resetRunOnceCounter } from '../integrations/run-once'
35
import type { ResolvedConfig } from '../types'
46
import type { DiffOptions } from '../types/matcher-utils'
@@ -35,3 +37,23 @@ export async function loadDiffConfig(config: ResolvedConfig, executor: VitestExe
3537
else
3638
throw new Error(`invalid diff config file ${config.diff}. Must have a default export with config object`)
3739
}
40+
41+
export async function loadSnapshotSerializers(config: ResolvedConfig, executor: VitestExecutor) {
42+
const files = config.snapshotSerializers
43+
44+
const snapshotSerializers = await Promise.all(
45+
files.map(async (file) => {
46+
const mo = await executor.executeId(file)
47+
if (!mo || typeof mo.default !== 'object' || mo.default === null)
48+
throw new Error(`invalid snapshot serializer file ${file}. Must export a default object`)
49+
50+
const config = mo.default
51+
if (typeof config.test !== 'function' || (typeof config.serialize !== 'function' && typeof config.print !== 'function'))
52+
throw new Error(`invalid snapshot serializer in ${file}. Must have a 'test' method along with either a 'serialize' or 'print' method.`)
53+
54+
return config as SnapshotSerializer
55+
}),
56+
)
57+
58+
snapshotSerializers.forEach(serializer => addSerializer(serializer))
59+
}

‎packages/vitest/src/types/config.ts

+5
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,11 @@ export interface InlineConfig {
533533
*/
534534
diff?: string
535535

536+
/**
537+
* Paths to snapshot serializer modules.
538+
*/
539+
snapshotSerializers?: string[]
540+
536541
/**
537542
* Resolve custom snapshot path
538543
*/

‎packages/vitest/src/types/snapshot.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ export type {
66
SnapshotResult,
77
UncheckedSnapshot,
88
SnapshotSummary,
9+
SnapshotSerializer,
910
} from '@vitest/snapshot'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { expect, test } from 'vitest'
2+
import { runVitest } from '../../test-utils'
3+
4+
test('it should pass', async () => {
5+
const { stdout, stderr } = await runVitest({
6+
root: 'test/fixtures/custom-serializers',
7+
})
8+
9+
expect(stdout).toContain('✓ custom-serializers.test.ts >')
10+
expect(stderr).toBe('')
11+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { test, expect } from "vitest";
2+
3+
test("", () => {
4+
expect({foo: {
5+
a: 1,
6+
b: 2
7+
}}).toMatchInlineSnapshot(`
8+
Pretty foo: {
9+
"a": 1,
10+
"b": 2,
11+
}
12+
`);
13+
14+
expect({bar: {
15+
a: 1,
16+
b: 2
17+
}}).toMatchInlineSnapshot(`
18+
Pretty bar: {
19+
"a": 1,
20+
"b": 2,
21+
}
22+
`);
23+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export default {
2+
serialize(val, config, indentation, depth, refs, printer) {
3+
return `Pretty foo: ${printer(
4+
val.foo,
5+
config,
6+
indentation,
7+
depth,
8+
refs,
9+
)}`
10+
},
11+
test(val) {
12+
return val && Object.prototype.hasOwnProperty.call(val, 'foo')
13+
},
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { SnapshotSerializer } from 'vitest'
2+
3+
export default {
4+
serialize(val, config, indentation, depth, refs, printer) {
5+
return `Pretty bar: ${printer(
6+
val.bar,
7+
config,
8+
indentation,
9+
depth,
10+
refs,
11+
)}`
12+
},
13+
test(val) {
14+
return val && Object.prototype.hasOwnProperty.call(val, 'bar')
15+
},
16+
} satisfies SnapshotSerializer
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { defineConfig } from "vitest/config";
2+
3+
export default defineConfig({
4+
test: {
5+
snapshotSerializers: ['./serializer-1.js', './serializer-2.ts']
6+
}
7+
})

0 commit comments

Comments
 (0)
Please sign in to comment.