Skip to content

Commit

Permalink
feat: test.each support string template (#2337)
Browse files Browse the repository at this point in the history
  • Loading branch information
poyoho committed Nov 22, 2022
1 parent c395177 commit f789776
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 4 deletions.
40 changes: 38 additions & 2 deletions docs/api/index.md
Expand Up @@ -196,7 +196,7 @@ You cannot use this syntax, when using Vitest as [type checker](/guide/testing-t
:::

### test.each
- **Type:** `(cases: ReadonlyArray<T>) => void`
- **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 @@ -243,6 +243,23 @@ You cannot use this syntax, when using Vitest as [type checker](/guide/testing-t
// ✓ add(2, 1) -> 3
```

You can also access template string table.
* First row of variable name column headings separated with `|`
* One or more subsequent rows of data supplied as template literal expressions using ${value} syntax.

```ts
test.each`
a | b | expected
${1} | ${1} | ${2}
${'a'} | ${'b'} | ${'ab'}
${[]} | ${'b'} | ${'b'}
${{}} | ${'b'} | ${'[object Object]b'}
${{ asd: 1 }} | ${'b'} | ${'[object Object]b'}
`('returns $expected when $a is added $b', ({ a, b, expected }) => {
expect(a + b).toBe(expected)
})
```

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

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

### describe.each

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

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

Expand All @@ -571,6 +588,25 @@ You cannot use this syntax, when using Vitest as [type checker](/guide/testing-t
})
```

You can also access template string table.
* First row of variable name column headings separated with `|`
* One or more subsequent rows of data supplied as template literal expressions using ${value} syntax.

```ts
describe.each`
a | b | expected
${1} | ${1} | ${2}
${'a'} | ${'b'} | ${'ab'}
${[]} | ${'b'} | ${'b'}
${{}} | ${'b'} | ${'[object Object]b'}
${{ asd: 1 }} | ${'b'} | ${'[object Object]b'}
`('describe template string add($a, $b)', ({ a, b, expected }) => {
test(`returns ${expected}`, () => {
expect(a + b).toBe(expected)
})
})
```

::: warning
You cannot use this syntax, when using Vitest as [type checker](/guide/testing-types).
:::
Expand Down
23 changes: 21 additions & 2 deletions packages/vitest/src/runtime/suite.ts
Expand Up @@ -180,8 +180,12 @@ function createSuite() {
return createSuiteCollector(name, factory, mode, this.concurrent, this.shuffle, options)
}

suiteFn.each = function<T>(this: { withContext: () => SuiteAPI }, cases: ReadonlyArray<T>) {
suiteFn.each = function<T>(this: { withContext: () => SuiteAPI }, cases: ReadonlyArray<T>, ...args: any[]) {
const suite = this.withContext()

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

return (name: string, fn: (...args: T[]) => void, options?: number | TestOptions) => {
const arrayOnlyCases = cases.every(Array.isArray)
cases.forEach((i, idx) => {
Expand Down Expand Up @@ -212,9 +216,12 @@ function createTest(fn: (
)) {
const testFn = fn as any

testFn.each = function<T>(this: { withContext: () => TestAPI }, cases: ReadonlyArray<T>) {
testFn.each = function<T>(this: { withContext: () => TestAPI }, cases: ReadonlyArray<T>, ...args: any[]) {
const test = this.withContext()

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

return (name: string, fn: (...args: T[]) => void, options?: number | TestOptions) => {
const arrayOnlyCases = cases.every(Array.isArray)
cases.forEach((i, idx) => {
Expand Down Expand Up @@ -272,3 +279,15 @@ function formatTitle(template: string, items: any[], idx: number) {
}
return formatted
}

function formatTemplateString(cases: any[], args: any[]): any[] {
const header = cases.join('').trim().replace(/ /g, '').split('\n').map(i => i.split('|'))[0]
const res: any[] = []
for (let i = 0; i < Math.floor((args.length) / header.length); i++) {
const oneCase: Record<string, any> = {}
for (let j = 0; j < header.length; j++)
oneCase[header[j]] = args[i * header.length + j] as any
res.push(oneCase)
}
return res
}
5 changes: 5 additions & 0 deletions packages/vitest/src/types/tasks.ts
Expand Up @@ -130,6 +130,11 @@ interface TestEachFunction {
fn: (...args: T[]) => Awaitable<void>,
options?: number | TestOptions,
) => void
(...args: [TemplateStringsArray, ...any]): (
name: string,
fn: (...args: any[]) => Awaitable<void>,
options?: number | TestOptions,
) => void
}

type ChainableTestAPI<ExtraContext = {}> = ChainableFunction<
Expand Down
39 changes: 39 additions & 0 deletions test/core/test/each.test.ts
Expand Up @@ -169,3 +169,42 @@ test.each([
expect(value1).toBeNull()
expect(value2).toBeNull()
})

describe.each`
a | b | expected
${1} | ${1} | ${2}
${'a'} | ${'b'} | ${'ab'}
${[]} | ${'b'} | ${'b'}
${{}} | ${'b'} | ${'[object Object]b'}
${{ asd: 1 }} | ${'b'} | ${'[object Object]b'}
`('describe template string add($a, $b)', ({ a, b, expected }) => {
test(`returns ${expected}`, () => {
expect(a + b).toBe(expected)
})
})

test.each`
a | b | expected
${1} | ${1} | ${2}
${'a'} | ${'b'} | ${'ab'}
${[]} | ${'b'} | ${'b'}
${{}} | ${'b'} | ${'[object Object]b'}
${{ asd: 1 }} | ${'b'} | ${'[object Object]b'}
`('returns $expected when $a is added $b', ({ a, b, expected }) => {
expect(a + b).toBe(expected)
})

test.each`
a | b | expected
${true} | ${true} | ${true}
`('($a && $b) -> $expected', ({ a, b, expected }) => {
expect(a && b).toBe(expected)
})

test.each`
a | b | expected
${{ val: 1 }} | ${{ val: 2 }}} | ${3}
`('($a && $b) -> $expected', ({ a, b, expected }) => {
expect(a.val + b.val).toBe(expected)
})

0 comments on commit f789776

Please sign in to comment.