diff --git a/docs/guide/test-context.md b/docs/guide/test-context.md index ba2c3be45..6e8ba9fdf 100644 --- a/docs/guide/test-context.md +++ b/docs/guide/test-context.md @@ -79,6 +79,21 @@ it('math is hard', ({ skip, mind }) => { }) ``` +#### `context.signal` 3.2.0 {#context-signal} + +An [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that can be aborted by Vitest. The signal is aborted in these situations: + +- Test times out +- User manually cancelled the test run with Ctrl+C +- [`vitest.cancelCurrentRun`](/advanced/api/vitest#cancelcurrentrun) was called programmatically +- Another test failed in parallel and the [`bail`](/config/#bail) flag is set + +```ts +it('stop request when test times out', async ({ signal }) => { + await fetch('/resource', { signal }) +}, 2000) +``` + #### `onTestFailed` The [`onTestFailed`](/api/#ontestfailed) hook bound to the current test. This API is useful if you are running tests concurrently and need to have a special handling only for this specific test. diff --git a/packages/browser/src/client/tester/runner.ts b/packages/browser/src/client/tester/runner.ts index bc42eeecf..a877aad77 100644 --- a/packages/browser/src/client/tester/runner.ts +++ b/packages/browser/src/client/tester/runner.ts @@ -62,8 +62,8 @@ export function createBrowserRunner( const currentFailures = 1 + previousFailures if (currentFailures >= this.config.bail) { - rpc().onCancel('test-failure') - this.onCancel('test-failure') + rpc().cancelCurrentRun('test-failure') + this.cancel('test-failure') } } } @@ -81,8 +81,8 @@ export function createBrowserRunner( } } - onCancel = (reason: CancelReason) => { - super.onCancel?.(reason) + cancel = (reason: CancelReason) => { + super.cancel?.(reason) globalChannel.postMessage({ type: 'cancel', reason }) } @@ -196,7 +196,7 @@ export async function initiateRunner( cachedRunner = runner onCancel.then((reason) => { - runner.onCancel?.(reason) + runner.cancel?.(reason) }) const [diffOptions] = await Promise.all([ diff --git a/packages/browser/src/node/rpc.ts b/packages/browser/src/node/rpc.ts index ec856c7b9..76876d7cc 100644 --- a/packages/browser/src/node/rpc.ts +++ b/packages/browser/src/node/rpc.ts @@ -202,7 +202,7 @@ export function setupBrowserRpc(globalServer: ParentBrowserProject, defaultMocke const mod = globalServer.vite.moduleGraph.getModuleById(id) return mod?.transformResult?.map }, - onCancel(reason) { + cancelCurrentRun(reason) { vitest.cancelCurrentRun(reason) }, async resolveId(id, importer) { diff --git a/packages/browser/src/node/types.ts b/packages/browser/src/node/types.ts index 96d373210..b4361ff2a 100644 --- a/packages/browser/src/node/types.ts +++ b/packages/browser/src/node/types.ts @@ -21,7 +21,7 @@ export interface WebSocketBrowserHandlers { onCollected: (method: TestExecutionMethod, files: RunnerTestFile[]) => Promise onTaskUpdate: (method: TestExecutionMethod, packs: TaskResultPack[], events: TaskEventPack[]) => void onAfterSuiteRun: (meta: AfterSuiteRunMeta) => void - onCancel: (reason: CancelReason) => void + cancelCurrentRun: (reason: CancelReason) => void getCountOfFailedTests: () => number readSnapshotFile: (id: string) => Promise saveSnapshotFile: (id: string, content: string) => Promise diff --git a/packages/runner/src/context.ts b/packages/runner/src/context.ts index a15305a9f..781050945 100644 --- a/packages/runner/src/context.ts +++ b/packages/runner/src/context.ts @@ -5,6 +5,7 @@ import type { SuiteCollector, Test, TestContext, + WriteableTestContext, } from './types/tasks' import { getSafeTimers } from '@vitest/utils' import { PendingError } from './errors' @@ -36,6 +37,7 @@ export function withTimeout any>( timeout: number, isHook = false, stackTraceError?: Error, + onTimeout?: (args: T extends (...args: infer A) => any ? A : never, error: Error) => void, ): T { if (timeout <= 0 || timeout === Number.POSITIVE_INFINITY) { return fn @@ -58,7 +60,9 @@ export function withTimeout any>( timer.unref?.() function rejectTimeoutError() { - reject_(makeTimeoutError(isHook, timeout, stackTraceError)) + const error = makeTimeoutError(isHook, timeout, stackTraceError) + onTimeout?.(args, error) + reject_(error) } function resolve(result: unknown) { @@ -102,14 +106,35 @@ export function withTimeout any>( }) as T } +const abortControllers = new WeakMap() + +export function abortIfTimeout([context]: [TestContext?], error: Error): void { + if (context) { + abortContextSignal(context, error) + } +} + +export function abortContextSignal(context: TestContext, error: Error): void { + const abortController = abortControllers.get(context) + abortController?.abort(error) +} + export function createTestContext( test: Test, runner: VitestRunner, ): TestContext { const context = function () { throw new Error('done() callback is deprecated, use promise instead') - } as unknown as TestContext + } as unknown as WriteableTestContext + + let abortController = abortControllers.get(context) + + if (!abortController) { + abortController = new AbortController() + abortControllers.set(context, abortController) + } + context.signal = abortController.signal context.task = test context.skip = (condition?: boolean | string, note?: string): never => { @@ -129,14 +154,26 @@ export function createTestContext( context.onTestFailed = (handler, timeout) => { test.onFailed ||= [] test.onFailed.push( - withTimeout(handler, timeout ?? runner.config.hookTimeout, true, new Error('STACK_TRACE_ERROR')), + withTimeout( + handler, + timeout ?? runner.config.hookTimeout, + true, + new Error('STACK_TRACE_ERROR'), + (_, error) => abortController.abort(error), + ), ) } context.onTestFinished = (handler, timeout) => { test.onFinished ||= [] test.onFinished.push( - withTimeout(handler, timeout ?? runner.config.hookTimeout, true, new Error('STACK_TRACE_ERROR')), + withTimeout( + handler, + timeout ?? runner.config.hookTimeout, + true, + new Error('STACK_TRACE_ERROR'), + (_, error) => abortController.abort(error), + ), ) } diff --git a/packages/runner/src/errors.ts b/packages/runner/src/errors.ts index 2d3090a7f..53d2f551c 100644 --- a/packages/runner/src/errors.ts +++ b/packages/runner/src/errors.ts @@ -1,3 +1,4 @@ +import type { CancelReason } from './types/runner' import type { TaskBase } from './types/tasks' export class PendingError extends Error { @@ -9,3 +10,12 @@ export class PendingError extends Error { this.taskId = task.id } } + +export class TestRunAbortError extends Error { + public name = 'TestRunAbortError' + public reason: CancelReason + constructor(message: string, reason: CancelReason) { + super(message) + this.reason = reason + } +} diff --git a/packages/runner/src/hooks.ts b/packages/runner/src/hooks.ts index 974024090..55e3eb3fe 100644 --- a/packages/runner/src/hooks.ts +++ b/packages/runner/src/hooks.ts @@ -7,9 +7,10 @@ import type { OnTestFinishedHandler, TaskHook, TaskPopulated, + TestContext, } from './types/tasks' import { assertTypes } from '@vitest/utils' -import { withTimeout } from './context' +import { abortContextSignal, abortIfTimeout, withTimeout } from './context' import { withFixtures } from './fixture' import { getCurrentSuite, getRunner } from './suite' import { getCurrentTest } from './test-state' @@ -21,7 +22,7 @@ function getDefaultHookTimeout() { const CLEANUP_TIMEOUT_KEY = Symbol.for('VITEST_CLEANUP_TIMEOUT') const CLEANUP_STACK_TRACE_KEY = Symbol.for('VITEST_CLEANUP_STACK_TRACE') -export function getBeforeHookCleanupCallback(hook: Function, result: any): Function | undefined { +export function getBeforeHookCleanupCallback(hook: Function, result: any, context?: TestContext): Function | undefined { if (typeof result === 'function') { const timeout = CLEANUP_TIMEOUT_KEY in hook && typeof hook[CLEANUP_TIMEOUT_KEY] === 'number' @@ -31,7 +32,17 @@ export function getBeforeHookCleanupCallback(hook: Function, result: any): Funct = CLEANUP_STACK_TRACE_KEY in hook && hook[CLEANUP_STACK_TRACE_KEY] instanceof Error ? hook[CLEANUP_STACK_TRACE_KEY] : undefined - return withTimeout(result, timeout, true, stackTraceError) + return withTimeout( + result, + timeout, + true, + stackTraceError, + (_, error) => { + if (context) { + abortContextSignal(context, error) + } + }, + ) } } @@ -136,6 +147,7 @@ export function beforeEach( timeout ?? getDefaultHookTimeout(), true, stackTraceError, + abortIfTimeout, ), { [CLEANUP_TIMEOUT_KEY]: timeout, @@ -174,6 +186,7 @@ export function afterEach( timeout ?? getDefaultHookTimeout(), true, new Error('STACK_TRACE_ERROR'), + abortIfTimeout, ), ) } @@ -206,6 +219,7 @@ export const onTestFailed: TaskHook = createTestHook( timeout ?? getDefaultHookTimeout(), true, new Error('STACK_TRACE_ERROR'), + abortIfTimeout, ), ) }, @@ -244,6 +258,7 @@ export const onTestFinished: TaskHook = createTestHook( timeout ?? getDefaultHookTimeout(), true, new Error('STACK_TRACE_ERROR'), + abortIfTimeout, ), ) }, diff --git a/packages/runner/src/run.ts b/packages/runner/src/run.ts index a7f50157e..f8513b22b 100644 --- a/packages/runner/src/run.ts +++ b/packages/runner/src/run.ts @@ -15,15 +15,17 @@ import type { TaskUpdateEvent, Test, TestContext, + WriteableTestContext, } from './types/tasks' import { shuffle } from '@vitest/utils' import { processError } from '@vitest/utils/error' import { collectTests } from './collect' -import { PendingError } from './errors' +import { abortContextSignal } from './context' +import { PendingError, TestRunAbortError } from './errors' import { callFixtureCleanup } from './fixture' import { getBeforeHookCleanupCallback } from './hooks' import { getFn, getHooks } from './map' -import { setCurrentTest } from './test-state' +import { addRunningTest, getRunningTests, setCurrentTest } from './test-state' import { limitConcurrency } from './utils/limit-concurrency' import { partitionSuiteChildren } from './utils/suite' import { hasFailed, hasTests } from './utils/tasks' @@ -87,12 +89,14 @@ async function callTestHooks( return } + const context = test.context as WriteableTestContext + const onTestFailed = test.context.onTestFailed const onTestFinished = test.context.onTestFinished - test.context.onTestFailed = () => { + context.onTestFailed = () => { throw new Error(`Cannot call "onTestFailed" inside a test hook.`) } - test.context.onTestFinished = () => { + context.onTestFinished = () => { throw new Error(`Cannot call "onTestFinished" inside a test hook.`) } @@ -115,8 +119,8 @@ async function callTestHooks( } } - test.context.onTestFailed = onTestFailed - test.context.onTestFinished = onTestFinished + context.onTestFailed = onTestFailed + context.onTestFinished = onTestFinished } export async function callSuiteHook( @@ -145,7 +149,11 @@ export async function callSuiteHook( } async function runHook(hook: Function) { - return getBeforeHookCleanupCallback(hook, await hook(...args)) + return getBeforeHookCleanupCallback( + hook, + await hook(...args), + name === 'beforeEach' ? args[0] : undefined, + ) } if (sequence === 'parallel') { @@ -274,6 +282,7 @@ export async function runTest(test: Test, runner: VitestRunner): Promise { } updateTask('test-prepare', test, runner) + const cleanupRunningTest = addRunningTest(test) setCurrentTest(test) const suite = test.suite || test.file @@ -374,6 +383,7 @@ export async function runTest(test: Test, runner: VitestRunner): Promise { } updateTask('test-finished', test, runner) setCurrentTest(undefined) + cleanupRunningTest() return } @@ -405,6 +415,7 @@ export async function runTest(test: Test, runner: VitestRunner): Promise { } } + cleanupRunningTest() setCurrentTest(undefined) test.result.duration = now() - start @@ -588,21 +599,38 @@ export async function runFiles(files: File[], runner: VitestRunner): Promise { - const paths = specs.map(f => typeof f === 'string' ? f : f.filepath) - await runner.onBeforeCollect?.(paths) + const cancel = runner.cancel?.bind(runner) + // Ideally, we need to have an event listener for this, but only have a runner here. + // Adding another onCancel felt wrong (maybe it needs to be refactored) + runner.cancel = (reason) => { + // We intentionally create only one error since there is only one test run that can be cancelled + const error = new TestRunAbortError('The test run was aborted by the user.', reason) + getRunningTests().forEach(test => + abortContextSignal(test.context, error), + ) + return cancel?.(reason) + } - const files = await collectTests(specs, runner) + try { + const paths = specs.map(f => typeof f === 'string' ? f : f.filepath) + await runner.onBeforeCollect?.(paths) - await runner.onCollected?.(files) - await runner.onBeforeRunFiles?.(files) + const files = await collectTests(specs, runner) - await runFiles(files, runner) + await runner.onCollected?.(files) + await runner.onBeforeRunFiles?.(files) - await runner.onAfterRunFiles?.(files) + await runFiles(files, runner) - await finishSendTasksUpdate(runner) + await runner.onAfterRunFiles?.(files) - return files + await finishSendTasksUpdate(runner) + + return files + } + finally { + runner.cancel = cancel + } } async function publicCollect(specs: string[] | FileSpecification[], runner: VitestRunner): Promise { diff --git a/packages/runner/src/suite.ts b/packages/runner/src/suite.ts index da8b8d913..6884b6bea 100644 --- a/packages/runner/src/suite.ts +++ b/packages/runner/src/suite.ts @@ -27,6 +27,7 @@ import { } from '@vitest/utils' import { parseSingleStack } from '@vitest/utils/source-map' import { + abortIfTimeout, collectorContext, collectTask, createTestContext, @@ -357,6 +358,7 @@ function createSuiteCollector( timeout, false, stackTraceError, + (_, error) => abortIfTimeout([context], error), ), ) } diff --git a/packages/runner/src/test-state.ts b/packages/runner/src/test-state.ts index af1d5b041..d4e76aeb8 100644 --- a/packages/runner/src/test-state.ts +++ b/packages/runner/src/test-state.ts @@ -9,3 +9,15 @@ export function setCurrentTest(test: T | undefined): void { export function getCurrentTest(): T { return _test as T } + +const tests: Array = [] +export function addRunningTest(test: Test): () => void { + tests.push(test) + return () => { + tests.splice(tests.indexOf(test)) + } +} + +export function getRunningTests(): Array { + return tests +} diff --git a/packages/runner/src/types/runner.ts b/packages/runner/src/types/runner.ts index 81bd8d73b..13ca7de5e 100644 --- a/packages/runner/src/types/runner.ts +++ b/packages/runner/src/types/runner.ts @@ -77,7 +77,7 @@ export interface VitestRunner { * Runner should listen for this method and mark tests and suites as skipped in * "onBeforeRunSuite" and "onBeforeRunTask" when called. */ - onCancel?: (reason: CancelReason) => unknown + cancel?: (reason: CancelReason) => unknown /** * Called before running a single test. Doesn't have "result" yet. diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts index c7a871d7c..fe457aeb5 100644 --- a/packages/runner/src/types/tasks.ts +++ b/packages/runner/src/types/tasks.ts @@ -647,23 +647,29 @@ export interface TestContext { /** * Metadata of the current test */ - task: Readonly> + readonly task: Readonly + + /** + * An [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that will be aborted if the test times out or + * the test run was cancelled. + */ + readonly signal: AbortSignal /** * Extract hooks on test failed */ - onTestFailed: (fn: OnTestFailedHandler, timeout?: number) => void + readonly onTestFailed: (fn: OnTestFailedHandler, timeout?: number) => void /** * Extract hooks on test failed */ - onTestFinished: (fn: OnTestFinishedHandler, timeout?: number) => void + readonly onTestFinished: (fn: OnTestFinishedHandler, timeout?: number) => void /** * Mark tests as skipped. All execution after this call will be skipped. * This function throws an error, so make sure you are not catching it accidentally. */ - skip: { + readonly skip: { (note?: string): never (condition: boolean, note?: string): void } @@ -687,3 +693,7 @@ export interface TaskHook { export type SequenceHooks = 'stack' | 'list' | 'parallel' export type SequenceSetupFiles = 'list' | 'parallel' + +export type WriteableTestContext = { + -readonly [P in keyof TestContext]: TestContext[P] +} diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 791ddfd9d..e9676f470 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -150,7 +150,7 @@ export async function resolveWorkspace( fileProjects.map(p => ` - ${relative(vitest.config.root, p)}`).join('\n'), '\n\n', ].join('') - : [' '] + : ' ' throw new Error([ `Project name "${name}"`, project.vite.config.configFile ? ` from "${relative(vitest.config.root, project.vite.config.configFile)}"` : '', @@ -226,7 +226,7 @@ export async function resolveBrowserWorkspace( const ending = nth === 2 ? 'nd' : nth === 3 ? 'rd' : 'th' throw new Error(`The browser configuration must have a "browser" property. The ${nth}${ending} item in "browser.instances" doesn't have it. Make sure your${originalName ? ` "${originalName}"` : ''} configuration is correct.`) } - const name = config.name! + const name = config.name if (name == null) { throw new Error(`The browser configuration must have a "name" property. This is a bug in Vitest. Please, open a new issue with reproduction`) diff --git a/packages/vitest/src/runtime/runBaseTests.ts b/packages/vitest/src/runtime/runBaseTests.ts index d1027988d..7f2a1acb6 100644 --- a/packages/vitest/src/runtime/runBaseTests.ts +++ b/packages/vitest/src/runtime/runBaseTests.ts @@ -40,7 +40,7 @@ export async function run( workerState.onCancel.then((reason) => { closeInspector(config) - runner.onCancel?.(reason) + runner.cancel?.(reason) }) workerState.durations.prepare = performance.now() - workerState.durations.prepare diff --git a/packages/vitest/src/runtime/runVmTests.ts b/packages/vitest/src/runtime/runVmTests.ts index 3fed9a678..8ebaa2443 100644 --- a/packages/vitest/src/runtime/runVmTests.ts +++ b/packages/vitest/src/runtime/runVmTests.ts @@ -79,7 +79,7 @@ export async function run( workerState.onCancel.then((reason) => { closeInspector(config) - runner.onCancel?.(reason) + runner.cancel?.(reason) }) workerState.durations.prepare diff --git a/packages/vitest/src/runtime/runners/index.ts b/packages/vitest/src/runtime/runners/index.ts index 8dcd019f9..f7021e6a1 100644 --- a/packages/vitest/src/runtime/runners/index.ts +++ b/packages/vitest/src/runtime/runners/index.ts @@ -113,7 +113,7 @@ export async function resolveTestRunner( if (currentFailures >= config.bail) { rpc().onCancel('test-failure') - testRunner.onCancel?.('test-failure') + testRunner.cancel?.('test-failure') } } await originalOnAfterRunTask?.call(testRunner, test) diff --git a/packages/vitest/src/runtime/runners/test.ts b/packages/vitest/src/runtime/runners/test.ts index b1ab1154d..3d673d738 100644 --- a/packages/vitest/src/runtime/runners/test.ts +++ b/packages/vitest/src/runtime/runners/test.ts @@ -76,7 +76,7 @@ export class VitestTestRunner implements VitestRunner { this.workerState.current = test.suite || test.file } - onCancel(_reason: CancelReason): void { + cancel(_reason: CancelReason): void { this.cancelRun = true } diff --git a/packages/vitest/src/types/global.ts b/packages/vitest/src/types/global.ts index 1b039f61e..47a38e083 100644 --- a/packages/vitest/src/types/global.ts +++ b/packages/vitest/src/types/global.ts @@ -106,7 +106,12 @@ declare module '@vitest/expect' { declare module '@vitest/runner' { interface TestContext { - expect: ExpectStatic + /** + * `expect` instance bound to the current test. + * + * This API is useful for running snapshot tests concurrently because global expect cannot track them. + */ + readonly expect: ExpectStatic } interface TaskMeta { diff --git a/test/cli/test/signal.test.ts b/test/cli/test/signal.test.ts new file mode 100644 index 000000000..58a2e3e63 --- /dev/null +++ b/test/cli/test/signal.test.ts @@ -0,0 +1,156 @@ +import type { UserConsoleLog } from 'vitest' +import type { Reporter, Vitest } from 'vitest/node' +import { expect, test } from 'vitest' +import { runInlineTests } from '../../test-utils' + +test('timeout aborts the signal without fixtures', async () => { + const { stderr, results } = await runInlineTests({ + 'basic.test.ts': /* ts */` + import { test } from 'vitest' + import { setTimeout } from 'node:timers/promises' + test('timeouts', async ({ signal, task, example }) => { + signal.addEventListener('abort', () => { + task.meta.aborted = true + }) + await setTimeout(100) + }, 10) + `, + }) + expect(stderr).toContain('Test timed out in 10ms.') + expect(results).toHaveLength(1) + expect(results[0].children.at(0)?.meta()).toEqual({ + aborted: true, + }) +}) + +test('timeout aborts the signal', async () => { + const { stderr, results } = await runInlineTests({ + 'basic.test.ts': /* ts */` + import { test } from 'vitest' + import { setTimeout } from 'node:timers/promises' + test.extend({ + // .extend to force fixture initialisation + example: true, + })('timeouts', async ({ signal, task, example }) => { + signal.addEventListener('abort', () => { + task.meta.aborted = true + }) + await setTimeout(100) + }, 10) + `, + }) + expect(stderr).toContain('Test timed out in 10ms.') + expect(results).toHaveLength(1) + expect(results[0].children.at(0)?.meta()).toEqual({ + aborted: true, + }) +}) + +test('timeout aborts all signals in concurrent tests', async () => { + const { stderr, results } = await runInlineTests({ + 'basic.test.ts': /* ts */` + import { test } from 'vitest' + import { setTimeout } from 'node:timers/promises' + test + // .extend to force fixture initialisation + .extend({ example: true }) + .concurrent + .for([1, 1, 1]) + ('timeouts', async (_, { signal, task, example }) => { + signal.addEventListener('abort', () => { + task.meta.aborted = true + }) + await setTimeout(100) + }, 10) + `, + }) + expect(stderr).toContain('Test timed out in 10ms.') + expect(results).toHaveLength(1) + expect(results[0].children.at(0)?.meta()).toEqual({ + aborted: true, + }) + expect(results[0].children.at(1)?.meta()).toEqual({ + aborted: true, + }) + expect(results[0].children.at(2)?.meta()).toEqual({ + aborted: true, + }) +}) + +class AbortReporter implements Reporter { + vitest!: Vitest + onInit(vitest: Vitest) { + this.vitest = vitest + } + + onUserConsoleLog(log: UserConsoleLog) { + if (log.content.includes('ready')) { + this.vitest.cancelCurrentRun('keyboard-input') + } + } +} + +test('cancelling test run aborts the signal', async () => { + const { results, stderr } = await runInlineTests({ + 'basic.test.ts': /* ts */ ` + import { test } from 'vitest' + test('aborted', async ({ signal, task }) => { + return new Promise(resolve => { + console.log('ready') + signal.addEventListener('abort', () => { + task.meta.aborted = true + resolve() + }) + }) + }, Infinity) + `, + }, { + reporters: [ + 'default', + new AbortReporter(), + ], + }) + expect(stderr).toBe('') + expect(results).toHaveLength(1) + expect(results[0].children.at(0)?.meta()).toEqual({ + aborted: true, + }) +}) + +test('cancelling test run aborts the signal in all concurrent tests', async () => { + const { results, stderr } = await runInlineTests({ + 'basic.test.ts': /* ts */` + import { test } from 'vitest' + test.concurrent.for([1, 2, 3])( + 'aborted', + { timeout: Infinity }, + async (number, { signal, task }) => { + return new Promise(resolve => { + if (number === 3) { + console.log('ready') + } + signal.addEventListener('abort', () => { + task.meta.aborted = true + resolve() + }) + }) + }) + `, + }, { + reporters: [ + 'default', + new AbortReporter(), + ], + }) + expect(stderr).toBe('') + expect(results).toHaveLength(1) + expect(results[0].children.at(0)?.meta()).toEqual({ + aborted: true, + }) + expect(results[0].children.at(1)?.meta()).toEqual({ + aborted: true, + }) + expect(results[0].children.at(2)?.meta()).toEqual({ + aborted: true, + }) +}) diff --git a/test/core/vitest-environment-custom/index.ts b/test/core/vitest-environment-custom/index.ts index b061c7a8b..75bf988ab 100644 --- a/test/core/vitest-environment-custom/index.ts +++ b/test/core/vitest-environment-custom/index.ts @@ -14,6 +14,7 @@ export default { option: custom.option, setTimeout, clearTimeout, + AbortController, }) return { getVmContext() {