From 4b4b03d4756f159f01e0af3986bcd57c0b2cd3c2 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Sun, 24 Apr 2022 20:18:10 +0300 Subject: [PATCH] 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