Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vitest): allow calling skip dynamically #3966

Merged
merged 3 commits into from Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)
})