diff --git a/docs/config/index.md b/docs/config/index.md index 3bb2f7733998..5f8a9091b3b2 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -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 1.3.0+ + +- **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 - **Type**: `(testPath: string, snapExtension: string) => string` diff --git a/docs/guide/snapshot.md b/docs/guide/snapshot.md index cfbc73f3d4e8..a91a109349b5 100644 --- a/docs/guide/snapshot.md +++ b/docs/guide/snapshot.md @@ -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({ @@ -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 diff --git a/packages/browser/src/client/main.ts b/packages/browser/src/client/main.ts index b52032bddcf9..5a103644575c 100644 --- a/packages/browser/src/client/main.ts +++ b/packages/browser/src/client/main.ts @@ -211,6 +211,7 @@ async function prepareTestEnvironment(config: ResolvedConfig) { startTests, setupCommonEnv, loadDiffConfig, + loadSnapshotSerializers, takeCoverageInsideWorker, } = await importId('vitest/browser') as typeof import('vitest/browser') @@ -228,6 +229,7 @@ async function prepareTestEnvironment(config: ResolvedConfig) { startTests, setupCommonEnv, loadDiffConfig, + loadSnapshotSerializers, executor, runner, } @@ -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) @@ -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) => { diff --git a/packages/snapshot/src/index.ts b/packages/snapshot/src/index.ts index 3e37a83cf8cc..7112c6da7b64 100644 --- a/packages/snapshot/src/index.ts +++ b/packages/snapshot/src/index.ts @@ -10,6 +10,7 @@ export type { SnapshotStateOptions, SnapshotMatchOptions, SnapshotResult, + SnapshotSerializer, UncheckedSnapshot, SnapshotSummary, } from './types' diff --git a/packages/snapshot/src/types/index.ts b/packages/snapshot/src/types/index.ts index ff4f100e76f2..66e7baf98ed1 100644 --- a/packages/snapshot/src/types/index.ts +++ b/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' @@ -7,6 +7,8 @@ export type SnapshotData = Record export type SnapshotUpdateState = 'all' | 'new' | 'none' +export type SnapshotSerializer = PrettyFormatPlugin + export interface SnapshotStateOptions { updateSnapshot: SnapshotUpdateState snapshotEnvironment: SnapshotEnvironment diff --git a/packages/vitest/src/browser.ts b/packages/vitest/src/browser.ts index 8081a6623b63..3887e79d2d89 100644 --- a/packages/vitest/src/browser.ts +++ b/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' diff --git a/packages/vitest/src/node/config.ts b/packages/vitest/src/node/config.ts index a032dbe10546..4087ebba3da8 100644 --- a/packages/vitest/src/node/config.ts +++ b/packages/vitest/src/node/config.ts @@ -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 diff --git a/packages/vitest/src/runtime/runners/index.ts b/packages/vitest/src/runtime/runners/index.ts index c7aca87e65ec..40197e90bc41 100644 --- a/packages/vitest/src/runtime/runners/index.ts +++ b/packages/vitest/src/runtime/runners/index.ts @@ -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') @@ -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 diff --git a/packages/vitest/src/runtime/setup-common.ts b/packages/vitest/src/runtime/setup-common.ts index 672e830272e3..1430366f7833 100644 --- a/packages/vitest/src/runtime/setup-common.ts +++ b/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' @@ -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)) +} diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index 6e4be4951b9f..adc7f3c06e2b 100644 --- a/packages/vitest/src/types/config.ts +++ b/packages/vitest/src/types/config.ts @@ -533,6 +533,11 @@ export interface InlineConfig { */ diff?: string + /** + * Paths to snapshot serializer modules. + */ + snapshotSerializers?: string[] + /** * Resolve custom snapshot path */ diff --git a/packages/vitest/src/types/snapshot.ts b/packages/vitest/src/types/snapshot.ts index 0aa0149af4c7..6d3e27ef5216 100644 --- a/packages/vitest/src/types/snapshot.ts +++ b/packages/vitest/src/types/snapshot.ts @@ -6,4 +6,5 @@ export type { SnapshotResult, UncheckedSnapshot, SnapshotSummary, + SnapshotSerializer, } from '@vitest/snapshot' diff --git a/test/snapshots/test/custom-serializers.test.ts b/test/snapshots/test/custom-serializers.test.ts new file mode 100644 index 000000000000..0a9fa46b385d --- /dev/null +++ b/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('') +}) diff --git a/test/snapshots/test/fixtures/custom-serializers/custom-serializers.test.ts b/test/snapshots/test/fixtures/custom-serializers/custom-serializers.test.ts new file mode 100644 index 000000000000..c93504a7ef83 --- /dev/null +++ b/test/snapshots/test/fixtures/custom-serializers/custom-serializers.test.ts @@ -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, + } + `); +}) diff --git a/test/snapshots/test/fixtures/custom-serializers/serializer-1.js b/test/snapshots/test/fixtures/custom-serializers/serializer-1.js new file mode 100644 index 000000000000..8a46483de1f6 --- /dev/null +++ b/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') + }, +} diff --git a/test/snapshots/test/fixtures/custom-serializers/serializer-2.ts b/test/snapshots/test/fixtures/custom-serializers/serializer-2.ts new file mode 100644 index 000000000000..a031ea5f04d8 --- /dev/null +++ b/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 diff --git a/test/snapshots/test/fixtures/custom-serializers/vitest.config.ts b/test/snapshots/test/fixtures/custom-serializers/vitest.config.ts new file mode 100644 index 000000000000..b410a059c950 --- /dev/null +++ b/test/snapshots/test/fixtures/custom-serializers/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + snapshotSerializers: ['./serializer-1.js', './serializer-2.ts'] + } +})