diff --git a/CHANGELOG.md b/CHANGELOG.md index f5ea35996abb..27857db8a7c2 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-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)) - `[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)) diff --git a/docs/UpgradingToJest29.md b/docs/UpgradingToJest29.md index a11d9626550c..2dad8da9854f 100644 --- a/docs/UpgradingToJest29.md +++ b/docs/UpgradingToJest29.md @@ -40,6 +40,10 @@ If you want to keep the old behavior, you can set the `snapshotFormat` property Notably, `jsdom@20` includes support for `crypto.getRandomValues()`, which means packages like `jsdom` and `nanoid`, which doesn't work properly in Jest@28, can work without extra polyfills. +## `jest-mock` + +Exports of `Mocked*` utility types changed. `MaybeMockedDeep` and `MaybeMocked` now are exported as `Mocked` and `MockedShallow` respectively; only deep mocked variants of `MockedClass`, `MockedFunction` and `MockedObject` are exposed. + ## `pretty-format` `ConvertAnsi` plugin is removed in favour of [`jest-serializer-ansi-escapes`](https://github.com/mrazauskas/jest-serializer-ansi-escapes). 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(); diff --git a/packages/jest-mock/__typetests__/Mocked.test.ts b/packages/jest-mock/__typetests__/Mocked.test.ts new file mode 100644 index 000000000000..a3ef06684c19 --- /dev/null +++ b/packages/jest-mock/__typetests__/Mocked.test.ts @@ -0,0 +1,218 @@ +/** + * 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, expectError, expectType} from 'tsd-lite'; +import type {MockInstance, Mocked} from 'jest-mock'; + +/// mocks class + +class SomeClass { + constructor(one: string, two?: boolean) {} + + methodA() { + return true; + } + methodB(a: string, b?: number) { + return; + } +} + +const MockSomeClass = SomeClass as Mocked; + +expectType<[one: string, two?: boolean]>(MockSomeClass.mock.calls[0]); + +expectType<[]>(MockSomeClass.prototype.methodA.mock.calls[0]); +expectType<[a: string, b?: number]>( + MockSomeClass.prototype.methodB.mock.calls[0], +); + +expectError(MockSomeClass.prototype.methodA.mockReturnValue('true')); +expectError( + MockSomeClass.prototype.methodB.mockImplementation( + (a: string, b?: string) => { + return; + }, + ), +); + +expectType<[]>(MockSomeClass.mock.instances[0].methodA.mock.calls[0]); +expectType<[a: string, b?: number]>( + MockSomeClass.prototype.methodB.mock.calls[0], +); + +const mockSomeInstance = new MockSomeClass('a') as Mocked< + InstanceType +>; + +expectType<[]>(mockSomeInstance.methodA.mock.calls[0]); +expectType<[a: string, b?: number]>(mockSomeInstance.methodB.mock.calls[0]); + +expectError(mockSomeInstance.methodA.mockReturnValue('true')); +expectError( + mockSomeInstance.methodB.mockImplementation((a: string, b?: string) => { + return; + }), +); + +expectAssignable(mockSomeInstance); + +// mocks function + +function someFunction(a: string, b?: number): boolean { + return true; +} + +const mockFunction = someFunction as Mocked; + +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 { + return 'true'; +} + +const mockAsyncFunction = someAsyncFunction as Mocked; + +expectType<[Array]>(mockAsyncFunction.mock.calls[0]); + +expectError(mockAsyncFunction.mockResolvedValue(123)); +expectError( + mockAsyncFunction.mockImplementation((a: Array) => + Promise.resolve(true), + ), +); + +expectAssignable(mockAsyncFunction); + +// mocks function object + +interface SomeFunctionObject { + (a: number, b?: string): void; + one: { + (oneA: number, oneB?: boolean): boolean; + more: { + time: { + (time: number): void; + }; + }; + }; +} + +declare const someFunctionObject: SomeFunctionObject; + +const mockFunctionObject = someFunctionObject as Mocked< + typeof someFunctionObject +>; + +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; + }), +); + +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( + 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 af36d752bf70..78f3fcc49389 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -47,68 +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 MaybeMockedConstructor = T extends new ( - ...args: Array -) => infer R - ? MockInstance<(...args: ConstructorParameters) => R> - : T; - -export interface MockWithArgs extends MockInstance { - new (...args: ConstructorParameters): T; - (...args: Parameters): ReturnType; -} +export type MockedClass = MockInstance< + (...args: ConstructorParameters) => Mocked> +> & + MockedObject; -export type MockedFunction = MockWithArgs & { - [K in keyof T]: T[K]; -}; +export type MockedFunction = MockInstance & + MockedObject; -export type MockedFunctionDeep = MockWithArgs & - MockedObjectDeep; +type MockedFunctionShallow = MockInstance & T; -export type MockedObject = MaybeMockedConstructor & { - [K in MethodLikeKeys]: T[K] extends FunctionLike +export 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]}; +} & T; -export type MockedObjectDeep = MaybeMockedConstructor & { - [K in MethodLikeKeys]: T[K] extends FunctionLike - ? MockedFunctionDeep +type MockedObjectShallow = { + [K in keyof T]: T[K] extends ClassLike + ? MockedClass + : T[K] extends FunctionLike + ? MockedFunctionShallow : 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 +type MockedShallow = T extends ClassLike + ? MockedClass + : T extends FunctionLike + ? MockedFunctionShallow : T extends object - ? MockedObjectDeep + ? MockedObjectShallow : 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; /** @@ -1234,13 +1216,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-types/__typetests__/jest.test.ts b/packages/jest-types/__typetests__/jest.test.ts index ce95ca1a828a..58f6a1020ffa 100644 --- a/packages/jest-types/__typetests__/jest.test.ts +++ b/packages/jest-types/__typetests__/jest.test.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {expectError, expectType} from 'tsd-lite'; +import {expectAssignable, expectError, expectType} from 'tsd-lite'; import {jest} from '@jest/globals'; import type {Mock, ModuleMocker, SpyInstance} from 'jest-mock'; @@ -117,7 +117,6 @@ expectError(jest.unmock()); // Mock Functions -expectType(jest.retryTimes(3, {logErrorsBeforeRetry: true})); expectType(jest.clearAllMocks()); expectError(jest.clearAllMocks('moduleName')); @@ -211,6 +210,144 @@ expectType(jest.fn); expectType(jest.spyOn); +// deep mocked() + +class SomeClass { + constructor(one: string, two?: boolean) {} + + methodA() { + return true; + } + methodB(a: string, b?: number) { + return; + } +} + +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 mockObjectA = jest.mocked(someObject, true); + +expectError(jest.mocked('abc', true)); + +expectType<[]>(mockObjectA.methodA.mock.calls[0]); +expectType<[b: string]>(mockObjectA.methodB.mock.calls[0]); +expectType<[c: number]>(mockObjectA.methodC.mock.calls[0]); + +expectType<[t: number]>(mockObjectA.one.more.time.mock.calls[0]); + +expectType<[one: string, two?: boolean]>(mockObjectA.SomeClass.mock.calls[0]); +expectType<[]>(mockObjectA.SomeClass.prototype.methodA.mock.calls[0]); +expectType<[a: string, b?: number]>( + mockObjectA.SomeClass.prototype.methodB.mock.calls[0], +); + +expectType<[]>(mockObjectA.someClassInstance.methodA.mock.calls[0]); +expectType<[a: string, b?: number]>( + mockObjectA.someClassInstance.methodB.mock.calls[0], +); + +expectError(mockObjectA.methodA.mockReturnValue(123)); +expectError(mockObjectA.methodA.mockImplementation((a: number) => 123)); +expectError(mockObjectA.methodB.mockReturnValue(123)); +expectError(mockObjectA.methodB.mockImplementation((b: number) => 123)); +expectError(mockObjectA.methodC.mockReturnValue(123)); +expectError(mockObjectA.methodC.mockImplementation((c: number) => 123)); + +expectError(mockObjectA.one.more.time.mockReturnValue(123)); +expectError(mockObjectA.one.more.time.mockImplementation((t: boolean) => 123)); + +expectError(mockObjectA.SomeClass.prototype.methodA.mockReturnValue(123)); +expectError( + mockObjectA.SomeClass.prototype.methodA.mockImplementation( + (a: number) => 123, + ), +); +expectError(mockObjectA.SomeClass.prototype.methodB.mockReturnValue(123)); +expectError( + mockObjectA.SomeClass.prototype.methodB.mockImplementation( + (a: number) => 123, + ), +); + +expectError(mockObjectA.someClassInstance.methodA.mockReturnValue(123)); +expectError( + mockObjectA.someClassInstance.methodA.mockImplementation((a: number) => 123), +); +expectError(mockObjectA.someClassInstance.methodB.mockReturnValue(123)); +expectError( + mockObjectA.someClassInstance.methodB.mockImplementation((a: number) => 123), +); + +expectAssignable(mockObjectA); + +// mocked() + +const mockObjectB = jest.mocked(someObject); + +expectError(jest.mocked('abc')); + +expectType<[]>(mockObjectB.methodA.mock.calls[0]); +expectType<[b: string]>(mockObjectB.methodB.mock.calls[0]); +expectType<[c: number]>(mockObjectB.methodC.mock.calls[0]); + +expectError<[t: number]>(mockObjectB.one.more.time.mock.calls[0]); + +expectType<[one: string, two?: boolean]>(mockObjectB.SomeClass.mock.calls[0]); +expectType<[]>(mockObjectB.SomeClass.prototype.methodA.mock.calls[0]); +expectType<[a: string, b?: number]>( + mockObjectB.SomeClass.prototype.methodB.mock.calls[0], +); + +expectError<[]>(mockObjectB.someClassInstance.methodA.mock.calls[0]); +expectError<[a: string, b?: number]>( + mockObjectB.someClassInstance.methodB.mock.calls[0], +); + +expectError(mockObjectB.methodA.mockReturnValue(123)); +expectError(mockObjectB.methodA.mockImplementation((a: number) => 123)); +expectError(mockObjectB.methodB.mockReturnValue(123)); +expectError(mockObjectB.methodB.mockImplementation((b: number) => 123)); +expectError(mockObjectB.methodC.mockReturnValue(123)); +expectError(mockObjectB.methodC.mockImplementation((c: number) => 123)); + +expectError(mockObjectB.SomeClass.prototype.methodA.mockReturnValue(123)); +expectError( + mockObjectB.SomeClass.prototype.methodA.mockImplementation( + (a: number) => 123, + ), +); +expectError(mockObjectB.SomeClass.prototype.methodB.mockReturnValue(123)); +expectError( + mockObjectB.SomeClass.prototype.methodB.mockImplementation( + (a: number) => 123, + ), +); + +expectAssignable(mockObjectB); + // Mock Timers expectType(jest.advanceTimersByTime(6000)); @@ -301,8 +438,11 @@ expectError(jest.useRealTimers(true)); // Misc -expectType(jest.setTimeout(6000)); -expectError(jest.setTimeout()); - 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());