From b0dc2e4204ab973f62f3da985bf68a5556dfb485 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Sat, 26 Feb 2022 20:39:26 +0200 Subject: [PATCH] refactor(jest-mock)!: simplify usage of `jest.fn` generic type arguments (#12489) --- CHANGELOG.md | 3 +- docs/JestObjectAPI.md | 8 +- docs/MockFunctionAPI.md | 393 +++++++++++------- packages/jest-mock/README.md | 22 +- .../__typetests__/mock-functions.test.ts | 111 +++-- .../__typetests__/utility-types.test.ts | 32 +- packages/jest-mock/src/index.ts | 317 +++++++------- packages/jest-runtime/src/index.ts | 5 +- .../jest-types/__typetests__/jest.test.ts | 14 +- 9 files changed, 528 insertions(+), 377 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a8c3863378b..dca7f2bb486d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,8 +23,9 @@ - `[@jest/expect-utils]` New module exporting utils for `expect` ([#12323](https://github.com/facebook/jest/pull/12323)) - `[jest-haste-map]` [**BREAKING**] `HasteMap.create` now returns a promise ([#12008](https://github.com/facebook/jest/pull/12008)) - `[jest-haste-map]` Add support for `dependencyExtractor` written in ESM ([#12008](https://github.com/facebook/jest/pull/12008)) -- `[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-mock]` Add support for auto-mocking async generator functions ([#11080](https://github.com/facebook/jest/pull/11080)) - `[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)) 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 388a3f0fa836..1c3a50555100 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" @@ -15,6 +25,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 +105,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 +123,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,82 +131,141 @@ 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)`._ +:::tip -For example: +`jest.fn(implementation)` is a shorthand for `jest.fn().mockImplementation(implementation)`. + +::: ```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 ``` `mockImplementation` 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 { + method: mockMethod, + }; +}); + +const some = new SomeClass(); +some.method('a', 'b'); + +console.log('Calls to method: ', mockMethod.mock.calls); +``` + + + + + +```ts title="SomeClass.ts" +export class SomeClass { + method(a: string, b: string): void {} +} +``` + +```ts 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 { - 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); ``` + + + ### `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 +``` + + + + -myMockFn((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)); + +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(); ``` @@ -221,29 +292,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 +372,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 +383,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 +408,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 +419,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 +458,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: @@ -313,78 +496,64 @@ 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: + + + ```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 - -Jest itself is written in [TypeScript](https://www.typescriptlang.org). + -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. + -Otherwise, please see our [Getting Started](GettingStarted.md#using-typescript) guide for to get setup 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.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). +```ts +test('async test', async () => { + const asyncMock = jest + .fn<() => Promise>() + .mockResolvedValueOnce('first call') + .mockRejectedValueOnce(new Error('Async error message')); -You can use `jest.MockedFunction` to represent a function that has been replaced by a Jest mock. + await asyncMock(); // 'first call' + await asyncMock(); // throws 'Async error message' +}); +``` -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'; +## TypeScript Usage -jest.mock('./add'); +:::tip -// Our mock of `add` is now fully typed -const mockAdd = add as jest.MockedFunction; +Please consult the [Getting Started](GettingStarted.md#using-typescript) guide for details on how to setup Jest with TypeScript. -test('calculate calls add', () => { - calculate('Add', 1, 2); +::: - expect(mockAdd).toBeCalledTimes(1); - expect(mockAdd).toBeCalledWith(1, 2); -}); -``` +### `jest.fn(implementation?)` -Example using [`jest.fn`](JestObjectAPI.md#jestfnimplementation): +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 -// Here `add` is imported for its type -import add from './add'; +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() 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`. - // https://jestjs.io/docs/mock-function-api - - // `.mockImplementation` can now infer that `a` and `b` are `number` + // `.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 @@ -392,67 +561,11 @@ 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); 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); -}); -``` 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. diff --git a/packages/jest-mock/__typetests__/mock-functions.test.ts b/packages/jest-mock/__typetests__/mock-functions.test.ts index 6a8905a901d2..e1ace1d254f4 100644 --- a/packages/jest-mock/__typetests__/mock-functions.test.ts +++ b/packages/jest-mock/__typetests__/mock-functions.test.ts @@ -15,30 +15,51 @@ import {Mock, SpyInstance, fn, spyOn} from 'jest-mock'; // jest.fn() -expectType, []>>( +expectType Promise>>( fn(async () => 'value') .mockClear() .mockReset() - .mockImplementation(fn()) - .mockImplementationOnce(fn()) + .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>(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 +84,7 @@ const MockObject = fn((credentials: string) => ({ })); expectType<{ - connect(): Mock>; + connect(): Mock<(...args: Array) => unknown>; disconnect(): void; }>(new MockObject('credentials')); expectError(new MockObject()); @@ -92,7 +113,7 @@ expectType>(mockFn.mock.invocationCallOrder); expectType< Array<{ - connect(): Mock>; + connect(): Mock<(...args: Array) => unknown>; disconnect(): void; }> >(MockObject.mock.instances); @@ -114,12 +135,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 +148,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 +159,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 +167,7 @@ expectType, [p: boolean]>>( ); expectError(mockAsyncFn.mockImplementation(a => 'mock value')); -expectType>( +expectType boolean>>( mockFn.mockImplementationOnce((a, b) => { expectType(a); expectType(b); @@ -157,7 +178,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 +186,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 +282,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 +313,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/__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 32e7cdb8a164..e9b295f84b82 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -18,36 +18,34 @@ export type MockFunctionMetadataType = | 'undefined'; export type MockFunctionMetadata< - T, - Y extends Array, + T extends UnknownFunction = UnknownFunction, MetadataType = MockFunctionMetadataType, > = { ref?: number; - members?: Record>; - mockImpl?: (...args: Y) => T; + members?: Record>; + mockImpl?: FunctionType; name?: string; refID?: number; type?: MetadataType; - value?: T; + 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]; @@ -61,99 +59,114 @@ 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> { +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, Parameters> - : 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< - InstanceType, - T extends new (...args: infer P) => any ? P : never +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 = Array> +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 { - new (...args: Y): T; - (...args: Y): T; + 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; -export interface MockInstance> { +type RejectType = ReturnType extends PromiseLike + ? unknown + : never; + +export interface MockInstance { _isMockFunction: true; _protoImpl: Function; - getMockImplementation(): ((...args: Y) => T) | undefined; + getMockImplementation(): FunctionType | undefined; getMockName(): string; - mock: MockFunctionState; + mock: MockFunctionState; mockClear(): this; mockReset(): this; mockRestore(): void; - mockImplementation(fn: (...args: Y) => T): this; - /** @internal */ - mockImplementation(fn: () => Promise): this; - mockImplementationOnce(fn: (...args: Y) => T): this; - /** @internal */ - mockImplementationOnce(fn: () => Promise): this; + mockImplementation(fn: FunctionType): this; + mockImplementationOnce(fn: FunctionType): this; mockName(name: string): this; mockReturnThis(): this; - mockReturnValue(value: T): this; - mockReturnValueOnce(value: T): this; - mockResolvedValue(value: Unpromisify): this; - mockResolvedValueOnce(value: Unpromisify): this; - mockRejectedValue(value: unknown): this; - mockRejectedValueOnce(value: unknown): this; + mockReturnValue(value: ReturnType): this; + mockReturnValueOnce(value: ReturnType): this; + mockResolvedValue(value: ResolveType): this; + mockResolvedValueOnce(value: ResolveType): this; + mockRejectedValue(value: RejectType): this; + mockRejectedValueOnce(value: RejectType): this; } -export interface SpyInstance> - extends MockInstance {} +export interface SpyInstance + extends MockInstance {} type MockFunctionResultIncomplete = { type: 'incomplete'; @@ -164,12 +177,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'; @@ -179,20 +192,20 @@ type MockFunctionResultThrow = { value: unknown; }; -type MockFunctionResult = +type MockFunctionResult = | MockFunctionResultIncomplete | MockFunctionResultReturn | MockFunctionResultThrow; -type MockFunctionState> = { +type MockFunctionState = { /** * 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,7 +215,7 @@ 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. */ @@ -431,7 +444,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' || @@ -462,7 +475,7 @@ function isReadonlyProp(object: any, prop: string): boolean { export class ModuleMocker { private _environmentGlobal: typeof globalThis; - private _mockState: WeakMap, MockFunctionState>; + private _mockState: WeakMap; private _mockConfigRegistry: WeakMap; private _spyState: Set<() => void>; private _invocationCallCounter: number; @@ -526,9 +539,7 @@ export class ModuleMocker { return Array.from(slots); } - private _ensureMockConfig>( - f: Mock, - ): MockFunctionConfig { + private _ensureMockConfig(f: Mock): MockFunctionConfig { let config = this._mockConfigRegistry.get(f); if (!config) { config = this._defaultMockConfig(); @@ -537,9 +548,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(); @@ -560,10 +571,7 @@ export class ModuleMocker { }; } - private _defaultMockState>(): MockFunctionState< - T, - Y - > { + private _defaultMockState(): MockFunctionState { return { calls: [], instances: [], @@ -572,40 +580,39 @@ export class ModuleMocker { }; } - private _makeComponent>( - metadata: MockFunctionMetadata, + private _makeComponent( + metadata: MockFunctionMetadata, restore?: () => void, ): Record; - private _makeComponent>( - metadata: MockFunctionMetadata, + private _makeComponent( + metadata: MockFunctionMetadata, restore?: () => void, ): Array; - private _makeComponent>( - metadata: MockFunctionMetadata, + private _makeComponent( + metadata: MockFunctionMetadata, restore?: () => void, ): RegExp; - private _makeComponent>( + private _makeComponent( metadata: MockFunctionMetadata< T, - Y, 'constant' | 'collection' | 'null' | 'undefined' >, restore?: () => void, ): T; - private _makeComponent>( - metadata: MockFunctionMetadata, + private _makeComponent( + metadata: MockFunctionMetadata, restore?: () => void, - ): Mock; - private _makeComponent>( - metadata: MockFunctionMetadata, + ): Mock; + private _makeComponent( + 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') { @@ -627,7 +634,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); @@ -636,7 +646,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, }; @@ -713,21 +723,19 @@ 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 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', { @@ -753,29 +761,27 @@ 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: ResolveType) => + f.mockImplementationOnce(() => Promise.resolve(value)); 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: ResolveType) => + f.mockImplementation(() => Promise.resolve(value)); f.mockRejectedValue = (value: unknown) => f.mockImplementation(() => Promise.reject(value)); - f.mockImplementationOnce = ( - fn: ((...args: Y) => T) | (() => 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); @@ -783,9 +789,7 @@ export class ModuleMocker { return f; }; - f.mockImplementation = ( - fn: ((...args: Y) => T) | (() => Promise), - ): Mock => { + f.mockImplementation = (fn: UnknownFunction) => { // next function call will use mock implementation return value const mockConfig = this._ensureMockConfig(f); mockConfig.mockImpl = fn; @@ -793,7 +797,7 @@ export class ModuleMocker { }; f.mockReturnThis = () => - f.mockImplementation(function (this: T) { + f.mockImplementation(function (this: ReturnType) { return this; }); @@ -821,8 +825,8 @@ export class ModuleMocker { } } - private _createMockFunction>( - metadata: MockFunctionMetadata, + private _createMockFunction( + metadata: MockFunctionMetadata, mockConstructor: Function, ): Function { let name = metadata.name; @@ -881,19 +885,19 @@ export class ModuleMocker { return createConstructor(mockConstructor); } - private _generateMock>( - 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 @@ -924,7 +928,7 @@ export class ModuleMocker { mock.prototype.constructor = mock; } - return mock as Mock; + return mock as Mock; } /** @@ -932,9 +936,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); @@ -946,11 +950,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( + component: ReturnType, + _refs?: Map, number>, + ): MockFunctionMetadata | null { + const refs = _refs || new Map, number>(); const ref = refs.get(component); if (ref != null) { return {ref}; @@ -961,7 +965,7 @@ export class ModuleMocker { return null; } - const metadata: MockFunctionMetadata = {type}; + const metadata: MockFunctionMetadata = {type}; if ( type === 'constant' || type === 'collection' || @@ -971,11 +975,9 @@ export class ModuleMocker { metadata.value = component; return metadata; } else if (type === 'function') { - // @ts-expect-error this is a function so it has a name + // @ts-expect-error component 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 + if (this.isMockFunction(component)) { metadata.mockImpl = component.getMockImplementation(); } } @@ -984,21 +986,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' && - // @ts-expect-error may be a mock - component._isMockFunction === true && + this.isMockFunction(component) && slot.match(/^mock/) ) { 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 = {}; @@ -1015,25 +1017,23 @@ 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( + fn: SpyInstance, + ): fn is SpyInstance; + isMockFunction

, R extends unknown>( + fn: (...args: P) => R, + ): fn is Mock<(...args: P) => R>; + isMockFunction(fn: unknown): fn is Mock; + isMockFunction(fn: unknown): fn is Mock { + return fn != null && (fn as any)._isMockFunction === true; } - fn>( - implementation?: (...args: Y) => T, - ): Mock { + fn(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); } @@ -1044,27 +1044,25 @@ 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> + ): T[M] extends ClassLike + ? SpyInstance<(...args: ConstructorParameters) => InstanceType> : never; spyOn>( object: T, methodName: M, - ): T[M] extends MethodLike - ? SpyInstance, Parameters> - : never; + ): T[M] extends FunctionLike ? SpyInstance> : never; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types spyOn>( @@ -1108,7 +1106,7 @@ export class ModuleMocker { proto = Object.getPrototypeOf(proto); } - let mock: Mock>; + let mock: Mock; if (descriptor && descriptor.get) { const originalGet = descriptor.get; @@ -1142,7 +1140,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', @@ -1194,14 +1192,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 @@ -1210,7 +1207,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 4154d3b43d7d..c33a16b2103b 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)) {