Skip to content

Commit

Permalink
feat(vitest): allow calling skip dynamically (#3966)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed Aug 16, 2023
1 parent 3073b9a commit 5c88d8e
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 2 deletions.
12 changes: 12 additions & 0 deletions docs/api/index.md
Expand Up @@ -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`
Expand Down
37 changes: 36 additions & 1 deletion docs/guide/test-context.md
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions packages/runner/src/context.ts
Expand Up @@ -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: [],
Expand Down Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions 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
}
}
14 changes: 14 additions & 0 deletions packages/runner/src/run.ts
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions packages/runner/src/types/tasks.ts
Expand Up @@ -59,6 +59,7 @@ export interface File extends Suite {
export interface Test<ExtraContext = {}> extends TaskBase {
type: 'test'
suite: Suite
pending?: boolean
result?: TaskResult
fails?: boolean
context: TestContext & ExtraContext
Expand Down Expand Up @@ -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<void>
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/src/helpers.ts
Expand Up @@ -147,7 +147,7 @@ export function objectAttr(source: any, path: string, defaultValue = undefined)
return result
}

type DeferPromise<T> = Promise<T> & {
export type DeferPromise<T> = Promise<T> & {
resolve: (value: T | PromiseLike<T>) => void
reject: (reason?: any) => void
}
Expand Down
14 changes: 14 additions & 0 deletions packages/vitest/src/node/state.ts
Expand Up @@ -32,6 +32,17 @@ export class StateManager {
else
err = { type, message: err }

const _err = err as Record<string, any>
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)
}

Expand Down Expand Up @@ -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'
}
}
}
Expand Down
39 changes: 39 additions & 0 deletions 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)
})

0 comments on commit 5c88d8e

Please sign in to comment.