Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: move snapshot implementation into @vitest/snapshot #3032

Merged
merged 15 commits into from
Mar 29, 2023
Merged
14 changes: 5 additions & 9 deletions packages/browser/src/client/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { createClient } from '@vitest/ws-client'
import type { ResolvedConfig } from 'vitest'
import type { VitestRunner } from '@vitest/runner'
import { createBrowserRunner } from './runner'
import { BrowserSnapshotEnvironment } from './snapshot'
import { importId } from './utils'
import { setupConsoleLogSpy } from './logger'
import { createSafeRpc, rpc, rpcDone } from './rpc'
import { setupDialogsSpy } from './dialog'
import { BrowserSnapshotEnvironment } from './snapshot'

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

await setupConsoleLogSpy()
setupDialogsSpy()
await runTests(paths, config)
await runTests(paths, config!)
})

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

const {
startTests,
setupCommonEnv,
setupSnapshotEnvironment,
takeCoverageInsideWorker,
} = await importId('vitest/browser') as typeof import('vitest/browser')

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

if (!hasSnapshot) {
setupSnapshotEnvironment(new BrowserSnapshotEnvironment())
hasSnapshot = true
}
if (!config.snapshotOptions.snapshotEnvironment)
config.snapshotOptions.snapshotEnvironment = new BrowserSnapshotEnvironment()

try {
await setupCommonEnv(config)
Expand Down
8 changes: 8 additions & 0 deletions packages/browser/src/client/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import { rpc } from './rpc'
import type { SnapshotEnvironment } from '#types'

export class BrowserSnapshotEnvironment implements SnapshotEnvironment {
getVersion(): string {
return '1'
}

getHeader(): string {
return `// Vitest Snapshot v${this.getVersion()}, https://vitest.dev/guide/snapshot.html`
}

readSnapshotFile(filepath: string): Promise<string | null> {
return rpc().readFile(filepath)
}
Expand Down
22 changes: 1 addition & 21 deletions packages/runner/src/utils/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,7 @@ import { deepClone, format, getOwnProperties, getType, stringify } from '@vitest
import type { DiffOptions } from '@vitest/utils/diff'
import { unifiedDiff } from '@vitest/utils/diff'

export interface ParsedStack {
method: string
file: string
line: number
column: number
}

export interface ErrorWithDiff extends Error {
name: string
nameStr?: string
stack?: string
stackStr?: string
stacks?: ParsedStack[]
showDiff?: boolean
diff?: string
actual?: any
expected?: any
operator?: string
type?: string
frame?: string
}
export type { ParsedStack, ErrorWithDiff } from '@vitest/utils'

const IS_RECORD_SYMBOL = '@@__IMMUTABLE_RECORD__@@'
const IS_COLLECTION_SYMBOL = '@@__IMMUTABLE_ITERABLE__@@'
Expand Down
71 changes: 71 additions & 0 deletions packages/snapshot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# @vitest/snapshot

Lightweight implementation of Jest's snapshots.

## Usage

```js
import { SnapshotClient } from '@vitest/snapshot'
import { NodeSnapshotEnvironment } from '@vitest/snapshot/environment'
import { SnapshotManager } from '@vitest/snapshot/manager'

export class CustomSnapshotClient extends SnapshotClient {
// by default, @vitest/snapshot checks equality with `!==`
// you need to provide your own equality check implementation
// this function is called when `.toMatchSnapshot({ property: 1 })` is called
equalityCheck(received, expected) {
return equals(received, expected, [iterableEquality, subsetEquality])
}
}

const client = new CustomSnapshotClient()
// class that implements snapshot saving and reading
// by default uses fs module, but you can provide your own implementation depending on the environment
const environment = new NodeSnapshotEnvironment()

const getCurrentFilepath = () => '/file.spec.ts'
const getCurrentTestName = () => 'test1'

const wrapper = (received) => {
function __INLINE_SNAPSHOT__(inlineSnapshot, message) {
client.assert({
received,
message,
isInline: true,
inlineSnapshot,
// you need to implement this yourselves,
// this depends on your runner
filepath: getCurrentFilepath(),
name: getCurrentTestName(),
})
}
return {
// 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)
// you can override this behaviour in SnapshotState's `_inferInlineSnapshotStack` method by providing your own SnapshotState to SnapshotClient constructor
toMatchInlineSnapshot: (...args) => __INLINE_SNAPSHOT__(...args),
}
}

const options = {
updateSnapshot: 'new',
snapshotEnvironment: environment,
}

await client.setTest(getCurrentFilepath(), getCurrentTestName(), options)

// uses "pretty-format", so it requires quotes
// also naming is hard-coded when parsing test files
wrapper('text 1').toMatchInlineSnapshot()
wrapper('text 2').toMatchInlineSnapshot('"text 2"')

const result = await client.resetCurrent() // this saves files and returns SnapshotResult

// you can use manager to manage several clients
const manager = new SnapshotManager(options)
manager.add(result)

// do something
// and then read the summary

console.log(manager.summary)
```
50 changes: 50 additions & 0 deletions packages/snapshot/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "@vitest/snapshot",
"type": "module",
"version": "0.29.3",
"description": "Vitest Snapshot Resolver",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/vitest-dev/vitest.git",
"directory": "packages/snapshot"
},
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./environment": {
"types": "./dist/environment.d.ts",
"import": "./dist/environment.js"
},
"./manager": {
"types": "./dist/manager.d.ts",
"import": "./dist/manager.js"
},
"./*": "./*"
},
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist",
"*.d.ts"
],
"scripts": {
"build": "rimraf dist && rollup -c",
"dev": "rollup -c --watch",
"prepublishOnly": "pnpm build"
},
"dependencies": {
"magic-string": "^0.27.0",
"pathe": "^1.1.0",
"pretty-format": "^27.5.1"
},
"devDependencies": {
"@types/natural-compare": "^1.4.1",
"@vitest/utils": "workspace:*",
"natural-compare": "^1.4.0"
}
}
63 changes: 63 additions & 0 deletions packages/snapshot/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { builtinModules } from 'module'
import esbuild from 'rollup-plugin-esbuild'
import nodeResolve from '@rollup/plugin-node-resolve'
import dts from 'rollup-plugin-dts'
import commonjs from '@rollup/plugin-commonjs'
import { defineConfig } from 'rollup'
import pkg from './package.json'

const external = [
...builtinModules,
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
]

const entries = {
index: 'src/index.ts',
environment: 'src/environment.ts',
manager: 'src/manager.ts',
}

const plugins = [
nodeResolve({
preferBuiltins: true,
}),
commonjs(),
esbuild({
target: 'node14',
}),
]

export default defineConfig([
{
input: entries,
output: {
dir: 'dist',
format: 'esm',
entryFileNames: '[name].js',
chunkFileNames: 'chunk-[name].js',
},
external,
plugins,
onwarn,
},
{
input: entries,
output: {
dir: 'dist',
entryFileNames: '[name].d.ts',
format: 'esm',
},
external,
plugins: [
dts({ respectExternal: true }),
],
onwarn,
},
])

function onwarn(message) {
if (['EMPTY_BUNDLE', 'CIRCULAR_DEPENDENCY'].includes(message.code))
return
console.error(message)
}