Skip to content

Commit c238072

Browse files
feat(runner): implement test.for (#5861)
Co-authored-by: Vladimir <[email protected]>
1 parent 7cbd943 commit c238072

File tree

6 files changed

+356
-4
lines changed

6 files changed

+356
-4
lines changed

docs/api/index.md

+46-2
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,11 @@ You cannot use this syntax, when using Vitest as [type checker](/guide/testing-t
306306

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

309+
::: tip
310+
While `test.each` is provided for Jest compatibility,
311+
Vitest also has [`test.for`](#test-for) with an additional feature to integrate [`TestContext`](/guide/test-context).
312+
:::
313+
309314
Use `test.each` when you need to run the same test with different variables.
310315
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.
311316

@@ -392,8 +397,6 @@ test.each`
392397
})
393398
```
394399

395-
If you want to have access to `TestContext`, use `describe.each` with a single test.
396-
397400
::: tip
398401
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.
399402
:::
@@ -402,6 +405,47 @@ Vitest processes `$values` with Chai `format` method. If the value is too trunca
402405
You cannot use this syntax, when using Vitest as [type checker](/guide/testing-types).
403406
:::
404407

408+
### test.for
409+
410+
- **Alias:** `it.for`
411+
412+
Alternative of `test.each` to provide [`TestContext`](/guide/test-context).
413+
414+
The difference from `test.each` is how array case is provided in the arguments.
415+
Other non array case (including template string usage) works exactly same.
416+
417+
```ts
418+
// `each` spreads array case
419+
test.each([
420+
[1, 1, 2],
421+
[1, 2, 3],
422+
[2, 1, 3],
423+
])('add(%i, %i) -> %i', (a, b, expected) => { // [!code --]
424+
expect(a + b).toBe(expected)
425+
})
426+
427+
// `for` doesn't spread array case
428+
test.for([
429+
[1, 1, 2],
430+
[1, 2, 3],
431+
[2, 1, 3],
432+
])('add(%i, %i) -> %i', ([a, b, expected]) => { // [!code ++]
433+
expect(a + b).toBe(expected)
434+
})
435+
```
436+
437+
2nd argument is [`TestContext`](/guide/test-context) and it can be used for concurrent snapshot, for example,
438+
439+
```ts
440+
test.concurrent.for([
441+
[1, 1],
442+
[1, 2],
443+
[2, 1],
444+
])('add(%i, %i)', ([a, b], { expect }) => {
445+
expect(a + b).matchSnapshot()
446+
})
447+
```
448+
405449
## bench
406450

407451
- **Type:** `(name: string | Function, fn: BenchFunction, options?: BenchOptions) => void`

packages/runner/src/fixture.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,13 @@ function getUsedProps(fn: Function) {
177177
if (!args.length)
178178
return []
179179

180-
const first = args[0]
180+
let first = args[0]
181+
if ('__VITEST_FIXTURE_INDEX__' in fn) {
182+
first = args[(fn as any).__VITEST_FIXTURE_INDEX__]
183+
if (!first)
184+
return []
185+
}
186+
181187
if (!(first.startsWith('{') && first.endsWith('}')))
182188
throw new Error(`The first argument inside a fixture must use object destructuring pattern, e.g. ({ test } => {}). Instead, received "${first}".`)
183189

packages/runner/src/suite.ts

+31-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { format, isNegativeNaN, isObject, objDisplay, objectAttr } from '@vitest/utils'
1+
import { format, isNegativeNaN, isObject, objDisplay, objectAttr, toArray } from '@vitest/utils'
22
import { parseSingleStack } from '@vitest/utils/source-map'
33
import type { Custom, CustomAPI, File, Fixtures, RunMode, Suite, SuiteAPI, SuiteCollector, SuiteFactory, SuiteHooks, Task, TaskCustomOptions, Test, TestAPI, TestFunction, TestOptions } from './types'
44
import type { VitestRunner } from './types/runner'
@@ -383,6 +383,36 @@ export function createTaskCollector(
383383
}
384384
}
385385

386+
taskFn.for = function <T>(
387+
this: {
388+
withContext: () => SuiteAPI
389+
setContext: (key: string, value: boolean | undefined) => SuiteAPI
390+
},
391+
cases: ReadonlyArray<T>,
392+
...args: any[]
393+
) {
394+
const test = this.withContext()
395+
396+
if (Array.isArray(cases) && args.length)
397+
cases = formatTemplateString(cases, args)
398+
399+
return (
400+
name: string | Function,
401+
optionsOrFn: ((...args: T[]) => void) | TestOptions,
402+
fnOrOptions?: ((...args: T[]) => void) | number | TestOptions,
403+
) => {
404+
const _name = formatName(name)
405+
const { options, handler } = parseArguments(optionsOrFn, fnOrOptions)
406+
cases.forEach((item, idx) => {
407+
// monkey-patch handler to allow parsing fixture
408+
const handlerWrapper = (ctx: any) => handler(item, ctx);
409+
(handlerWrapper as any).__VITEST_FIXTURE_INDEX__ = 1;
410+
(handlerWrapper as any).toString = () => handler.toString()
411+
test(formatTitle(_name, toArray(item), idx), options, handlerWrapper)
412+
})
413+
}
414+
}
415+
386416
taskFn.skipIf = function (this: TestAPI, condition: any) {
387417
return condition ? this.skip : this
388418
}

packages/runner/src/types/tasks.ts

+26
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,31 @@ interface TestEachFunction {
143143
(...args: [TemplateStringsArray, ...any]): EachFunctionReturn<any[]>
144144
}
145145

146+
interface TestForFunctionReturn<Arg, Context> {
147+
(
148+
name: string | Function,
149+
fn: (arg: Arg, context: Context) => Awaitable<void>,
150+
): void
151+
(
152+
name: string | Function,
153+
options: TestOptions,
154+
fn: (args: Arg, context: Context) => Awaitable<void>,
155+
): void
156+
}
157+
158+
interface TestForFunction<ExtraContext> {
159+
// test.for([1, 2, 3])
160+
// test.for([[1, 2], [3, 4, 5]])
161+
<T>(cases: ReadonlyArray<T>): TestForFunctionReturn<T, ExtendedContext<Test> & ExtraContext>
162+
163+
// test.for`
164+
// a | b
165+
// {1} | {2}
166+
// {3} | {4}
167+
// `
168+
(strings: TemplateStringsArray, ...values: any[]): TestForFunctionReturn<any, ExtendedContext<Test> & ExtraContext>
169+
}
170+
146171
interface TestCollectorCallable<C = {}> {
147172
/**
148173
* @deprecated Use options as the second argument instead
@@ -157,6 +182,7 @@ type ChainableTestAPI<ExtraContext = {}> = ChainableFunction<
157182
TestCollectorCallable<ExtraContext>,
158183
{
159184
each: TestEachFunction
185+
for: TestForFunction<ExtraContext>
160186
}
161187
>
162188

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`[docs] add(1, 1) 1`] = `2`;
4+
5+
exports[`[docs] add(1, 2) 1`] = `3`;
6+
7+
exports[`[docs] add(2, 1) 1`] = `3`;
8+
9+
exports[`array case1-x case1-y 1`] = `
10+
{
11+
"args": [
12+
"case1-x",
13+
"case1-y",
14+
],
15+
"myFixture": 1234,
16+
}
17+
`;
18+
19+
exports[`array case2-x case2-y 1`] = `
20+
{
21+
"args": [
22+
"case2-x",
23+
"case2-y",
24+
],
25+
"myFixture": 1234,
26+
}
27+
`;
28+
29+
exports[`array destructure case1-x case1-y 1`] = `
30+
{
31+
"myFixture": 1234,
32+
"x": "case1-x",
33+
"y": "case1-y",
34+
}
35+
`;
36+
37+
exports[`array destructure case2-x case2-y 1`] = `
38+
{
39+
"myFixture": 1234,
40+
"x": "case2-x",
41+
"y": "case2-y",
42+
}
43+
`;
44+
45+
exports[`basic case1 1`] = `
46+
{
47+
"args": "case1",
48+
}
49+
`;
50+
51+
exports[`basic case2 1`] = `
52+
{
53+
"args": "case2",
54+
}
55+
`;
56+
57+
exports[`concurrent case1 1`] = `
58+
{
59+
"args": "case1",
60+
"myFixture": 1234,
61+
}
62+
`;
63+
64+
exports[`concurrent case2 1`] = `
65+
{
66+
"args": "case2",
67+
"myFixture": 1234,
68+
}
69+
`;
70+
71+
exports[`const case1 1`] = `
72+
{
73+
"args": "case1",
74+
"myFixture": 1234,
75+
}
76+
`;
77+
78+
exports[`const case2 1`] = `
79+
{
80+
"args": "case2",
81+
"myFixture": 1234,
82+
}
83+
`;
84+
85+
exports[`fixture case1 1`] = `
86+
{
87+
"args": "case1",
88+
"myFixture": 1234,
89+
}
90+
`;
91+
92+
exports[`fixture case2 1`] = `
93+
{
94+
"args": "case2",
95+
"myFixture": 1234,
96+
}
97+
`;
98+
99+
exports[`object 'case1' 1`] = `
100+
{
101+
"args": {
102+
"k": "case1",
103+
},
104+
"myFixture": 1234,
105+
}
106+
`;
107+
108+
exports[`object 'case2' 1`] = `
109+
{
110+
"args": {
111+
"k": "case2",
112+
},
113+
"myFixture": 1234,
114+
}
115+
`;
116+
117+
exports[`object destructure 'case1' 1`] = `
118+
{
119+
"myFixture": 1234,
120+
"v": "case1",
121+
}
122+
`;
123+
124+
exports[`object destructure 'case2' 1`] = `
125+
{
126+
"myFixture": 1234,
127+
"v": "case2",
128+
}
129+
`;
130+
131+
exports[`template 'x' true 1`] = `
132+
{
133+
"args": {
134+
"a": "x",
135+
"b": true,
136+
},
137+
"myFixture": 1234,
138+
}
139+
`;
140+
141+
exports[`template 'y' false 1`] = `
142+
{
143+
"args": {
144+
"a": "y",
145+
"b": false,
146+
},
147+
"myFixture": 1234,
148+
}
149+
`;

0 commit comments

Comments
 (0)