From 637c85daeea30e4c9efd9b68b5fe9dfc1914cf0c Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Mon, 7 Nov 2022 20:00:19 +0800 Subject: [PATCH] feat: `onTestFailed` hook (#2210) --- packages/vitest/src/runtime/context.ts | 4 +++ packages/vitest/src/runtime/hooks.ts | 19 ++++++++++- packages/vitest/src/runtime/run.ts | 8 +++++ packages/vitest/src/runtime/suite.ts | 41 +++++++++++------------ packages/vitest/src/runtime/test-state.ts | 11 ++++++ packages/vitest/src/types/tasks.ts | 8 +++++ test/core/test/on-failed.test.ts | 27 +++++++++++++++ 7 files changed, 96 insertions(+), 22 deletions(-) create mode 100644 packages/vitest/src/runtime/test-state.ts create mode 100644 test/core/test/on-failed.test.ts diff --git a/packages/vitest/src/runtime/context.ts b/packages/vitest/src/runtime/context.ts index 9a8beee3348e..76ed38716f2a 100644 --- a/packages/vitest/src/runtime/context.ts +++ b/packages/vitest/src/runtime/context.ts @@ -66,6 +66,10 @@ export function createTestContext(test: Test): TestContext { return _expect != null }, }) + context.onTestFailed = (fn) => { + test.onFailed ||= [] + test.onFailed.push(fn) + } return context } diff --git a/packages/vitest/src/runtime/hooks.ts b/packages/vitest/src/runtime/hooks.ts index df20f8605ec1..ed391e109aa2 100644 --- a/packages/vitest/src/runtime/hooks.ts +++ b/packages/vitest/src/runtime/hooks.ts @@ -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 = (fn: SuiteHooks['beforeEach'][0], timeout?: number) => getCurrentSuite().on('beforeEach', withTimeout(fn, timeout ?? getDefaultHookTimeout(), true)) export const afterEach = (fn: SuiteHooks['afterEach'][0], timeout?: number) => getCurrentSuite().on('afterEach', withTimeout(fn, timeout ?? getDefaultHookTimeout(), true)) + +export const onTestFailed = createTestHook('onTestFailed', (test, handler) => { + test.onFailed ||= [] + test.onFailed.push(handler) +}) + +function createTestHook(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) + } +} diff --git a/packages/vitest/src/runtime/run.ts b/packages/vitest/src/runtime/run.ts index 4e87e15f3327..cec0882ca7e2 100644 --- a/packages/vitest/src/runtime/run.ts +++ b/packages/vitest/src/runtime/run.ts @@ -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() diff --git a/packages/vitest/src/runtime/suite.ts b/packages/vitest/src/runtime/suite.ts index cd6ef0d475e2..6f661de46b9d 100644 --- a/packages/vitest/src/runtime/suite.ts +++ b/packages/vitest/src/runtime/suite.ts @@ -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 +} diff --git a/packages/vitest/src/runtime/test-state.ts b/packages/vitest/src/runtime/test-state.ts new file mode 100644 index 000000000000..26c81cf024f8 --- /dev/null +++ b/packages/vitest/src/runtime/test-state.ts @@ -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 +} diff --git a/packages/vitest/src/types/tasks.ts b/packages/vitest/src/types/tasks.ts index c7bab516322e..c4e426d84078 100644 --- a/packages/vitest/src/types/tasks.ts +++ b/packages/vitest/src/types/tasks.ts @@ -52,6 +52,7 @@ export interface Test 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 diff --git a/test/core/test/on-failed.test.ts b/test/core/test/on-failed.test.ts new file mode 100644 index 000000000000..12281177847d --- /dev/null +++ b/test/core/test/on-failed.test.ts @@ -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, + }, + ] + `) +})