diff --git a/CHANGELOG.md b/CHANGELOG.md index f0e7bb57f87f..a0a2dbd41e21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/packages/jest-mock/__typetests__/utility-types.test.ts b/packages/jest-mock/__typetests__/utility-types.test.ts new file mode 100644 index 000000000000..af4efdb60a49 --- /dev/null +++ b/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(SomeClass); +expectNotAssignable(() => {}); +expectNotAssignable(function abc() { + return; +}); +expectNotAssignable('abc'); +expectNotAssignable(123); +expectNotAssignable(false); +expectNotAssignable(someObject); + +// FunctionLike + +expectAssignable(() => {}); +expectAssignable(function abc() { + return; +}); +expectNotAssignable('abc'); +expectNotAssignable(123); +expectNotAssignable(false); +expectNotAssignable(SomeClass); +expectNotAssignable(someObject); + +// ConstructorKeys + +declare const constructorKeys: ConstructorLikeKeys; + +expectType<'SomeClass'>(constructorKeys); + +// MethodKeys + +declare const classMethods: MethodLikeKeys; +declare const objectMethods: MethodLikeKeys; + +expectType<'methodA' | 'methodB'>(classMethods); +expectType<'methodA' | 'methodB' | 'methodC'>(objectMethods); + +// PropertyKeys + +declare const classProperties: PropertyLikeKeys; +declare const objectProperties: PropertyLikeKeys; + +expectType<'propertyA' | 'propertyB' | 'propertyC'>(classProperties); +expectType<'propertyA' | 'propertyB' | 'someClassInstance'>(objectProperties); diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index bae820bde600..16fee2a2addc 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -20,87 +20,101 @@ export type MockFunctionMetadataType = export type MockFunctionMetadata< T, Y extends Array, - Type = MockFunctionMetadataType, + MetadataType = MockFunctionMetadataType, > = { ref?: number; members?: Record>; mockImpl?: (...args: Y) => T; name?: string; refID?: number; - type?: Type; + type?: MetadataType; value?: T; length?: number; }; -export type MockableFunction = (...args: Array) => any; -export type MethodKeysOf = { - [K in keyof T]: T[K] extends MockableFunction ? K : never; +export type ConstructorLike = {new (...args: Array): any}; + +export type MethodLike = (...args: Array) => any; + +export type ConstructorLikeKeys = { + [K in keyof T]: T[K] extends ConstructorLike ? K : never; }[keyof T]; -export type PropertyKeysOf = { - [K in keyof T]: T[K] extends MockableFunction ? never : K; + +export type MethodLikeKeys = { + [K in keyof T]: T[K] extends MethodLike ? K : never; }[keyof T]; -export type ArgumentsOf = T extends (...args: infer A) => any ? A : never; +export type PropertyLikeKeys = { + [K in keyof T]: T[K] extends MethodLike + ? never + : T[K] extends ConstructorLike + ? never + : K; +}[keyof T]; -export type ConstructorArgumentsOf = 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 extends new (...args: infer P) => any + ? P : never; + export type MaybeMockedConstructor = T extends new ( ...args: Array ) => infer R - ? MockInstance> + ? MockInstance> : T; -export type MockedFunction = MockWithArgs & { + +export interface MockWithArgs + extends MockInstance, Parameters> { + new (...args: ConstructorParameters): T; + (...args: Parameters): ReturnType; +} + +export type MockedFunction = MockWithArgs & { [K in keyof T]: T[K]; }; -export type MockedFunctionDeep = MockWithArgs & + +export type MockedFunctionDeep = MockWithArgs & MockedObjectDeep; + export type MockedObject = MaybeMockedConstructor & { - [K in MethodKeysOf]: T[K] extends MockableFunction + [K in MethodLikeKeys]: T[K] extends MethodLike ? MockedFunction : T[K]; -} & {[K in PropertyKeysOf]: T[K]}; +} & {[K in PropertyLikeKeys]: T[K]}; + export type MockedObjectDeep = MaybeMockedConstructor & { - [K in MethodKeysOf]: T[K] extends MockableFunction + [K in MethodLikeKeys]: T[K] extends MethodLike ? MockedFunctionDeep : T[K]; -} & {[K in PropertyKeysOf]: MaybeMockedDeep}; +} & {[K in PropertyLikeKeys]: MaybeMockedDeep}; -export type MaybeMockedDeep = T extends MockableFunction - ? MockedFunctionDeep +export type MaybeMocked = T extends MethodLike + ? MockedFunction : T extends object - ? MockedObjectDeep + ? MockedObject : T; -export type MaybeMocked = T extends MockableFunction - ? MockedFunction +export type MaybeMockedDeep = T extends MethodLike + ? MockedFunctionDeep : T extends object - ? MockedObject + ? MockedObjectDeep : T; -export type ArgsType = T extends (...args: infer A) => any ? A : never; export type Mocked = { - [P in keyof T]: T[P] extends (...args: Array) => any - ? MockInstance, ArgsType> - : T[P] extends Constructable + [P in keyof T]: T[P] extends MethodLike + ? MockInstance, Parameters> + : T[P] extends ConstructorLike ? MockedClass : T[P]; } & T; -export type MockedClass = MockInstance< + +export type MockedClass = MockInstance< InstanceType, T extends new (...args: infer P) => any ? P : never > & { prototype: T extends {prototype: any} ? Mocked : never; } & T; -export interface Constructable { - new (...args: Array): any; -} - -export interface MockWithArgs - extends MockInstance, ArgumentsOf> { - new (...args: ConstructorArgumentsOf): T; - (...args: ArgumentsOf): ReturnType; -} export interface Mock = Array> extends Function, @@ -109,8 +123,9 @@ export interface Mock = Array> (...args: Y): T; } -export interface SpyInstance> - extends MockInstance {} +// 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 extends Promise ? R : never; export interface MockInstance> { _isMockFunction: true; @@ -137,7 +152,8 @@ export interface MockInstance> { mockRejectedValueOnce(value: unknown): this; } -type Unpromisify = T extends Promise ? R : never; +export interface SpyInstance> + extends MockInstance {} type MockFunctionResultIncomplete = { type: 'incomplete'; @@ -200,20 +216,6 @@ type MockFunctionConfig = { specificMockImpls: Array; }; -// see https://github.com/Microsoft/TypeScript/issues/25215 -type NonFunctionPropertyNames = { - [K in keyof T]: T[K] extends (...args: Array) => any ? never : K; -}[keyof T] & - string; -type FunctionPropertyNames = { - [K in keyof T]: T[K] extends (...args: Array) => any ? K : never; -}[keyof T] & - string; -type ConstructorPropertyNames = { - [K in keyof T]: T[K] extends new (...args: Array) => any ? K : never; -}[keyof T] & - string; - const MOCK_CONSTRUCTOR_NAME = 'mockConstructor'; const FUNCTION_NAME_RESERVED_PATTERN = /[\s!-\/:-@\[-`{-~]/; @@ -1036,34 +1038,34 @@ export class ModuleMocker { return fn; } - spyOn>( + spyOn>( object: T, methodName: M, accessType: 'get', ): SpyInstance; - spyOn>( + spyOn>( object: T, methodName: M, accessType: 'set', ): SpyInstance; - spyOn>( + spyOn>( object: T, methodName: M, - ): T[M] extends new (...args: Array) => any + ): T[M] extends ConstructorLike ? SpyInstance, ConstructorParameters> : never; - spyOn>( + spyOn>( object: T, methodName: M, - ): T[M] extends (...args: Array) => any + ): T[M] extends MethodLike ? SpyInstance, Parameters> : never; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - spyOn>( + spyOn>( object: T, methodName: M, accessType?: 'get' | 'set', @@ -1134,10 +1136,11 @@ export class ModuleMocker { return object[methodName]; } - private _spyOnProperty< - T extends object, - M extends NonFunctionPropertyNames, - >(obj: T, propertyName: M, accessType: 'get' | 'set' = 'get'): Mock { + private _spyOnProperty>( + obj: T, + propertyName: M, + accessType: 'get' | 'set' = 'get', + ): Mock { if (typeof obj !== 'object' && typeof obj !== 'function') { throw new Error( 'Cannot spyOn on a primitive value; ' + this._typeOf(obj) + ' given',