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

chore: extend and improve type tests for jest object #12442

Merged
merged 9 commits into from Feb 21, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 5 additions & 12 deletions packages/jest-environment/src/index.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand All @@ -132,9 +127,7 @@ export interface Jest {
/**
* Determines if the given function is a mocked function.
*/
isMockFunction(
fn: (...args: Array<any>) => unknown,
): fn is ReturnType<typeof JestMockFn>;
isMockFunction: ModuleMocker['isMockFunction'];
/**
* Mocks a module with an auto-mocked version when it is being required.
*/
Expand Down Expand Up @@ -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`!
Expand Down Expand Up @@ -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
Expand Down
65 changes: 45 additions & 20 deletions packages/jest-mock/src/index.ts
Expand Up @@ -32,6 +32,14 @@ export type MockFunctionMetadata<
length?: number;
};

type ClassLike = {
new (...args: Array<any>): any;
};

type ObjectLike = {
[key: string]: any;
};

export type MockableFunction = (...args: Array<any>) => any;
export type MethodKeysOf<T> = {
[K in keyof T]: T[K] extends MockableFunction ? K : never;
Expand Down Expand Up @@ -79,6 +87,9 @@ export type MaybeMocked<T> = T extends MockableFunction
: T;

export type ArgsType<T> = T extends (...args: infer A) => any ? A : never;
type ConstructorArgsType<T> = T extends new (...args: infer A) => any
? A
: never;
export type Mocked<T> = {
[P in keyof T]: T[P] extends (...args: Array<any>) => any
? MockInstance<ReturnType<T[P]>, ArgsType<T[P]>>
Expand Down Expand Up @@ -192,6 +203,10 @@ type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends (...args: Array<any>) => any ? K : never;
}[keyof T] &
string;
type ConstructorPropertyNames<T> = {
[K in keyof T]: T[K] extends new (...args: Array<any>) => any ? K : never;
}[keyof T] &
string;

const MOCK_CONSTRUCTOR_NAME = 'mockConstructor';

Expand Down Expand Up @@ -988,6 +1003,10 @@ export class ModuleMocker {
return metadata;
}

isMockFunction<T, Y extends Array<unknown> = Array<unknown>>(
fn: (...args: Y) => T,
): fn is Mock<T, Y>;
isMockFunction(fn: unknown): boolean;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The first overload will infer return type and types of args if a function is passed. Second one allows to pass any value. This way types do the same job as before, but with added feature.

Copy link
Member

Choose a reason for hiding this comment

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

should second be isMockFunction(fn: unknown): fn is Mock<unknown, unknown> instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this case, if string is passed the typed in if (isMockFunction(someString)) looks like this:

Screenshot 2022-02-21 at 07 39 48

No harm at all, because this branch will not execute anyway. Simply seemed unnecessary. So boolean looked like cleaner solution.

Copy link
Member

Choose a reason for hiding this comment

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

what if hardlyMock is any or unknown?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These are good edge cases. I added tests

Copy link
Member

@SimenB SimenB Feb 21, 2022

Choose a reason for hiding this comment

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

right, and then isMockFunction(fn: unknown): fn is Mock<unknown, unknown> still doesn't help in setting the correct type in the if?

isMockFunction<T>(fn: unknown): fn is Mock<T> {
return !!fn && (fn as any)._isMockFunction === true;
}
Expand All @@ -1003,31 +1022,38 @@ export class ModuleMocker {
return fn;
}

spyOn<T extends {}, M extends NonFunctionPropertyNames<T>>(
object: T,
methodName: M,
accessType: 'get',
): SpyInstance<T[M], []>;
spyOn<
T extends ClassLike | ObjectLike,
M extends NonFunctionPropertyNames<T>,
>(object: T, methodName: M, accessType: 'get'): SpyInstance<T[M], []>;

spyOn<T extends {}, M extends NonFunctionPropertyNames<T>>(
spyOn<
T extends ClassLike | ObjectLike,
M extends NonFunctionPropertyNames<T>,
>(object: T, methodName: M, accessType: 'set'): SpyInstance<void, [T[M]]>;

spyOn<
T extends ClassLike | ObjectLike,
M extends ConstructorPropertyNames<Required<T>>,
>(
object: T,
methodName: M,
accessType: 'set',
): SpyInstance<void, [T[M]]>;
method: M,
): T[M] extends new (...args: Array<any>) => any
? SpyInstance<InstanceType<T[M]>, ConstructorArgsType<T[M]>>
: never;

spyOn<T extends {}, M extends FunctionPropertyNames<T>>(
spyOn<T extends ClassLike | ObjectLike, M extends FunctionPropertyNames<T>>(
object: T,
methodName: M,
): T[M] extends (...args: Array<any>) => any
? SpyInstance<ReturnType<T[M]>, Parameters<T[M]>>
: never;

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
spyOn<T extends {}, M extends NonFunctionPropertyNames<T>>(
object: T,
methodName: M,
accessType?: 'get' | 'set',
) {
spyOn<
T extends ClassLike | ObjectLike,
M extends NonFunctionPropertyNames<T>,
>(object: T, methodName: M, accessType?: 'get' | 'set') {
if (accessType) {
return this._spyOnProperty(object, methodName, accessType);
}
Expand Down Expand Up @@ -1094,11 +1120,10 @@ export class ModuleMocker {
return object[methodName];
}

private _spyOnProperty<T extends {}, M extends NonFunctionPropertyNames<T>>(
obj: T,
propertyName: M,
accessType: 'get' | 'set' = 'get',
): Mock<T> {
private _spyOnProperty<
T extends ClassLike | ObjectLike,
M extends NonFunctionPropertyNames<T>,
>(obj: T, propertyName: M, accessType: 'get' | 'set' = 'get'): Mock<T> {
if (typeof obj !== 'object' && typeof obj !== 'function') {
throw new Error(
'Cannot spyOn on a primitive value; ' + this._typeOf(obj) + ' given',
Expand Down