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/environment, jest-runtime): allow passing a generic type argument to jest.createMockFromModule<T>() method #13202

Merged
merged 17 commits into from
Sep 3, 2022
Merged
6 changes: 3 additions & 3 deletions packages/jest-environment/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +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 {ModuleMocker} from 'jest-mock';
import type {Mocked, ModuleMocker} from 'jest-mock';

export type EnvironmentContext = {
console: Console;
Expand Down Expand Up @@ -92,7 +92,7 @@ export interface Jest {
* This is useful when you want to create a manual mock that extends the
* automatic mock's behavior.
*/
createMockFromModule(moduleName: string): unknown;
createMockFromModule<T = unknown>(moduleName: string): Mocked<T>;
/**
* Indicates that the module system should never return a mocked version of
* the specified module and its dependencies.
Expand Down Expand Up @@ -138,7 +138,7 @@ export interface Jest {
*
* @deprecated Use `jest.createMockFromModule()` instead
*/
genMockFromModule(moduleName: string): unknown;
genMockFromModule<T = unknown>(moduleName: string): Mocked<T>;
/**
* When mocking time, `Date.now()` will also be mocked. If you for some reason
* need access to the real current time, you can invoke this function.
Expand Down
84 changes: 43 additions & 41 deletions packages/jest-mock/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

/* eslint-disable local/ban-types-eventually, local/prefer-rest-params-eventually */

export type MockFunctionMetadataType =
export type MockMetadataType =
| 'object'
| 'array'
| 'regexp'
Expand All @@ -17,20 +17,27 @@ export type MockFunctionMetadataType =
| 'null'
| 'undefined';

export type MockFunctionMetadata<
T extends UnknownFunction = UnknownFunction,
MetadataType = MockFunctionMetadataType,
> = {
// TODO remove re-export in Jest 30
export type MockFunctionMetadataType = MockMetadataType;

export type MetaDataValue<T> = T extends UnknownFunction
? ReturnType<T>
: undefined;

export type MockMetadata<T, MetadataType = MockMetadataType> = {
ref?: number;
members?: Record<string, MockFunctionMetadata<T>>;
members?: Record<string, MockMetadata<T>>;
mockImpl?: T;
name?: string;
refID?: number;
type?: MetadataType;
value?: ReturnType<T>;
value?: MetaDataValue<T>;
length?: number;
};

// TODO remove re-export in Jest 30
export type MockFunctionMetadata<T, U = MockMetadataType> = MockMetadata<T, U>;

export type ClassLike = {new (...args: any): any};
export type FunctionLike = (...args: any) => any;

Expand Down Expand Up @@ -75,15 +82,15 @@ type MockedObjectShallow<T extends object> = {
: T[K];
} & T;

export type Mocked<T extends object> = T extends ClassLike
export type Mocked<T> = T extends ClassLike
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved
? MockedClass<T>
: T extends FunctionLike
? MockedFunction<T>
: T extends object
? MockedObject<T>
: T;

export type MockedShallow<T extends object> = T extends ClassLike
export type MockedShallow<T> = T extends ClassLike
? MockedClass<T>
: T extends FunctionLike
? MockedFunctionShallow<T>
Expand Down Expand Up @@ -386,7 +393,7 @@ function getObjectType(value: unknown): string {
return Object.prototype.toString.apply(value).slice(8, -1);
}

function getType(ref?: unknown): MockFunctionMetadataType | null {
function getType(ref?: unknown): MockMetadataType | null {
const typeName = getObjectType(ref);
if (
typeName === 'Function' ||
Expand Down Expand Up @@ -560,31 +567,28 @@ export class ModuleMocker {
};
}

private _makeComponent<T extends UnknownFunction>(
metadata: MockFunctionMetadata<T, 'object'>,
private _makeComponent<T>(
metadata: MockMetadata<T, 'object'>,
restore?: () => void,
): Record<string, any>;
private _makeComponent<T extends UnknownFunction>(
metadata: MockFunctionMetadata<T, 'array'>,
private _makeComponent<T>(
metadata: MockMetadata<T, 'array'>,
restore?: () => void,
): Array<unknown>;
private _makeComponent<T extends UnknownFunction>(
metadata: MockFunctionMetadata<T, 'regexp'>,
private _makeComponent<T>(
metadata: MockMetadata<T, 'regexp'>,
restore?: () => void,
): RegExp;
private _makeComponent<T extends UnknownFunction>(
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved
metadata: MockFunctionMetadata<
T,
'constant' | 'collection' | 'null' | 'undefined'
>,
private _makeComponent<T>(
metadata: MockMetadata<T, 'constant' | 'collection' | 'null' | 'undefined'>,
restore?: () => void,
): T;
private _makeComponent<T extends UnknownFunction>(
metadata: MockFunctionMetadata<T, 'function'>,
metadata: MockMetadata<T, 'function'>,
restore?: () => void,
): Mock<T>;
private _makeComponent<T extends UnknownFunction>(
metadata: MockFunctionMetadata<T>,
metadata: MockMetadata<T>,
restore?: () => void,
):
| Record<string, any>
Expand Down Expand Up @@ -808,7 +812,7 @@ export class ModuleMocker {
}

private _createMockFunction<T extends UnknownFunction>(
metadata: MockFunctionMetadata<T>,
metadata: MockMetadata<T>,
mockConstructor: Function,
): Function {
let name = metadata.name;
Expand Down Expand Up @@ -862,8 +866,8 @@ export class ModuleMocker {
return createConstructor(mockConstructor);
}

private _generateMock<T extends UnknownFunction>(
metadata: MockFunctionMetadata<T>,
private _generateMock<T>(
metadata: MockMetadata<T>,
callbacks: Array<Function>,
refs: {
[key: string]:
Expand All @@ -872,9 +876,9 @@ export class ModuleMocker {
| RegExp
| UnknownFunction
| undefined
| Mock<T>;
| Mock<UnknownFunction>;
},
): Mock<T> {
): Mocked<T> {
// metadata not compatible but it's the same type, maybe problem with
// overloading of _makeComponent and not _generateMock?
// @ts-expect-error - unsure why TSC complains here?
Expand Down Expand Up @@ -905,20 +909,18 @@ export class ModuleMocker {
mock.prototype.constructor = mock;
}

return mock as Mock<T>;
return mock as Mocked<T>;
}

/**
* @see README.md
* @param metadata Metadata for the mock in the schema returned by the
* getMetadata method of this module.
*/
generateFromMetadata<T extends UnknownFunction>(
metadata: MockFunctionMetadata<T>,
): Mock<T> {
generateFromMetadata<T>(metadata: MockMetadata<T>): Mocked<T> {
const callbacks: Array<Function> = [];
const refs = {};
const mock = this._generateMock(metadata, callbacks, refs);
const mock = this._generateMock<T>(metadata, callbacks, refs);
callbacks.forEach(setter => setter());
return mock;
}
Expand All @@ -927,11 +929,11 @@ export class ModuleMocker {
* @see README.md
* @param component The component for which to retrieve metadata.
*/
getMetadata<T extends UnknownFunction>(
component: ReturnType<T>,
_refs?: Map<ReturnType<T>, number>,
): MockFunctionMetadata<T> | null {
const refs = _refs || new Map<ReturnType<T>, number>();
getMetadata<T>(
component: MetaDataValue<T>,
_refs?: Map<MetaDataValue<T>, number>,
): MockMetadata<T> | null {
const refs = _refs || new Map<MetaDataValue<T>, number>();
const ref = refs.get(component);
if (ref != null) {
return {ref};
Expand All @@ -942,7 +944,7 @@ export class ModuleMocker {
return null;
}

const metadata: MockFunctionMetadata<T> = {type};
const metadata: MockMetadata<T> = {};
if (
type === 'constant' ||
type === 'collection' ||
Expand All @@ -967,7 +969,7 @@ export class ModuleMocker {
refs.set(component, metadata.refID);

let members: {
[key: string]: MockFunctionMetadata<T>;
[key: string]: MockMetadata<T>;
} | null = null;
// Leave arrays alone
if (type !== 'array') {
Expand Down Expand Up @@ -1007,7 +1009,7 @@ export class ModuleMocker {
): fn is Mock<(...args: P) => R>;
isMockFunction(fn: unknown): fn is Mock<UnknownFunction>;
isMockFunction(fn: unknown): fn is Mock<UnknownFunction> {
return fn != null && (fn as any)._isMockFunction === true;
return fn != null && (fn as Mock)._isMockFunction === true;
}

fn<T extends FunctionLike = UnknownFunction>(implementation?: T): Mock<T> {
Expand Down
15 changes: 9 additions & 6 deletions packages/jest-runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import {
import type {Config, Global} from '@jest/types';
import HasteMap, {IModuleMap} from 'jest-haste-map';
import {formatStackTrace, separateMessageFromStack} from 'jest-message-util';
import type {MockFunctionMetadata, ModuleMocker} from 'jest-mock';
import type {MetaDataValue, MockMetadata, ModuleMocker} from 'jest-mock';
import {escapePathForRegex} from 'jest-regex-util';
import Resolver, {ResolveModuleConfig} from 'jest-resolve';
import {EXTENSION as SnapshotExtension} from 'jest-snapshot';
Expand Down Expand Up @@ -168,7 +168,7 @@ export default class Runtime {
private _isCurrentlyExecutingManualMock: string | null;
private _mainModule: Module | null;
private readonly _mockFactories: Map<string, () => unknown>;
private readonly _mockMetaDataCache: Map<string, MockFunctionMetadata>;
private readonly _mockMetaDataCache: Map<string, MockMetadata<any>>;
SimenB marked this conversation as resolved.
Show resolved Hide resolved
private _mockRegistry: Map<string, any>;
private _isolatedMockRegistry: Map<string, any> | null;
Comment on lines -171 to 173
Copy link
Contributor Author

@mrazauskas mrazauskas Sep 3, 2022

Choose a reason for hiding this comment

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

Ideally these three anys should be some T (a type of some mock module). unknown does not work unfortunately, because T cannot be assigned to unknown. The T could be assigned to T, but there is no way to have it here. Tricky indeed.

At the same time, here the shape of the mock is not important at all. Hence any looked fine. Hm.. I will try one more time.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope. No luck. It might make sense to create type MockModule = any and to use it here instead of any. Looks redundant, but perhaps that way this is more clear?

Copy link
Member

Choose a reason for hiding this comment

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

nah, current approach is fine 👍

private _moduleMockRegistry: Map<string, VMModule>;
Expand Down Expand Up @@ -1710,7 +1710,7 @@ export default class Runtime {
return Module;
}

private _generateMock(from: string, moduleName: string) {
private _generateMock<T>(from: string, moduleName: string) {
const modulePath =
this._resolver.resolveStubModuleName(from, moduleName) ||
this._resolveCjsModule(from, moduleName);
Expand All @@ -1720,7 +1720,7 @@ export default class Runtime {

this._mockMetaDataCache.set(
modulePath,
this._moduleMocker.getMetadata({}) || {},
this._moduleMocker.getMetadata({} as MetaDataValue<T>) || {},
);

// In order to avoid it being possible for automocking to potentially
Expand All @@ -1732,7 +1732,10 @@ export default class Runtime {
this._mockRegistry = new Map();
this._moduleRegistry = new Map();

const moduleExports = this.requireModule(from, moduleName);
const moduleExports = this.requireModule<MetaDataValue<T>>(
from,
moduleName,
);

// Restore the "real" module/mock registries
this._mockRegistry = origMockRegistry;
Expand All @@ -1747,7 +1750,7 @@ export default class Runtime {
}
this._mockMetaDataCache.set(modulePath, mockMetadata);
}
return this._moduleMocker.generateFromMetadata(
return this._moduleMocker.generateFromMetadata<T>(
// added above if missing
this._mockMetaDataCache.get(modulePath)!,
);
Expand Down