diff --git a/CHANGELOG.md b/CHANGELOG.md index b086303c9286..b7ef82ef73a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - `[babel-jest]` Export `createTransformer` function ([#12399](https://github.com/facebook/jest/pull/12399)) - `[expect]` Expose `AsymmetricMatchers`, `MatcherFunction` and `MatcherFunctionWithState` interfaces ([#12363](https://github.com/facebook/jest/pull/12363), [#12376](https://github.com/facebook/jest/pull/12376)) +- `[jest-circus, jest-jasmine2]` Allowed classes and functions as `describe` and `it`/`test` names ([#12484](https://github.com/facebook/jest/pull/12484)) - `[jest-cli, jest-config]` [**BREAKING**] Remove `testURL` config, use `testEnvironmentOptions.url` instead ([#10797](https://github.com/facebook/jest/pull/10797)) - `[jest-config]` [**BREAKING**] Stop shipping `jest-environment-jsdom` by default ([#12354](https://github.com/facebook/jest/pull/12354)) - `[jest-config]` [**BREAKING**] Stop shipping `jest-jasmine2` by default ([#12355](https://github.com/facebook/jest/pull/12355)) diff --git a/e2e/__tests__/__snapshots__/globals.test.ts.snap b/e2e/__tests__/__snapshots__/globals.test.ts.snap index e9071a204e06..47e0dd3db70d 100644 --- a/e2e/__tests__/__snapshots__/globals.test.ts.snap +++ b/e2e/__tests__/__snapshots__/globals.test.ts.snap @@ -81,13 +81,26 @@ Time: <> Ran all test suites." `; -exports[`function as descriptor 1`] = ` +exports[`function as describe() descriptor 1`] = ` "PASS __tests__/functionAsDescriptor.test.js Foo ✓ it" `; -exports[`function as descriptor 2`] = ` +exports[`function as describe() descriptor 2`] = ` +"Test Suites: 1 passed, 1 total +Tests: 1 passed, 1 total +Snapshots: 0 total +Time: <> +Ran all test suites." +`; + +exports[`function as it() descriptor 1`] = ` +"PASS __tests__/functionAsDescriptor.test.js + ✓ Foo" +`; + +exports[`function as it() descriptor 2`] = ` "Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total diff --git a/e2e/__tests__/globals.test.ts b/e2e/__tests__/globals.test.ts index b90315cdb732..587efa3ed2a1 100644 --- a/e2e/__tests__/globals.test.ts +++ b/e2e/__tests__/globals.test.ts @@ -262,7 +262,7 @@ test('cannot test with no implementation with expand arg', () => { expect(exitCode).toBe(1); }); -test('function as descriptor', () => { +test('function as describe() descriptor', () => { const filename = 'functionAsDescriptor.test.js'; const content = ` function Foo() {} @@ -279,3 +279,19 @@ test('function as descriptor', () => { expect(summary).toMatchSnapshot(); expect(exitCode).toBe(0); }); + +test('function as it() descriptor', () => { + const filename = 'functionAsDescriptor.test.js'; + const content = ` + function Foo() {} + it(Foo, () => {}); + `; + + writeFiles(TEST_DIR, {[filename]: content}); + const {stderr, exitCode} = runJest(DIR); + + const {summary, rest} = extractSummary(stderr); + expect(rest).toMatchSnapshot(); + expect(summary).toMatchSnapshot(); + expect(exitCode).toBe(0); +}); diff --git a/packages/jest-circus/src/__tests__/__snapshots__/baseTest.test.ts.snap b/packages/jest-circus/src/__tests__/__snapshots__/baseTest.test.ts.snap index a7e8e0cba778..f44411f9ac58 100644 --- a/packages/jest-circus/src/__tests__/__snapshots__/baseTest.test.ts.snap +++ b/packages/jest-circus/src/__tests__/__snapshots__/baseTest.test.ts.snap @@ -33,6 +33,24 @@ run_finish unhandledErrors: 0" `; +exports[`function descriptors 1`] = ` +"start_describe_definition: describer +add_test: One +finish_describe_definition: describer +run_start +run_describe_start: ROOT_DESCRIBE_BLOCK +run_describe_start: describer +test_start: One +test_fn_start: One +test_fn_success: One +test_done: One +run_describe_finish: describer +run_describe_finish: ROOT_DESCRIBE_BLOCK +run_finish + +unhandledErrors: 0" +`; + exports[`simple test 1`] = ` "start_describe_definition: describe add_hook: beforeEach diff --git a/packages/jest-circus/src/__tests__/baseTest.test.ts b/packages/jest-circus/src/__tests__/baseTest.test.ts index 429edbb39d43..2b98c9371a55 100644 --- a/packages/jest-circus/src/__tests__/baseTest.test.ts +++ b/packages/jest-circus/src/__tests__/baseTest.test.ts @@ -20,6 +20,16 @@ test('simple test', () => { expect(stdout).toMatchSnapshot(); }); +test('function descriptors', () => { + const {stdout} = runTest(` + describe(function describer() {}, () => { + test(class One {}, () => {}); + }) + `); + + expect(stdout).toMatchSnapshot(); +}); + test('failures', () => { const {stdout} = runTest(` describe('describe', () => { diff --git a/packages/jest-circus/src/__tests__/circusItTestError.test.ts b/packages/jest-circus/src/__tests__/circusItTestError.test.ts index d75acece9f78..f4f7a03d3611 100644 --- a/packages/jest-circus/src/__tests__/circusItTestError.test.ts +++ b/packages/jest-circus/src/__tests__/circusItTestError.test.ts @@ -35,11 +35,13 @@ describe('test/it error throwing', () => { 'Missing second argument. It must be a callback function. Perhaps you want to use `test.todo` for a test placeholder.', ); }); - it("it throws an error when first argument isn't a string", () => { + it("it throws an error when first argument isn't valid", () => { expect(() => { // @ts-expect-error: Easy, we're testing runtime errors here circusIt(() => {}); - }).toThrowError('Invalid first argument, () => {}. It must be a string.'); + }).toThrowError( + 'Invalid first argument, () => {}. It must be a named class, named function, number, or string.', + ); }); it('it throws an error when callback function is not a function', () => { expect(() => { @@ -66,7 +68,9 @@ describe('test/it error throwing', () => { expect(() => { // @ts-expect-error: Easy, we're testing runtime errors here circusTest(() => {}); - }).toThrowError('Invalid first argument, () => {}. It must be a string.'); + }).toThrowError( + 'Invalid first argument, () => {}. It must be a named class, named function, number, or string.', + ); }); it('test throws an error when callback function is not a function', () => { expect(() => { diff --git a/packages/jest-circus/src/index.ts b/packages/jest-circus/src/index.ts index 6a19ea87b71e..33c4e43dec50 100644 --- a/packages/jest-circus/src/index.ts +++ b/packages/jest-circus/src/index.ts @@ -7,7 +7,7 @@ import type {Circus, Global} from '@jest/types'; import {bind as bindEach} from 'jest-each'; -import {ErrorWithStack, isPromise} from 'jest-util'; +import {ErrorWithStack, convertDescriptorToString, isPromise} from 'jest-util'; import {dispatchSync} from './state'; export {setState, getState, resetState} from './state'; @@ -15,16 +15,16 @@ export {default as run} from './run'; type THook = (fn: Circus.HookFn, timeout?: number) => void; type DescribeFn = ( - blockName: Circus.BlockName, + blockName: Circus.BlockNameLike, blockFn: Circus.BlockFn, ) => void; const describe = (() => { - const describe = (blockName: Circus.BlockName, blockFn: Circus.BlockFn) => + const describe = (blockName: Circus.BlockNameLike, blockFn: Circus.BlockFn) => _dispatchDescribe(blockFn, blockName, describe); - const only = (blockName: Circus.BlockName, blockFn: Circus.BlockFn) => + const only = (blockName: Circus.BlockNameLike, blockFn: Circus.BlockFn) => _dispatchDescribe(blockFn, blockName, only, 'only'); - const skip = (blockName: Circus.BlockName, blockFn: Circus.BlockFn) => + const skip = (blockName: Circus.BlockNameLike, blockFn: Circus.BlockFn) => _dispatchDescribe(blockFn, blockName, skip, 'skip'); describe.each = bindEach(describe, false); @@ -40,7 +40,7 @@ const describe = (() => { const _dispatchDescribe = ( blockFn: Circus.BlockFn, - blockName: Circus.BlockName, + blockName: Circus.BlockNameLike, describeFn: DescribeFn, mode?: Circus.BlockMode, ) => { @@ -54,6 +54,13 @@ const _dispatchDescribe = ( asyncError.message = `Invalid second argument, ${blockFn}. It must be a callback function.`; throw asyncError; } + try { + blockName = convertDescriptorToString(blockName); + } catch (error) { + asyncError.message = (error as Error).message; + throw asyncError; + } + dispatchSync({ asyncError, blockName, @@ -107,22 +114,22 @@ const afterAll: THook = (fn, timeout) => const test: Global.It = (() => { const test = ( - testName: Circus.TestName, + testName: Circus.TestNameLike, fn: Circus.TestFn, timeout?: number, ): void => _addTest(testName, undefined, fn, test, timeout); const skip = ( - testName: Circus.TestName, + testName: Circus.TestNameLike, fn?: Circus.TestFn, timeout?: number, ): void => _addTest(testName, 'skip', fn, skip, timeout); const only = ( - testName: Circus.TestName, + testName: Circus.TestNameLike, fn: Circus.TestFn, timeout?: number, ): void => _addTest(testName, 'only', fn, test.only, timeout); - test.todo = (testName: Circus.TestName, ...rest: Array): void => { + test.todo = (testName: Circus.TestNameLike, ...rest: Array): void => { if (rest.length > 0 || typeof testName !== 'string') { throw new ErrorWithStack( 'Todo must be called with only a description.', @@ -133,11 +140,11 @@ const test: Global.It = (() => { }; const _addTest = ( - testName: Circus.TestName, + testName: Circus.TestNameLike, mode: Circus.TestMode, fn: Circus.TestFn | undefined, testFn: ( - testName: Circus.TestName, + testName: Circus.TestNameLike, fn: Circus.TestFn, timeout?: number, ) => void, @@ -145,11 +152,13 @@ const test: Global.It = (() => { ) => { const asyncError = new ErrorWithStack(undefined, testFn); - if (typeof testName !== 'string') { - asyncError.message = `Invalid first argument, ${testName}. It must be a string.`; - + try { + testName = convertDescriptorToString(testName); + } catch (error) { + asyncError.message = (error as Error).message; throw asyncError; } + if (fn === undefined) { asyncError.message = 'Missing second argument. It must be a callback function. Perhaps you want to use `test.todo` for a test placeholder.'; diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts index f4dbf6b7c16c..0acbd432ab37 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts @@ -78,7 +78,7 @@ export const initialize = async ({ globalsObject.test.concurrent = (test => { const concurrent = ( - testName: string, + testName: Global.TestNameLike, testFn: Global.ConcurrentTestFn, timeout?: number, ) => { @@ -96,7 +96,7 @@ export const initialize = async ({ }; const only = ( - testName: string, + testName: Global.TestNameLike, testFn: Global.ConcurrentTestFn, timeout?: number, ) => { diff --git a/packages/jest-each/src/bind.ts b/packages/jest-each/src/bind.ts index 6b6a7b8b27a4..e7d663446629 100644 --- a/packages/jest-each/src/bind.ts +++ b/packages/jest-each/src/bind.ts @@ -7,7 +7,7 @@ */ import type {Global} from '@jest/types'; -import {ErrorWithStack} from 'jest-util'; +import {ErrorWithStack, convertDescriptorToString} from 'jest-util'; import convertArrayTable from './table/array'; import convertTemplateTable from './table/template'; import { @@ -37,10 +37,11 @@ export default function bind( ...taggedTemplateData: Global.TemplateData ) => function eachBind( - title: string, + title: Global.BlockNameLike, test: Global.EachTestFn, timeout?: number, ): void { + title = convertDescriptorToString(title); try { const tests = isArrayTable(taggedTemplateData) ? buildArrayTests(title, table) diff --git a/packages/jest-jasmine2/src/__tests__/itTestError.test.ts b/packages/jest-jasmine2/src/__tests__/itTestError.test.ts index f2fe712d56da..24ad11cea344 100644 --- a/packages/jest-jasmine2/src/__tests__/itTestError.test.ts +++ b/packages/jest-jasmine2/src/__tests__/itTestError.test.ts @@ -14,11 +14,13 @@ describe('test/it error throwing', () => { 'Missing second argument. It must be a callback function. Perhaps you want to use `test.todo` for a test placeholder.', ); }); - it("it throws an error when first argument isn't a string", () => { + it("it throws an error when first argument isn't valid", () => { expect(() => { // @ts-expect-error it(() => {}); - }).toThrowError('Invalid first argument, () => {}. It must be a string.'); + }).toThrowError( + 'Invalid first argument, () => {}. It must be a named class, named function, number, or string.', + ); }); it('it throws an error when callback function is not a function', () => { expect(() => { @@ -35,11 +37,13 @@ describe('test/it error throwing', () => { 'Missing second argument. It must be a callback function. Perhaps you want to use `test.todo` for a test placeholder.', ); }); - test("test throws an error when first argument isn't a string", () => { + test("test throws an error when first argument isn't valid", () => { expect(() => { // @ts-expect-error test(() => {}); - }).toThrowError('Invalid first argument, () => {}. It must be a string.'); + }).toThrowError( + 'Invalid first argument, () => {}. It must be a named class, named function, number, or string.', + ); }); test('test throws an error when callback function is not a function', () => { expect(() => { diff --git a/packages/jest-jasmine2/src/jasmine/Env.ts b/packages/jest-jasmine2/src/jasmine/Env.ts index cd8a34b074ea..7062dc4e7749 100644 --- a/packages/jest-jasmine2/src/jasmine/Env.ts +++ b/packages/jest-jasmine2/src/jasmine/Env.ts @@ -31,7 +31,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. /* eslint-disable sort-keys, local/prefer-spread-eventually, local/prefer-rest-params-eventually */ import {AssertionError} from 'assert'; -import {ErrorWithStack, isPromise} from 'jest-util'; +import type {Circus} from '@jest/types'; +import {ErrorWithStack, convertDescriptorToString, isPromise} from 'jest-util'; import assertionErrorMessage from '../assertionErrorMessage'; import isError from '../isError'; import queueRunner, { @@ -59,7 +60,11 @@ export default function jasmineEnv(j$: Jasmine) { fail: (error: Error | AssertionErrorWithStack) => void; pending: (message: string) => void; afterAll: (afterAllFunction: QueueableFn['fn'], timeout?: number) => void; - fit: (description: string, fn: QueueableFn['fn'], timeout?: number) => Spec; + fit: ( + description: Circus.TestNameLike, + fn: QueueableFn['fn'], + timeout?: number, + ) => Spec; throwingExpectationFailures: () => boolean; randomizeTests: (value: unknown) => void; randomTests: () => boolean; @@ -69,7 +74,7 @@ export default function jasmineEnv(j$: Jasmine) { suiteTree?: Suite, ) => Promise; fdescribe: ( - description: string, + description: Circus.TestNameLike, specDefinitions: SpecDefinitionsFn, ) => Suite; spyOn: ( @@ -84,18 +89,26 @@ export default function jasmineEnv(j$: Jasmine) { afterEach: (afterEachFunction: QueueableFn['fn'], timeout?: number) => void; clearReporters: () => void; addReporter: (reporterToAdd: Reporter) => void; - it: (description: string, fn: QueueableFn['fn'], timeout?: number) => Spec; + it: ( + description: Circus.TestNameLike, + fn: QueueableFn['fn'], + timeout?: number, + ) => Spec; xdescribe: ( - description: string, + description: Circus.TestNameLike, specDefinitions: SpecDefinitionsFn, ) => Suite; - xit: (description: string, fn: QueueableFn['fn'], timeout?: number) => Spec; + xit: ( + description: Circus.TestNameLike, + fn: QueueableFn['fn'], + timeout?: number, + ) => Spec; beforeAll: (beforeAllFunction: QueueableFn['fn'], timeout?: number) => void; todo: () => Spec; provideFallbackReporter: (reporterToAdd: Reporter) => void; allowRespy: (allow: boolean) => void; describe: ( - description: string, + description: Circus.TestNameLike, specDefinitions: SpecDefinitionsFn, ) => Suite; @@ -364,7 +377,7 @@ export default function jasmineEnv(j$: Jasmine) { return spyRegistry.spyOn.apply(spyRegistry, args); }; - const suiteFactory = function (description: string) { + const suiteFactory = function (description: Circus.TestNameLike) { const suite = new j$.Suite({ id: getNextSuiteId(), description, @@ -378,7 +391,10 @@ export default function jasmineEnv(j$: Jasmine) { return suite; }; - this.describe = function (description: string, specDefinitions) { + this.describe = function ( + description: Circus.TestNameLike, + specDefinitions, + ) { const suite = suiteFactory(description); if (specDefinitions === undefined) { throw new Error( @@ -483,7 +499,7 @@ export default function jasmineEnv(j$: Jasmine) { } const specFactory = ( - description: string, + description: Circus.TestNameLike, fn: QueueableFn['fn'], suite: Suite, timeout?: number, @@ -534,11 +550,7 @@ export default function jasmineEnv(j$: Jasmine) { }; this.it = function (description, fn, timeout) { - if (typeof description !== 'string') { - throw new Error( - `Invalid first argument, ${description}. It must be a string.`, - ); - } + description = convertDescriptorToString(description); if (fn === undefined) { throw new Error( 'Missing second argument. It must be a callback function. Perhaps you want to use `test.todo` for a test placeholder.', diff --git a/packages/jest-jasmine2/src/jasmine/Spec.ts b/packages/jest-jasmine2/src/jasmine/Spec.ts index 0767a720e940..bb020debc2dd 100644 --- a/packages/jest-jasmine2/src/jasmine/Spec.ts +++ b/packages/jest-jasmine2/src/jasmine/Spec.ts @@ -32,6 +32,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import {AssertionError} from 'assert'; import type {FailedAssertion, Milliseconds, Status} from '@jest/test-result'; +import type {Circus} from '@jest/types'; +import {convertDescriptorToString} from 'jest-util'; import ExpectationFailed from '../ExpectationFailed'; import assertionErrorMessage from '../assertionErrorMessage'; import expectationResultFactory, { @@ -43,7 +45,7 @@ import type {AssertionErrorWithStack} from '../types'; export type Attributes = { id: string; resultCallback: (result: Spec['result']) => void; - description: string; + description: Circus.TestNameLike; throwOnExpectationFailure: unknown; getTestPath: () => string; queueableFn: QueueableFn; @@ -108,7 +110,7 @@ export default class Spec { constructor(attrs: Attributes) { this.resultCallback = attrs.resultCallback || function () {}; this.id = attrs.id; - this.description = attrs.description || ''; + this.description = convertDescriptorToString(attrs.description); this.queueableFn = attrs.queueableFn; this.beforeAndAfterFns = attrs.beforeAndAfterFns || diff --git a/packages/jest-jasmine2/src/jasmine/Suite.ts b/packages/jest-jasmine2/src/jasmine/Suite.ts index b815ae31d41d..14a4ed10f8e6 100644 --- a/packages/jest-jasmine2/src/jasmine/Suite.ts +++ b/packages/jest-jasmine2/src/jasmine/Suite.ts @@ -31,6 +31,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. /* eslint-disable sort-keys, local/prefer-spread-eventually, local/prefer-rest-params-eventually */ +import type {Circus} from '@jest/types'; import {convertDescriptorToString} from 'jest-util'; import ExpectationFailed from '../ExpectationFailed'; import expectationResultFactory from '../expectationResultFactory'; @@ -49,7 +50,7 @@ export type SuiteResult = { export type Attributes = { id: string; parentSuite?: Suite; - description: string; + description: Circus.TestNameLike; throwOnExpectationFailure?: boolean; getTestPath: () => string; }; @@ -57,7 +58,7 @@ export type Attributes = { export default class Suite { id: string; parentSuite?: Suite; - description: string; + description: Circus.TestNameLike; throwOnExpectationFailure: boolean; beforeFns: Array; afterFns: Array; diff --git a/packages/jest-jasmine2/src/jasmineAsyncInstall.ts b/packages/jest-jasmine2/src/jasmineAsyncInstall.ts index 3d9e85eb9552..836b0df29726 100644 --- a/packages/jest-jasmine2/src/jasmineAsyncInstall.ts +++ b/packages/jest-jasmine2/src/jasmineAsyncInstall.ts @@ -100,7 +100,7 @@ function promisifyLifeCycleFunction( // when the return value is neither a Promise nor `undefined` function promisifyIt( originalFn: ( - description: string, + description: Global.TestNameLike, fn: QueueableFn['fn'], timeout?: number, ) => Spec, @@ -108,7 +108,7 @@ function promisifyIt( jasmine: Jasmine, ) { return function ( - specName: string, + specName: Global.TestNameLike, fn?: (done: DoneFn) => void | PromiseLike, timeout?: number, ): Spec { @@ -183,7 +183,7 @@ function promisifyIt( function makeConcurrent( originalFn: ( - description: string, + description: Global.TestNameLike, fn: QueueableFn['fn'], timeout?: number, ) => Spec, @@ -191,7 +191,7 @@ function makeConcurrent( mutex: ReturnType, ): Global.ItConcurrentBase { const concurrentFn = function ( - specName: string, + specName: Global.TestNameLike, fn: Global.ConcurrentTestFn, timeout?: number, ) { diff --git a/packages/jest-types/__typetests__/globals.test.ts b/packages/jest-types/__typetests__/globals.test.ts index fb530a548145..fa8f3cf20940 100644 --- a/packages/jest-types/__typetests__/globals.test.ts +++ b/packages/jest-types/__typetests__/globals.test.ts @@ -67,10 +67,15 @@ expectType(test(testName, asyncFn, timeout)); expectType(test(testName, doneFn, timeout)); expectType(test(testName, genFn, timeout)); +expectType(test(123, fn)); +expectType(test(() => {}, fn)); +expectType(test(function named() {}, fn)); +expectType(test(class {}, fn)); +expectType(test(class Named {}, fn)); + // wrong arguments expectError(test(testName)); expectError(test(testName, timeout)); -expectError(test(timeout, fn)); // wrong return value expectError(test(testName, () => 42)); @@ -98,6 +103,12 @@ expectType(test.each(table)(testName, fn, timeout)); expectType(test.each(readonlyTable)(testName, fn)); expectType(test.each(readonlyTable)(testName, fn, timeout)); +expectType(test.each(list)(123, fn)); +expectType(test.each(list)(() => {}, fn)); +expectType(test.each(list)(function named() {}, fn)); +expectType(test.each(list)(class {}, fn)); +expectType(test.each(list)(class Named {}, fn)); + expectType(test.only.each(list)(testName, fn)); expectType(test.only.each(list)(testName, fn, timeout)); expectType(test.only.each(table)(testName, fn)); @@ -112,6 +123,18 @@ expectType(test.skip.each(table)(testName, fn, timeout)); expectType(test.skip.each(readonlyTable)(testName, fn)); expectType(test.skip.each(readonlyTable)(testName, fn, timeout)); +expectType(test.skip(123, fn)); +expectType(test.skip(() => {}, fn)); +expectType(test.skip(function named() {}, fn)); +expectType(test.skip(class {}, fn)); +expectType(test.skip(class Named {}, fn)); + +expectType(test.skip.each(list)(123, fn)); +expectType(test.skip.each(list)(() => {}, fn)); +expectType(test.skip.each(list)(function named() {}, fn)); +expectType(test.skip.each(list)(class {}, fn)); +expectType(test.skip.each(list)(class Named {}, fn)); + expectType( test.each` a | b | expected @@ -247,6 +270,13 @@ expectType( `(testName, asyncFn, timeout), ); +expectType(describe(testName, fn)); +expectType(describe(123, fn)); +expectType(describe(() => {}, fn)); +expectType(describe(function named() {}, fn)); +expectType(describe(class {}, fn)); +expectType(describe(class Named {}, fn)); + expectType(describe.each(list)(testName, fn)); expectType(describe.each(list)(testName, fn, timeout)); expectType(describe.each(table)(testName, fn)); @@ -254,6 +284,12 @@ expectType(describe.each(table)(testName, fn, timeout)); expectType(describe.each(readonlyTable)(testName, fn)); expectType(describe.each(readonlyTable)(testName, fn, timeout)); +expectType(describe.each(list)(testName, fn)); +expectType(describe.each(list)(123, fn)); +expectType(describe.each(list)(() => {}, fn)); +expectType(describe.each(list)(function named() {}, fn)); +expectType(describe.each(list)(class Named {}, fn)); + expectType(describe.only.each(list)(testName, fn)); expectType(describe.only.each(list)(testName, fn, timeout)); expectType(describe.only.each(table)(testName, fn)); @@ -261,6 +297,12 @@ expectType(describe.only.each(table)(testName, fn, timeout)); expectType(describe.only.each(readonlyTable)(testName, fn)); expectType(describe.only.each(readonlyTable)(testName, fn, timeout)); +expectType(describe.only.each(list)(testName, fn)); +expectType(describe.only.each(list)(123, fn)); +expectType(describe.only.each(list)(() => {}, fn)); +expectType(describe.only.each(list)(function named() {}, fn)); +expectType(describe.only.each(list)(class Named {}, fn)); + expectType(describe.skip.each(list)(testName, fn)); expectType(describe.skip.each(list)(testName, fn, timeout)); expectType(describe.skip.each(table)(testName, fn)); @@ -268,6 +310,12 @@ expectType(describe.skip.each(table)(testName, fn, timeout)); expectType(describe.skip.each(readonlyTable)(testName, fn)); expectType(describe.skip.each(readonlyTable)(testName, fn, timeout)); +expectType(describe.skip.each(list)(testName, fn)); +expectType(describe.skip.each(list)(123, fn)); +expectType(describe.skip.each(list)(() => {}, fn)); +expectType(describe.skip.each(list)(function named() {}, fn)); +expectType(describe.skip.each(list)(class Named {}, fn)); + expectType( describe.each` a | b | expected diff --git a/packages/jest-types/src/Circus.ts b/packages/jest-types/src/Circus.ts index d93e653d7822..47d9d61a7623 100644 --- a/packages/jest-types/src/Circus.ts +++ b/packages/jest-types/src/Circus.ts @@ -12,9 +12,11 @@ type Process = NodeJS.Process; export type DoneFn = Global.DoneFn; export type BlockFn = Global.BlockFn; export type BlockName = Global.BlockName; +export type BlockNameLike = Global.BlockNameLike; export type BlockMode = void | 'skip' | 'only' | 'todo'; export type TestMode = BlockMode; export type TestName = Global.TestName; +export type TestNameLike = Global.TestNameLike; export type TestFn = Global.TestFn; export type HookFn = Global.HookFn; export type AsyncFn = TestFn | HookFn; diff --git a/packages/jest-types/src/Global.ts b/packages/jest-types/src/Global.ts index e0a7a1d802e5..9d7d4162ecf9 100644 --- a/packages/jest-types/src/Global.ts +++ b/packages/jest-types/src/Global.ts @@ -27,7 +27,11 @@ export type GeneratorReturningTestFn = ( this: TestContext | undefined, ) => TestReturnValueGenerator; +// eslint-disable-next-line @typescript-eslint/ban-types +export type NameLike = number | Function; + export type TestName = string; +export type TestNameLike = TestName | NameLike; export type TestFn = | PromiseReturningTestFn | GeneratorReturningTestFn @@ -35,6 +39,8 @@ export type TestFn = export type ConcurrentTestFn = () => TestReturnValuePromise; export type BlockFn = () => void; export type BlockName = string; +export type BlockNameLike = BlockName | NameLike; + export type HookFn = TestFn; export type Col = unknown; @@ -51,15 +57,11 @@ export type EachTestFn = ( ...args: ReadonlyArray ) => ReturnType; -type Each = +type Each = | (( table: EachTable, ...taggedTemplateData: TemplateData - ) => ( - name: BlockName | TestName, - test: EachTestFn, - timeout?: number, - ) => void) + ) => (name: Name, test: EachTestFn, timeout?: number) => void) | (() => () => void); export interface HookBase { @@ -67,19 +69,19 @@ export interface HookBase { } export interface ItBase { - (testName: TestName, fn: TestFn, timeout?: number): void; - each: Each; + (testName: TestNameLike, fn: TestFn, timeout?: number): void; + each: Each; } export interface It extends ItBase { only: ItBase; skip: ItBase; - todo: (testName: TestName) => void; + todo: (testName: TestNameLike) => void; } export interface ItConcurrentBase { - (testName: TestName, testFn: ConcurrentTestFn, timeout?: number): void; - each: Each; + (testName: TestNameLike, testFn: ConcurrentTestFn, timeout?: number): void; + each: Each; } export interface ItConcurrentExtended extends ItConcurrentBase { @@ -92,8 +94,8 @@ export interface ItConcurrent extends It { } export interface DescribeBase { - (blockName: BlockName, blockFn: BlockFn): void; - each: Each; + (blockName: BlockNameLike, blockFn: BlockFn): void; + each: Each; } export interface Describe extends DescribeBase { diff --git a/packages/jest-util/src/__tests__/convertDescriptorToString.test.ts b/packages/jest-util/src/__tests__/convertDescriptorToString.test.ts new file mode 100644 index 000000000000..5aba082873a2 --- /dev/null +++ b/packages/jest-util/src/__tests__/convertDescriptorToString.test.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import convertDescriptorToString from '../convertDescriptorToString'; + +describe(convertDescriptorToString, () => { + test.each([ + [undefined, 'undefined'], + ['name', 'name'], + [123, '123'], + [function named() {}, 'named'], + [class Named {}, 'Named'], + ])('%p', (input, output) => { + expect(convertDescriptorToString(input)).toBe(output); + }); + + test.each([ + ['null', null], + ['array', ['abc']], + ['object', {abc: 'def'}], + ['anonymous function expression', function () {}], + ['anonymous arrow function', () => {}], + ['anonymous class expression', class {}], + ])('%s', (_, input) => { + expect(() => { + // @ts-expect-error + return convertDescriptorToString(input); + }).toThrowError( + `Invalid first argument, ${input}. It must be a named class, named function, number, or string.`, + ); + }); +}); diff --git a/packages/jest-util/src/convertDescriptorToString.ts b/packages/jest-util/src/convertDescriptorToString.ts index 6f13d02c1897..1cb7f382532c 100644 --- a/packages/jest-util/src/convertDescriptorToString.ts +++ b/packages/jest-util/src/convertDescriptorToString.ts @@ -5,35 +5,27 @@ * LICENSE file in the root directory of this source tree. */ -/* eslint-disable local/ban-types-eventually */ +import type {Global} from '@jest/types'; -// See: https://github.com/facebook/jest/pull/5154 -export default function convertDescriptorToString< - T extends number | string | Function | undefined, ->(descriptor: T): T | string { - if ( - typeof descriptor === 'string' || - typeof descriptor === 'number' || - descriptor === undefined - ) { - return descriptor; - } +export default function convertDescriptorToString( + descriptor: Global.BlockNameLike | undefined, +): string { + switch (typeof descriptor) { + case 'function': + if (descriptor.name) { + return descriptor.name; + } + break; - if (typeof descriptor !== 'function') { - throw new Error('describe expects a class, function, number, or string.'); - } + case 'number': + case 'undefined': + return `${descriptor}`; - if (descriptor.name !== undefined) { - return descriptor.name; + case 'string': + return descriptor; } - // Fallback for old browsers, pardon Flow - const stringified = descriptor.toString(); - const typeDescriptorMatch = stringified.match(/class|function/); - const indexOfNameSpace = - // @ts-expect-error: typeDescriptorMatch exists - typeDescriptorMatch.index + typeDescriptorMatch[0].length; - const indexOfNameAfterSpace = stringified.search(/\(|\{/); - const name = stringified.substring(indexOfNameSpace, indexOfNameAfterSpace); - return name.trim(); + throw new Error( + `Invalid first argument, ${descriptor}. It must be a named class, named function, number, or string.`, + ); }