diff --git a/docs/config/index.md b/docs/config/index.md
index 45fe17789f58..3a778920b3cd 100644
--- a/docs/config/index.md
+++ b/docs/config/index.md
@@ -2200,3 +2200,31 @@ The `location` property has `column` and `line` values that correspond to the `t
::: tip
This option has no effect if you do not use custom code that relies on this.
:::
+
+### snapshotEnvironment 1.6.0 {#snapshotEnvironment}
+
+- **Type:** `string`
+
+Path to a custom snapshot environment implementation. This is useful if you are running your tests in an environment that doesn't support Node.js APIs. This option doesn't have any effect on a browser runner.
+
+This object should have the shape of `SnapshotEnvironment` and is used to resolve and read/write snapshot files:
+
+```ts
+export interface SnapshotEnvironment {
+ getVersion: () => string
+ getHeader: () => string
+ resolvePath: (filepath: string) => Promise
+ resolveRawPath: (testPath: string, rawPath: string) => Promise
+ saveSnapshotFile: (filepath: string, snapshot: string) => Promise
+ readSnapshotFile: (filepath: string) => Promise
+ removeSnapshotFile: (filepath: string) => Promise
+}
+```
+
+You can extend default `VitestSnapshotEnvironment` from `vitest/snapshot` entry point if you need to overwrite only a part of the API.
+
+::: warning
+This is a low-level option and should be used only for advanced cases where you don't have access to default Node.js APIs.
+
+If you just need to configure snapshots feature, use [`snapshotFormat`](#snapshotformat) or [`resolveSnapshotPath`](#resolvesnapshotpath) options.
+:::
diff --git a/packages/browser/src/client/runner.ts b/packages/browser/src/client/runner.ts
index 2f762ff61159..e25921ad7193 100644
--- a/packages/browser/src/client/runner.ts
+++ b/packages/browser/src/client/runner.ts
@@ -3,7 +3,7 @@ import type { ResolvedConfig } from 'vitest'
import type { VitestExecutor } from 'vitest/execute'
import { rpc } from './rpc'
import { getConfig, importId } from './utils'
-import { BrowserSnapshotEnvironment } from './snapshot'
+import { VitestBrowserSnapshotEnvironment } from './snapshot'
interface BrowserRunnerOptions {
config: ResolvedConfig
@@ -92,7 +92,10 @@ export async function initiateRunner() {
if (cachedRunner)
return cachedRunner
const config = getConfig()
- const [{ VitestTestRunner, NodeBenchmarkRunner }, { takeCoverageInsideWorker, loadDiffConfig, loadSnapshotSerializers }] = await Promise.all([
+ const [
+ { VitestTestRunner, NodeBenchmarkRunner },
+ { takeCoverageInsideWorker, loadDiffConfig, loadSnapshotSerializers },
+ ] = await Promise.all([
importId('vitest/runners') as Promise,
importId('vitest/browser') as Promise,
])
@@ -101,7 +104,7 @@ export async function initiateRunner() {
takeCoverage: () => takeCoverageInsideWorker(config.coverage, { executeId: importId }),
})
if (!config.snapshotOptions.snapshotEnvironment)
- config.snapshotOptions.snapshotEnvironment = new BrowserSnapshotEnvironment()
+ config.snapshotOptions.snapshotEnvironment = new VitestBrowserSnapshotEnvironment()
const runner = new BrowserRunner({
config,
})
diff --git a/packages/browser/src/client/snapshot.ts b/packages/browser/src/client/snapshot.ts
index adf9180c20e2..a441423b81a7 100644
--- a/packages/browser/src/client/snapshot.ts
+++ b/packages/browser/src/client/snapshot.ts
@@ -1,7 +1,7 @@
-import type { SnapshotEnvironment } from 'vitest'
-import { rpc } from './rpc'
+import type { VitestClient } from '@vitest/ws-client'
+import type { SnapshotEnvironment } from 'vitest/snapshot'
-export class BrowserSnapshotEnvironment implements SnapshotEnvironment {
+export class VitestBrowserSnapshotEnvironment implements SnapshotEnvironment {
getVersion(): string {
return '1'
}
@@ -30,3 +30,8 @@ export class BrowserSnapshotEnvironment implements SnapshotEnvironment {
return rpc().removeSnapshotFile(filepath)
}
}
+
+function rpc(): VitestClient['rpc'] {
+ // @ts-expect-error not typed global
+ return globalThis.__vitest_worker__.rpc
+}
diff --git a/packages/vitest/package.json b/packages/vitest/package.json
index 9c22a758b82f..5ae171420400 100644
--- a/packages/vitest/package.json
+++ b/packages/vitest/package.json
@@ -91,6 +91,10 @@
"./reporters": {
"types": "./dist/reporters.d.ts",
"default": "./dist/reporters.js"
+ },
+ "./snapshot": {
+ "types": "./dist/snapshot.d.ts",
+ "default": "./dist/snapshot.js"
}
},
"main": "./dist/index.js",
diff --git a/packages/vitest/rollup.config.js b/packages/vitest/rollup.config.js
index 8030ae847fd8..06cd3da01cec 100644
--- a/packages/vitest/rollup.config.js
+++ b/packages/vitest/rollup.config.js
@@ -41,6 +41,8 @@ const entries = {
'workers/vmForks': 'src/runtime/workers/vmForks.ts',
'workers/runVmTests': 'src/runtime/runVmTests.ts',
+
+ 'snapshot': 'src/snapshot.ts',
}
const dtsEntries = {
@@ -56,6 +58,7 @@ const dtsEntries = {
execute: 'src/public/execute.ts',
reporters: 'src/public/reporters.ts',
workers: 'src/workers.ts',
+ snapshot: 'src/snapshot.ts',
}
const external = [
diff --git a/packages/vitest/snapshot.d.ts b/packages/vitest/snapshot.d.ts
new file mode 100644
index 000000000000..e032fa789764
--- /dev/null
+++ b/packages/vitest/snapshot.d.ts
@@ -0,0 +1 @@
+export * from './dist/snapshot.js'
diff --git a/packages/vitest/src/index.ts b/packages/vitest/src/index.ts
index aab37704962d..4b298149dc56 100644
--- a/packages/vitest/src/index.ts
+++ b/packages/vitest/src/index.ts
@@ -17,6 +17,7 @@ export * from './integrations/chai'
export * from './integrations/vi'
export * from './integrations/utils'
export { inject } from './integrations/inject'
+// TODO: remove in 2.0.0, import from vitest/snapshot directly
export type { SnapshotEnvironment } from '@vitest/snapshot/environment'
export * from './types'
diff --git a/packages/vitest/src/integrations/snapshot/environments/node.ts b/packages/vitest/src/integrations/snapshot/environments/node.ts
index 739501a67f29..938a85836ac2 100644
--- a/packages/vitest/src/integrations/snapshot/environments/node.ts
+++ b/packages/vitest/src/integrations/snapshot/environments/node.ts
@@ -1,16 +1,13 @@
import { NodeSnapshotEnvironment } from '@vitest/snapshot/environment'
-import type { WorkerRPC } from '../../../types/worker'
-
-export class VitestSnapshotEnvironment extends NodeSnapshotEnvironment {
- constructor(private rpc: WorkerRPC) {
- super()
- }
+import { getWorkerState } from '../../../utils'
+export class VitestNodeSnapshotEnvironment extends NodeSnapshotEnvironment {
getHeader(): string {
return `// Vitest Snapshot v${this.getVersion()}, https://vitest.dev/guide/snapshot.html`
}
resolvePath(filepath: string): Promise {
- return this.rpc.resolveSnapshotPath(filepath)
+ const rpc = getWorkerState().rpc
+ return rpc.resolveSnapshotPath(filepath)
}
}
diff --git a/packages/vitest/src/integrations/snapshot/environments/resolveSnapshotEnvironment.ts b/packages/vitest/src/integrations/snapshot/environments/resolveSnapshotEnvironment.ts
new file mode 100644
index 000000000000..8dce8c769013
--- /dev/null
+++ b/packages/vitest/src/integrations/snapshot/environments/resolveSnapshotEnvironment.ts
@@ -0,0 +1,18 @@
+import type { SnapshotEnvironment } from '@vitest/snapshot/environment'
+import type { VitestExecutor } from '../../../runtime/execute'
+import type { ResolvedConfig } from '../../../types'
+
+export async function resolveSnapshotEnvironment(
+ config: ResolvedConfig,
+ executor: VitestExecutor,
+): Promise {
+ if (!config.snapshotEnvironment) {
+ const { VitestNodeSnapshotEnvironment } = await import('./node')
+ return new VitestNodeSnapshotEnvironment()
+ }
+
+ const mod = await executor.executeId(config.snapshotEnvironment)
+ if (typeof mod.default !== 'object' || !mod.default)
+ throw new Error('Snapshot environment module must have a default export object with a shape of `SnapshotEnvironment`')
+ return mod.default
+}
diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts
index 459db90a5472..0ae8b529b75d 100644
--- a/packages/vitest/src/node/cli/cli-config.ts
+++ b/packages/vitest/src/node/cli/cli-config.ts
@@ -637,4 +637,5 @@ export const cliOptionsConfig: VitestCLIOptions = {
deps: null,
name: null,
includeTaskLocation: null,
+ snapshotEnvironment: null,
}
diff --git a/packages/vitest/src/node/config.ts b/packages/vitest/src/node/config.ts
index 37b13f416d6f..66bbdcb6e899 100644
--- a/packages/vitest/src/node/config.ts
+++ b/packages/vitest/src/node/config.ts
@@ -251,6 +251,9 @@ export function resolveConfig(
if (resolved.runner)
resolved.runner = resolvePath(resolved.runner, resolved.root)
+ if (resolved.snapshotEnvironment)
+ resolved.snapshotEnvironment = resolvePath(resolved.snapshotEnvironment, resolved.root)
+
resolved.testNamePattern = resolved.testNamePattern
? resolved.testNamePattern instanceof RegExp
? resolved.testNamePattern
diff --git a/packages/vitest/src/runtime/runBaseTests.ts b/packages/vitest/src/runtime/runBaseTests.ts
index d9775aa308b0..9d8ae41b2414 100644
--- a/packages/vitest/src/runtime/runBaseTests.ts
+++ b/packages/vitest/src/runtime/runBaseTests.ts
@@ -14,7 +14,7 @@ import { closeInspector } from './inspector'
export async function run(files: string[], config: ResolvedConfig, environment: ResolvedTestEnvironment, executor: VitestExecutor): Promise {
const workerState = getWorkerState()
- await setupGlobalEnv(config, environment)
+ await setupGlobalEnv(config, environment, executor)
await startCoverageInsideWorker(config.coverage, executor)
if (config.chaiConfig)
diff --git a/packages/vitest/src/runtime/runVmTests.ts b/packages/vitest/src/runtime/runVmTests.ts
index 3121b7e6ffa3..34dcb3361c42 100644
--- a/packages/vitest/src/runtime/runVmTests.ts
+++ b/packages/vitest/src/runtime/runVmTests.ts
@@ -10,8 +10,8 @@ import { setupChaiConfig } from '../integrations/chai/config'
import { startCoverageInsideWorker, stopCoverageInsideWorker } from '../integrations/coverage'
import type { ResolvedConfig } from '../types'
import { getWorkerState } from '../utils/global'
-import { VitestSnapshotEnvironment } from '../integrations/snapshot/environments/node'
import * as VitestIndex from '../index'
+import { resolveSnapshotEnvironment } from '../integrations/snapshot/environments/resolveSnapshotEnvironment'
import type { VitestExecutor } from './execute'
import { resolveTestRunner } from './runners'
import { setupCommonEnv } from './setup-common'
@@ -27,8 +27,6 @@ export async function run(files: string[], config: ResolvedConfig, executor: Vit
enumerable: false,
})
- config.snapshotOptions.snapshotEnvironment = new VitestSnapshotEnvironment(workerState.rpc)
-
setupColors(createColors(isatty(1)))
if (workerState.environment.transformMode === 'web') {
@@ -55,7 +53,12 @@ export async function run(files: string[], config: ResolvedConfig, executor: Vit
if (config.chaiConfig)
setupChaiConfig(config.chaiConfig)
- const runner = await resolveTestRunner(config, executor)
+ const [runner, snapshotEnvironment] = await Promise.all([
+ resolveTestRunner(config, executor),
+ resolveSnapshotEnvironment(config, executor),
+ ])
+
+ config.snapshotOptions.snapshotEnvironment = snapshotEnvironment
workerState.onCancel.then((reason) => {
closeInspector(config)
diff --git a/packages/vitest/src/runtime/setup-node.ts b/packages/vitest/src/runtime/setup-node.ts
index 809eaccfe70a..0c52f5b8e5d9 100644
--- a/packages/vitest/src/runtime/setup-node.ts
+++ b/packages/vitest/src/runtime/setup-node.ts
@@ -5,15 +5,16 @@ import { isatty } from 'node:tty'
import { installSourcemapsSupport } from 'vite-node/source-map'
import { createColors, setupColors } from '@vitest/utils'
import type { EnvironmentOptions, ResolvedConfig, ResolvedTestEnvironment } from '../types'
-import { VitestSnapshotEnvironment } from '../integrations/snapshot/environments/node'
import { getSafeTimers, getWorkerState } from '../utils'
import * as VitestIndex from '../index'
import { expect } from '../integrations/chai'
+import { resolveSnapshotEnvironment } from '../integrations/snapshot/environments/resolveSnapshotEnvironment'
import { setupCommonEnv } from './setup-common'
+import type { VitestExecutor } from './execute'
// this should only be used in Node
let globalSetup = false
-export async function setupGlobalEnv(config: ResolvedConfig, { environment }: ResolvedTestEnvironment) {
+export async function setupGlobalEnv(config: ResolvedConfig, { environment }: ResolvedTestEnvironment, executor: VitestExecutor) {
await setupCommonEnv(config)
Object.defineProperty(globalThis, '__vitest_index__', {
@@ -24,7 +25,7 @@ export async function setupGlobalEnv(config: ResolvedConfig, { environment }: Re
const state = getWorkerState()
if (!state.config.snapshotOptions.snapshotEnvironment)
- state.config.snapshotOptions.snapshotEnvironment = new VitestSnapshotEnvironment(state.rpc)
+ state.config.snapshotOptions.snapshotEnvironment = await resolveSnapshotEnvironment(config, executor)
if (globalSetup)
return
diff --git a/packages/vitest/src/snapshot.ts b/packages/vitest/src/snapshot.ts
new file mode 100644
index 000000000000..12ca4245fee7
--- /dev/null
+++ b/packages/vitest/src/snapshot.ts
@@ -0,0 +1,4 @@
+export type { SnapshotEnvironment } from '@vitest/snapshot/environment'
+export {
+ VitestNodeSnapshotEnvironment as VitestSnapshotEnvironment,
+} from './integrations/snapshot/environments/node'
diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts
index efbd238904bd..a45546afa43c 100644
--- a/packages/vitest/src/types/config.ts
+++ b/packages/vitest/src/types/config.ts
@@ -560,6 +560,11 @@ export interface InlineConfig {
*/
resolveSnapshotPath?: (path: string, extension: string) => string
+ /**
+ * Path to a custom snapshot environment module that has a defualt export of `SnapshotEnvironment` object.
+ */
+ snapshotEnvironment?: string
+
/**
* Pass with no tests
*/
diff --git a/test/browser/custom-snapshot-env.ts b/test/browser/custom-snapshot-env.ts
new file mode 100644
index 000000000000..2265376b05c1
--- /dev/null
+++ b/test/browser/custom-snapshot-env.ts
@@ -0,0 +1 @@
+throw new Error('This file should not be executed')
diff --git a/test/browser/vitest.config.mts b/test/browser/vitest.config.mts
index 3056fad93a08..97bd6e9180e2 100644
--- a/test/browser/vitest.config.mts
+++ b/test/browser/vitest.config.mts
@@ -20,6 +20,8 @@ export default defineConfig({
},
test: {
include: ['test/**.test.{ts,js}'],
+ // having a snapshot environment doesn't affect browser tests
+ snapshotEnvironment: './custom-snapshot-env.ts',
browser: {
enabled: true,
name: browser,
diff --git a/test/snapshots/test/custom-environment.test.ts b/test/snapshots/test/custom-environment.test.ts
new file mode 100644
index 000000000000..215de0b1bc08
--- /dev/null
+++ b/test/snapshots/test/custom-environment.test.ts
@@ -0,0 +1,33 @@
+import { readFileSync, rmSync, writeFileSync } from 'node:fs'
+import { afterEach, expect, test } from 'vitest'
+import { dirname, resolve } from 'pathe'
+import { runVitest } from '../../test-utils'
+
+const testFileName = resolve(import.meta.dirname, './fixtures/custom-snapshot-environment/test/snapshots.test.ts')
+const snapshotFile = resolve(dirname(testFileName), './__snapshots__/snapshots.test.ts.snap')
+const testFile = readFileSync(testFileName, 'utf-8')
+
+afterEach(() => {
+ writeFileSync(testFileName, testFile)
+ rmSync(snapshotFile)
+})
+
+test('custom environment resolved correctly', async () => {
+ const { stdout, stderr } = await runVitest({
+ root: 'test/fixtures/custom-snapshot-environment',
+ update: true,
+ })
+
+ const snapshotLogs = stdout.split('\n').filter(i => i.startsWith('## ')).join('\n')
+ expect(stderr).toBe('')
+ expect(snapshotLogs).toMatchInlineSnapshot(`
+ "## resolvePath test/fixtures/custom-snapshot-environment/test/snapshots.test.ts
+ ## readSnapshotFile test/fixtures/custom-snapshot-environment/test/__snapshots__/snapshots.test.ts.snap
+ ## getHeader
+ ## getVersion
+ ## readSnapshotFile test/fixtures/custom-snapshot-environment/test/__snapshots__/snapshots.test.ts.snap
+ ## saveSnapshotFile test/fixtures/custom-snapshot-environment/test/__snapshots__/snapshots.test.ts.snap
+ ## readSnapshotFile test/fixtures/custom-snapshot-environment/test/snapshots.test.ts
+ ## saveSnapshotFile test/fixtures/custom-snapshot-environment/test/snapshots.test.ts"
+ `)
+})
diff --git a/test/snapshots/test/fixtures/custom-snapshot-environment/snapshot-environment.ts b/test/snapshots/test/fixtures/custom-snapshot-environment/snapshot-environment.ts
new file mode 100644
index 000000000000..1675ee9395cf
--- /dev/null
+++ b/test/snapshots/test/fixtures/custom-snapshot-environment/snapshot-environment.ts
@@ -0,0 +1,45 @@
+import { relative as _relative } from 'pathe'
+import { VitestSnapshotEnvironment } from 'vitest/snapshot'
+
+function relative(file: string) {
+ return _relative(process.cwd(), file)
+}
+
+class CustomSnapshotEnvironment extends VitestSnapshotEnvironment {
+ getVersion(): string {
+ console.log('## getVersion')
+ return super.getVersion()
+ }
+
+ getHeader() {
+ console.log('## getHeader')
+ return super.getHeader()
+ }
+
+ resolvePath(filepath: string) {
+ console.log('## resolvePath', relative(filepath))
+ return super.resolvePath(filepath)
+ }
+
+ resolveRawPath(testPath: string, rawPath: string) {
+ console.log('## resolveRawPath', relative(testPath), relative(rawPath))
+ return super.resolveRawPath(testPath, rawPath)
+ }
+
+ saveSnapshotFile(filepath: string, snapshot: string) {
+ console.log('## saveSnapshotFile', relative(filepath))
+ return super.saveSnapshotFile(filepath, snapshot)
+ }
+
+ readSnapshotFile(filepath: string) {
+ console.log('## readSnapshotFile', relative(filepath))
+ return super.readSnapshotFile(filepath)
+ }
+
+ removeSnapshotFile(filepath: string) {
+ console.log('## removeSnapshotFile', relative(filepath))
+ return super.removeSnapshotFile(filepath)
+ }
+}
+
+export default new CustomSnapshotEnvironment()
diff --git a/test/snapshots/test/fixtures/custom-snapshot-environment/test/snapshots.test.ts b/test/snapshots/test/fixtures/custom-snapshot-environment/test/snapshots.test.ts
new file mode 100644
index 000000000000..d6198ffce8ef
--- /dev/null
+++ b/test/snapshots/test/fixtures/custom-snapshot-environment/test/snapshots.test.ts
@@ -0,0 +1,9 @@
+import {test, expect} from 'vitest'
+
+test('regular snapshot', () => {
+ expect({ a: 1 }).toMatchSnapshot()
+})
+
+test('inline snapshot', () => {
+ expect({ a: 1 }).toMatchInlineSnapshot()
+})
diff --git a/test/snapshots/test/fixtures/custom-snapshot-environment/vitest.config.ts b/test/snapshots/test/fixtures/custom-snapshot-environment/vitest.config.ts
new file mode 100644
index 000000000000..9c804c75d2ef
--- /dev/null
+++ b/test/snapshots/test/fixtures/custom-snapshot-environment/vitest.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ snapshotEnvironment: './snapshot-environment.ts'
+ }
+})
\ No newline at end of file