From 801d5c01f0c54edf87d593b009461615f3f7f735 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Fri, 2 Sep 2022 14:20:57 +0300 Subject: [PATCH] fix(jest-mock): `generateFromMetadata` return type --- .../__typetests__/ModuleMocker.test.ts | 75 +++++++++++ packages/jest-mock/src/index.ts | 116 ++++++++---------- 2 files changed, 126 insertions(+), 65 deletions(-) create mode 100644 packages/jest-mock/__typetests__/ModuleMocker.test.ts diff --git a/packages/jest-mock/__typetests__/ModuleMocker.test.ts b/packages/jest-mock/__typetests__/ModuleMocker.test.ts new file mode 100644 index 000000000000..c7b275bfebe3 --- /dev/null +++ b/packages/jest-mock/__typetests__/ModuleMocker.test.ts @@ -0,0 +1,75 @@ +/** + * 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 {expectType} from 'tsd-lite'; +import {MockMetadata, Mocked, ModuleMocker} from 'jest-mock'; + +class ExampleClass { + memberA: Array; + + constructor() { + this.memberA = [1, 2, 3]; + } + memberB() {} +} + +const exampleModule = { + instance: new ExampleClass(), + + methodA: function square(a: number, b: number) { + return a * b; + }, + methodB: async function asyncSquare(a: number, b: number) { + const result = (await a) * b; + return result; + }, + + propertyA: { + one: 'foo', + three: { + nine: 1, + ten: [1, 2, 3], + }, + two() {}, + }, + propertyB: [1, 2, 3], + propertyC: 123, + propertyD: 'baz', + propertyE: true, + propertyF: Symbol.for('a.b.c'), +}; + +const moduleMocker = new ModuleMocker(globalThis); + +// getMetadata + +const exampleMetadata = moduleMocker.getMetadata(exampleModule); + +expectType | null>(exampleMetadata); + +// generateFromMetadata + +const exampleMock = moduleMocker.generateFromMetadata(exampleMetadata!); + +expectType>(exampleMock); + +expectType>(exampleMock.methodA.mock.calls); +expectType>(exampleMock.methodB.mock.calls); + +expectType>(exampleMock.instance.memberA); +expectType>(exampleMock.instance.memberB.mock.calls); + +expectType(exampleMock.propertyA.one); +expectType>(exampleMock.propertyA.two.mock.calls); +expectType(exampleMock.propertyA.three.nine); +expectType>(exampleMock.propertyA.three.ten); + +expectType>(exampleMock.propertyB); +expectType(exampleMock.propertyC); +expectType(exampleMock.propertyD); +expectType(exampleMock.propertyE); +expectType(exampleMock.propertyF); diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 305699b0a50c..224ac7e9c70a 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -7,7 +7,7 @@ /* eslint-disable local/ban-types-eventually, local/prefer-rest-params-eventually */ -export type MockFunctionMetadataType = +export type MockMetadataType = | 'object' | 'array' | 'regexp' @@ -17,20 +17,26 @@ export type MockFunctionMetadataType = | 'null' | 'undefined'; -export type MockFunctionMetadata< - T extends UnknownFunction = UnknownFunction, - MetadataType = MockFunctionMetadataType, -> = { +// TODO remove re-export in Jest 30 +export type MockFunctionMetadataType = MockMetadataType; + +export type MockMetadata = { ref?: number; - members?: Record>; + members?: Record>; mockImpl?: T; name?: string; refID?: number; type?: MetadataType; - value?: ReturnType; + value?: T; length?: number; }; +// TODO remove re-export in Jest 30 +export type MockFunctionMetadata< + T = unknown, + MetadataType = MockMetadataType, +> = MockMetadata; + export type ClassLike = {new (...args: any): any}; export type FunctionLike = (...args: any) => any; @@ -75,7 +81,7 @@ type MockedObjectShallow = { : T[K]; } & T; -export type Mocked = T extends ClassLike +export type Mocked = T extends ClassLike ? MockedClass : T extends FunctionLike ? MockedFunction @@ -83,7 +89,7 @@ export type Mocked = T extends ClassLike ? MockedObject : T; -export type MockedShallow = T extends ClassLike +export type MockedShallow = T extends ClassLike ? MockedClass : T extends FunctionLike ? MockedFunctionShallow @@ -386,7 +392,7 @@ function getObjectType(value: unknown): string { return Object.prototype.toString.apply(value).slice(8, -1); } -function getType(ref?: unknown): MockFunctionMetadataType | null { +function getType(ref?: unknown): MockMetadataType | null { const typeName = getObjectType(ref); if ( typeName === 'Function' || @@ -560,39 +566,30 @@ export class ModuleMocker { }; } - private _makeComponent( - metadata: MockFunctionMetadata, + private _makeComponent>( + metadata: MockMetadata, restore?: () => void, - ): Record; - private _makeComponent( - metadata: MockFunctionMetadata, + ): T; + private _makeComponent>( + metadata: MockMetadata, restore?: () => void, - ): Array; - private _makeComponent( - metadata: MockFunctionMetadata, + ): T; + private _makeComponent( + metadata: MockMetadata, restore?: () => void, - ): RegExp; - private _makeComponent( - metadata: MockFunctionMetadata< - T, - 'constant' | 'collection' | 'null' | 'undefined' - >, + ): T; + private _makeComponent( + metadata: MockMetadata, restore?: () => void, ): T; private _makeComponent( - metadata: MockFunctionMetadata, + metadata: MockMetadata, restore?: () => void, ): Mock; private _makeComponent( - metadata: MockFunctionMetadata, + metadata: MockMetadata, restore?: () => void, - ): - | Record - | Array - | RegExp - | ReturnType - | undefined - | Mock { + ): Record | Array | RegExp | T | Mock | undefined { if (metadata.type === 'object') { return new this._environmentGlobal.Object(); } else if (metadata.type === 'array') { @@ -808,7 +805,7 @@ export class ModuleMocker { } private _createMockFunction( - metadata: MockFunctionMetadata, + metadata: MockMetadata, mockConstructor: Function, ): Function { let name = metadata.name; @@ -828,15 +825,13 @@ export class ModuleMocker { } while (name && name.startsWith(boundFunctionPrefix)); } - // Special case functions named `mockConstructor` to guard for infinite - // loops. + // Special case functions named `mockConstructor` to guard for infinite loops if (name === MOCK_CONSTRUCTOR_NAME) { return mockConstructor; } if ( - // It's a syntax error to define functions with a reserved keyword - // as name. + // It's a syntax error to define functions with a reserved keyword as name RESERVED_KEYWORDS.has(name) || // It's also a syntax error to define functions with a name that starts with a number /^\d/.test(name) @@ -862,19 +857,14 @@ export class ModuleMocker { return createConstructor(mockConstructor); } - private _generateMock( - metadata: MockFunctionMetadata, + private _generateMock( + metadata: MockMetadata, callbacks: Array, - refs: { - [key: string]: - | Record - | Array - | RegExp - | UnknownFunction - | undefined - | Mock; - }, - ): Mock { + refs: Record< + number, + Record | Array | RegExp | T | Mock | undefined + >, + ): Mocked { // metadata not compatible but it's the same type, maybe problem with // overloading of _makeComponent and not _generateMock? // @ts-expect-error - unsure why TSC complains here? @@ -905,7 +895,7 @@ export class ModuleMocker { mock.prototype.constructor = mock; } - return mock as Mock; + return mock as Mocked; } /** @@ -913,12 +903,10 @@ export class ModuleMocker { * @param metadata Metadata for the mock in the schema returned by the * getMetadata method of this module. */ - generateFromMetadata( - metadata: MockFunctionMetadata, - ): Mock { + generateFromMetadata(metadata: MockMetadata): Mocked { const callbacks: Array = []; const refs = {}; - const mock = this._generateMock(metadata, callbacks, refs); + const mock = this._generateMock(metadata, callbacks, refs); callbacks.forEach(setter => setter()); return mock; } @@ -927,11 +915,11 @@ export class ModuleMocker { * @see README.md * @param component The component for which to retrieve metadata. */ - getMetadata( - component: ReturnType, - _refs?: Map, number>, - ): MockFunctionMetadata | null { - const refs = _refs || new Map, number>(); + getMetadata( + component: T, + _refs?: Map, + ): MockMetadata | null { + const refs = _refs || new Map(); const ref = refs.get(component); if (ref != null) { return {ref}; @@ -942,7 +930,7 @@ export class ModuleMocker { return null; } - const metadata: MockFunctionMetadata = {type}; + const metadata: MockMetadata = {type}; if ( type === 'constant' || type === 'collection' || @@ -966,9 +954,7 @@ export class ModuleMocker { metadata.refID = refs.size; refs.set(component, metadata.refID); - let members: { - [key: string]: MockFunctionMetadata; - } | null = null; + let members: Record> | null = null; // Leave arrays alone if (type !== 'array') { // @ts-expect-error component is object @@ -1007,7 +993,7 @@ export class ModuleMocker { ): fn is Mock<(...args: P) => R>; isMockFunction(fn: unknown): fn is Mock; isMockFunction(fn: unknown): fn is Mock { - return fn != null && (fn as any)._isMockFunction === true; + return fn != null && (fn as Mock)._isMockFunction === true; } fn(implementation?: T): Mock {