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(runner): implement test.for #5861

Merged
merged 6 commits into from
Jun 11, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading