Skip to content

Commit bdc06dc

Browse files
authoredApr 3, 2023
feat(snapshot): introduce toMatchFileSnapshot and auto queuing expect promise (#3116)
1 parent 035230b commit bdc06dc

File tree

28 files changed

+325
-23
lines changed

28 files changed

+325
-23
lines changed
 

‎docs/api/expect.md

+16
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,22 @@ type Awaitable<T> = T | PromiseLike<T>
678678
})
679679
```
680680

681+
## toMatchFileSnapshot
682+
683+
- **Type:** `<T>(filepath: string, message?: string) => Promise<void>`
684+
685+
Compare or update the snapshot with the content of a file explicitly specified (instead of the `.snap` file).
686+
687+
```ts
688+
import { expect, it } from 'vitest'
689+
690+
it('render basic', async () => {
691+
const result = renderHTML(h('div', { class: 'foo' }))
692+
await expect(result).toMatchFileSnapshot('./test/basic.output.html')
693+
})
694+
```
695+
696+
Note that since file system operation is async, you need to use `await` with `toMatchFileSnapshot()`.
681697

682698
## toThrowErrorMatchingSnapshot
683699

‎docs/guide/snapshot.md

+17
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,23 @@ Or you can use the `--update` or `-u` flag in the CLI to make Vitest update snap
7979
vitest -u
8080
```
8181

