Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(@jest/globals, jest-mock): add jest.Spied* utility types #13440

Merged
merged 14 commits into from Oct 16, 2022
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.
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved

### `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"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I am happy about the example. The point was to show that typings can be useful in some test util.

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>;

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