Skip to content

Commit

Permalink
feat: onTestFailed hook (#2210)
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Nov 7, 2022
1 parent 10ec04d commit 637c85d
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 22 deletions.
4 changes: 4 additions & 0 deletions packages/vitest/src/runtime/context.ts
Expand Up @@ -66,6 +66,10 @@ export function createTestContext(test: Test): TestContext {
return _expect != null
},
})
context.onTestFailed = (fn) => {
test.onFailed ||= []
test.onFailed.push(fn)
}

return context
}
Expand Down
19 changes: 18 additions & 1 deletion 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 = <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)
}
}
8 changes: 8 additions & 0 deletions packages/vitest/src/runtime/run.ts
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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') {
Expand All @@ -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()
Expand Down
41 changes: 20 additions & 21 deletions packages/vitest/src/runtime/suite.ts
Expand Up @@ -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('')
Expand All @@ -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)[] = []
Expand Down Expand Up @@ -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 changes: 11 additions & 0 deletions 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
}
8 changes: 8 additions & 0 deletions packages/vitest/src/types/tasks.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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 changes: 27 additions & 0 deletions 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,
},
]
`)
})

0 comments on commit 637c85d

Please sign in to comment.