Skip to content

Commit

Permalink
refactor(jest-mock)!: rename and clean up utility types (#12435)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrazauskas committed Feb 23, 2022
1 parent 98341d4 commit a653a6f
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 65 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -18,6 +18,7 @@
- `[jest-environment-node]` [**BREAKING**] Second argument `context` to constructor is mandatory ([#12469](https://github.com/facebook/jest/pull/12469))
- `[@jest/expect]` New module which extends `expect` with `jest-snapshot` matchers ([#12404](https://github.com/facebook/jest/pull/12404), [#12410](https://github.com/facebook/jest/pull/12410), [#12418](https://github.com/facebook/jest/pull/12418))
- `[@jest/expect-utils]` New module exporting utils for `expect` ([#12323](https://github.com/facebook/jest/pull/12323))
- `[jest-mock]` [**BREAKING**] Rename exported utility types `ConstructorLike`, `MethodLike`, `ConstructorLikeKeys`, `MethodLikeKeys`, `PropertyLikeKeys`; remove exports of utility types `ArgumentsOf`, `ArgsType`, `ConstructorArgumentsOf` - TS builtin utility types `ConstructorParameters` and `Parameters` should be used instead ([#12435](https://github.com/facebook/jest/pull/12435))
- `[jest-mock]` Improve `isMockFunction` to infer types of passed function ([#12442](https://github.com/facebook/jest/pull/12442))
- `[jest-resolve]` [**BREAKING**] Add support for `package.json` `exports` ([#11961](https://github.com/facebook/jest/pull/11961), [#12373](https://github.com/facebook/jest/pull/12373))
- `[jest-resolve, jest-runtime]` Add support for `data:` URI import and mock ([#12392](https://github.com/facebook/jest/pull/12392))
Expand Down
103 changes: 103 additions & 0 deletions packages/jest-mock/__typetests__/utility-types.test.ts
@@ -0,0 +1,103 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import {expectAssignable, expectNotAssignable, expectType} from 'tsd-lite';
import type {
ConstructorLike,
ConstructorLikeKeys,
MethodLike,
MethodLikeKeys,
PropertyLikeKeys,
} from 'jest-mock';

class SomeClass {
propertyB = 123;
private _propertyC: undefined;
#propertyD = 'abc';

constructor(public propertyA: string) {}

methodA(): void {
return;
}

methodB(b: string): string {
return b;
}

get propertyC() {
return this._propertyC;
}
set propertyC(value) {
this._propertyC = value;
}
}

const someObject = {
SomeClass,

methodA() {
return;
},
methodB(b: string) {
return true;
},
methodC: (c: number) => true,

propertyA: 123,
propertyB: 'value',

someClassInstance: new SomeClass('value'),
};

type SomeObject = typeof someObject;

// ClassLike

expectAssignable<ConstructorLike>(SomeClass);
expectNotAssignable<ConstructorLike>(() => {});
expectNotAssignable<ConstructorLike>(function abc() {
return;
});
expectNotAssignable<ConstructorLike>('abc');
expectNotAssignable<ConstructorLike>(123);
expectNotAssignable<ConstructorLike>(false);
expectNotAssignable<ConstructorLike>(someObject);

// FunctionLike

expectAssignable<MethodLike>(() => {});
expectAssignable<MethodLike>(function abc() {
return;
});
expectNotAssignable<MethodLike>('abc');
expectNotAssignable<MethodLike>(123);
expectNotAssignable<MethodLike>(false);
expectNotAssignable<MethodLike>(SomeClass);
expectNotAssignable<MethodLike>(someObject);

// ConstructorKeys

declare const constructorKeys: ConstructorLikeKeys<SomeObject>;

expectType<'SomeClass'>(constructorKeys);

// MethodKeys

declare const classMethods: MethodLikeKeys<SomeClass>;
declare const objectMethods: MethodLikeKeys<SomeObject>;

expectType<'methodA' | 'methodB'>(classMethods);
expectType<'methodA' | 'methodB' | 'methodC'>(objectMethods);

// PropertyKeys

declare const classProperties: PropertyLikeKeys<SomeClass>;
declare const objectProperties: PropertyLikeKeys<SomeObject>;

expectType<'propertyA' | 'propertyB' | 'propertyC'>(classProperties);
expectType<'propertyA' | 'propertyB' | 'someClassInstance'>(objectProperties);
133 changes: 68 additions & 65 deletions packages/jest-mock/src/index.ts
Expand Up @@ -20,87 +20,101 @@ export type MockFunctionMetadataType =
export type MockFunctionMetadata<
T,
Y extends Array<unknown>,
Type = MockFunctionMetadataType,
MetadataType = MockFunctionMetadataType,
> = {
ref?: number;
members?: Record<string, MockFunctionMetadata<T, Y>>;
mockImpl?: (...args: Y) => T;
name?: string;
refID?: number;
type?: Type;
type?: MetadataType;
value?: T;
length?: number;
};

export type MockableFunction = (...args: Array<any>) => any;
export type MethodKeysOf<T> = {
[K in keyof T]: T[K] extends MockableFunction ? K : never;
export type ConstructorLike = {new (...args: Array<any>): any};

export type MethodLike = (...args: Array<any>) => any;

export type ConstructorLikeKeys<T> = {
[K in keyof T]: T[K] extends ConstructorLike ? K : never;
}[keyof T];
export type PropertyKeysOf<T> = {
[K in keyof T]: T[K] extends MockableFunction ? never : K;

export type MethodLikeKeys<T> = {
[K in keyof T]: T[K] extends MethodLike ? K : never;
}[keyof T];

export type ArgumentsOf<T> = T extends (...args: infer A) => any ? A : never;
export type PropertyLikeKeys<T> = {
[K in keyof T]: T[K] extends MethodLike
? never
: T[K] extends ConstructorLike
? never
: K;
}[keyof T];

export type ConstructorArgumentsOf<T> = T extends new (...args: infer A) => any
? A
// TODO Figure out how to replace this with TS ConstructorParameters utility type
// https://www.typescriptlang.org/docs/handbook/utility-types.html#constructorparameterstype
type ConstructorParameters<T> = T extends new (...args: infer P) => any
? P
: never;

export type MaybeMockedConstructor<T> = T extends new (
...args: Array<any>
) => infer R
? MockInstance<R, ConstructorArgumentsOf<T>>
? MockInstance<R, ConstructorParameters<T>>
: T;
export type MockedFunction<T extends MockableFunction> = MockWithArgs<T> & {

export interface MockWithArgs<T extends MethodLike>
extends MockInstance<ReturnType<T>, Parameters<T>> {
new (...args: ConstructorParameters<T>): T;
(...args: Parameters<T>): ReturnType<T>;
}

export type MockedFunction<T extends MethodLike> = MockWithArgs<T> & {
[K in keyof T]: T[K];
};
export type MockedFunctionDeep<T extends MockableFunction> = MockWithArgs<T> &

export type MockedFunctionDeep<T extends MethodLike> = MockWithArgs<T> &
MockedObjectDeep<T>;

export type MockedObject<T> = MaybeMockedConstructor<T> & {
[K in MethodKeysOf<T>]: T[K] extends MockableFunction
[K in MethodLikeKeys<T>]: T[K] extends MethodLike
? MockedFunction<T[K]>
: T[K];
} & {[K in PropertyKeysOf<T>]: T[K]};
} & {[K in PropertyLikeKeys<T>]: T[K]};

export type MockedObjectDeep<T> = MaybeMockedConstructor<T> & {
[K in MethodKeysOf<T>]: T[K] extends MockableFunction
[K in MethodLikeKeys<T>]: T[K] extends MethodLike
? MockedFunctionDeep<T[K]>
: T[K];
} & {[K in PropertyKeysOf<T>]: MaybeMockedDeep<T[K]>};
} & {[K in PropertyLikeKeys<T>]: MaybeMockedDeep<T[K]>};

export type MaybeMockedDeep<T> = T extends MockableFunction
? MockedFunctionDeep<T>
export type MaybeMocked<T> = T extends MethodLike
? MockedFunction<T>
: T extends object
? MockedObjectDeep<T>
? MockedObject<T>
: T;

export type MaybeMocked<T> = T extends MockableFunction
? MockedFunction<T>
export type MaybeMockedDeep<T> = T extends MethodLike
? MockedFunctionDeep<T>
: T extends object
? MockedObject<T>
? MockedObjectDeep<T>
: T;

export type ArgsType<T> = T extends (...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]>>
: T[P] extends Constructable
[P in keyof T]: T[P] extends MethodLike
? MockInstance<ReturnType<T[P]>, Parameters<T[P]>>
: T[P] extends ConstructorLike
? MockedClass<T[P]>
: T[P];
} & T;
export type MockedClass<T extends Constructable> = MockInstance<

export type MockedClass<T extends ConstructorLike> = MockInstance<
InstanceType<T>,
T extends new (...args: infer P) => any ? P : never
> & {
prototype: T extends {prototype: any} ? Mocked<T['prototype']> : never;
} & T;
export interface Constructable {
new (...args: Array<any>): any;
}

export interface MockWithArgs<T extends MockableFunction>
extends MockInstance<ReturnType<T>, ArgumentsOf<T>> {
new (...args: ConstructorArgumentsOf<T>): T;
(...args: ArgumentsOf<T>): ReturnType<T>;
}

export interface Mock<T, Y extends Array<unknown> = Array<unknown>>
extends Function,
Expand All @@ -109,8 +123,9 @@ export interface Mock<T, Y extends Array<unknown> = Array<unknown>>
(...args: Y): T;
}

export interface SpyInstance<T, Y extends Array<unknown>>
extends MockInstance<T, Y> {}
// TODO Replace with Awaited utility type when minimum supported TS version will be 4.5 or above
//https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-5.html#the-awaited-type-and-promise-improvements
type Unpromisify<T> = T extends Promise<infer R> ? R : never;

export interface MockInstance<T, Y extends Array<unknown>> {
_isMockFunction: true;
Expand All @@ -137,7 +152,8 @@ export interface MockInstance<T, Y extends Array<unknown>> {
mockRejectedValueOnce(value: unknown): this;
}

type Unpromisify<T> = T extends Promise<infer R> ? R : never;
export interface SpyInstance<T, Y extends Array<unknown>>
extends MockInstance<T, Y> {}

type MockFunctionResultIncomplete = {
type: 'incomplete';
Expand Down Expand Up @@ -200,20 +216,6 @@ type MockFunctionConfig = {
specificMockImpls: Array<Function>;
};

// see https://github.com/Microsoft/TypeScript/issues/25215
type NonFunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends (...args: Array<any>) => any ? never : K;
}[keyof T] &
string;
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';

const FUNCTION_NAME_RESERVED_PATTERN = /[\s!-\/:-@\[-`{-~]/;
Expand Down Expand Up @@ -1036,34 +1038,34 @@ export class ModuleMocker {
return fn;
}

spyOn<T extends object, M extends NonFunctionPropertyNames<T>>(
spyOn<T extends object, M extends PropertyLikeKeys<T>>(
object: T,
methodName: M,
accessType: 'get',
): SpyInstance<T[M], []>;

spyOn<T extends object, M extends NonFunctionPropertyNames<T>>(
spyOn<T extends object, M extends PropertyLikeKeys<T>>(
object: T,
methodName: M,
accessType: 'set',
): SpyInstance<void, [T[M]]>;

spyOn<T extends object, M extends ConstructorPropertyNames<T>>(
spyOn<T extends object, M extends ConstructorLikeKeys<T>>(
object: T,
methodName: M,
): T[M] extends new (...args: Array<any>) => any
): T[M] extends ConstructorLike
? SpyInstance<InstanceType<T[M]>, ConstructorParameters<T[M]>>
: never;

spyOn<T extends object, M extends FunctionPropertyNames<T>>(
spyOn<T extends object, M extends MethodLikeKeys<T>>(
object: T,
methodName: M,
): T[M] extends (...args: Array<any>) => any
): T[M] extends MethodLike
? SpyInstance<ReturnType<T[M]>, Parameters<T[M]>>
: never;

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
spyOn<T extends object, M extends NonFunctionPropertyNames<T>>(
spyOn<T extends object, M extends PropertyLikeKeys<T>>(
object: T,
methodName: M,
accessType?: 'get' | 'set',
Expand Down Expand Up @@ -1134,10 +1136,11 @@ export class ModuleMocker {
return object[methodName];
}

private _spyOnProperty<
T extends object,
M extends NonFunctionPropertyNames<T>,
>(obj: T, propertyName: M, accessType: 'get' | 'set' = 'get'): Mock<T> {
private _spyOnProperty<T extends object, M extends PropertyLikeKeys<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

0 comments on commit a653a6f

Please sign in to comment.