From 6f5b42b7a8e8a3c2ddb1c6876ea474553d686e28 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 7 Feb 2024 11:35:17 +0100 Subject: [PATCH] feat(vitest): add onTestFinished hook (#5128) --- docs/api/index.md | 97 ++++++++++++++++++++++++++++++ packages/runner/src/context.ts | 5 ++ packages/runner/src/hooks.ts | 9 ++- packages/runner/src/index.ts | 2 +- packages/runner/src/run.ts | 17 +++++- packages/runner/src/types/tasks.ts | 7 +++ packages/vitest/src/index.ts | 1 + test/core/test/on-finished.test.ts | 43 +++++++++++++ 8 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 test/core/test/on-finished.test.ts diff --git a/docs/api/index.md b/docs/api/index.md index c3521b68002f..f245730919a4 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -853,6 +853,10 @@ afterEach(async () => { Here, the `afterEach` ensures that testing data is cleared after each test runs. +::: tip +Vitest 1.3.0 added [`onTestFinished`](##ontestfinished-1-3-0) hook. You can call it during the test execution to cleanup any state after the test has finished running. +::: + ### beforeAll - **Type:** `beforeAll(fn: () => Awaitable, timeout?: number)` @@ -906,3 +910,96 @@ afterAll(async () => { ``` Here the `afterAll` ensures that `stopMocking` method is called after all tests run. + +## Test Hooks + +Vitest provides a few hooks that you can call _during_ the test execution to cleanup the state when the test has finished runnning. + +::: warning +These hooks will throw an error if they are called outside of the test body. +::: + +### onTestFinished 1.3.0+ + +This hook is always called after the test has finished running. It is called after `afterEach` hooks since they can influence the test result. It receives a `TaskResult` object with the current test result. + +```ts +import { onTestFinished, test } from 'vitest' + +test('performs a query', () => { + const db = connectDb() + onTestFinished(() => db.close()) + db.query('SELECT * FROM users') +}) +``` + +::: warning +If you are running tests concurrently, you should always use `onTestFinished` hook from the test context since Vitest doesn't track concurrent tests in global hooks: + +```ts +import { test } from 'vitest' + +test.concurrent('performs a query', (t) => { + const db = connectDb() + t.onTestFinished(() => db.close()) + db.query('SELECT * FROM users') +}) +``` +::: + +This hook is particularly useful when creating reusable logic: + +```ts +// this can be in a separate file +function getTestDb() { + const db = connectMockedDb() + onTestFinished(() => db.close()) + return db +} + +test('performs a user query', async () => { + const db = getTestDb() + expect( + await db.query('SELECT * from users').perform() + ).toEqual([]) +}) + +test('performs an organization query', async () => { + const db = getTestDb() + expect( + await db.query('SELECT * from organizations').perform() + ).toEqual([]) +}) +``` + +### onTestFailed + +This hook is called only after the test has failed. It is called after `afterEach` hooks since they can influence the test result. It receives a `TaskResult` object with the current test result. This hook is useful for debugging. + +```ts +import { onTestFailed, test } from 'vitest' + +test('performs a query', () => { + const db = connectDb() + onTestFailed((e) => { + console.log(e.result.errors) + }) + db.query('SELECT * FROM users') +}) +``` + +::: warning +If you are running tests concurrently, you should always use `onTestFailed` hook from the test context since Vitest doesn't track concurrent tests in global hooks: + +```ts +import { test } from 'vitest' + +test.concurrent('performs a query', (t) => { + const db = connectDb() + onTestFailed((result) => { + console.log(result.errors) + }) + db.query('SELECT * FROM users') +}) +``` +::: diff --git a/packages/runner/src/context.ts b/packages/runner/src/context.ts index dec0a30506be..456052cf4e60 100644 --- a/packages/runner/src/context.ts +++ b/packages/runner/src/context.ts @@ -59,6 +59,11 @@ export function createTestContext(test: T, runner: Vite test.onFailed.push(fn) } + context.onTestFinished = (fn) => { + test.onFinished ||= [] + test.onFinished.push(fn) + } + return runner.extendTaskContext?.(context) as ExtendedContext || context } diff --git a/packages/runner/src/hooks.ts b/packages/runner/src/hooks.ts index 47a724fc2d2c..96aa09256d94 100644 --- a/packages/runner/src/hooks.ts +++ b/packages/runner/src/hooks.ts @@ -1,4 +1,4 @@ -import type { OnTestFailedHandler, SuiteHooks, TaskPopulated } from './types' +import type { OnTestFailedHandler, OnTestFinishedHandler, SuiteHooks, TaskPopulated } from './types' import { getCurrentSuite, getRunner } from './suite' import { getCurrentTest } from './test-state' import { withTimeout } from './context' @@ -27,6 +27,11 @@ export const onTestFailed = createTestHook('onTestFailed', test.onFailed.push(handler) }) +export const onTestFinished = createTestHook('onTestFinished', (test, handler) => { + test.onFinished ||= [] + test.onFinished.push(handler) +}) + function createTestHook(name: string, handler: (test: TaskPopulated, handler: T) => void) { return (fn: T) => { const current = getCurrentTest() @@ -34,6 +39,6 @@ function createTestHook(name: string, handler: (test: TaskPopulated, handler: if (!current) throw new Error(`Hook ${name}() can only be called inside a test`) - handler(current, fn) + return handler(current, fn) } } diff --git a/packages/runner/src/index.ts b/packages/runner/src/index.ts index 3c3ad026ebf1..52e06cf8ca5a 100644 --- a/packages/runner/src/index.ts +++ b/packages/runner/src/index.ts @@ -1,6 +1,6 @@ export { startTests, updateTask } from './run' export { test, it, describe, suite, getCurrentSuite, createTaskCollector } from './suite' -export { beforeAll, beforeEach, afterAll, afterEach, onTestFailed } from './hooks' +export { beforeAll, beforeEach, afterAll, afterEach, onTestFailed, onTestFinished } from './hooks' export { setFn, getFn, getHooks, setHooks } from './map' export { getCurrentTest } from './test-state' export { processError } from '@vitest/utils/error' diff --git a/packages/runner/src/run.ts b/packages/runner/src/run.ts index ab82f9193368..4087ab3b29fb 100644 --- a/packages/runner/src/run.ts +++ b/packages/runner/src/run.ts @@ -210,8 +210,21 @@ export async function runTest(test: Test | Custom, runner: VitestRunner) { } } - if (test.result.state === 'fail') - await Promise.all(test.onFailed?.map(fn => fn(test.result!)) || []) + try { + await Promise.all(test.onFinished?.map(fn => fn(test.result!)) || []) + } + catch (e) { + failTask(test.result, e, runner.config.diffOptions) + } + + if (test.result.state === 'fail') { + try { + await Promise.all(test.onFailed?.map(fn => fn(test.result!)) || []) + } + catch (e) { + failTask(test.result, e, runner.config.diffOptions) + } + } // if test is marked to be failed, flip the result if (test.fails) { diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts index dd44ce5571c2..199ff8dc8c4b 100644 --- a/packages/runner/src/types/tasks.ts +++ b/packages/runner/src/types/tasks.ts @@ -26,6 +26,7 @@ export interface TaskPopulated extends TaskBase { result?: TaskResult fails?: boolean onFailed?: OnTestFailedHandler[] + onFinished?: OnTestFinishedHandler[] /** * Store promises (from async expects) to wait for them before finishing the test */ @@ -296,6 +297,11 @@ export interface TaskContext { */ onTestFailed: (fn: OnTestFailedHandler) => void + /** + * Extract hooks on test failed + */ + onTestFinished: (fn: OnTestFinishedHandler) => void + /** * Mark tests as skipped. All execution after this call will be skipped. */ @@ -305,6 +311,7 @@ export interface TaskContext { export type ExtendedContext = TaskContext & TestContext export type OnTestFailedHandler = (result: TaskResult) => Awaitable +export type OnTestFinishedHandler = (result: TaskResult) => Awaitable export type SequenceHooks = 'stack' | 'list' | 'parallel' export type SequenceSetupFiles = 'list' | 'parallel' diff --git a/packages/vitest/src/index.ts b/packages/vitest/src/index.ts index 9f5668d16649..aab37704962d 100644 --- a/packages/vitest/src/index.ts +++ b/packages/vitest/src/index.ts @@ -8,6 +8,7 @@ export { afterAll, afterEach, onTestFailed, + onTestFinished, } from '@vitest/runner' export { bench } from './runtime/benchmark' diff --git a/test/core/test/on-finished.test.ts b/test/core/test/on-finished.test.ts new file mode 100644 index 000000000000..5f4f050d7534 --- /dev/null +++ b/test/core/test/on-finished.test.ts @@ -0,0 +1,43 @@ +import { expect, it, onTestFinished } from 'vitest' + +const collected: any[] = [] + +it('on-finished regular', () => { + collected.push(1) + onTestFinished(() => { + collected.push(3) + }) + collected.push(2) +}) + +it('on-finished context', (t) => { + collected.push(4) + t.onTestFinished(() => { + collected.push(6) + }) + collected.push(5) +}) + +it.fails('failed finish', () => { + collected.push(7) + onTestFinished(() => { + collected.push(9) + }) + collected.push(8) + expect.fail('failed') + collected.push(null) +}) + +it.fails('failed finish context', (t) => { + collected.push(10) + t.onTestFinished(() => { + collected.push(12) + }) + collected.push(11) + expect.fail('failed') + collected.push(null) +}) + +it('after', () => { + expect(collected).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) +})