82+
## File Snapshots
83+
84+
When calling `toMatchSnapshot()`, we store all snapshots in a formatted snap file. That means we need to escaping some characters (namely the double-quote `"` and backtick `\``) in the snapshot string. Meanwhile, you might lose the syntax highlighting for the snapshot content (if they are in some language).
85+
86+
To improve this case, we introduce [`toMatchFileSnapshot()`](/api/expect#tomatchfilesnapshot) to explicitly snapshot in a file. This allows you to assign any file extension to the snapshot file, and making them more readable.
87+
88+
```ts
89+
import { expect, it } from 'vitest'
90+
91+
it('render basic', async () => {
92+
const result = renderHTML(h('div', { class: 'foo' }))
93+
await expect(result).toMatchFileSnapshot('./test/basic.output.html')
94+
})
95+
```
96+
97+
It will compare with the content of `./test/basic.output.html`. And can be written back with the `--update` flag.
98+
8299
## Image Snapshots
83100

84101
It's also possible to snapshot images using [`jest-image-snapshot`](https://github.com/americanexpress/jest-image-snapshot).

‎packages/browser/src/client/snapshot.ts

+4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ export class BrowserSnapshotEnvironment implements SnapshotEnvironment {
2222
return rpc().resolveSnapshotPath(filepath)
2323
}
2424

25+
resolveRawPath(testPath: string, rawPath: string): Promise<string> {
26+
return rpc().resolveSnapshotRawPath(testPath, rawPath)
27+
}
28+
2529
removeSnapshotFile(filepath: string): Promise<void> {
2630
return rpc().removeFile(filepath)
2731
}

‎packages/expect/src/jest-expect.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { arrayBufferEquality, generateToBeMessage, iterableEquality, equals as j
88
import type { AsymmetricMatcher } from './jest-asymmetric-matchers'
99
import { diff, stringify } from './jest-matcher-utils'
1010
import { JEST_MATCHERS_OBJECT } from './constants'
11+
import { recordAsyncExpect } from './utils'
1112

1213
// Jest Expect Compact
1314
export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
@@ -633,6 +634,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
633634
utils.addProperty(chai.Assertion.prototype, 'resolves', function __VITEST_RESOLVES__(this: any) {
634635
utils.flag(this, 'promise', 'resolves')
635636
utils.flag(this, 'error', new Error('resolves'))
637+
const test = utils.flag(this, 'vitest-test')
636638
const obj = utils.flag(this, 'object')
637639

638640
if (typeof obj?.then !== 'function')
@@ -646,7 +648,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
646648
return result instanceof chai.Assertion ? proxy : result
647649

648650
return async (...args: any[]) => {
649-
return obj.then(
651+
const promise = obj.then(
650652
(value: any) => {
651653
utils.flag(this, 'object', value)
652654
return result.call(this, ...args)
@@ -655,6 +657,8 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
655657
throw new Error(`promise rejected "${String(err)}" instead of resolving`)
656658
},
657659
)
660+
661+
return recordAsyncExpect(test, promise)
658662
}
659663
},
660664
})
@@ -665,6 +669,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
665669
utils.addProperty(chai.Assertion.prototype, 'rejects', function __VITEST_REJECTS__(this: any) {
666670
utils.flag(this, 'promise', 'rejects')
667671
utils.flag(this, 'error', new Error('rejects'))
672+
const test = utils.flag(this, 'vitest-test')
668673
const obj = utils.flag(this, 'object')
669674
const wrapper = typeof obj === 'function' ? obj() : obj // for jest compat
670675

@@ -679,7 +684,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
679684
return result instanceof chai.Assertion ? proxy : result
680685

681686
return async (...args: any[]) => {
682-
return wrapper.then(
687+
const promise = wrapper.then(
683688
(value: any) => {
684689
throw new Error(`promise resolved "${String(value)}" instead of rejecting`)
685690
},
@@ -688,6 +693,8 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
688693
return result.call(this, ...args)
689694
},
690695
)
696+
697+
return recordAsyncExpect(test, promise)
691698
}
692699
},
693700
})

‎packages/expect/src/utils.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export function recordAsyncExpect(test: any, promise: Promise<any>) {
2+
// record promise for test, that resolves before test ends
3+
if (test) {
4+
// if promise is explicitly awaited, remove it from the list
5+
promise = promise.finally(() => {
6+
const index = test.promises.indexOf(promise)
7+
if (index !== -1)
8+
test.promises.splice(index, 1)
9+
})
10+
11+
// record promise
12+
if (!test.promises)
13+
test.promises = []
14+
test.promises.push(promise)
15+
}
16+
17+
return promise
18+
}

‎packages/runner/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export { startTests, updateTask } from './run'
22
export { test, it, describe, suite, getCurrentSuite } from './suite'
33
export { beforeAll, beforeEach, afterAll, afterEach, onTestFailed } from './hooks'
44
export { setFn, getFn } from './map'
5+
export { getCurrentTest } from './test-state'
56
export * from './types'

‎packages/runner/src/run.ts

+17-4
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,14 @@ export async function runTest(test: Test, runner: VitestRunner) {
145145
await fn()
146146
}
147147

148+
// some async expect will be added to this array, in case user forget to await theme
149+
if (test.promises) {
150+
const result = await Promise.allSettled(test.promises)
151+
const errors = result.map(r => r.status === 'rejected' ? r.reason : undefined).filter(Boolean)
152+
if (errors.length)
153+
throw errors
154+
}
155+
148156
await runner.onAfterTryTest?.(test, retryCount)
149157

150158
test.result.state = 'pass'
@@ -197,10 +205,15 @@ export async function runTest(test: Test, runner: VitestRunner) {
197205

198206
function failTask(result: TaskResult, err: unknown, runner: VitestRunner) {
199207
result.state = 'fail'
200-
const error = processError(err, runner.config)
201-
result.error = error
202-
result.errors ??= []
203-
result.errors.push(error)
208+
const errors = Array.isArray(err)
209+
? err
210+
: [err]
211+
for (const e of errors) {
212+
const error = processError(e, runner.config)
213+
result.error ??= error
214+
result.errors ??= []
215+
result.errors.push(error)
216+
}
204217
}
205218

206219
function markTasksAsSkipped(suite: Suite, runner: VitestRunner) {

‎packages/runner/src/types/tasks.ts

+4
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ export interface Test<ExtraContext = {}> extends TaskBase {
5959
fails?: boolean
6060
context: TestContext & ExtraContext
6161
onFailed?: OnTestFailedHandler[]
62+
/**
63+
* Store promises (from async expects) to wait for them before finishing the test
64+
*/
65+
promises?: Promise<any>[]
6266
}
6367

6468
export type Task = Test | Suite | TaskCustom | File

‎packages/snapshot/src/client.ts

+30-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { deepMergeSnapshot } from './port/utils'
22
import SnapshotState from './port/state'
33
import type { SnapshotStateOptions } from './types'
4+
import type { RawSnapshotInfo } from './port/rawSnapshot'
45

56
const createMismatchError = (message: string, actual: unknown, expected: unknown) => {
67
const error = new Error(message)
@@ -35,6 +36,7 @@ interface AssertOptions {
3536
inlineSnapshot?: string
3637
error?: Error
3738
errorMessage?: string
39+
rawSnapshot?: RawSnapshotInfo
3840
}
3941

4042
export class SnapshotClient {
@@ -79,7 +81,7 @@ export class SnapshotClient {
7981
}
8082

8183
/**
82-
* Should be overriden by the consumer.
84+
* Should be overridden by the consumer.
8385
*
8486
* Vitest checks equality with @vitest/expect.
8587
*/
@@ -97,6 +99,7 @@ export class SnapshotClient {
9799
inlineSnapshot,
98100
error,
99101
errorMessage,
102+
rawSnapshot,
100103
} = options
101104
let { received } = options
102105

@@ -134,12 +137,38 @@ export class SnapshotClient {
134137
isInline,
135138
error,
136139
inlineSnapshot,
140+
rawSnapshot,
137141
})
138142

139143
if (!pass)
140144
throw createMismatchError(`Snapshot \`${key || 'unknown'}\` mismatched`, actual?.trim(), expected?.trim())
141145
}
142146

147+
async assertRaw(options: AssertOptions): Promise<void> {
148+
if (!options.rawSnapshot)
149+
throw new Error('Raw snapshot is required')
150+
151+
const {
152+
filepath = this.filepath,
153+
rawSnapshot,
154+
} = options
155+
156+
if (rawSnapshot.content == null) {
157+
if (!filepath)
158+
throw new Error('Snapshot cannot be used outside of test')
159+
160+
const snapshotState = this.getSnapshotState(filepath)
161+
162+
// save the filepath, so it don't lose even if the await make it out-of-context
163+
options.filepath ||= filepath
164+
// resolve and read the raw snapshot file
165+
rawSnapshot.file = await snapshotState.environment.resolveRawPath(filepath, rawSnapshot.file)
166+
rawSnapshot.content = await snapshotState.environment.readSnapshotFile(rawSnapshot.file) || undefined
167+
}
168+
169+
return this.assert(options)
170+
}
171+
143172
async resetCurrent() {
144173
if (!this.snapshotState)
145174
return null

‎packages/snapshot/src/env/node.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { existsSync, promises as fs } from 'node:fs'
2-
import { basename, dirname, join } from 'pathe'
2+
import { basename, dirname, isAbsolute, join, resolve } from 'pathe'
33
import type { SnapshotEnvironment } from '../types'
44

55
export class NodeSnapshotEnvironment implements SnapshotEnvironment {
@@ -11,6 +11,12 @@ export class NodeSnapshotEnvironment implements SnapshotEnvironment {
1111
return `// Snapshot v${this.getVersion()}`
1212
}
1313

14+
async resolveRawPath(testPath: string, rawPath: string) {
15+
return isAbsolute(rawPath)
16+
? rawPath
17+
: resolve(dirname(testPath), rawPath)
18+
}
19+
1420
async resolvePath(filepath: string): Promise<string> {
1521
return join(
1622
join(

‎packages/snapshot/src/manager.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { basename, dirname, join } from 'pathe'
1+
import { basename, dirname, isAbsolute, join, resolve } from 'pathe'
22
import type { SnapshotResult, SnapshotStateOptions, SnapshotSummary } from './types'
33

44
export class SnapshotManager {
@@ -28,6 +28,12 @@ export class SnapshotManager {
2828

2929
return resolver(testPath, this.extension)
3030
}
31+
32+
resolveRawPath(testPath: string, rawPath: string) {
33+
return isAbsolute(rawPath)
34+
? rawPath
35+
: resolve(dirname(testPath), rawPath)
36+
}
3137
}
3238

3339
export function emptySummary(options: Omit<SnapshotStateOptions, 'snapshotEnvironment'>): SnapshotSummary {
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { SnapshotEnvironment } from '../types'
2+
3+
export interface RawSnapshotInfo {
4+
file: string
5+
readonly?: boolean
6+
content?: string
7+
}
8+
9+
export interface RawSnapshot extends RawSnapshotInfo {
10+
snapshot: string
11+
file: string
12+
}
13+
14+
export async function saveRawSnapshots(
15+
environment: SnapshotEnvironment,
16+
snapshots: Array<RawSnapshot>,
17+
) {
18+
await Promise.all(snapshots.map(async (snap) => {
19+
if (!snap.readonly)
20+
await environment.saveSnapshotFile(snap.file, snap.snapshot)
21+
}))
22+
}

‎packages/snapshot/src/port/state.ts

+36-8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import type { OptionsReceived as PrettyFormatOptions } from 'pretty-format'
1111
import type { SnapshotData, SnapshotEnvironment, SnapshotMatchOptions, SnapshotResult, SnapshotStateOptions, SnapshotUpdateState } from '../types'
1212
import type { InlineSnapshot } from './inlineSnapshot'
1313
import { saveInlineSnapshots } from './inlineSnapshot'
14+
import type { RawSnapshot, RawSnapshotInfo } from './rawSnapshot'
15+
import { saveRawSnapshots } from './rawSnapshot'
1416

1517
import {
1618
addExtraLineBreaks,
@@ -43,6 +45,7 @@ export default class SnapshotState {
4345
private _snapshotData: SnapshotData
4446
private _initialData: SnapshotData
4547
private _inlineSnapshots: Array<InlineSnapshot>
48+
private _rawSnapshots: Array<RawSnapshot>
4649
private _uncheckedKeys: Set<string>
4750
private _snapshotFormat: PrettyFormatOptions
4851
private _environment: SnapshotEnvironment
@@ -69,6 +72,7 @@ export default class SnapshotState {
6972
this._snapshotData = data
7073
this._dirty = dirty
7174
this._inlineSnapshots = []
75+
this._rawSnapshots = []
7276
this._uncheckedKeys = new Set(Object.keys(this._snapshotData))
7377
this._counters = new Map()
7478
this.expand = options.expand || false
@@ -93,6 +97,10 @@ export default class SnapshotState {
9397
return new SnapshotState(testFilePath, snapshotPath, content, options)
9498
}
9599

100+
get environment() {
101+
return this._environment
102+
}
103+
96104
markSnapshotsAsCheckedForTest(testName: string): void {
97105
this._uncheckedKeys.forEach((uncheckedKey) => {
98106
if (keyToTestName(uncheckedKey) === testName)
@@ -115,7 +123,7 @@ export default class SnapshotState {
115123
private _addSnapshot(
116124
key: string,
117125
receivedSerialized: string,
118-
options: { isInline: boolean; error?: Error },
126+
options: { isInline: boolean; rawSnapshot?: RawSnapshotInfo; error?: Error },
119127
): void {
120128
this._dirty = true
121129
if (options.isInline) {
@@ -135,6 +143,12 @@ export default class SnapshotState {
135143
...stack,
136144
})
137145
}
146+
else if (options.rawSnapshot) {
147+
this._rawSnapshots.push({
148+
...options.rawSnapshot,
149+
snapshot: receivedSerialized,
150+
})
151+
}
138152
else {
139153
this._snapshotData[key] = receivedSerialized
140154
}
@@ -154,7 +168,8 @@ export default class SnapshotState {
154168
async save(): Promise<SaveStatus> {
155169
const hasExternalSnapshots = Object.keys(this._snapshotData).length
156170
const hasInlineSnapshots = this._inlineSnapshots.length
157-
const isEmpty = !hasExternalSnapshots && !hasInlineSnapshots
171+
const hasRawSnapshots = this._rawSnapshots.length
172+
const isEmpty = !hasExternalSnapshots && !hasInlineSnapshots && !hasRawSnapshots
158173

159174
const status: SaveStatus = {
160175
deleted: false,
@@ -168,6 +183,8 @@ export default class SnapshotState {
168183
}
169184
if (hasInlineSnapshots)
170185
await saveInlineSnapshots(this._environment, this._inlineSnapshots)
186+
if (hasRawSnapshots)
187+
await saveRawSnapshots(this._environment, this._rawSnapshots)
171188

172189
status.saved = true
173190
}
@@ -206,6 +223,7 @@ export default class SnapshotState {
206223
inlineSnapshot,
207224
isInline,
208225
error,
226+
rawSnapshot,
209227
}: SnapshotMatchOptions): SnapshotReturnOptions {
210228
this._counters.set(testName, (this._counters.get(testName) || 0) + 1)
211229
const count = Number(this._counters.get(testName))
@@ -219,14 +237,24 @@ export default class SnapshotState {
219237
if (!(isInline && this._snapshotData[key] !== undefined))
220238
this._uncheckedKeys.delete(key)
221239

222-
const receivedSerialized = addExtraLineBreaks(serialize(received, undefined, this._snapshotFormat))
223-
const expected = isInline ? inlineSnapshot : this._snapshotData[key]
240+
let receivedSerialized = rawSnapshot && typeof received === 'string'
241+
? received as string
242+
: serialize(received, undefined, this._snapshotFormat)
243+
244+
if (!rawSnapshot)
245+
receivedSerialized = addExtraLineBreaks(receivedSerialized)
246+
247+
const expected = isInline
248+
? inlineSnapshot
249+
: rawSnapshot
250+
? rawSnapshot.content
251+
: this._snapshotData[key]
224252
const expectedTrimmed = prepareExpected(expected)
225253
const pass = expectedTrimmed === prepareExpected(receivedSerialized)
226254
const hasSnapshot = expected !== undefined
227-
const snapshotIsPersisted = isInline || this._fileExists
255+
const snapshotIsPersisted = isInline || this._fileExists || (rawSnapshot && rawSnapshot.content != null)
228256

229-
if (pass && !isInline) {
257+
if (pass && !isInline && !rawSnapshot) {
230258
// Executing a snapshot file as JavaScript and writing the strings back
231259
// when other snapshots have changed loses the proper escaping for some
232260
// characters. Since we check every snapshot in every test, use the newly
@@ -255,14 +283,14 @@ export default class SnapshotState {
255283
else
256284
this.added++
257285

258-
this._addSnapshot(key, receivedSerialized, { error, isInline })
286+
this._addSnapshot(key, receivedSerialized, { error, isInline, rawSnapshot })
259287
}
260288
else {
261289
this.matched++
262290
}
263291
}
264292
else {
265-
this._addSnapshot(key, receivedSerialized, { error, isInline })
293+
this._addSnapshot(key, receivedSerialized, { error, isInline, rawSnapshot })
266294
this.added++
267295
}
268296

‎packages/snapshot/src/port/utils.ts

+18
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,24 @@ export async function saveSnapshotFile(
163163
)
164164
}
165165

166+
export async function saveSnapshotFileRaw(
167+
environment: SnapshotEnvironment,
168+
content: string,
169+
snapshotPath: string,
170+
) {
171+
const oldContent = await environment.readSnapshotFile(snapshotPath)
172+
const skipWriting = oldContent && oldContent === content
173+
174+
if (skipWriting)
175+
return
176+
177+
await ensureDirectoryExists(environment, snapshotPath)
178+
await environment.saveSnapshotFile(
179+
snapshotPath,
180+
content,
181+
)
182+
}
183+
166184
export function prepareExpected(expected?: string) {
167185
function findStartIndent() {
168186
// Attempts to find indentation for objects.

‎packages/snapshot/src/types/environment.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export interface SnapshotEnvironment {
22
getVersion(): string
33
getHeader(): string
44
resolvePath(filepath: string): Promise<string>
5+
resolveRawPath(testPath: string, rawPath: string): Promise<string>
56
prepareDirectory(filepath: string): Promise<void>
67
saveSnapshotFile(filepath: string, snapshot: string): Promise<void>
78
readSnapshotFile(filepath: string): Promise<string | null>

‎packages/snapshot/src/types/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { OptionsReceived as PrettyFormatOptions } from 'pretty-format'
2+
import type { RawSnapshotInfo } from '../port/rawSnapshot'
23
import type { SnapshotEnvironment } from './environment'
34

45
export type { SnapshotEnvironment }
@@ -21,6 +22,7 @@ export interface SnapshotMatchOptions {
2122
inlineSnapshot?: string
2223
isInline: boolean
2324
error?: Error
25+
rawSnapshot?: RawSnapshotInfo
2426
}
2527

2628
export interface SnapshotResult {

‎packages/vitest/src/api/setup.ts

+3
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ export function setup(ctx: Vitest, server?: ViteDevServer) {
6161
resolveSnapshotPath(testPath) {
6262
return ctx.snapshot.resolvePath(testPath)
6363
},
64+
resolveSnapshotRawPath(testPath, rawPath) {
65+
return ctx.snapshot.resolveRawPath(testPath, rawPath)
66+
},
6467
removeFile(id) {
6568
return fs.unlink(id)
6669
},

‎packages/vitest/src/api/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface WebSocketHandlers {
1515
getPaths(): string[]
1616
getConfig(): ResolvedConfig
1717
resolveSnapshotPath(testPath: string): string
18+
resolveSnapshotRawPath(testPath: string, rawPath: string): string
1819
getModuleGraph(id: string): Promise<ModuleGraphData>
1920
getTransformResult(id: string): Promise<TransformResultWithSource | undefined>
2021
readFile(id: string): Promise<string | null>

‎packages/vitest/src/integrations/chai/index.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as chai from 'chai'
22
import './setup'
33
import type { Test } from '@vitest/runner'
4+
import { getCurrentTest } from '@vitest/runner'
45
import { GLOBAL_EXPECT, getState, setState } from '@vitest/expect'
56
import type { MatcherState } from '../../types/chai'
67
import { getCurrentEnvironment, getFullName } from '../../utils'
@@ -10,9 +11,10 @@ export function createExpect(test?: Test) {
1011
const { assertionCalls } = getState(expect)
1112
setState({ assertionCalls: assertionCalls + 1 }, expect)
1213
const assert = chai.expect(value, message) as unknown as Vi.Assertion
13-
if (test)
14+
const _test = test || getCurrentTest()
15+
if (_test)
1416
// @ts-expect-error internal
15-
return assert.withTest(test) as Vi.Assertion
17+
return assert.withTest(_test) as Vi.Assertion
1618
else
1719
return assert
1820
}) as Vi.ExpectStatic

‎packages/vitest/src/integrations/snapshot/chai.ts

+25
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Test } from '@vitest/runner'
33
import { getNames } from '@vitest/runner/utils'
44
import type { SnapshotClient } from '@vitest/snapshot'
55
import { addSerializer, stripSnapshotIndentation } from '@vitest/snapshot'
6+
import { recordAsyncExpect } from '../../../../expect/src/utils'
67
import { VitestSnapshotClient } from './client'
78

89
let _client: SnapshotClient
@@ -72,6 +73,30 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => {
7273
},
7374
)
7475
}
76+
77+
utils.addMethod(
78+
chai.Assertion.prototype,
79+
'toMatchFileSnapshot',
80+
function (this: Record<string, unknown>, file: string, message?: string) {
81+
const expected = utils.flag(this, 'object')
82+
const test = utils.flag(this, 'vitest-test') as Test
83+
const errorMessage = utils.flag(this, 'message')
84+
85+
const promise = getSnapshotClient().assertRaw({
86+
received: expected,
87+
message,
88+
isInline: false,
89+
rawSnapshot: {
90+
file,
91+
},
92+
errorMessage,
93+
...getTestNames(test),
94+
})
95+
96+
return recordAsyncExpect(test, promise)
97+
},
98+
)
99+
75100
utils.addMethod(
76101
chai.Assertion.prototype,
77102
'toMatchInlineSnapshot',

‎packages/vitest/src/types/global.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,13 @@ declare global {
6666

6767
interface JestAssertion<T = any> extends jest.Matchers<void, T> {
6868
// Snapshot
69-
toMatchSnapshot<U extends { [P in keyof T]: any }>(snapshot: Partial<U>, message?: string): void
70-
toMatchSnapshot(message?: string): void
7169
matchSnapshot<U extends { [P in keyof T]: any }>(snapshot: Partial<U>, message?: string): void
7270
matchSnapshot(message?: string): void
71+
toMatchSnapshot<U extends { [P in keyof T]: any }>(snapshot: Partial<U>, message?: string): void
72+
toMatchSnapshot(message?: string): void
7373
toMatchInlineSnapshot<U extends { [P in keyof T]: any }>(properties: Partial<U>, snapshot?: string, message?: string): void
7474
toMatchInlineSnapshot(snapshot?: string, message?: string): void
75+
toMatchFileSnapshot(filepath: string, message?: string): Promise<void>
7576
toThrowErrorMatchingSnapshot(message?: string): void
7677
toThrowErrorMatchingInlineSnapshot(snapshot?: string, message?: string): void
7778

‎test/core/test/jest-expect.test.ts

+27-2
Original file line numberDiff line numberDiff line change
@@ -544,15 +544,15 @@ describe('async expect', () => {
544544
})()).resolves.not.toThrow(Error)
545545
})
546546

547-
it('resolves trows chai', async () => {
547+
it('resolves throws chai', async () => {
548548
const assertion = async () => {
549549
await expect((async () => new Error('msg'))()).resolves.toThrow()
550550
}
551551

552552
await expect(assertion).rejects.toThrowError('expected promise to throw an error, but it didn\'t')
553553
})
554554

555-
it('resolves trows jest', async () => {
555+
it('resolves throws jest', async () => {
556556
const assertion = async () => {
557557
await expect((async () => new Error('msg'))()).resolves.toThrow(Error)
558558
}
@@ -679,6 +679,31 @@ describe('async expect', () => {
679679
expect(error).toEqual(toEqualError2)
680680
}
681681
})
682+
683+
describe('promise auto queuing', () => {
684+
it.fails('fails', () => {
685+
expect(() => new Promise((resolve, reject) => setTimeout(reject, 500)))
686+
.resolves
687+
.toBe('true')
688+
})
689+
690+
let value = 0
691+
692+
it('pass first', () => {
693+
expect((async () => {
694+
await new Promise(resolve => setTimeout(resolve, 500))
695+
value += 1
696+
return value
697+
})())
698+
.resolves
699+
.toBe(1)
700+
})
701+
702+
it('pass second', () => {
703+
// even if 'pass first' is sync, we will still wait the expect to resolve
704+
expect(value).toBe(1)
705+
})
706+
})
682707
})
683708

684709
it('compatible with jest', () => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[
2+
[
3+
".name",
4+
{
5+
"color": "red"
6+
}
7+
]
8+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.name {
2+
color: red;
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[
2+
[
3+
".text-red",
4+
{
5+
"color": "red"
6+
}
7+
],
8+
[
9+
".text-lg",
10+
{
11+
"font-size": "1.25rem",
12+
"line-height": "1.75rem"
13+
}
14+
]
15+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.text-red {
2+
color: red;
3+
}
4+
.text-lg {
5+
font-size: 1.25rem;
6+
line-height: 1.75rem;
7+
}
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { expect, test } from 'vitest'
2+
3+
function objectToCSS(selector: string, obj: Record<string, string>) {
4+
const body = Object.entries(obj)
5+
.map(([key, value]) => ` ${key}: ${value};`)
6+
.join('\n')
7+
return `${selector} {\n${body}\n}`
8+
}
9+
10+
describe('snapshots', () => {
11+
const files = import.meta.glob('./fixtures/**/input.json', { as: 'raw' })
12+
13+
for (const [path, file] of Object.entries(files)) {
14+
test(path, async () => {
15+
const entries = JSON.parse(await file()) as any[]
16+
expect(entries.map(i => objectToCSS(i[0], i[1])).join('\n'))
17+
.toMatchFileSnapshot(path.replace('input.json', 'output.css'))
18+
})
19+
}
20+
})

0 commit comments

Comments
 (0)
Please sign in to comment.