From 15253890e6b1876b3de330d64c9b3f51c4642fc9 Mon Sep 17 00:00:00 2001 From: Han Feng Date: Mon, 5 Jun 2023 20:26:15 +0800 Subject: [PATCH] feat(runner): support using function/class as `describe`/`test` name (#3497) --- docs/api/index.md | 24 +++++++-------- packages/runner/src/suite.ts | 30 +++++++++++-------- packages/runner/src/types/tasks.ts | 24 +++++++-------- .../fixtures/function-as-name.test.ts | 28 +++++++++++++++++ test/reporters/tests/function-as-name.test.ts | 14 +++++++++ 5 files changed, 84 insertions(+), 36 deletions(-) create mode 100644 test/reporters/fixtures/function-as-name.test.ts create mode 100644 test/reporters/tests/function-as-name.test.ts diff --git a/docs/api/index.md b/docs/api/index.md index 5da908cc8b25..0ef06edccf81 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -36,7 +36,7 @@ In Jest, `TestFunction` can also be of type `(done: DoneCallback) => void`. If t ## test -- **Type:** `(name: string, fn: TestFunction, timeout?: number | TestOptions) => void` +- **Type:** `(name: string | Function, fn: TestFunction, timeout?: number | TestOptions) => void` - **Alias:** `it` `test` defines a set of related expectations. It receives the test name and a function that holds the expectations to test. @@ -53,7 +53,7 @@ In Jest, `TestFunction` can also be of type `(done: DoneCallback) => void`. If t ### test.skip -- **Type:** `(name: string, fn: TestFunction, timeout?: number | TestOptions) => void` +- **Type:** `(name: string | Function, fn: TestFunction, timeout?: number | TestOptions) => void` - **Alias:** `it.skip` If you want to skip running certain tests, but you don't want to delete the code due to any reason, you can use `test.skip` to avoid running them. @@ -111,7 +111,7 @@ You cannot use this syntax, when using Vitest as [type checker](/guide/testing-t ### test.only -- **Type:** `(name: string, fn: TestFunction, timeout?: number) => void` +- **Type:** `(name: string | Function, fn: TestFunction, timeout?: number) => void` - **Alias:** `it.only` Use `test.only` to only run certain tests in a given suite. This is useful when debugging. @@ -136,7 +136,7 @@ You cannot use this syntax, when using Vitest as [type checker](/guide/testing-t ### test.concurrent -- **Type:** `(name: string, fn: TestFunction, timeout?: number) => void` +- **Type:** `(name: string | Function, fn: TestFunction, timeout?: number) => void` - **Alias:** `it.concurrent` `test.concurrent` marks consecutive tests to be run in parallel. It receives the test name, an async function with the tests to collect, and an optional timeout (in milliseconds). @@ -179,7 +179,7 @@ You cannot use this syntax, when using Vitest as [type checker](/guide/testing-t ### test.todo -- **Type:** `(name: string) => void` +- **Type:** `(name: string | Function) => void` - **Alias:** `it.todo` Use `test.todo` to stub tests to be implemented later. An entry will be shown in the report for the tests so you know how many tests you still need to implement. @@ -191,7 +191,7 @@ You cannot use this syntax, when using Vitest as [type checker](/guide/testing-t ### test.fails -- **Type:** `(name: string, fn: TestFunction, timeout?: number) => void` +- **Type:** `(name: string | Function, fn: TestFunction, timeout?: number) => void` - **Alias:** `it.fails` Use `test.fails` to indicate that an assertion will fail explicitly. @@ -502,7 +502,7 @@ When you use `test` or `bench` in the top level of file, they are collected as p ### describe.skip -- **Type:** `(name: string, fn: TestFunction, options?: number | TestOptions) => void` +- **Type:** `(name: string | Function, fn: TestFunction, options?: number | TestOptions) => void` Use `describe.skip` in a suite to avoid running a particular describe block. @@ -539,7 +539,7 @@ You cannot use this syntax when using Vitest as [type checker](/guide/testing-ty ### describe.only -- **Type:** `(name: string, fn: TestFunction, options?: number | TestOptions) => void` +- **Type:** `(name: string | Function, fn: TestFunction, options?: number | TestOptions) => void` Use `describe.only` to only run certain suites @@ -565,7 +565,7 @@ You cannot use this syntax when using Vitest as [type checker](/guide/testing-ty ### describe.concurrent -- **Type:** `(name: string, fn: TestFunction, options?: number | TestOptions) => void` +- **Type:** `(name: string | Function, fn: TestFunction, options?: number | TestOptions) => void` `describe.concurrent` in a suite marks every tests as concurrent @@ -606,7 +606,7 @@ You cannot use this syntax, when using Vitest as [type checker](/guide/testing-t ### describe.shuffle -- **Type:** `(name: string, fn: TestFunction, options?: number | TestOptions) => void` +- **Type:** `(name: string | Function, fn: TestFunction, options?: number | TestOptions) => void` Vitest provides a way to run all tests in random order via CLI flag [`--sequence.shuffle`](/guide/cli) or config option [`sequence.shuffle`](/config/#sequence-shuffle), but if you want to have only part of your test suite to run tests in random order, you can mark it with this flag. @@ -627,7 +627,7 @@ You cannot use this syntax, when using Vitest as [type checker](/guide/testing-t ### describe.todo -- **Type:** `(name: string) => void` +- **Type:** `(name: string | Function) => void` Use `describe.todo` to stub suites to be implemented later. An entry will be shown in the report for the tests so you know how many tests you still need to implement. @@ -638,7 +638,7 @@ You cannot use this syntax, when using Vitest as [type checker](/guide/testing-t ### describe.each -- **Type:** `(cases: ReadonlyArray, ...args: any[]): (name: string, fn: (...args: T[]) => void, options?: number | TestOptions) => void` +- **Type:** `(cases: ReadonlyArray, ...args: any[]): (name: string | Function, fn: (...args: T[]) => void, options?: number | TestOptions) => void` Use `describe.each` if you have more than one test that depends on the same data. diff --git a/packages/runner/src/suite.ts b/packages/runner/src/suite.ts index e223b92de4e4..acd191eda960 100644 --- a/packages/runner/src/suite.ts +++ b/packages/runner/src/suite.ts @@ -8,8 +8,8 @@ import { getHooks, setFn, setHooks } from './map' // apis export const suite = createSuite() export const test = createTest( - function (name: string, fn?: TestFunction, options?: number | TestOptions) { - getCurrentSuite().test.fn.call(this, name, fn, options) + function (name: string | Function, fn?: TestFunction, options?: number | TestOptions) { + getCurrentSuite().test.fn.call(this, formatName(name), fn, options) }, ) @@ -59,7 +59,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m initSuite() - const test = createTest(function (name: string, fn = noop, options) { + const test = createTest(function (name: string | Function, fn = noop, options) { const mode = this.only ? 'only' : this.skip ? 'skip' : this.todo ? 'todo' : 'run' if (typeof options === 'number') @@ -78,7 +78,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m const test: Test = { id: '', type: 'test', - name, + name: formatName(name), each: this.each, mode, suite: undefined!, @@ -189,7 +189,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m } function createSuite() { - function suiteFn(this: Record, name: string, factory?: SuiteFactory, options?: number | TestOptions) { + function suiteFn(this: Record, name: string | Function, factory?: SuiteFactory, options?: number | TestOptions) { const mode: RunMode = this.only ? 'only' : this.skip ? 'skip' : this.todo ? 'todo' : 'run' const currentSuite = getCurrentSuite() @@ -200,7 +200,7 @@ function createSuite() { if (currentSuite?.options) options = { ...currentSuite.options, ...options } - return createSuiteCollector(name, factory, mode, this.concurrent, this.shuffle, this.each, options) + return createSuiteCollector(formatName(name), factory, mode, this.concurrent, this.shuffle, this.each, options) } suiteFn.each = function(this: { withContext: () => SuiteAPI; setContext: (key: string, value: boolean | undefined) => SuiteAPI }, cases: ReadonlyArray, ...args: any[]) { @@ -210,13 +210,14 @@ function createSuite() { if (Array.isArray(cases) && args.length) cases = formatTemplateString(cases, args) - return (name: string, fn: (...args: T[]) => void, options?: number | TestOptions) => { + return (name: string | Function, fn: (...args: T[]) => void, options?: number | TestOptions) => { + const _name = formatName(name) const arrayOnlyCases = cases.every(Array.isArray) cases.forEach((i, idx) => { const items = Array.isArray(i) ? i : [i] arrayOnlyCases - ? suite(formatTitle(name, items, idx), () => fn(...items), options) - : suite(formatTitle(name, items, idx), () => fn(i), options) + ? suite(formatTitle(_name, items, idx), () => fn(...items), options) + : suite(formatTitle(_name, items, idx), () => fn(i), options) }) this.setContext('each', undefined) @@ -249,14 +250,15 @@ function createTest(fn: ( if (Array.isArray(cases) && args.length) cases = formatTemplateString(cases, args) - return (name: string, fn: (...args: T[]) => void, options?: number | TestOptions) => { + return (name: string | Function, fn: (...args: T[]) => void, options?: number | TestOptions) => { + const _name = formatName(name) const arrayOnlyCases = cases.every(Array.isArray) cases.forEach((i, idx) => { const items = Array.isArray(i) ? i : [i] arrayOnlyCases - ? test(formatTitle(name, items, idx), () => fn(...items), options) - : test(formatTitle(name, items, idx), () => fn(i), options) + ? test(formatTitle(_name, items, idx), () => fn(...items), options) + : test(formatTitle(_name, items, idx), () => fn(i), options) }) this.setContext('each', undefined) @@ -272,6 +274,10 @@ function createTest(fn: ( ) as TestAPI } +function formatName(name: string | Function) { + return typeof name === 'string' ? name : name instanceof Function ? name.name : String(name) +} + function formatTitle(template: string, items: any[], idx: number) { if (template.includes('%#')) { // '%#' match index of the test case diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts index 7ff9c4e4b7b1..eeca49956da7 100644 --- a/packages/runner/src/types/tasks.ts +++ b/packages/runner/src/types/tasks.ts @@ -112,37 +112,37 @@ type ExtractEachCallbackArgs> = { interface SuiteEachFunction { (cases: ReadonlyArray): ( - name: string, + name: string | Function, fn: (...args: T) => Awaitable, ) => void >(cases: ReadonlyArray): ( - name: string, + name: string | Function, fn: (...args: ExtractEachCallbackArgs) => Awaitable, ) => void (cases: ReadonlyArray): ( - name: string, + name: string | Function, fn: (...args: T[]) => Awaitable, ) => void } interface TestEachFunction { (cases: ReadonlyArray): ( - name: string, + name: string | Function, fn: (...args: T) => Awaitable, options?: number | TestOptions, ) => void >(cases: ReadonlyArray): ( - name: string, + name: string | Function, fn: (...args: ExtractEachCallbackArgs) => Awaitable, options?: number | TestOptions, ) => void (cases: ReadonlyArray): ( - name: string, + name: string | Function, fn: (...args: T[]) => Awaitable, options?: number | TestOptions, ) => void (...args: [TemplateStringsArray, ...any]): ( - name: string, + name: string | Function, fn: (...args: any[]) => Awaitable, options?: number | TestOptions, ) => void @@ -150,11 +150,11 @@ interface TestEachFunction { type ChainableTestAPI = ChainableFunction< 'concurrent' | 'only' | 'skip' | 'todo' | 'fails', - [name: string, fn?: TestFunction, options?: number | TestOptions], + [name: string | Function, fn?: TestFunction, options?: number | TestOptions], void, { each: TestEachFunction - (name: string, fn?: TestFunction, options?: number | TestOptions): void + (name: string | Function, fn?: TestFunction, options?: number | TestOptions): void } > @@ -188,11 +188,11 @@ export type TestAPI = ChainableTestAPI & { type ChainableSuiteAPI = ChainableFunction< 'concurrent' | 'only' | 'skip' | 'todo' | 'shuffle', - [name: string, factory?: SuiteFactory, options?: number | TestOptions], + [name: string | Function, factory?: SuiteFactory, options?: number | TestOptions], SuiteCollector, { each: TestEachFunction - (name: string, factory?: SuiteFactory): SuiteCollector + (name: string | Function, factory?: SuiteFactory): SuiteCollector } > @@ -226,7 +226,7 @@ export interface SuiteCollector { on: >(name: T, ...fn: SuiteHooks[T]) => void } -export type SuiteFactory = (test: (name: string, fn: TestFunction) => void) => Awaitable +export type SuiteFactory = (test: (name: string | Function, fn: TestFunction) => void) => Awaitable export interface RuntimeContext { tasks: (SuiteCollector | Test)[] diff --git a/test/reporters/fixtures/function-as-name.test.ts b/test/reporters/fixtures/function-as-name.test.ts new file mode 100644 index 000000000000..fb01790f0ec8 --- /dev/null +++ b/test/reporters/fixtures/function-as-name.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, test } from 'vitest' + +function foo() {} +class Bar {} + +describe(foo, () => { + test(Bar, () => { + expect(0).toBe(0) + }) +}) + +describe(Bar, () => { + test(foo, () => { + expect(0).toBe(0) + }) +}) + +describe.each([1])(foo, () => { + test.each([1])(foo, () => { + expect(0).toBe(0) + }) +}) + +describe.each([1])(Bar, () => { + test.each([1])(Bar, () => { + expect(0).toBe(0) + }) +}) diff --git a/test/reporters/tests/function-as-name.test.ts b/test/reporters/tests/function-as-name.test.ts new file mode 100644 index 000000000000..75714f30b053 --- /dev/null +++ b/test/reporters/tests/function-as-name.test.ts @@ -0,0 +1,14 @@ +import { expect, test } from 'vitest' +import { resolve } from 'pathe' +import { runVitest } from '../../test-utils' + +test('should print function name', async () => { + const filename = resolve('./fixtures/function-as-name.test.ts') + const { stdout } = await runVitest({ root: './fixtures' }, [filename]) + + expect(stdout).toBeTruthy() + expect(stdout).toContain('function-as-name.test.ts > foo > Bar') + expect(stdout).toContain('function-as-name.test.ts > Bar > foo') + expect(stdout).toContain('function-as-name.test.ts > foo > foo') + expect(stdout).toContain('function-as-name.test.ts > Bar > Bar') +})