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() {