Skip to content

Commit 30f728b

Browse files
authored
feat: custom "snapshotEnvironment" option (#5449)
1 parent 2f91322 commit 30f728b

22 files changed

+195
-21
lines changed

docs/config/index.md

+28
Original file line numberDiff line numberDiff line change
@@ -2200,3 +2200,31 @@ The `location` property has `column` and `line` values that correspond to the `t
22002200
::: tip
22012201
This option has no effect if you do not use custom code that relies on this.
22022202
:::
2203+
2204+
### snapshotEnvironment <Version>1.6.0</Version> {#snapshotEnvironment}
2205+
2206+
- **Type:** `string`
2207+
2208+
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.
2209+
2210+
This object should have the shape of `SnapshotEnvironment` and is used to resolve and read/write snapshot files:
2211+
2212+
```ts
2213+
export interface SnapshotEnvironment {
2214+
getVersion: () => string
2215+
getHeader: () => string
2216+
resolvePath: (filepath: string) => Promise<string>
2217+
resolveRawPath: (testPath: string, rawPath: string) => Promise<string>
2218+
saveSnapshotFile: (filepath: string, snapshot: string) => Promise<void>
2219+
readSnapshotFile: (filepath: string) => Promise<string | null>
2220+
removeSnapshotFile: (filepath: string) => Promise<void>
2221+
}
2222+
```
2223+
2224+
You can extend default `VitestSnapshotEnvironment` from `vitest/snapshot` entry point if you need to overwrite only a part of the API.
2225+
2226+
::: warning
2227+
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.
2228+
2229+
If you just need to configure snapshots feature, use [`snapshotFormat`](#snapshotformat) or [`resolveSnapshotPath`](#resolvesnapshotpath) options.
2230+
:::

packages/browser/src/client/runner.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { ResolvedConfig } from 'vitest'
33
import type { VitestExecutor } from 'vitest/execute'
44
import { rpc } from './rpc'
55
import { getConfig, importId } from './utils'
6-
import { BrowserSnapshotEnvironment } from './snapshot'
6+
import { VitestBrowserSnapshotEnvironment } from './snapshot'
77

88
interface BrowserRunnerOptions {
99
config: ResolvedConfig
@@ -92,7 +92,10 @@ export async function initiateRunner() {
9292
if (cachedRunner)
9393
return cachedRunner
9494
const config = getConfig()
95-
const [{ VitestTestRunner, NodeBenchmarkRunner }, { takeCoverageInsideWorker, loadDiffConfig, loadSnapshotSerializers }] = await Promise.all([
95+
const [
96+
{ VitestTestRunner, NodeBenchmarkRunner },
97+
{ takeCoverageInsideWorker, loadDiffConfig, loadSnapshotSerializers },
98+
] = await Promise.all([
9699
importId('vitest/runners') as Promise<typeof import('vitest/runners')>,
97100
importId('vitest/browser') as Promise<typeof import('vitest/browser')>,
98101
])
@@ -101,7 +104,7 @@ export async function initiateRunner() {
101104
takeCoverage: () => takeCoverageInsideWorker(config.coverage, { executeId: importId }),
102105
})
103106
if (!config.snapshotOptions.snapshotEnvironment)
104-
config.snapshotOptions.snapshotEnvironment = new BrowserSnapshotEnvironment()
107+
config.snapshotOptions.snapshotEnvironment = new VitestBrowserSnapshotEnvironment()
105108
const runner = new BrowserRunner({
106109
config,
107110
})

packages/browser/src/client/snapshot.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type { SnapshotEnvironment } from 'vitest'
2-
import { rpc } from './rpc'
1+
import type { VitestClient } from '@vitest/ws-client'
2+
import type { SnapshotEnvironment } from 'vitest/snapshot'
33

4-
export class BrowserSnapshotEnvironment implements SnapshotEnvironment {
4+
export class VitestBrowserSnapshotEnvironment implements SnapshotEnvironment {
55
getVersion(): string {
66
return '1'
77
}
@@ -30,3 +30,8 @@ export class BrowserSnapshotEnvironment implements SnapshotEnvironment {
3030
return rpc().removeSnapshotFile(filepath)
3131
}
3232
}
33+
34+
function rpc(): VitestClient['rpc'] {
35+
// @ts-expect-error not typed global
36+
return globalThis.__vitest_worker__.rpc
37+
}

packages/vitest/package.json

+4
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@
9191
"./reporters": {
9292
"types": "./dist/reporters.d.ts",
9393
"default": "./dist/reporters.js"
94+
},
95+
"./snapshot": {
96+
"types": "./dist/snapshot.d.ts",
97+
"default": "./dist/snapshot.js"
9498
}
9599
},
96100
"main": "./dist/index.js",

packages/vitest/rollup.config.js

+3
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ const entries = {
4141
'workers/vmForks': 'src/runtime/workers/vmForks.ts',
4242

4343
'workers/runVmTests': 'src/runtime/runVmTests.ts',
44+
45+
'snapshot': 'src/snapshot.ts',
4446
}
4547

4648
const dtsEntries = {
@@ -56,6 +58,7 @@ const dtsEntries = {
5658
execute: 'src/public/execute.ts',
5759
reporters: 'src/public/reporters.ts',
5860
workers: 'src/workers.ts',
61+
snapshot: 'src/snapshot.ts',
5962
}
6063

6164
const external = [

packages/vitest/snapshot.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './dist/snapshot.js'

packages/vitest/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export * from './integrations/chai'
1717
export * from './integrations/vi'
1818
export * from './integrations/utils'
1919
export { inject } from './integrations/inject'
20+
// TODO: remove in 2.0.0, import from vitest/snapshot directly
2021
export type { SnapshotEnvironment } from '@vitest/snapshot/environment'
2122

2223
export * from './types'
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import { NodeSnapshotEnvironment } from '@vitest/snapshot/environment'
2-
import type { WorkerRPC } from '../../../types/worker'
3-
4-
export class VitestSnapshotEnvironment extends NodeSnapshotEnvironment {
5-
constructor(private rpc: WorkerRPC) {
6-
super()
7-
}
2+
import { getWorkerState } from '../../../utils'
83

4+
export class VitestNodeSnapshotEnvironment extends NodeSnapshotEnvironment {
95
getHeader(): string {
106
return `// Vitest Snapshot v${this.getVersion()}, https://vitest.dev/guide/snapshot.html`
117
}
128

