From 1132c7b88415273a324563be278d0209ac0464af Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Thu, 24 Feb 2022 18:59:25 +0200 Subject: [PATCH 01/23] refactor(jest-mock): Mock generic type arguments --- .../__typetests__/mock-functions.test.ts | 82 +++--- packages/jest-mock/src/index.ts | 256 +++++++++--------- packages/jest-runtime/src/index.ts | 5 +- .../jest-types/__typetests__/jest.test.ts | 14 +- 4 files changed, 188 insertions(+), 169 deletions(-) diff --git a/packages/jest-mock/__typetests__/mock-functions.test.ts b/packages/jest-mock/__typetests__/mock-functions.test.ts index 6a8905a901d2..945bd0244b17 100644 --- a/packages/jest-mock/__typetests__/mock-functions.test.ts +++ b/packages/jest-mock/__typetests__/mock-functions.test.ts @@ -15,12 +15,12 @@ import {Mock, SpyInstance, fn, spyOn} from 'jest-mock'; // jest.fn() -expectType, []>>( +expectType Promise>>( fn(async () => 'value') .mockClear() .mockReset() - .mockImplementation(fn()) - .mockImplementationOnce(fn()) + .mockImplementation(async () => 'value') + .mockImplementationOnce(async () => 'value') .mockName('mock') .mockReturnThis() .mockReturnValue(Promise.resolve('value')) @@ -33,12 +33,12 @@ expectType, []>>( expectAssignable(fn()); // eslint-disable-line @typescript-eslint/ban-types -expectType>(fn()); -expectType>(fn(() => {})); -expectType>( +expectType) => unknown>>(fn()); +expectType void>>(fn(() => {})); +expectType boolean>>( fn((a: string, b?: number) => true), ); -expectType>( +expectType never>>( fn((e: any) => { throw new Error(); }), @@ -63,7 +63,7 @@ const MockObject = fn((credentials: string) => ({ })); expectType<{ - connect(): Mock>; + connect(): Mock<(...args: Array) => unknown>; disconnect(): void; }>(new MockObject('credentials')); expectError(new MockObject()); @@ -92,7 +92,7 @@ expectType>(mockFn.mock.invocationCallOrder); expectType< Array<{ - connect(): Mock>; + connect(): Mock<(...args: Array) => unknown>; disconnect(): void; }> >(MockObject.mock.instances); @@ -114,12 +114,12 @@ if (returnValue.type === 'throw') { expectType(returnValue.value); } -expectType>( +expectType boolean>>( mockFn.mockClear(), ); expectError(mockFn.mockClear('some-mock')); -expectType>( +expectType boolean>>( mockFn.mockReset(), ); expectError(mockFn.mockClear('some-mock')); @@ -127,7 +127,7 @@ expectError(mockFn.mockClear('some-mock')); expectType(mockFn.mockRestore()); expectError(mockFn.mockClear('some-mock')); -expectType>( +expectType boolean>>( mockFn.mockImplementation((a, b) => { expectType(a); expectType(b); @@ -138,7 +138,7 @@ expectError(mockFn.mockImplementation((a: number) => false)); expectError(mockFn.mockImplementation(a => 'false')); expectError(mockFn.mockImplementation()); -expectType, [p: boolean]>>( +expectType Promise>>( mockAsyncFn.mockImplementation(async a => { expectType(a); return 'mock value'; @@ -146,7 +146,7 @@ expectType, [p: boolean]>>( ); expectError(mockAsyncFn.mockImplementation(a => 'mock value')); -expectType>( +expectType boolean>>( mockFn.mockImplementationOnce((a, b) => { expectType(a); expectType(b); @@ -157,7 +157,7 @@ expectError(mockFn.mockImplementationOnce((a: number) => false)); expectError(mockFn.mockImplementationOnce(a => 'false')); expectError(mockFn.mockImplementationOnce()); -expectType, [p: boolean]>>( +expectType Promise>>( mockAsyncFn.mockImplementationOnce(async a => { expectType(a); return 'mock value'; @@ -165,63 +165,63 @@ expectType, [p: boolean]>>( ); expectError(mockAsyncFn.mockImplementationOnce(a => 'mock value')); -expectType>( +expectType boolean>>( mockFn.mockName('mockedFunction'), ); expectError(mockFn.mockName(123)); expectError(mockFn.mockName()); -expectType>( +expectType boolean>>( mockFn.mockReturnThis(), ); expectError(mockFn.mockReturnThis('this')); -expectType>( +expectType boolean>>( mockFn.mockReturnValue(false), ); expectError(mockFn.mockReturnValue('true')); expectError(mockFn.mockReturnValue()); -expectType, [p: boolean]>>( +expectType Promise>>( mockAsyncFn.mockReturnValue(Promise.resolve('mock value')), ); expectError(mockAsyncFn.mockReturnValue(Promise.resolve(true))); -expectType>( +expectType boolean>>( mockFn.mockReturnValueOnce(false), ); expectError(mockFn.mockReturnValueOnce('true')); expectError(mockFn.mockReturnValueOnce()); -expectType, [p: boolean]>>( +expectType Promise>>( mockAsyncFn.mockReturnValueOnce(Promise.resolve('mock value')), ); expectError(mockAsyncFn.mockReturnValueOnce(Promise.resolve(true))); -expectType, []>>( +expectType Promise>>( fn(() => Promise.resolve('')).mockResolvedValue('Mock value'), ); expectError(fn(() => Promise.resolve('')).mockResolvedValue(123)); expectError(fn(() => Promise.resolve('')).mockResolvedValue()); -expectType, []>>( +expectType Promise>>( fn(() => Promise.resolve('')).mockResolvedValueOnce('Mock value'), ); expectError(fn(() => Promise.resolve('')).mockResolvedValueOnce(123)); expectError(fn(() => Promise.resolve('')).mockResolvedValueOnce()); -expectType, []>>( +expectType Promise>>( fn(() => Promise.resolve('')).mockRejectedValue(new Error('Mock error')), ); -expectType, []>>( +expectType Promise>>( fn(() => Promise.resolve('')).mockRejectedValue('Mock error'), ); expectError(fn(() => Promise.resolve('')).mockRejectedValue()); -expectType, []>>( +expectType Promise>>( fn(() => Promise.resolve('')).mockRejectedValueOnce(new Error('Mock error')), ); -expectType, []>>( +expectType Promise>>( fn(() => Promise.resolve('')).mockRejectedValueOnce('Mock error'), ); expectError(fn(() => Promise.resolve('')).mockRejectedValueOnce()); @@ -261,22 +261,28 @@ expectNotAssignable(spy); // eslint-disable-line @typescript-eslint/ba expectError(spy()); expectError(new spy()); -expectType>(spyOn(spiedObject, 'methodA')); -expectType>( +expectType>( + spyOn(spiedObject, 'methodA'), +); +expectType>( spyOn(spiedObject, 'methodB'), ); -expectType>(spyOn(spiedObject, 'methodC')); +expectType>( + spyOn(spiedObject, 'methodC'), +); -expectType>(spyOn(spiedObject, 'propertyB', 'get')); -expectType>( +expectType boolean>>(spyOn(spiedObject, 'propertyB', 'get')); +expectType void>>( spyOn(spiedObject, 'propertyB', 'set'), ); expectError(spyOn(spiedObject, 'propertyB')); expectError(spyOn(spiedObject, 'methodB', 'get')); expectError(spyOn(spiedObject, 'methodB', 'set')); -expectType>(spyOn(spiedObject, 'propertyA', 'get')); -expectType>(spyOn(spiedObject, 'propertyA', 'set')); +expectType string>>(spyOn(spiedObject, 'propertyA', 'get')); +expectType void>>( + spyOn(spiedObject, 'propertyA', 'set'), +); expectError(spyOn(spiedObject, 'propertyA')); expectError(spyOn(spiedObject, 'notThere')); @@ -286,17 +292,17 @@ expectError(spyOn(true, 'methodA')); expectError(spyOn(spiedObject)); expectError(spyOn()); -expectType>( +expectType boolean>>( spyOn(spiedArray as unknown as ArrayConstructor, 'isArray'), ); expectError(spyOn(spiedArray, 'isArray')); -expectType>( +expectType string>>( spyOn(spiedFunction as unknown as Function, 'toString'), // eslint-disable-line @typescript-eslint/ban-types ); expectError(spyOn(spiedFunction, 'toString')); -expectType>( +expectType Date>>( spyOn(globalThis, 'Date'), ); -expectType>(spyOn(Date, 'now')); +expectType number>>(spyOn(Date, 'now')); diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 16fee2a2addc..1a2944e78a2c 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -18,17 +18,18 @@ export type MockFunctionMetadataType = | 'undefined'; export type MockFunctionMetadata< - T, - Y extends Array, + T extends (...args: Array) => unknown = ( + ...args: Array + ) => unknown, MetadataType = MockFunctionMetadataType, > = { ref?: number; - members?: Record>; - mockImpl?: (...args: Y) => T; + members?: Record>; + mockImpl?: (...args: Parameters) => ReturnType; name?: string; refID?: number; type?: MetadataType; - value?: T; + value?: ReturnType; length?: number; }; @@ -61,11 +62,11 @@ type ConstructorParameters = T extends new (...args: infer P) => any export type MaybeMockedConstructor = T extends new ( ...args: Array ) => infer R - ? MockInstance> + ? MockInstance<(...args: ConstructorParameters) => R> : T; export interface MockWithArgs - extends MockInstance, Parameters> { + extends MockInstance<(...args: Parameters) => ReturnType> { new (...args: ConstructorParameters): T; (...args: Parameters): ReturnType; } @@ -103,57 +104,62 @@ export type MaybeMockedDeep = T extends MethodLike export type Mocked = { [P in keyof T]: T[P] extends MethodLike - ? MockInstance, Parameters> + ? MockInstance<(...args: Parameters) => ReturnType> : T[P] extends ConstructorLike ? MockedClass : T[P]; } & T; export type MockedClass = MockInstance< - InstanceType, - T extends new (...args: infer P) => any ? P : never + (args: T extends new (...args: infer P) => any ? P : never) => InstanceType > & { prototype: T extends {prototype: any} ? Mocked : never; } & T; -export interface Mock = Array> - extends Function, - MockInstance { - new (...args: Y): T; - (...args: Y): T; +export interface Mock< + T extends (...args: Array) => any = (...args: Array) => unknown, +> extends Function, + MockInstance { + new (...args: Parameters): ReturnType; + (...args: Parameters): ReturnType; } // TODO Replace with Awaited utility type when minimum supported TS version will be 4.5 or above //https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-5.html#the-awaited-type-and-promise-improvements type Unpromisify = T extends Promise ? R : never; -export interface MockInstance> { +export interface MockInstance< + T extends (...args: any) => unknown = (...args: Array) => unknown, +> { _isMockFunction: true; _protoImpl: Function; - getMockImplementation(): ((...args: Y) => T) | undefined; + getMockImplementation(): + | ((...args: Parameters) => ReturnType) + | undefined; getMockName(): string; - mock: MockFunctionState; + mock: MockFunctionState; mockClear(): this; mockReset(): this; mockRestore(): void; - mockImplementation(fn: (...args: Y) => T): this; + mockImplementation(fn: (...args: Parameters) => ReturnType): this; /** @internal */ - mockImplementation(fn: () => Promise): this; - mockImplementationOnce(fn: (...args: Y) => T): this; + mockImplementation(fn: (...args: Array) => unknown): this; + mockImplementationOnce(fn: (...args: Parameters) => ReturnType): this; /** @internal */ - mockImplementationOnce(fn: () => Promise): this; + mockImplementationOnce(fn: (...args: Array) => unknown): this; mockName(name: string): this; mockReturnThis(): this; - mockReturnValue(value: T): this; - mockReturnValueOnce(value: T): this; - mockResolvedValue(value: Unpromisify): this; - mockResolvedValueOnce(value: Unpromisify): this; + mockReturnValue(value: ReturnType): this; + mockReturnValueOnce(value: ReturnType): this; + mockResolvedValue(value: Unpromisify>): this; + mockResolvedValueOnce(value: Unpromisify>): this; mockRejectedValue(value: unknown): this; mockRejectedValueOnce(value: unknown): this; } -export interface SpyInstance> - extends MockInstance {} +export interface SpyInstance< + T extends (...arg: any) => unknown = (...args: Array) => unknown, +> extends MockInstance {} type MockFunctionResultIncomplete = { type: 'incomplete'; @@ -184,15 +190,15 @@ type MockFunctionResult = | MockFunctionResultReturn | MockFunctionResultThrow; -type MockFunctionState> = { +type MockFunctionState) => unknown> = { /** * List of the call arguments of all calls that have been made to the mock. */ - calls: Array; + calls: Array>; /** * List of all the object instances that have been instantiated from the mock. */ - instances: Array; + instances: Array>; /** * List of the call order indexes of the mock. Jest is indexing the order of * invocations of all mocks in a test file. The index is starting with `1`. @@ -202,11 +208,11 @@ type MockFunctionState> = { * List of the call arguments of the last call that was made to the mock. * If the function was not called, it will return `undefined`. */ - lastCall?: Y; + lastCall?: Parameters; /** * List of the results of all calls that have been made to the mock. */ - results: Array>; + results: Array>>; }; type MockFunctionConfig = { @@ -460,7 +466,10 @@ function isReadonlyProp(object: any, prop: string): boolean { export class ModuleMocker { private _environmentGlobal: typeof globalThis; - private _mockState: WeakMap, MockFunctionState>; + private _mockState: WeakMap< + Mock<(...args: Array) => unknown>, + MockFunctionState<(...args: Array) => unknown> + >; private _mockConfigRegistry: WeakMap; private _spyState: Set<() => void>; private _invocationCallCounter: number; @@ -524,8 +533,8 @@ export class ModuleMocker { return Array.from(slots); } - private _ensureMockConfig>( - f: Mock, + private _ensureMockConfig) => unknown>( + f: Mock, ): MockFunctionConfig { let config = this._mockConfigRegistry.get(f); if (!config) { @@ -535,9 +544,9 @@ export class ModuleMocker { return config; } - private _ensureMockState>( - f: Mock, - ): MockFunctionState { + private _ensureMockState) => unknown>( + f: Mock, + ): MockFunctionState { let state = this._mockState.get(f); if (!state) { state = this._defaultMockState(); @@ -546,7 +555,7 @@ export class ModuleMocker { if (state.calls.length > 0) { state.lastCall = state.calls[state.calls.length - 1]; } - return state; + return state as MockFunctionState; } private _defaultMockConfig(): MockFunctionConfig { @@ -558,10 +567,9 @@ export class ModuleMocker { }; } - private _defaultMockState>(): MockFunctionState< - T, - Y - > { + private _defaultMockState< + T extends (...args: Array) => unknown, + >(): MockFunctionState { return { calls: [], instances: [], @@ -570,40 +578,39 @@ export class ModuleMocker { }; } - private _makeComponent>( - metadata: MockFunctionMetadata, + private _makeComponent) => unknown>( + metadata: MockFunctionMetadata, restore?: () => void, ): Record; - private _makeComponent>( - metadata: MockFunctionMetadata, + private _makeComponent) => unknown>( + metadata: MockFunctionMetadata, restore?: () => void, ): Array; - private _makeComponent>( - metadata: MockFunctionMetadata, + private _makeComponent) => unknown>( + metadata: MockFunctionMetadata, restore?: () => void, ): RegExp; - private _makeComponent>( + private _makeComponent) => unknown>( metadata: MockFunctionMetadata< T, - Y, 'constant' | 'collection' | 'null' | 'undefined' >, restore?: () => void, ): T; - private _makeComponent>( - metadata: MockFunctionMetadata, + private _makeComponent) => unknown>( + metadata: MockFunctionMetadata, restore?: () => void, - ): Mock; - private _makeComponent>( - metadata: MockFunctionMetadata, + ): Mock; + private _makeComponent) => unknown>( + metadata: MockFunctionMetadata, restore?: () => void, ): | Record | Array | RegExp - | T + | ReturnType | undefined - | Mock { + | Mock { if (metadata.type === 'object') { return new this._environmentGlobal.Object(); } else if (metadata.type === 'array') { @@ -625,7 +632,10 @@ export class ModuleMocker { {}; const prototypeSlots = this._getSlots(prototype); const mocker = this; - const mockConstructor = matchArity(function (this: T, ...args: Y) { + const mockConstructor = matchArity(function ( + this: ReturnType, + ...args: Parameters + ) { const mockState = mocker._ensureMockState(f); const mockConfig = mocker._ensureMockConfig(f); mockState.instances.push(this); @@ -711,21 +721,21 @@ export class ModuleMocker { } return finalReturnValue; - }, metadata.length || 0); + }, + metadata.length || 0); - const f = this._createMockFunction( - metadata, - mockConstructor, - ) as unknown as Mock; + const f = this._createMockFunction(metadata, mockConstructor) as Mock; f._isMockFunction = true; f.getMockImplementation = () => - this._ensureMockConfig(f).mockImpl as unknown as (...args: Y) => T; + this._ensureMockConfig(f).mockImpl as ( + ...args: Parameters + ) => ReturnType; if (typeof restore === 'function') { this._spyState.add(restore); } - this._mockState.set(f, this._defaultMockState()); + this._mockState.set(f, this._defaultMockState()); this._mockConfigRegistry.set(f, this._defaultMockConfig()); Object.defineProperty(f, 'mock', { @@ -751,29 +761,29 @@ export class ModuleMocker { return restore ? restore() : undefined; }; - f.mockReturnValueOnce = (value: T) => + f.mockReturnValueOnce = (value: ReturnType) => // next function call will return this value or default return value f.mockImplementationOnce(() => value); - f.mockResolvedValueOnce = (value: Unpromisify) => - f.mockImplementationOnce(() => Promise.resolve(value as T)); + f.mockResolvedValueOnce = (value: Unpromisify>) => + f.mockImplementationOnce(() => Promise.resolve(value as ReturnType)); f.mockRejectedValueOnce = (value: unknown) => f.mockImplementationOnce(() => Promise.reject(value)); - f.mockReturnValue = (value: T) => + f.mockReturnValue = (value: ReturnType) => // next function call will return specified return value or this one f.mockImplementation(() => value); - f.mockResolvedValue = (value: Unpromisify) => - f.mockImplementation(() => Promise.resolve(value as T)); + f.mockResolvedValue = (value: Unpromisify>) => + f.mockImplementation(() => Promise.resolve(value as ReturnType)); f.mockRejectedValue = (value: unknown) => f.mockImplementation(() => Promise.reject(value)); f.mockImplementationOnce = ( - fn: ((...args: Y) => T) | (() => Promise), - ): Mock => { + fn: ((...args: Array) => any) | (() => Promise), + ): Mock => { // next function call will use this mock implementation return value // or default mock implementation return value const mockConfig = this._ensureMockConfig(f); @@ -782,8 +792,8 @@ export class ModuleMocker { }; f.mockImplementation = ( - fn: ((...args: Y) => T) | (() => Promise), - ): Mock => { + fn: ((...args: Array) => any) | (() => Promise), + ): Mock => { // next function call will use mock implementation return value const mockConfig = this._ensureMockConfig(f); mockConfig.mockImpl = fn; @@ -791,7 +801,7 @@ export class ModuleMocker { }; f.mockReturnThis = () => - f.mockImplementation(function (this: T) { + f.mockImplementation(function (this: ReturnType) { return this; }); @@ -819,8 +829,8 @@ export class ModuleMocker { } } - private _createMockFunction>( - metadata: MockFunctionMetadata, + private _createMockFunction) => unknown>( + metadata: MockFunctionMetadata, mockConstructor: Function, ): Function { let name = metadata.name; @@ -879,8 +889,8 @@ export class ModuleMocker { return createConstructor(mockConstructor); } - private _generateMock>( - metadata: MockFunctionMetadata, + private _generateMock) => unknown>( + metadata: MockFunctionMetadata, callbacks: Array, refs: { [key: string]: @@ -889,9 +899,9 @@ export class ModuleMocker { | RegExp | T | undefined - | Mock; + | Mock; }, - ): Mock { + ): Mock { // metadata not compatible but it's the same type, maybe problem with // overloading of _makeComponent and not _generateMock? // @ts-expect-error @@ -922,7 +932,7 @@ export class ModuleMocker { mock.prototype.constructor = mock; } - return mock as Mock; + return mock as Mock; } /** @@ -930,9 +940,9 @@ 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) => unknown>( + _metadata: MockFunctionMetadata, + ): Mock { const callbacks: Array = []; const refs = {}; const mock = this._generateMock(_metadata, callbacks, refs); @@ -944,11 +954,11 @@ export class ModuleMocker { * @see README.md * @param component The component for which to retrieve metadata. */ - getMetadata>( - component: T, - _refs?: Map, - ): MockFunctionMetadata | null { - const refs = _refs || new Map(); + getMetadata) => unknown>( + component: ReturnType, + _refs?: Map, number>, + ): MockFunctionMetadata | null { + const refs = _refs || new Map, number>(); const ref = refs.get(component); if (ref != null) { return {ref}; @@ -959,7 +969,7 @@ export class ModuleMocker { return null; } - const metadata: MockFunctionMetadata = {type}; + const metadata: MockFunctionMetadata = {type}; if ( type === 'constant' || type === 'collection' || @@ -982,10 +992,11 @@ export class ModuleMocker { refs.set(component, metadata.refID); let members: { - [key: string]: MockFunctionMetadata; + [key: string]: MockFunctionMetadata; } | null = null; // Leave arrays alone if (type !== 'array') { + // @ts-expect-error component is object this._getSlots(component).forEach(slot => { if ( type === 'function' && @@ -996,7 +1007,7 @@ export class ModuleMocker { return; } // @ts-expect-error no index signature - const slotMetadata = this.getMetadata(component[slot], refs); + const slotMetadata = this.getMetadata(component[slot], refs); if (slotMetadata) { if (!members) { members = {}; @@ -1013,25 +1024,29 @@ export class ModuleMocker { return metadata; } - isMockFunction = Array>( - fn: Mock, - ): fn is Mock; - isMockFunction = Array>( - fn: SpyInstance, - ): fn is SpyInstance; - isMockFunction = Array>( - fn: (...args: Y) => T, - ): fn is Mock; - isMockFunction(fn: unknown): fn is Mock; - isMockFunction(fn: unknown): fn is Mock { - return !!fn && (fn as Mock)._isMockFunction === true; + isMockFunction any>( + fn: SpyInstance, + ): fn is SpyInstance; + isMockFunction>( + fn: (...args: P) => R, + ): fn is Mock<(...args: P) => R>; + isMockFunction(fn: unknown): fn is Mock<(...args: Array) => unknown>; + isMockFunction( + fn: unknown, + ): fn is Mock<(...args: Array) => unknown> { + return !!fn && (fn as any)._isMockFunction === true; } - fn>( - implementation?: (...args: Y) => T, - ): Mock { + fn< + T extends (...args: Array) => any = ( + ...args: Array + ) => unknown, + >(implementation?: T): Mock { const length = implementation ? implementation.length : 0; - const fn = this._makeComponent({length, type: 'function'}); + const fn = this._makeComponent({ + length, + type: 'function', + }); if (implementation) { fn.mockImplementation(implementation); } @@ -1042,26 +1057,26 @@ export class ModuleMocker { object: T, methodName: M, accessType: 'get', - ): SpyInstance; + ): SpyInstance<() => T[M]>; spyOn>( object: T, methodName: M, accessType: 'set', - ): SpyInstance; + ): SpyInstance<(arg: T[M]) => void>; spyOn>( object: T, methodName: M, ): T[M] extends ConstructorLike - ? SpyInstance, ConstructorParameters> + ? SpyInstance<(...args: ConstructorParameters) => InstanceType> : never; spyOn>( object: T, methodName: M, ): T[M] extends MethodLike - ? SpyInstance, Parameters> + ? SpyInstance<(...args: Parameters) => ReturnType> : never; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -1106,7 +1121,7 @@ export class ModuleMocker { proto = Object.getPrototypeOf(proto); } - let mock: Mock>; + let mock: Mock<(...args: Array) => unknown>; if (descriptor && descriptor.get) { const originalGet = descriptor.get; @@ -1140,7 +1155,7 @@ export class ModuleMocker { obj: T, propertyName: M, accessType: 'get' | 'set' = 'get', - ): Mock { + ): Mock<() => T> { if (typeof obj !== 'object' && typeof obj !== 'function') { throw new Error( 'Cannot spyOn on a primitive value; ' + this._typeOf(obj) + ' given', @@ -1192,14 +1207,13 @@ export class ModuleMocker { ); } - // @ts-expect-error: mock is assignable descriptor[accessType] = this._makeComponent({type: 'function'}, () => { // @ts-expect-error: mock is assignable descriptor![accessType] = original; Object.defineProperty(obj, propertyName, descriptor!); }); - (descriptor[accessType] as Mock).mockImplementation(function ( + (descriptor[accessType] as Mock<() => T>).mockImplementation(function ( this: unknown, ) { // @ts-expect-error @@ -1208,7 +1222,7 @@ export class ModuleMocker { } Object.defineProperty(obj, propertyName, descriptor); - return descriptor[accessType] as Mock; + return descriptor[accessType] as Mock<() => T>; } clearAllMocks(): void { diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 710a6a9b747b..8806b10fe927 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -193,10 +193,7 @@ export default class Runtime { private _isCurrentlyExecutingManualMock: string | null; private _mainModule: Module | null; private readonly _mockFactories: Map unknown>; - private readonly _mockMetaDataCache: Map< - string, - MockFunctionMetadata> - >; + private readonly _mockMetaDataCache: Map; private _mockRegistry: Map; private _isolatedMockRegistry: Map | null; private _moduleMockRegistry: Map; diff --git a/packages/jest-types/__typetests__/jest.test.ts b/packages/jest-types/__typetests__/jest.test.ts index 7dceb9589aa3..6c2a9f17b75a 100644 --- a/packages/jest-types/__typetests__/jest.test.ts +++ b/packages/jest-types/__typetests__/jest.test.ts @@ -133,7 +133,7 @@ expectError(jest.isMockFunction()); const maybeMock = (a: string, b: number) => true; if (jest.isMockFunction(maybeMock)) { - expectType>(maybeMock); + expectType boolean>>(maybeMock); maybeMock.mockReturnValueOnce(false); expectError(maybeMock.mockReturnValueOnce(123)); @@ -146,7 +146,7 @@ if (!jest.isMockFunction(maybeMock)) { const surelyMock = jest.fn((a: string, b: number) => true); if (jest.isMockFunction(surelyMock)) { - expectType>(surelyMock); + expectType boolean>>(surelyMock); surelyMock.mockReturnValueOnce(false); expectError(surelyMock.mockReturnValueOnce(123)); @@ -165,7 +165,7 @@ const spiedObject = { const surelySpy = jest.spyOn(spiedObject, 'methodA'); if (jest.isMockFunction(surelySpy)) { - expectType>(surelySpy); + expectType boolean>>(surelySpy); surelySpy.mockReturnValueOnce(false); expectError(surelyMock.mockReturnValueOnce(123)); @@ -178,7 +178,9 @@ if (!jest.isMockFunction(surelySpy)) { declare const stringMaybeMock: string; if (jest.isMockFunction(stringMaybeMock)) { - expectType>>(stringMaybeMock); + expectType) => unknown>>( + stringMaybeMock, + ); } if (!jest.isMockFunction(stringMaybeMock)) { @@ -188,7 +190,7 @@ if (!jest.isMockFunction(stringMaybeMock)) { declare const anyMaybeMock: any; if (jest.isMockFunction(anyMaybeMock)) { - expectType>>(anyMaybeMock); + expectType) => unknown>>(anyMaybeMock); } if (!jest.isMockFunction(anyMaybeMock)) { @@ -198,7 +200,7 @@ if (!jest.isMockFunction(anyMaybeMock)) { declare const unknownMaybeMock: unknown; if (jest.isMockFunction(unknownMaybeMock)) { - expectType>>(unknownMaybeMock); + expectType) => unknown>>(unknownMaybeMock); } if (!jest.isMockFunction(unknownMaybeMock)) { From 7df0d637502eba1dd69af1126c3430a095656cea Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Fri, 25 Feb 2022 22:30:04 +0200 Subject: [PATCH 02/23] TS examples --- docs/MockFunctionAPI.md | 308 ++++++++++++++++++++++++++++++++++------ 1 file changed, 261 insertions(+), 47 deletions(-) diff --git a/docs/MockFunctionAPI.md b/docs/MockFunctionAPI.md index 388a3f0fa836..134ac6bc059c 100644 --- a/docs/MockFunctionAPI.md +++ b/docs/MockFunctionAPI.md @@ -15,6 +15,8 @@ import TOCInline from "@theme/TOCInline" ## Reference +import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; + ### `mockFn.getMockName()` Returns the mock name string set by calling `mockFn.mockName(value)`. @@ -93,7 +95,7 @@ For example: A mock function `f` that has been called twice, with the arguments Clears all information stored in the [`mockFn.mock.calls`](#mockfnmockcalls), [`mockFn.mock.instances`](#mockfnmockinstances) and [`mockFn.mock.results`](#mockfnmockresults) arrays. Often this is useful when you want to clean up a mocks usage data between two assertions. -Beware that `mockClear` will replace `mockFn.mock`, not just these three properties! You should, therefore, avoid assigning `mockFn.mock` to other variables, temporary or not, to make sure you don't access stale data. +Beware that `mockFn.mockClear()` will replace `mockFn.mock`, not just these three properties! You should, therefore, avoid assigning `mockFn.mock` to other variables, temporary or not, to make sure you don't access stale data. The [`clearMocks`](configuration#clearmocks-boolean) configuration option is available to clear mocks automatically before each tests. @@ -111,7 +113,7 @@ Does everything that [`mockFn.mockReset()`](#mockfnmockreset) does, and also res This is useful when you want to mock functions in certain test cases and restore the original implementation in others. -Beware that `mockFn.mockRestore` only works when the mock was created with `jest.spyOn`. Thus you have to take care of restoration yourself when manually assigning `jest.fn()`. +Beware that `mockFn.mockRestore()` only works when the mock was created with `jest.spyOn()`. Thus you have to take care of restoration yourself when manually assigning `jest.fn()`. The [`restoreMocks`](configuration#restoremocks-boolean) configuration option is available to restore mocks automatically before each test. @@ -119,9 +121,14 @@ The [`restoreMocks`](configuration#restoremocks-boolean) configuration option is Accepts a function that should be used as the implementation of the mock. The mock itself will still record all calls that go into and instances that come from itself – the only difference is that the implementation will also be executed when the mock is called. -_Note: `jest.fn(implementation)` is a shorthand for `jest.fn().mockImplementation(implementation)`._ +:::note -For example: +Both `jest.fn(fn)` and `jest.fn().mockImplementation(fn)` are equivalent. If your tests are written in TypeScript, mock types will be inferred from `jest.fn()`, but `.mockImplementation()` will need manual type hints. See example bellow. + +::: + + + ```js const mockFn = jest.fn().mockImplementation(scalar => 42 + scalar); @@ -137,74 +144,154 @@ mockFn.mock.calls[0][0] === 0; // true mockFn.mock.calls[1][0] === 1; // true ``` -`mockImplementation` can also be used to mock class constructors: + + + + +```ts +const mockFn = jest + .fn<(number) => number>() + .mockImplementation(scalar => 42 + scalar); +// or: jest.fn(scalar => 42 + scalar); + +const a = mockFn(0); +const b = mockFn(1); + +a === 42; // true +b === 43; // true + +mockFn.mock.calls[0][0] === 0; // true +mockFn.mock.calls[1][0] === 1; // true +``` + + + + +It can also be used to mock class constructors: + + + ```js title="SomeClass.js" module.exports = class SomeClass { - m(a, b) {} + method(a, b) {} }; ``` -```js title="OtherModule.test.js" -jest.mock('./SomeClass'); // this happens automatically with automocking +```js title="SomeClass.test.js" const SomeClass = require('./SomeClass'); -const mMock = jest.fn(); + +jest.mock('./SomeClass'); // this happens automatically with automocking + +const mockMethod = jest.fn(); SomeClass.mockImplementation(() => { return { - m: mMock, + method: mockMethod, }; }); const some = new SomeClass(); -some.m('a', 'b'); -console.log('Calls to m: ', mMock.mock.calls); +some.method('a', 'b'); + +console.log('Calls to method: ', mockMethod.mock.calls); ``` + + + + +```js title="SomeClass.ts" +export class SomeClass { + method(a: string, b: string): void {} +} +``` + +```js title="SomeClass.test.ts" +import {SomeClass} from './SomeClass'; + +jest.mock('./SomeClass'); // this happens automatically with automocking + +const mockMethod = jest.fn<(a: string, b: string) => void>(); +SomeClass.mockImplementation(() => { + return { + method: mockMethod, + }; +}); + +const some = new SomeClass(); +some.method('a', 'b'); + +console.log('Calls to method: ', mockMethod.mock.calls); +``` + + + + ### `mockFn.mockImplementationOnce(fn)` Accepts a function that will be used as an implementation of the mock for one call to the mocked function. Can be chained so that multiple function calls produce different results. + + + ```js -const myMockFn = jest +const mockFn = jest .fn() .mockImplementationOnce(cb => cb(null, true)) .mockImplementationOnce(cb => cb(null, false)); -myMockFn((err, val) => console.log(val)); // true +mockFn((err, val) => console.log(val)); // true +mockFn((err, val) => console.log(val)); // false +``` + + + + + +```ts +const mockFn = jest + .fn<(cb: (a: null, b: boolean) => void) => void>() + .mockImplementationOnce(cb => cb(null, true)) + .mockImplementationOnce(cb => cb(null, false)); -myMockFn((err, val) => console.log(val)); // false +mockFn((err, val) => console.log(val)); // true +mockFn((err, val) => console.log(val)); // false ``` -When the mocked function runs out of implementations defined with mockImplementationOnce, it will execute the default implementation set with `jest.fn(() => defaultValue)` or `.mockImplementation(() => defaultValue)` if they were called: + + + +When the mocked function runs out of implementations defined with `.mockImplementationOnce()`, it will execute the default implementation set with `jest.fn(() => defaultValue)` or `.mockImplementation(() => defaultValue)` if they were called: ```js -const myMockFn = jest +const mockFn = jest .fn(() => 'default') .mockImplementationOnce(() => 'first call') .mockImplementationOnce(() => 'second call'); -// 'first call', 'second call', 'default', 'default' -console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn()); +mockFn(); // 'first call' +mockFn(); // 'second call' +mockFn(); // 'default' +mockFn(); // 'default' ``` -### `mockFn.mockName(value)` +### `mockFn.mockName(name)` -Accepts a string to use in test result output in place of "jest.fn()" to indicate which mock function is being referenced. +Accepts a string to use in test result output in place of `'jest.fn()'` to indicate which mock function is being referenced. For example: ```js const mockFn = jest.fn().mockName('mockedFunction'); + // mockFn(); expect(mockFn).toHaveBeenCalled(); ``` Will result in this error: -``` +```bash expect(mockedFunction).toHaveBeenCalled() - -Expected mock function "mockedFunction" to have been called, but it was not called. ``` ### `mockFn.mockReturnThis()` @@ -221,29 +308,76 @@ jest.fn(function () { Accepts a value that will be returned whenever the mock function is called. + + + ```js const mock = jest.fn(); + +mock.mockReturnValue(42); +mock(); // 42 + +mock.mockReturnValue(43); +mock(); // 43 +``` + + + + + +```ts +const mock = jest.fn<() => number>(); + mock.mockReturnValue(42); mock(); // 42 + mock.mockReturnValue(43); mock(); // 43 ``` + + + ### `mockFn.mockReturnValueOnce(value)` Accepts a value that will be returned for one call to the mock function. Can be chained so that successive calls to the mock function return different values. When there are no more `mockReturnValueOnce` values to use, calls will return a value specified by `mockReturnValue`. + + + ```js -const myMockFn = jest +const mockFn = jest .fn() .mockReturnValue('default') .mockReturnValueOnce('first call') .mockReturnValueOnce('second call'); -// 'first call', 'second call', 'default', 'default' -console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn()); +mockFn(); // 'first call' +mockFn(); // 'second call' +mockFn(); // 'default' +mockFn(); // 'default' ``` + + + + +```ts +const mockFn = jest + .fn<() => string>() + .mockReturnValue('default') + .mockReturnValueOnce('first call') + .mockReturnValueOnce('second call'); + +mockFn(); // 'first call' +mockFn(); // 'second call' +mockFn(); // 'default' +mockFn(); // 'default' +``` + + + + ### `mockFn.mockResolvedValue(value)` Syntactic sugar function for: @@ -254,6 +388,9 @@ jest.fn().mockImplementation(() => Promise.resolve(value)); Useful to mock async functions in async tests: + + + ```js test('async test', async () => { const asyncMock = jest.fn().mockResolvedValue(43); @@ -262,6 +399,21 @@ test('async test', async () => { }); ``` + + + + +```ts +test('async test', async () => { + const asyncMock = jest.fn<() => Promise>().mockResolvedValue(43); + + await asyncMock(); // 43 +}); +``` + + + + ### `mockFn.mockResolvedValueOnce(value)` Syntactic sugar function for: @@ -272,6 +424,9 @@ jest.fn().mockImplementationOnce(() => Promise.resolve(value)); Useful to resolve different values over multiple async calls: + + + ```js test('async test', async () => { const asyncMock = jest @@ -280,13 +435,35 @@ test('async test', async () => { .mockResolvedValueOnce('first call') .mockResolvedValueOnce('second call'); - await asyncMock(); // first call - await asyncMock(); // second call - await asyncMock(); // default - await asyncMock(); // default + await asyncMock(); // 'first call' + await asyncMock(); // 'second call' + await asyncMock(); // 'default' + await asyncMock(); // 'default' }); ``` + + + + +```ts +test('async test', async () => { + const asyncMock = jest + .fn<() => Promise>() + .mockResolvedValue('default') + .mockResolvedValueOnce('first call') + .mockResolvedValueOnce('second call'); + + await asyncMock(); // 'first call' + await asyncMock(); // 'second call' + await asyncMock(); // 'default' + await asyncMock(); // 'default' +}); +``` + + + + ### `mockFn.mockRejectedValue(value)` Syntactic sugar function for: @@ -297,14 +474,36 @@ jest.fn().mockImplementation(() => Promise.reject(value)); Useful to create async mock functions that will always reject: + + + ```js test('async test', async () => { - const asyncMock = jest.fn().mockRejectedValue(new Error('Async error')); + const asyncMock = jest + .fn() + .mockRejectedValue(new Error('Async error message')); + + await asyncMock(); // throws 'Async error message' +}); +``` + + + + + +```ts +test('async test', async () => { + const asyncMock = jest + .fn<() => Promise>() + .mockRejectedValue(new Error('Async error message')); - await asyncMock(); // throws "Async error" + await asyncMock(); // throws 'Async error message' }); ``` + + + ### `mockFn.mockRejectedValueOnce(value)` Syntactic sugar function for: @@ -315,21 +514,41 @@ jest.fn().mockImplementationOnce(() => Promise.reject(value)); Example usage: + + + ```js test('async test', async () => { const asyncMock = jest .fn() .mockResolvedValueOnce('first call') - .mockRejectedValueOnce(new Error('Async error')); + .mockRejectedValueOnce(new Error('Async error message')); - await asyncMock(); // first call - await asyncMock(); // throws "Async error" + await asyncMock(); // 'first call' + await asyncMock(); // throws 'Async error message' }); ``` -## TypeScript + + + + +```ts +test('async test', async () => { + const asyncMock = jest + .fn<() => Promise>() + .mockResolvedValueOnce('first call') + .mockRejectedValueOnce(new Error('Async error message')); + + await asyncMock(); // 'first call' + await asyncMock(); // throws 'Async error message' +}); +``` -Jest itself is written in [TypeScript](https://www.typescriptlang.org). + + + +## TypeScript If you are using [Create React App](https://create-react-app.dev) then the [TypeScript template](https://create-react-app.dev/docs/adding-typescript/) has everything you need to start writing tests in TypeScript. @@ -368,17 +587,12 @@ test('calculate calls add', () => { Example using [`jest.fn`](JestObjectAPI.md#jestfnimplementation): ```ts -// Here `add` is imported for its type -import add from './add'; +import type add from './add'; import calculate from './calc'; test('calculate calls add', () => { // Create a new mock that can be used in place of `add`. - const mockAdd = jest.fn() as jest.MockedFunction; - - // Note: You can use the `jest.fn` type directly like this if you want: - // const mockAdd = jest.fn, Parameters>(); - // `jest.MockedFunction` is a more friendly shortcut. + const mockAdd = jest.fn(); // Now we can easily set up mock implementations. // All the `.mock*` API can now give you proper types for `add`. @@ -392,8 +606,8 @@ test('calculate calls add', () => { return a + b; }); - // `mockAdd` is properly typed and therefore accepted by - // anything requiring `add`. + // `mockAdd` is properly typed and therefore accepted by anything + // requiring `add`. calculate(mockAdd, 1, 2); expect(mockAdd).toBeCalledTimes(1); From a08489e884b4d3ae9169b84babc748f81a2a8b80 Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Fri, 25 Feb 2022 22:32:49 +0200 Subject: [PATCH 03/23] fix ts --- docs/MockFunctionAPI.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/MockFunctionAPI.md b/docs/MockFunctionAPI.md index 134ac6bc059c..d4491f351996 100644 --- a/docs/MockFunctionAPI.md +++ b/docs/MockFunctionAPI.md @@ -200,13 +200,13 @@ console.log('Calls to method: ', mockMethod.mock.calls); -```js title="SomeClass.ts" +```ts title="SomeClass.ts" export class SomeClass { method(a: string, b: string): void {} } ``` -```js title="SomeClass.test.ts" +```ts title="SomeClass.test.ts" import {SomeClass} from './SomeClass'; jest.mock('./SomeClass'); // this happens automatically with automocking From 561b90706694630dd735ea3e01caf78ef7770b9b Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 26 Feb 2022 08:38:02 +0200 Subject: [PATCH 04/23] improve docs --- docs/MockFunctionAPI.md | 50 ++++++++--------------------------------- 1 file changed, 9 insertions(+), 41 deletions(-) diff --git a/docs/MockFunctionAPI.md b/docs/MockFunctionAPI.md index d4491f351996..96adf39f8a4c 100644 --- a/docs/MockFunctionAPI.md +++ b/docs/MockFunctionAPI.md @@ -121,53 +121,21 @@ The [`restoreMocks`](configuration#restoremocks-boolean) configuration option is Accepts a function that should be used as the implementation of the mock. The mock itself will still record all calls that go into and instances that come from itself – the only difference is that the implementation will also be executed when the mock is called. -:::note - -Both `jest.fn(fn)` and `jest.fn().mockImplementation(fn)` are equivalent. If your tests are written in TypeScript, mock types will be inferred from `jest.fn()`, but `.mockImplementation()` will need manual type hints. See example bellow. - -::: - - - +Both `jest.fn(fn)` and `jest.fn().mockImplementation(fn)` are equivalent. For instance, you can use `.mockImplementation()` to replace the implementation of a mock: ```js -const mockFn = jest.fn().mockImplementation(scalar => 42 + scalar); -// or: jest.fn(scalar => 42 + scalar); +const mockFn = jest.fn(scalar => 42 + scalar); -const a = mockFn(0); -const b = mockFn(1); +mockFn(0); // 42 +mockFn(1); // 43 -a === 42; // true -b === 43; // true +mockFn.mockImplementation(scalar => 36 + scalar); -mockFn.mock.calls[0][0] === 0; // true -mockFn.mock.calls[1][0] === 1; // true +mockFn(2); // 38 +mockFn(3); // 39 ``` - - - - -```ts -const mockFn = jest - .fn<(number) => number>() - .mockImplementation(scalar => 42 + scalar); -// or: jest.fn(scalar => 42 + scalar); - -const a = mockFn(0); -const b = mockFn(1); - -a === 42; // true -b === 43; // true - -mockFn.mock.calls[0][0] === 0; // true -mockFn.mock.calls[1][0] === 1; // true -``` - - - - -It can also be used to mock class constructors: +It also helps to mock class constructors: @@ -512,7 +480,7 @@ Syntactic sugar function for: jest.fn().mockImplementationOnce(() => Promise.reject(value)); ``` -Example usage: +Useful together with `.mockResolvedValueOnce()` or to reject with different exceptions over multiple async calls: From 4dce116d8628bcda5a27f7c75f7270127ffe9fd6 Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 26 Feb 2022 11:03:10 +0200 Subject: [PATCH 05/23] refactor types --- .../__typetests__/mock-functions.test.ts | 33 ++- .../__typetests__/utility-types.test.ts | 32 +- packages/jest-mock/src/index.ts | 274 ++++++++---------- 3 files changed, 166 insertions(+), 173 deletions(-) diff --git a/packages/jest-mock/__typetests__/mock-functions.test.ts b/packages/jest-mock/__typetests__/mock-functions.test.ts index 945bd0244b17..e1ace1d254f4 100644 --- a/packages/jest-mock/__typetests__/mock-functions.test.ts +++ b/packages/jest-mock/__typetests__/mock-functions.test.ts @@ -19,18 +19,39 @@ expectType Promise>>( fn(async () => 'value') .mockClear() .mockReset() - .mockImplementation(async () => 'value') - .mockImplementationOnce(async () => 'value') + .mockImplementation(fn(async () => 'value')) + .mockImplementationOnce(fn(async () => 'value')) .mockName('mock') - .mockReturnThis() - .mockReturnValue(Promise.resolve('value')) - .mockReturnValueOnce(Promise.resolve('value')) .mockResolvedValue('value') .mockResolvedValueOnce('value') .mockRejectedValue('error') - .mockRejectedValue('error'), + .mockRejectedValueOnce('error') + .mockReturnThis() + .mockReturnValue(Promise.resolve('value')) + .mockReturnValueOnce(Promise.resolve('value')), ); +expectType string>>( + fn(() => 'value') + .mockClear() + .mockReset() + .mockImplementation(() => 'value') + .mockImplementationOnce(() => 'value') + .mockName('mock') + .mockReturnThis() + .mockReturnValue('value') + .mockReturnValueOnce('value'), +); + +expectError(fn(() => 'value').mockReturnValue(Promise.resolve('value'))); +expectError(fn(() => 'value').mockReturnValueOnce(Promise.resolve('value'))); + +expectError(fn(() => 'value').mockResolvedValue('value')); +expectError(fn(() => 'value').mockResolvedValueOnce('value')); + +expectError(fn(() => 'value').mockRejectedValue('error')); +expectError(fn(() => 'value').mockRejectedValueOnce('error')); + expectAssignable(fn()); // eslint-disable-line @typescript-eslint/ban-types expectType) => unknown>>(fn()); diff --git a/packages/jest-mock/__typetests__/utility-types.test.ts b/packages/jest-mock/__typetests__/utility-types.test.ts index af4efdb60a49..3b866f801e49 100644 --- a/packages/jest-mock/__typetests__/utility-types.test.ts +++ b/packages/jest-mock/__typetests__/utility-types.test.ts @@ -7,9 +7,9 @@ import {expectAssignable, expectNotAssignable, expectType} from 'tsd-lite'; import type { - ConstructorLike, + ClassLike, ConstructorLikeKeys, - MethodLike, + FunctionLike, MethodLikeKeys, PropertyLikeKeys, } from 'jest-mock'; @@ -58,27 +58,27 @@ type SomeObject = typeof someObject; // ClassLike -expectAssignable(SomeClass); -expectNotAssignable(() => {}); -expectNotAssignable(function abc() { +expectAssignable(SomeClass); +expectNotAssignable(() => {}); +expectNotAssignable(function abc() { return; }); -expectNotAssignable('abc'); -expectNotAssignable(123); -expectNotAssignable(false); -expectNotAssignable(someObject); +expectNotAssignable('abc'); +expectNotAssignable(123); +expectNotAssignable(false); +expectNotAssignable(someObject); // FunctionLike -expectAssignable(() => {}); -expectAssignable(function abc() { +expectAssignable(() => {}); +expectAssignable(function abc() { return; }); -expectNotAssignable('abc'); -expectNotAssignable(123); -expectNotAssignable(false); -expectNotAssignable(SomeClass); -expectNotAssignable(someObject); +expectNotAssignable('abc'); +expectNotAssignable(123); +expectNotAssignable(false); +expectNotAssignable(SomeClass); +expectNotAssignable(someObject); // ConstructorKeys diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 1a2944e78a2c..2beb9bfa4a29 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -7,48 +7,21 @@ /* eslint-disable local/ban-types-eventually, local/prefer-rest-params-eventually */ -export type MockFunctionMetadataType = - | 'object' - | 'array' - | 'regexp' - | 'function' - | 'constant' - | 'collection' - | 'null' - | 'undefined'; - -export type MockFunctionMetadata< - T extends (...args: Array) => unknown = ( - ...args: Array - ) => unknown, - MetadataType = MockFunctionMetadataType, -> = { - ref?: number; - members?: Record>; - mockImpl?: (...args: Parameters) => ReturnType; - name?: string; - refID?: number; - type?: MetadataType; - value?: ReturnType; - length?: number; -}; - -export type ConstructorLike = {new (...args: Array): any}; - -export type MethodLike = (...args: Array) => any; +export type ClassLike = {new (...args: any): any}; +export type FunctionLike = (...args: any) => any; export type ConstructorLikeKeys = { - [K in keyof T]: T[K] extends ConstructorLike ? K : never; + [K in keyof T]: T[K] extends ClassLike ? K : never; }[keyof T]; export type MethodLikeKeys = { - [K in keyof T]: T[K] extends MethodLike ? K : never; + [K in keyof T]: T[K] extends FunctionLike ? K : never; }[keyof T]; export type PropertyLikeKeys = { - [K in keyof T]: T[K] extends MethodLike + [K in keyof T]: T[K] extends FunctionLike ? never - : T[K] extends ConstructorLike + : T[K] extends ClassLike ? never : K; }[keyof T]; @@ -65,101 +38,127 @@ export type MaybeMockedConstructor = T extends new ( ? MockInstance<(...args: ConstructorParameters) => R> : T; -export interface MockWithArgs - extends MockInstance<(...args: Parameters) => ReturnType> { +export interface MockWithArgs + extends MockInstance> { new (...args: ConstructorParameters): T; (...args: Parameters): ReturnType; } -export type MockedFunction = MockWithArgs & { +export type MockedFunction = MockWithArgs & { [K in keyof T]: T[K]; }; -export type MockedFunctionDeep = MockWithArgs & +export type MockedFunctionDeep = MockWithArgs & MockedObjectDeep; export type MockedObject = MaybeMockedConstructor & { - [K in MethodLikeKeys]: T[K] extends MethodLike + [K in MethodLikeKeys]: T[K] extends FunctionLike ? MockedFunction : T[K]; } & {[K in PropertyLikeKeys]: T[K]}; export type MockedObjectDeep = MaybeMockedConstructor & { - [K in MethodLikeKeys]: T[K] extends MethodLike + [K in MethodLikeKeys]: T[K] extends FunctionLike ? MockedFunctionDeep : T[K]; } & {[K in PropertyLikeKeys]: MaybeMockedDeep}; -export type MaybeMocked = T extends MethodLike +export type MaybeMocked = T extends FunctionLike ? MockedFunction : T extends object ? MockedObject : T; -export type MaybeMockedDeep = T extends MethodLike +export type MaybeMockedDeep = T extends FunctionLike ? MockedFunctionDeep : T extends object ? MockedObjectDeep : T; export type Mocked = { - [P in keyof T]: T[P] extends MethodLike - ? MockInstance<(...args: Parameters) => ReturnType> - : T[P] extends ConstructorLike + [P in keyof T]: T[P] extends FunctionLike + ? MockInstance> + : T[P] extends ClassLike ? MockedClass : T[P]; } & T; -export type MockedClass = MockInstance< +export type MockedClass = MockInstance< (args: T extends new (...args: infer P) => any ? P : never) => InstanceType > & { prototype: T extends {prototype: any} ? Mocked : never; } & T; -export interface Mock< - T extends (...args: Array) => any = (...args: Array) => unknown, -> extends Function, +type UnknownFunction = (...args: Array) => unknown; + +export interface Mock + extends Function, MockInstance { new (...args: Parameters): ReturnType; (...args: Parameters): ReturnType; } -// TODO Replace with Awaited utility type when minimum supported TS version will be 4.5 or above -//https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-5.html#the-awaited-type-and-promise-improvements -type Unpromisify = T extends Promise ? R : never; +type FunctionType = ( + ...args: Parameters +) => ReturnType; + +type ResolveType = ReturnType extends PromiseLike< + infer U +> + ? U + : never; + +type RejectType = ReturnType extends PromiseLike + ? unknown + : never; -export interface MockInstance< - T extends (...args: any) => unknown = (...args: Array) => unknown, -> { +export type MockFunctionMetadataType = + | 'object' + | 'array' + | 'regexp' + | 'function' + | 'constant' + | 'collection' + | 'null' + | 'undefined'; + +export type MockFunctionMetadata< + T extends FunctionLike = UnknownFunction, + MetadataType = MockFunctionMetadataType, +> = { + ref?: number; + members?: Record>; + mockImpl?: FunctionType; + name?: string; + refID?: number; + type?: MetadataType; + value?: ReturnType; + length?: number; +}; + +export interface MockInstance { _isMockFunction: true; _protoImpl: Function; - getMockImplementation(): - | ((...args: Parameters) => ReturnType) - | undefined; + getMockImplementation(): FunctionType | undefined; getMockName(): string; mock: MockFunctionState; mockClear(): this; mockReset(): this; mockRestore(): void; - mockImplementation(fn: (...args: Parameters) => ReturnType): this; - /** @internal */ - mockImplementation(fn: (...args: Array) => unknown): this; - mockImplementationOnce(fn: (...args: Parameters) => ReturnType): this; - /** @internal */ - mockImplementationOnce(fn: (...args: Array) => unknown): this; + mockImplementation(fn: FunctionType): this; + mockImplementationOnce(fn: FunctionType): this; mockName(name: string): this; mockReturnThis(): this; mockReturnValue(value: ReturnType): this; mockReturnValueOnce(value: ReturnType): this; - mockResolvedValue(value: Unpromisify>): this; - mockResolvedValueOnce(value: Unpromisify>): this; - mockRejectedValue(value: unknown): this; - mockRejectedValueOnce(value: unknown): this; + mockResolvedValue(value: ResolveType): this; + mockResolvedValueOnce(value: ResolveType): this; + mockRejectedValue(value: RejectType): this; + mockRejectedValueOnce(value: RejectType): this; } -export interface SpyInstance< - T extends (...arg: any) => unknown = (...args: Array) => unknown, -> extends MockInstance {} +export interface SpyInstance + extends MockInstance {} type MockFunctionResultIncomplete = { type: 'incomplete'; @@ -170,12 +169,12 @@ type MockFunctionResultIncomplete = { */ value: undefined; }; -type MockFunctionResultReturn = { +type MockFunctionResultReturn = { type: 'return'; /** * Result of a single call to a mock function that returned. */ - value: T; + value: ReturnType; }; type MockFunctionResultThrow = { type: 'throw'; @@ -185,12 +184,12 @@ type MockFunctionResultThrow = { value: unknown; }; -type MockFunctionResult = +type MockFunctionResult = | MockFunctionResultIncomplete | MockFunctionResultReturn | MockFunctionResultThrow; -type MockFunctionState) => unknown> = { +type MockFunctionState = { /** * List of the call arguments of all calls that have been made to the mock. */ @@ -212,7 +211,7 @@ type MockFunctionState) => unknown> = { /** * List of the results of all calls that have been made to the mock. */ - results: Array>>; + results: Array>; }; type MockFunctionConfig = { @@ -436,7 +435,7 @@ function getType(ref?: unknown): MockFunctionMetadataType | null { } } -function isReadonlyProp(object: any, prop: string): boolean { +function isReadonlyProp(object: unknown, prop: string): boolean { if ( prop === 'arguments' || prop === 'caller' || @@ -466,10 +465,7 @@ function isReadonlyProp(object: any, prop: string): boolean { export class ModuleMocker { private _environmentGlobal: typeof globalThis; - private _mockState: WeakMap< - Mock<(...args: Array) => unknown>, - MockFunctionState<(...args: Array) => unknown> - >; + private _mockState: WeakMap; private _mockConfigRegistry: WeakMap; private _spyState: Set<() => void>; private _invocationCallCounter: number; @@ -533,9 +529,7 @@ export class ModuleMocker { return Array.from(slots); } - private _ensureMockConfig) => unknown>( - f: Mock, - ): MockFunctionConfig { + private _ensureMockConfig(f: Mock): MockFunctionConfig { let config = this._mockConfigRegistry.get(f); if (!config) { config = this._defaultMockConfig(); @@ -544,9 +538,7 @@ export class ModuleMocker { return config; } - private _ensureMockState) => unknown>( - f: Mock, - ): MockFunctionState { + private _ensureMockState(f: Mock): MockFunctionState { let state = this._mockState.get(f); if (!state) { state = this._defaultMockState(); @@ -555,7 +547,7 @@ export class ModuleMocker { if (state.calls.length > 0) { state.lastCall = state.calls[state.calls.length - 1]; } - return state as MockFunctionState; + return state; } private _defaultMockConfig(): MockFunctionConfig { @@ -567,9 +559,7 @@ export class ModuleMocker { }; } - private _defaultMockState< - T extends (...args: Array) => unknown, - >(): MockFunctionState { + private _defaultMockState(): MockFunctionState { return { calls: [], instances: [], @@ -578,30 +568,30 @@ export class ModuleMocker { }; } - private _makeComponent) => unknown>( + private _makeComponent( metadata: MockFunctionMetadata, restore?: () => void, ): Record; - private _makeComponent) => unknown>( + private _makeComponent( metadata: MockFunctionMetadata, restore?: () => void, ): Array; - private _makeComponent) => unknown>( + private _makeComponent( metadata: MockFunctionMetadata, restore?: () => void, ): RegExp; - private _makeComponent) => unknown>( + private _makeComponent( metadata: MockFunctionMetadata< T, 'constant' | 'collection' | 'null' | 'undefined' >, restore?: () => void, ): T; - private _makeComponent) => unknown>( + private _makeComponent( metadata: MockFunctionMetadata, restore?: () => void, - ): Mock; - private _makeComponent) => unknown>( + ): Mock; + private _makeComponent( metadata: MockFunctionMetadata, restore?: () => void, ): @@ -610,7 +600,7 @@ export class ModuleMocker { | RegExp | ReturnType | undefined - | Mock { + | Mock { if (metadata.type === 'object') { return new this._environmentGlobal.Object(); } else if (metadata.type === 'array') { @@ -644,7 +634,7 @@ export class ModuleMocker { // calling rather than waiting for the mock to return. This avoids // issues caused by recursion where results can be recorded in the // wrong order. - const mockResult: MockFunctionResult = { + const mockResult: MockFunctionResult = { type: 'incomplete', value: undefined, }; @@ -724,18 +714,16 @@ export class ModuleMocker { }, metadata.length || 0); - const f = this._createMockFunction(metadata, mockConstructor) as Mock; + const f = this._createMockFunction(metadata, mockConstructor) as Mock; f._isMockFunction = true; f.getMockImplementation = () => - this._ensureMockConfig(f).mockImpl as ( - ...args: Parameters - ) => ReturnType; + this._ensureMockConfig(f).mockImpl as UnknownFunction; if (typeof restore === 'function') { this._spyState.add(restore); } - this._mockState.set(f, this._defaultMockState()); + this._mockState.set(f, this._defaultMockState()); this._mockConfigRegistry.set(f, this._defaultMockConfig()); Object.defineProperty(f, 'mock', { @@ -765,8 +753,8 @@ export class ModuleMocker { // next function call will return this value or default return value f.mockImplementationOnce(() => value); - f.mockResolvedValueOnce = (value: Unpromisify>) => - f.mockImplementationOnce(() => Promise.resolve(value as ReturnType)); + f.mockResolvedValueOnce = (value: ResolveType) => + f.mockImplementationOnce(() => Promise.resolve(value)); f.mockRejectedValueOnce = (value: unknown) => f.mockImplementationOnce(() => Promise.reject(value)); @@ -775,15 +763,13 @@ export class ModuleMocker { // next function call will return specified return value or this one f.mockImplementation(() => value); - f.mockResolvedValue = (value: Unpromisify>) => - f.mockImplementation(() => Promise.resolve(value as ReturnType)); + f.mockResolvedValue = (value: ResolveType) => + f.mockImplementation(() => Promise.resolve(value)); f.mockRejectedValue = (value: unknown) => f.mockImplementation(() => Promise.reject(value)); - f.mockImplementationOnce = ( - fn: ((...args: Array) => any) | (() => Promise), - ): Mock => { + f.mockImplementationOnce = (fn: UnknownFunction) => { // next function call will use this mock implementation return value // or default mock implementation return value const mockConfig = this._ensureMockConfig(f); @@ -791,9 +777,7 @@ export class ModuleMocker { return f; }; - f.mockImplementation = ( - fn: ((...args: Array) => any) | (() => Promise), - ): Mock => { + f.mockImplementation = (fn: UnknownFunction) => { // next function call will use mock implementation return value const mockConfig = this._ensureMockConfig(f); mockConfig.mockImpl = fn; @@ -829,8 +813,8 @@ export class ModuleMocker { } } - private _createMockFunction) => unknown>( - metadata: MockFunctionMetadata, + private _createMockFunction( + metadata: MockFunctionMetadata, mockConstructor: Function, ): Function { let name = metadata.name; @@ -889,19 +873,19 @@ export class ModuleMocker { return createConstructor(mockConstructor); } - private _generateMock) => unknown>( - metadata: MockFunctionMetadata, + private _generateMock( + metadata: MockFunctionMetadata, callbacks: Array, refs: { [key: string]: | Record | Array | RegExp - | T + | UnknownFunction | undefined - | Mock; + | Mock; }, - ): Mock { + ): Mock { // metadata not compatible but it's the same type, maybe problem with // overloading of _makeComponent and not _generateMock? // @ts-expect-error @@ -932,7 +916,7 @@ export class ModuleMocker { mock.prototype.constructor = mock; } - return mock as Mock; + return mock as Mock; } /** @@ -940,9 +924,7 @@ export class ModuleMocker { * @param _metadata Metadata for the mock in the schema returned by the * getMetadata method of this module. */ - generateFromMetadata) => unknown>( - _metadata: MockFunctionMetadata, - ): Mock { + generateFromMetadata(_metadata: MockFunctionMetadata): Mock { const callbacks: Array = []; const refs = {}; const mock = this._generateMock(_metadata, callbacks, refs); @@ -954,10 +936,10 @@ export class ModuleMocker { * @see README.md * @param component The component for which to retrieve metadata. */ - getMetadata) => unknown>( + getMetadata( component: ReturnType, _refs?: Map, number>, - ): MockFunctionMetadata | null { + ): MockFunctionMetadata | null { const refs = _refs || new Map, number>(); const ref = refs.get(component); if (ref != null) { @@ -969,7 +951,7 @@ export class ModuleMocker { return null; } - const metadata: MockFunctionMetadata = {type}; + const metadata: MockFunctionMetadata = {type}; if ( type === 'constant' || type === 'collection' || @@ -992,7 +974,7 @@ export class ModuleMocker { refs.set(component, metadata.refID); let members: { - [key: string]: MockFunctionMetadata; + [key: string]: MockFunctionMetadata; } | null = null; // Leave arrays alone if (type !== 'array') { @@ -1007,7 +989,7 @@ export class ModuleMocker { return; } // @ts-expect-error no index signature - const slotMetadata = this.getMetadata(component[slot], refs); + const slotMetadata = this.getMetadata(component[slot], refs); if (slotMetadata) { if (!members) { members = {}; @@ -1024,24 +1006,18 @@ export class ModuleMocker { return metadata; } - isMockFunction any>( + isMockFunction( fn: SpyInstance, ): fn is SpyInstance; - isMockFunction>( + isMockFunction

