[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat(snapshot): introduce `toMatchFileSnapshot` and auto queuing expect promise (#3116)

authored by

Anthony Fu and committed by
GitHub
(Apr 3, 2023, 4:03 PM +0200) bdc06dcb 035230b4

+338 -36
+16
docs/api/expect.md
··· 678 678 }) 679 679 ``` 680 680 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()`. 681 697 682 698 ## toThrowErrorMatchingSnapshot 683 699
+17
docs/guide/snapshot.md
··· 79 79 vitest -u 80 80 ``` 81 81 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 + 82 99 ## Image Snapshots 83 100 84 101 It's also possible to snapshot images using [`jest-image-snapshot`](https://github.com/americanexpress/jest-image-snapshot).
+9 -2
packages/expect/src/jest-expect.ts
··· 8 8 import type { AsymmetricMatcher } from './jest-asymmetric-matchers' 9 9 import { diff, stringify } from './jest-matcher-utils' 10 10 import { JEST_MATCHERS_OBJECT } from './constants' 11 + import { recordAsyncExpect } from './utils' 11 12 12 13 // Jest Expect Compact 13 14 export const JestChaiExpect: ChaiPlugin = (chai, utils) => { ··· 633 634 utils.addProperty(chai.Assertion.prototype, 'resolves', function __VITEST_RESOLVES__(this: any) { 634 635 utils.flag(this, 'promise', 'resolves') 635 636 utils.flag(this, 'error', new Error('resolves')) 637 + const test = utils.flag(this, 'vitest-test') 636 638 const obj = utils.flag(this, 'object') 637 639 638 640 if (typeof obj?.then !== 'function') ··· 646 648 return result instanceof chai.Assertion ? proxy : result 647 649 648 650 return async (...args: any[]) => { 649 - return obj.then( 651 + const promise = obj.then( 650 652 (value: any) => { 651 653 utils.flag(this, 'object', value) 652 654 return result.call(this, ...args) ··· 655 657 throw new Error(`promise rejected "${String(err)}" instead of resolving`) 656 658 }, 657 659 ) 660 + 661 + return recordAsyncExpect(test, promise) 658 662 } 659 663 }, 660 664 }) ··· 665 669 utils.addProperty(chai.Assertion.prototype, 'rejects', function __VITEST_REJECTS__(this: any) { 666 670 utils.flag(this, 'promise', 'rejects') 667 671 utils.flag(this, 'error', new Error('rejects')) 672 + const test = utils.flag(this, 'vitest-test') 668 673 const obj = utils.flag(this, 'object') 669 674 const wrapper = typeof obj === 'function' ? obj() : obj // for jest compat 670 675 ··· 679 684 return result instanceof chai.Assertion ? proxy : result 680 685 681 686 return async (...args: any[]) => { 682 - return wrapper.then( 687 + const promise = wrapper.then( 683 688 (value: any) => { 684 689 throw new Error(`promise resolved "${String(value)}" instead of rejecting`) 685 690 }, ··· 688 693 return result.call(this, ...args) 689 694 }, 690 695 ) 696 + 697 + return recordAsyncExpect(test, promise) 691 698 } 692 699 }, 693 700 })
+18
packages/expect/src/utils.ts
··· 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 + }
+1
packages/runner/src/index.ts
··· 2 2 export { test, it, describe, suite, getCurrentSuite } from './suite' 3 3 export { beforeAll, beforeEach, afterAll, afterEach, onTestFailed } from './hooks' 4 4 export { setFn, getFn } from './map' 5 + export { getCurrentTest } from './test-state' 5 6 export * from './types'
+17 -4
packages/runner/src/run.ts
··· 145 145 await fn() 146 146 } 147 147 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 + 148 156 await runner.onAfterTryTest?.(test, retryCount) 149 157 150 158 test.result.state = 'pass' ··· 197 205 198 206 function failTask(result: TaskResult, err: unknown, runner: VitestRunner) { 199 207 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 + } 204 217 } 205 218 206 219 function markTasksAsSkipped(suite: Suite, runner: VitestRunner) {
+30 -1
packages/snapshot/src/client.ts
··· 1 1 import { deepMergeSnapshot } from './port/utils' 2 2 import SnapshotState from './port/state' 3 3 import type { SnapshotStateOptions } from './types' 4 + import type { RawSnapshotInfo } from './port/rawSnapshot' 4 5 5 6 const createMismatchError = (message: string, actual: unknown, expected: unknown) => { 6 7 const error = new Error(message) ··· 35 36 inlineSnapshot?: string 36 37 error?: Error 37 38 errorMessage?: string 39 + rawSnapshot?: RawSnapshotInfo 38 40 } 39 41 40 42 export class SnapshotClient { ··· 79 81 } 80 82 81 83 /** 82 - * Should be overriden by the consumer. 84 + * Should be overridden by the consumer. 83 85 * 84 86 * Vitest checks equality with @vitest/expect. 85 87 */ ··· 97 99 inlineSnapshot, 98 100 error, 99 101 errorMessage, 102 + rawSnapshot, 100 103 } = options 101 104 let { received } = options 102 105 ··· 134 137 isInline, 135 138 error, 136 139 inlineSnapshot, 140 + rawSnapshot, 137 141 }) 138 142 139 143 if (!pass) 140 144 throw createMismatchError(`Snapshot \`${key || 'unknown'}\` mismatched`, actual?.trim(), expected?.trim()) 145 + } 146 + 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) 141 170 } 142 171 143 172 async resetCurrent() {
+7 -1
packages/snapshot/src/manager.ts
··· 1 - import { basename, dirname, join } from 'pathe' 1 + import { basename, dirname, isAbsolute, join, resolve } from 'pathe' 2 2 import type { SnapshotResult, SnapshotStateOptions, SnapshotSummary } from './types' 3 3 4 4 export class SnapshotManager { ··· 27 27 }) 28 28 29 29 return resolver(testPath, this.extension) 30 + } 31 + 32 + resolveRawPath(testPath: string, rawPath: string) { 33 + return isAbsolute(rawPath) 34 + ? rawPath 35 + : resolve(dirname(testPath), rawPath) 30 36 } 31 37 } 32 38
+27 -2
test/core/test/jest-expect.test.ts
··· 544 544 })()).resolves.not.toThrow(Error) 545 545 }) 546 546 547 - it('resolves trows chai', async () => { 547 + it('resolves throws chai', async () => { 548 548 const assertion = async () => { 549 549 await expect((async () => new Error('msg'))()).resolves.toThrow() 550 550 } ··· 552 552 await expect(assertion).rejects.toThrowError('expected promise to throw an error, but it didn\'t') 553 553 }) 554 554 555 - it('resolves trows jest', async () => { 555 + it('resolves throws jest', async () => { 556 556 const assertion = async () => { 557 557 await expect((async () => new Error('msg'))()).resolves.toThrow(Error) 558 558 } ··· 678 678 catch (error) { 679 679 expect(error).toEqual(toEqualError2) 680 680 } 681 + }) 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 + }) 681 706 }) 682 707 }) 683 708
+20
test/snapshots/test/shapshots-file.test.ts
··· 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 + })
-13
test/snapshots/test/snapshot-async.test.ts
··· 1 - import { expect, test } from 'vitest' 2 - 3 - const resolve = () => Promise.resolve('foo') 4 - const reject = () => Promise.reject(new Error('foo')) 5 - 6 - test('resolved inline', async () => { 7 - await expect(resolve()).resolves.toMatchInlineSnapshot('"foo"') 8 - }) 9 - 10 - test('rejected inline', async () => { 11 - await expect(reject()).rejects.toMatchInlineSnapshot('[Error: foo]') 12 - await expect(reject()).rejects.toThrowErrorMatchingInlineSnapshot('"foo"') 13 - })
+13
test/snapshots/test/snapshots-async.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + 3 + const resolve = () => Promise.resolve('foo') 4 + const reject = () => Promise.reject(new Error('foo')) 5 + 6 + test('resolved inline', async () => { 7 + await expect(resolve()).resolves.toMatchInlineSnapshot('"foo"') 8 + }) 9 + 10 + test('rejected inline', async () => { 11 + await expect(reject()).rejects.toMatchInlineSnapshot('[Error: foo]') 12 + await expect(reject()).rejects.toThrowErrorMatchingInlineSnapshot('"foo"') 13 + })
+4
packages/browser/src/client/snapshot.ts
··· 22 22 return rpc().resolveSnapshotPath(filepath) 23 23 } 24 24 25 + resolveRawPath(testPath: string, rawPath: string): Promise<string> { 26 + return rpc().resolveSnapshotRawPath(testPath, rawPath) 27 + } 28 + 25 29 removeSnapshotFile(filepath: string): Promise<void> { 26 30 return rpc().removeFile(filepath) 27 31 }
+4
packages/runner/src/types/tasks.ts
··· 59 59 fails?: boolean 60 60 context: TestContext & ExtraContext 61 61 onFailed?: OnTestFailedHandler[] 62 + /** 63 + * Store promises (from async expects) to wait for them before finishing the test 64 + */ 65 + promises?: Promise<any>[] 62 66 } 63 67 64 68 export type Task = Test | Suite | TaskCustom | File
+7 -1
packages/snapshot/src/env/node.ts
··· 1 1 import { existsSync, promises as fs } from 'node:fs' 2 - import { basename, dirname, join } from 'pathe' 2 + import { basename, dirname, isAbsolute, join, resolve } from 'pathe' 3 3 import type { SnapshotEnvironment } from '../types' 4 4 5 5 export class NodeSnapshotEnvironment implements SnapshotEnvironment { ··· 9 9 10 10 getHeader(): string { 11 11 return `// Snapshot v${this.getVersion()}` 12 + } 13 + 14 + async resolveRawPath(testPath: string, rawPath: string) { 15 + return isAbsolute(rawPath) 16 + ? rawPath 17 + : resolve(dirname(testPath), rawPath) 12 18 } 13 19 14 20 async resolvePath(filepath: string): Promise<string> {
+22
packages/snapshot/src/port/rawSnapshot.ts
··· 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 + }
+36 -8
packages/snapshot/src/port/state.ts
··· 11 11 import type { SnapshotData, SnapshotEnvironment, SnapshotMatchOptions, SnapshotResult, SnapshotStateOptions, SnapshotUpdateState } from '../types' 12 12 import type { InlineSnapshot } from './inlineSnapshot' 13 13 import { saveInlineSnapshots } from './inlineSnapshot' 14 + import type { RawSnapshot, RawSnapshotInfo } from './rawSnapshot' 15 + import { saveRawSnapshots } from './rawSnapshot' 14 16 15 17 import { 16 18 addExtraLineBreaks, ··· 43 45 private _snapshotData: SnapshotData 44 46 private _initialData: SnapshotData 45 47 private _inlineSnapshots: Array<InlineSnapshot> 48 + private _rawSnapshots: Array<RawSnapshot> 46 49 private _uncheckedKeys: Set<string> 47 50 private _snapshotFormat: PrettyFormatOptions 48 51 private _environment: SnapshotEnvironment ··· 69 72 this._snapshotData = data 70 73 this._dirty = dirty 71 74 this._inlineSnapshots = [] 75 + this._rawSnapshots = [] 72 76 this._uncheckedKeys = new Set(Object.keys(this._snapshotData)) 73 77 this._counters = new Map() 74 78 this.expand = options.expand || false ··· 93 97 return new SnapshotState(testFilePath, snapshotPath, content, options) 94 98 } 95 99 100 + get environment() { 101 + return this._environment 102 + } 103 + 96 104 markSnapshotsAsCheckedForTest(testName: string): void { 97 105 this._uncheckedKeys.forEach((uncheckedKey) => { 98 106 if (keyToTestName(uncheckedKey) === testName) ··· 115 123 private _addSnapshot( 116 124 key: string, 117 125 receivedSerialized: string, 118 - options: { isInline: boolean; error?: Error }, 126 + options: { isInline: boolean; rawSnapshot?: RawSnapshotInfo; error?: Error }, 119 127 ): void { 120 128 this._dirty = true 121 129 if (options.isInline) { ··· 133 141 this._inlineSnapshots.push({ 134 142 snapshot: receivedSerialized, 135 143 ...stack, 144 + }) 145 + } 146 + else if (options.rawSnapshot) { 147 + this._rawSnapshots.push({ 148 + ...options.rawSnapshot, 149 + snapshot: receivedSerialized, 136 150 }) 137 151 } 138 152 else { ··· 154 168 async save(): Promise<SaveStatus> { 155 169 const hasExternalSnapshots = Object.keys(this._snapshotData).length 156 170 const hasInlineSnapshots = this._inlineSnapshots.length 157 - const isEmpty = !hasExternalSnapshots && !hasInlineSnapshots 171 + const hasRawSnapshots = this._rawSnapshots.length 172 + const isEmpty = !hasExternalSnapshots && !hasInlineSnapshots && !hasRawSnapshots 158 173 159 174 const status: SaveStatus = { 160 175 deleted: false, ··· 168 183 } 169 184 if (hasInlineSnapshots) 170 185 await saveInlineSnapshots(this._environment, this._inlineSnapshots) 186 + if (hasRawSnapshots) 187 + await saveRawSnapshots(this._environment, this._rawSnapshots) 171 188 172 189 status.saved = true 173 190 } ··· 206 223 inlineSnapshot, 207 224 isInline, 208 225 error, 226 + rawSnapshot, 209 227 }: SnapshotMatchOptions): SnapshotReturnOptions { 210 228 this._counters.set(testName, (this._counters.get(testName) || 0) + 1) 211 229 const count = Number(this._counters.get(testName)) ··· 219 237 if (!(isInline && this._snapshotData[key] !== undefined)) 220 238 this._uncheckedKeys.delete(key) 221 239 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] 224 252 const expectedTrimmed = prepareExpected(expected) 225 253 const pass = expectedTrimmed === prepareExpected(receivedSerialized) 226 254 const hasSnapshot = expected !== undefined 227 - const snapshotIsPersisted = isInline || this._fileExists 255 + const snapshotIsPersisted = isInline || this._fileExists || (rawSnapshot && rawSnapshot.content != null) 228 256 229 - if (pass && !isInline) { 257 + if (pass && !isInline && !rawSnapshot) { 230 258 // Executing a snapshot file as JavaScript and writing the strings back 231 259 // when other snapshots have changed loses the proper escaping for some 232 260 // characters. Since we check every snapshot in every test, use the newly ··· 255 283 else 256 284 this.added++ 257 285 258 - this._addSnapshot(key, receivedSerialized, { error, isInline }) 286 + this._addSnapshot(key, receivedSerialized, { error, isInline, rawSnapshot }) 259 287 } 260 288 else { 261 289 this.matched++ 262 290 } 263 291 } 264 292 else { 265 - this._addSnapshot(key, receivedSerialized, { error, isInline }) 293 + this._addSnapshot(key, receivedSerialized, { error, isInline, rawSnapshot }) 266 294 this.added++ 267 295 } 268 296
+18
packages/snapshot/src/port/utils.ts
··· 163 163 ) 164 164 } 165 165 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 + 166 184 export function prepareExpected(expected?: string) { 167 185 function findStartIndent() { 168 186 // Attempts to find indentation for objects.
+1
packages/snapshot/src/types/environment.ts
··· 2 2 getVersion(): string 3 3 getHeader(): string 4 4 resolvePath(filepath: string): Promise<string> 5 + resolveRawPath(testPath: string, rawPath: string): Promise<string> 5 6 prepareDirectory(filepath: string): Promise<void> 6 7 saveSnapshotFile(filepath: string, snapshot: string): Promise<void> 7 8 readSnapshotFile(filepath: string): Promise<string | null>
+2
packages/snapshot/src/types/index.ts
··· 1 1 import type { OptionsReceived as PrettyFormatOptions } from 'pretty-format' 2 + import type { RawSnapshotInfo } from '../port/rawSnapshot' 2 3 import type { SnapshotEnvironment } from './environment' 3 4 4 5 export type { SnapshotEnvironment } ··· 21 22 inlineSnapshot?: string 22 23 isInline: boolean 23 24 error?: Error 25 + rawSnapshot?: RawSnapshotInfo 24 26 } 25 27 26 28 export interface SnapshotResult {
+3
packages/vitest/src/api/setup.ts
··· 61 61 resolveSnapshotPath(testPath) { 62 62 return ctx.snapshot.resolvePath(testPath) 63 63 }, 64 + resolveSnapshotRawPath(testPath, rawPath) { 65 + return ctx.snapshot.resolveRawPath(testPath, rawPath) 66 + }, 64 67 removeFile(id) { 65 68 return fs.unlink(id) 66 69 },
+1
packages/vitest/src/api/types.ts
··· 15 15 getPaths(): string[] 16 16 getConfig(): ResolvedConfig 17 17 resolveSnapshotPath(testPath: string): string 18 + resolveSnapshotRawPath(testPath: string, rawPath: string): string 18 19 getModuleGraph(id: string): Promise<ModuleGraphData> 19 20 getTransformResult(id: string): Promise<TransformResultWithSource | undefined> 20 21 readFile(id: string): Promise<string | null>
+3 -2
packages/vitest/src/types/global.ts
··· 66 66 67 67 interface JestAssertion<T = any> extends jest.Matchers<void, T> { 68 68 // Snapshot 69 - toMatchSnapshot<U extends { [P in keyof T]: any }>(snapshot: Partial<U>, message?: string): void 70 - toMatchSnapshot(message?: string): void 71 69 matchSnapshot<U extends { [P in keyof T]: any }>(snapshot: Partial<U>, message?: string): void 72 70 matchSnapshot(message?: string): void 71 + toMatchSnapshot<U extends { [P in keyof T]: any }>(snapshot: Partial<U>, message?: string): void 72 + toMatchSnapshot(message?: string): void 73 73 toMatchInlineSnapshot<U extends { [P in keyof T]: any }>(properties: Partial<U>, snapshot?: string, message?: string): void 74 74 toMatchInlineSnapshot(snapshot?: string, message?: string): void 75 + toMatchFileSnapshot(filepath: string, message?: string): Promise<void> 75 76 toThrowErrorMatchingSnapshot(message?: string): void 76 77 toThrowErrorMatchingInlineSnapshot(snapshot?: string, message?: string): void 77 78
+4 -2
packages/vitest/src/integrations/chai/index.ts
··· 1 1 import * as chai from 'chai' 2 2 import './setup' 3 3 import type { Test } from '@vitest/runner' 4 + import { getCurrentTest } from '@vitest/runner' 4 5 import { GLOBAL_EXPECT, getState, setState } from '@vitest/expect' 5 6 import type { MatcherState } from '../../types/chai' 6 7 import { getCurrentEnvironment, getFullName } from '../../utils' ··· 10 11 const { assertionCalls } = getState(expect) 11 12 setState({ assertionCalls: assertionCalls + 1 }, expect) 12 13 const assert = chai.expect(value, message) as unknown as Vi.Assertion 13 - if (test) 14 + const _test = test || getCurrentTest() 15 + if (_test) 14 16 // @ts-expect-error internal 15 - return assert.withTest(test) as Vi.Assertion 17 + return assert.withTest(_test) as Vi.Assertion 16 18 else 17 19 return assert 18 20 }) as Vi.ExpectStatic
+25
packages/vitest/src/integrations/snapshot/chai.ts
··· 3 3 import { getNames } from '@vitest/runner/utils' 4 4 import type { SnapshotClient } from '@vitest/snapshot' 5 5 import { addSerializer, stripSnapshotIndentation } from '@vitest/snapshot' 6 + import { recordAsyncExpect } from '../../../../expect/src/utils' 6 7 import { VitestSnapshotClient } from './client' 7 8 8 9 let _client: SnapshotClient ··· 72 73 }, 73 74 ) 74 75 } 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 + 75 100 utils.addMethod( 76 101 chai.Assertion.prototype, 77 102 'toMatchInlineSnapshot',
+8
test/snapshots/test/fixtures/basic/input.json
··· 1 + [ 2 + [ 3 + ".name", 4 + { 5 + "color": "red" 6 + } 7 + ] 8 + ]
+3
test/snapshots/test/fixtures/basic/output.css
··· 1 + .name { 2 + color: red; 3 + }
+15
test/snapshots/test/fixtures/multiple/input.json
··· 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 + ]
+7
test/snapshots/test/fixtures/multiple/output.css
··· 1 + .text-red { 2 + color: red; 3 + } 4 + .text-lg { 5 + font-size: 1.25rem; 6 + line-height: 1.75rem; 7 + }