mirror of
https://github.com/vitest-dev/vitest.git
synced 2026-02-01 17:36:51 +00:00
feat(snapshot): introduce toMatchFileSnapshot and auto queuing expect promise (#3116)
This commit is contained in:
parent
035230b497
commit
bdc06dcbc4
@ -678,6 +678,22 @@ type Awaitable<T> = T | PromiseLike<T>
|
||||
})
|
||||
```
|
||||
|
||||
## toMatchFileSnapshot
|
||||
|
||||
- **Type:** `<T>(filepath: string, message?: string) => Promise<void>`
|
||||
|
||||
Compare or update the snapshot with the content of a file explicitly specified (instead of the `.snap` file).
|
||||
|
||||
```ts
|
||||
import { expect, it } from 'vitest'
|
||||
|
||||
it('render basic', async () => {
|
||||
const result = renderHTML(h('div', { class: 'foo' }))
|
||||
await expect(result).toMatchFileSnapshot('./test/basic.output.html')
|
||||
})
|
||||
```
|
||||
|
||||
Note that since file system operation is async, you need to use `await` with `toMatchFileSnapshot()`.
|
||||
|
||||
## toThrowErrorMatchingSnapshot
|
||||
|
||||
|
||||
@ -79,6 +79,23 @@ Or you can use the `--update` or `-u` flag in the CLI to make Vitest update snap
|
||||
vitest -u
|
||||
```
|
||||
|
||||
## File Snapshots
|
||||
|
||||
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).
|
||||
|
||||
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.
|
||||
|
||||
```ts
|
||||
import { expect, it } from 'vitest'
|
||||
|
||||
it('render basic', async () => {
|
||||
const result = renderHTML(h('div', { class: 'foo' }))
|
||||
await expect(result).toMatchFileSnapshot('./test/basic.output.html')
|
||||
})
|
||||
```
|
||||
|
||||
It will compare with the content of `./test/basic.output.html`. And can be written back with the `--update` flag.
|
||||
|
||||
## Image Snapshots
|
||||
|
||||
It's also possible to snapshot images using [`jest-image-snapshot`](https://github.com/americanexpress/jest-image-snapshot).
|
||||
|
||||
@ -22,6 +22,10 @@ export class BrowserSnapshotEnvironment implements SnapshotEnvironment {
|
||||
return rpc().resolveSnapshotPath(filepath)
|
||||
}
|
||||
|
||||
resolveRawPath(testPath: string, rawPath: string): Promise<string> {
|
||||
return rpc().resolveSnapshotRawPath(testPath, rawPath)
|
||||
}
|
||||
|
||||
removeSnapshotFile(filepath: string): Promise<void> {
|
||||
return rpc().removeFile(filepath)
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import { arrayBufferEquality, generateToBeMessage, iterableEquality, equals as j
|
||||
import type { AsymmetricMatcher } from './jest-asymmetric-matchers'
|
||||
import { diff, stringify } from './jest-matcher-utils'
|
||||
import { JEST_MATCHERS_OBJECT } from './constants'
|
||||
import { recordAsyncExpect } from './utils'
|
||||
|
||||
// Jest Expect Compact
|
||||
export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
|
||||
@ -633,6 +634,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
|
||||
utils.addProperty(chai.Assertion.prototype, 'resolves', function __VITEST_RESOLVES__(this: any) {
|
||||
utils.flag(this, 'promise', 'resolves')
|
||||
utils.flag(this, 'error', new Error('resolves'))
|
||||
const test = utils.flag(this, 'vitest-test')
|
||||
const obj = utils.flag(this, 'object')
|
||||
|
||||
if (typeof obj?.then !== 'function')
|
||||
@ -646,7 +648,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
|
||||
return result instanceof chai.Assertion ? proxy : result
|
||||
|
||||
return async (...args: any[]) => {
|
||||
return obj.then(
|
||||
const promise = obj.then(
|
||||
(value: any) => {
|
||||
utils.flag(this, 'object', value)
|
||||
return result.call(this, ...args)
|
||||
@ -655,6 +657,8 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
|
||||
throw new Error(`promise rejected "${String(err)}" instead of resolving`)
|
||||
},
|
||||
)
|
||||
|
||||
return recordAsyncExpect(test, promise)
|
||||
}
|
||||
},
|
||||
})
|
||||
@ -665,6 +669,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
|
||||
utils.addProperty(chai.Assertion.prototype, 'rejects', function __VITEST_REJECTS__(this: any) {
|
||||
utils.flag(this, 'promise', 'rejects')
|
||||
utils.flag(this, 'error', new Error('rejects'))
|
||||
const test = utils.flag(this, 'vitest-test')
|
||||
const obj = utils.flag(this, 'object')
|
||||
const wrapper = typeof obj === 'function' ? obj() : obj // for jest compat
|
||||
|
||||
@ -679,7 +684,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
|
||||
return result instanceof chai.Assertion ? proxy : result
|
||||
|
||||
return async (...args: any[]) => {
|
||||
return wrapper.then(
|
||||
const promise = wrapper.then(
|
||||
(value: any) => {
|
||||
throw new Error(`promise resolved "${String(value)}" instead of rejecting`)
|
||||
},
|
||||
@ -688,6 +693,8 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
|
||||
return result.call(this, ...args)
|
||||
},
|
||||
)
|
||||
|
||||
return recordAsyncExpect(test, promise)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
18
packages/expect/src/utils.ts
Normal file
18
packages/expect/src/utils.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export function recordAsyncExpect(test: any, promise: Promise<any>) {
|
||||
// record promise for test, that resolves before test ends
|
||||
if (test) {
|
||||
// if promise is explicitly awaited, remove it from the list
|
||||
promise = promise.finally(() => {
|
||||
const index = test.promises.indexOf(promise)
|
||||
if (index !== -1)
|
||||
test.promises.splice(index, 1)
|
||||
})
|
||||
|
||||
// record promise
|
||||
if (!test.promises)
|
||||
test.promises = []
|
||||
test.promises.push(promise)
|
||||
}
|
||||
|
||||
return promise
|
||||
}
|
||||
@ -2,4 +2,5 @@ export { startTests, updateTask } from './run'
|
||||
export { test, it, describe, suite, getCurrentSuite } from './suite'
|
||||
export { beforeAll, beforeEach, afterAll, afterEach, onTestFailed } from './hooks'
|
||||
export { setFn, getFn } from './map'
|
||||
export { getCurrentTest } from './test-state'
|
||||
export * from './types'
|
||||
|
||||
@ -145,6 +145,14 @@ export async function runTest(test: Test, runner: VitestRunner) {
|
||||
await fn()
|
||||
}
|
||||
|
||||
// some async expect will be added to this array, in case user forget to await theme
|
||||
if (test.promises) {
|
||||
const result = await Promise.allSettled(test.promises)
|
||||
const errors = result.map(r => r.status === 'rejected' ? r.reason : undefined).filter(Boolean)
|
||||
if (errors.length)
|
||||
throw errors
|
||||
}
|
||||
|
||||
await runner.onAfterTryTest?.(test, retryCount)
|
||||
|
||||
test.result.state = 'pass'
|
||||
@ -197,10 +205,15 @@ export async function runTest(test: Test, runner: VitestRunner) {
|
||||
|
||||
function failTask(result: TaskResult, err: unknown, runner: VitestRunner) {
|
||||
result.state = 'fail'
|
||||
const error = processError(err, runner.config)
|
||||
result.error = error
|
||||
result.errors ??= []
|
||||
result.errors.push(error)
|
||||
const errors = Array.isArray(err)
|
||||
? err
|
||||
: [err]
|
||||
for (const e of errors) {
|
||||
const error = processError(e, runner.config)
|
||||
result.error ??= error
|
||||
result.errors ??= []
|
||||
result.errors.push(error)
|
||||
}
|
||||
}
|
||||
|
||||
function markTasksAsSkipped(suite: Suite, runner: VitestRunner) {
|
||||
|
||||
@ -59,6 +59,10 @@ export interface Test<ExtraContext = {}> extends TaskBase {
|
||||
fails?: boolean
|
||||
context: TestContext & ExtraContext
|
||||
onFailed?: OnTestFailedHandler[]
|
||||
/**
|
||||
* Store promises (from async expects) to wait for them before finishing the test
|
||||
*/
|
||||
promises?: Promise<any>[]
|
||||
}
|
||||
|
||||
export type Task = Test | Suite | TaskCustom | File
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { deepMergeSnapshot } from './port/utils'
|
||||
import SnapshotState from './port/state'
|
||||
import type { SnapshotStateOptions } from './types'
|
||||
import type { RawSnapshotInfo } from './port/rawSnapshot'
|
||||
|
||||
const createMismatchError = (message: string, actual: unknown, expected: unknown) => {
|
||||
const error = new Error(message)
|
||||
@ -35,6 +36,7 @@ interface AssertOptions {
|
||||
inlineSnapshot?: string
|
||||
error?: Error
|
||||
errorMessage?: string
|
||||
rawSnapshot?: RawSnapshotInfo
|
||||
}
|
||||
|
||||
export class SnapshotClient {
|
||||
@ -79,7 +81,7 @@ export class SnapshotClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be overriden by the consumer.
|
||||
* Should be overridden by the consumer.
|
||||
*
|
||||
* Vitest checks equality with @vitest/expect.
|
||||
*/
|
||||
@ -97,6 +99,7 @@ export class SnapshotClient {
|
||||
inlineSnapshot,
|
||||
error,
|
||||
errorMessage,
|
||||
rawSnapshot,
|
||||
} = options
|
||||
let { received } = options
|
||||
|
||||
@ -134,12 +137,38 @@ export class SnapshotClient {
|
||||
isInline,
|
||||
error,
|
||||
inlineSnapshot,
|
||||
rawSnapshot,
|
||||
})
|
||||
|
||||
if (!pass)
|
||||
throw createMismatchError(`Snapshot \`${key || 'unknown'}\` mismatched`, actual?.trim(), expected?.trim())
|
||||
}
|
||||
|
||||
async assertRaw(options: AssertOptions): Promise<void> {
|
||||
if (!options.rawSnapshot)
|
||||
throw new Error('Raw snapshot is required')
|
||||
|
||||
const {
|
||||
filepath = this.filepath,
|
||||
rawSnapshot,
|
||||
} = options
|
||||
|
||||
if (rawSnapshot.content == null) {
|
||||
if (!filepath)
|
||||
throw new Error('Snapshot cannot be used outside of test')
|
||||
|
||||
const snapshotState = this.getSnapshotState(filepath)
|
||||
|
||||
// save the filepath, so it don't lose even if the await make it out-of-context
|
||||
options.filepath ||= filepath
|
||||
// resolve and read the raw snapshot file
|
||||
rawSnapshot.file = await snapshotState.environment.resolveRawPath(filepath, rawSnapshot.file)
|
||||
rawSnapshot.content = await snapshotState.environment.readSnapshotFile(rawSnapshot.file) || undefined
|
||||
}
|
||||
|
||||
return this.assert(options)
|
||||
}
|
||||
|
||||
async resetCurrent() {
|
||||
if (!this.snapshotState)
|
||||
return null
|
||||
|
||||
8
packages/snapshot/src/env/node.ts
vendored
8
packages/snapshot/src/env/node.ts
vendored
@ -1,5 +1,5 @@
|
||||
import { existsSync, promises as fs } from 'node:fs'
|
||||
import { basename, dirname, join } from 'pathe'
|
||||
import { basename, dirname, isAbsolute, join, resolve } from 'pathe'
|
||||
import type { SnapshotEnvironment } from '../types'
|
||||
|
||||
export class NodeSnapshotEnvironment implements SnapshotEnvironment {
|
||||
@ -11,6 +11,12 @@ export class NodeSnapshotEnvironment implements SnapshotEnvironment {
|
||||
return `// Snapshot v${this.getVersion()}`
|
||||
}
|
||||
|
||||
async resolveRawPath(testPath: string, rawPath: string) {
|
||||
return isAbsolute(rawPath)
|
||||
? rawPath
|
||||
: resolve(dirname(testPath), rawPath)
|
||||
}
|
||||
|
||||
async resolvePath(filepath: string): Promise<string> {
|
||||
return join(
|
||||
join(
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { basename, dirname, join } from 'pathe'
|
||||
import { basename, dirname, isAbsolute, join, resolve } from 'pathe'
|
||||
import type { SnapshotResult, SnapshotStateOptions, SnapshotSummary } from './types'
|
||||
|
||||
export class SnapshotManager {
|
||||
@ -28,6 +28,12 @@ export class SnapshotManager {
|
||||
|
||||
return resolver(testPath, this.extension)
|
||||
}
|
||||
|
||||
resolveRawPath(testPath: string, rawPath: string) {
|
||||
return isAbsolute(rawPath)
|
||||
? rawPath
|
||||
: resolve(dirname(testPath), rawPath)
|
||||
}
|
||||
}
|
||||
|
||||
export function emptySummary(options: Omit<SnapshotStateOptions, 'snapshotEnvironment'>): SnapshotSummary {
|
||||
|
||||
22
packages/snapshot/src/port/rawSnapshot.ts
Normal file
22
packages/snapshot/src/port/rawSnapshot.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import type { SnapshotEnvironment } from '../types'
|
||||
|
||||
export interface RawSnapshotInfo {
|
||||
file: string
|
||||
readonly?: boolean
|
||||
content?: string
|
||||
}
|
||||
|
||||
export interface RawSnapshot extends RawSnapshotInfo {
|
||||
snapshot: string
|
||||
file: string
|
||||
}
|
||||
|
||||
export async function saveRawSnapshots(
|
||||
environment: SnapshotEnvironment,
|
||||
snapshots: Array<RawSnapshot>,
|
||||
) {
|
||||
await Promise.all(snapshots.map(async (snap) => {
|
||||
if (!snap.readonly)
|
||||
await environment.saveSnapshotFile(snap.file, snap.snapshot)
|
||||
}))
|
||||
}
|
||||
@ -11,6 +11,8 @@ import type { OptionsReceived as PrettyFormatOptions } from 'pretty-format'
|
||||
import type { SnapshotData, SnapshotEnvironment, SnapshotMatchOptions, SnapshotResult, SnapshotStateOptions, SnapshotUpdateState } from '../types'
|
||||
import type { InlineSnapshot } from './inlineSnapshot'
|
||||
import { saveInlineSnapshots } from './inlineSnapshot'
|
||||
import type { RawSnapshot, RawSnapshotInfo } from './rawSnapshot'
|
||||
import { saveRawSnapshots } from './rawSnapshot'
|
||||
|
||||
import {
|
||||
addExtraLineBreaks,
|
||||
@ -43,6 +45,7 @@ export default class SnapshotState {
|
||||
private _snapshotData: SnapshotData
|
||||
private _initialData: SnapshotData
|
||||
private _inlineSnapshots: Array<InlineSnapshot>
|
||||
private _rawSnapshots: Array<RawSnapshot>
|
||||
private _uncheckedKeys: Set<string>
|
||||
private _snapshotFormat: PrettyFormatOptions
|
||||
private _environment: SnapshotEnvironment
|
||||
@ -69,6 +72,7 @@ export default class SnapshotState {
|
||||
this._snapshotData = data
|
||||
this._dirty = dirty
|
||||
this._inlineSnapshots = []
|
||||
this._rawSnapshots = []
|
||||
this._uncheckedKeys = new Set(Object.keys(this._snapshotData))
|
||||
this._counters = new Map()
|
||||
this.expand = options.expand || false
|
||||
@ -93,6 +97,10 @@ export default class SnapshotState {
|
||||
return new SnapshotState(testFilePath, snapshotPath, content, options)
|
||||
}
|
||||
|
||||
get environment() {
|
||||
return this._environment
|
||||
}
|
||||
|
||||
markSnapshotsAsCheckedForTest(testName: string): void {
|
||||
this._uncheckedKeys.forEach((uncheckedKey) => {
|
||||
if (keyToTestName(uncheckedKey) === testName)
|
||||
@ -115,7 +123,7 @@ export default class SnapshotState {
|
||||
private _addSnapshot(
|
||||
key: string,
|
||||
receivedSerialized: string,
|
||||
options: { isInline: boolean; error?: Error },
|
||||
options: { isInline: boolean; rawSnapshot?: RawSnapshotInfo; error?: Error },
|
||||
): void {
|
||||
this._dirty = true
|
||||
if (options.isInline) {
|
||||
@ -135,6 +143,12 @@ export default class SnapshotState {
|
||||
...stack,
|
||||
})
|
||||
}
|
||||
else if (options.rawSnapshot) {
|
||||
this._rawSnapshots.push({
|
||||
...options.rawSnapshot,
|
||||
snapshot: receivedSerialized,
|
||||
})
|
||||
}
|
||||
else {
|
||||
this._snapshotData[key] = receivedSerialized
|
||||
}
|
||||
@ -154,7 +168,8 @@ export default class SnapshotState {
|
||||
async save(): Promise<SaveStatus> {
|
||||
const hasExternalSnapshots = Object.keys(this._snapshotData).length
|
||||
const hasInlineSnapshots = this._inlineSnapshots.length
|
||||
const isEmpty = !hasExternalSnapshots && !hasInlineSnapshots
|
||||
const hasRawSnapshots = this._rawSnapshots.length
|
||||
const isEmpty = !hasExternalSnapshots && !hasInlineSnapshots && !hasRawSnapshots
|
||||
|
||||
const status: SaveStatus = {
|
||||
deleted: false,
|
||||
@ -168,6 +183,8 @@ export default class SnapshotState {
|
||||
}
|
||||
if (hasInlineSnapshots)
|
||||
await saveInlineSnapshots(this._environment, this._inlineSnapshots)
|
||||
if (hasRawSnapshots)
|
||||
await saveRawSnapshots(this._environment, this._rawSnapshots)
|
||||
|
||||
status.saved = true
|
||||
}
|
||||
@ -206,6 +223,7 @@ export default class SnapshotState {
|
||||
inlineSnapshot,
|
||||
isInline,
|
||||
error,
|
||||
rawSnapshot,
|
||||
}: SnapshotMatchOptions): SnapshotReturnOptions {
|
||||
this._counters.set(testName, (this._counters.get(testName) || 0) + 1)
|
||||
const count = Number(this._counters.get(testName))
|
||||
@ -219,14 +237,24 @@ export default class SnapshotState {
|
||||
if (!(isInline && this._snapshotData[key] !== undefined))
|
||||
this._uncheckedKeys.delete(key)
|
||||
|
||||
const receivedSerialized = addExtraLineBreaks(serialize(received, undefined, this._snapshotFormat))
|
||||
const expected = isInline ? inlineSnapshot : this._snapshotData[key]
|
||||
let receivedSerialized = rawSnapshot && typeof received === 'string'
|
||||
? received as string
|
||||
: serialize(received, undefined, this._snapshotFormat)
|
||||
|
||||
if (!rawSnapshot)
|
||||
receivedSerialized = addExtraLineBreaks(receivedSerialized)
|
||||
|
||||
const expected = isInline
|
||||
? inlineSnapshot
|
||||
: rawSnapshot
|
||||
? rawSnapshot.content
|
||||
: this._snapshotData[key]
|
||||
const expectedTrimmed = prepareExpected(expected)
|
||||
const pass = expectedTrimmed === prepareExpected(receivedSerialized)
|
||||
const hasSnapshot = expected !== undefined
|
||||
const snapshotIsPersisted = isInline || this._fileExists
|
||||
const snapshotIsPersisted = isInline || this._fileExists || (rawSnapshot && rawSnapshot.content != null)
|
||||
|
||||
if (pass && !isInline) {
|
||||
if (pass && !isInline && !rawSnapshot) {
|
||||
// Executing a snapshot file as JavaScript and writing the strings back
|
||||
// when other snapshots have changed loses the proper escaping for some
|
||||
// characters. Since we check every snapshot in every test, use the newly
|
||||
@ -255,14 +283,14 @@ export default class SnapshotState {
|
||||
else
|
||||
this.added++
|
||||
|
||||
this._addSnapshot(key, receivedSerialized, { error, isInline })
|
||||
this._addSnapshot(key, receivedSerialized, { error, isInline, rawSnapshot })
|
||||
}
|
||||
else {
|
||||
this.matched++
|
||||
}
|
||||
}
|
||||
else {
|
||||
this._addSnapshot(key, receivedSerialized, { error, isInline })
|
||||
this._addSnapshot(key, receivedSerialized, { error, isInline, rawSnapshot })
|
||||
this.added++
|
||||
}
|
||||
|
||||
|
||||
@ -163,6 +163,24 @@ export async function saveSnapshotFile(
|
||||
)
|
||||
}
|
||||
|
||||
export async function saveSnapshotFileRaw(
|
||||
environment: SnapshotEnvironment,
|
||||
content: string,
|
||||
snapshotPath: string,
|
||||
) {
|
||||
const oldContent = await environment.readSnapshotFile(snapshotPath)
|
||||
const skipWriting = oldContent && oldContent === content
|
||||
|
||||
if (skipWriting)
|
||||
return
|
||||
|
||||
await ensureDirectoryExists(environment, snapshotPath)
|
||||
await environment.saveSnapshotFile(
|
||||
snapshotPath,
|
||||
content,
|
||||
)
|
||||
}
|
||||
|
||||
export function prepareExpected(expected?: string) {
|
||||
function findStartIndent() {
|
||||
// Attempts to find indentation for objects.
|
||||
|
||||
@ -2,6 +2,7 @@ export interface SnapshotEnvironment {
|
||||
getVersion(): string
|
||||
getHeader(): string
|
||||
resolvePath(filepath: string): Promise<string>
|
||||
resolveRawPath(testPath: string, rawPath: string): Promise<string>
|
||||
prepareDirectory(filepath: string): Promise<void>
|
||||
saveSnapshotFile(filepath: string, snapshot: string): Promise<void>
|
||||
readSnapshotFile(filepath: string): Promise<string | null>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { OptionsReceived as PrettyFormatOptions } from 'pretty-format'
|
||||
import type { RawSnapshotInfo } from '../port/rawSnapshot'
|
||||
import type { SnapshotEnvironment } from './environment'
|
||||
|
||||
export type { SnapshotEnvironment }
|
||||
@ -21,6 +22,7 @@ export interface SnapshotMatchOptions {
|
||||
inlineSnapshot?: string
|
||||
isInline: boolean
|
||||
error?: Error
|
||||
rawSnapshot?: RawSnapshotInfo
|
||||
}
|
||||
|
||||
export interface SnapshotResult {
|
||||
|
||||
@ -61,6 +61,9 @@ export function setup(ctx: Vitest, server?: ViteDevServer) {
|
||||
resolveSnapshotPath(testPath) {
|
||||
return ctx.snapshot.resolvePath(testPath)
|
||||
},
|
||||
resolveSnapshotRawPath(testPath, rawPath) {
|
||||
return ctx.snapshot.resolveRawPath(testPath, rawPath)
|
||||
},
|
||||
removeFile(id) {
|
||||
return fs.unlink(id)
|
||||
},
|
||||
|
||||
@ -15,6 +15,7 @@ export interface WebSocketHandlers {
|
||||
getPaths(): string[]
|
||||
getConfig(): ResolvedConfig
|
||||
resolveSnapshotPath(testPath: string): string
|
||||
resolveSnapshotRawPath(testPath: string, rawPath: string): string
|
||||
getModuleGraph(id: string): Promise<ModuleGraphData>
|
||||
getTransformResult(id: string): Promise<TransformResultWithSource | undefined>
|
||||
readFile(id: string): Promise<string | null>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import * as chai from 'chai'
|
||||
import './setup'
|
||||
import type { Test } from '@vitest/runner'
|
||||
import { getCurrentTest } from '@vitest/runner'
|
||||
import { GLOBAL_EXPECT, getState, setState } from '@vitest/expect'
|
||||
import type { MatcherState } from '../../types/chai'
|
||||
import { getCurrentEnvironment, getFullName } from '../../utils'
|
||||
@ -10,9 +11,10 @@ export function createExpect(test?: Test) {
|
||||
const { assertionCalls } = getState(expect)
|
||||
setState({ assertionCalls: assertionCalls + 1 }, expect)
|
||||
const assert = chai.expect(value, message) as unknown as Vi.Assertion
|
||||
if (test)
|
||||
const _test = test || getCurrentTest()
|
||||
if (_test)
|
||||
// @ts-expect-error internal
|
||||
return assert.withTest(test) as Vi.Assertion
|
||||
return assert.withTest(_test) as Vi.Assertion
|
||||
else
|
||||
return assert
|
||||
}) as Vi.ExpectStatic
|
||||
|
||||
@ -3,6 +3,7 @@ import type { Test } from '@vitest/runner'
|
||||
import { getNames } from '@vitest/runner/utils'
|
||||
import type { SnapshotClient } from '@vitest/snapshot'
|
||||
import { addSerializer, stripSnapshotIndentation } from '@vitest/snapshot'
|
||||
import { recordAsyncExpect } from '../../../../expect/src/utils'
|
||||
import { VitestSnapshotClient } from './client'
|
||||
|
||||
let _client: SnapshotClient
|
||||
@ -72,6 +73,30 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
utils.addMethod(
|
||||
chai.Assertion.prototype,
|
||||
'toMatchFileSnapshot',
|
||||
function (this: Record<string, unknown>, file: string, message?: string) {
|
||||
const expected = utils.flag(this, 'object')
|
||||
const test = utils.flag(this, 'vitest-test') as Test
|
||||
const errorMessage = utils.flag(this, 'message')
|
||||
|
||||
const promise = getSnapshotClient().assertRaw({
|
||||
received: expected,
|
||||
message,
|
||||
isInline: false,
|
||||
rawSnapshot: {
|
||||
file,
|
||||
},
|
||||
errorMessage,
|
||||
...getTestNames(test),
|
||||
})
|
||||
|
||||
return recordAsyncExpect(test, promise)
|
||||
},
|
||||
)
|
||||
|
||||
utils.addMethod(
|
||||
chai.Assertion.prototype,
|
||||
'toMatchInlineSnapshot',
|
||||
|
||||
@ -66,12 +66,13 @@ declare global {
|
||||
|
||||
interface JestAssertion<T = any> extends jest.Matchers<void, T> {
|
||||
// Snapshot
|
||||
toMatchSnapshot<U extends { [P in keyof T]: any }>(snapshot: Partial<U>, message?: string): void
|
||||
toMatchSnapshot(message?: string): void
|
||||
matchSnapshot<U extends { [P in keyof T]: any }>(snapshot: Partial<U>, message?: string): void
|
||||
matchSnapshot(message?: string): void
|
||||
toMatchSnapshot<U extends { [P in keyof T]: any }>(snapshot: Partial<U>, message?: string): void
|
||||
toMatchSnapshot(message?: string): void
|
||||
toMatchInlineSnapshot<U extends { [P in keyof T]: any }>(properties: Partial<U>, snapshot?: string, message?: string): void
|
||||
toMatchInlineSnapshot(snapshot?: string, message?: string): void
|
||||
toMatchFileSnapshot(filepath: string, message?: string): Promise<void>
|
||||
toThrowErrorMatchingSnapshot(message?: string): void
|
||||
toThrowErrorMatchingInlineSnapshot(snapshot?: string, message?: string): void
|
||||
|
||||
|
||||
@ -544,7 +544,7 @@ describe('async expect', () => {
|
||||
})()).resolves.not.toThrow(Error)
|
||||
})
|
||||
|
||||
it('resolves trows chai', async () => {
|
||||
it('resolves throws chai', async () => {
|
||||
const assertion = async () => {
|
||||
await expect((async () => new Error('msg'))()).resolves.toThrow()
|
||||
}
|
||||
@ -552,7 +552,7 @@ describe('async expect', () => {
|
||||
await expect(assertion).rejects.toThrowError('expected promise to throw an error, but it didn\'t')
|
||||
})
|
||||
|
||||
it('resolves trows jest', async () => {
|
||||
it('resolves throws jest', async () => {
|
||||
const assertion = async () => {
|
||||
await expect((async () => new Error('msg'))()).resolves.toThrow(Error)
|
||||
}
|
||||
@ -679,6 +679,31 @@ describe('async expect', () => {
|
||||
expect(error).toEqual(toEqualError2)
|
||||
}
|
||||
})
|
||||
|
||||
describe('promise auto queuing', () => {
|
||||
it.fails('fails', () => {
|
||||
expect(() => new Promise((resolve, reject) => setTimeout(reject, 500)))
|
||||
.resolves
|
||||
.toBe('true')
|
||||
})
|
||||
|
||||
let value = 0
|
||||
|
||||
it('pass first', () => {
|
||||
expect((async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
value += 1
|
||||
return value
|
||||
})())
|
||||
.resolves
|
||||
.toBe(1)
|
||||
})
|
||||
|
||||
it('pass second', () => {
|
||||
// even if 'pass first' is sync, we will still wait the expect to resolve
|
||||
expect(value).toBe(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('compatible with jest', () => {
|
||||
|
||||
8
test/snapshots/test/fixtures/basic/input.json
vendored
Normal file
8
test/snapshots/test/fixtures/basic/input.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
[
|
||||
[
|
||||
".name",
|
||||
{
|
||||
"color": "red"
|
||||
}
|
||||
]
|
||||
]
|
||||
3
test/snapshots/test/fixtures/basic/output.css
vendored
Normal file
3
test/snapshots/test/fixtures/basic/output.css
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
.name {
|
||||
color: red;
|
||||
}
|
||||
15
test/snapshots/test/fixtures/multiple/input.json
vendored
Normal file
15
test/snapshots/test/fixtures/multiple/input.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
[
|
||||
[
|
||||
".text-red",
|
||||
{
|
||||
"color": "red"
|
||||
}
|
||||
],
|
||||
[
|
||||
".text-lg",
|
||||
{
|
||||
"font-size": "1.25rem",
|
||||
"line-height": "1.75rem"
|
||||
}
|
||||
]
|
||||
]
|
||||
7
test/snapshots/test/fixtures/multiple/output.css
vendored
Normal file
7
test/snapshots/test/fixtures/multiple/output.css
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
.text-red {
|
||||
color: red;
|
||||
}
|
||||
.text-lg {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
20
test/snapshots/test/shapshots-file.test.ts
Normal file
20
test/snapshots/test/shapshots-file.test.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { expect, test } from 'vitest'
|
||||
|
||||
function objectToCSS(selector: string, obj: Record<string, string>) {
|
||||
const body = Object.entries(obj)
|
||||
.map(([key, value]) => ` ${key}: ${value};`)
|
||||
.join('\n')
|
||||
return `${selector} {\n${body}\n}`
|
||||
}
|
||||
|
||||
describe('snapshots', () => {
|
||||
const files = import.meta.glob('./fixtures/**/input.json', { as: 'raw' })
|
||||
|
||||
for (const [path, file] of Object.entries(files)) {
|
||||
test(path, async () => {
|
||||
const entries = JSON.parse(await file()) as any[]
|
||||
expect(entries.map(i => objectToCSS(i[0], i[1])).join('\n'))
|
||||
.toMatchFileSnapshot(path.replace('input.json', 'output.css'))
|
||||
})
|
||||
}
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user