diff --git a/docs/api/index.md b/docs/api/index.md index 5b08b91ffeaf..2848986a892c 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -102,6 +102,18 @@ In Jest, `TestFunction` can also be of type `(done: DoneCallback) => void`. If t }) ``` + You can also skip test by calling `skip` on its [context](/guide/test-context) dynamically: + + ```ts + import { assert, test } from 'vitest' + + test('skipped test', (context) => { + context.skip() + // Test skipped, no error + assert.equal(Math.sqrt(4), 3) + }) + ``` + ### test.skipIf - **Type:** `(condition: any) => Test` diff --git a/docs/guide/test-context.md b/docs/guide/test-context.md index ac1c7a5ee53c..87f5819229f5 100644 --- a/docs/guide/test-context.md +++ b/docs/guide/test-context.md @@ -27,7 +27,42 @@ A readonly object containing metadata about the test. #### `context.expect` -The `expect` API bound to the current test. +The `expect` API bound to the current test: + +```ts +import { it } from 'vitest' + +it('math is easy', ({ expect }) => { + expect(2 + 2).toBe(4) +}) +``` + +This API is useful for running snapshot tests concurrently because global expect cannot track them: + +```ts +import { it } from 'vitest' + +it.concurrent('math is easy', ({ expect }) => { + expect(2 + 2).toMatchInlineSnapshot() +}) + +it.concurrent('math is hard', ({ expect }) => { + expect(2 * 2).toMatchInlineSnapshot() +}) +``` + +#### `context.skip` + +Skips subsequent test execution and marks test as skipped: + +```ts +import { expect, it } from 'vitest' + +it('math is hard', ({ skip }) => { + skip() + expect(2 + 2).toBe(5) +}) +``` ## Extend Test Context diff --git a/packages/runner/src/context.ts b/packages/runner/src/context.ts index b6bdd0195346..6e357f8f19a9 100644 --- a/packages/runner/src/context.ts +++ b/packages/runner/src/context.ts @@ -2,6 +2,7 @@ import type { Awaitable } from '@vitest/utils' import { getSafeTimers } from '@vitest/utils' import type { RuntimeContext, SuiteCollector, Test, TestContext } from './types' import type { VitestRunner } from './types/runner' +import { PendingError } from './errors' export const collectorContext: RuntimeContext = { tasks: [], @@ -49,6 +50,11 @@ export function createTestContext(test: Test, runner: VitestRunner): TestContext context.meta = test context.task = test + context.skip = () => { + test.pending = true + throw new PendingError('test is skipped; abort execution', test) + } + context.onTestFailed = (fn) => { test.onFailed ||= [] test.onFailed.push(fn) diff --git a/packages/runner/src/errors.ts b/packages/runner/src/errors.ts new file mode 100644 index 000000000000..b9c50bcd47f9 --- /dev/null +++ b/packages/runner/src/errors.ts @@ -0,0 +1,11 @@ +import type { TaskBase } from './types' + +export class PendingError extends Error { + public code = 'VITEST_PENDING' + public taskId: string + + constructor(public message: string, task: TaskBase) { + super(message) + this.taskId = task.id + } +} diff --git a/packages/runner/src/run.ts b/packages/runner/src/run.ts index a97b3e64df7b..6765c9f95265 100644 --- a/packages/runner/src/run.ts +++ b/packages/runner/src/run.ts @@ -8,6 +8,7 @@ import { getFn, getHooks } from './map' import { collectTests } from './collect' import { setCurrentTest } from './test-state' import { hasFailed, hasTests } from './utils/tasks' +import { PendingError } from './errors' const now = Date.now @@ -175,6 +176,14 @@ export async function runTest(test: Test, runner: VitestRunner) { failTask(test.result, e) } + // skipped with new PendingError + if (test.pending || test.result?.state === 'skip') { + test.mode = 'skip' + test.result = { state: 'skip' } + updateTask(test, runner) + return + } + try { await callSuiteHook(test.suite, test, 'afterEach', runner, [test.context, test.suite]) await callCleanupHooks(beforeEachCleanups) @@ -225,6 +234,11 @@ export async function runTest(test: Test, runner: VitestRunner) { } function failTask(result: TaskResult, err: unknown) { + if (err instanceof PendingError) { + result.state = 'skip' + return + } + result.state = 'fail' const errors = Array.isArray(err) ? err diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts index b18a1e997cbb..60b6101f7abb 100644 --- a/packages/runner/src/types/tasks.ts +++ b/packages/runner/src/types/tasks.ts @@ -59,6 +59,7 @@ export interface File extends Suite { export interface Test extends TaskBase { type: 'test' suite: Suite + pending?: boolean result?: TaskResult fails?: boolean context: TestContext & ExtraContext @@ -262,6 +263,11 @@ export interface TestContext { * Extract hooks on test failed */ onTestFailed: (fn: OnTestFailedHandler) => void + + /** + * Mark tests as skipped. All execution after this call will be skipped. + */ + skip: () => void } export type OnTestFailedHandler = (result: TaskResult) => Awaitable diff --git a/packages/utils/src/helpers.ts b/packages/utils/src/helpers.ts index 9e3d0470c051..e0fca92c693a 100644 --- a/packages/utils/src/helpers.ts +++ b/packages/utils/src/helpers.ts @@ -147,7 +147,7 @@ export function objectAttr(source: any, path: string, defaultValue = undefined) return result } -type DeferPromise = Promise & { +export type DeferPromise = Promise & { resolve: (value: T | PromiseLike) => void reject: (reason?: any) => void } diff --git a/packages/vitest/src/node/state.ts b/packages/vitest/src/node/state.ts index c1c01d7ece7b..2b849be8aef5 100644 --- a/packages/vitest/src/node/state.ts +++ b/packages/vitest/src/node/state.ts @@ -32,6 +32,17 @@ export class StateManager { else err = { type, message: err } + const _err = err as Record + if (_err && typeof _err === 'object' && _err.code === 'VITEST_PENDING') { + const task = this.idMap.get(_err.taskId) + if (task) { + task.mode = 'skip' + task.result ??= { state: 'skip' } + task.result.state = 'skip' + } + return + } + this.errorsSet.add(err) } @@ -119,6 +130,9 @@ export class StateManager { if (task) { task.result = result task.meta = meta + // skipped with new PendingError + if (result?.state === 'skip') + task.mode = 'skip' } } } diff --git a/test/core/test/skip.test.ts b/test/core/test/skip.test.ts new file mode 100644 index 000000000000..ad78894cde6b --- /dev/null +++ b/test/core/test/skip.test.ts @@ -0,0 +1,39 @@ +import EventEmitter from 'node:events' +import { expect, it } from 'vitest' + +const sleep = (ms?: number) => new Promise(resolve => setTimeout(resolve, ms)) + +it('correctly skips sync tests', ({ skip }) => { + skip() + expect(1).toBe(2) +}) + +it('correctly skips async tests with skip before async', async ({ skip }) => { + await sleep(100) + skip() + expect(1).toBe(2) +}) + +it('correctly skips async tests with async after skip', async ({ skip }) => { + skip() + await sleep(100) + expect(1).toBe(2) +}) + +it('correctly skips tests with callback', ({ skip }) => { + const emitter = new EventEmitter() + emitter.on('test', () => { + skip() + }) + emitter.emit('test') + expect(1).toBe(2) +}) + +it('correctly skips tests with async callback', ({ skip }) => { + const emitter = new EventEmitter() + emitter.on('test', async () => { + skip() + }) + emitter.emit('test') + expect(1).toBe(2) +})