diff --git a/packages/runner/src/run.ts b/packages/runner/src/run.ts index 59c1a6251..5f0cd7c7b 100644 --- a/packages/runner/src/run.ts +++ b/packages/runner/src/run.ts @@ -271,12 +271,14 @@ async function callAroundHooks( timeout: number, phase: 'setup' | 'teardown', stackTraceError: Error | undefined, - ): { promise: Promise; clear: () => void } => { + ): { promise: Promise; isTimedOut: () => boolean; clear: () => void } => { let timer: ReturnType | undefined + let timedout = false const promise = new Promise((_, reject) => { if (timeout > 0 && timeout !== Number.POSITIVE_INFINITY) { timer = setTimeout(() => { + timedout = true const error = makeAroundHookTimeoutError(hookName, phase, timeout, stackTraceError) onTimeout?.(error) reject(error) @@ -292,7 +294,7 @@ async function callAroundHooks( } } - return { promise, clear } + return { promise, clear, isTimedOut: () => timedout } } const runNextHook = async (index: number): Promise => { @@ -305,8 +307,8 @@ async function callAroundHooks( const stackTraceError = getAroundHookStackTrace(hook) let useCalled = false - let setupTimeout: { promise: Promise; clear: () => void } - let teardownTimeout: { promise: Promise; clear: () => void } | undefined + let setupTimeout: ReturnType + let teardownTimeout: ReturnType | undefined // Promise that resolves when use() is called (setup phase complete) let resolveUseCalled!: () => void @@ -329,6 +331,14 @@ async function callAroundHooks( }) const use = async () => { + // shouldn't continue to next (runTest/Suite or inner aroundEach/All) when aroundEach/All setup timed out. + if (setupTimeout.isTimedOut()) { + // we can throw any error to bail out. + // this error is not seen by end users since `runNextHook` already rejected with timeout error + // and this error is caught by `rejectHookComplete`. + throw new Error('__VITEST_INTERNAL_AROUND_HOOK_ABORT__') + } + if (useCalled) { throw new AroundHookMultipleCallsError( `The \`${callbackName}\` callback was called multiple times in the \`${hookName}\` hook. ` diff --git a/test/cli/test/around-each.test.ts b/test/cli/test/around-each.test.ts index ad6c695ec..0c52e77e4 100644 --- a/test/cli/test/around-each.test.ts +++ b/test/cli/test/around-each.test.ts @@ -2022,3 +2022,141 @@ test('aroundAll teardown timeout works when runTest error is caught', async () = } `) }) + +test('aroundEach aborts late runTest after setup timeout', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'late-run-test-after-timeout.test.ts': ` + import { afterAll, aroundEach, test } from 'vitest' + + afterAll(async () => { + console.log('>> afterAll 0ms') + await new Promise(r => setTimeout(r, 200)) + console.log('>> afterAll 200ms') + }) + + aroundEach(async (runTest) => { + console.log(">> outer aroundEach setup 0ms") + await new Promise(r => setTimeout(r, 100)) + // this is still executed after timeout error + console.log(">> outer aroundEach setup 100ms") + // but this shouldn't continue to inner aroundEach or test + await runTest() + console.log(">> outer aroundEach teardown") + }, 10) + + aroundEach(async (runTest) => { + console.log('>> inner aroundEach setup') + await runTest() + console.log('>> inner aroundEach teardown') + }) + + test('basic', () => { + console.log('>> test') + }) + `, + }) + + expect(stderr).toMatchInlineSnapshot(` + " + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL late-run-test-after-timeout.test.ts > basic + AroundHookSetupError: The setup phase of "aroundEach" hook timed out after 10ms. + ❯ late-run-test-after-timeout.test.ts:10:7 + 8| }) + 9| + 10| aroundEach(async (runTest) => { + | ^ + 11| console.log(">> outer aroundEach setup 0ms") + 12| await new Promise(r => setTimeout(r, 100)) + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ + + " + `) + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> outer aroundEach setup 0ms + >> afterAll 0ms + >> outer aroundEach setup 100ms + >> afterAll 200ms" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "late-run-test-after-timeout.test.ts": { + "basic": [ + "The setup phase of "aroundEach" hook timed out after 10ms.", + ], + }, + } + `) +}) + +test('aroundAll aborts late runSuite after setup timeout', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'late-run-suite-after-timeout.test.ts': ` + import { afterAll, aroundAll, describe, test } from 'vitest' + + afterAll(async () => { + console.log('>> afterAll 0ms') + await new Promise(r => setTimeout(r, 200)) + console.log('>> afterAll 200ms') + }) + + describe('timed out suite', () => { + aroundAll(async (runSuite) => { + console.log('>> outer aroundAll setup 0ms') + await new Promise(r => setTimeout(r, 100)) + // this is still executed after timeout error + console.log('>> outer aroundAll setup 100ms') + // but this should not continue to inner aroundAll or tests + await runSuite() + console.log('>> outer aroundAll teardown') + }, 10) + + aroundAll(async (runSuite) => { + console.log('>> inner aroundAll setup') + await runSuite() + console.log('>> inner aroundAll teardown') + }) + + test('basic', () => { + console.log('>> test') + }) + }) + `, + }) + + expect(stderr).toMatchInlineSnapshot(` + " + ⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL late-run-suite-after-timeout.test.ts > timed out suite + AroundHookSetupError: The setup phase of "aroundAll" hook timed out after 10ms. + ❯ late-run-suite-after-timeout.test.ts:11:9 + 9| + 10| describe('timed out suite', () => { + 11| aroundAll(async (runSuite) => { + | ^ + 12| console.log('>> outer aroundAll setup 0ms') + 13| await new Promise(r => setTimeout(r, 100)) + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ + + " + `) + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> outer aroundAll setup 0ms + >> afterAll 0ms + >> outer aroundAll setup 100ms + >> afterAll 200ms" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "late-run-suite-after-timeout.test.ts": { + "timed out suite": { + "basic": "skipped", + }, + }, + } + `) +})