Skip to content

Commit

Permalink
feat(@jest/globals, jest-mock): add jest.Spied* utility types (#13440)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrazauskas committed Oct 16, 2022
1 parent 1f6c4e9 commit faef42e
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 60 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions docs/JestObjectAPI.md
Expand Up @@ -699,6 +699,10 @@ test('plays audio', () => {
});
```

### `jest.Spied<Source>`

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.
Expand Down
34 changes: 34 additions & 0 deletions docs/MockFunctionAPI.md
Expand Up @@ -644,3 +644,37 @@ test('direct usage', () => {
expect(jest.mocked(console.log).mock.calls).toHaveLength(1);
});
```

### `jest.Spied<Source>`

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<typeof Date.now> {
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<typeof Date.now> | 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<Source>`. If you prefer to constrain the input type, use: `jest.SpiedClass<Source>` or `jest.SpiedFunction<Source>`.

Use `jest.SpiedGetter<Source>` or `jest.SpiedSetter<Source>` to create the type of a spied getter or setter respectively.
25 changes: 25 additions & 0 deletions packages/jest-globals/src/index.ts
Expand Up @@ -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';

Expand Down Expand Up @@ -58,6 +63,26 @@ declare namespace jest {
* Wraps an object type with Jest mock type definitions.
*/
export type MockedObject<T extends object> = JestMockedObject<T>;
/**
* Constructs the type of a spied class or function.
*/
export type Spied<T extends ClassLike | FunctionLike> = JestSpied<T>;
/**
* Constructs the type of a spied class.
*/
export type SpiedClass<T extends ClassLike> = JestSpiedClass<T>;
/**
* Constructs the type of a spied function.
*/
export type SpiedFunction<T extends FunctionLike> = JestSpiedFunction<T>;
/**
* Constructs the type of a spied getter.
*/
export type SpiedGetter<T> = JestSpiedGetter<T>;
/**
* Constructs the type of a spied setter.
*/
export type SpiedSetter<T> = JestSpiedSetter<T>;
}

export {jest};
Expand Down
74 changes: 42 additions & 32 deletions packages/jest-mock/__typetests__/mock-functions.test.ts
Expand Up @@ -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()

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

expectType<SpyInstance<typeof spiedObject.methodA>>(
expectType<SpiedFunction<typeof spiedObject.methodA>>(
spyOn(spiedObject, 'methodA'),
);
expectType<SpyInstance<typeof spiedObject.methodB>>(
expectType<SpiedFunction<typeof spiedObject.methodB>>(
spyOn(spiedObject, 'methodB'),
);
expectType<SpyInstance<typeof spiedObject.methodC>>(
expectType<SpiedFunction<typeof spiedObject.methodC>>(
spyOn(spiedObject, 'methodC'),
);

expectType<SpyInstance<() => boolean>>(spyOn(spiedObject, 'propertyB', 'get'));
expectType<SpyInstance<(value: boolean) => void>>(
expectType<SpiedGetter<typeof spiedObject.propertyB>>(
spyOn(spiedObject, 'propertyB', 'get'),
);
expectType<SpiedSetter<typeof spiedObject.propertyB>>(
spyOn(spiedObject, 'propertyB', 'set'),
);
expectError(spyOn(spiedObject, 'propertyB'));
expectError(spyOn(spiedObject, 'methodB', 'get'));
expectError(spyOn(spiedObject, 'methodB', 'set'));

expectType<SpyInstance<() => string>>(spyOn(spiedObject, 'propertyA', 'get'));
expectType<SpyInstance<(value: string) => void>>(
expectType<SpiedGetter<typeof spiedObject.propertyA>>(
spyOn(spiedObject, 'propertyA', 'get'),
);
expectType<SpiedSetter<typeof spiedObject.propertyA>>(
spyOn(spiedObject, 'propertyA', 'set'),
);
expectError(spyOn(spiedObject, 'propertyA'));
Expand All @@ -351,40 +363,38 @@ expectError(spyOn(true, 'methodA'));
expectError(spyOn(spiedObject));
expectError(spyOn());

expectType<SpyInstance<(arg: any) => boolean>>(
expectType<SpiedFunction<typeof Array.isArray>>(
spyOn(spiedArray as unknown as ArrayConstructor, 'isArray'),
);
expectError(spyOn(spiedArray, 'isArray'));

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

expectType<SpyInstance<(value: string | number | Date) => Date>>(
spyOn(globalThis, 'Date'),
);
expectType<SpyInstance<() => number>>(spyOn(Date, 'now'));
expectType<SpiedClass<typeof Date>>(spyOn(globalThis, 'Date'));
expectType<SpiedFunction<typeof Date.now>>(spyOn(Date, 'now'));

// object with index signatures

expectType<SpyInstance<typeof indexSpiedObject.methodA>>(
expectType<SpiedFunction<typeof indexSpiedObject.methodA>>(
spyOn(indexSpiedObject, 'methodA'),
);
expectType<SpyInstance<typeof indexSpiedObject.methodB>>(
expectType<SpiedFunction<typeof indexSpiedObject.methodB>>(
spyOn(indexSpiedObject, 'methodB'),
);
expectType<SpyInstance<typeof indexSpiedObject.methodC>>(
expectType<SpiedFunction<typeof indexSpiedObject.methodC>>(
spyOn(indexSpiedObject, 'methodC'),
);
expectType<SpyInstance<typeof indexSpiedObject.methodE>>(
expectType<SpiedFunction<typeof indexSpiedObject.methodE>>(
spyOn(indexSpiedObject, 'methodE'),
);

expectType<SpyInstance<() => {a: string}>>(
expectType<SpiedGetter<typeof indexSpiedObject.propertyA>>(
spyOn(indexSpiedObject, 'propertyA', 'get'),
);
expectType<SpyInstance<(value: {a: string}) => void>>(
expectType<SpiedSetter<typeof indexSpiedObject.propertyA>>(
spyOn(indexSpiedObject, 'propertyA', 'set'),
);
expectError(spyOn(indexSpiedObject, 'propertyA'));
Expand Down Expand Up @@ -419,48 +429,48 @@ interface OptionalInterface {

const optionalSpiedObject = {} as OptionalInterface;

expectType<SpyInstance<(one: string) => SomeClass>>(
expectType<SpiedClass<NonNullable<typeof optionalSpiedObject.constructorA>>>(
spyOn(optionalSpiedObject, 'constructorA'),
);
expectType<SpyInstance<(one: string, two: boolean) => SomeClass>>(
expectType<SpiedClass<typeof optionalSpiedObject.constructorB>>(
spyOn(optionalSpiedObject, 'constructorB'),
);

expectError(spyOn(optionalSpiedObject, 'constructorA', 'get'));
expectError(spyOn(optionalSpiedObject, 'constructorA', 'set'));

expectType<SpyInstance<(a: boolean) => void>>(
expectType<SpiedFunction<NonNullable<typeof optionalSpiedObject.methodA>>>(
spyOn(optionalSpiedObject, 'methodA'),
);
expectType<SpyInstance<(b: string) => boolean>>(
expectType<SpiedFunction<typeof optionalSpiedObject.methodB>>(
spyOn(optionalSpiedObject, 'methodB'),
);

expectError(spyOn(optionalSpiedObject, 'methodA', 'get'));
expectError(spyOn(optionalSpiedObject, 'methodA', 'set'));

expectType<SpyInstance<() => number>>(
expectType<SpiedGetter<NonNullable<typeof optionalSpiedObject.propertyA>>>(
spyOn(optionalSpiedObject, 'propertyA', 'get'),
);
expectType<SpyInstance<(arg: number) => void>>(
expectType<SpiedSetter<NonNullable<typeof optionalSpiedObject.propertyA>>>(
spyOn(optionalSpiedObject, 'propertyA', 'set'),
);
expectType<SpyInstance<() => number>>(
expectType<SpiedGetter<NonNullable<typeof optionalSpiedObject.propertyB>>>(
spyOn(optionalSpiedObject, 'propertyB', 'get'),
);
expectType<SpyInstance<(arg: number) => void>>(
expectType<SpiedSetter<NonNullable<typeof optionalSpiedObject.propertyB>>>(
spyOn(optionalSpiedObject, 'propertyB', 'set'),
);
expectType<SpyInstance<() => number | undefined>>(
expectType<SpiedGetter<typeof optionalSpiedObject.propertyC>>(
spyOn(optionalSpiedObject, 'propertyC', 'get'),
);
expectType<SpyInstance<(arg: number | undefined) => void>>(
expectType<SpiedSetter<typeof optionalSpiedObject.propertyC>>(
spyOn(optionalSpiedObject, 'propertyC', 'set'),
);
expectType<SpyInstance<() => string>>(
expectType<SpiedGetter<typeof optionalSpiedObject.propertyD>>(
spyOn(optionalSpiedObject, 'propertyD', 'get'),
);
expectType<SpyInstance<(arg: string) => void>>(
expectType<SpiedSetter<typeof optionalSpiedObject.propertyD>>(
spyOn(optionalSpiedObject, 'propertyD', 'set'),
);

Expand Down
60 changes: 34 additions & 26 deletions packages/jest-mock/src/index.ts
Expand Up @@ -100,6 +100,29 @@ export type MockedShallow<T> = T extends ClassLike
: T;

export type UnknownFunction = (...args: Array<unknown>) => unknown;
export type UnknownClass = {new (...args: Array<unknown>): unknown};

export type SpiedClass<T extends ClassLike = UnknownClass> = MockInstance<
(...args: ConstructorParameters<T>) => InstanceType<T>
>;

export type SpiedFunction<T extends FunctionLike = UnknownFunction> =
MockInstance<(...args: Parameters<T>) => ReturnType<T>>;

export type SpiedGetter<T> = MockInstance<() => T>;

export type SpiedSetter<T> = MockInstance<(arg: T) => void>;

export type Spied<T extends ClassLike | FunctionLike> = T extends ClassLike
? MockInstance<(...args: ConstructorParameters<T>) => InstanceType<T>>
: T extends FunctionLike
? MockInstance<(...args: Parameters<T>) => ReturnType<T>>
: never;

// TODO in Jest 30 remove `SpyInstance` in favour of `Spied`
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface SpyInstance<T extends FunctionLike = UnknownFunction>
extends MockInstance<T> {}

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

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface SpyInstance<T extends FunctionLike = UnknownFunction>
extends MockInstance<T> {}

type MockFunctionResultIncomplete = {
type: 'incomplete';
/**
Expand Down Expand Up @@ -1080,10 +1099,9 @@ export class ModuleMocker {
}

isMockFunction<T extends FunctionLike = UnknownFunction>(
fn: SpyInstance<T>,
): fn is SpyInstance<T>;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
isMockFunction<P extends Array<unknown>, R extends unknown>(
fn: MockInstance<T>,
): fn is MockInstance<T>;
isMockFunction<P extends Array<unknown>, R>(
fn: (...args: P) => R,
): fn is Mock<(...args: P) => R>;
isMockFunction(fn: unknown): fn is Mock<UnknownFunction>;
Expand All @@ -1107,35 +1125,25 @@ export class ModuleMocker {
T extends object,
K extends PropertyLikeKeys<T>,
V extends Required<T>[K],
>(object: T, methodKey: K, accessType: 'get'): SpyInstance<() => V>;

spyOn<
T extends object,
K extends PropertyLikeKeys<T>,
V extends Required<T>[K],
>(object: T, methodKey: K, accessType: 'set'): SpyInstance<(arg: V) => void>;

spyOn<
T extends object,
K extends ConstructorLikeKeys<T>,
V extends Required<T>[K],
A extends 'get' | 'set',
>(
object: T,
methodKey: K,
): V extends ClassLike
? SpyInstance<(...args: ConstructorParameters<V>) => InstanceType<V>>
accessType: A,
): A extends 'get'
? SpiedGetter<V>
: A extends 'set'
? SpiedSetter<V>
: never;

spyOn<
T extends object,
K extends MethodLikeKeys<T>,
K extends ConstructorLikeKeys<T> | MethodLikeKeys<T>,
V extends Required<T>[K],
>(
object: T,
methodKey: K,
): V extends FunctionLike
? SpyInstance<(...args: Parameters<V>) => ReturnType<V>>
: never;
): V extends ClassLike | FunctionLike ? Spied<V> : never;

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

0 comments on commit faef42e

Please sign in to comment.