, R extends unknown>( fn: (...args: P) => R, ): fn is Mock<(...args: P) => R>; - isMockFunction(fn: unknown): fn is Mock<(...args: Array) => unknown>; - isMockFunction( - fn: unknown, - ): fn is Mock<(...args: Array) => unknown> { + isMockFunction(fn: unknown): fn is Mock; + isMockFunction(fn: unknown): fn is Mock { return !!fn && (fn as any)._isMockFunction === true; } - fn< - T extends (...args: Array) => any = ( - ...args: Array - ) => unknown, - >(implementation?: T): Mock { + fn(implementation?: T): Mock { const length = implementation ? implementation.length : 0; const fn = this._makeComponent({ length, @@ -1068,16 +1044,14 @@ export class ModuleMocker { spyOn>( object: T, methodName: M, - ): T[M] extends ConstructorLike + ): T[M] extends ClassLike ? SpyInstance<(...args: ConstructorParameters) => InstanceType> : never; spyOn>( object: T, methodName: M, - ): T[M] extends MethodLike - ? SpyInstance<(...args: Parameters) => ReturnType> - : never; + ): T[M] extends FunctionLike ? SpyInstance> : never; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types spyOn>( @@ -1121,7 +1095,7 @@ export class ModuleMocker { proto = Object.getPrototypeOf(proto); } - let mock: Mock<(...args: Array) => unknown>; + let mock: Mock; if (descriptor && descriptor.get) { const originalGet = descriptor.get; @@ -1213,7 +1187,7 @@ export class ModuleMocker { Object.defineProperty(obj, propertyName, descriptor!); }); - (descriptor[accessType] as Mock<() => T>).mockImplementation(function ( + (descriptor[accessType] as Mock).mockImplementation(function ( this: unknown, ) { // @ts-expect-error @@ -1222,7 +1196,7 @@ export class ModuleMocker { } Object.defineProperty(obj, propertyName, descriptor); - return descriptor[accessType] as Mock<() => T>; + return descriptor[accessType] as Mock; } clearAllMocks(): void { @@ -1245,11 +1219,9 @@ export class ModuleMocker { // the typings test helper mocked(item: T, deep?: false): MaybeMocked; - mocked(item: T, deep: true): MaybeMockedDeep; - mocked(item: T, _deep = false): MaybeMocked | MaybeMockedDeep { - return item as any; + return item as MaybeMocked | MaybeMockedDeep; } } From 3f7fa09d6ffb4fbc45007a9048295f18ab73b21b Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 26 Feb 2022 11:19:24 +0200 Subject: [PATCH 06/23] tiny fix --- packages/jest-mock/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 2beb9bfa4a29..7722ae8a0a17 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -989,7 +989,7 @@ export class ModuleMocker { return; } // @ts-expect-error no index signature - const slotMetadata = this.getMetadata(component[slot], refs); + const slotMetadata = this.getMetadata(component[slot], refs); if (slotMetadata) { if (!members) { members = {}; From bba8fdff76fd60cd9d15962a41f46198d981b5e7 Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 26 Feb 2022 11:23:25 +0200 Subject: [PATCH 07/23] reduce diff --- packages/jest-mock/src/index.ts | 48 ++++++++++++++++----------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 7722ae8a0a17..ebac1901a513 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -7,6 +7,30 @@ /* eslint-disable local/ban-types-eventually, local/prefer-rest-params-eventually */ +export type MockFunctionMetadataType = + | 'object' + | 'array' + | 'regexp' + | 'function' + | 'constant' + | 'collection' + | 'null' + | 'undefined'; + +export type MockFunctionMetadata< + T extends FunctionLike = UnknownFunction, + MetadataType = MockFunctionMetadataType, +> = { + ref?: number; + members?: Record>; + mockImpl?: FunctionType; + name?: string; + refID?: number; + type?: MetadataType; + value?: ReturnType; + length?: number; +}; + export type ClassLike = {new (...args: any): any}; export type FunctionLike = (...args: any) => any; @@ -112,30 +136,6 @@ type RejectType = ReturnType extends PromiseLike ? unknown : never; -export type MockFunctionMetadataType = - | 'object' - | 'array' - | 'regexp' - | 'function' - | 'constant' - | 'collection' - | 'null' - | 'undefined'; - -export type MockFunctionMetadata< - T extends FunctionLike = UnknownFunction, - MetadataType = MockFunctionMetadataType, -> = { - ref?: number; - members?: Record>; - mockImpl?: FunctionType; - name?: string; - refID?: number; - type?: MetadataType; - value?: ReturnType; - length?: number; -}; - export interface MockInstance { _isMockFunction: true; _protoImpl: Function; From ab690af520cab97b25e03ee1ec90934383ccb64c Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 26 Feb 2022 11:27:57 +0200 Subject: [PATCH 08/23] reduce diff --- packages/jest-mock/src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index ebac1901a513..7e8e4f0ae08a 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -1219,9 +1219,11 @@ export class ModuleMocker { // the typings test helper mocked(item: T, deep?: false): MaybeMocked; + mocked(item: T, deep: true): MaybeMockedDeep; + mocked(item: T, _deep = false): MaybeMocked | MaybeMockedDeep { - return item as MaybeMocked | MaybeMockedDeep; + return item as any; } } From 2b1f2ab0e814e192de2efae022ef0f6d8384129a Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 26 Feb 2022 13:18:20 +0200 Subject: [PATCH 09/23] tweak docs --- docs/JestObjectAPI.md | 8 +++- docs/MockFunctionAPI.md | 79 ++++++++++++++++++++---------------- packages/jest-mock/README.md | 22 ++++++---- 3 files changed, 67 insertions(+), 42 deletions(-) diff --git a/docs/JestObjectAPI.md b/docs/JestObjectAPI.md index 16ca6cdcd6f5..0449d43a24bd 100644 --- a/docs/JestObjectAPI.md +++ b/docs/JestObjectAPI.md @@ -453,7 +453,7 @@ const otherCopyOfMyModule = require('myModule'); ## Mock Functions -### `jest.fn(implementation)` +### `jest.fn(implementation?)` Returns a new, unused [mock function](MockFunctionAPI.md). Optionally takes a mock implementation. @@ -467,6 +467,12 @@ const returnsTrue = jest.fn(() => true); console.log(returnsTrue()); // true; ``` +:::note + +See [Mock Functions](MockFunctionAPI.md#jestfnimplementation) page for details on TypeScript usage. + +::: + ### `jest.isMockFunction(fn)` Determines if the given function is a mocked function. diff --git a/docs/MockFunctionAPI.md b/docs/MockFunctionAPI.md index 96adf39f8a4c..be8b7d8f1ab9 100644 --- a/docs/MockFunctionAPI.md +++ b/docs/MockFunctionAPI.md @@ -5,6 +5,16 @@ title: Mock Functions Mock functions are also known as "spies", because they let you spy on the behavior of a function that is called indirectly by some other code, rather than only testing the output. You can create a mock function with `jest.fn()`. If no implementation is given, the mock function will return `undefined` when invoked. +:::info + +The TypeScript examples from this page will only work as document if you import `jest` from `'@jest/globals'`: + +```ts +import {jest} from '@jest/globals'; +``` + +::: + ## Methods import TOCInline from "@theme/TOCInline" @@ -258,8 +268,10 @@ expect(mockFn).toHaveBeenCalled(); Will result in this error: -```bash +``` expect(mockedFunction).toHaveBeenCalled() + +Expected mock function "mockedFunction" to have been called, but it was not called. ``` ### `mockFn.mockReturnThis()` @@ -516,7 +528,7 @@ test('async test', async () => { -## TypeScript +## TypeScript Usage If you are using [Create React App](https://create-react-app.dev) then the [TypeScript template](https://create-react-app.dev/docs/adding-typescript/) has everything you need to start writing tests in TypeScript. @@ -524,11 +536,41 @@ Otherwise, please see our [Getting Started](GettingStarted.md#using-typescript) You can see an example of using Jest with TypeScript in our [GitHub repository](https://github.com/facebook/jest/tree/main/examples/typescript). +### `jest.fn(implementation?)` + +Correct mock typings will be inferred, if implementation is passed to [`jest.fn()`](JestObjectAPI.md#jestfnimplementation). There are many use cases there the implementation is omitted. To ensure type safety you may pass a generic type argument (also see the examples above for more reference): + +```ts +import {expect, jest, test} from '@jest/globals'; +import type add from './add'; +import calculate from './calc'; + +test('calculate calls add', () => { + // Create a new mock that can be used in place of `add`. + const mockAdd = jest.fn(); + + // `.mockImplementation()` now can infer that `a` and `b` are `number` + // and that the returned value is a `number`. + mockAdd.mockImplementation((a, b) => { + // Yes, this mock is still adding two numbers but imagine this + // was a complex function we are mocking. + return a + b; + }); + + // `mockAdd` is properly typed and therefore accepted by anything + // requiring `add`. + calculate(mockAdd, 1, 2); + + expect(mockAdd).toBeCalledTimes(1); + expect(mockAdd).toBeCalledWith(1, 2); +}); +``` + ### `jest.MockedFunction` > `jest.MockedFunction` is available in the `@types/jest` module from version `24.9.0`. -The following examples will assume you have an understanding of how [Jest mock functions work with JavaScript](MockFunctions.md). +The following examples will assume you have an understanding of how [Jest mock functions work with JavaScript](MockFunctions.md) You can use `jest.MockedFunction` to represent a function that has been replaced by a Jest mock. @@ -552,37 +594,6 @@ test('calculate calls add', () => { }); ``` -Example using [`jest.fn`](JestObjectAPI.md#jestfnimplementation): - -```ts -import type add from './add'; -import calculate from './calc'; - -test('calculate calls add', () => { - // Create a new mock that can be used in place of `add`. - const mockAdd = jest.fn(); - - // Now we can easily set up mock implementations. - // All the `.mock*` API can now give you proper types for `add`. - // https://jestjs.io/docs/mock-function-api - - // `.mockImplementation` can now infer that `a` and `b` are `number` - // and that the returned value is a `number`. - mockAdd.mockImplementation((a, b) => { - // Yes, this mock is still adding two numbers but imagine this - // was a complex function we are mocking. - return a + b; - }); - - // `mockAdd` is properly typed and therefore accepted by anything - // requiring `add`. - calculate(mockAdd, 1, 2); - - expect(mockAdd).toBeCalledTimes(1); - expect(mockAdd).toBeCalledWith(1, 2); -}); -``` - ### `jest.MockedClass` > `jest.MockedClass` is available in the `@types/jest` module from version `24.9.0`. diff --git a/packages/jest-mock/README.md b/packages/jest-mock/README.md index da440ff63994..5bc75093f8fa 100644 --- a/packages/jest-mock/README.md +++ b/packages/jest-mock/README.md @@ -1,5 +1,7 @@ # jest-mock +**Note:** More details on user side API can be found in [Jest documentation](https://jestjs.io/docs/mock-function-api). + ## API ```js @@ -12,7 +14,7 @@ Creates a new module mocker that generates mocks as if they were created in an e ### `generateFromMetadata(metadata)` -Generates a mock based on the given metadata (Metadata for the mock in the schema returned by the getMetadata method of this module). Mocks treat functions specially, and all mock functions have additional members, described in the documentation for `fn` in this module. +Generates a mock based on the given metadata (Metadata for the mock in the schema returned by the `getMetadata()` method of this module). Mocks treat functions specially, and all mock functions have additional members, described in the documentation for `fn()` in this module. One important note: function prototypes are handled specially by this mocking framework. For functions with prototypes, when called as a constructor, the mock will install mocked function members on the instance. This allows different instances of the same constructor to have different values for its mocks member and its return values. @@ -56,9 +58,9 @@ const refID = { }; ``` -defines an object with a slot named `self` that refers back to the object. +Defines an object with a slot named `self` that refers back to the object. -### `fn` +### `fn(implementation?)` Generates a stand-alone function with members that help drive unit tests or confirm expectations. Specifically, functions returned by this method have the following members: @@ -84,9 +86,15 @@ Sets the default mock implementation for the function. ##### `.mockReturnThis()` -Syntactic sugar for .mockImplementation(function() {return this;}) +Syntactic sugar for: + +```js +mockFn.mockImplementation(function () { + return this; +}); +``` -In case both `mockImplementationOnce()/mockImplementation()` and `mockReturnValueOnce()/mockReturnValue()` are called. The priority of which to use is based on what is the last call: +In case both `.mockImplementationOnce()` / `.mockImplementation()` and `.mockReturnValueOnce()` / `.mockReturnValue()` are called. The priority of which to use is based on what is the last call: -- if the last call is mockReturnValueOnce() or mockReturnValue(), use the specific return value or default return value. If specific return values are used up or no default return value is set, fall back to try mockImplementation(); -- if the last call is mockImplementationOnce() or mockImplementation(), run the specific implementation and return the result or run default implementation and return the result. +- if the last call is `.mockReturnValueOnce()` or `.mockReturnValue()`, use the specific return value or default return value. If specific return values are used up or no default return value is set, fall back to try `.mockImplementation()`; +- if the last call is `.mockImplementationOnce()` or `.mockImplementation()`, run the specific implementation and return the result or run default implementation and return the result. From b649fd2dc95bc67a48af5acfe0781536729fac54 Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 26 Feb 2022 13:35:07 +0200 Subject: [PATCH 10/23] add comment --- packages/jest-mock/src/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 7e8e4f0ae08a..e9c3103ae880 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -115,6 +115,14 @@ export type MockedClass = MockInstance< type UnknownFunction = (...args: Array) => unknown; +/** + * All what the internal typings need is to be sure that we have any-function. + * `FunctionLike` type ensures that and helps to constrain the type as well. + * The default of `UnknownFunction` makes sure that `any`s do not leak to the + * user side. For instance, calling `fn()` without implementation will return + * a mock of `(...args: Array) => unknown` type. If implementation + * is provided, its typings are inferred correctly. + */ export interface Mock extends Function, MockInstance { From c24d45d454fea2c868dcc320af8f9da82687bd0d Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 26 Feb 2022 14:23:41 +0200 Subject: [PATCH 11/23] more docs --- docs/MockFunctionAPI.md | 84 ----------------------------------------- 1 file changed, 84 deletions(-) diff --git a/docs/MockFunctionAPI.md b/docs/MockFunctionAPI.md index be8b7d8f1ab9..589abacbb067 100644 --- a/docs/MockFunctionAPI.md +++ b/docs/MockFunctionAPI.md @@ -565,87 +565,3 @@ test('calculate calls add', () => { expect(mockAdd).toBeCalledWith(1, 2); }); ``` - -### `jest.MockedFunction` - -> `jest.MockedFunction` is available in the `@types/jest` module from version `24.9.0`. - -The following examples will assume you have an understanding of how [Jest mock functions work with JavaScript](MockFunctions.md) - -You can use `jest.MockedFunction` to represent a function that has been replaced by a Jest mock. - -Example using [automatic `jest.mock`](JestObjectAPI.md#jestmockmodulename-factory-options): - -```ts -// Assume `add` is imported and used within `calculate`. -import add from './add'; -import calculate from './calc'; - -jest.mock('./add'); - -// Our mock of `add` is now fully typed -const mockAdd = add as jest.MockedFunction; - -test('calculate calls add', () => { - calculate('Add', 1, 2); - - expect(mockAdd).toBeCalledTimes(1); - expect(mockAdd).toBeCalledWith(1, 2); -}); -``` - -### `jest.MockedClass` - -> `jest.MockedClass` is available in the `@types/jest` module from version `24.9.0`. - -The following examples will assume you have an understanding of how [Jest mock classes work with JavaScript](Es6ClassMocks.md). - -You can use `jest.MockedClass` to represent a class that has been replaced by a Jest mock. - -Converting the [ES6 Class automatic mock example](Es6ClassMocks.md#automatic-mock) would look like this: - -```ts -import SoundPlayer from '../sound-player'; -import SoundPlayerConsumer from '../sound-player-consumer'; - -jest.mock('../sound-player'); // SoundPlayer is now a mock constructor - -const SoundPlayerMock = SoundPlayer as jest.MockedClass; - -beforeEach(() => { - // Clear all instances and calls to constructor and all methods: - SoundPlayerMock.mockClear(); -}); - -it('We can check if the consumer called the class constructor', () => { - const soundPlayerConsumer = new SoundPlayerConsumer(); - expect(SoundPlayerMock).toHaveBeenCalledTimes(1); -}); - -it('We can check if the consumer called a method on the class instance', () => { - // Show that mockClear() is working: - expect(SoundPlayerMock).not.toHaveBeenCalled(); - - const soundPlayerConsumer = new SoundPlayerConsumer(); - // Constructor should have been called again: - expect(SoundPlayerMock).toHaveBeenCalledTimes(1); - - const coolSoundFileName = 'song.mp3'; - soundPlayerConsumer.playSomethingCool(); - - // mock.instances is available with automatic mocks: - const mockSoundPlayerInstance = SoundPlayerMock.mock.instances[0]; - - // However, it will not allow access to `.mock` in TypeScript as it - // is returning `SoundPlayer`. Instead, you can check the calls to a - // method like this fully typed: - expect(SoundPlayerMock.prototype.playSoundFile.mock.calls[0][0]).toEqual( - coolSoundFileName, - ); - // Equivalent to above check: - expect(SoundPlayerMock.prototype.playSoundFile).toHaveBeenCalledWith( - coolSoundFileName, - ); - expect(SoundPlayerMock.prototype.playSoundFile).toHaveBeenCalledTimes(1); -}); -``` From b65d40af31ffcfe401e279b13e6c5101a697158c Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 26 Feb 2022 14:43:22 +0200 Subject: [PATCH 12/23] tip --- docs/MockFunctionAPI.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/MockFunctionAPI.md b/docs/MockFunctionAPI.md index 589abacbb067..866bbf3fb872 100644 --- a/docs/MockFunctionAPI.md +++ b/docs/MockFunctionAPI.md @@ -530,11 +530,11 @@ test('async test', async () => { ## TypeScript Usage -If you are using [Create React App](https://create-react-app.dev) then the [TypeScript template](https://create-react-app.dev/docs/adding-typescript/) has everything you need to start writing tests in TypeScript. +:::tip -Otherwise, please see our [Getting Started](GettingStarted.md#using-typescript) guide for to get setup with TypeScript. +Please consult the [Getting Started](GettingStarted.md#using-typescript) guide for details on how to setup Jest with TypeScript. -You can see an example of using Jest with TypeScript in our [GitHub repository](https://github.com/facebook/jest/tree/main/examples/typescript). +::: ### `jest.fn(implementation?)` From d32567819d7931045c01c3a1313461ca4f662195 Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 26 Feb 2022 14:47:45 +0200 Subject: [PATCH 13/23] add changelog entry --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36dab227162f..8d021adcf730 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,8 +20,9 @@ - `[jest-environment-node]` [**BREAKING**] Second argument `context` to constructor is mandatory ([#12469](https://github.com/facebook/jest/pull/12469)) - `[@jest/expect]` New module which extends `expect` with `jest-snapshot` matchers ([#12404](https://github.com/facebook/jest/pull/12404), [#12410](https://github.com/facebook/jest/pull/12410), [#12418](https://github.com/facebook/jest/pull/12418)) - `[@jest/expect-utils]` New module exporting utils for `expect` ([#12323](https://github.com/facebook/jest/pull/12323)) -- `[jest-mock]` [**BREAKING**] Rename exported utility types `ConstructorLike`, `MethodLike`, `ConstructorLikeKeys`, `MethodLikeKeys`, `PropertyLikeKeys`; remove exports of utility types `ArgumentsOf`, `ArgsType`, `ConstructorArgumentsOf` - TS builtin utility types `ConstructorParameters` and `Parameters` should be used instead ([#12435](https://github.com/facebook/jest/pull/12435)) +- `[jest-mock]` [**BREAKING**] Rename exported utility types `ClassLike`, `FunctionLike`, `ConstructorLikeKeys`, `MethodLikeKeys`, `PropertyLikeKeys`; remove exports of utility types `ArgumentsOf`, `ArgsType`, `ConstructorArgumentsOf` - TS builtin utility types `ConstructorParameters` and `Parameters` should be used instead ([#12435](https://github.com/facebook/jest/pull/12435), [#12489](https://github.com/facebook/jest/pull/12489)) - `[jest-mock]` Improve `isMockFunction` to infer types of passed function ([#12442](https://github.com/facebook/jest/pull/12442)) +- `[jest-mock]` [**BREAKING**] Improve the usage of `jest.fn` generic type argument ([#12489](https://github.com/facebook/jest/pull/12489)) - `[jest-resolve]` [**BREAKING**] Add support for `package.json` `exports` ([#11961](https://github.com/facebook/jest/pull/11961), [#12373](https://github.com/facebook/jest/pull/12373)) - `[jest-resolve, jest-runtime]` Add support for `data:` URI import and mock ([#12392](https://github.com/facebook/jest/pull/12392)) - `[jest-resolve, jest-runtime]` Add support for async resolver ([#11540](https://github.com/facebook/jest/pull/11540)) From d2e72f250c5c9de62e5bc866fbb8f7b2604b34ad Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 26 Feb 2022 15:06:41 +0200 Subject: [PATCH 14/23] cleaner getMetadata --- packages/jest-mock/src/index.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index e9c3103ae880..10f405005518 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -944,10 +944,10 @@ export class ModuleMocker { * @see README.md * @param component The component for which to retrieve metadata. */ - getMetadata( + getMetadata( component: ReturnType, _refs?: Map, number>, - ): MockFunctionMetadata | null { + ): MockFunctionMetadata | null { const refs = _refs || new Map, number>(); const ref = refs.get(component); if (ref != null) { @@ -969,11 +969,8 @@ export class ModuleMocker { metadata.value = component; return metadata; } else if (type === 'function') { - // @ts-expect-error this is a function so it has a name metadata.name = component.name; - // @ts-expect-error may be a mock if (component._isMockFunction === true) { - // @ts-expect-error may be a mock metadata.mockImpl = component.getMockImplementation(); } } @@ -986,17 +983,14 @@ export class ModuleMocker { } | null = null; // Leave arrays alone if (type !== 'array') { - // @ts-expect-error component is object this._getSlots(component).forEach(slot => { if ( type === 'function' && - // @ts-expect-error may be a mock component._isMockFunction === true && slot.match(/^mock/) ) { return; } - // @ts-expect-error no index signature const slotMetadata = this.getMetadata(component[slot], refs); if (slotMetadata) { if (!members) { From 10f55969f246eb5e767a7bb4a94b4bb3d8f6e5ea Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 26 Feb 2022 15:06:57 +0200 Subject: [PATCH 15/23] bring back few generics --- packages/jest-mock/src/index.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 10f405005518..52fbf2d1eb5d 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -598,7 +598,7 @@ export class ModuleMocker { private _makeComponent( metadata: MockFunctionMetadata, restore?: () => void, - ): Mock; + ): Mock; private _makeComponent( metadata: MockFunctionMetadata, restore?: () => void, @@ -608,7 +608,7 @@ export class ModuleMocker { | RegExp | ReturnType | undefined - | Mock { + | Mock { if (metadata.type === 'object') { return new this._environmentGlobal.Object(); } else if (metadata.type === 'array') { @@ -821,8 +821,8 @@ export class ModuleMocker { } } - private _createMockFunction( - metadata: MockFunctionMetadata, + private _createMockFunction( + metadata: MockFunctionMetadata, mockConstructor: Function, ): Function { let name = metadata.name; @@ -881,8 +881,8 @@ export class ModuleMocker { return createConstructor(mockConstructor); } - private _generateMock( - metadata: MockFunctionMetadata, + private _generateMock( + metadata: MockFunctionMetadata, callbacks: Array, refs: { [key: string]: @@ -891,9 +891,9 @@ export class ModuleMocker { | RegExp | UnknownFunction | undefined - | Mock; + | Mock; }, - ): Mock { + ): Mock { // metadata not compatible but it's the same type, maybe problem with // overloading of _makeComponent and not _generateMock? // @ts-expect-error @@ -932,7 +932,9 @@ 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: MockFunctionMetadata, + ): Mock { const callbacks: Array = []; const refs = {}; const mock = this._generateMock(_metadata, callbacks, refs); From 7e9c4a22c6e25f0f7a45cf4cb6a15bd1a80e6c8d Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 26 Feb 2022 15:17:32 +0200 Subject: [PATCH 16/23] two more bits --- packages/jest-mock/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 52fbf2d1eb5d..dba9d93cfc05 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -924,7 +924,7 @@ export class ModuleMocker { mock.prototype.constructor = mock; } - return mock as Mock; + return mock as Mock; } /** @@ -981,7 +981,7 @@ export class ModuleMocker { refs.set(component, metadata.refID); let members: { - [key: string]: MockFunctionMetadata; + [key: string]: MockFunctionMetadata; } | null = null; // Leave arrays alone if (type !== 'array') { From 7c61681a9e47dbd938327944e0ab1ab36f78ffbd Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 26 Feb 2022 15:25:21 +0200 Subject: [PATCH 17/23] really last ones --- packages/jest-mock/src/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index dba9d93cfc05..10670f8590da 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -546,7 +546,9 @@ export class ModuleMocker { return config; } - private _ensureMockState(f: Mock): MockFunctionState { + private _ensureMockState( + f: Mock, + ): MockFunctionState { let state = this._mockState.get(f); if (!state) { state = this._defaultMockState(); @@ -1191,7 +1193,7 @@ export class ModuleMocker { Object.defineProperty(obj, propertyName, descriptor!); }); - (descriptor[accessType] as Mock).mockImplementation(function ( + (descriptor[accessType] as Mock<() => T>).mockImplementation(function ( this: unknown, ) { // @ts-expect-error @@ -1200,7 +1202,7 @@ export class ModuleMocker { } Object.defineProperty(obj, propertyName, descriptor); - return descriptor[accessType] as Mock; + return descriptor[accessType] as Mock<() => T>; } clearAllMocks(): void { From feb57221aaa1daf7f6a834c303c341552b41b934 Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 26 Feb 2022 15:31:43 +0200 Subject: [PATCH 18/23] this is it --- packages/jest-mock/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 10670f8590da..9eee3ab64485 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -963,7 +963,7 @@ export class ModuleMocker { return null; } - const metadata: MockFunctionMetadata = {type}; + const metadata: MockFunctionMetadata = {type}; if ( type === 'constant' || type === 'collection' || From 6b0a31d863ac117c8f0f03d211914835a058b8a4 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Sat, 26 Feb 2022 15:35:18 +0200 Subject: [PATCH 19/23] Update packages/jest-mock/src/index.ts Co-authored-by: Simen Bekkhus --- packages/jest-mock/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 9eee3ab64485..e064951f170a 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -1020,7 +1020,7 @@ export class ModuleMocker { ): fn is Mock<(...args: P) => R>; isMockFunction(fn: unknown): fn is Mock; isMockFunction(fn: unknown): fn is Mock { - return !!fn && (fn as any)._isMockFunction === true; + return fn != null && (fn as any)._isMockFunction === true; } fn(implementation?: T): Mock { From 2cfbb5fece6623c2fccd53472e6c457583d349a1 Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 26 Feb 2022 16:20:26 +0200 Subject: [PATCH 20/23] fix getMetadata types --- packages/jest-mock/src/index.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index e064951f170a..b3305f3e47fb 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -948,11 +948,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: unknown, + _refs?: Map, + ): MockFunctionMetadata | null { + const refs = _refs || new Map(); const ref = refs.get(component); if (ref != null) { return {ref}; @@ -963,7 +963,7 @@ export class ModuleMocker { return null; } - const metadata: MockFunctionMetadata = {type}; + const metadata: MockFunctionMetadata = {type}; if ( type === 'constant' || type === 'collection' || @@ -973,8 +973,9 @@ export class ModuleMocker { metadata.value = component; return metadata; } else if (type === 'function') { + // @ts-expect-error component is a function so it has a name metadata.name = component.name; - if (component._isMockFunction === true) { + if (this.isMockFunction(component)) { metadata.mockImpl = component.getMockImplementation(); } } @@ -983,19 +984,21 @@ export class ModuleMocker { refs.set(component, metadata.refID); let members: { - [key: string]: MockFunctionMetadata; + [key: string]: MockFunctionMetadata; } | null = null; // Leave arrays alone if (type !== 'array') { + // @ts-expect-error component is object this._getSlots(component).forEach(slot => { if ( type === 'function' && - component._isMockFunction === true && + this.isMockFunction(component) && slot.match(/^mock/) ) { return; } - const slotMetadata = this.getMetadata(component[slot], refs); + // @ts-expect-error no index signature + const slotMetadata = this.getMetadata(component[slot], refs); if (slotMetadata) { if (!members) { members = {}; From f7312ddd61f6bab54960384d00cd117074205fa2 Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 26 Feb 2022 16:26:08 +0200 Subject: [PATCH 21/23] better --- packages/jest-mock/src/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index b3305f3e47fb..ccef682d8b04 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -948,10 +948,10 @@ export class ModuleMocker { * @see README.md * @param component The component for which to retrieve metadata. */ - getMetadata( + getMetadata( component: unknown, _refs?: Map, - ): MockFunctionMetadata | null { + ): MockFunctionMetadata | null { const refs = _refs || new Map(); const ref = refs.get(component); if (ref != null) { @@ -963,14 +963,14 @@ export class ModuleMocker { return null; } - const metadata: MockFunctionMetadata = {type}; + const metadata: MockFunctionMetadata = {type}; if ( type === 'constant' || type === 'collection' || type === 'undefined' || type === 'null' ) { - metadata.value = component; + metadata.value = component as ReturnType; return metadata; } else if (type === 'function') { // @ts-expect-error component is a function so it has a name @@ -998,7 +998,7 @@ export class ModuleMocker { return; } // @ts-expect-error no index signature - const slotMetadata = this.getMetadata(component[slot], refs); + const slotMetadata = this.getMetadata(component[slot], refs); if (slotMetadata) { if (!members) { members = {}; From f5c620daf5b8064a75cd8ae1a86b9f68724cdb2f Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 26 Feb 2022 16:50:12 +0200 Subject: [PATCH 22/23] remove unnescesary FunctionLike --- packages/jest-mock/src/index.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index ccef682d8b04..1ef0c85b156f 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -18,7 +18,7 @@ export type MockFunctionMetadataType = | 'undefined'; export type MockFunctionMetadata< - T extends FunctionLike = UnknownFunction, + T extends UnknownFunction = UnknownFunction, MetadataType = MockFunctionMetadataType, > = { ref?: number; @@ -546,7 +546,7 @@ export class ModuleMocker { return config; } - private _ensureMockState( + private _ensureMockState( f: Mock, ): MockFunctionState { let state = this._mockState.get(f); @@ -823,7 +823,7 @@ export class ModuleMocker { } } - private _createMockFunction( + private _createMockFunction( metadata: MockFunctionMetadata, mockConstructor: Function, ): Function { @@ -883,7 +883,7 @@ export class ModuleMocker { return createConstructor(mockConstructor); } - private _generateMock( + private _generateMock( metadata: MockFunctionMetadata, callbacks: Array, refs: { @@ -934,7 +934,7 @@ export class ModuleMocker { * @param _metadata Metadata for the mock in the schema returned by the * getMetadata method of this module. */ - generateFromMetadata( + generateFromMetadata( _metadata: MockFunctionMetadata, ): Mock { const callbacks: Array = []; @@ -948,11 +948,11 @@ export class ModuleMocker { * @see README.md * @param component The component for which to retrieve metadata. */ - getMetadata( - component: unknown, - _refs?: Map, + getMetadata( + component: ReturnType, + _refs?: Map, number>, ): MockFunctionMetadata | null { - const refs = _refs || new Map(); + const refs = _refs || new Map, number>(); const ref = refs.get(component); if (ref != null) { return {ref}; @@ -970,7 +970,7 @@ export class ModuleMocker { type === 'undefined' || type === 'null' ) { - metadata.value = component as ReturnType; + metadata.value = component; return metadata; } else if (type === 'function') { // @ts-expect-error component is a function so it has a name @@ -984,7 +984,7 @@ export class ModuleMocker { refs.set(component, metadata.refID); let members: { - [key: string]: MockFunctionMetadata; + [key: string]: MockFunctionMetadata; } | null = null; // Leave arrays alone if (type !== 'array') { From 4171124c1ac4e7f7dbff816d7871262d2d79ea67 Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sat, 26 Feb 2022 20:10:40 +0200 Subject: [PATCH 23/23] last bit of docs --- docs/MockFunctionAPI.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/MockFunctionAPI.md b/docs/MockFunctionAPI.md index 866bbf3fb872..1c3a50555100 100644 --- a/docs/MockFunctionAPI.md +++ b/docs/MockFunctionAPI.md @@ -131,7 +131,11 @@ The [`restoreMocks`](configuration#restoremocks-boolean) configuration option is Accepts a function that should be used as the implementation of the mock. The mock itself will still record all calls that go into and instances that come from itself – the only difference is that the implementation will also be executed when the mock is called. -Both `jest.fn(fn)` and `jest.fn().mockImplementation(fn)` are equivalent. For instance, you can use `.mockImplementation()` to replace the implementation of a mock: +:::tip + +`jest.fn(implementation)` is a shorthand for `jest.fn().mockImplementation(implementation)`. + +::: ```js const mockFn = jest.fn(scalar => 42 + scalar); @@ -145,7 +149,7 @@ mockFn(2); // 38 mockFn(3); // 39 ``` -It also helps to mock class constructors: +`mockImplementation` can also be used to mock class constructors: