Skip to content

Commit faef42e

Browse files
authored
feat(@jest/globals, jest-mock): add jest.Spied* utility types (#13440)
1 parent 1f6c4e9 commit faef42e

File tree

8 files changed

+206
-60
lines changed

8 files changed

+206
-60
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
### Features
44

5+
- `[@jest/globals, jest-mock]` Add `jest.Spied*` utility types ([#13440](https://github.com/facebook/jest/pull/13440))
6+
57
### Fixes
68

79
### Chore & Maintenance

docs/JestObjectAPI.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,10 @@ test('plays audio', () => {
699699
});
700700
```
701701

702+
### `jest.Spied<Source>`
703+
704+
See [TypeScript Usage](MockFunctionAPI.md#jestspiedsource) chapter of Mock Functions page for documentation.
705+
702706
### `jest.clearAllMocks()`
703707

704708
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.

docs/MockFunctionAPI.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,3 +644,37 @@ test('direct usage', () => {
644644
expect(jest.mocked(console.log).mock.calls).toHaveLength(1);
645645
});
646646
```
647+
648+
### `jest.Spied<Source>`
649+
650+
Constructs the type of a spied class or function (i.e. the return type of `jest.spyOn()`).
651+
652+
```ts title="__utils__/setDateNow.ts"
653+
import {jest} from '@jest/globals';
654+
655+
export function setDateNow(now: number): jest.Spied<typeof Date.now> {
656+
return jest.spyOn(Date, 'now').mockReturnValue(now);
657+
}
658+
```
659+
660+
```ts
661+
import {afterEach, expect, jest, test} from '@jest/globals';
662+
import {setDateNow} from './__utils__/setDateNow';
663+
664+
let spiedDateNow: jest.Spied<typeof Date.now> | undefined = undefined;
665+
666+
afterEach(() => {
667+
spiedDateNow?.mockReset();
668+
});
669+
670+
test('renders correctly with a given date', () => {
671+
spiedDateNow = setDateNow(1482363367071);
672+
// ...
673+
674+
expect(spiedDateNow).toHaveBeenCalledTimes(1);
675+
});
676+
```
677+
678+
Types of a class or function can be passed as type argument to `jest.Spied<Source>`. If you prefer to constrain the input type, use: `jest.SpiedClass<Source>` or `jest.SpiedFunction<Source>`.
679+
680+
Use `jest.SpiedGetter<Source>` or `jest.SpiedSetter<Source>` to create the type of a spied getter or setter respectively.

packages/jest-globals/src/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ import type {
1616
MockedClass as JestMockedClass,
1717
MockedFunction as JestMockedFunction,
1818
MockedObject as JestMockedObject,
19+
Spied as JestSpied,
20+
SpiedClass as JestSpiedClass,
21+
SpiedFunction as JestSpiedFunction,
22+
SpiedGetter as JestSpiedGetter,
23+
SpiedSetter as JestSpiedSetter,
1924
UnknownFunction,
2025
} from 'jest-mock';
2126

@@ -58,6 +63,26 @@ declare namespace jest {
5863
* Wraps an object type with Jest mock type definitions.
5964
*/
6065
export type MockedObject<T extends object> = JestMockedObject<T>;
66+
/**
67+
* Constructs the type of a spied class or function.
68+
*/
69+
export type Spied<T extends ClassLike | FunctionLike> = JestSpied<T>;
70+
/**
71+
* Constructs the type of a spied class.
72+
*/
73+
export type SpiedClass<T extends ClassLike> = JestSpiedClass<T>;
74+
/**
75+
* Constructs the type of a spied function.
76+
*/
77+
export type SpiedFunction<T extends FunctionLike> = JestSpiedFunction<T>;
78+
/**
79+
* Constructs the type of a spied getter.
80+
*/
81+
export type SpiedGetter<T> = JestSpiedGetter<T>;
82+
/**
83+
* Constructs the type of a spied setter.
84+
*/
85+
export type SpiedSetter<T> = JestSpiedSetter<T>;
6186
}
6287

6388
export {jest};

packages/jest-mock/__typetests__/mock-functions.test.ts

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,15 @@ import {
1111
expectNotAssignable,
1212
expectType,
1313
} from 'tsd-lite';
14-
import {Mock, SpyInstance, fn, spyOn} from 'jest-mock';
14+
import {
15+
Mock,
16+
SpiedClass,
17+
SpiedFunction,
18+
SpiedGetter,
19+
SpiedSetter,
20+
fn,
21+
spyOn,
22+
} from 'jest-mock';
1523

1624
// jest.fn()
1725

@@ -320,26 +328,30 @@ expectNotAssignable<Function>(spy); // eslint-disable-line @typescript-eslint/ba
320328
expectError(spy());
321329
expectError(new spy());
322330

323-
expectType<SpyInstance<typeof spiedObject.methodA>>(
331+
expectType<SpiedFunction<typeof spiedObject.methodA>>(
324332
spyOn(spiedObject, 'methodA'),
325333
);
326-
expectType<SpyInstance<typeof spiedObject.methodB>>(
334+
expectType<SpiedFunction<typeof spiedObject.methodB>>(
327335
spyOn(spiedObject, 'methodB'),
328336
);
329-
expectType<SpyInstance<typeof spiedObject.methodC>>(
337+
expectType<SpiedFunction<typeof spiedObject.methodC>>(
330338
spyOn(spiedObject, 'methodC'),
331339
);
332340

333-
expectType<SpyInstance<() => boolean>>(spyOn(spiedObject, 'propertyB', 'get'));
334-
expectType<SpyInstance<(value: boolean) => void>>(
341+
expectType<SpiedGetter<typeof spiedObject.propertyB>>(
342+
spyOn(spiedObject, 'propertyB', 'get'),
343+
);
344+
expectType<SpiedSetter<typeof spiedObject.propertyB>>(
335345
spyOn(spiedObject, 'propertyB', 'set'),
336346
);
337347
expectError(spyOn(spiedObject, 'propertyB'));
338348
expectError(spyOn(spiedObject, 'methodB', 'get'));
339349
expectError(spyOn(spiedObject, 'methodB', 'set'));
340350

341-
expectType<SpyInstance<() => string>>(spyOn(spiedObject, 'propertyA', 'get'));
342-
expectType<SpyInstance<(value: string) => void>>(
351+
expectType<SpiedGetter<typeof spiedObject.propertyA>>(
352+
spyOn(spiedObject, 'propertyA', 'get'),
353+
);
354+
expectType<SpiedSetter<typeof spiedObject.propertyA>>(
343355
spyOn(spiedObject, 'propertyA', 'set'),
344356
);
345357
expectError(spyOn(spiedObject, 'propertyA'));
@@ -351,40 +363,38 @@ expectError(spyOn(true, 'methodA'));
351363
expectError(spyOn(spiedObject));
352364
expectError(spyOn());
353365

354-
expectType<SpyInstance<(arg: any) => boolean>>(
366+
expectType<SpiedFunction<typeof Array.isArray>>(
355367
spyOn(spiedArray as unknown as ArrayConstructor, 'isArray'),
356368
);
357369
expectError(spyOn(spiedArray, 'isArray'));
358370

359-
expectType<SpyInstance<() => string>>(
371+
expectType<SpiedFunction<typeof spiedFunction.toString>>(
360372
spyOn(spiedFunction as unknown as Function, 'toString'), // eslint-disable-line @typescript-eslint/ban-types
361373
);
362374
expectError(spyOn(spiedFunction, 'toString'));
363375

364-
expectType<SpyInstance<(value: string | number | Date) => Date>>(
365-
spyOn(globalThis, 'Date'),
366-
);
367-
expectType<SpyInstance<() => number>>(spyOn(Date, 'now'));
376+
expectType<SpiedClass<typeof Date>>(spyOn(globalThis, 'Date'));
377+
expectType<SpiedFunction<typeof Date.now>>(spyOn(Date, 'now'));
368378

369379
// object with index signatures
370380

371-
expectType<SpyInstance<typeof indexSpiedObject.methodA>>(
381+
expectType<SpiedFunction<typeof indexSpiedObject.methodA>>(
372382
spyOn(indexSpiedObject, 'methodA'),
373383
);
374-
expectType<SpyInstance<typeof indexSpiedObject.methodB>>(
384+
expectType<SpiedFunction<typeof indexSpiedObject.methodB>>(
375385
spyOn(indexSpiedObject, 'methodB'),
376386
);
377-
expectType<SpyInstance<typeof indexSpiedObject.methodC>>(
387+
expectType<SpiedFunction<typeof indexSpiedObject.methodC>>(
378388
spyOn(indexSpiedObject, 'methodC'),
379389
);
380-
expectType<SpyInstance<typeof indexSpiedObject.methodE>>(
390+
expectType<SpiedFunction<typeof indexSpiedObject.methodE>>(
381391
spyOn(indexSpiedObject, 'methodE'),
382392
);
383393

384-
expectType<SpyInstance<() => {a: string}>>(
394+
expectType<SpiedGetter<typeof indexSpiedObject.propertyA>>(
385395
spyOn(indexSpiedObject, 'propertyA', 'get'),
386396
);
387-
expectType<SpyInstance<(value: {a: string}) => void>>(
397+
expectType<SpiedSetter<typeof indexSpiedObject.propertyA>>(
388398
spyOn(indexSpiedObject, 'propertyA', 'set'),
389399
);
390400
expectError(spyOn(indexSpiedObject, 'propertyA'));
@@ -419,48 +429,48 @@ interface OptionalInterface {
419429

420430
const optionalSpiedObject = {} as OptionalInterface;
421431

422-
expectType<SpyInstance<(one: string) => SomeClass>>(
432+
expectType<SpiedClass<NonNullable<typeof optionalSpiedObject.constructorA>>>(
423433
spyOn(optionalSpiedObject, 'constructorA'),
424434
);
425-
expectType<SpyInstance<(one: string, two: boolean) => SomeClass>>(
435+
expectType<SpiedClass<typeof optionalSpiedObject.constructorB>>(
426436
spyOn(optionalSpiedObject, 'constructorB'),
427437
);
428438

429439
expectError(spyOn(optionalSpiedObject, 'constructorA', 'get'));
430440
expectError(spyOn(optionalSpiedObject, 'constructorA', 'set'));
431441

432-
expectType<SpyInstance<(a: boolean) => void>>(
442+
expectType<SpiedFunction<NonNullable<typeof optionalSpiedObject.methodA>>>(
433443
spyOn(optionalSpiedObject, 'methodA'),
434444
);
435-
expectType<SpyInstance<(b: string) => boolean>>(
445+
expectType<SpiedFunction<typeof optionalSpiedObject.methodB>>(
436446
spyOn(optionalSpiedObject, 'methodB'),
437447
);
438448

439449
expectError(spyOn(optionalSpiedObject, 'methodA', 'get'));
440450
expectError(spyOn(optionalSpiedObject, 'methodA', 'set'));
441451

442-
expectType<SpyInstance<() => number>>(
452+
expectType<SpiedGetter<NonNullable<typeof optionalSpiedObject.propertyA>>>(
443453
spyOn(optionalSpiedObject, 'propertyA', 'get'),
444454
);
445-
expectType<SpyInstance<(arg: number) => void>>(
455+
expectType<SpiedSetter<NonNullable<typeof optionalSpiedObject.propertyA>>>(
446456
spyOn(optionalSpiedObject, 'propertyA', 'set'),
447457
);
448-
expectType<SpyInstance<() => number>>(
458+
expectType<SpiedGetter<NonNullable<typeof optionalSpiedObject.propertyB>>>(
449459
spyOn(optionalSpiedObject, 'propertyB', 'get'),
450460
);
451-
expectType<SpyInstance<(arg: number) => void>>(
461+
expectType<SpiedSetter<NonNullable<typeof optionalSpiedObject.propertyB>>>(
452462
spyOn(optionalSpiedObject, 'propertyB', 'set'),
453463
);
454-
expectType<SpyInstance<() => number | undefined>>(
464+
expectType<SpiedGetter<typeof optionalSpiedObject.propertyC>>(
455465
spyOn(optionalSpiedObject, 'propertyC', 'get'),
456466
);
457-
expectType<SpyInstance<(arg: number | undefined) => void>>(
467+
expectType<SpiedSetter<typeof optionalSpiedObject.propertyC>>(
458468
spyOn(optionalSpiedObject, 'propertyC', 'set'),
459469
);
460-
expectType<SpyInstance<() => string>>(
470+
expectType<SpiedGetter<typeof optionalSpiedObject.propertyD>>(
461471
spyOn(optionalSpiedObject, 'propertyD', 'get'),
462472
);
463-
expectType<SpyInstance<(arg: string) => void>>(
473+
expectType<SpiedSetter<typeof optionalSpiedObject.propertyD>>(
464474
spyOn(optionalSpiedObject, 'propertyD', 'set'),
465475
);
466476

packages/jest-mock/src/index.ts

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,29 @@ export type MockedShallow<T> = T extends ClassLike
100100
: T;
101101

102102
export type UnknownFunction = (...args: Array<unknown>) => unknown;
103+
export type UnknownClass = {new (...args: Array<unknown>): unknown};
104+
105+
export type SpiedClass<T extends ClassLike = UnknownClass> = MockInstance<
106+
(...args: ConstructorParameters<T>) => InstanceType<T>
107+
>;
108+
109+
export type SpiedFunction<T extends FunctionLike = UnknownFunction> =
110+
MockInstance<(...args: Parameters<T>) => ReturnType<T>>;
111+
112+
export type SpiedGetter<T> = MockInstance<() => T>;
113+
114+
export type SpiedSetter<T> = MockInstance<(arg: T) => void>;
115+
116+
export type Spied<T extends ClassLike | FunctionLike> = T extends ClassLike
117+
? MockInstance<(...args: ConstructorParameters<T>) => InstanceType<T>>
118+
: T extends FunctionLike
119+
? MockInstance<(...args: Parameters<T>) => ReturnType<T>>
120+
: never;
121+
122+
// TODO in Jest 30 remove `SpyInstance` in favour of `Spied`
123+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
124+
export interface SpyInstance<T extends FunctionLike = UnknownFunction>
125+
extends MockInstance<T> {}
103126

104127
/**
105128
* All what the internal typings need is to be sure that we have any-function.
@@ -149,10 +172,6 @@ export interface MockInstance<T extends FunctionLike = UnknownFunction> {
149172
mockRejectedValueOnce(value: RejectType<T>): this;
150173
}
151174

152-
// eslint-disable-next-line @typescript-eslint/no-empty-interface
153-
export interface SpyInstance<T extends FunctionLike = UnknownFunction>
154-
extends MockInstance<T> {}
155-
156175
type MockFunctionResultIncomplete = {
157176
type: 'incomplete';
158177
/**
@@ -1080,10 +1099,9 @@ export class ModuleMocker {
10801099
}
10811100

10821101
isMockFunction<T extends FunctionLike = UnknownFunction>(
1083-
fn: SpyInstance<T>,
1084-
): fn is SpyInstance<T>;
1085-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
1086-
isMockFunction<P extends Array<unknown>, R extends unknown>(
1102+
fn: MockInstance<T>,
1103+
): fn is MockInstance<T>;
1104+
isMockFunction<P extends Array<unknown>, R>(
10871105
fn: (...args: P) => R,
10881106
): fn is Mock<(...args: P) => R>;
10891107
isMockFunction(fn: unknown): fn is Mock<UnknownFunction>;
@@ -1107,35 +1125,25 @@ export class ModuleMocker {
11071125
T extends object,
11081126
K extends PropertyLikeKeys<T>,
11091127
V extends Required<T>[K],
1110-
>(object: T, methodKey: K, accessType: 'get'): SpyInstance<() => V>;
1111-
1112-
spyOn<
1113-
T extends object,
1114-
K extends PropertyLikeKeys<T>,
1115-
V extends Required<T>[K],
1116-
>(object: T, methodKey: K, accessType: 'set'): SpyInstance<(arg: V) => void>;
1117-
1118-
spyOn<
1119-
T extends object,
1120-
K extends ConstructorLikeKeys<T>,
1121-
V extends Required<T>[K],
1128+
A extends 'get' | 'set',
11221129
>(
11231130
object: T,
11241131
methodKey: K,
1125-
): V extends ClassLike
1126-
? SpyInstance<(...args: ConstructorParameters<V>) => InstanceType<V>>
1132+
accessType: A,
1133+
): A extends 'get'
1134+
? SpiedGetter<V>
1135+
: A extends 'set'
1136+
? SpiedSetter<V>
11271137
: never;
11281138

11291139
spyOn<
11301140
T extends object,
1131-
K extends MethodLikeKeys<T>,
1141+
K extends ConstructorLikeKeys<T> | MethodLikeKeys<T>,
11321142
V extends Required<T>[K],
11331143
>(
11341144
object: T,
11351145
methodKey: K,
1136-
): V extends FunctionLike
1137-
? SpyInstance<(...args: Parameters<V>) => ReturnType<V>>
1138-
: never;
1146+
): V extends ClassLike | FunctionLike ? Spied<V> : never;
11391147

11401148
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
11411149
spyOn<T extends object, K extends PropertyLikeKeys<T>>(

0 commit comments

Comments
 (0)