diff --git a/docs/api/mock.md b/docs/api/mock.md
index 4b9e6aefb328..b1fbce45cd47 100644
--- a/docs/api/mock.md
+++ b/docs/api/mock.md
@@ -418,6 +418,40 @@ const myMockFn = vi
console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn())
```
+## mockThrow 4.1.0 {#mockthrow}
+
+```ts
+function mockThrow(value: unknown): Mock
+```
+
+Accepts a value that will be thrown whenever the mock function is called.
+
+```ts
+const myMockFn = vi.fn()
+myMockFn.mockThrow(new Error('error message'))
+myMockFn() // throws Error<'error message'>
+```
+
+## mockThrowOnce 4.1.0 {#mockthrowonce}
+
+```ts
+function mockThrowOnce(value: unknown): Mock
+```
+
+Accepts a value that will be thrown during the next function call. If chained, every consecutive call will throw the specified value.
+
+```ts
+const myMockFn = vi
+ .fn()
+ .mockReturnValue('default')
+ .mockThrowOnce(new Error('first call error'))
+ .mockThrowOnce('second call error')
+
+expect(() => myMockFn()).toThrow('first call error')
+expect(() => myMockFn()).toThrow('second call error')
+expect(myMockFn()).toEqual('default')
+```
+
## mock.calls
```ts
diff --git a/packages/runner/src/run.ts b/packages/runner/src/run.ts
index 5f0cd7c7bb2e..57688d9ad971 100644
--- a/packages/runner/src/run.ts
+++ b/packages/runner/src/run.ts
@@ -267,6 +267,8 @@ async function callAroundHooks(
return
}
+ const hookErrors: unknown[] = []
+
const createTimeoutPromise = (
timeout: number,
phase: 'setup' | 'teardown',
@@ -352,23 +354,13 @@ async function callAroundHooks(
setupTimeout.clear()
// Run inner hooks - don't time this against our teardown timeout
- let nextError: { value: unknown } | undefined
- try {
- await runNextHook(index + 1)
- }
- catch (value) {
- nextError = { value }
- }
+ await runNextHook(index + 1).catch(e => hookErrors.push(e))
// Start teardown timer after inner hooks complete - only times this hook's teardown code
teardownTimeout = createTimeoutPromise(timeout, 'teardown', stackTraceError)
// Signal that use() is returning (teardown phase starting)
resolveUseReturned()
-
- if (nextError) {
- throw nextError.value
- }
}
// Start setup timeout
@@ -422,7 +414,11 @@ async function callAroundHooks(
}
}
- await runNextHook(0)
+ await runNextHook(0).catch(e => hookErrors.push(e))
+
+ if (hookErrors.length > 0) {
+ throw hookErrors
+ }
}
async function callAroundAllHooks(
diff --git a/packages/spy/src/index.ts b/packages/spy/src/index.ts
index 0b20accc9803..80a1e48bd6b0 100644
--- a/packages/spy/src/index.ts
+++ b/packages/spy/src/index.ts
@@ -140,6 +140,20 @@ export function createMockInstance(options: MockInstanceOption = {}): Mock e
* console.log(myMockFn(), myMockFn(), myMockFn())
*/
mockReturnValueOnce(value: MockReturnType): this
+ /**
+ * Accepts a value that will be thrown whenever the mock function is called.
+ * @see https://vitest.dev/api/mock#mockthrow
+ * @example
+ * const myMockFn = vi.fn().mockThrow(new Error('error'))
+ * myMockFn() // throws 'error'
+ */
+ mockThrow(value: unknown): this
+ /**
+ * Accepts a value that will be thrown during the next function call. If chained, every consecutive call will throw the specified value.
+ * @example
+ * const myMockFn = vi
+ * .fn()
+ * .mockReturnValue('default')
+ * .mockThrowOnce(new Error('first call error'))
+ * .mockThrowOnce('second call error')
+ *
+ * expect(() => myMockFn()).toThrowError('first call error')
+ * expect(() => myMockFn()).toThrowError('second call error')
+ * expect(myMockFn()).toEqual('default')
+ */
+ mockThrowOnce(value: unknown): this
/**
* Accepts a value that will be resolved when the async function is called. TypeScript will only accept values that match the return type of the original function.
* @example
diff --git a/packages/vitest/src/integrations/env/loader.ts b/packages/vitest/src/integrations/env/loader.ts
index 3c761c8e7d29..cd06d913a81e 100644
--- a/packages/vitest/src/integrations/env/loader.ts
+++ b/packages/vitest/src/integrations/env/loader.ts
@@ -6,7 +6,7 @@ import { readFileSync } from 'node:fs'
import { isBuiltin } from 'node:module'
import { pathToFileURL } from 'node:url'
import { resolve } from 'pathe'
-import { ModuleRunner } from 'vite/module-runner'
+import { EvaluatedModules, ModuleRunner } from 'vite/module-runner'
import { VitestTransport } from '../../runtime/moduleRunner/moduleTransport'
import { environments } from './index'
@@ -24,6 +24,7 @@ export function createEnvironmentLoader(root: string, rpc: WorkerRPC): ModuleRun
if (!cachedLoader || cachedLoader.isClosed()) {
_loaders.delete(root)
+ const evaluatedModules = new EvaluatedModules()
const moduleRunner = new ModuleRunner({
hmr: false,
sourcemapInterceptor: 'prepareStackTrace',
@@ -46,7 +47,7 @@ export function createEnvironmentLoader(root: string, rpc: WorkerRPC): ModuleRun
async resolveId(id, importer) {
return rpc.resolve(id, importer, '__vitest__')
},
- }),
+ }, evaluatedModules, new WeakMap()),
})
_loaders.set(root, moduleRunner)
}
diff --git a/packages/vitest/src/node/logger.ts b/packages/vitest/src/node/logger.ts
index f15635e152b3..c96cb73dc921 100644
--- a/packages/vitest/src/node/logger.ts
+++ b/packages/vitest/src/node/logger.ts
@@ -301,7 +301,7 @@ export class Logger {
this.error(errorMessage)
errors.forEach((err) => {
this.printError(err, {
- fullStack: true,
+ fullStack: (err as any).name !== 'EnvironmentTeardownError',
type: (err as any).type || 'Unhandled Error',
})
})
diff --git a/packages/vitest/src/runtime/moduleRunner/moduleRunner.ts b/packages/vitest/src/runtime/moduleRunner/moduleRunner.ts
index 6f7eb54ddbd7..af3093baed71 100644
--- a/packages/vitest/src/runtime/moduleRunner/moduleRunner.ts
+++ b/packages/vitest/src/runtime/moduleRunner/moduleRunner.ts
@@ -49,11 +49,13 @@ export class VitestModuleRunner
public mocker: VitestMocker
public moduleExecutionInfo: ModuleExecutionInfo
private _otel: Traces
+ private _callstacks: WeakMap
constructor(private vitestOptions: VitestModuleRunnerOptions) {
const options = vitestOptions
- const transport = new VitestTransport(options.transport)
const evaluatedModules = options.evaluatedModules
+ const callstacks = new WeakMap()
+ const transport = new VitestTransport(options.transport, evaluatedModules, callstacks)
super(
{
transport,
@@ -64,6 +66,7 @@ export class VitestModuleRunner
},
options.evaluator,
)
+ this._callstacks = callstacks
this._otel = vitestOptions.traces || new Traces({ enabled: false })
this.moduleExecutionInfo = options.getWorkerState().moduleExecutionInfo
this.mocker = options.mocker || new VitestMocker(this, {
@@ -153,6 +156,9 @@ export class VitestModuleRunner
metadata?: SSRImportMetadata,
ignoreMock = false,
): Promise {
+ // Track for a better error message if dynamic import is not resolved properly
+ this._callstacks.set(mod, callstack)
+
if (ignoreMock) {
return this._cachedRequest(url, mod, callstack, metadata)
}
diff --git a/packages/vitest/src/runtime/moduleRunner/moduleTransport.ts b/packages/vitest/src/runtime/moduleRunner/moduleTransport.ts
index 90fe7dc728b8..75398656ffb2 100644
--- a/packages/vitest/src/runtime/moduleRunner/moduleTransport.ts
+++ b/packages/vitest/src/runtime/moduleRunner/moduleTransport.ts
@@ -1,5 +1,6 @@
-import type { FetchFunction, ModuleRunnerTransport } from 'vite/module-runner'
+import type { EvaluatedModuleNode, EvaluatedModules, FetchFunction, ModuleRunnerTransport } from 'vite/module-runner'
import type { ResolveFunctionResult } from '../../types/general'
+import { EnvironmentTeardownError } from '../utils'
export interface VitestTransportOptions {
fetchModule: FetchFunction
@@ -7,7 +8,11 @@ export interface VitestTransportOptions {
}
export class VitestTransport implements ModuleRunnerTransport {
- constructor(private options: VitestTransportOptions) {}
+ constructor(
+ private options: VitestTransportOptions,
+ private evaluatedModules: EvaluatedModules,
+ private callstacks: WeakMap,
+ ) {}
async invoke(event: any): Promise<{ result: any } | { error: any }> {
if (event.type !== 'custom') {
@@ -29,8 +34,24 @@ export class VitestTransport implements ModuleRunnerTransport {
const result = await this.options.fetchModule(...data as Parameters)
return { result }
}
- catch (error) {
- return { error }
+ catch (cause) {
+ if (cause instanceof EnvironmentTeardownError) {
+ const [id, importer] = data as Parameters
+ let message = `Cannot load '${id}'${importer ? ` imported from ${importer}` : ''} after the environment was torn down. `
+ + `This is not a bug in Vitest.`
+
+ const moduleNode = importer ? this.evaluatedModules.getModuleById(importer) : undefined
+ const callstack = moduleNode ? this.callstacks.get(moduleNode) : undefined
+ if (callstack) {
+ message += ` The last recorded callstack:\n- ${[...callstack, importer, id].reverse().join('\n- ')}`
+ }
+ const error = new EnvironmentTeardownError(message)
+ if (cause.stack) {
+ error.stack = cause.stack.replace(cause.message, error.message)
+ }
+ return { error }
+ }
+ return { error: cause }
}
}
}
diff --git a/packages/vitest/src/runtime/utils.ts b/packages/vitest/src/runtime/utils.ts
index fcf660bda1af..e19c86bb9fe5 100644
--- a/packages/vitest/src/runtime/utils.ts
+++ b/packages/vitest/src/runtime/utils.ts
@@ -4,6 +4,10 @@ import { getSafeTimers } from '@vitest/utils/timers'
const NAME_WORKER_STATE = '__vitest_worker__'
+export class EnvironmentTeardownError extends Error {
+ name = 'EnvironmentTeardownError'
+}
+
export function getWorkerState(): WorkerGlobalState {
// @ts-expect-error untyped global
const workerState = globalThis[NAME_WORKER_STATE]
diff --git a/packages/vitest/src/runtime/worker.ts b/packages/vitest/src/runtime/worker.ts
index 62888bf6cf92..b08b8e6dc20a 100644
--- a/packages/vitest/src/runtime/worker.ts
+++ b/packages/vitest/src/runtime/worker.ts
@@ -6,6 +6,7 @@ import { setupInspect } from './inspector'
import * as listeners from './listeners'
import { VitestEvaluatedModules } from './moduleRunner/evaluatedModules'
import { onCancel, rpcDone } from './rpc'
+import { EnvironmentTeardownError } from './utils'
const resolvingModules = new Set()
@@ -21,7 +22,7 @@ async function execute(method: 'run' | 'collect', ctx: ContextRPC, worker: Vites
// do not close the RPC channel so that we can get the error messages sent to the main thread
cleanups.push(async () => {
await Promise.all(rpc.$rejectPendingCalls(({ method, reject }) => {
- reject(new Error(`[vitest-worker]: Closing rpc while "${method}" was pending`))
+ reject(new EnvironmentTeardownError(`[vitest-worker]: Closing rpc while "${method}" was pending`))
}))
})
diff --git a/test/cli/test/around-each.test.ts b/test/cli/test/around-each.test.ts
index 0c52e77e4902..e57d8e4748eb 100644
--- a/test/cli/test/around-each.test.ts
+++ b/test/cli/test/around-each.test.ts
@@ -920,7 +920,8 @@ test('multiple aroundEach hooks with different timeouts', async () => {
expect(extractLogs(stdout)).toMatchInlineSnapshot(`
">> outer setup
- >> inner setup start"
+ >> inner setup start
+ >> outer teardown"
`)
expect(stderr).toMatchInlineSnapshot(`
"
@@ -982,7 +983,8 @@ test('multiple aroundEach hooks where inner teardown times out', async () => {
">> outer setup
>> inner setup
>> test
- >> inner teardown start"
+ >> inner teardown start
+ >> outer teardown"
`)
expect(stderr).toMatchInlineSnapshot(`
"
@@ -1893,7 +1895,7 @@ test('tests are skipped when aroundAll setup fails', async () => {
`)
})
-test('aroundEach teardown timeout works when runTest error is caught', async () => {
+test('aroundEach teardown timeout works when inner fails', async () => {
const { stderr, errorTree } = await runInlineTests({
'caught-inner-error-timeout.test.ts': `
import { aroundEach, afterAll, describe, expect, test } from 'vitest'
@@ -1901,7 +1903,7 @@ test('aroundEach teardown timeout works when runTest error is caught', async ()
let errorCaught = false
afterAll(() => {
- expect(errorCaught).toBe(true)
+ expect(errorCaught).toBe(false)
})
describe('suite', () => {
@@ -1932,6 +1934,18 @@ test('aroundEach teardown timeout works when runTest error is caught', async ()
"
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
+ FAIL caught-inner-error-timeout.test.ts > suite > test
+ Error: inner aroundEach teardown failure
+ ❯ caught-inner-error-timeout.test.ts:24:17
+ 22| aroundEach(async (runTest) => {
+ 23| await runTest()
+ 24| throw new Error('inner aroundEach teardown failure')
+ | ^
+ 25| })
+ 26|
+
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯
+
FAIL caught-inner-error-timeout.test.ts > suite > test
AroundHookTeardownError: The teardown phase of "aroundEach" hook timed out after 50ms.
❯ caught-inner-error-timeout.test.ts:11:9
@@ -1942,7 +1956,7 @@ test('aroundEach teardown timeout works when runTest error is caught', async ()
12| try {
13| await runTest()
- ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯
"
`)
@@ -1951,6 +1965,7 @@ test('aroundEach teardown timeout works when runTest error is caught', async ()
"caught-inner-error-timeout.test.ts": {
"suite": {
"test": [
+ "inner aroundEach teardown failure",
"The teardown phase of "aroundEach" hook timed out after 50ms.",
],
},
@@ -1959,7 +1974,7 @@ test('aroundEach teardown timeout works when runTest error is caught', async ()
`)
})
-test('aroundAll teardown timeout works when runTest error is caught', async () => {
+test('aroundAll teardown timeout works when inner fails', async () => {
const { stderr, errorTree } = await runInlineTests({
'caught-inner-error-timeout.test.ts': `
import { aroundAll, afterAll, describe, expect, test } from 'vitest'
@@ -1967,7 +1982,7 @@ test('aroundAll teardown timeout works when runTest error is caught', async () =
let errorCaught = false
afterAll(() => {
- expect(errorCaught).toBe(true)
+ expect(errorCaught).toBe(false)
})
describe('suite', () => {
@@ -1998,6 +2013,18 @@ test('aroundAll teardown timeout works when runTest error is caught', async () =
"
⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯
+ FAIL caught-inner-error-timeout.test.ts > suite
+ Error: inner aroundAll teardown failure
+ ❯ caught-inner-error-timeout.test.ts:24:17
+ 22| aroundAll(async (runTest) => {
+ 23| await runTest()
+ 24| throw new Error('inner aroundAll teardown failure')
+ | ^
+ 25| })
+ 26|
+
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯
+
FAIL caught-inner-error-timeout.test.ts > suite
AroundHookTeardownError: The teardown phase of "aroundAll" hook timed out after 50ms.
❯ caught-inner-error-timeout.test.ts:11:9
@@ -2008,7 +2035,7 @@ test('aroundAll teardown timeout works when runTest error is caught', async () =
12| try {
13| await runTest()
- ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯
"
`)
@@ -2160,3 +2187,441 @@ test('aroundAll aborts late runSuite after setup timeout', async () => {
}
`)
})
+
+test('nested aroundEach setup error is not propagated to outer runTest catch', async () => {
+ const { stdout, stderr, errorTree } = await runInlineTests({
+ 'nested-around-each-setup-error.test.ts': `
+ import { aroundEach, test } from 'vitest'
+
+ aroundEach(async (runTest) => {
+ console.log('>> outer setup')
+ try {
+ await runTest()
+ }
+ catch (error) {
+ console.log('>> outer caught', String(error))
+ }
+ console.log('>> outer teardown')
+ })
+
+ aroundEach(async (_runTest) => {
+ console.log('>> inner setup')
+ throw new Error('inner aroundEach setup error')
+ })
+
+ test('repro', () => {
+ console.log('>> test body')
+ })
+ `,
+ })
+
+ expect(stderr).toMatchInlineSnapshot(`
+ "
+ ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
+
+ FAIL nested-around-each-setup-error.test.ts > repro
+ Error: inner aroundEach setup error
+ ❯ nested-around-each-setup-error.test.ts:17:15
+ 15| aroundEach(async (_runTest) => {
+ 16| console.log('>> inner setup')
+ 17| throw new Error('inner aroundEach setup error')
+ | ^
+ 18| })
+ 19|
+ ❯ nested-around-each-setup-error.test.ts:7:17
+
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
+
+ "
+ `)
+ expect(extractLogs(stdout)).toMatchInlineSnapshot(`
+ ">> outer setup
+ >> inner setup
+ >> outer teardown"
+ `)
+ expect(errorTree()).toMatchInlineSnapshot(`
+ {
+ "nested-around-each-setup-error.test.ts": {
+ "repro": [
+ "inner aroundEach setup error",
+ ],
+ },
+ }
+ `)
+})
+
+test('nested aroundEach teardown error is not propagated to outer runTest catch', async () => {
+ const { stdout, stderr, errorTree } = await runInlineTests({
+ 'nested-around-each-teardown-error.test.ts': `
+ import { aroundEach, test } from 'vitest'
+
+ aroundEach(async (runTest) => {
+ console.log('>> outer setup')
+ try {
+ await runTest()
+ }
+ catch (error) {
+ console.log('>> outer caught', String(error))
+ }
+ console.log('>> outer teardown')
+ })
+
+ aroundEach(async (runTest) => {
+ console.log('>> inner setup')
+ await runTest()
+ console.log('>> inner teardown')
+ throw new Error('inner aroundEach teardown error')
+ })
+
+ test('repro', () => {
+ console.log('>> test body')
+ })
+ `,
+ })
+
+ expect(stderr).toMatchInlineSnapshot(`
+ "
+ ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
+
+ FAIL nested-around-each-teardown-error.test.ts > repro
+ Error: inner aroundEach teardown error
+ ❯ nested-around-each-teardown-error.test.ts:19:15
+ 17| await runTest()
+ 18| console.log('>> inner teardown')
+ 19| throw new Error('inner aroundEach teardown error')
+ | ^
+ 20| })
+ 21|
+
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
+
+ "
+ `)
+ expect(extractLogs(stdout)).toMatchInlineSnapshot(`
+ ">> outer setup
+ >> inner setup
+ >> test body
+ >> inner teardown
+ >> outer teardown"
+ `)
+ expect(errorTree()).toMatchInlineSnapshot(`
+ {
+ "nested-around-each-teardown-error.test.ts": {
+ "repro": [
+ "inner aroundEach teardown error",
+ ],
+ },
+ }
+ `)
+})
+
+test('nested aroundAll setup error is not propagated to outer runSuite catch', async () => {
+ const { stdout, stderr, errorTree } = await runInlineTests({
+ 'nested-around-all-setup-error.test.ts': `
+ import { aroundAll, test } from 'vitest'
+
+ aroundAll(async (runSuite) => {
+ console.log('>> outer setup')
+ try {
+ await runSuite()
+ }
+ catch (error) {
+ console.log('>> outer caught', String(error))
+ }
+ console.log('>> outer teardown')
+ })
+
+ aroundAll(async (_runSuite) => {
+ console.log('>> inner setup')
+ throw new Error('inner aroundAll setup error')
+ })
+
+ test('repro', () => {
+ console.log('>> test body')
+ })
+ `,
+ })
+
+ expect(stderr).toMatchInlineSnapshot(`
+ "
+ ⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯
+
+ FAIL nested-around-all-setup-error.test.ts [ nested-around-all-setup-error.test.ts ]
+ Error: inner aroundAll setup error
+ ❯ nested-around-all-setup-error.test.ts:17:15
+ 15| aroundAll(async (_runSuite) => {
+ 16| console.log('>> inner setup')
+ 17| throw new Error('inner aroundAll setup error')
+ | ^
+ 18| })
+ 19|
+ ❯ nested-around-all-setup-error.test.ts:7:17
+
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
+
+ "
+ `)
+ expect(extractLogs(stdout)).toMatchInlineSnapshot(`
+ ">> outer setup
+ >> inner setup
+ >> outer teardown"
+ `)
+ expect(errorTree()).toMatchInlineSnapshot(`
+ {
+ "nested-around-all-setup-error.test.ts": {
+ "repro": "skipped",
+ },
+ }
+ `)
+})
+
+test('nested aroundAll teardown error is not propagated to outer runSuite catch', async () => {
+ const { stdout, stderr, errorTree } = await runInlineTests({
+ 'nested-around-all-teardown-error.test.ts': `
+ import { aroundAll, test } from 'vitest'
+
+ aroundAll(async (runSuite) => {
+ console.log('>> outer setup')
+ try {
+ await runSuite()
+ }
+ catch (error) {
+ console.log('>> outer caught', String(error))
+ }
+ console.log('>> outer teardown')
+ })
+
+ aroundAll(async (runSuite) => {
+ console.log('>> inner setup')
+ await runSuite()
+ console.log('>> inner teardown')
+ throw new Error('inner aroundAll teardown error')
+ })
+
+ test('repro', () => {
+ console.log('>> test body')
+ })
+ `,
+ })
+
+ expect(stderr).toMatchInlineSnapshot(`
+ "
+ ⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯
+
+ FAIL nested-around-all-teardown-error.test.ts [ nested-around-all-teardown-error.test.ts ]
+ Error: inner aroundAll teardown error
+ ❯ nested-around-all-teardown-error.test.ts:19:15
+ 17| await runSuite()
+ 18| console.log('>> inner teardown')
+ 19| throw new Error('inner aroundAll teardown error')
+ | ^
+ 20| })
+ 21|
+
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
+
+ "
+ `)
+ expect(extractLogs(stdout)).toMatchInlineSnapshot(`
+ ">> outer setup
+ >> inner setup
+ >> test body
+ >> inner teardown
+ >> outer teardown"
+ `)
+ expect(errorTree()).toMatchInlineSnapshot(`
+ {
+ "nested-around-all-teardown-error.test.ts": {
+ "repro": "passed",
+ },
+ }
+ `)
+})
+
+test('three nested aroundEach teardown errors are all reported', async () => {
+ const { stdout, stderr, errorTree } = await runInlineTests({
+ 'triple-around-each-teardown-errors.test.ts': `
+ import { aroundEach, test } from 'vitest'
+
+ aroundEach(async (runTest) => {
+ console.log('>> outer setup')
+ await runTest()
+ console.log('>> outer teardown')
+ throw new Error('outer aroundEach teardown error')
+ })
+
+ aroundEach(async (runTest) => {
+ console.log('>> middle setup')
+ await runTest()
+ console.log('>> middle teardown')
+ throw new Error('middle aroundEach teardown error')
+ })
+
+ aroundEach(async (runTest) => {
+ console.log('>> inner setup')
+ await runTest()
+ console.log('>> inner teardown')
+ throw new Error('inner aroundEach teardown error')
+ })
+
+ test('repro', () => {
+ console.log('>> test body')
+ })
+ `,
+ })
+
+ expect(stderr).toMatchInlineSnapshot(`
+ "
+ ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
+
+ FAIL triple-around-each-teardown-errors.test.ts > repro
+ Error: inner aroundEach teardown error
+ ❯ triple-around-each-teardown-errors.test.ts:22:15
+ 20| await runTest()
+ 21| console.log('>> inner teardown')
+ 22| throw new Error('inner aroundEach teardown error')
+ | ^
+ 23| })
+ 24|
+
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/3]⎯
+
+ FAIL triple-around-each-teardown-errors.test.ts > repro
+ Error: middle aroundEach teardown error
+ ❯ triple-around-each-teardown-errors.test.ts:15:15
+ 13| await runTest()
+ 14| console.log('>> middle teardown')
+ 15| throw new Error('middle aroundEach teardown error')
+ | ^
+ 16| })
+ 17|
+
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/3]⎯
+
+ FAIL triple-around-each-teardown-errors.test.ts > repro
+ Error: outer aroundEach teardown error
+ ❯ triple-around-each-teardown-errors.test.ts:8:15
+ 6| await runTest()
+ 7| console.log('>> outer teardown')
+ 8| throw new Error('outer aroundEach teardown error')
+ | ^
+ 9| })
+ 10|
+
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/3]⎯
+
+ "
+ `)
+ expect(extractLogs(stdout)).toMatchInlineSnapshot(`
+ ">> outer setup
+ >> middle setup
+ >> inner setup
+ >> test body
+ >> inner teardown
+ >> middle teardown
+ >> outer teardown"
+ `)
+ expect(errorTree()).toMatchInlineSnapshot(`
+ {
+ "triple-around-each-teardown-errors.test.ts": {
+ "repro": [
+ "inner aroundEach teardown error",
+ "middle aroundEach teardown error",
+ "outer aroundEach teardown error",
+ ],
+ },
+ }
+ `)
+})
+
+test('three nested aroundAll teardown errors are all reported', async () => {
+ const { stdout, stderr, errorTree } = await runInlineTests({
+ 'triple-around-all-teardown-errors.test.ts': `
+ import { aroundAll, test } from 'vitest'
+
+ aroundAll(async (runSuite) => {
+ console.log('>> outer setup')
+ await runSuite()
+ console.log('>> outer teardown')
+ throw new Error('outer aroundAll teardown error')
+ })
+
+ aroundAll(async (runSuite) => {
+ console.log('>> middle setup')
+ await runSuite()
+ console.log('>> middle teardown')
+ throw new Error('middle aroundAll teardown error')
+ })
+
+ aroundAll(async (runSuite) => {
+ console.log('>> inner setup')
+ await runSuite()
+ console.log('>> inner teardown')
+ throw new Error('inner aroundAll teardown error')
+ })
+
+ test('repro', () => {
+ console.log('>> test body')
+ })
+ `,
+ })
+
+ expect(stderr).toMatchInlineSnapshot(`
+ "
+ ⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯
+
+ FAIL triple-around-all-teardown-errors.test.ts [ triple-around-all-teardown-errors.test.ts ]
+ Error: inner aroundAll teardown error
+ ❯ triple-around-all-teardown-errors.test.ts:22:15
+ 20| await runSuite()
+ 21| console.log('>> inner teardown')
+ 22| throw new Error('inner aroundAll teardown error')
+ | ^
+ 23| })
+ 24|
+
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/3]⎯
+
+ FAIL triple-around-all-teardown-errors.test.ts [ triple-around-all-teardown-errors.test.ts ]
+ Error: middle aroundAll teardown error
+ ❯ triple-around-all-teardown-errors.test.ts:15:15
+ 13| await runSuite()
+ 14| console.log('>> middle teardown')
+ 15| throw new Error('middle aroundAll teardown error')
+ | ^
+ 16| })
+ 17|
+
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/3]⎯
+
+ FAIL triple-around-all-teardown-errors.test.ts [ triple-around-all-teardown-errors.test.ts ]
+ Error: outer aroundAll teardown error
+ ❯ triple-around-all-teardown-errors.test.ts:8:15
+ 6| await runSuite()
+ 7| console.log('>> outer teardown')
+ 8| throw new Error('outer aroundAll teardown error')
+ | ^
+ 9| })
+ 10|
+
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/3]⎯
+
+ "
+ `)
+ expect(extractLogs(stdout)).toMatchInlineSnapshot(`
+ ">> outer setup
+ >> middle setup
+ >> inner setup
+ >> test body
+ >> inner teardown
+ >> middle teardown
+ >> outer teardown"
+ `)
+ expect(errorTree()).toMatchInlineSnapshot(`
+ {
+ "triple-around-all-teardown-errors.test.ts": {
+ "repro": "passed",
+ },
+ }
+ `)
+})
diff --git a/test/core/test/mocking/vi-fn.test.ts b/test/core/test/mocking/vi-fn.test.ts
index f3269ee3d637..71e3d5e8f4af 100644
--- a/test/core/test/mocking/vi-fn.test.ts
+++ b/test/core/test/mocking/vi-fn.test.ts
@@ -519,6 +519,88 @@ describe('vi.fn() implementations', () => {
expect(mock()).toBe(undefined)
})
+ test('vi.fn() with mockThrow', async () => {
+ const mock = vi.fn()
+ mock.mockThrow(new Error('error'))
+ expect(() => mock()).toThrow('error')
+ expect(() => mock()).toThrow('error')
+ expect(() => mock()).toThrow('error')
+ mock.mockReset()
+ expect(mock()).toBe(undefined)
+ })
+
+ test('vi.fn(class) with mockThrow', async () => {
+ const Mock = vi.fn(class {})
+ Mock.mockThrow(new Error('error'))
+ expect(() => new Mock()).toThrow('error')
+ expect(() => new Mock()).toThrow('error')
+ expect(() => new Mock()).toThrow('error')
+ Mock.mockReset()
+ expect(new Mock()).toBeInstanceOf(Mock)
+ })
+
+ test('vi.fn() with mockThrow overriding original mock', async () => {
+ const mock = vi.fn(() => 42)
+ mock.mockThrow(new Error('error'))
+ expect(() => mock()).toThrow('error')
+ expect(() => mock()).toThrow('error')
+ expect(() => mock()).toThrow('error')
+ mock.mockReset()
+ expect(mock()).toBe(42)
+ })
+
+ test('vi.fn() with mockThrow overriding another mock', async () => {
+ const mock = vi.fn().mockImplementation(() => 42)
+ mock.mockThrow(new Error('error'))
+ expect(() => mock()).toThrow('error')
+ expect(() => mock()).toThrow('error')
+ expect(() => mock()).toThrow('error')
+ mock.mockReset()
+ expect(mock()).toBe(undefined)
+ })
+
+ test('vi.fn() with mockThrowOnce', async () => {
+ const mock = vi.fn()
+ mock.mockThrowOnce(new Error('error'))
+ expect(() => mock()).toThrow('error')
+ expect(mock()).toBe(undefined)
+ expect(mock()).toBe(undefined)
+ mock.mockThrowOnce(new Error('error'))
+ mock.mockReset()
+ expect(mock()).toBe(undefined)
+ })
+
+ test('vi.fn(class) with mockThrowOnce', async () => {
+ const Mock = vi.fn(class {})
+ Mock.mockThrowOnce(new Error('error'))
+ expect(() => new Mock()).toThrow('error')
+ expect(new Mock()).toBeInstanceOf(Mock)
+ expect(new Mock()).toBeInstanceOf(Mock)
+ Mock.mockThrowOnce(new Error('error'))
+ Mock.mockReset()
+ expect(new Mock()).toBeInstanceOf(Mock)
+ })
+
+ test('vi.fn() with mockThrowOnce overriding original mock', async () => {
+ const mock = vi.fn(() => 42)
+ mock.mockThrowOnce(new Error('error'))
+ expect(() => mock()).toThrow('error')
+ expect(mock()).toBe(42)
+ expect(mock()).toBe(42)
+ mock.mockReset()
+ expect(mock()).toBe(42)
+ })
+
+ test('vi.fn() with mockThrowOnce overriding another mock', async () => {
+ const mock = vi.fn().mockImplementation(() => 42)
+ mock.mockThrowOnce(new Error('error'))
+ expect(() => mock()).toThrow('error')
+ expect(mock()).toBe(42)
+ expect(mock()).toBe(42)
+ mock.mockReset()
+ expect(mock()).toBe(undefined)
+ })
+
test('vi.fn() with mockResolvedValue', async () => {
const mock = vi.fn()
mock.mockResolvedValue(42)