139
resolvePath(filepath: string): Promise<string> {
14-
return this.rpc.resolveSnapshotPath(filepath)
10+
const rpc = getWorkerState().rpc
11+
return rpc.resolveSnapshotPath(filepath)
1512
}
1613
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { SnapshotEnvironment } from '@vitest/snapshot/environment'
2+
import type { VitestExecutor } from '../../../runtime/execute'
3+
import type { ResolvedConfig } from '../../../types'
4+
5+
export async function resolveSnapshotEnvironment(
6+
config: ResolvedConfig,
7+
executor: VitestExecutor,
8+
): Promise<SnapshotEnvironment> {
9+
if (!config.snapshotEnvironment) {
10+
const { VitestNodeSnapshotEnvironment } = await import('./node')
11+
return new VitestNodeSnapshotEnvironment()
12+
}
13+
14+
const mod = await executor.executeId(config.snapshotEnvironment)
15+
if (typeof mod.default !== 'object' || !mod.default)
16+
throw new Error('Snapshot environment module must have a default export object with a shape of `SnapshotEnvironment`')
17+
return mod.default
18+
}

packages/vitest/src/node/cli/cli-config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -637,4 +637,5 @@ export const cliOptionsConfig: VitestCLIOptions = {
637637
deps: null,
638638
name: null,
639639
includeTaskLocation: null,
640+
snapshotEnvironment: null,
640641
}

packages/vitest/src/node/config.ts

+3
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,9 @@ export function resolveConfig(
251251
if (resolved.runner)
252252
resolved.runner = resolvePath(resolved.runner, resolved.root)
253253

254+
if (resolved.snapshotEnvironment)
255+
resolved.snapshotEnvironment = resolvePath(resolved.snapshotEnvironment, resolved.root)
256+
254257
resolved.testNamePattern = resolved.testNamePattern
255258
? resolved.testNamePattern instanceof RegExp
256259
? resolved.testNamePattern

packages/vitest/src/runtime/runBaseTests.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { closeInspector } from './inspector'
1414
export async function run(files: string[], config: ResolvedConfig, environment: ResolvedTestEnvironment, executor: VitestExecutor): Promise<void> {
1515
const workerState = getWorkerState()
1616

17-
await setupGlobalEnv(config, environment)
17+
await setupGlobalEnv(config, environment, executor)
1818
await startCoverageInsideWorker(config.coverage, executor)
1919

2020
if (config.chaiConfig)

packages/vitest/src/runtime/runVmTests.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import { setupChaiConfig } from '../integrations/chai/config'
1010
import { startCoverageInsideWorker, stopCoverageInsideWorker } from '../integrations/coverage'
1111
import type { ResolvedConfig } from '../types'
1212
import { getWorkerState } from '../utils/global'
13-
import { VitestSnapshotEnvironment } from '../integrations/snapshot/environments/node'
1413
import * as VitestIndex from '../index'
14+
import { resolveSnapshotEnvironment } from '../integrations/snapshot/environments/resolveSnapshotEnvironment'
1515
import type { VitestExecutor } from './execute'
1616
import { resolveTestRunner } from './runners'
1717
import { setupCommonEnv } from './setup-common'
@@ -27,8 +27,6 @@ export async function run(files: string[], config: ResolvedConfig, executor: Vit
2727
enumerable: false,
2828
})
2929

30-
config.snapshotOptions.snapshotEnvironment = new VitestSnapshotEnvironment(workerState.rpc)
31-
3230
setupColors(createColors(isatty(1)))
3331

3432
if (workerState.environment.transformMode === 'web') {
@@ -55,7 +53,12 @@ export async function run(files: string[], config: ResolvedConfig, executor: Vit
5553
if (config.chaiConfig)
5654
setupChaiConfig(config.chaiConfig)
5755

58-
const runner = await resolveTestRunner(config, executor)
56+
const [runner, snapshotEnvironment] = await Promise.all([
57+
resolveTestRunner(config, executor),
58+
resolveSnapshotEnvironment(config, executor),
59+
])
60+
61+
config.snapshotOptions.snapshotEnvironment = snapshotEnvironment
5962

6063
workerState.onCancel.then((reason) => {
6164
closeInspector(config)

packages/vitest/src/runtime/setup-node.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@ import { isatty } from 'node:tty'
55
import { installSourcemapsSupport } from 'vite-node/source-map'
66
import { createColors, setupColors } from '@vitest/utils'
77
import type { EnvironmentOptions, ResolvedConfig, ResolvedTestEnvironment } from '../types'
8-
import { VitestSnapshotEnvironment } from '../integrations/snapshot/environments/node'
98
import { getSafeTimers, getWorkerState } from '../utils'
109
import * as VitestIndex from '../index'
1110
import { expect } from '../integrations/chai'
11+
import { resolveSnapshotEnvironment } from '../integrations/snapshot/environments/resolveSnapshotEnvironment'
1212
import { setupCommonEnv } from './setup-common'
13+
import type { VitestExecutor } from './execute'
1314

1415
// this should only be used in Node
1516
let globalSetup = false
16-
export async function setupGlobalEnv(config: ResolvedConfig, { environment }: ResolvedTestEnvironment) {
17+
export async function setupGlobalEnv(config: ResolvedConfig, { environment }: ResolvedTestEnvironment, executor: VitestExecutor) {
1718
await setupCommonEnv(config)
1819

1920
Object.defineProperty(globalThis, '__vitest_index__', {
@@ -24,7 +25,7 @@ export async function setupGlobalEnv(config: ResolvedConfig, { environment }: Re
2425
const state = getWorkerState()
2526

2627
if (!state.config.snapshotOptions.snapshotEnvironment)
27-
state.config.snapshotOptions.snapshotEnvironment = new VitestSnapshotEnvironment(state.rpc)
28+
state.config.snapshotOptions.snapshotEnvironment = await resolveSnapshotEnvironment(config, executor)
2829

2930
if (globalSetup)
3031
return

packages/vitest/src/snapshot.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type { SnapshotEnvironment } from '@vitest/snapshot/environment'
2+
export {
3+
VitestNodeSnapshotEnvironment as VitestSnapshotEnvironment,
4+
} from './integrations/snapshot/environments/node'

packages/vitest/src/types/config.ts

+5
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,11 @@ export interface InlineConfig {
560560
*/
561561
resolveSnapshotPath?: (path: string, extension: string) => string
562562

563+
/**
564+
* Path to a custom snapshot environment module that has a defualt export of `SnapshotEnvironment` object.
565+
*/
566+
snapshotEnvironment?: string
567+
563568
/**
564569
* Pass with no tests
565570
*/

test/browser/custom-snapshot-env.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
throw new Error('This file should not be executed')

test/browser/vitest.config.mts

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export default defineConfig({
2020
},
2121
test: {
2222
include: ['test/**.test.{ts,js}'],
23+
// having a snapshot environment doesn't affect browser tests
24+
snapshotEnvironment: './custom-snapshot-env.ts',
2325
browser: {
2426
enabled: true,
2527
name: browser,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { readFileSync, rmSync, writeFileSync } from 'node:fs'
2+
import { afterEach, expect, test } from 'vitest'
3+
import { dirname, resolve } from 'pathe'
4+
import { runVitest } from '../../test-utils'
5+
6+
const testFileName = resolve(import.meta.dirname, './fixtures/custom-snapshot-environment/test/snapshots.test.ts')
7+
const snapshotFile = resolve(dirname(testFileName), './__snapshots__/snapshots.test.ts.snap')
8+
const testFile = readFileSync(testFileName, 'utf-8')
9+
10+
afterEach(() => {
11+
writeFileSync(testFileName, testFile)
12+
rmSync(snapshotFile)
13+
})
14+
15+
test('custom environment resolved correctly', async () => {
16+
const { stdout, stderr } = await runVitest({
17+
root: 'test/fixtures/custom-snapshot-environment',
18+
update: true,
19+
})
20+
21+
const snapshotLogs = stdout.split('\n').filter(i => i.startsWith('## ')).join('\n')
22+
expect(stderr).toBe('')
23+
expect(snapshotLogs).toMatchInlineSnapshot(`
24+
"## resolvePath test/fixtures/custom-snapshot-environment/test/snapshots.test.ts
25+
## readSnapshotFile test/fixtures/custom-snapshot-environment/test/__snapshots__/snapshots.test.ts.snap
26+
## getHeader
27+
## getVersion
28+
## readSnapshotFile test/fixtures/custom-snapshot-environment/test/__snapshots__/snapshots.test.ts.snap
29+
## saveSnapshotFile test/fixtures/custom-snapshot-environment/test/__snapshots__/snapshots.test.ts.snap
30+
## readSnapshotFile test/fixtures/custom-snapshot-environment/test/snapshots.test.ts
31+
## saveSnapshotFile test/fixtures/custom-snapshot-environment/test/snapshots.test.ts"
32+
`)
33+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { relative as _relative } from 'pathe'
2+
import { VitestSnapshotEnvironment } from 'vitest/snapshot'
3+
4+
function relative(file: string) {
5+
return _relative(process.cwd(), file)
6+
}
7+
8+
class CustomSnapshotEnvironment extends VitestSnapshotEnvironment {
9+
getVersion(): string {
10+
console.log('## getVersion')
11+
return super.getVersion()
12+
}
13+
14+
getHeader() {
15+
console.log('## getHeader')
16+
return super.getHeader()
17+
}
18+
19+
resolvePath(filepath: string) {
20+
console.log('## resolvePath', relative(filepath))
21+
return super.resolvePath(filepath)
22+
}
23+
24+
resolveRawPath(testPath: string, rawPath: string) {
25+
console.log('## resolveRawPath', relative(testPath), relative(rawPath))
26+
return super.resolveRawPath(testPath, rawPath)
27+
}
28+
29+
saveSnapshotFile(filepath: string, snapshot: string) {
30+
console.log('## saveSnapshotFile', relative(filepath))
31+
return super.saveSnapshotFile(filepath, snapshot)
32+
}
33+
34+
readSnapshotFile(filepath: string) {
35+
console.log('## readSnapshotFile', relative(filepath))
36+
return super.readSnapshotFile(filepath)
37+
}
38+
39+
removeSnapshotFile(filepath: string) {
40+
console.log('## removeSnapshotFile', relative(filepath))
41+
return super.removeSnapshotFile(filepath)
42+
}
43+
}
44+
45+
export default new CustomSnapshotEnvironment()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import {test, expect} from 'vitest'
2+
3+
test('regular snapshot', () => {
4+
expect({ a: 1 }).toMatchSnapshot()
5+
})
6+
7+
test('inline snapshot', () => {
8+
expect({ a: 1 }).toMatchInlineSnapshot()
9+
})
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+
snapshotEnvironment: './snapshot-environment.ts'
6+
}
7+
})

0 commit comments

Comments
 (0)