Skip to content

Commit

Permalink
feat(runner): implement test.for (#5861)
Browse files Browse the repository at this point in the history
Co-authored-by: Vladimir <sleuths.slews0s@icloud.com>
  • Loading branch information
hi-ogawa and sheremet-va authored Jun 11, 2024
1 parent 7cbd943 commit c238072
Show file tree
Hide file tree
Showing 6 changed files with 356 additions and 4 deletions.
48 changes: 46 additions & 2 deletions docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,11 @@ You cannot use this syntax, when using Vitest as [type checker](/guide/testing-t

- **Alias:** `it.each`

::: tip
While `test.each` is provided for Jest compatibility,
Vitest also has [`test.for`](#test-for) with an additional feature to integrate [`TestContext`](/guide/test-context).
:::

Use `test.each` when you need to run the same test with different variables.
You can inject parameters with [printf formatting](https://nodejs.org/api/util.html#util_util_format_format_args) in the test name in the order of the test function parameters.

Expand Down Expand Up @@ -392,8 +397,6 @@ test.each`
})
```

If you want to have access to `TestContext`, use `describe.each` with a single test.

::: tip
Vitest processes `$values` with Chai `format` method. If the value is too truncated, you can increase [chaiConfig.truncateThreshold](/config/#chaiconfig-truncatethreshold) in your config file.
:::
Expand All @@ -402,6 +405,47 @@ Vitest processes `$values` with Chai `format` method. If the value is too trunca
You cannot use this syntax, when using Vitest as [type checker](/guide/testing-types).
:::

### test.for

- **Alias:** `it.for`

Alternative of `test.each` to provide [`TestContext`](/guide/test-context).

The difference from `test.each` is how array case is provided in the arguments.
Other non array case (including template string usage) works exactly same.

```ts
// `each` spreads array case
test.each([
[1, 1, 2],
[1, 2, 3],
[2, 1, 3],
])('add(%i, %i) -> %i', (a, b, expected) => { // [!code --]
expect(a + b).toBe(expected)
})
// `for` doesn't spread array case
test.for([
[1, 1, 2],
[1, 2, 3],
[2, 1, 3],
])('add(%i, %i) -> %i', ([a, b, expected]) => { // [!code ++]
expect(a + b).toBe(expected)
})
```

2nd argument is [`TestContext`](/guide/test-context) and it can be used for concurrent snapshot, for example,

```ts
test.concurrent.for([
[1, 1],
[1, 2],
[2, 1],
])('add(%i, %i)', ([a, b], { expect }) => {
expect(a + b).matchSnapshot()
})
```

## bench

- **Type:** `(name: string | Function, fn: BenchFunction, options?: BenchOptions) => void`
Expand Down
8 changes: 7 additions & 1 deletion packages/runner/src/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,13 @@ function getUsedProps(fn: Function) {
if (!args.length)
return []

const first = args[0]
let first = args[0]
if ('__VITEST_FIXTURE_INDEX__' in fn) {
first = args[(fn as any).__VITEST_FIXTURE_INDEX__]
if (!first)
return []
}

if (!(first.startsWith('{') && first.endsWith('}')))
throw new Error(`The first argument inside a fixture must use object destructuring pattern, e.g. ({ test } => {}). Instead, received "${first}".`)

Expand Down
32 changes: 31 additions & 1 deletion packages/runner/src/suite.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { format, isNegativeNaN, isObject, objDisplay, objectAttr } from '@vitest/utils'
import { format, isNegativeNaN, isObject, objDisplay, objectAttr, toArray } from '@vitest/utils'
import { parseSingleStack } from '@vitest/utils/source-map'
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'
Expand Down Expand Up @@ -383,6 +383,36 @@ export function createTaskCollector(
}
}

taskFn.for = function <T>(
this: {
withContext: () => SuiteAPI
setContext: (key: string, value: boolean | undefined) => SuiteAPI
},
cases: ReadonlyArray<T>,
...args: any[]
) {
const test = this.withContext()

if (Array.isArray(cases) && args.length)
cases = formatTemplateString(cases, args)

return (
name: string | Function,
optionsOrFn: ((...args: T[]) => void) | TestOptions,
fnOrOptions?: ((...args: T[]) => void) | number | TestOptions,
) => {
const _name = formatName(name)
const { options, handler } = parseArguments(optionsOrFn, fnOrOptions)
cases.forEach((item, idx) => {
// monkey-patch handler to allow parsing fixture
const handlerWrapper = (ctx: any) => handler(item, ctx);
(handlerWrapper as any).__VITEST_FIXTURE_INDEX__ = 1;
(handlerWrapper as any).toString = () => handler.toString()
test(formatTitle(_name, toArray(item), idx), options, handlerWrapper)
})
}
}

taskFn.skipIf = function (this: TestAPI, condition: any) {
return condition ? this.skip : this
}
Expand Down
26 changes: 26 additions & 0 deletions packages/runner/src/types/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,31 @@ interface TestEachFunction {
(...args: [TemplateStringsArray, ...any]): EachFunctionReturn<any[]>
}

interface TestForFunctionReturn<Arg, Context> {
(
name: string | Function,
fn: (arg: Arg, context: Context) => Awaitable<void>,
): void
(
name: string | Function,
options: TestOptions,
fn: (args: Arg, context: Context) => Awaitable<void>,
): void
}

interface TestForFunction<ExtraContext> {
// test.for([1, 2, 3])
// test.for([[1, 2], [3, 4, 5]])
<T>(cases: ReadonlyArray<T>): TestForFunctionReturn<T, ExtendedContext<Test> & ExtraContext>

// test.for`
// a | b
// {1} | {2}
// {3} | {4}
// `
(strings: TemplateStringsArray, ...values: any[]): TestForFunctionReturn<any, ExtendedContext<Test> & ExtraContext>
}

interface TestCollectorCallable<C = {}> {
/**
* @deprecated Use options as the second argument instead
Expand All @@ -157,6 +182,7 @@ type ChainableTestAPI<ExtraContext = {}> = ChainableFunction<
TestCollectorCallable<ExtraContext>,
{
each: TestEachFunction
for: TestForFunction<ExtraContext>
}
>

Expand Down
149 changes: 149 additions & 0 deletions test/core/test/__snapshots__/test-for.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`[docs] add(1, 1) 1`] = `2`;

exports[`[docs] add(1, 2) 1`] = `3`;

exports[`[docs] add(2, 1) 1`] = `3`;

exports[`array case1-x case1-y 1`] = `
{
"args": [
"case1-x",
"case1-y",
],
"myFixture": 1234,
}
`;

exports[`array case2-x case2-y 1`] = `
{
"args": [
"case2-x",
"case2-y",
],
"myFixture": 1234,
}
`;

exports[`array destructure case1-x case1-y 1`] = `
{
"myFixture": 1234,
"x": "case1-x",
"y": "case1-y",
}
`;

exports[`array destructure case2-x case2-y 1`] = `
{
"myFixture": 1234,
"x": "case2-x",
"y": "case2-y",
}
`;

exports[`basic case1 1`] = `
{
"args": "case1",
}
`;

exports[`basic case2 1`] = `
{
"args": "case2",
}
`;

exports[`concurrent case1 1`] = `
{
"args": "case1",
"myFixture": 1234,
}
`;

exports[`concurrent case2 1`] = `
{
"args": "case2",
"myFixture": 1234,
}
`;

exports[`const case1 1`] = `
{
"args": "case1",
"myFixture": 1234,
}
`;

exports[`const case2 1`] = `
{
"args": "case2",
"myFixture": 1234,
}
`;

exports[`fixture case1 1`] = `
{
"args": "case1",
"myFixture": 1234,
}
`;

exports[`fixture case2 1`] = `
{
"args": "case2",
"myFixture": 1234,
}
`;

exports[`object 'case1' 1`] = `
{
"args": {
"k": "case1",
},
"myFixture": 1234,
}
`;

exports[`object 'case2' 1`] = `
{
"args": {
"k": "case2",
},
"myFixture": 1234,
}
`;

exports[`object destructure 'case1' 1`] = `
{
"myFixture": 1234,
"v": "case1",
}
`;

exports[`object destructure 'case2' 1`] = `
{
"myFixture": 1234,
"v": "case2",
}
`;

exports[`template 'x' true 1`] = `
{
"args": {
"a": "x",
"b": true,
},
"myFixture": 1234,
}
`;

exports[`template 'y' false 1`] = `
{
"args": {
"a": "y",
"b": false,
},
"myFixture": 1234,
}
`;
Loading

0 comments on commit c238072

Please sign in to comment.