From 0ad2860bf6647b4951aea69b60b76d05117518c8 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 15 Jan 2025 16:49:42 +0900 Subject: [PATCH] feat: add `describe.for` (#7253) --- docs/api/index.md | 36 +++++++++++++++ packages/runner/src/suite.ts | 25 +++++++++++ packages/runner/src/types/tasks.ts | 6 +++ .../__snapshots__/test-for-suite.test.ts.snap | 45 +++++++++++++++++++ test/core/test/test-for-suite.test.ts | 35 +++++++++++++++ 5 files changed, 147 insertions(+) create mode 100644 test/core/test/__snapshots__/test-for-suite.test.ts.snap create mode 100644 test/core/test/test-for-suite.test.ts diff --git a/docs/api/index.md b/docs/api/index.md index fadfe14233dc..729ca7576b3d 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -948,6 +948,11 @@ describe.todo('unimplemented suite') - **Alias:** `suite.each` +::: tip +While `describe.each` is provided for Jest compatibility, +Vitest also has [`describe.for`](#describe-for) which simplifies argument types and aligns with [`test.for`](#test-for). +::: + Use `describe.each` if you have more than one test that depends on the same data. ```ts @@ -998,6 +1003,37 @@ describe.each` You cannot use this syntax when using Vitest as [type checker](/guide/testing-types). ::: +### describe.for + +- **Alias:** `suite.for` + +The difference from `describe.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 +describe.each([ + [1, 1, 2], + [1, 2, 3], + [2, 1, 3], +])('add(%i, %i) -> %i', (a, b, expected) => { // [!code --] + test('test', () => { + expect(a + b).toBe(expected) + }) +}) + +// `for` doesn't spread array case +describe.for([ + [1, 1, 2], + [1, 2, 3], + [2, 1, 3], +])('add(%i, %i) -> %i', ([a, b, expected]) => { // [!code ++] + test('test', () => { + expect(a + b).toBe(expected) + }) +}) +``` + ## Setup and Teardown These functions allow you to hook into the life cycle of tests to avoid repeating setup and teardown code. They apply to the current context: the file if they are used at the top-level or the current suite if they are inside a `describe` block. These hooks are not called, when you are running Vitest as a type checker. diff --git a/packages/runner/src/suite.ts b/packages/runner/src/suite.ts index 7ba7254c0a00..360a36c45709 100644 --- a/packages/runner/src/suite.ts +++ b/packages/runner/src/suite.ts @@ -594,6 +594,31 @@ function createSuite() { } } + suiteFn.for = function ( + this: { + withContext: () => SuiteAPI + setContext: (key: string, value: boolean | undefined) => SuiteAPI + }, + cases: ReadonlyArray, + ...args: any[] + ) { + 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) => { + suite(formatTitle(name_, toArray(item), idx), options, () => handler(item)) + }) + } + } + suiteFn.skipIf = (condition: any) => (condition ? suite.skip : suite) as SuiteAPI suiteFn.runIf = (condition: any) => diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts index 48ea70d77381..c68c9c045543 100644 --- a/packages/runner/src/types/tasks.ts +++ b/packages/runner/src/types/tasks.ts @@ -360,6 +360,11 @@ interface TestForFunction { > } +interface SuiteForFunction { + (cases: ReadonlyArray): EachFunctionReturn<[T]> + (...args: [TemplateStringsArray, ...any]): EachFunctionReturn +} + interface TestCollectorCallable { /** * @deprecated Use options as the second argument instead @@ -525,6 +530,7 @@ type ChainableSuiteAPI = ChainableFunction< SuiteCollectorCallable, { each: TestEachFunction + for: SuiteForFunction } > diff --git a/test/core/test/__snapshots__/test-for-suite.test.ts.snap b/test/core/test/__snapshots__/test-for-suite.test.ts.snap new file mode 100644 index 000000000000..d19a26c4753e --- /dev/null +++ b/test/core/test/__snapshots__/test-for-suite.test.ts.snap @@ -0,0 +1,45 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`add(1, 1) > test 1`] = `2`; + +exports[`add(1, 2) > test 1`] = `3`; + +exports[`add(2, 1) > test 1`] = `3`; + +exports[`basic case1 > test 1`] = ` +{ + "args": [ + "case1", + ], +} +`; + +exports[`basic case2 > test 1`] = ` +{ + "args": [ + "case2", + ], +} +`; + +exports[`template 'x' true > test 1`] = ` +{ + "args": [ + { + "a": "x", + "b": true, + }, + ], +} +`; + +exports[`template 'y' false > test 1`] = ` +{ + "args": [ + { + "a": "y", + "b": false, + }, + ], +} +`; diff --git a/test/core/test/test-for-suite.test.ts b/test/core/test/test-for-suite.test.ts new file mode 100644 index 000000000000..8e849ebf7ba5 --- /dev/null +++ b/test/core/test/test-for-suite.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, expectTypeOf, test } from 'vitest' + +describe.for(['case1', 'case2'])( + 'basic %s', + (...args) => { + test('test', () => { + expectTypeOf(args).toEqualTypeOf<[string]>() + expect({ args }).matchSnapshot() + }) + }, +) + +describe.for` + a | b + ${'x'} | ${true} + ${'y'} | ${false} +`( + 'template $a $b', + (...args) => { + test('test', () => { + expectTypeOf(args).toEqualTypeOf() + expect({ args }).toMatchSnapshot() + }) + }, +) + +describe.for([ + [1, 1], + [1, 2], + [2, 1], +])('add(%i, %i)', ([a, b]) => { + test('test', () => { + expect(a + b).matchSnapshot() + }) +})