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']
+ }
+})