Skip to content

Commit

Permalink
feat(config): add snapshotSerializers option (#5092)
Browse files Browse the repository at this point in the history
  • Loading branch information
fenghan34 committed Feb 6, 2024
1 parent 97c94ed commit 5b1021d
Show file tree
Hide file tree
Showing 16 changed files with 166 additions and 8 deletions.
9 changes: 8 additions & 1 deletion docs/config/index.md
Expand Up @@ -1662,9 +1662,16 @@ Format options for snapshot testing. These options are passed down to [`pretty-f
::: tip
Beware that `plugins` field on this object will be ignored.

If you need to extend snapshot serializer via pretty-format plugins, please, use [`expect.addSnapshotSerializer`](/api/expect#expect-addsnapshotserializer) API.
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.
:::

### snapshotSerializers<NonProjectOption /> <Badge type="info">1.3.0+</Badge>

- **Type:** `string[]`
- **Default:** `[]`

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.

### resolveSnapshotPath<NonProjectOption />

- **Type**: `(testPath: string, snapExtension: string) => string`
Expand Down
35 changes: 34 additions & 1 deletion docs/guide/snapshot.md
Expand Up @@ -117,7 +117,7 @@ You can learn more in the [`examples/image-snapshot`](https://github.com/vitest-

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.

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

```ts
expect.addSnapshotSerializer({
Expand All @@ -137,6 +137,39 @@ expect.addSnapshotSerializer({
})
```

We also support [snapshotSerializers](/config/#snapshotserializers-1-3-0) option to implicitly add custom serializers.

```ts
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 SnapshotSerializer
```


```ts
import { defineConfig } from 'vite'

export default defineConfig({
test: {
snapshotSerializers: ['path/to/custom-serializer.ts']
},
})
```

After adding a test like this:

```ts
Expand Down
10 changes: 8 additions & 2 deletions packages/browser/src/client/main.ts
Expand Up @@ -211,6 +211,7 @@ async function prepareTestEnvironment(config: ResolvedConfig) {
startTests,
setupCommonEnv,
loadDiffConfig,
loadSnapshotSerializers,
takeCoverageInsideWorker,
} = await importId('vitest/browser') as typeof import('vitest/browser')

Expand All @@ -228,6 +229,7 @@ async function prepareTestEnvironment(config: ResolvedConfig) {
startTests,
setupCommonEnv,
loadDiffConfig,
loadSnapshotSerializers,
executor,
runner,
}
Expand All @@ -244,7 +246,7 @@ async function runTests(paths: string[], config: ResolvedConfig) {
return
}

const { startTests, setupCommonEnv, loadDiffConfig, executor, runner } = preparedData!
const { startTests, setupCommonEnv, loadDiffConfig, loadSnapshotSerializers, executor, runner } = preparedData!

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

try {
runner.config.diffOptions = await loadDiffConfig(config, executor as VitestExecutor)
const [diffOptions] = await Promise.all([
loadDiffConfig(config, executor as VitestExecutor),
loadSnapshotSerializers(config, executor as VitestExecutor),
])
runner.config.diffOptions = diffOptions

await setupCommonEnv(config)
const files = paths.map((path) => {
Expand Down
1 change: 1 addition & 0 deletions packages/snapshot/src/index.ts
Expand Up @@ -10,6 +10,7 @@ export type {
SnapshotStateOptions,
SnapshotMatchOptions,
SnapshotResult,
SnapshotSerializer,
UncheckedSnapshot,
SnapshotSummary,
} from './types'
4 changes: 3 additions & 1 deletion packages/snapshot/src/types/index.ts
@@ -1,4 +1,4 @@
import type { OptionsReceived as PrettyFormatOptions } from 'pretty-format'
import type { OptionsReceived as PrettyFormatOptions, Plugin as PrettyFormatPlugin } from 'pretty-format'
import type { RawSnapshotInfo } from '../port/rawSnapshot'
import type { SnapshotEnvironment, SnapshotEnvironmentOptions } from './environment'

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

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

export type SnapshotSerializer = PrettyFormatPlugin

export interface SnapshotStateOptions {
updateSnapshot: SnapshotUpdateState
snapshotEnvironment: SnapshotEnvironment
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/browser.ts
@@ -1,3 +1,3 @@
export { startTests, processError } from '@vitest/runner'
export { setupCommonEnv, loadDiffConfig } from './runtime/setup-common'
export { setupCommonEnv, loadDiffConfig, loadSnapshotSerializers } from './runtime/setup-common'
export { takeCoverageInsideWorker, stopCoverageInsideWorker, getCoverageProvider, startCoverageInsideWorker } from './integrations/coverage'
6 changes: 6 additions & 0 deletions packages/vitest/src/node/config.ts
Expand Up @@ -234,6 +234,12 @@ export function resolveConfig(
snapshotEnvironment: null as any,
}

resolved.snapshotSerializers ??= []
resolved.snapshotSerializers = resolved.snapshotSerializers.map(file =>
resolvePath(file, resolved.root),
)
resolved.forceRerunTriggers.push(...resolved.snapshotSerializers)

if (options.resolveSnapshotPath)
delete (resolved as UserConfig).resolveSnapshotPath

Expand Down
8 changes: 6 additions & 2 deletions packages/vitest/src/runtime/runners/index.ts
Expand Up @@ -6,7 +6,7 @@ import { distDir } from '../../paths'
import { getWorkerState } from '../../utils/global'
import { rpc } from '../rpc'
import { takeCoverageInsideWorker } from '../../integrations/coverage'
import { loadDiffConfig } from '../setup-common'
import { loadDiffConfig, loadSnapshotSerializers } from '../setup-common'

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

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

testRunner.config.diffOptions = await loadDiffConfig(config, executor)
const [diffOptions] = await Promise.all([
loadDiffConfig(config, executor),
loadSnapshotSerializers(config, executor),
])
testRunner.config.diffOptions = diffOptions

// patch some methods, so custom runners don't need to call RPC
const originalOnTaskUpdate = testRunner.onTaskUpdate
Expand Down
22 changes: 22 additions & 0 deletions packages/vitest/src/runtime/setup-common.ts
@@ -1,4 +1,6 @@
import { setSafeTimers } from '@vitest/utils'
import { addSerializer } from '@vitest/snapshot'
import type { SnapshotSerializer } from '@vitest/snapshot'
import { resetRunOnceCounter } from '../integrations/run-once'
import type { ResolvedConfig } from '../types'
import type { DiffOptions } from '../types/matcher-utils'
Expand Down Expand Up @@ -35,3 +37,23 @@ export async function loadDiffConfig(config: ResolvedConfig, executor: VitestExe
else
throw new Error(`invalid diff config file ${config.diff}. Must have a default export with config object`)
}

export async function loadSnapshotSerializers(config: ResolvedConfig, executor: VitestExecutor) {
const files = config.snapshotSerializers

const snapshotSerializers = await Promise.all(
files.map(async (file) => {
const mo = await executor.executeId(file)
if (!mo || typeof mo.default !== 'object' || mo.default === null)
throw new Error(`invalid snapshot serializer file ${file}. Must export a default object`)

const config = mo.default
if (typeof config.test !== 'function' || (typeof config.serialize !== 'function' && typeof config.print !== 'function'))
throw new Error(`invalid snapshot serializer in ${file}. Must have a 'test' method along with either a 'serialize' or 'print' method.`)

return config as SnapshotSerializer
}),
)

snapshotSerializers.forEach(serializer => addSerializer(serializer))
}
5 changes: 5 additions & 0 deletions packages/vitest/src/types/config.ts
Expand Up @@ -533,6 +533,11 @@ export interface InlineConfig {
*/
diff?: string

/**
* Paths to snapshot serializer modules.
*/
snapshotSerializers?: string[]

/**
* Resolve custom snapshot path
*/
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/types/snapshot.ts
Expand Up @@ -6,4 +6,5 @@ export type {
SnapshotResult,
UncheckedSnapshot,
SnapshotSummary,
SnapshotSerializer,
} from '@vitest/snapshot'
11 changes: 11 additions & 0 deletions test/snapshots/test/custom-serializers.test.ts
@@ -0,0 +1,11 @@
import { expect, test } from 'vitest'
import { runVitest } from '../../test-utils'

test('it should pass', async () => {
const { stdout, stderr } = await runVitest({
root: 'test/fixtures/custom-serializers',
})

expect(stdout).toContain('✓ custom-serializers.test.ts >')
expect(stderr).toBe('')
})
@@ -0,0 +1,23 @@
import { test, expect } from "vitest";

test("", () => {
expect({foo: {
a: 1,
b: 2
}}).toMatchInlineSnapshot(`
Pretty foo: {
"a": 1,
"b": 2,
}
`);

expect({bar: {
a: 1,
b: 2
}}).toMatchInlineSnapshot(`
Pretty bar: {
"a": 1,
"b": 2,
}
`);
})
14 changes: 14 additions & 0 deletions test/snapshots/test/fixtures/custom-serializers/serializer-1.js
@@ -0,0 +1,14 @@
export default {
serialize(val, config, indentation, depth, refs, printer) {
return `Pretty foo: ${printer(
val.foo,
config,
indentation,
depth,
refs,
)}`
},
test(val) {
return val && Object.prototype.hasOwnProperty.call(val, 'foo')
},
}
16 changes: 16 additions & 0 deletions test/snapshots/test/fixtures/custom-serializers/serializer-2.ts
@@ -0,0 +1,16 @@
import { SnapshotSerializer } from 'vitest'

export default {
serialize(val, config, indentation, depth, refs, printer) {
return `Pretty bar: ${printer(
val.bar,
config,
indentation,
depth,
refs,
)}`
},
test(val) {
return val && Object.prototype.hasOwnProperty.call(val, 'bar')
},
} satisfies SnapshotSerializer
@@ -0,0 +1,7 @@
import { defineConfig } from "vitest/config";

export default defineConfig({
test: {
snapshotSerializers: ['./serializer-1.js', './serializer-2.ts']
}
})

0 comments on commit 5b1021d

Please sign in to comment.