From 4b4b03d4756f159f01e0af3986bcd57c0b2cd3c2 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Sun, 24 Apr 2022 20:18:10 +0300 Subject: [PATCH 01/17] feat: add jest.Mocked --- packages/jest-globals/package.json | 3 +- packages/jest-globals/src/index.ts | 20 ++++- packages/jest-globals/tsconfig.json | 1 + .../jest-mock/__typetests__/Mocked.test.ts | 85 +++++++++++++++++++ .../jest-mock/__typetests__/tsconfig.json | 2 +- packages/jest-mock/src/index.ts | 16 +--- packages/jest-mock/src/types.ts | 37 ++++++++ .../jest-types/__typetests__/jest.test.ts | 15 +++- yarn.lock | 1 + 9 files changed, 159 insertions(+), 21 deletions(-) create mode 100644 packages/jest-mock/__typetests__/Mocked.test.ts create mode 100644 packages/jest-mock/src/types.ts diff --git a/packages/jest-globals/package.json b/packages/jest-globals/package.json index 79f68f4c293d..f5203b624fbb 100644 --- a/packages/jest-globals/package.json +++ b/packages/jest-globals/package.json @@ -22,7 +22,8 @@ "dependencies": { "@jest/environment": "^28.0.0-alpha.11", "@jest/expect": "^28.0.0-alpha.11", - "@jest/types": "^28.0.0-alpha.9" + "@jest/types": "^28.0.0-alpha.9", + "jest-mock": "^28.0.0-alpha.9" }, "publishConfig": { "access": "public" diff --git a/packages/jest-globals/src/index.ts b/packages/jest-globals/src/index.ts index 90f50d9665ec..e6ffece38583 100644 --- a/packages/jest-globals/src/index.ts +++ b/packages/jest-globals/src/index.ts @@ -9,7 +9,7 @@ import type {Jest} from '@jest/environment'; import type {JestExpect} from '@jest/expect'; import type {Global} from '@jest/types'; -export declare const jest: Jest; +declare const jest: Jest; export declare const expect: JestExpect; @@ -26,6 +26,24 @@ export declare const beforeEach: Global.GlobalAdditions['beforeEach']; export declare const afterEach: Global.GlobalAdditions['afterEach']; export declare const afterAll: Global.GlobalAdditions['afterAll']; +declare namespace jest { + /** + * Wraps class, function, object or module with mock definitions. + * + * @example + * + * jest.mock('../api'); + * import * as api from '../api'; + * + * const mockApi = api as jest.Mocked; + * + * mockApi.someMethod.mockImplementation(() => 'test'); + */ + type Mocked = import('jest-mock').Mocked; +} + +export {jest}; + throw new Error( 'Do not import `@jest/globals` outside of the Jest test environment', ); diff --git a/packages/jest-globals/tsconfig.json b/packages/jest-globals/tsconfig.json index 16a7adbc92db..90f222531f6b 100644 --- a/packages/jest-globals/tsconfig.json +++ b/packages/jest-globals/tsconfig.json @@ -11,6 +11,7 @@ "references": [ {"path": "../jest-environment"}, {"path": "../jest-expect"}, + {"path": "../jest-mock"}, {"path": "../jest-types"} ] } diff --git a/packages/jest-mock/__typetests__/Mocked.test.ts b/packages/jest-mock/__typetests__/Mocked.test.ts new file mode 100644 index 000000000000..8b6e83f23ada --- /dev/null +++ b/packages/jest-mock/__typetests__/Mocked.test.ts @@ -0,0 +1,85 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {expectAssignable, expectType} from 'tsd-lite'; +import type {MockInstance, Mocked} from 'jest-mock'; + +/// mocks class + +class ExampleClass { + constructor(c: string, d?: boolean) {} + + // _propertyB: false; + + methodA() { + return true; + } + methodB(a: string, b: number) { + return; + } + methodC(e: any) { + throw new Error(); + } + + // propertyA: 'abc', +} + +const MockExampleClass = ExampleClass as Mocked; + +const xx = MockExampleClass.mock.calls[0]; +const yy = MockExampleClass.prototype.methodB.mock.calls[0]; + +const ww = MockExampleClass.mock.instances[0].methodB.mock.calls[0]; + +const mockExample = new MockExampleClass('c') as Mocked< + InstanceType +>; + +const zz = mockExample.methodB.mock.calls[0]; + +/// mocks function + +function someFunction(a: number, b?: string): boolean { + return true; +} + +async function someAsyncFunction(a: Array): Promise { + return 'true'; +} + +const mockFunction = someFunction as Mocked; + +expectType(mockFunction.mock.calls[0][0]); +expectType(mockFunction.mock.calls[0][1]); + +const mockFunctionResult = mockFunction.mock.results[0]; + +if (mockFunctionResult.type === 'return') { + expectType(mockFunctionResult.value); +} + +const mockAsyncFunction = someAsyncFunction as Mocked; + +expectType>(mockAsyncFunction.mock.calls[0][0]); +// expectError(mockAsyncFunction.mock.calls[0][1]); + +const mockAsyncFunctionResult = mockAsyncFunction.mock.results[0]; + +if (mockAsyncFunctionResult.type === 'return') { + expectType>(mockAsyncFunctionResult.value); +} + +// mocks object + +const mockConsole = console as Mocked; + +expectAssignable( + mockConsole.log.mockImplementation(() => {}), +); +expectAssignable>( + mockConsole.log.mockImplementation(() => {}), +); diff --git a/packages/jest-mock/__typetests__/tsconfig.json b/packages/jest-mock/__typetests__/tsconfig.json index 165ba1343021..d1974ed987b7 100644 --- a/packages/jest-mock/__typetests__/tsconfig.json +++ b/packages/jest-mock/__typetests__/tsconfig.json @@ -6,7 +6,7 @@ "noUnusedParameters": false, "skipLibCheck": true, - "types": [] + "types": ["node"] }, "include": ["./**/*"] } diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index b4164f26463c..947840c8cec8 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -7,6 +7,8 @@ /* eslint-disable local/ban-types-eventually, local/prefer-rest-params-eventually */ +export type {Mocked} from './types'; + export type MockFunctionMetadataType = | 'object' | 'array' @@ -98,20 +100,6 @@ export type MaybeMockedDeep = T extends FunctionLike ? MockedObjectDeep : T; -export type Mocked = { - [P in keyof T]: T[P] extends FunctionLike - ? MockInstance - : T[P] extends ClassLike - ? MockedClass - : T[P]; -} & T; - -export type MockedClass = MockInstance< - (args: T extends new (...args: infer P) => any ? P : never) => InstanceType -> & { - prototype: T extends {prototype: any} ? Mocked : never; -} & T; - export type UnknownFunction = (...args: Array) => unknown; /** diff --git a/packages/jest-mock/src/types.ts b/packages/jest-mock/src/types.ts new file mode 100644 index 000000000000..769c7e985d6e --- /dev/null +++ b/packages/jest-mock/src/types.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import type {MockInstance} from './index'; + +type ClassLike = {new (...args: any): any}; +type FunctionLike = (...args: any) => any; + +type MockedClass = MockInstance< + (...args: ConstructorParameters) => Mocked> +> & { + prototype: T extends {prototype: any} ? Mocked : never; +} & MockedObject; + +type MockedFunction = MockInstance & MockedObject; + +type MockedObject = { + [K in keyof T]: T[K] extends ClassLike + ? MockedClass + : T[K] extends FunctionLike + ? MockedFunction + : T[K] extends object + ? MockedObject + : T[K]; +} & T; + +export type Mocked = T extends ClassLike + ? MockedClass + : T extends FunctionLike + ? MockedFunction + : T extends object + ? MockedObject + : T; diff --git a/packages/jest-types/__typetests__/jest.test.ts b/packages/jest-types/__typetests__/jest.test.ts index ce95ca1a828a..97977ac0a345 100644 --- a/packages/jest-types/__typetests__/jest.test.ts +++ b/packages/jest-types/__typetests__/jest.test.ts @@ -7,7 +7,7 @@ import {expectError, expectType} from 'tsd-lite'; import {jest} from '@jest/globals'; -import type {Mock, ModuleMocker, SpyInstance} from 'jest-mock'; +import type {Mock, Mocked, ModuleMocker, SpyInstance} from 'jest-mock'; expectType( jest @@ -117,7 +117,6 @@ expectError(jest.unmock()); // Mock Functions -expectType(jest.retryTimes(3, {logErrorsBeforeRetry: true})); expectType(jest.clearAllMocks()); expectError(jest.clearAllMocks('moduleName')); @@ -301,8 +300,16 @@ expectError(jest.useRealTimers(true)); // Misc -expectType(jest.setTimeout(6000)); -expectError(jest.setTimeout()); +type VoidFn = () => void; +declare const voidFn: VoidFn; + +expectType>(voidFn as jest.Mocked); expectType(jest.retryTimes(3)); +expectType(jest.retryTimes(3, {logErrorsBeforeRetry: true})); +expectError(jest.retryTimes(3, {logErrorsBeforeRetry: 'all'})); +expectError(jest.retryTimes({logErrorsBeforeRetry: true})); expectError(jest.retryTimes()); + +expectType(jest.setTimeout(6000)); +expectError(jest.setTimeout()); diff --git a/yarn.lock b/yarn.lock index f6617c129033..64d01de7bf7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2658,6 +2658,7 @@ __metadata: "@jest/environment": ^28.0.0-alpha.11 "@jest/expect": ^28.0.0-alpha.11 "@jest/types": ^28.0.0-alpha.9 + jest-mock: ^28.0.0-alpha.9 languageName: unknown linkType: soft From b1e9b813afc82c3843f53e49b9da3fa0f1a6d4e5 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Sun, 24 Apr 2022 21:11:30 +0300 Subject: [PATCH 02/17] export more types --- packages/jest-globals/src/index.ts | 32 +++++++++++-------- packages/jest-mock/src/index.ts | 2 +- packages/jest-mock/src/types.ts | 7 ++-- .../jest-types/__typetests__/jest.test.ts | 30 ++++++++++++++--- 4 files changed, 49 insertions(+), 22 deletions(-) diff --git a/packages/jest-globals/src/index.ts b/packages/jest-globals/src/index.ts index e6ffece38583..69347ad68673 100644 --- a/packages/jest-globals/src/index.ts +++ b/packages/jest-globals/src/index.ts @@ -8,8 +8,13 @@ import type {Jest} from '@jest/environment'; import type {JestExpect} from '@jest/expect'; import type {Global} from '@jest/types'; - -declare const jest: Jest; +import type { + ClassLike, + FunctionLike, + Mocked as JestMocked, + MockedClass as JestMockedClass, + MockedFunction as JestMockedFunction, +} from 'jest-mock'; export declare const expect: JestExpect; @@ -26,20 +31,21 @@ export declare const beforeEach: Global.GlobalAdditions['beforeEach']; export declare const afterEach: Global.GlobalAdditions['afterEach']; export declare const afterAll: Global.GlobalAdditions['afterAll']; +declare const jest: Jest; + declare namespace jest { /** - * Wraps class, function, object or module with mock definitions. - * - * @example - * - * jest.mock('../api'); - * import * as api from '../api'; - * - * const mockApi = api as jest.Mocked; - * - * mockApi.someMethod.mockImplementation(() => 'test'); + * Wraps any object type with definitions of Jest mock. + */ + type Mocked = JestMocked; + /** + * Wraps a class type with definitions of Jest mock. + */ + type MockedClass = JestMockedClass; + /** + * Wraps a function type with definitions of Jest mock. */ - type Mocked = import('jest-mock').Mocked; + type MockedFunction = JestMockedFunction; } export {jest}; diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 947840c8cec8..80f2e1d07cec 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -7,7 +7,7 @@ /* eslint-disable local/ban-types-eventually, local/prefer-rest-params-eventually */ -export type {Mocked} from './types'; +export type {Mocked, MockedClass} from './types'; export type MockFunctionMetadataType = | 'object' diff --git a/packages/jest-mock/src/types.ts b/packages/jest-mock/src/types.ts index 769c7e985d6e..dbb57d3ebcca 100644 --- a/packages/jest-mock/src/types.ts +++ b/packages/jest-mock/src/types.ts @@ -10,15 +10,16 @@ import type {MockInstance} from './index'; type ClassLike = {new (...args: any): any}; type FunctionLike = (...args: any) => any; -type MockedClass = MockInstance< +export type MockedClass = MockInstance< (...args: ConstructorParameters) => Mocked> > & { prototype: T extends {prototype: any} ? Mocked : never; } & MockedObject; -type MockedFunction = MockInstance & MockedObject; +export type MockedFunction = MockInstance & + MockedObject; -type MockedObject = { +export type MockedObject = { [K in keyof T]: T[K] extends ClassLike ? MockedClass : T[K] extends FunctionLike diff --git a/packages/jest-types/__typetests__/jest.test.ts b/packages/jest-types/__typetests__/jest.test.ts index 97977ac0a345..5b72ba1d4bd5 100644 --- a/packages/jest-types/__typetests__/jest.test.ts +++ b/packages/jest-types/__typetests__/jest.test.ts @@ -7,7 +7,14 @@ import {expectError, expectType} from 'tsd-lite'; import {jest} from '@jest/globals'; -import type {Mock, Mocked, ModuleMocker, SpyInstance} from 'jest-mock'; +import type { + Mock, + Mocked, + MockedClass, + MockedFunction, + ModuleMocker, + SpyInstance, +} from 'jest-mock'; expectType( jest @@ -298,12 +305,25 @@ expectError(jest.useFakeTimers('modern')); expectType(jest.useRealTimers()); expectError(jest.useRealTimers(true)); -// Misc +// Mock + +type SomeFn = () => void; +declare const voidFn: SomeFn; + +expectType>(voidFn as jest.Mocked); + +class SomeClass { + constructor() {} + someMethod() {} +} + +expectType>( + SomeClass as jest.MockedClass, +); -type VoidFn = () => void; -declare const voidFn: VoidFn; +expectType>(voidFn as jest.MockedFunction); -expectType>(voidFn as jest.Mocked); +// Misc expectType(jest.retryTimes(3)); expectType(jest.retryTimes(3, {logErrorsBeforeRetry: true})); From 8d87f79b268443007b8c7b8a23141fa73b3870bc Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Sun, 24 Apr 2022 21:20:06 +0300 Subject: [PATCH 03/17] add constrain --- packages/jest-globals/src/index.ts | 4 ++-- packages/jest-mock/src/types.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/jest-globals/src/index.ts b/packages/jest-globals/src/index.ts index 69347ad68673..55660ecb7617 100644 --- a/packages/jest-globals/src/index.ts +++ b/packages/jest-globals/src/index.ts @@ -35,9 +35,9 @@ declare const jest: Jest; declare namespace jest { /** - * Wraps any object type with definitions of Jest mock. + * Wraps a class, function or object type with definitions of Jest mock. */ - type Mocked = JestMocked; + type Mocked = JestMocked; /** * Wraps a class type with definitions of Jest mock. */ diff --git a/packages/jest-mock/src/types.ts b/packages/jest-mock/src/types.ts index dbb57d3ebcca..ec6d29e60355 100644 --- a/packages/jest-mock/src/types.ts +++ b/packages/jest-mock/src/types.ts @@ -19,7 +19,7 @@ export type MockedClass = MockInstance< export type MockedFunction = MockInstance & MockedObject; -export type MockedObject = { +type MockedObject = { [K in keyof T]: T[K] extends ClassLike ? MockedClass : T[K] extends FunctionLike @@ -29,7 +29,7 @@ export type MockedObject = { : T[K]; } & T; -export type Mocked = T extends ClassLike +export type Mocked = T extends ClassLike ? MockedClass : T extends FunctionLike ? MockedFunction From 3503613a13c1bdf7cf8a9e1d4aadcca176774d00 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Sun, 24 Apr 2022 21:31:31 +0300 Subject: [PATCH 04/17] slimmer --- packages/jest-mock/src/types.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/jest-mock/src/types.ts b/packages/jest-mock/src/types.ts index ec6d29e60355..534b0e86ca80 100644 --- a/packages/jest-mock/src/types.ts +++ b/packages/jest-mock/src/types.ts @@ -5,16 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -import type {MockInstance} from './index'; - -type ClassLike = {new (...args: any): any}; -type FunctionLike = (...args: any) => any; +import type {ClassLike, FunctionLike, MockInstance} from './index'; export type MockedClass = MockInstance< (...args: ConstructorParameters) => Mocked> -> & { - prototype: T extends {prototype: any} ? Mocked : never; -} & MockedObject; +> & + MockedObject; export type MockedFunction = MockInstance & MockedObject; From f1a2585a30f8d3bca83b74ce1ab4608b32e01733 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Sat, 30 Apr 2022 16:41:49 +0300 Subject: [PATCH 05/17] export type --- packages/jest-globals/src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/jest-globals/src/index.ts b/packages/jest-globals/src/index.ts index 55660ecb7617..3383854e054c 100644 --- a/packages/jest-globals/src/index.ts +++ b/packages/jest-globals/src/index.ts @@ -37,15 +37,15 @@ declare namespace jest { /** * Wraps a class, function or object type with definitions of Jest mock. */ - type Mocked = JestMocked; + export type Mocked = JestMocked; /** * Wraps a class type with definitions of Jest mock. */ - type MockedClass = JestMockedClass; + export type MockedClass = JestMockedClass; /** * Wraps a function type with definitions of Jest mock. */ - type MockedFunction = JestMockedFunction; + export type MockedFunction = JestMockedFunction; } export {jest}; From b51b2efde3f4b84adb38b82afd8d2ff97229e02c Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Tue, 3 May 2022 10:31:04 +0300 Subject: [PATCH 06/17] more tests --- .../jest-mock/__typetests__/Mocked.test.ts | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/jest-mock/__typetests__/Mocked.test.ts b/packages/jest-mock/__typetests__/Mocked.test.ts index 8b6e83f23ada..eb77efddd48c 100644 --- a/packages/jest-mock/__typetests__/Mocked.test.ts +++ b/packages/jest-mock/__typetests__/Mocked.test.ts @@ -41,16 +41,12 @@ const mockExample = new MockExampleClass('c') as Mocked< const zz = mockExample.methodB.mock.calls[0]; -/// mocks function +// mocks function function someFunction(a: number, b?: string): boolean { return true; } -async function someAsyncFunction(a: Array): Promise { - return 'true'; -} - const mockFunction = someFunction as Mocked; expectType(mockFunction.mock.calls[0][0]); @@ -62,10 +58,15 @@ if (mockFunctionResult.type === 'return') { expectType(mockFunctionResult.value); } +// mocks async function + +async function someAsyncFunction(a: Array): Promise { + return 'true'; +} + const mockAsyncFunction = someAsyncFunction as Mocked; expectType>(mockAsyncFunction.mock.calls[0][0]); -// expectError(mockAsyncFunction.mock.calls[0][1]); const mockAsyncFunctionResult = mockAsyncFunction.mock.results[0]; @@ -73,6 +74,30 @@ if (mockAsyncFunctionResult.type === 'return') { expectType>(mockAsyncFunctionResult.value); } +// mocks function object + +type SomeFunctionObject = { + (a: number, b: string): void; + one: { + (oneA: number, oneB?: boolean): boolean; + more: { + time: { + (time: number): void; + }; + }; + }; +}; + +declare const someFunctionObject: SomeFunctionObject; + +someFunctionObject.one.more.time(12); + +const mockFunctionObject = someFunctionObject as Mocked< + typeof someFunctionObject +>; + +mockFunctionObject.one.more.time.mock; + // mocks object const mockConsole = console as Mocked; From b98bc917ce42e4373c80c8ad28a0869054b919e0 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Thu, 11 Aug 2022 12:28:48 +0300 Subject: [PATCH 07/17] fix int --- packages/jest-globals/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/jest-globals/src/index.ts b/packages/jest-globals/src/index.ts index 3383854e054c..c6164dc058d7 100644 --- a/packages/jest-globals/src/index.ts +++ b/packages/jest-globals/src/index.ts @@ -33,6 +33,7 @@ export declare const afterAll: Global.GlobalAdditions['afterAll']; declare const jest: Jest; +// eslint-disable-next-line @typescript-eslint/no-namespace declare namespace jest { /** * Wraps a class, function or object type with definitions of Jest mock. From d01681065217ed6e7384ba8d77a13df6996c0187 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Thu, 11 Aug 2022 17:33:06 +0300 Subject: [PATCH 08/17] add mocked class and mocked function test --- .../jest-mock/__typetests__/Mocked.test.ts | 102 +++++++++++------- 1 file changed, 64 insertions(+), 38 deletions(-) diff --git a/packages/jest-mock/__typetests__/Mocked.test.ts b/packages/jest-mock/__typetests__/Mocked.test.ts index eb77efddd48c..0b359f6d8e12 100644 --- a/packages/jest-mock/__typetests__/Mocked.test.ts +++ b/packages/jest-mock/__typetests__/Mocked.test.ts @@ -5,58 +5,73 @@ * LICENSE file in the root directory of this source tree. */ -import {expectAssignable, expectType} from 'tsd-lite'; +import {expectAssignable, expectError, expectType} from 'tsd-lite'; import type {MockInstance, Mocked} from 'jest-mock'; /// mocks class -class ExampleClass { - constructor(c: string, d?: boolean) {} - - // _propertyB: false; +class SomeClass { + constructor(one: string, two?: boolean) {} methodA() { return true; } - methodB(a: string, b: number) { + methodB(a: string, b?: number) { return; } - methodC(e: any) { - throw new Error(); - } - - // propertyA: 'abc', } -const MockExampleClass = ExampleClass as Mocked; +const MockSomeClass = SomeClass as Mocked; + +expectType<[one: string, two?: boolean | undefined]>( + MockSomeClass.mock.calls[0], +); + +expectType<[]>(MockSomeClass.prototype.methodA.mock.calls[0]); +expectType<[a: string, b?: number]>( + MockSomeClass.prototype.methodB.mock.calls[0], +); -const xx = MockExampleClass.mock.calls[0]; -const yy = MockExampleClass.prototype.methodB.mock.calls[0]; +expectError(MockSomeClass.prototype.methodA.mockReturnValue('true')); +expectError( + MockSomeClass.prototype.methodB.mockImplementation( + (a: string, b?: string) => { + return; + }, + ), +); -const ww = MockExampleClass.mock.instances[0].methodB.mock.calls[0]; +expectType<[]>(MockSomeClass.mock.instances[0].methodA.mock.calls[0]); +expectType<[a: string, b?: number]>( + MockSomeClass.prototype.methodB.mock.calls[0], +); -const mockExample = new MockExampleClass('c') as Mocked< - InstanceType +const mockExample = new MockSomeClass('c') as Mocked< + InstanceType >; -const zz = mockExample.methodB.mock.calls[0]; +expectType<[]>(mockExample.methodA.mock.calls[0]); +expectType<[a: string, b?: number]>(mockExample.methodB.mock.calls[0]); + +expectError(mockExample.methodA.mockReturnValue('true')); +expectError( + mockExample.methodB.mockImplementation((a: string, b?: string) => { + return; + }), +); // mocks function -function someFunction(a: number, b?: string): boolean { +function someFunction(a: string, b?: number): boolean { return true; } const mockFunction = someFunction as Mocked; -expectType(mockFunction.mock.calls[0][0]); -expectType(mockFunction.mock.calls[0][1]); +expectType<[a: string, b?: number]>(mockFunction.mock.calls[0]); -const mockFunctionResult = mockFunction.mock.results[0]; - -if (mockFunctionResult.type === 'return') { - expectType(mockFunctionResult.value); -} +expectError(mockFunction.mockReturnValue(123)); +expectError(mockFunction.mockImplementation((a: boolean, b?: number) => true)); // mocks async function @@ -66,18 +81,19 @@ async function someAsyncFunction(a: Array): Promise { const mockAsyncFunction = someAsyncFunction as Mocked; -expectType>(mockAsyncFunction.mock.calls[0][0]); - -const mockAsyncFunctionResult = mockAsyncFunction.mock.results[0]; +expectType<[Array]>(mockAsyncFunction.mock.calls[0]); -if (mockAsyncFunctionResult.type === 'return') { - expectType>(mockAsyncFunctionResult.value); -} +expectError(mockAsyncFunction.mockResolvedValue(123)); +expectError( + mockAsyncFunction.mockImplementation((a: Array) => + Promise.resolve(true), + ), +); // mocks function object -type SomeFunctionObject = { - (a: number, b: string): void; +interface SomeFunctionObject { + (a: number, b?: string): void; one: { (oneA: number, oneB?: boolean): boolean; more: { @@ -86,17 +102,27 @@ type SomeFunctionObject = { }; }; }; -}; +} declare const someFunctionObject: SomeFunctionObject; -someFunctionObject.one.more.time(12); - const mockFunctionObject = someFunctionObject as Mocked< typeof someFunctionObject >; -mockFunctionObject.one.more.time.mock; +expectType<[a: number, b?: string]>(mockFunctionObject.mock.calls[0]); + +expectError(mockFunctionObject.mockReturnValue(123)); +expectError(mockFunctionObject.mockImplementation(() => true)); + +expectType<[time: number]>(mockFunctionObject.one.more.time.mock.calls[0]); + +expectError(mockFunctionObject.one.more.time.mockReturnValue(123)); +expectError( + mockFunctionObject.one.more.time.mockImplementation((time: string) => { + return; + }), +); // mocks object From 0072a54d4a9ef8a17ecd2fee5192fe52fcf61e8f Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Thu, 11 Aug 2022 17:57:39 +0300 Subject: [PATCH 09/17] move Mocked types into index --- packages/jest-mock/src/index.ts | 80 ++++++++++++++++----------------- packages/jest-mock/src/types.ts | 34 -------------- 2 files changed, 38 insertions(+), 76 deletions(-) delete mode 100644 packages/jest-mock/src/types.ts diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 1c9d71b03fa1..f1a31521a6e5 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -7,8 +7,6 @@ /* eslint-disable local/ban-types-eventually, local/prefer-rest-params-eventually */ -export type {Mocked, MockedClass} from './types'; - export type MockFunctionMetadataType = | 'object' | 'array' @@ -49,52 +47,50 @@ export type PropertyLikeKeys = Exclude< ConstructorLikeKeys | MethodLikeKeys >; -// TODO Figure out how to replace this with TS ConstructorParameters utility type -// https://www.typescriptlang.org/docs/handbook/utility-types.html#constructorparameterstype -type ConstructorParameters = T extends new (...args: infer P) => any - ? P - : never; +export type MockedClass = MockInstance< + (...args: ConstructorParameters) => Mocked> +> & + MockedObject; -export type MaybeMockedConstructor = T extends new ( - ...args: Array -) => infer R - ? MockInstance<(...args: ConstructorParameters) => R> - : T; +export type MockedFunction = MockInstance & + MockedObject; -export interface MockWithArgs extends MockInstance { - new (...args: ConstructorParameters): T; - (...args: Parameters): ReturnType; -} +export type MockedFunctionShallow = MockInstance & T; -export type MockedFunction = MockWithArgs & { - [K in keyof T]: T[K]; -}; - -export type MockedFunctionDeep = MockWithArgs & - MockedObjectDeep; - -export type MockedObject = MaybeMockedConstructor & { - [K in MethodLikeKeys]: T[K] extends FunctionLike +type MockedObject = { + [K in keyof T]: T[K] extends ClassLike + ? MockedClass + : T[K] extends FunctionLike ? MockedFunction + : T[K] extends object + ? MockedObject : T[K]; -} & {[K in PropertyLikeKeys]: T[K]}; - -export type MockedObjectDeep = MaybeMockedConstructor & { - [K in MethodLikeKeys]: T[K] extends FunctionLike - ? MockedFunctionDeep +} & T; + +type MockedObjectShallow = { + [K in keyof T]: T[K] extends ClassLike + ? MockedClass + : T[K] extends FunctionLike + ? MockedFunctionShallow + : T[K] extends object + ? MockedObjectShallow : T[K]; -} & {[K in PropertyLikeKeys]: MaybeMockedDeep}; +} & T; -export type MaybeMocked = T extends FunctionLike +export type Mocked = T extends ClassLike + ? MockedClass + : T extends FunctionLike ? MockedFunction : T extends object ? MockedObject : T; -export type MaybeMockedDeep = T extends FunctionLike - ? MockedFunctionDeep +export type MockedShallow = T extends ClassLike + ? MockedClass + : T extends FunctionLike + ? MockedFunctionShallow : T extends object - ? MockedObjectDeep + ? MockedObjectShallow : T; export type UnknownFunction = (...args: Array) => unknown; @@ -1222,13 +1218,13 @@ export class ModuleMocker { return value == null ? `${value}` : typeof value; } - // 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; + mocked(item: T, deep?: false): MockedShallow; + mocked(item: T, deep: true): Mocked; + mocked( + item: T, + _deep = false, + ): Mocked | MockedShallow { + return item as Mocked | MockedShallow; } } diff --git a/packages/jest-mock/src/types.ts b/packages/jest-mock/src/types.ts deleted file mode 100644 index 534b0e86ca80..000000000000 --- a/packages/jest-mock/src/types.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import type {ClassLike, FunctionLike, MockInstance} from './index'; - -export type MockedClass = MockInstance< - (...args: ConstructorParameters) => Mocked> -> & - MockedObject; - -export type MockedFunction = MockInstance & - MockedObject; - -type MockedObject = { - [K in keyof T]: T[K] extends ClassLike - ? MockedClass - : T[K] extends FunctionLike - ? MockedFunction - : T[K] extends object - ? MockedObject - : T[K]; -} & T; - -export type Mocked = T extends ClassLike - ? MockedClass - : T extends FunctionLike - ? MockedFunction - : T extends object - ? MockedObject - : T; From 6f0f2eea46f4abbfc3d2f5236121ed67d30be054 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Thu, 11 Aug 2022 18:01:35 +0300 Subject: [PATCH 10/17] clean up exports --- packages/jest-mock/src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index f1a31521a6e5..1006a0047fa9 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -55,9 +55,9 @@ export type MockedClass = MockInstance< export type MockedFunction = MockInstance & MockedObject; -export type MockedFunctionShallow = MockInstance & T; +type MockedFunctionShallow = MockInstance & T; -type MockedObject = { +export type MockedObject = { [K in keyof T]: T[K] extends ClassLike ? MockedClass : T[K] extends FunctionLike @@ -85,7 +85,7 @@ export type Mocked = T extends ClassLike ? MockedObject : T; -export type MockedShallow = T extends ClassLike +type MockedShallow = T extends ClassLike ? MockedClass : T extends FunctionLike ? MockedFunctionShallow From ff05715fd51b44242202a0ff1719c5734e9a55b7 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Fri, 12 Aug 2022 09:15:23 +0300 Subject: [PATCH 11/17] add assignability tests --- .../jest-mock/__typetests__/Mocked.test.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/jest-mock/__typetests__/Mocked.test.ts b/packages/jest-mock/__typetests__/Mocked.test.ts index 0b359f6d8e12..945bda233e6d 100644 --- a/packages/jest-mock/__typetests__/Mocked.test.ts +++ b/packages/jest-mock/__typetests__/Mocked.test.ts @@ -46,20 +46,22 @@ expectType<[a: string, b?: number]>( MockSomeClass.prototype.methodB.mock.calls[0], ); -const mockExample = new MockSomeClass('c') as Mocked< +const mockSomeInstance = new MockSomeClass('a') as Mocked< InstanceType >; -expectType<[]>(mockExample.methodA.mock.calls[0]); -expectType<[a: string, b?: number]>(mockExample.methodB.mock.calls[0]); +expectType<[]>(mockSomeInstance.methodA.mock.calls[0]); +expectType<[a: string, b?: number]>(mockSomeInstance.methodB.mock.calls[0]); -expectError(mockExample.methodA.mockReturnValue('true')); +expectError(mockSomeInstance.methodA.mockReturnValue('true')); expectError( - mockExample.methodB.mockImplementation((a: string, b?: string) => { + mockSomeInstance.methodB.mockImplementation((a: string, b?: string) => { return; }), ); +expectAssignable(mockSomeInstance); + // mocks function function someFunction(a: string, b?: number): boolean { @@ -73,6 +75,8 @@ expectType<[a: string, b?: number]>(mockFunction.mock.calls[0]); expectError(mockFunction.mockReturnValue(123)); expectError(mockFunction.mockImplementation((a: boolean, b?: number) => true)); +expectAssignable(mockFunction); + // mocks async function async function someAsyncFunction(a: Array): Promise { @@ -90,6 +94,8 @@ expectError( ), ); +expectAssignable(mockAsyncFunction); + // mocks function object interface SomeFunctionObject { @@ -124,6 +130,8 @@ expectError( }), ); +expectAssignable(mockFunctionObject); + // mocks object const mockConsole = console as Mocked; From 715c56422059c95d343aefad5d7b2101f279c0d5 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Fri, 12 Aug 2022 09:29:07 +0300 Subject: [PATCH 12/17] use `jest.mocked()` in TS example --- examples/typescript/__tests__/calc.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/typescript/__tests__/calc.test.ts b/examples/typescript/__tests__/calc.test.ts index cb9bd0210549..7a05eadd8757 100644 --- a/examples/typescript/__tests__/calc.test.ts +++ b/examples/typescript/__tests__/calc.test.ts @@ -1,3 +1,7 @@ +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + +import {jest} from '@jest/globals'; + import Memory from '../memory'; import sub from '../sub'; import sum from '../sum'; @@ -9,7 +13,7 @@ jest.mock('../sum'); const mockSub = jest.mocked(sub); const mockSum = jest.mocked(sum); -const MockMemory = Memory as jest.MockedClass; +const MockMemory = jest.mocked(Memory); describe('calc - mocks', () => { const memory = new MockMemory(); From 273b08c829ab390818d5ae305e60f3e9652979ab Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Fri, 12 Aug 2022 10:09:41 +0300 Subject: [PATCH 13/17] add mocked object test --- .../jest-mock/__typetests__/Mocked.test.ts | 82 ++++++++++++++++++- 1 file changed, 78 insertions(+), 4 deletions(-) diff --git a/packages/jest-mock/__typetests__/Mocked.test.ts b/packages/jest-mock/__typetests__/Mocked.test.ts index 945bda233e6d..a3ef06684c19 100644 --- a/packages/jest-mock/__typetests__/Mocked.test.ts +++ b/packages/jest-mock/__typetests__/Mocked.test.ts @@ -23,9 +23,7 @@ class SomeClass { const MockSomeClass = SomeClass as Mocked; -expectType<[one: string, two?: boolean | undefined]>( - MockSomeClass.mock.calls[0], -); +expectType<[one: string, two?: boolean]>(MockSomeClass.mock.calls[0]); expectType<[]>(MockSomeClass.prototype.methodA.mock.calls[0]); expectType<[a: string, b?: number]>( @@ -130,10 +128,86 @@ expectError( }), ); -expectAssignable(mockFunctionObject); +expectAssignable(mockFunctionObject); // mocks object +const someObject = { + SomeClass, + + methodA() { + return; + }, + methodB(b: string) { + return true; + }, + methodC: (c: number) => true, + + one: { + more: { + time: (t: number) => { + return; + }, + }, + }, + + propertyA: 123, + propertyB: 'value', + + someClassInstance: new SomeClass('value'), +}; + +const mockObject = someObject as Mocked; + +expectType<[]>(mockObject.methodA.mock.calls[0]); +expectType<[b: string]>(mockObject.methodB.mock.calls[0]); +expectType<[c: number]>(mockObject.methodC.mock.calls[0]); + +expectType<[t: number]>(mockObject.one.more.time.mock.calls[0]); + +expectType<[one: string, two?: boolean]>(mockObject.SomeClass.mock.calls[0]); +expectType<[]>(mockObject.SomeClass.prototype.methodA.mock.calls[0]); +expectType<[a: string, b?: number]>( + mockObject.SomeClass.prototype.methodB.mock.calls[0], +); + +expectType<[]>(mockObject.someClassInstance.methodA.mock.calls[0]); +expectType<[a: string, b?: number]>( + mockObject.someClassInstance.methodB.mock.calls[0], +); + +expectError(mockObject.methodA.mockReturnValue(123)); +expectError(mockObject.methodA.mockImplementation((a: number) => 123)); +expectError(mockObject.methodB.mockReturnValue(123)); +expectError(mockObject.methodB.mockImplementation((b: number) => 123)); +expectError(mockObject.methodC.mockReturnValue(123)); +expectError(mockObject.methodC.mockImplementation((c: number) => 123)); + +expectError(mockObject.one.more.time.mockReturnValue(123)); +expectError(mockObject.one.more.time.mockImplementation((t: boolean) => 123)); + +expectError(mockObject.SomeClass.prototype.methodA.mockReturnValue(123)); +expectError( + mockObject.SomeClass.prototype.methodA.mockImplementation((a: number) => 123), +); +expectError(mockObject.SomeClass.prototype.methodB.mockReturnValue(123)); +expectError( + mockObject.SomeClass.prototype.methodB.mockImplementation((a: number) => 123), +); + +expectError(mockObject.someClassInstance.methodA.mockReturnValue(123)); +expectError( + mockObject.someClassInstance.methodA.mockImplementation((a: number) => 123), +); +expectError(mockObject.someClassInstance.methodB.mockReturnValue(123)); +expectError( + mockObject.someClassInstance.methodB.mockImplementation((a: number) => 123), +); + +expectAssignable(mockObject); + +// mocks 'console' object + const mockConsole = console as Mocked; expectAssignable( From f777a3092b0d27f2516da54033354b940d8faca8 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Fri, 12 Aug 2022 17:22:39 +0300 Subject: [PATCH 14/17] add MockedObject --- packages/jest-globals/src/index.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/jest-globals/src/index.ts b/packages/jest-globals/src/index.ts index c6164dc058d7..a06db18df650 100644 --- a/packages/jest-globals/src/index.ts +++ b/packages/jest-globals/src/index.ts @@ -14,6 +14,7 @@ import type { Mocked as JestMocked, MockedClass as JestMockedClass, MockedFunction as JestMockedFunction, + MockedObject as JestMockedObject, } from 'jest-mock'; export declare const expect: JestExpect; @@ -36,17 +37,21 @@ declare const jest: Jest; // eslint-disable-next-line @typescript-eslint/no-namespace declare namespace jest { /** - * Wraps a class, function or object type with definitions of Jest mock. + * Wraps a class, function or object type with Jest mock type definitions. */ export type Mocked = JestMocked; /** - * Wraps a class type with definitions of Jest mock. + * Wraps a class type with Jest mock type definitions. */ export type MockedClass = JestMockedClass; /** - * Wraps a function type with definitions of Jest mock. + * Wraps a function type with Jest mock type definitions. */ export type MockedFunction = JestMockedFunction; + /** + * Wraps an object type with Jest mock type definitions. + */ + export type MockedObject = JestMockedObject; } export {jest}; From 247f12fc21799df56cae780b3f37d2eb4811ac47 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Fri, 12 Aug 2022 17:38:54 +0300 Subject: [PATCH 15/17] tests --- .../jest-types/__typetests__/jest.test.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/jest-types/__typetests__/jest.test.ts b/packages/jest-types/__typetests__/jest.test.ts index 111e5c836bc4..f5f12ba68f05 100644 --- a/packages/jest-types/__typetests__/jest.test.ts +++ b/packages/jest-types/__typetests__/jest.test.ts @@ -12,6 +12,7 @@ import type { Mocked, MockedClass, MockedFunction, + MockedObject, ModuleMocker, SpyInstance, } from 'jest-mock'; @@ -217,7 +218,7 @@ expectType(jest.fn); expectType(jest.spyOn); -// deep mocked() +// Mocked* class SomeClass { constructor(one: string, two?: boolean) {} @@ -230,6 +231,10 @@ class SomeClass { } } +function someFunction(a: string, b?: number): boolean { + return true; +} + const someObject = { SomeClass, @@ -255,6 +260,24 @@ const someObject = { someClassInstance: new SomeClass('value'), }; +expectType>( + someObject as jest.Mocked, +); + +expectType>( + SomeClass as jest.MockedClass, +); + +expectType>( + someFunction as jest.MockedFunction, +); + +expectType>( + someObject as jest.MockedObject, +); + +// deep mocked() + const mockObjectA = jest.mocked(someObject, true); expectError(jest.mocked('abc', true)); From 11fc9fe73f68ddc0a6941362f8600dc0a3dd3a51 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Fri, 12 Aug 2022 18:02:57 +0300 Subject: [PATCH 16/17] docs --- docs/MockFunctionAPI.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/MockFunctionAPI.md b/docs/MockFunctionAPI.md index db8f2d5ea4b3..0b2fea48b464 100644 --- a/docs/MockFunctionAPI.md +++ b/docs/MockFunctionAPI.md @@ -520,3 +520,28 @@ test('calculate calls add', () => { expect(mockAdd).toBeCalledWith(1, 2); }); ``` + +### `jest.Mocked` + +The `jest.Mocked` utility type returns the `Source` type wrapped with type definitions of Jest mock function. + +```ts +import fetch from 'node-fetch'; +import {expect, jest, test} from '@jest/globals'; + +jest.mock('node-fetch'); + +let mockedFetch: jest.Mocked; + +test('makes correct call', () => { + mockedFetch = getMockedFetch(); + // ... +}); + +test('returns correct data', () => { + mockedFetch = getMockedFetch(); + // ... +}); +``` + +Types of classes, functions or objects can be passed as type argument to `jest.Mocked`. If you prefer to constrain the input type, use: `jest.MockedClass`, `jest.MockedFunction` or `jest.MockedObject`. From a884b46b8eccdd0ce05320cf6a6f805c6be7d1c2 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Fri, 12 Aug 2022 18:03:09 +0300 Subject: [PATCH 17/17] changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02d6f94c4885..c59948435fde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - `[jest-config]` [**BREAKING**] Make `snapshotFormat` default to `escapeString: false` and `printBasicPrototype: false` ([#13036](https://github.com/facebook/jest/pull/13036)) - `[jest-environment-jsdom]` [**BREAKING**] Upgrade to `jsdom@20` ([#13037](https://github.com/facebook/jest/pull/13037), [#13058](https://github.com/facebook/jest/pull/13058)) +- `[@jest/globals]` Add `jest.Mocked`, `jest.MockedClass`, `jest.MockedFunction` and `jest.MockedObject` utility types ([#12727](https://github.com/facebook/jest/pull/12727)) - `[jest-mock]` [**BREAKING**] Refactor `Mocked*` utility types. `MaybeMockedDeep` and `MaybeMocked` became `Mocked` and `MockedShallow` respectively; only deep mocked variants of `MockedClass`, `MockedFunction` and `MockedObject` are exported ([#13123](https://github.com/facebook/jest/pull/13123), [#13124](https://github.com/facebook/jest/pull/13124)) - `[jest-worker]` Adds `workerIdleMemoryLimit` option which is used as a check for worker memory leaks >= Node 16.11.0 and recycles child workers as required. ([#13056](https://github.com/facebook/jest/pull/13056), [#13105](https://github.com/facebook/jest/pull/13105), [#13106](https://github.com/facebook/jest/pull/13106), [#13107](https://github.com/facebook/jest/pull/13107)) - `[pretty-format]` [**BREAKING**] Remove `ConvertAnsi` plugin in favour of `jest-serializer-ansi-escapes` ([#13040](https://github.com/facebook/jest/pull/13040))