Skip to content

Commit 6aff017

Browse files
authored
feat!: move snapshot implementation into @vitest/snapshot (#3032)
1 parent 446308d commit 6aff017

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1197
-1082
lines changed

packages/browser/src/client/main.ts

+5-9
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import { createClient } from '@vitest/ws-client'
33
import type { ResolvedConfig } from 'vitest'
44
import type { VitestRunner } from '@vitest/runner'
55
import { createBrowserRunner } from './runner'
6-
import { BrowserSnapshotEnvironment } from './snapshot'
76
import { importId } from './utils'
87
import { setupConsoleLogSpy } from './logger'
98
import { createSafeRpc, rpc, rpcDone } from './rpc'
109
import { setupDialogsSpy } from './dialog'
10+
import { BrowserSnapshotEnvironment } from './snapshot'
1111

1212
// @ts-expect-error mocking some node apis
1313
globalThis.process = { env: {}, argv: [], cwd: () => '/', stdout: { write: () => {} }, nextTick: cb => cb() }
@@ -75,19 +75,17 @@ ws.addEventListener('open', async () => {
7575

7676
await setupConsoleLogSpy()
7777
setupDialogsSpy()
78-
await runTests(paths, config)
78+
await runTests(paths, config!)
7979
})
8080

81-
let hasSnapshot = false
82-
async function runTests(paths: string[], config: any) {
81+
async function runTests(paths: string[], config: ResolvedConfig) {
8382
// need to import it before any other import, otherwise Vite optimizer will hang
8483
const viteClientPath = '/@vite/client'
8584
await import(viteClientPath)
8685

8786
const {
8887
startTests,
8988
setupCommonEnv,
90-
setupSnapshotEnvironment,
9189
takeCoverageInsideWorker,
9290
} = await importId('vitest/browser') as typeof import('vitest/browser')
9391

@@ -101,10 +99,8 @@ async function runTests(paths: string[], config: any) {
10199
runner = new BrowserRunner({ config, browserHashMap })
102100
}
103101

104-
if (!hasSnapshot) {
105-
setupSnapshotEnvironment(new BrowserSnapshotEnvironment())
106-
hasSnapshot = true
107-
}
102+
if (!config.snapshotOptions.snapshotEnvironment)
103+
config.snapshotOptions.snapshotEnvironment = new BrowserSnapshotEnvironment()
108104

109105
try {
110106
await setupCommonEnv(config)

packages/browser/src/client/snapshot.ts

+8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@ import { rpc } from './rpc'
22
import type { SnapshotEnvironment } from '#types'
33

44
export class BrowserSnapshotEnvironment implements SnapshotEnvironment {
5+
getVersion(): string {
6+
return '1'
7+
}
8+
9+
getHeader(): string {
10+
return `// Vitest Snapshot v${this.getVersion()}, https://vitest.dev/guide/snapshot.html`
11+
}
12+
513
readSnapshotFile(filepath: string): Promise<string | null> {
614
return rpc().readFile(filepath)
715
}

packages/runner/src/utils/error.ts

+1-21
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,7 @@ import { deepClone, format, getOwnProperties, getType, stringify } from '@vitest
22
import type { DiffOptions } from '@vitest/utils/diff'
33
import { unifiedDiff } from '@vitest/utils/diff'
44

5-
export interface ParsedStack {
6-
method: string
7-
file: string
8-
line: number
9-
column: number
10-
}
11-
12-
export interface ErrorWithDiff extends Error {
13-
name: string
14-
nameStr?: string
15-
stack?: string
16-
stackStr?: string
17-
stacks?: ParsedStack[]
18-
showDiff?: boolean
19-
diff?: string
20-
actual?: any
21-
expected?: any
22-
operator?: string
23-
type?: string
24-
frame?: string
25-
}
5+
export type { ParsedStack, ErrorWithDiff } from '@vitest/utils'
266

277
const IS_RECORD_SYMBOL = '@@__IMMUTABLE_RECORD__@@'
288
const IS_COLLECTION_SYMBOL = '@@__IMMUTABLE_ITERABLE__@@'

packages/snapshot/README.md

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# @vitest/snapshot
2+
3+
Lightweight implementation of Jest's snapshots.
4+
5+
## Usage
6+
7+
```js
8+
import { SnapshotClient } from '@vitest/snapshot'
9+
import { NodeSnapshotEnvironment } from '@vitest/snapshot/environment'
10+
import { SnapshotManager } from '@vitest/snapshot/manager'
11+
12+
export class CustomSnapshotClient extends SnapshotClient {
13+
// by default, @vitest/snapshot checks equality with `!==`
14+
// you need to provide your own equality check implementation
15+
// this function is called when `.toMatchSnapshot({ property: 1 })` is called
16+
equalityCheck(received, expected) {
17+
return equals(received, expected, [iterableEquality, subsetEquality])
18+
}
19+
}
20+
21+
const client = new CustomSnapshotClient()
22+
// class that implements snapshot saving and reading
23+
// by default uses fs module, but you can provide your own implementation depending on the environment
24+
const environment = new NodeSnapshotEnvironment()
25+
26+
const getCurrentFilepath = () => '/file.spec.ts'
27+
const getCurrentTestName = () => 'test1'
28+
29+
const wrapper = (received) => {
30+
function __INLINE_SNAPSHOT__(inlineSnapshot, message) {
31+
client.assert({
32+
received,
33+
message,
34+
isInline: true,
35+
inlineSnapshot,
36+
// you need to implement this yourselves,
37+
// this depends on your runner
38+
filepath: getCurrentFilepath(),
39+
name: getCurrentTestName(),
40+
})
41+
}
42+
return {
43+
// the name is hard-coded, it should be inside another function, so Vitest can find the actual test file where it was called (parses call stack trace + 2)
44+
// you can override this behaviour in SnapshotState's `_inferInlineSnapshotStack` method by providing your own SnapshotState to SnapshotClient constructor
45+
toMatchInlineSnapshot: (...args) => __INLINE_SNAPSHOT__(...args),
46+
}
47+
}
48+
49+
const options = {
50+
updateSnapshot: 'new',
51+
snapshotEnvironment: environment,
52+
}
53+
54+
await client.setTest(getCurrentFilepath(), getCurrentTestName(), options)
55+
56+
// uses "pretty-format", so it requires quotes
57+
// also naming is hard-coded when parsing test files
58+
wrapper('text 1').toMatchInlineSnapshot()
59+
wrapper('text 2').toMatchInlineSnapshot('"text 2"')
60+
61+
const result = await client.resetCurrent() // this saves files and returns SnapshotResult
62+
63+
// you can use manager to manage several clients
64+
const manager = new SnapshotManager(options)
65+
manager.add(result)
66+
67+
// do something
68+
// and then read the summary
69+
70+
console.log(manager.summary)
71+
```

packages/snapshot/package.json

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"name": "@vitest/snapshot",
3+
"type": "module",
4+
"version": "0.29.3",
5+
"description": "Vitest Snapshot Resolver",
6+
"license": "MIT",
7+
"repository": {
8+
"type": "git",
9+
"url": "git+https://github.com/vitest-dev/vitest.git",
10+
"directory": "packages/snapshot"
11+
},
12+
"sideEffects": false,
13+
"exports": {
14+
".": {
15+
"types": "./dist/index.d.ts",
16+
"import": "./dist/index.js"
17+
},
18+
"./environment": {
19+
"types": "./dist/environment.d.ts",
20+
"import": "./dist/environment.js"
21+
},
22+
"./manager": {
23+
"types": "./dist/manager.d.ts",
24+
"import": "./dist/manager.js"
25+
},
26+
"./*": "./*"
27+
},
28+
"main": "./dist/index.js",
29+
"module": "./dist/index.js",
30+
"types": "./dist/index.d.ts",
31+
"files": [
32+
"dist",
33+
"*.d.ts"
34+
],
35+
"scripts": {
36+
"build": "rimraf dist && rollup -c",
37+
"dev": "rollup -c --watch",
38+
"prepublishOnly": "pnpm build"
39+
},
40+
"dependencies": {
41+
"magic-string": "^0.27.0",
42+
"pathe": "^1.1.0",
43+
"pretty-format": "^27.5.1"
44+
},
45+
"devDependencies": {
46+
"@types/natural-compare": "^1.4.1",
47+
"@vitest/utils": "workspace:*",
48+
"natural-compare": "^1.4.0"
49+
}
50+
}

packages/snapshot/rollup.config.js

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { builtinModules } from 'module'
2+
import esbuild from 'rollup-plugin-esbuild'
3+
import nodeResolve from '@rollup/plugin-node-resolve'
4+
import dts from 'rollup-plugin-dts'
5+
import commonjs from '@rollup/plugin-commonjs'
6+
import { defineConfig } from 'rollup'
7+
import pkg from './package.json'
8+
9+
const external = [
10+
...builtinModules,
11+
...Object.keys(pkg.dependencies || {}),
12+
...Object.keys(pkg.peerDependencies || {}),
13+
]
14+
15+
const entries = {
16+
index: 'src/index.ts',
17+
environment: 'src/environment.ts',
18+
manager: 'src/manager.ts',
19+
}
20+
21+
const plugins = [
22+
nodeResolve({
23+
preferBuiltins: true,
24+
}),
25+
commonjs(),
26+
esbuild({
27+
target: 'node14',
28+
}),
29+
]
30+
31+
export default defineConfig([
32+
{
33+
input: entries,
34+
output: {
35+
dir: 'dist',
36+
format: 'esm',
37+
entryFileNames: '[name].js',
38+
chunkFileNames: 'chunk-[name].js',
39+
},
40+
external,
41+
plugins,
42+
onwarn,
43+
},
44+
{
45+
input: entries,
46+
output: {
47+
dir: 'dist',
48+
entryFileNames: '[name].d.ts',
49+
format: 'esm',
50+
},
51+
external,
52+
plugins: [
53+
dts({ respectExternal: true }),
54+
],
55+
onwarn,
56+
},
57+
])
58+
59+
function onwarn(message) {
60+
if (['EMPTY_BUNDLE', 'CIRCULAR_DEPENDENCY'].includes(message.code))
61+
return
62+
console.error(message)
63+
}

0 commit comments

Comments
 (0)