diff --git a/CHANGELOG.md b/CHANGELOG.md index c1c45709268e..d559084a4e5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Features +- `[@jest/globals, jest-mock]` Add `jest.Spied*` utility types ([#13440](https://github.com/facebook/jest/pull/13440)) + ### Fixes ### Chore & Maintenance diff --git a/docs/JestObjectAPI.md b/docs/JestObjectAPI.md index 6314b7f0797c..a961dab0fd0d 100644 --- a/docs/JestObjectAPI.md +++ b/docs/JestObjectAPI.md @@ -699,6 +699,10 @@ test('plays audio', () => { }); ``` +### `jest.Spied` + +See [TypeScript Usage](MockFunctionAPI.md#jestspiedsource) chapter of Mock Functions page for documentation. + ### `jest.clearAllMocks()` Clears the `mock.calls`, `mock.instances`, `mock.contexts` and `mock.results` properties of all mocks. Equivalent to calling [`.mockClear()`](MockFunctionAPI.md#mockfnmockclear) on every mocked function. diff --git a/docs/MockFunctionAPI.md b/docs/MockFunctionAPI.md index fef7d983aec6..58b02dc8d8f0 100644 --- a/docs/MockFunctionAPI.md +++ b/docs/MockFunctionAPI.md @@ -644,3 +644,37 @@ test('direct usage', () => { expect(jest.mocked(console.log).mock.calls).toHaveLength(1); }); ``` + +### `jest.Spied` + +Constructs the type of a spied class or function (i.e. the return type of `jest.spyOn()`). + +```ts title="__utils__/setDateNow.ts" +import {jest} from '@jest/globals'; + +export function setDateNow(now: number): jest.Spied { + return jest.spyOn(Date, 'now').mockReturnValue(now); +} +``` + +```ts +import {afterEach, expect, jest, test} from '@jest/globals'; +import {setDateNow} from './__utils__/setDateNow'; + +let spiedDateNow: jest.Spied | undefined = undefined; + +afterEach(() => { + spiedDateNow?.mockReset(); +}); + +test('renders correctly with a given date', () => { + spiedDateNow = setDateNow(1482363367071); + // ... + + expect(spiedDateNow).toHaveBeenCalledTimes(1); +}); +``` + +Types of a class or function can be passed as type argument to `jest.Spied`. If you prefer to constrain the input type, use: `jest.SpiedClass` or `jest.SpiedFunction`. + +Use `jest.SpiedGetter` or `jest.SpiedSetter` to create the type of a spied getter or setter respectively. diff --git a/packages/jest-globals/src/index.ts b/packages/jest-globals/src/index.ts index 956d2b3fece7..c2bd3abb7f59 100644 --- a/packages/jest-globals/src/index.ts +++ b/packages/jest-globals/src/index.ts @@ -16,6 +16,11 @@ import type { MockedClass as JestMockedClass, MockedFunction as JestMockedFunction, MockedObject as JestMockedObject, + Spied as JestSpied, + SpiedClass as JestSpiedClass, + SpiedFunction as JestSpiedFunction, + SpiedGetter as JestSpiedGetter, + SpiedSetter as JestSpiedSetter, UnknownFunction, } from 'jest-mock'; @@ -58,6 +63,26 @@ declare namespace jest { * Wraps an object type with Jest mock type definitions. */ export type MockedObject = JestMockedObject; + /** + * Constructs the type of a spied class or function. + */ + export type Spied = JestSpied; + /** + * Constructs the type of a spied class. + */ + export type SpiedClass = JestSpiedClass; + /** + * Constructs the type of a spied function. + */ + export type SpiedFunction = JestSpiedFunction; + /** + * Constructs the type of a spied getter. + */ + export type SpiedGetter = JestSpiedGetter; + /** + * Constructs the type of a spied setter. + */ + export type SpiedSetter = JestSpiedSetter; } export {jest}; diff --git a/packages/jest-mock/__typetests__/mock-functions.test.ts b/packages/jest-mock/__typetests__/mock-functions.test.ts index 3d72cc4d37d0..58efe52a828b 100644 --- a/packages/jest-mock/__typetests__/mock-functions.test.ts +++ b/packages/jest-mock/__typetests__/mock-functions.test.ts @@ -11,7 +11,15 @@ import { expectNotAssignable, expectType, } from 'tsd-lite'; -import {Mock, SpyInstance, fn, spyOn} from 'jest-mock'; +import { + Mock, + SpiedClass, + SpiedFunction, + SpiedGetter, + SpiedSetter, + fn, + spyOn, +} from 'jest-mock'; // jest.fn() @@ -320,26 +328,30 @@ expectNotAssignable(spy); // eslint-disable-line @typescript-eslint/ba expectError(spy()); expectError(new spy()); -expectType>( +expectType>( spyOn(spiedObject, 'methodA'), ); -expectType>( +expectType>( spyOn(spiedObject, 'methodB'), ); -expectType>( +expectType>( spyOn(spiedObject, 'methodC'), ); -expectType boolean>>(spyOn(spiedObject, 'propertyB', 'get')); -expectType void>>( +expectType>( + spyOn(spiedObject, 'propertyB', 'get'), +); +expectType>( spyOn(spiedObject, 'propertyB', 'set'), ); expectError(spyOn(spiedObject, 'propertyB')); expectError(spyOn(spiedObject, 'methodB', 'get')); expectError(spyOn(spiedObject, 'methodB', 'set')); -expectType string>>(spyOn(spiedObject, 'propertyA', 'get')); -expectType void>>( +expectType>( + spyOn(spiedObject, 'propertyA', 'get'), +); +expectType>( spyOn(spiedObject, 'propertyA', 'set'), ); expectError(spyOn(spiedObject, 'propertyA')); @@ -351,40 +363,38 @@ expectError(spyOn(true, 'methodA')); expectError(spyOn(spiedObject)); expectError(spyOn()); -expectType boolean>>( +expectType>( spyOn(spiedArray as unknown as ArrayConstructor, 'isArray'), ); expectError(spyOn(spiedArray, 'isArray')); -expectType string>>( +expectType>( spyOn(spiedFunction as unknown as Function, 'toString'), // eslint-disable-line @typescript-eslint/ban-types ); expectError(spyOn(spiedFunction, 'toString')); -expectType Date>>( - spyOn(globalThis, 'Date'), -); -expectType number>>(spyOn(Date, 'now')); +expectType>(spyOn(globalThis, 'Date')); +expectType>(spyOn(Date, 'now')); // object with index signatures -expectType>( +expectType>( spyOn(indexSpiedObject, 'methodA'), ); -expectType>( +expectType>( spyOn(indexSpiedObject, 'methodB'), ); -expectType>( +expectType>( spyOn(indexSpiedObject, 'methodC'), ); -expectType>( +expectType>( spyOn(indexSpiedObject, 'methodE'), ); -expectType {a: string}>>( +expectType>( spyOn(indexSpiedObject, 'propertyA', 'get'), ); -expectType void>>( +expectType>( spyOn(indexSpiedObject, 'propertyA', 'set'), ); expectError(spyOn(indexSpiedObject, 'propertyA')); @@ -419,48 +429,48 @@ interface OptionalInterface { const optionalSpiedObject = {} as OptionalInterface; -expectType SomeClass>>( +expectType>>( spyOn(optionalSpiedObject, 'constructorA'), ); -expectType SomeClass>>( +expectType>( spyOn(optionalSpiedObject, 'constructorB'), ); expectError(spyOn(optionalSpiedObject, 'constructorA', 'get')); expectError(spyOn(optionalSpiedObject, 'constructorA', 'set')); -expectType void>>( +expectType>>( spyOn(optionalSpiedObject, 'methodA'), ); -expectType boolean>>( +expectType>( spyOn(optionalSpiedObject, 'methodB'), ); expectError(spyOn(optionalSpiedObject, 'methodA', 'get')); expectError(spyOn(optionalSpiedObject, 'methodA', 'set')); -expectType number>>( +expectType>>( spyOn(optionalSpiedObject, 'propertyA', 'get'), ); -expectType void>>( +expectType>>( spyOn(optionalSpiedObject, 'propertyA', 'set'), ); -expectType number>>( +expectType>>( spyOn(optionalSpiedObject, 'propertyB', 'get'), ); -expectType void>>( +expectType>>( spyOn(optionalSpiedObject, 'propertyB', 'set'), ); -expectType number | undefined>>( +expectType>( spyOn(optionalSpiedObject, 'propertyC', 'get'), ); -expectType void>>( +expectType>( spyOn(optionalSpiedObject, 'propertyC', 'set'), ); -expectType string>>( +expectType>( spyOn(optionalSpiedObject, 'propertyD', 'get'), ); -expectType void>>( +expectType>( spyOn(optionalSpiedObject, 'propertyD', 'set'), ); diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 408690efaaab..1f8d8c910327 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -100,6 +100,29 @@ export type MockedShallow = T extends ClassLike : T; export type UnknownFunction = (...args: Array) => unknown; +export type UnknownClass = {new (...args: Array): unknown}; + +export type SpiedClass = MockInstance< + (...args: ConstructorParameters) => InstanceType +>; + +export type SpiedFunction = + MockInstance<(...args: Parameters) => ReturnType>; + +export type SpiedGetter = MockInstance<() => T>; + +export type SpiedSetter = MockInstance<(arg: T) => void>; + +export type Spied = T extends ClassLike + ? MockInstance<(...args: ConstructorParameters) => InstanceType> + : T extends FunctionLike + ? MockInstance<(...args: Parameters) => ReturnType> + : never; + +// TODO in Jest 30 remove `SpyInstance` in favour of `Spied` +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SpyInstance + extends MockInstance {} /** * All what the internal typings need is to be sure that we have any-function. @@ -149,10 +172,6 @@ export interface MockInstance { mockRejectedValueOnce(value: RejectType): this; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface SpyInstance - extends MockInstance {} - type MockFunctionResultIncomplete = { type: 'incomplete'; /** @@ -1080,10 +1099,9 @@ export class ModuleMocker { } isMockFunction( - fn: SpyInstance, - ): fn is SpyInstance; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint - isMockFunction

, R extends unknown>( + fn: MockInstance, + ): fn is MockInstance; + isMockFunction

, R>( fn: (...args: P) => R, ): fn is Mock<(...args: P) => R>; isMockFunction(fn: unknown): fn is Mock; @@ -1107,35 +1125,25 @@ export class ModuleMocker { T extends object, K extends PropertyLikeKeys, V extends Required[K], - >(object: T, methodKey: K, accessType: 'get'): SpyInstance<() => V>; - - spyOn< - T extends object, - K extends PropertyLikeKeys, - V extends Required[K], - >(object: T, methodKey: K, accessType: 'set'): SpyInstance<(arg: V) => void>; - - spyOn< - T extends object, - K extends ConstructorLikeKeys, - V extends Required[K], + A extends 'get' | 'set', >( object: T, methodKey: K, - ): V extends ClassLike - ? SpyInstance<(...args: ConstructorParameters) => InstanceType> + accessType: A, + ): A extends 'get' + ? SpiedGetter + : A extends 'set' + ? SpiedSetter : never; spyOn< T extends object, - K extends MethodLikeKeys, + K extends ConstructorLikeKeys | MethodLikeKeys, V extends Required[K], >( object: T, methodKey: K, - ): V extends FunctionLike - ? SpyInstance<(...args: Parameters) => ReturnType> - : never; + ): V extends ClassLike | FunctionLike ? Spied : never; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types spyOn>( diff --git a/packages/jest-types/__typetests__/jest.test.ts b/packages/jest-types/__typetests__/jest.test.ts index e6a9cc895e1f..ad5717d43645 100644 --- a/packages/jest-types/__typetests__/jest.test.ts +++ b/packages/jest-types/__typetests__/jest.test.ts @@ -9,13 +9,13 @@ import {expectAssignable, expectError, expectType} from 'tsd-lite'; import {jest} from '@jest/globals'; import type { Mock, + MockInstance, Mocked, MockedClass, MockedFunction, MockedObject, MockedShallow, ModuleMocker, - SpyInstance, } from 'jest-mock'; expectType( @@ -216,7 +216,7 @@ const spiedObject = { const surelySpy = jest.spyOn(spiedObject, 'methodA'); if (jest.isMockFunction(surelySpy)) { - expectType boolean>>(surelySpy); + expectType boolean>>(surelySpy); surelySpy.mockReturnValueOnce(false); expectError(surelyMock.mockReturnValueOnce(123)); @@ -287,6 +287,8 @@ function someFunction(a: string, b?: number): boolean { const someObject = { SomeClass, + _propertyC: false, + methodA() { return; }, @@ -304,8 +306,16 @@ const someObject = { }, propertyA: 123, + propertyB: 'value', + set propertyC(value) { + this._propertyC = value; + }, + get propertyC() { + return this._propertyC; + }, + someClassInstance: new SomeClass('value'), }; @@ -433,6 +443,34 @@ expectError( expectAssignable(mockObjectB); +// Spied + +expectAssignable>( + jest.spyOn(someObject, 'methodA'), +); + +expectAssignable>( + jest.spyOn(someObject, 'SomeClass'), +); + +// Spied* + +expectAssignable>( + jest.spyOn(someObject, 'SomeClass'), +); + +expectAssignable>( + jest.spyOn(someObject, 'methodB'), +); + +expectAssignable>( + jest.spyOn(someObject, 'propertyC', 'get'), +); + +expectAssignable>( + jest.spyOn(someObject, 'propertyC', 'set'), +); + // Mock Timers expectType(jest.advanceTimersByTime(6000)); diff --git a/packages/test-globals/src/index.ts b/packages/test-globals/src/index.ts index 94eedd3e3520..ddeaf6e1b7a0 100644 --- a/packages/test-globals/src/index.ts +++ b/packages/test-globals/src/index.ts @@ -16,6 +16,11 @@ import type { MockedClass as JestMockedClass, MockedFunction as JestMockedFunction, MockedObject as JestMockedObject, + Spied as JestSpied, + SpiedClass as JestSpiedClass, + SpiedFunction as JestSpiedFunction, + SpiedGetter as JestSpiedGetter, + SpiedSetter as JestSpiedSetter, UnknownFunction, } from 'jest-mock'; @@ -63,5 +68,25 @@ declare global { * Wraps an object type with Jest mock type definitions. */ export type MockedObject = JestMockedObject; + /** + * Constructs the type of a spied class or function. + */ + export type Spied = JestSpied; + /** + * Constructs the type of a spied class. + */ + export type SpiedClass = JestSpiedClass; + /** + * Constructs the type of a spied function. + */ + export type SpiedFunction = JestSpiedFunction; + /** + * Constructs the type of a spied getter. + */ + export type SpiedGetter = JestSpiedGetter; + /** + * Constructs the type of a spied setter. + */ + export type SpiedSetter = JestSpiedSetter; } }