import type { Test } from '../../vitest/src/runtime/runner/types' import type { Assertion } from './types' import { processError } from '@vitest/utils/error' import { noop } from '@vitest/utils/helpers' export function createAssertionMessage( util: Chai.ChaiUtils, assertion: Chai.Assertion, hasArgs: boolean, ) { const soft = util.flag(assertion, 'soft') ? '.soft' : '' const not = util.flag(assertion, 'negate') ? 'not.' : '' const name = `${util.flag(assertion, '_name')}(${hasArgs ? 'expected' : ''})` const promiseName = util.flag(assertion, 'promise') const promise = promiseName ? `.${promiseName}` : '' return `expect${soft}(actual)${promise}.${not}${name}` } export function recordAsyncExpect( _test: any, promise: Promise, assertion: string, error: Error, isSoft?: boolean, ): Promise { const test = _test as Test | undefined // record promise for test, that resolves before test ends if (test && promise instanceof Promise) { // if promise is explicitly awaited, remove it from the list promise = promise.finally(() => { if (!test.promises) { return } const index = test.promises.indexOf(promise) if (index !== -1) { test.promises.splice(index, 1) } }) // record promise if (!test.promises) { test.promises = [] } // setup `expect.soft` handler here instead of `wrapAssertion` // to avoid double error tracking while keeping non-await promise detection. if (isSoft) { promise = promise.then(noop, (err) => { handleTestError(test, err) }) } test.promises.push(promise) let resolved = false test.onFinished ??= [] test.onFinished.push(() => { if (!resolved) { const awaitError = new Error( `Promise returned by \`${assertion}\` was not awaited. This assertion is asynchronous and must be awaited; otherwise, it is not guaranteed to complete before the test finishes:\n\nawait ${assertion}\n`, ) if (error.stack) { awaitError.stack = error.stack.replace(error.message, awaitError.message) } throw awaitError } }) return { then(onFulfilled, onRejected) { resolved = true return promise.then(onFulfilled, onRejected) }, catch(onRejected) { resolved = true return promise.catch(onRejected) }, finally(onFinally) { resolved = true return promise.finally(onFinally) }, [Symbol.toStringTag]: 'Promise', } satisfies Promise } return promise } function handleTestError(test: Test, err: unknown) { test.result ||= { state: 'fail' } test.result.state = 'fail' test.result.errors ||= [] test.result.errors.push(processError(err)) } /** wrap assertion function to support `expect.soft` and provide assertion name as `_name` */ export function wrapAssertion( utils: Chai.ChaiUtils, name: string, fn: (this: Chai.AssertionStatic & Assertion, ...args: any[]) => void | PromiseLike, ) { return function ( this: Chai.AssertionStatic & Assertion, ...args: any[] ): void | PromiseLike { // private if (name !== 'withTest') { utils.flag(this, '_name', name) } if (!utils.flag(this, 'soft')) { // avoid WebKit's proper tail call to preserve stacktrace offset for inline snapshot // https://webkit.org/blog/6240/ecmascript-6-proper-tail-calls-in-webkit try { return fn.apply(this, args) } finally { // no lint } } const test: Test = utils.flag(this, 'vitest-test') if (!test) { throw new Error('expect.soft() can only be used inside a test') } try { const result = fn.apply(this, args) if (result && typeof result === 'object' && typeof result.then === 'function') { return result.then(noop, (err) => { handleTestError(test, err) }) } return result } catch (err) { handleTestError(test, err) } } }