Skip to content

Commit

Permalink
feat(vitest): "test" accepts options object as the second parameter (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va authored Feb 15, 2024
1 parent aa72740 commit 7d9b1fb
Show file tree
Hide file tree
Showing 10 changed files with 259 additions and 137 deletions.
56 changes: 37 additions & 19 deletions docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,42 @@ interface TestOptions {
}
```

Vitest 1.3.0 deprecates the use of options as the last parameter. You will see a deprecation message until 2.0.0 when this syntax will be removed. If you need to pass down options, use `test` function's second argument:

```ts
import { test } from 'vitest'

test('flaky test', () => {}, { retry: 3 }) // [!code --]
test('flaky test', { retry: 3 }, () => {}) // [!code ++]
```

When a test function returns a promise, the runner will wait until it is resolved to collect async expectations. If the promise is rejected, the test will fail.

::: tip
In Jest, `TestFunction` can also be of type `(done: DoneCallback) => void`. If this form is used, the test will not be concluded until `done` is called. You can achieve the same using an `async` function, see the [Migration guide Done Callback section](/guide/migration#done-callback).
:::

Since Vitest 1.3.0 most options support both dot-syntax and object-syntax allowing you to use whatever style you prefer.

:::code-group
```ts [dot-syntax]
import { test } from 'vitest'

test.skip('skipped test', () => {
// some logic that fails right now
})
```
```ts [object-syntax <Badge type="info">1.3.0+</Badge>]
import { test } from 'vitest'

test('skipped test', { skip: true }, () => {
// some logic that fails right now
})
```
:::

## test

- **Type:** `(name: string | Function, fn: TestFunction, timeout?: number | TestOptions) => void`
- **Alias:** `it`

`test` defines a set of related expectations. It receives the test name and a function that holds the expectations to test.
Expand All @@ -57,7 +84,6 @@ test('should work as expected', () => {

### test.extend <Badge type="info">0.32.3+</Badge>

- **Type:** `<T extends Record<string, any>>(fixtures: Fixtures<T>): TestAPI<ExtraContext & T>`
- **Alias:** `it.extend`

Use `test.extend` to extend the test context with custom fixtures. This will return a new `test` and it's also extendable, so you can compose more fixtures or override existing ones by extending it as you need. See [Extend Test Context](/guide/test-context.html#test-extend) for more information.
Expand Down Expand Up @@ -87,7 +113,6 @@ myTest('add item', ({ todos }) => {

### test.skip

- **Type:** `(name: string | Function, fn: TestFunction, timeout?: number | TestOptions) => void`
- **Alias:** `it.skip`

If you want to skip running certain tests, but you don't want to delete the code due to any reason, you can use `test.skip` to avoid running them.
Expand Down Expand Up @@ -115,7 +140,6 @@ test('skipped test', (context) => {

### test.skipIf

- **Type:** `(condition: any) => Test`
- **Alias:** `it.skipIf`

In some cases you might run tests multiple times with different environments, and some of the tests might be environment-specific. Instead of wrapping the test code with `if`, you can use `test.skipIf` to skip the test whenever the condition is truthy.
Expand All @@ -136,7 +160,6 @@ You cannot use this syntax, when using Vitest as [type checker](/guide/testing-t

### test.runIf

- **Type:** `(condition: any) => Test`
- **Alias:** `it.runIf`

Opposite of [test.skipIf](#test-skipif).
Expand All @@ -157,7 +180,6 @@ You cannot use this syntax, when using Vitest as [type checker](/guide/testing-t

### test.only

- **Type:** `(name: string | Function, fn: TestFunction, timeout?: number) => void`
- **Alias:** `it.only`

Use `test.only` to only run certain tests in a given suite. This is useful when debugging.
Expand All @@ -182,7 +204,6 @@ In order to do that run `vitest` with specific file containing the tests in ques

### test.concurrent

- **Type:** `(name: string | Function, fn: TestFunction, timeout?: number) => void`
- **Alias:** `it.concurrent`

`test.concurrent` marks consecutive tests to be run in parallel. It receives the test name, an async function with the tests to collect, and an optional timeout (in milliseconds).
Expand Down Expand Up @@ -224,7 +245,7 @@ You cannot use this syntax, when using Vitest as [type checker](/guide/testing-t

### test.sequential

- **Type:** `(name: string | Function, fn: TestFunction, timeout?: number) => void`
- **Alias:** `it.sequential`

`test.sequential` marks a test as sequential. This is useful if you want to run tests in sequence within `describe.concurrent` or with the `--sequence.concurrent` command option.

Expand All @@ -248,7 +269,6 @@ describe.concurrent('suite', () => {

### test.todo

- **Type:** `(name: string | Function) => void`
- **Alias:** `it.todo`

Use `test.todo` to stub tests to be implemented later. An entry will be shown in the report for the tests so you know how many tests you still need to implement.
Expand All @@ -260,7 +280,6 @@ test.todo('unimplemented test')

### test.fails

- **Type:** `(name: string | Function, fn: TestFunction, timeout?: number) => void`
- **Alias:** `it.fails`

Use `test.fails` to indicate that an assertion will fail explicitly.
Expand All @@ -282,7 +301,6 @@ You cannot use this syntax, when using Vitest as [type checker](/guide/testing-t

### test.each

- **Type:** `(cases: ReadonlyArray<T>, ...args: any[]) => void`
- **Alias:** `it.each`

Use `test.each` when you need to run the same test with different variables.
Expand Down Expand Up @@ -570,7 +588,7 @@ describe('numberToCurrency', () => {

### describe.skip

- **Type:** `(name: string | Function, fn: TestFunction, options?: number | TestOptions) => void`
- **Alias:** `suite.skip`

Use `describe.skip` in a suite to avoid running a particular describe block.

Expand All @@ -587,7 +605,7 @@ describe.skip('skipped suite', () => {

### describe.skipIf

- **Type:** `(condition: any) => void`
- **Alias:** `suite.skipIf`

In some cases, you might run suites multiple times with different environments, and some of the suites might be environment-specific. Instead of wrapping the suite with `if`, you can use `describe.skipIf` to skip the suite whenever the condition is truthy.

Expand All @@ -607,7 +625,7 @@ You cannot use this syntax when using Vitest as [type checker](/guide/testing-ty

### describe.runIf

- **Type:** `(condition: any) => void`
- **Alias:** `suite.runIf`

Opposite of [describe.skipIf](#describe-skipif).

Expand Down Expand Up @@ -653,7 +671,7 @@ In order to do that run `vitest` with specific file containing the tests in ques

### describe.concurrent

- **Type:** `(name: string | Function, fn: TestFunction, options?: number | TestOptions) => void`
- **Alias:** `suite.concurrent`

`describe.concurrent` in a suite marks every tests as concurrent

Expand Down Expand Up @@ -694,7 +712,7 @@ You cannot use this syntax, when using Vitest as [type checker](/guide/testing-t

### describe.sequential

- **Type:** `(name: string | Function, fn: TestFunction, options?: number | TestOptions) => void`
- **Alias:** `suite.sequential`

`describe.sequential` in a suite marks every test as sequential. This is useful if you want to run tests in sequence within `describe.concurrent` or with the `--sequence.concurrent` command option.

Expand All @@ -712,7 +730,7 @@ describe.concurrent('suite', () => {

### describe.shuffle

- **Type:** `(name: string | Function, fn: TestFunction, options?: number | TestOptions) => void`
- **Alias:** `suite.shuffle`

Vitest provides a way to run all tests in random order via CLI flag [`--sequence.shuffle`](/guide/cli) or config option [`sequence.shuffle`](/config/#sequence-shuffle), but if you want to have only part of your test suite to run tests in random order, you can mark it with this flag.

Expand All @@ -733,7 +751,7 @@ You cannot use this syntax, when using Vitest as [type checker](/guide/testing-t

### describe.todo

- **Type:** `(name: string | Function) => void`
- **Alias:** `suite.todo`

Use `describe.todo` to stub suites to be implemented later. An entry will be shown in the report for the tests so you know how many tests you still need to implement.

Expand All @@ -744,7 +762,7 @@ describe.todo('unimplemented suite')

### describe.each

- **Type:** `(cases: ReadonlyArray<T>, ...args: any[]): (name: string | Function, fn: (...args: T[]) => void, options?: number | TestOptions) => void`
- **Alias:** `suite.each`

Use `describe.each` if you have more than one test that depends on the same data.

Expand Down
100 changes: 78 additions & 22 deletions packages/runner/src/suite.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { format, isObject, noop, objDisplay, objectAttr } from '@vitest/utils'
import { format, isObject, objDisplay, objectAttr } from '@vitest/utils'
import type { Custom, CustomAPI, File, Fixtures, RunMode, Suite, SuiteAPI, SuiteCollector, SuiteFactory, SuiteHooks, Task, TaskCustomOptions, Test, TestAPI, TestFunction, TestOptions } from './types'
import type { VitestRunner } from './types/runner'
import { createChainable } from './utils/chain'
Expand All @@ -11,11 +11,11 @@ import { getCurrentTest } from './test-state'
// apis
export const suite = createSuite()
export const test = createTest(
function (name: string | Function, fn?: TestFunction, options?: number | TestOptions) {
function (name: string | Function, optionsOrFn?: TestOptions | TestFunction, optionsOrTest?: number | TestOptions | TestFunction) {
if (getCurrentTest())
throw new Error('Calling the test function inside another test function is not allowed. Please put it inside "describe" or "suite" so it can be properly collected.')

getCurrentSuite().test.fn.call(this, formatName(name), fn, options)
getCurrentSuite().test.fn.call(this, formatName(name), optionsOrFn as TestOptions, optionsOrTest as TestFunction)
},
)

Expand Down Expand Up @@ -56,8 +56,48 @@ export function createSuiteHooks() {
}
}

function parseArguments<T extends (...args: any[]) => any>(
optionsOrFn: T | object | undefined,
optionsOrTest: object | T | number | undefined,
) {
let options: TestOptions = {}
let fn: T = (() => {}) as T

// it('', () => {}, { retry: 2 })
if (typeof optionsOrTest === 'object') {
// it('', { retry: 2 }, { retry: 3 })
if (typeof optionsOrFn === 'object')
throw new TypeError('Cannot use two objects as arguments. Please provide options and a function callback in that order.')
// TODO: more info, add a name
// console.warn('The third argument is deprecated. Please use the second argument for options.')
options = optionsOrTest
}
// it('', () => {}, 1000)
else if (typeof optionsOrTest === 'number') {
options = { timeout: optionsOrTest }
}
// it('', { retry: 2 }, () => {})
else if (typeof optionsOrFn === 'object') {
options = optionsOrFn
}

if (typeof optionsOrFn === 'function') {
if (typeof optionsOrTest === 'function')
throw new TypeError('Cannot use two functions as arguments. Please use the second argument for options.')
fn = optionsOrFn as T
}
else if (typeof optionsOrTest === 'function') {
fn = optionsOrTest as T
}

return {
options,
handler: fn,
}
}

// implementations
function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, mode: RunMode, concurrent?: boolean, sequential?: boolean, shuffle?: boolean, each?: boolean, suiteOptions?: TestOptions) {
function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, mode: RunMode, shuffle?: boolean, each?: boolean, suiteOptions?: TestOptions) {
const tasks: (Test | Custom | Suite | SuiteCollector)[] = []
const factoryQueue: (Test | Suite | SuiteCollector)[] = []

Expand Down Expand Up @@ -104,9 +144,11 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m
return task
}

const test = createTest(function (name: string | Function, fn = noop, options = {}) {
if (typeof options === 'number')
options = { timeout: options }
const test = createTest(function (name: string | Function, optionsOrFn?: TestOptions | TestFunction, optionsOrTest?: number | TestOptions | TestFunction) {
let { options, handler } = parseArguments(
optionsOrFn,
optionsOrTest,
)

// inherit repeats, retry, timeout from suite
if (typeof suiteOptions === 'object')
Expand All @@ -118,7 +160,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m

const test = task(
formatName(name),
{ ...this, ...options, handler: fn as any },
{ ...this, ...options, handler },
) as unknown as Test

test.type = 'test'
Expand Down Expand Up @@ -193,12 +235,14 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m
}

function createSuite() {
function suiteFn(this: Record<string, boolean | undefined>, name: string | Function, factory?: SuiteFactory, options: number | TestOptions = {}) {
function suiteFn(this: Record<string, boolean | undefined>, name: string | Function, factoryOrOptions?: SuiteFactory | TestOptions, optionsOrFactory: number | TestOptions | SuiteFactory = {}) {
const mode: RunMode = this.only ? 'only' : this.skip ? 'skip' : this.todo ? 'todo' : 'run'
const currentSuite = getCurrentSuite()

if (typeof options === 'number')
options = { timeout: options }
let { options, handler: factory } = parseArguments(
factoryOrOptions,
optionsOrFactory,
)

// inherit options from current suite
if (currentSuite?.options)
Expand All @@ -208,7 +252,7 @@ function createSuite() {
options.concurrent = this.concurrent || (!this.sequential && options?.concurrent)
options.sequential = this.sequential || (!this.concurrent && options?.sequential)

return createSuiteCollector(formatName(name), factory, mode, this.concurrent, this.sequential, this.shuffle, this.each, options)
return createSuiteCollector(formatName(name), factory, mode, this.shuffle, this.each, options)
}

suiteFn.each = function<T>(this: { withContext: () => SuiteAPI; setContext: (key: string, value: boolean | undefined) => SuiteAPI }, cases: ReadonlyArray<T>, ...args: any[]) {
Expand All @@ -218,14 +262,20 @@ function createSuite() {
if (Array.isArray(cases) && args.length)
cases = formatTemplateString(cases, args)

return (name: string | Function, fn: (...args: T[]) => void, options?: number | TestOptions) => {
return (name: string | Function, optionsOrFn: ((...args: T[]) => void) | TestOptions, fnOrOptions?: ((...args: T[]) => void) | number | TestOptions) => {
const _name = formatName(name)
const arrayOnlyCases = cases.every(Array.isArray)

const { options, handler } = parseArguments(
optionsOrFn,
fnOrOptions,
)

cases.forEach((i, idx) => {
const items = Array.isArray(i) ? i : [i]
arrayOnlyCases
? suite(formatTitle(_name, items, idx), () => fn(...items), options)
: suite(formatTitle(_name, items, idx), () => fn(i), options)
? suite(formatTitle(_name, items, idx), options, () => handler(...items))
: suite(formatTitle(_name, items, idx), options, () => handler(i))
})

this.setContext('each', undefined)
Expand Down Expand Up @@ -254,15 +304,21 @@ export function createTaskCollector(
if (Array.isArray(cases) && args.length)
cases = formatTemplateString(cases, args)

return (name: string | Function, fn: (...args: T[]) => void, options?: number | TestOptions) => {
return (name: string | Function, optionsOrFn: ((...args: T[]) => void) | TestOptions, fnOrOptions?: ((...args: T[]) => void) | number | TestOptions) => {
const _name = formatName(name)
const arrayOnlyCases = cases.every(Array.isArray)

const { options, handler } = parseArguments(
optionsOrFn,
fnOrOptions,
)

cases.forEach((i, idx) => {
const items = Array.isArray(i) ? i : [i]

arrayOnlyCases
? test(formatTitle(_name, items, idx), () => fn(...items), options)
: test(formatTitle(_name, items, idx), () => fn(i), options)
? test(formatTitle(_name, items, idx), options, () => handler(...items))
: test(formatTitle(_name, items, idx), options, () => handler(i))
})

this.setContext('each', undefined)
Expand All @@ -279,8 +335,8 @@ export function createTaskCollector(
taskFn.extend = function (fixtures: Fixtures<Record<string, any>>) {
const _context = mergeContextFixtures(fixtures, context)

return createTest(function fn(name: string | Function, fn?: TestFunction, options?: number | TestOptions) {
getCurrentSuite().test.fn.call(this, formatName(name), fn, options)
return createTest(function fn(name: string | Function, optionsOrFn?: TestOptions | TestFunction, optionsOrTest?: number | TestOptions | TestFunction) {
getCurrentSuite().test.fn.call(this, formatName(name), optionsOrFn as TestOptions, optionsOrTest as TestFunction)
}, _context)
}

Expand All @@ -299,8 +355,8 @@ function createTest(fn: (
(
this: Record<'concurrent' | 'sequential' | 'skip' | 'only' | 'todo' | 'fails' | 'each', boolean | undefined> & { fixtures?: FixtureItem[] },
title: string,
fn?: TestFunction,
options?: number | TestOptions
optionsOrFn?: TestOptions | TestFunction,
optionsOrTest?: number | TestOptions | TestFunction,
) => void
), context?: Record<string, any>) {
return createTaskCollector(fn, context) as TestAPI
Expand Down
Loading

0 comments on commit 7d9b1fb

Please sign in to comment.