diff --git a/CHANGELOG.md b/CHANGELOG.md index 10f2f668712b..07859598202e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - `[jest-core]` Add support for `testResultsProcessor` written in ESM ([#12006](https://github.com/facebook/jest/pull/12006)) - `[jest-diff, pretty-format]` Add `compareKeys` option for custom sorting of object keys ([#11992](https://github.com/facebook/jest/pull/11992)) +- `[jest-mock]` Add `ts-jest` mock util functions ([#12089](https://github.com/facebook/jest/pull/12089)) - `[expect]` Enhancing the `toHaveProperty` matcher to support array selection ([#12092](https://github.com/facebook/jest/pull/12092)) ### Fixes diff --git a/docs/JestObjectAPI.md b/docs/JestObjectAPI.md index ddfd7fafea74..c4cd184b5661 100644 --- a/docs/JestObjectAPI.md +++ b/docs/JestObjectAPI.md @@ -578,6 +578,50 @@ Returns the `jest` object for chaining. Restores all mocks back to their original value. Equivalent to calling [`.mockRestore()`](MockFunctionAPI.md#mockfnmockrestore) on every mocked function. Beware that `jest.restoreAllMocks()` only works when the mock was created with `jest.spyOn`; other mocks will require you to manually restore them. +### `jest.mocked(item: T, deep = false)` + +The `mocked` test helper provides typings on your mocked modules and even their deep methods, based on the typing of its source. It makes use of the latest TypeScript feature, so you even have argument types completion in the IDE (as opposed to `jest.MockInstance`). + +_Note: while it needs to be a function so that input type is changed, the helper itself does nothing else than returning the given input value._ + +Example: + +```ts +// foo.ts +export const foo = { + a: { + b: { + c: { + hello: (name: string) => `Hello, ${name}`, + }, + }, + }, + name: () => 'foo', +}; +``` + +```ts +// foo.spec.ts +import {foo} from './foo'; +jest.mock('./foo'); + +// here the whole foo var is mocked deeply +const mockedFoo = jest.mocked(foo, true); + +test('deep', () => { + // there will be no TS error here, and you'll have completion in modern IDEs + mockedFoo.a.b.c.hello('me'); + // same here + expect(mockedFoo.a.b.c.hello.mock.calls).toHaveLength(1); +}); + +test('direct', () => { + foo.name(); + // here only foo.name is mocked (or its methods if it's an object) + expect(mocked(foo.name).mock.calls).toHaveLength(1); +}); +``` + ## Mock Timers ### `jest.useFakeTimers(implementation?: 'modern' | 'legacy')` diff --git a/packages/jest-mock/src/__tests__/index.test.ts b/packages/jest-mock/src/__tests__/index.test.ts index 854d79a1c7b7..bcee51078f13 100644 --- a/packages/jest-mock/src/__tests__/index.test.ts +++ b/packages/jest-mock/src/__tests__/index.test.ts @@ -9,7 +9,7 @@ /* eslint-disable local/ban-types-eventually, local/prefer-rest-params-eventually */ import vm, {Context} from 'vm'; -import {ModuleMocker, fn, spyOn} from '../'; +import {ModuleMocker, fn, mocked, spyOn} from '../'; describe('moduleMocker', () => { let moduleMocker: ModuleMocker; @@ -1452,6 +1452,13 @@ describe('moduleMocker', () => { }); }); +describe('mocked', () => { + it('should return unmodified input', () => { + const subject = {}; + expect(mocked(subject)).toBe(subject); + }); +}); + test('`fn` and `spyOn` do not throw', () => { expect(() => { fn(); diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 0d0cdce00a8c..8e25c3461f6b 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -32,6 +32,77 @@ export type MockFunctionMetadata< length?: number; }; +export type MockableFunction = (...args: Array) => any; +export type MethodKeysOf = { + [K in keyof T]: T[K] extends MockableFunction ? K : never; +}[keyof T]; +export type PropertyKeysOf = { + [K in keyof T]: T[K] extends MockableFunction ? never : K; +}[keyof T]; + +export type ArgumentsOf = T extends (...args: infer A) => any ? A : never; + +export type ConstructorArgumentsOf = T extends new (...args: infer A) => any + ? A + : never; +export type MaybeMockedConstructor = T extends new ( + ...args: Array +) => infer R + ? MockInstance> + : T; +export type MockedFunction = MockWithArgs & { + [K in keyof T]: T[K]; +}; +export type MockedFunctionDeep = MockWithArgs & + MockedObjectDeep; +export type MockedObject = MaybeMockedConstructor & { + [K in MethodKeysOf]: T[K] extends MockableFunction + ? MockedFunction + : T[K]; +} & {[K in PropertyKeysOf]: T[K]}; +export type MockedObjectDeep = MaybeMockedConstructor & { + [K in MethodKeysOf]: T[K] extends MockableFunction + ? MockedFunctionDeep + : T[K]; +} & {[K in PropertyKeysOf]: MaybeMockedDeep}; + +export type MaybeMockedDeep = T extends MockableFunction + ? MockedFunctionDeep + : T extends object + ? MockedObjectDeep + : T; + +export type MaybeMocked = T extends MockableFunction + ? MockedFunction + : T extends object + ? MockedObject + : T; + +export type ArgsType = T extends (...args: infer A) => any ? A : never; +export type Mocked = { + [P in keyof T]: T[P] extends (...args: Array) => any + ? MockInstance, ArgsType> + : T[P] extends Constructable + ? MockedClass + : T[P]; +} & T; +export type MockedClass = MockInstance< + InstanceType, + T extends new (...args: infer P) => any ? P : never +> & { + prototype: T extends {prototype: any} ? Mocked : never; +} & T; + +export interface Constructable { + new (...args: Array): any; +} + +export interface MockWithArgs + extends MockInstance, ArgumentsOf> { + new (...args: ConstructorArgumentsOf): T; + (...args: ArgumentsOf): ReturnType; +} + export interface Mock = Array> extends Function, MockInstance { @@ -1109,9 +1180,19 @@ export class ModuleMocker { private _typeOf(value: any): string { 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; + } } const JestMock = new ModuleMocker(global as unknown as typeof globalThis); export const fn = JestMock.fn.bind(JestMock); export const spyOn = JestMock.spyOn.bind(JestMock); +export const mocked = JestMock.mocked.bind(JestMock);