From 7a05a6c9de73791e40f1c60405913cbc5897b417 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Mon, 21 Feb 2022 11:47:13 +0200 Subject: [PATCH] chore: extend and improve type tests for `jest` object (#12442) --- CHANGELOG.md | 2 + packages/jest-environment/src/index.ts | 17 +- packages/jest-mock/src/index.ts | 32 +- .../jest-types/__typetests__/jest.test.ts | 333 +++++++++++++++--- 4 files changed, 312 insertions(+), 72 deletions(-) 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 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..db143d16fe59 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -192,6 +192,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 +992,10 @@ export class ModuleMocker { return metadata; } + 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 any)._isMockFunction === true; } @@ -1003,19 +1011,26 @@ export class ModuleMocker { return fn; } - spyOn>( + spyOn>( object: T, methodName: M, accessType: 'get', ): SpyInstance; - spyOn>( + spyOn>( object: T, methodName: M, accessType: 'set', ): SpyInstance; - spyOn>( + spyOn>>( + object: T, + methodName: M, + ): T[M] extends new (...args: Array) => any + ? SpyInstance, ConstructorParameters> + : never; + + spyOn>( object: T, methodName: M, ): T[M] extends (...args: Array) => any @@ -1023,7 +1038,7 @@ export class ModuleMocker { : never; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - spyOn>( + spyOn>( object: T, methodName: M, accessType?: 'get' | 'set', @@ -1094,11 +1109,10 @@ export class ModuleMocker { return object[methodName]; } - private _spyOnProperty>( - obj: T, - propertyName: M, - accessType: 'get' | 'set' = 'get', - ): Mock { + private _spyOnProperty< + T extends object, + 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..821a322137b2 100644 --- a/packages/jest-types/__typetests__/jest.test.ts +++ b/packages/jest-types/__typetests__/jest.test.ts @@ -7,89 +7,320 @@ 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 stringMaybeMock: string; + +if (!jest.isMockFunction(stringMaybeMock)) { + expectType(stringMaybeMock); +} + +if (jest.isMockFunction(stringMaybeMock)) { + expectType>>(stringMaybeMock); +} + +declare const anyMaybeMock: any; + +if (!jest.isMockFunction(anyMaybeMock)) { + expectType(anyMaybeMock); +} + +if (jest.isMockFunction(anyMaybeMock)) { + expectType>>(anyMaybeMock); +} + +declare const unknownMaybeMock: unknown; + +if (!jest.isMockFunction(unknownMaybeMock)) { + expectType(unknownMaybeMock); +} + +if (jest.isMockFunction(unknownMaybeMock)) { + expectType>>(unknownMaybeMock); +} + +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('abc', 'methodA')); +expectError(jest.spyOn(123, 'methodA')); +expectError(jest.spyOn(true, 'methodA')); +expectError(jest.spyOn(spiedObject)); +expectError(jest.spyOn()); + +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'), +); +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)); -expectError(jest.useFakeTimers('foo')); +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)); + +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());