Skip to content

Commit a41c63a

Browse files
committed
feat(runtime): add jest.mockModule
1 parent a03b6fe commit a41c63a

File tree

5 files changed

+201
-22
lines changed

5 files changed

+201
-22
lines changed

e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Ran all test suites matching /native-esm.tla.test.js/i.
1010
1111
exports[`on node ^12.16.0 || >=13.7.0 runs test with native ESM 1`] = `
1212
Test Suites: 1 passed, 1 total
13-
Tests: 18 passed, 18 total
13+
Tests: 19 passed, 19 total
1414
Snapshots: 0 total
1515
Time: <<REPLACED>>
1616
Ran all test suites matching /native-esm.test.js/i.

e2e/native-esm/__tests__/native-esm.test.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,14 @@ test('handle circular dependency', async () => {
166166
expect(moduleA.moduleB.id).toBe('circularDependentB');
167167
expect(moduleA.moduleB.moduleA).toBe(moduleA);
168168
});
169+
170+
test('can mock module', async () => {
171+
jestObject.mockModule('../mockedModule.mjs', () => ({foo: 'bar'}), {
172+
virtual: true,
173+
});
174+
175+
const importedMock = await import('../mockedModule.mjs');
176+
177+
expect(Object.keys(importedMock)).toEqual(['foo']);
178+
expect(importedMock.foo).toEqual('bar');
179+
});

packages/jest-environment/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,14 @@ export interface Jest {
137137
moduleFactory?: () => unknown,
138138
options?: {virtual?: boolean},
139139
): Jest;
140+
/**
141+
* Mocks a module with an auto-mocked version when it is being required.
142+
*/
143+
mockModule(
144+
moduleName: string,
145+
moduleFactory?: () => Promise<unknown> | unknown,
146+
options?: {virtual?: boolean},
147+
): Jest;
140148
/**
141149
* Returns the actual module instead of a mock, bypassing all checks on
142150
* whether the module should receive a mock implementation or not.

packages/jest-resolve/src/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -313,10 +313,8 @@ export default class Resolver {
313313
getModuleID(
314314
virtualMocks: Map<string, boolean>,
315315
from: Config.Path,
316-
_moduleName?: string,
316+
moduleName = '',
317317
): string {
318-
const moduleName = _moduleName || '';
319-
320318
const key = from + path.delimiter + moduleName;
321319
const cachedModuleID = this._moduleIDCache.get(key);
322320
if (cachedModuleID) {

packages/jest-runtime/src/index.ts

Lines changed: 180 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ export default class Runtime {
155155
private _currentlyExecutingModulePath: string;
156156
private readonly _environment: JestEnvironment;
157157
private readonly _explicitShouldMock: Map<string, boolean>;
158+
private readonly _explicitShouldMockModule: Map<string, boolean>;
158159
private _fakeTimersImplementation:
159160
| LegacyFakeTimers<unknown>
160161
| ModernFakeTimers
@@ -169,6 +170,8 @@ export default class Runtime {
169170
>;
170171
private _mockRegistry: Map<string, any>;
171172
private _isolatedMockRegistry: Map<string, any> | null;
173+
private _moduleMockRegistry: Map<string, VMModule>;
174+
private readonly _moduleMockFactories: Map<string, () => unknown>;
172175
private readonly _moduleMocker: ModuleMocker;
173176
private _isolatedModuleRegistry: ModuleRegistry | null;
174177
private _moduleRegistry: ModuleRegistry;
@@ -191,6 +194,7 @@ export default class Runtime {
191194
private readonly _transitiveShouldMock: Map<string, boolean>;
192195
private _unmockList: RegExp | undefined;
193196
private readonly _virtualMocks: Map<string, boolean>;
197+
private readonly _virtualModuleMocks: Map<string, boolean>;
194198
private _moduleImplementation?: typeof nativeModule.Module;
195199
private readonly jestObjectCaches: Map<string, Jest>;
196200
private jestGlobals?: JestGlobals;
@@ -210,11 +214,14 @@ export default class Runtime {
210214
this._currentlyExecutingModulePath = '';
211215
this._environment = environment;
212216
this._explicitShouldMock = new Map();
217+
this._explicitShouldMockModule = new Map();
213218
this._internalModuleRegistry = new Map();
214219
this._isCurrentlyExecutingManualMock = null;
215220
this._mainModule = null;
216221
this._mockFactories = new Map();
217222
this._mockRegistry = new Map();
223+
this._moduleMockRegistry = new Map();
224+
this._moduleMockFactories = new Map();
218225
invariant(
219226
this._environment.moduleMocker,
220227
'`moduleMocker` must be set on an environment when created',
@@ -233,6 +240,7 @@ export default class Runtime {
233240
this._sourceMapRegistry = new Map();
234241
this._fileTransforms = new Map();
235242
this._virtualMocks = new Map();
243+
this._virtualModuleMocks = new Map();
236244
this.jestObjectCaches = new Map();
237245

238246
this._mockMetaDataCache = new Map();
@@ -458,6 +466,16 @@ export default class Runtime {
458466

459467
const [path, query] = specifier.split('?');
460468

469+
if (
470+
this._shouldMock(
471+
referencingIdentifier,
472+
path,
473+
this._explicitShouldMockModule,
474+
)
475+
) {
476+
return this.importMock(referencingIdentifier, path, context);
477+
}
478+
461479
const resolved = this._resolveModule(referencingIdentifier, path);
462480

463481
if (
@@ -498,6 +516,8 @@ export default class Runtime {
498516
async unstable_importModule(
499517
from: Config.Path,
500518
moduleName?: string,
519+
// TODO: implement this
520+
_isImportActual = false,
501521
): Promise<void> {
502522
invariant(
503523
runtimeSupportsVmModules,
@@ -547,6 +567,109 @@ export default class Runtime {
547567
return evaluateSyntheticModule(module);
548568
}
549569

570+
private async importMock<T = unknown>(
571+
from: Config.Path,
572+
moduleName: string,
573+
context: VMContext,
574+
): Promise<T> {
575+
const moduleID = this._resolver.getModuleID(
576+
this._virtualModuleMocks,
577+
from,
578+
moduleName,
579+
);
580+
581+
if (this._moduleMockRegistry.has(moduleID)) {
582+
return this._moduleMockRegistry.get(moduleID);
583+
}
584+
585+
if (this._moduleMockFactories.has(moduleID)) {
586+
const invokedFactory: any = await this._moduleMockFactories.get(
587+
moduleID,
588+
// has check above makes this ok
589+
)!();
590+
591+
const module = new SyntheticModule(
592+
Object.keys(invokedFactory),
593+
function () {
594+
Object.entries(invokedFactory).forEach(([key, value]) => {
595+
// @ts-expect-error: TS doesn't know what `this` is
596+
this.setExport(key, value);
597+
});
598+
},
599+
// should identifier be `node://${moduleName}`?
600+
{context, identifier: moduleName},
601+
);
602+
603+
this._moduleMockRegistry.set(moduleID, module);
604+
605+
return evaluateSyntheticModule(module);
606+
}
607+
608+
const manualMockOrStub = this._resolver.getMockModule(from, moduleName);
609+
610+
let modulePath =
611+
this._resolver.getMockModule(from, moduleName) ||
612+
this._resolveModule(from, moduleName);
613+
614+
let isManualMock =
615+
manualMockOrStub &&
616+
!this._resolver.resolveStubModuleName(from, moduleName);
617+
if (!isManualMock) {
618+
// If the actual module file has a __mocks__ dir sitting immediately next
619+
// to it, look to see if there is a manual mock for this file.
620+
//
621+
// subDir1/my_module.js
622+
// subDir1/__mocks__/my_module.js
623+
// subDir2/my_module.js
624+
// subDir2/__mocks__/my_module.js
625+
//
626+
// Where some other module does a relative require into each of the
627+
// respective subDir{1,2} directories and expects a manual mock
628+
// corresponding to that particular my_module.js file.
629+
630+
const moduleDir = path.dirname(modulePath);
631+
const moduleFileName = path.basename(modulePath);
632+
const potentialManualMock = path.join(
633+
moduleDir,
634+
'__mocks__',
635+
moduleFileName,
636+
);
637+
if (fs.existsSync(potentialManualMock)) {
638+
isManualMock = true;
639+
modulePath = potentialManualMock;
640+
}
641+
}
642+
if (isManualMock) {
643+
const localModule: InitialModule = {
644+
children: [],
645+
exports: {},
646+
filename: modulePath,
647+
id: modulePath,
648+
loaded: false,
649+
path: modulePath,
650+
};
651+
652+
this._loadModule(
653+
localModule,
654+
from,
655+
moduleName,
656+
modulePath,
657+
undefined,
658+
this._moduleMockRegistry,
659+
);
660+
661+
this._moduleMockRegistry.set(moduleID, localModule.exports);
662+
} else {
663+
// Look for a real module to generate an automock from
664+
this._moduleMockRegistry.set(
665+
moduleID,
666+
this._generateMock(from, moduleName),
667+
);
668+
}
669+
670+
return this._moduleMockRegistry.get(moduleID);
671+
}
672+
550673
private getExportsOfCjs(modulePath: Config.Path) {
551674
const cachedNamedExports = this._cjsNamedExports.get(modulePath);
552675

@@ -578,7 +701,7 @@ export default class Runtime {
578701
from: Config.Path,
579702
moduleName?: string,
580703
options?: InternalModuleOptions,
581-
isRequireActual?: boolean | null,
704+
isRequireActual = false,
582705
): T {
583706
const moduleID = this._resolver.getModuleID(
584707
this._virtualMocks,
@@ -615,12 +738,10 @@ export default class Runtime {
615738

616739
if (options?.isInternalModule) {
617740
moduleRegistry = this._internalModuleRegistry;
741+
} else if (this._isolatedModuleRegistry) {
742+
moduleRegistry = this._isolatedModuleRegistry;
618743
} else {
619-
if (this._isolatedModuleRegistry) {
620-
moduleRegistry = this._isolatedModuleRegistry;
621-
} else {
622-
moduleRegistry = this._moduleRegistry;
623-
}
744+
moduleRegistry = this._moduleRegistry;
624745
}
625746

626747
const module = moduleRegistry.get(modulePath);
@@ -687,17 +808,12 @@ export default class Runtime {
687808
moduleName,
688809
);
689810

690-
if (
691-
this._isolatedMockRegistry &&
692-
this._isolatedMockRegistry.get(moduleID)
693-
) {
694-
return this._isolatedMockRegistry.get(moduleID);
695-
} else if (this._mockRegistry.get(moduleID)) {
696-
return this._mockRegistry.get(moduleID);
697-
}
698-
699811
const mockRegistry = this._isolatedMockRegistry || this._mockRegistry;
700812

813+
if (mockRegistry.get(moduleID)) {
814+
return mockRegistry.get(moduleID);
815+
}
816+
701817
if (this._mockFactories.has(moduleID)) {
702818
// has check above makes this ok
703819
const module = this._mockFactories.get(moduleID)!();
@@ -814,7 +930,7 @@ export default class Runtime {
814930
}
815931

816932
try {
817-
if (this._shouldMock(from, moduleName)) {
933+
if (this._shouldMock(from, moduleName, this._explicitShouldMock)) {
818934
return this.requireMock<T>(from, moduleName);
819935
} else {
820936
return this.requireModule<T>(from, moduleName);
@@ -870,6 +986,7 @@ export default class Runtime {
870986
this._moduleRegistry.clear();
871987
this._esmoduleRegistry.clear();
872988
this._cjsNamedExports.clear();
989+
this._moduleMockRegistry.clear();
873990

874991
if (this._environment) {
875992
if (this._environment.global) {
@@ -958,6 +1075,26 @@ export default class Runtime {
9581075
this._mockFactories.set(moduleID, mockFactory);
9591076
}
9601077

1078+
private setModuleMock(
1079+
from: string,
1080+
moduleName: string,
1081+
mockFactory: () => Promise<unknown> | unknown,
1082+
options?: {virtual?: boolean},
1083+
): void {
1084+
if (options?.virtual) {
1085+
const mockPath = this._resolver.getModulePath(from, moduleName);
1086+
1087+
this._virtualModuleMocks.set(mockPath, true);
1088+
}
1089+
const moduleID = this._resolver.getModuleID(
1090+
this._virtualModuleMocks,
1091+
from,
1092+
moduleName,
1093+
);
1094+
this._explicitShouldMockModule.set(moduleID, true);
1095+
this._moduleMockFactories.set(moduleID, mockFactory);
1096+
}
1097+
9611098
restoreAllMocks(): void {
9621099
this._moduleMocker.restoreAllMocks();
9631100
}
@@ -978,12 +1115,15 @@ export default class Runtime {
9781115
this._internalModuleRegistry.clear();
9791116
this._mainModule = null;
9801117
this._mockFactories.clear();
1118+
this._moduleMockFactories.clear();
9811119
this._mockMetaDataCache.clear();
9821120
this._shouldMockModuleCache.clear();
9831121
this._shouldUnmockTransitiveDependenciesCache.clear();
9841122
this._explicitShouldMock.clear();
1123+
this._explicitShouldMockModule.clear();
9851124
this._transitiveShouldMock.clear();
9861125
this._virtualMocks.clear();
1126+
this._virtualModuleMocks.clear();
9871127
this._cacheFS.clear();
9881128
this._unmockList = undefined;
9891129

@@ -1424,8 +1564,11 @@ export default class Runtime {
14241564
);
14251565
}
14261566

1427-
private _shouldMock(from: Config.Path, moduleName: string): boolean {
1428-
const explicitShouldMock = this._explicitShouldMock;
1567+
private _shouldMock(
1568+
from: Config.Path,
1569+
moduleName: string,
1570+
explicitShouldMock: Map<string, boolean>,
1571+
): boolean {
14291572
const moduleID = this._resolver.getModuleID(
14301573
this._virtualMocks,
14311574
from,
@@ -1593,6 +1736,24 @@ export default class Runtime {
15931736
this.setMock(from, moduleName, mockFactory, options);
15941737
return jestObject;
15951738
};
1739+
const mockModule: Jest['mockModule'] = (
1740+
moduleName,
1741+
mockFactory,
1742+
options,
1743+
) => {
1744+
if (mockFactory !== undefined) {
1745+
this.setModuleMock(from, moduleName, mockFactory, options);
1746+
return jestObject;
1747+
}
1748+
1749+
const moduleID = this._resolver.getModuleID(
1750+
this._virtualMocks,
1751+
from,
1752+
moduleName,
1753+
);
1754+
this._explicitShouldMockModule.set(moduleID, true);
1755+
return jestObject;
1756+
};
15961757
const clearAllMocks = () => {
15971758
this.clearAllMocks();
15981759
return jestObject;
@@ -1691,6 +1852,7 @@ export default class Runtime {
16911852
isMockFunction: this._moduleMocker.isMockFunction,
16921853
isolateModules,
16931854
mock,
1855+
mockModule,
16941856
requireActual: this.requireActual.bind(this, from),
16951857
requireMock: this.requireMock.bind(this, from),
16961858
resetAllMocks,

0 commit comments

Comments
 (0)