Skip to content

Commit 5c88d8e

Browse files
authored
feat(vitest): allow calling skip dynamically (#3966)
1 parent 3073b9a commit 5c88d8e

File tree

9 files changed

+139
-2
lines changed

9 files changed

+139
-2
lines changed

docs/api/index.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,18 @@ In Jest, `TestFunction` can also be of type `(done: DoneCallback) => void`. If t
102102
})
103103
```
104104

105+
You can also skip test by calling `skip` on its [context](/guide/test-context) dynamically:
106+
107+
```ts
108+
import { assert, test } from 'vitest'
109+
110+
test('skipped test', (context) => {
111+
context.skip()
112+
// Test skipped, no error
113+
assert.equal(Math.sqrt(4), 3)
114+
})
115+
```
116+
105117
### test.skipIf
106118

107119
- **Type:** `(condition: any) => Test`

docs/guide/test-context.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,42 @@ A readonly object containing metadata about the test.
2727

2828
#### `context.expect`
2929

30-
The `expect` API bound to the current test.
30+
The `expect` API bound to the current test:
31+
32+
```ts
33+
import { it } from 'vitest'
34+
35+
it('math is easy', ({ expect }) => {
36+
expect(2 + 2).toBe(4)
37+
})
38+
```
39+
40+
This API is useful for running snapshot tests concurrently because global expect cannot track them:
41+
42+
```ts
43+
import { it } from 'vitest'
44+
45+
it.concurrent('math is easy', ({ expect }) => {
46+
expect(2 + 2).toMatchInlineSnapshot()
47+
})
48+
49+
it.concurrent('math is hard', ({ expect }) => {
50+
expect(2 * 2).toMatchInlineSnapshot()
51+
})
52+
```
53+
54+
#### `context.skip`
55+
56+
Skips subsequent test execution and marks test as skipped:
57+
58+
```ts
59+
import { expect, it } from 'vitest'
60+
61+
it('math is hard', ({ skip }) => {
62+
skip()
63+
expect(2 + 2).toBe(5)
64+
})
65+
```
3166

3267
## Extend Test Context
3368

packages/runner/src/context.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Awaitable } from '@vitest/utils'
22
import { getSafeTimers } from '@vitest/utils'
33
import type { RuntimeContext, SuiteCollector, Test, TestContext } from './types'
44
import type { VitestRunner } from './types/runner'
5+
import { PendingError } from './errors'
56

67
export const collectorContext: RuntimeContext = {
78
tasks: [],
@@ -49,6 +50,11 @@ export function createTestContext(test: Test, runner: VitestRunner): TestContext
4950
context.meta = test
5051
context.task = test
5152

53+
context.skip = () => {
54+
test.pending = true
55+
throw new PendingError('test is skipped; abort execution', test)
56+
}
57+
5258
context.onTestFailed = (fn) => {
5359
test.onFailed ||= []
5460
test.onFailed.push(fn)

packages/runner/src/errors.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { TaskBase } from './types'
2+
3+
export class PendingError extends Error {
4+
public code = 'VITEST_PENDING'
5+
public taskId: string
6+
7+
constructor(public message: string, task: TaskBase) {
8+
super(message)
9+
this.taskId = task.id
10+
}
11+
}

packages/runner/src/run.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { getFn, getHooks } from './map'
88
import { collectTests } from './collect'
99
import { setCurrentTest } from './test-state'
1010
import { hasFailed, hasTests } from './utils/tasks'
11+
import { PendingError } from './errors'
1112

1213
const now = Date.now
1314

@@ -175,6 +176,14 @@ export async function runTest(test: Test, runner: VitestRunner) {
175176
failTask(test.result, e)
176177
}
177178

179+
// skipped with new PendingError
180+
if (test.pending || test.result?.state === 'skip') {
181+
test.mode = 'skip'
182+
test.result = { state: 'skip' }
183+
updateTask(test, runner)
184+
return
185+
}
186+
178187
try {
179188
await callSuiteHook(test.suite, test, 'afterEach', runner, [test.context, test.suite])
180189
await callCleanupHooks(beforeEachCleanups)
@@ -225,6 +234,11 @@ export async function runTest(test: Test, runner: VitestRunner) {
225234
}
226235

227236
function failTask(result: TaskResult, err: unknown) {
237+
if (err instanceof PendingError) {
238+
result.state = 'skip'
239+
return
240+
}
241+
228242
result.state = 'fail'
229243
const errors = Array.isArray(err)
230244
? err

packages/runner/src/types/tasks.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export interface File extends Suite {
5959
export interface Test<ExtraContext = {}> extends TaskBase {
6060
type: 'test'
6161
suite: Suite
62+
pending?: boolean
6263
result?: TaskResult
6364
fails?: boolean
6465
context: TestContext & ExtraContext
@@ -262,6 +263,11 @@ export interface TestContext {
262263
* Extract hooks on test failed
263264
*/
264265
onTestFailed: (fn: OnTestFailedHandler) => void
266+
267+
/**
268+
* Mark tests as skipped. All execution after this call will be skipped.
269+
*/
270+
skip: () => void
265271
}
266272

267273
export type OnTestFailedHandler = (result: TaskResult) => Awaitable<void>

packages/utils/src/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ export function objectAttr(source: any, path: string, defaultValue = undefined)
147147
return result
148148
}
149149

150-
type DeferPromise<T> = Promise<T> & {
150+
export type DeferPromise<T> = Promise<T> & {
151151
resolve: (value: T | PromiseLike<T>) => void
152152
reject: (reason?: any) => void
153153
}

packages/vitest/src/node/state.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ export class StateManager {
3232
else
3333
err = { type, message: err }
3434

35+
const _err = err as Record<string, any>
36+
if (_err && typeof _err === 'object' && _err.code === 'VITEST_PENDING') {
37+
const task = this.idMap.get(_err.taskId)
38+
if (task) {
39+
task.mode = 'skip'
40+
task.result ??= { state: 'skip' }
41+
task.result.state = 'skip'
42+
}
43+
return
44+
}
45+
3546
this.errorsSet.add(err)
3647
}
3748

@@ -119,6 +130,9 @@ export class StateManager {
119130
if (task) {
120131
task.result = result
121132
task.meta = meta
133+
// skipped with new PendingError
134+
if (result?.state === 'skip')
135+
task.mode = 'skip'
122136
}
123137
}
124138
}

test/core/test/skip.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import EventEmitter from 'node:events'
2+
import { expect, it } from 'vitest'
3+
4+
const sleep = (ms?: number) => new Promise(resolve => setTimeout(resolve, ms))
5+
6+
it('correctly skips sync tests', ({ skip }) => {
7+
skip()
8+
expect(1).toBe(2)
9+
})
10+
11+
it('correctly skips async tests with skip before async', async ({ skip }) => {
12+
await sleep(100)
13+
skip()
14+
expect(1).toBe(2)
15+
})
16+
17+
it('correctly skips async tests with async after skip', async ({ skip }) => {
18+
skip()
19+
await sleep(100)
20+
expect(1).toBe(2)
21+
})
22+
23+
it('correctly skips tests with callback', ({ skip }) => {
24+
const emitter = new EventEmitter()
25+
emitter.on('test', () => {
26+
skip()
27+
})
28+
emitter.emit('test')
29+
expect(1).toBe(2)
30+
})
31+
32+
it('correctly skips tests with async callback', ({ skip }) => {
33+
const emitter = new EventEmitter()
34+
emitter.on('test', async () => {
35+
skip()
36+
})
37+
emitter.emit('test')
38+
expect(1).toBe(2)
39+
})

0 commit comments

Comments
 (0)