From b16cfc61f9967efca76f5339247731494b9c302d Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sun, 20 Feb 2022 21:22:37 +0200 Subject: [PATCH 1/9] add type tests for jest object --- packages/jest-environment/src/index.ts | 17 +- packages/jest-mock/src/index.ts | 65 ++-- .../jest-types/__typetests__/jest.test.ts | 305 +++++++++++++++--- 3 files changed, 304 insertions(+), 83 deletions(-) diff --git a/packages/jest-environment/src/index.ts b/packages/jest-environment/src/index.ts index 5f589c7e9b93..01695b50fb5a 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -8,12 +8,7 @@ import type {Context} from 'vm'; import type {LegacyFakeTimers, ModernFakeTimers} from '@jest/fake-timers'; import type {Circus, Config, Global} from '@jest/types'; -import type { - fn as JestMockFn, - mocked as JestMockMocked, - spyOn as JestMockSpyOn, - ModuleMocker, -} from 'jest-mock'; +import type {ModuleMocker} from 'jest-mock'; export type EnvironmentContext = { console: Console; @@ -110,7 +105,7 @@ export interface Jest { /** * Creates a mock function. Optionally takes a mock implementation. */ - fn: typeof JestMockFn; + fn: ModuleMocker['fn']; /** * Given the name of a module, use the automatic mocking system to generate a * mocked version of the module for you. @@ -132,9 +127,7 @@ export interface Jest { /** * Determines if the given function is a mocked function. */ - isMockFunction( - fn: (...args: Array) => unknown, - ): fn is ReturnType; + isMockFunction: ModuleMocker['isMockFunction']; /** * Mocks a module with an auto-mocked version when it is being required. */ @@ -196,7 +189,7 @@ export interface Jest { * jest.spyOn; other mocks will require you to manually restore them. */ restoreAllMocks(): Jest; - mocked: typeof JestMockMocked; + mocked: ModuleMocker['mocked']; /** * Runs failed tests n-times until they pass or until the max number of * retries is exhausted. This only works with `jest-circus`! @@ -259,7 +252,7 @@ export interface Jest { * Note: By default, jest.spyOn also calls the spied method. This is * different behavior from most other test libraries. */ - spyOn: typeof JestMockSpyOn; + spyOn: ModuleMocker['spyOn']; /** * Indicates that the module system should never return a mocked version of * the specified module from require() (e.g. that it should always return the diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index bd3e3e11c809..a7d333722788 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -32,6 +32,14 @@ export type MockFunctionMetadata< length?: number; }; +type ClassLike = { + new (...args: Array): any; +}; + +type ObjectLike = { + [key: string]: any; +}; + export type MockableFunction = (...args: Array) => any; export type MethodKeysOf = { [K in keyof T]: T[K] extends MockableFunction ? K : never; @@ -79,6 +87,9 @@ export type MaybeMocked = T extends MockableFunction : T; export type ArgsType = T extends (...args: infer A) => any ? A : never; +type ConstructorArgsType = T extends new (...args: infer A) => any + ? A + : never; export type Mocked = { [P in keyof T]: T[P] extends (...args: Array) => any ? MockInstance, ArgsType> @@ -192,6 +203,10 @@ type FunctionPropertyNames = { [K in keyof T]: T[K] extends (...args: Array) => any ? K : never; }[keyof T] & string; +type ConstructorPropertyNames = { + [K in keyof T]: T[K] extends new (...args: Array) => any ? K : never; +}[keyof T] & + string; const MOCK_CONSTRUCTOR_NAME = 'mockConstructor'; @@ -988,6 +1003,10 @@ export class ModuleMocker { return metadata; } + isMockFunction = Array>( + fn: (...args: Y) => T, + ): fn is Mock; + isMockFunction(fn: unknown): boolean; isMockFunction(fn: unknown): fn is Mock { return !!fn && (fn as any)._isMockFunction === true; } @@ -1003,19 +1022,27 @@ export class ModuleMocker { return fn; } - spyOn>( - object: T, - methodName: M, - accessType: 'get', - ): SpyInstance; + spyOn< + T extends ClassLike | ObjectLike, + M extends NonFunctionPropertyNames, + >(object: T, methodName: M, accessType: 'get'): SpyInstance; - spyOn>( + spyOn< + T extends ClassLike | ObjectLike, + M extends NonFunctionPropertyNames, + >(object: T, methodName: M, accessType: 'set'): SpyInstance; + + spyOn< + T extends ClassLike | ObjectLike, + M extends ConstructorPropertyNames>, + >( object: T, - methodName: M, - accessType: 'set', - ): SpyInstance; + method: M, + ): T[M] extends new (...args: Array) => any + ? SpyInstance, ConstructorArgsType> + : never; - spyOn>( + spyOn>( object: T, methodName: M, ): T[M] extends (...args: Array) => any @@ -1023,11 +1050,10 @@ export class ModuleMocker { : never; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - spyOn>( - object: T, - methodName: M, - accessType?: 'get' | 'set', - ) { + spyOn< + T extends ClassLike | ObjectLike, + M extends NonFunctionPropertyNames, + >(object: T, methodName: M, accessType?: 'get' | 'set') { if (accessType) { return this._spyOnProperty(object, methodName, accessType); } @@ -1094,11 +1120,10 @@ export class ModuleMocker { return object[methodName]; } - private _spyOnProperty>( - obj: T, - propertyName: M, - accessType: 'get' | 'set' = 'get', - ): Mock { + private _spyOnProperty< + T extends ClassLike | ObjectLike, + M extends NonFunctionPropertyNames, + >(obj: T, propertyName: M, accessType: 'get' | 'set' = 'get'): Mock { if (typeof obj !== 'object' && typeof obj !== 'function') { throw new Error( 'Cannot spyOn on a primitive value; ' + this._typeOf(obj) + ' given', diff --git a/packages/jest-types/__typetests__/jest.test.ts b/packages/jest-types/__typetests__/jest.test.ts index 3f5478f524ad..dcbb2e9f7acf 100644 --- a/packages/jest-types/__typetests__/jest.test.ts +++ b/packages/jest-types/__typetests__/jest.test.ts @@ -7,89 +7,292 @@ import {expectError, expectType} from 'tsd-lite'; import {jest} from '@jest/globals'; -import type {Mock} from 'jest-mock'; +import type {Mock, SpyInstance} from 'jest-mock'; + +expectType( + jest + .autoMockOff() + .autoMockOn() + .clearAllMocks() + .disableAutomock() + .enableAutomock() + .deepUnmock('moduleName') + .doMock('moduleName') + .doMock('moduleName', jest.fn()) + .doMock('moduleName', jest.fn(), {}) + .doMock('moduleName', jest.fn(), {virtual: true}) + .dontMock('moduleName') + .isolateModules(() => {}) + .mock('moduleName') + .mock('moduleName', jest.fn()) + .mock('moduleName', jest.fn(), {}) + .mock('moduleName', jest.fn(), {virtual: true}) + .unstable_mockModule('moduleName', jest.fn()) + .unstable_mockModule('moduleName', () => Promise.resolve(jest.fn())) + .unstable_mockModule('moduleName', jest.fn(), {}) + .unstable_mockModule('moduleName', () => Promise.resolve(jest.fn()), {}) + .unstable_mockModule('moduleName', jest.fn(), {virtual: true}) + .unstable_mockModule('moduleName', () => Promise.resolve(jest.fn()), { + virtual: true, + }) + .resetAllMocks() + .resetModules() + .restoreAllMocks() + .retryTimes(3) + .setMock('moduleName', {a: 'b'}) + .setTimeout(6000) + .unmock('moduleName') + .useFakeTimers() + .useFakeTimers('modern') + .useFakeTimers('legacy') + .useRealTimers(), +); expectType(jest.autoMockOff()); +expectError(jest.autoMockOff(true)); + expectType(jest.autoMockOn()); -expectType(jest.clearAllMocks()); -expectType(jest.clearAllTimers()); -expectType(jest.resetAllMocks()); -expectType(jest.restoreAllMocks()); -expectType(jest.clearAllTimers()); +expectError(jest.autoMockOn(false)); + +expectType(jest.createMockFromModule('moduleName')); +expectError(jest.createMockFromModule()); + expectType(jest.deepUnmock('moduleName')); -expectType(jest.disableAutomock()); +expectError(jest.deepUnmock()); + expectType(jest.doMock('moduleName')); expectType(jest.doMock('moduleName', jest.fn())); expectType(jest.doMock('moduleName', jest.fn(), {})); expectType(jest.doMock('moduleName', jest.fn(), {virtual: true})); +expectError(jest.doMock()); expectType(jest.dontMock('moduleName')); +expectError(jest.dontMock()); + +expectType(jest.disableAutomock()); +expectError(jest.disableAutomock(true)); + expectType(jest.enableAutomock()); +expectError(jest.enableAutomock('moduleName')); + +expectType(jest.isolateModules(() => {})); +expectError(jest.isolateModules()); + expectType(jest.mock('moduleName')); expectType(jest.mock('moduleName', jest.fn())); expectType(jest.mock('moduleName', jest.fn(), {})); expectType(jest.mock('moduleName', jest.fn(), {virtual: true})); -expectType(jest.resetModules()); -expectType(jest.isolateModules(() => {})); -expectType(jest.retryTimes(3)); -expectType, []>>( - jest - .fn(() => Promise.resolve('string value')) - .mockResolvedValueOnce('A string, not a Promise'), +expectError(jest.mock()); + +expectType(jest.unstable_mockModule('moduleName', jest.fn())); +expectType( + jest.unstable_mockModule('moduleName', () => Promise.resolve(jest.fn())), ); -expectType, []>>( - jest - .fn(() => Promise.resolve('string value')) - .mockResolvedValue('A string, not a Promise'), +expectType(jest.unstable_mockModule('moduleName', jest.fn(), {})); +expectType( + jest.unstable_mockModule('moduleName', () => Promise.resolve(jest.fn()), {}), ); -expectType, []>>( - jest - .fn(() => Promise.resolve('string value')) - .mockRejectedValueOnce(new Error('An error, not a string')), +expectType( + jest.unstable_mockModule('moduleName', jest.fn(), {virtual: true}), ); -expectType, []>>( - jest - .fn(() => Promise.resolve('string value')) - .mockRejectedValue(new Error('An error, not a string')), +expectType( + jest.unstable_mockModule('moduleName', () => Promise.resolve(jest.fn()), { + virtual: true, + }), ); -expectType(jest.runAllImmediates()); -expectType(jest.runAllTicks()); -expectType(jest.runAllTimers()); -expectType(jest.runOnlyPendingTimers()); -expectType(jest.advanceTimersByTime(9001)); +expectType(jest.requireActual('./pathToModule')); +expectError(jest.requireActual()); + +expectType(jest.requireMock('./pathToModule')); +expectError(jest.requireMock()); + +expectType(jest.resetModules()); +expectError(jest.resetModules('moduleName')); -expectType(jest.setMock('moduleName', {})); -expectType(jest.setMock('moduleName', {})); expectType(jest.setMock('moduleName', {a: 'b'})); -expectType(jest.setTimeout(9001)); +expectError(jest.setMock('moduleName')); + expectType(jest.unmock('moduleName')); -expectType(jest.useFakeTimers()); -expectType(jest.useRealTimers()); +expectError(jest.unmock()); + +// Mock Functions + +expectType(jest.clearAllMocks()); +expectError(jest.clearAllMocks('moduleName')); + +expectType(jest.isMockFunction(() => {})); +expectError(jest.isMockFunction()); + +const maybeMock = (a: string, b: number) => true; + +if (jest.isMockFunction(maybeMock)) { + expectType>(maybeMock); + + maybeMock.mockReturnValueOnce(false); + expectError(maybeMock.mockReturnValueOnce(123)); +} + +if (!jest.isMockFunction(maybeMock)) { + expectType<(a: string, b: number) => boolean>(maybeMock); +} + +const surelyMock = jest.fn((a: string, b: number) => true); + +if (jest.isMockFunction(surelyMock)) { + expectType>(surelyMock); + + surelyMock.mockReturnValueOnce(false); + expectError(surelyMock.mockReturnValueOnce(123)); +} + +if (!jest.isMockFunction(surelyMock)) { + expectType(surelyMock); +} + +declare const hardlyMockFn: string; + +if (!jest.isMockFunction(hardlyMockFn)) { + expectType(hardlyMockFn); +} + +if (jest.isMockFunction(hardlyMockFn)) { + expectType(hardlyMockFn); +} + +expectType>(jest.fn()); +expectType>(jest.fn(() => {})); +expectType>( + jest.fn((a: string, b: number) => true), +); +expectType>( + jest.fn((e: any) => { + throw new Error(); + }), +); +expectError(jest.fn('moduleName')); + +expectType(jest.resetAllMocks()); +expectError(jest.resetAllMocks(true)); + +expectType(jest.restoreAllMocks()); +expectError(jest.restoreAllMocks(false)); + +const spiedArray = ['a', 'b']; + +const spiedFunction = () => {}; + +spiedFunction.toString(); + +const spiedObject = { + _propertyB: false, + + methodA() { + return true; + }, + methodB(a: string, b: number) { + return; + }, + methodC(e: any) { + throw new Error(); + }, + + propertyA: 'abc', + + set propertyB(value) { + this._propertyB = value; + }, + get propertyB() { + return this._propertyB; + }, +}; + +expectType>(jest.spyOn(spiedObject, 'methodA')); +expectType>( + jest.spyOn(spiedObject, 'methodB'), +); +expectType>(jest.spyOn(spiedObject, 'methodC')); + +expectType>( + jest.spyOn(spiedObject, 'propertyB', 'get'), +); +expectType>( + jest.spyOn(spiedObject, 'propertyB', 'set'), +); +expectError(jest.spyOn(spiedObject, 'propertyB')); +expectError(jest.spyOn(spiedObject, 'methodB', 'get')); +expectError(jest.spyOn(spiedObject, 'methodB', 'set')); + +expectType>( + jest.spyOn(spiedObject, 'propertyA', 'get'), +); +expectType>( + jest.spyOn(spiedObject, 'propertyA', 'set'), +); +expectError(jest.spyOn(spiedObject, 'propertyA')); + +expectError(jest.spyOn(spiedObject, 'notThere')); +expectError(jest.spyOn(spiedArray as unknown as ArrayConstructor, 'from')); +expectError(jest.spyOn(spiedFunction as unknown as Function, 'toString')); // eslint-disable-line @typescript-eslint/ban-types +expectError(jest.spyOn('abc', 'methodA')); +expectError(jest.spyOn(123, 'methodA')); +expectError(jest.spyOn(true, 'methodA')); +expectError(jest.spyOn(spiedObject)); +expectError(jest.spyOn()); + +expectType>( + jest.spyOn(global, 'Date'), +); +expectType>(jest.spyOn(Date, 'now')); + +// Mock Timers + +expectType(jest.advanceTimersByTime(6000)); +expectError(jest.advanceTimersByTime()); expectType(jest.advanceTimersToNextTimer()); expectType(jest.advanceTimersToNextTimer(2)); +expectError(jest.advanceTimersToNextTimer('2')); -// https://jestjs.io/docs/jest-object#jestusefaketimersimplementation-modern--legacy -expectType(jest.useFakeTimers('modern')); -expectType(jest.useFakeTimers('legacy')); +expectType(jest.clearAllTimers()); +expectError(jest.clearAllTimers(false)); + +expectType(jest.getTimerCount()); +expectError(jest.getTimerCount(true)); + +expectType(jest.getRealSystemTime()); +expectError(jest.getRealSystemTime(true)); + +expectType(jest.runAllImmediates()); +expectError(jest.runAllImmediates(true)); + +expectType(jest.runAllTicks()); +expectError(jest.runAllTicks(true)); -expectError(jest.useFakeTimers('foo')); +expectType(jest.runAllTimers()); +expectError(jest.runAllTimers(false)); + +expectType(jest.runOnlyPendingTimers()); +expectError(jest.runOnlyPendingTimers(true)); -// https://jestjs.io/docs/jest-object#jestsetsystemtimenow-number--date expectType(jest.setSystemTime()); -expectType(jest.setSystemTime(0)); -expectType(jest.setSystemTime(new Date(0))); +expectType(jest.setSystemTime(1483228800000)); +expectType(jest.setSystemTime(Date.now())); +expectType(jest.setSystemTime(new Date(1995, 11, 17))); +expectError(jest.setSystemTime('1995-12-17T03:24:00')); -expectError(jest.setSystemTime('foo')); +expectType(jest.useFakeTimers()); +expectType(jest.useFakeTimers('modern')); +expectType(jest.useFakeTimers('legacy')); +expectError(jest.useFakeTimers('latest')); -// https://jestjs.io/docs/jest-object#jestgetrealsystemtime -expectType(jest.getRealSystemTime()); +expectType(jest.useRealTimers()); +expectError(jest.useRealTimers(true)); -expectError(jest.getRealSystemTime('foo')); +// Misc -// https://jestjs.io/docs/jest-object#jestrequireactualmodulename -expectType(jest.requireActual('./thisReturnsTheActualModule')); +expectType(jest.setTimeout(6000)); +expectError(jest.setTimeout()); -// https://jestjs.io/docs/jest-object#jestrequiremockmodulename -expectType(jest.requireMock('./thisAlwaysReturnsTheMock')); +expectType(jest.retryTimes(3)); +expectError(jest.retryTimes()); From e89c64b29be770a29de2d8ab90c0a959554f2508 Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sun, 20 Feb 2022 22:14:57 +0200 Subject: [PATCH 2/9] fix test --- packages/jest-types/__typetests__/jest.test.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/jest-types/__typetests__/jest.test.ts b/packages/jest-types/__typetests__/jest.test.ts index dcbb2e9f7acf..1a90a7306273 100644 --- a/packages/jest-types/__typetests__/jest.test.ts +++ b/packages/jest-types/__typetests__/jest.test.ts @@ -232,14 +232,26 @@ expectType>( expectError(jest.spyOn(spiedObject, 'propertyA')); expectError(jest.spyOn(spiedObject, 'notThere')); -expectError(jest.spyOn(spiedArray as unknown as ArrayConstructor, 'from')); -expectError(jest.spyOn(spiedFunction as unknown as Function, 'toString')); // eslint-disable-line @typescript-eslint/ban-types expectError(jest.spyOn('abc', 'methodA')); expectError(jest.spyOn(123, 'methodA')); expectError(jest.spyOn(true, 'methodA')); expectError(jest.spyOn(spiedObject)); expectError(jest.spyOn()); +expectType< + SpyInstance< + Array, + [ + iterable: Iterable | ArrayLike, + mapfn: (v: unknown, k: number) => unknown, + thisArg?: any, + ] + > +>(jest.spyOn(spiedArray as unknown as ArrayConstructor, 'from')); +expectType>( + jest.spyOn(spiedFunction as unknown as Function, 'toString'), // eslint-disable-line @typescript-eslint/ban-types +); + expectType>( jest.spyOn(global, 'Date'), ); From 19e56a6a6eac990d3778f63e5d82b47baf40a55d Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Sun, 20 Feb 2022 22:40:30 +0200 Subject: [PATCH 3/9] tweak constrain --- packages/jest-mock/src/index.ts | 44 ++++++++----------- .../jest-types/__typetests__/jest.test.ts | 16 +++---- 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index a7d333722788..d56e996d8346 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -32,14 +32,6 @@ export type MockFunctionMetadata< length?: number; }; -type ClassLike = { - new (...args: Array): any; -}; - -type ObjectLike = { - [key: string]: any; -}; - export type MockableFunction = (...args: Array) => any; export type MethodKeysOf = { [K in keyof T]: T[K] extends MockableFunction ? K : never; @@ -1022,27 +1014,26 @@ export class ModuleMocker { return fn; } - spyOn< - T extends ClassLike | ObjectLike, - M extends NonFunctionPropertyNames, - >(object: T, methodName: M, accessType: 'get'): SpyInstance; + spyOn>( + object: T, + methodName: M, + accessType: 'get', + ): SpyInstance; - spyOn< - T extends ClassLike | ObjectLike, - M extends NonFunctionPropertyNames, - >(object: T, methodName: M, accessType: 'set'): SpyInstance; + spyOn>( + object: T, + methodName: M, + accessType: 'set', + ): SpyInstance; - spyOn< - T extends ClassLike | ObjectLike, - M extends ConstructorPropertyNames>, - >( + spyOn>>( object: T, method: M, ): T[M] extends new (...args: Array) => any ? SpyInstance, ConstructorArgsType> : never; - spyOn>( + spyOn>( object: T, methodName: M, ): T[M] extends (...args: Array) => any @@ -1050,10 +1041,11 @@ export class ModuleMocker { : never; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - spyOn< - T extends ClassLike | ObjectLike, - M extends NonFunctionPropertyNames, - >(object: T, methodName: M, accessType?: 'get' | 'set') { + spyOn>( + object: T, + methodName: M, + accessType?: 'get' | 'set', + ) { if (accessType) { return this._spyOnProperty(object, methodName, accessType); } @@ -1121,7 +1113,7 @@ export class ModuleMocker { } private _spyOnProperty< - T extends ClassLike | ObjectLike, + T extends object, M extends NonFunctionPropertyNames, >(obj: T, propertyName: M, accessType: 'get' | 'set' = 'get'): Mock { if (typeof obj !== 'object' && typeof obj !== 'function') { diff --git a/packages/jest-types/__typetests__/jest.test.ts b/packages/jest-types/__typetests__/jest.test.ts index 1a90a7306273..d1a361af09f9 100644 --- a/packages/jest-types/__typetests__/jest.test.ts +++ b/packages/jest-types/__typetests__/jest.test.ts @@ -238,19 +238,15 @@ expectError(jest.spyOn(true, 'methodA')); expectError(jest.spyOn(spiedObject)); expectError(jest.spyOn()); -expectType< - SpyInstance< - Array, - [ - iterable: Iterable | ArrayLike, - mapfn: (v: unknown, k: number) => unknown, - thisArg?: any, - ] - > ->(jest.spyOn(spiedArray as unknown as ArrayConstructor, 'from')); +expectType>( + jest.spyOn(spiedArray as unknown as ArrayConstructor, 'isArray'), +); +expectError(jest.spyOn(spiedArray, 'isArray')); + expectType>( jest.spyOn(spiedFunction as unknown as Function, 'toString'), // eslint-disable-line @typescript-eslint/ban-types ); +expectError(jest.spyOn(spiedFunction, 'toString')); expectType>( jest.spyOn(global, 'Date'), From fe798c693461791ead1bb4fb3b2b9c40f94f5692 Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Mon, 21 Feb 2022 08:37:13 +0200 Subject: [PATCH 4/9] add changelog entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01f3284eb17e..27efaa30ea04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - `[jest-environment-node]` [**BREAKING**] Add default `node` and `node-addon` conditions to `exportConditions` for `node` environment ([#11924](https://github.com/facebook/jest/pull/11924)) - `[@jest/expect]` New module which extends `expect` with `jest-snapshot` matchers ([#12404](https://github.com/facebook/jest/pull/12404), [#12410](https://github.com/facebook/jest/pull/12410), [#12418](https://github.com/facebook/jest/pull/12418)) - `[@jest/expect-utils]` New module exporting utils for `expect` ([#12323](https://github.com/facebook/jest/pull/12323)) +- `[jest-mock]` Improve `isMockFunction` to infer types of passed function ([#12442](https://github.com/facebook/jest/pull/12442)) - `[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)) - `[@jest/schemas]` New module for JSON schemas for Jest's config ([#12384](https://github.com/facebook/jest/pull/12384)) @@ -27,6 +28,7 @@ - `[jest-haste-map]` Don't use partial results if file crawl errors ([#12420](https://github.com/facebook/jest/pull/12420)) - `[jest-jasmine2, jest-types]` [**BREAKING**] Move all `jasmine` specific types from `@jest/types` to its own package ([#12125](https://github.com/facebook/jest/pull/12125)) - `[jest-matcher-utils]` Pass maxWidth to `pretty-format` to avoid printing every element in arrays by default ([#12402](https://github.com/facebook/jest/pull/12402)) +- `[jest-mock]` Fix function overloads for `spyOn` to allow more correct type inference in complex object ([#12442](https://github.com/facebook/jest/pull/12442)) ### Chore & Maintenance From 8c25aab5a3bc16c104d9d528e8d63b02f8b7eae8 Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Mon, 21 Feb 2022 08:42:33 +0200 Subject: [PATCH 5/9] clean up --- packages/jest-mock/src/index.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index d56e996d8346..011d4a93cd4a 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -79,9 +79,6 @@ export type MaybeMocked = T extends MockableFunction : T; export type ArgsType = T extends (...args: infer A) => any ? A : never; -type ConstructorArgsType = T extends new (...args: infer A) => any - ? A - : never; export type Mocked = { [P in keyof T]: T[P] extends (...args: Array) => any ? MockInstance, ArgsType> @@ -1030,7 +1027,7 @@ export class ModuleMocker { object: T, method: M, ): T[M] extends new (...args: Array) => any - ? SpyInstance, ConstructorArgsType> + ? SpyInstance, ConstructorParameters> : never; spyOn>( From d98face4adadcb40b150b0f579e92d1956d7ef9b Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Mon, 21 Feb 2022 08:44:26 +0200 Subject: [PATCH 6/9] clean up --- .../__typetests__/mock-functions.test.ts | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 packages/jest-types/__typetests__/mock-functions.test.ts diff --git a/packages/jest-types/__typetests__/mock-functions.test.ts b/packages/jest-types/__typetests__/mock-functions.test.ts new file mode 100644 index 000000000000..48bb95287ff9 --- /dev/null +++ b/packages/jest-types/__typetests__/mock-functions.test.ts @@ -0,0 +1,90 @@ +/** + * 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 {expectError, expectType} from 'tsd-lite'; +import {jest} from '@jest/globals'; +import type {Mock} from 'jest-mock'; + +// jest.fn() + +const mockFn = jest.fn((a: string, b: number) => true); + +expectType(mockFn.getMockName()); +expectError(mockFn.getMockName('some-mock')); + +expectType(mockFn.mock.calls.length); + +// hm.. possibly undefined? +// https://github.com/facebook/jest/issues/10402 +expectType(mockFn.mock.calls[0][0]); +expectType(mockFn.mock.calls[0][1]); + +expectType(mockFn.mock.calls[1][0]); +expectType(mockFn.mock.calls[1][1]); + +expectType<[a: string, b: number] | undefined>(mockFn.mock.lastCall); + +expectType>(mockFn.mock.invocationCallOrder); + +const returnValue = mockFn.mock.results[0]; + +expectType<'incomplete' | 'return' | 'throw'>(returnValue.type); +expectType(returnValue.value); + +// TODO +// +// if (returnValue.type === 'incomplete') { +// expectType(returnValue.value); +// } + +// if (returnValue.type === 'return') { +// expectType(returnValue.value); +// } + +// if (returnValue.type === 'throw') { +// expectType(returnValue.value); +// } + +expectType>(mockFn.mockClear()); +expectError(mockFn.mockClear('some-mock')); + +expectType>(mockFn.mockReset()); +expectError(mockFn.mockClear('some-mock')); + +expectType(mockFn.mockRestore()); +expectError(mockFn.mockClear('some-mock')); + +// +// All bellow is TODO +// + +const mock = jest.fn(); + +mock.mockClear(); + +expectType, []>>( + jest + .fn(() => Promise.resolve('string value')) + .mockResolvedValueOnce('A string, not a Promise'), +); +expectType, []>>( + jest + .fn(() => Promise.resolve('string value')) + .mockResolvedValue('A string, not a Promise'), +); +expectType, []>>( + jest + .fn(() => Promise.resolve('string value')) + .mockRejectedValueOnce(new Error('An error, not a string')), +); +expectType, []>>( + jest + .fn(() => Promise.resolve('string value')) + .mockRejectedValue(new Error('An error, not a string')), +); + +// jest.spyOn() From f342cce83738f75adea88b78d1b037e672675b66 Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Mon, 21 Feb 2022 08:48:23 +0200 Subject: [PATCH 7/9] real clean up --- packages/jest-mock/src/index.ts | 2 +- .../__typetests__/mock-functions.test.ts | 90 ------------------- 2 files changed, 1 insertion(+), 91 deletions(-) delete mode 100644 packages/jest-types/__typetests__/mock-functions.test.ts diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 011d4a93cd4a..ad13745428ef 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -1025,7 +1025,7 @@ export class ModuleMocker { spyOn>>( object: T, - method: M, + methodName: M, ): T[M] extends new (...args: Array) => any ? SpyInstance, ConstructorParameters> : never; diff --git a/packages/jest-types/__typetests__/mock-functions.test.ts b/packages/jest-types/__typetests__/mock-functions.test.ts deleted file mode 100644 index 48bb95287ff9..000000000000 --- a/packages/jest-types/__typetests__/mock-functions.test.ts +++ /dev/null @@ -1,90 +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 {expectError, expectType} from 'tsd-lite'; -import {jest} from '@jest/globals'; -import type {Mock} from 'jest-mock'; - -// jest.fn() - -const mockFn = jest.fn((a: string, b: number) => true); - -expectType(mockFn.getMockName()); -expectError(mockFn.getMockName('some-mock')); - -expectType(mockFn.mock.calls.length); - -// hm.. possibly undefined? -// https://github.com/facebook/jest/issues/10402 -expectType(mockFn.mock.calls[0][0]); -expectType(mockFn.mock.calls[0][1]); - -expectType(mockFn.mock.calls[1][0]); -expectType(mockFn.mock.calls[1][1]); - -expectType<[a: string, b: number] | undefined>(mockFn.mock.lastCall); - -expectType>(mockFn.mock.invocationCallOrder); - -const returnValue = mockFn.mock.results[0]; - -expectType<'incomplete' | 'return' | 'throw'>(returnValue.type); -expectType(returnValue.value); - -// TODO -// -// if (returnValue.type === 'incomplete') { -// expectType(returnValue.value); -// } - -// if (returnValue.type === 'return') { -// expectType(returnValue.value); -// } - -// if (returnValue.type === 'throw') { -// expectType(returnValue.value); -// } - -expectType>(mockFn.mockClear()); -expectError(mockFn.mockClear('some-mock')); - -expectType>(mockFn.mockReset()); -expectError(mockFn.mockClear('some-mock')); - -expectType(mockFn.mockRestore()); -expectError(mockFn.mockClear('some-mock')); - -// -// All bellow is TODO -// - -const mock = jest.fn(); - -mock.mockClear(); - -expectType, []>>( - jest - .fn(() => Promise.resolve('string value')) - .mockResolvedValueOnce('A string, not a Promise'), -); -expectType, []>>( - jest - .fn(() => Promise.resolve('string value')) - .mockResolvedValue('A string, not a Promise'), -); -expectType, []>>( - jest - .fn(() => Promise.resolve('string value')) - .mockRejectedValueOnce(new Error('An error, not a string')), -); -expectType, []>>( - jest - .fn(() => Promise.resolve('string value')) - .mockRejectedValue(new Error('An error, not a string')), -); - -// jest.spyOn() From 93971bf4ac276e80bc5ae10d605cec9f16738f8a Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Mon, 21 Feb 2022 10:15:46 +0200 Subject: [PATCH 8/9] more tests --- .../jest-types/__typetests__/jest.test.ts | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/jest-types/__typetests__/jest.test.ts b/packages/jest-types/__typetests__/jest.test.ts index d1a361af09f9..a9f2e55aa24a 100644 --- a/packages/jest-types/__typetests__/jest.test.ts +++ b/packages/jest-types/__typetests__/jest.test.ts @@ -150,14 +150,34 @@ if (!jest.isMockFunction(surelyMock)) { expectType(surelyMock); } -declare const hardlyMockFn: string; +declare const stringNotMock: string; -if (!jest.isMockFunction(hardlyMockFn)) { - expectType(hardlyMockFn); +if (!jest.isMockFunction(stringNotMock)) { + expectType(stringNotMock); } -if (jest.isMockFunction(hardlyMockFn)) { - expectType(hardlyMockFn); +if (jest.isMockFunction(stringNotMock)) { + expectType(stringNotMock); +} + +declare const anyNotMock: any; + +if (!jest.isMockFunction(anyNotMock)) { + expectType(anyNotMock); +} + +if (jest.isMockFunction(anyNotMock)) { + expectType(anyNotMock); +} + +declare const unknownNotMock: unknown; + +if (!jest.isMockFunction(unknownNotMock)) { + expectType(unknownNotMock); +} + +if (jest.isMockFunction(unknownNotMock)) { + expectType(unknownNotMock); } expectType>(jest.fn()); From 92b21a8eb176016532c065455052fd98c9a279a6 Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Mon, 21 Feb 2022 10:25:38 +0200 Subject: [PATCH 9/9] tweak overload type --- packages/jest-mock/src/index.ts | 2 +- .../jest-types/__typetests__/jest.test.ts | 30 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index ad13745428ef..db143d16fe59 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -995,7 +995,7 @@ export class ModuleMocker { isMockFunction = Array>( fn: (...args: Y) => T, ): fn is Mock; - isMockFunction(fn: unknown): boolean; + isMockFunction(fn: unknown): fn is Mock; isMockFunction(fn: unknown): fn is Mock { return !!fn && (fn as any)._isMockFunction === true; } diff --git a/packages/jest-types/__typetests__/jest.test.ts b/packages/jest-types/__typetests__/jest.test.ts index a9f2e55aa24a..821a322137b2 100644 --- a/packages/jest-types/__typetests__/jest.test.ts +++ b/packages/jest-types/__typetests__/jest.test.ts @@ -150,34 +150,34 @@ if (!jest.isMockFunction(surelyMock)) { expectType(surelyMock); } -declare const stringNotMock: string; +declare const stringMaybeMock: string; -if (!jest.isMockFunction(stringNotMock)) { - expectType(stringNotMock); +if (!jest.isMockFunction(stringMaybeMock)) { + expectType(stringMaybeMock); } -if (jest.isMockFunction(stringNotMock)) { - expectType(stringNotMock); +if (jest.isMockFunction(stringMaybeMock)) { + expectType>>(stringMaybeMock); } -declare const anyNotMock: any; +declare const anyMaybeMock: any; -if (!jest.isMockFunction(anyNotMock)) { - expectType(anyNotMock); +if (!jest.isMockFunction(anyMaybeMock)) { + expectType(anyMaybeMock); } -if (jest.isMockFunction(anyNotMock)) { - expectType(anyNotMock); +if (jest.isMockFunction(anyMaybeMock)) { + expectType>>(anyMaybeMock); } -declare const unknownNotMock: unknown; +declare const unknownMaybeMock: unknown; -if (!jest.isMockFunction(unknownNotMock)) { - expectType(unknownNotMock); +if (!jest.isMockFunction(unknownMaybeMock)) { + expectType(unknownMaybeMock); } -if (jest.isMockFunction(unknownNotMock)) { - expectType(unknownNotMock); +if (jest.isMockFunction(unknownMaybeMock)) { + expectType>>(unknownMaybeMock); } expectType>(jest.fn());