mirror of
https://github.com/vitest-dev/vitest.git
synced 2025-12-08 18:26:03 +00:00
feat: onTestFailed hook (#2210)
This commit is contained in:
parent
10ec04d1c4
commit
637c85daee
@ -66,6 +66,10 @@ export function createTestContext(test: Test): TestContext {
|
||||
return _expect != null
|
||||
},
|
||||
})
|
||||
context.onTestFailed = (fn) => {
|
||||
test.onFailed ||= []
|
||||
test.onFailed.push(fn)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
@ -1,9 +1,26 @@
|
||||
import type { SuiteHooks } from '../types'
|
||||
import type { OnTestFailedHandler, SuiteHooks, Test } from '../types'
|
||||
import { getDefaultHookTimeout, withTimeout } from './context'
|
||||
import { getCurrentSuite } from './suite'
|
||||
import { getCurrentTest } from './test-state'
|
||||
|
||||
// suite hooks
|
||||
export const beforeAll = (fn: SuiteHooks['beforeAll'][0], timeout?: number) => getCurrentSuite().on('beforeAll', withTimeout(fn, timeout ?? getDefaultHookTimeout(), true))
|
||||
export const afterAll = (fn: SuiteHooks['afterAll'][0], timeout?: number) => getCurrentSuite().on('afterAll', withTimeout(fn, timeout ?? getDefaultHookTimeout(), true))
|
||||
export const beforeEach = <ExtraContext = {}>(fn: SuiteHooks<ExtraContext>['beforeEach'][0], timeout?: number) => getCurrentSuite<ExtraContext>().on('beforeEach', withTimeout(fn, timeout ?? getDefaultHookTimeout(), true))
|
||||
export const afterEach = <ExtraContext = {}>(fn: SuiteHooks<ExtraContext>['afterEach'][0], timeout?: number) => getCurrentSuite<ExtraContext>().on('afterEach', withTimeout(fn, timeout ?? getDefaultHookTimeout(), true))
|
||||
|
||||
export const onTestFailed = createTestHook<OnTestFailedHandler>('onTestFailed', (test, handler) => {
|
||||
test.onFailed ||= []
|
||||
test.onFailed.push(handler)
|
||||
})
|
||||
|
||||
function createTestHook<T>(name: string, handler: (test: Test, handler: T) => void) {
|
||||
return (fn: T) => {
|
||||
const current = getCurrentTest()
|
||||
|
||||
if (!current)
|
||||
throw new Error(`Hook ${name}() can only be called inside a test`)
|
||||
|
||||
handler(current, fn)
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import { getFn, getHooks } from './map'
|
||||
import { rpc } from './rpc'
|
||||
import { collectTests } from './collect'
|
||||
import { processError } from './error'
|
||||
import { setCurrentTest } from './test-state'
|
||||
|
||||
async function importTinybench() {
|
||||
if (!globalThis.EventTarget)
|
||||
@ -115,6 +116,8 @@ export async function runTest(test: Test) {
|
||||
|
||||
clearModuleMocks()
|
||||
|
||||
setCurrentTest(test)
|
||||
|
||||
if (isNode) {
|
||||
const { getSnapshotClient } = await import('../integrations/snapshot/chai')
|
||||
await getSnapshotClient().setTest(test)
|
||||
@ -180,6 +183,9 @@ export async function runTest(test: Test) {
|
||||
updateTask(test)
|
||||
}
|
||||
|
||||
if (test.result.state === 'fail')
|
||||
await Promise.all(test.onFailed?.map(fn => fn(test.result!)) || [])
|
||||
|
||||
// if test is marked to be failed, flip the result
|
||||
if (test.fails) {
|
||||
if (test.result.state === 'pass') {
|
||||
@ -195,6 +201,8 @@ export async function runTest(test: Test) {
|
||||
if (isBrowser && test.result.error)
|
||||
console.error(test.result.error.message, test.result.error.stackStr)
|
||||
|
||||
setCurrentTest(undefined)
|
||||
|
||||
if (isNode) {
|
||||
const { getSnapshotClient } = await import('../integrations/snapshot/chai')
|
||||
getSnapshotClient().clearTest()
|
||||
|
||||
@ -12,39 +12,18 @@ export const test = createTest(
|
||||
getCurrentSuite().test.fn.call(this, name, fn, options)
|
||||
},
|
||||
)
|
||||
|
||||
export const bench = createBenchmark(
|
||||
function (name, fn: BenchFunction = noop, options: BenchOptions = {}) {
|
||||
getCurrentSuite().benchmark.fn.call(this, name, fn, options)
|
||||
},
|
||||
)
|
||||
|
||||
function formatTitle(template: string, items: any[], idx: number) {
|
||||
if (template.includes('%#')) {
|
||||
// '%#' match index of the test case
|
||||
template = template
|
||||
.replace(/%%/g, '__vitest_escaped_%__')
|
||||
.replace(/%#/g, `${idx}`)
|
||||
.replace(/__vitest_escaped_%__/g, '%%')
|
||||
}
|
||||
|
||||
const count = template.split('%').length - 1
|
||||
let formatted = util.format(template, ...items.slice(0, count))
|
||||
if (isObject(items[0])) {
|
||||
formatted = formatted.replace(/\$([$\w_]+)/g, (_, key) => {
|
||||
return items[0][key]
|
||||
})
|
||||
}
|
||||
return formatted
|
||||
}
|
||||
|
||||
// alias
|
||||
export const describe = suite
|
||||
export const it = test
|
||||
|
||||
const workerState = getWorkerState()
|
||||
|
||||
// implementations
|
||||
export const defaultSuite = workerState.config.sequence.shuffle
|
||||
? suite.shuffle('')
|
||||
: suite('')
|
||||
@ -68,6 +47,7 @@ export function createSuiteHooks() {
|
||||
}
|
||||
}
|
||||
|
||||
// implementations
|
||||
function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, mode: RunMode, concurrent?: boolean, shuffle?: boolean, suiteOptions?: number | TestOptions) {
|
||||
const tasks: (Benchmark | Test | Suite | SuiteCollector)[] = []
|
||||
const factoryQueue: (Test | Suite | SuiteCollector)[] = []
|
||||
@ -267,3 +247,22 @@ function createBenchmark(fn: (
|
||||
|
||||
return benchmark as BenchmarkAPI
|
||||
}
|
||||
|
||||
function formatTitle(template: string, items: any[], idx: number) {
|
||||
if (template.includes('%#')) {
|
||||
// '%#' match index of the test case
|
||||
template = template
|
||||
.replace(/%%/g, '__vitest_escaped_%__')
|
||||
.replace(/%#/g, `${idx}`)
|
||||
.replace(/__vitest_escaped_%__/g, '%%')
|
||||
}
|
||||
|
||||
const count = template.split('%').length - 1
|
||||
let formatted = util.format(template, ...items.slice(0, count))
|
||||
if (isObject(items[0])) {
|
||||
formatted = formatted.replace(/\$([$\w_]+)/g, (_, key) => {
|
||||
return items[0][key]
|
||||
})
|
||||
}
|
||||
return formatted
|
||||
}
|
||||
|
||||
11
packages/vitest/src/runtime/test-state.ts
Normal file
11
packages/vitest/src/runtime/test-state.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import type { Test } from '../types'
|
||||
|
||||
let _test: Test | undefined
|
||||
|
||||
export function setCurrentTest(test: Test | undefined) {
|
||||
_test = test
|
||||
}
|
||||
|
||||
export function getCurrentTest() {
|
||||
return _test
|
||||
}
|
||||
@ -52,6 +52,7 @@ export interface Test<ExtraContext = {}> extends TaskBase {
|
||||
result?: TaskResult
|
||||
fails?: boolean
|
||||
context: TestContext & ExtraContext
|
||||
onFailed?: OnTestFailedHandler[]
|
||||
}
|
||||
|
||||
export type Task = Test | Suite | File | Benchmark
|
||||
@ -213,4 +214,11 @@ export interface TestContext {
|
||||
* A expect instance bound to the test
|
||||
*/
|
||||
expect: Vi.ExpectStatic
|
||||
|
||||
/**
|
||||
* Extract hooks on test failed
|
||||
*/
|
||||
onTestFailed: (fn: OnTestFailedHandler) => void
|
||||
}
|
||||
|
||||
export type OnTestFailedHandler = (result: TaskResult) => Awaitable<void>
|
||||
|
||||
27
test/core/test/on-failed.test.ts
Normal file
27
test/core/test/on-failed.test.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { expect, it, onTestFailed } from 'vitest'
|
||||
|
||||
const collected: any[] = []
|
||||
|
||||
it.fails('on-failed', () => {
|
||||
const square3 = 3 ** 2
|
||||
const square4 = 4 ** 2
|
||||
|
||||
onTestFailed(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Unexpected error encountered, internal states:', { square3, square4 })
|
||||
collected.push({ square3, square4 })
|
||||
})
|
||||
|
||||
expect(Math.sqrt(square3 + square4)).toBe(4)
|
||||
})
|
||||
|
||||
it('after', () => {
|
||||
expect(collected).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"square3": 9,
|
||||
"square4": 16,
|
||||
},
|
||||
]
|
||||
`)
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user