Skip to content

Commit 914cc75

Browse files
committed
feat(runtime): add jest.mockModule
1 parent b3c129e commit 914cc75

File tree

4 files changed

+197
-14
lines changed

4 files changed

+197
-14
lines changed

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

+1-1
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: 19 passed, 19 total
13+
Tests: 20 passed, 20 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

+11
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,14 @@ test('require of ESM should throw correct error', () => {
177177
}),
178178
);
179179
});
180+
181+
test('can mock module', async () => {
182+
jestObject.mockModule('../mockedModule.mjs', () => ({foo: 'bar'}), {
183+
virtual: true,
184+
});
185+
186+
const importedMock = await import('../mockedModule.mjs');
187+
188+
expect(Object.keys(importedMock)).toEqual(['foo']);
189+
expect(importedMock.foo).toEqual('bar');
190+
});

packages/jest-environment/src/index.ts

+8
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-runtime/src/index.ts

+177-13
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ export default class Runtime {
180180
private _currentlyExecutingModulePath: string;
181181
private readonly _environment: JestEnvironment;
182182
private readonly _explicitShouldMock: Map<string, boolean>;
183+
private readonly _explicitShouldMockModule: Map<string, boolean>;
183184
private _fakeTimersImplementation:
184185
| LegacyFakeTimers<unknown>
185186
| ModernFakeTimers
@@ -194,6 +195,8 @@ export default class Runtime {
194195
>;
195196
private _mockRegistry: Map<string, any>;
196197
private _isolatedMockRegistry: Map<string, any> | null;
198+
private _moduleMockRegistry: Map<string, VMModule>;
199+
private readonly _moduleMockFactories: Map<string, () => unknown>;
197200
private readonly _moduleMocker: ModuleMocker;
198201
private _isolatedModuleRegistry: ModuleRegistry | null;
199202
private _moduleRegistry: ModuleRegistry;
@@ -217,6 +220,7 @@ export default class Runtime {
217220
private readonly _transitiveShouldMock: Map<string, boolean>;
218221
private _unmockList: RegExp | undefined;
219222
private readonly _virtualMocks: Map<string, boolean>;
223+
private readonly _virtualModuleMocks: Map<string, boolean>;
220224
private _moduleImplementation?: typeof nativeModule.Module;
221225
private readonly jestObjectCaches: Map<string, Jest>;
222226
private jestGlobals?: JestGlobals;
@@ -236,11 +240,14 @@ export default class Runtime {
236240
this._currentlyExecutingModulePath = '';
237241
this._environment = environment;
238242
this._explicitShouldMock = new Map();
243+
this._explicitShouldMockModule = new Map();
239244
this._internalModuleRegistry = new Map();
240245
this._isCurrentlyExecutingManualMock = null;
241246
this._mainModule = null;
242247
this._mockFactories = new Map();
243248
this._mockRegistry = new Map();
249+
this._moduleMockRegistry = new Map();
250+
this._moduleMockFactories = new Map();
244251
invariant(
245252
this._environment.moduleMocker,
246253
'`moduleMocker` must be set on an environment when created',
@@ -260,6 +267,7 @@ export default class Runtime {
260267
this._fileTransforms = new Map();
261268
this._fileTransformsMutex = new Map();
262269
this._virtualMocks = new Map();
270+
this._virtualModuleMocks = new Map();
263271
this.jestObjectCaches = new Map();
264272

265273
this._mockMetaDataCache = new Map();
@@ -523,6 +531,16 @@ export default class Runtime {
523531

524532
const [path, query] = specifier.split('?');
525533

534+
if (
535+
this._shouldMock(
536+
referencingIdentifier,
537+
path,
538+
this._explicitShouldMockModule,
539+
)
540+
) {
541+
return this.importMock(referencingIdentifier, path, context);
542+
}
543+
526544
const resolved = this._resolveModule(referencingIdentifier, path);
527545

528546
if (
@@ -563,6 +581,8 @@ export default class Runtime {
563581
async unstable_importModule(
564582
from: Config.Path,
565583
moduleName?: string,
584+
// TODO: implement this
585+
_isImportActual = false,
566586
): Promise<void> {
567587
invariant(
568588
runtimeSupportsVmModules,
@@ -612,6 +632,109 @@ export default class Runtime {
612632
return evaluateSyntheticModule(module);
613633
}
614634

635+
private async importMock<T = unknown>(
636+
from: Config.Path,
637+
moduleName: string,
638+
context: VMContext,
639+
): Promise<T> {
640+
const moduleID = this._resolver.getModuleID(
641+
this._virtualModuleMocks,
642+
from,
643+
moduleName,
644+
);
645+
646+
if (this._moduleMockRegistry.has(moduleID)) {
647+
return this._moduleMockRegistry.get(moduleID);
648+
}
649+
650+
if (this._moduleMockFactories.has(moduleID)) {
651+
const invokedFactory: any = await this._moduleMockFactories.get(
652+
moduleID,
653+
// has check above makes this ok
654+
)!();
655+
656+
const module = new SyntheticModule(
657+
Object.keys(invokedFactory),
658+
function () {
659+
Object.entries(invokedFactory).forEach(([key, value]) => {
660+
// @ts-expect-error: TS doesn't know what `this` is
661+
this.setExport(key, value);
662+
});
663+
},
664+
// should identifier be `node://${moduleName}`?
665+
{context, identifier: moduleName},
666+
);
667+
668+
this._moduleMockRegistry.set(moduleID, module);
669+
670+
return evaluateSyntheticModule(module);
671+
}
672+
673+
const manualMockOrStub = this._resolver.getMockModule(from, moduleName);
674+
675+
let modulePath =
676+
this._resolver.getMockModule(from, moduleName) ||
677+
this._resolveModule(from, moduleName);
678+
679+
let isManualMock =
680+
manualMockOrStub &&
681+
!this._resolver.resolveStubModuleName(from, moduleName);
682+
if (!isManualMock) {
683+
// If the actual module file has a __mocks__ dir sitting immediately next
684+
// to it, look to see if there is a manual mock for this file.
685+
//
686+
// subDir1/my_module.js
687+
// subDir1/__mocks__/my_module.js
688+
// subDir2/my_module.js
689+
// subDir2/__mocks__/my_module.js
690+
//
691+
// Where some other module does a relative require into each of the
692+
// respective subDir{1,2} directories and expects a manual mock
693+
// corresponding to that particular my_module.js file.
694+
695+
const moduleDir = path.dirname(modulePath);
696+
const moduleFileName = path.basename(modulePath);
697+
const potentialManualMock = path.join(
698+
moduleDir,
699+
'__mocks__',
700+
moduleFileName,
701+
);
702+
if (fs.existsSync(potentialManualMock)) {
703+
isManualMock = true;
704+
modulePath = potentialManualMock;
705+
}
706+
}
707+
if (isManualMock) {
708+
const localModule: InitialModule = {
709+
children: [],
710+
exports: {},
711+
filename: modulePath,
712+
id: modulePath,
713+
loaded: false,
714+
path: modulePath,
715+
};
716+
717+
this._loadModule(
718+
localModule,
719+
from,
720+
moduleName,
721+
modulePath,
722+
undefined,
723+
this._moduleMockRegistry,
724+
);
725+
726+
this._moduleMockRegistry.set(moduleID, localModule.exports);
727+
} else {
728+
// Look for a real module to generate an automock from
729+
this._moduleMockRegistry.set(
730+
moduleID,
731+
this._generateMock(from, moduleName),
732+
);
733+
}
734+
735+
return this._moduleMockRegistry.get(moduleID);
736+
}
737+
615738
private getExportsOfCjs(modulePath: Config.Path) {
616739
const cachedNamedExports = this._cjsNamedExports.get(modulePath);
617740

@@ -643,7 +766,7 @@ export default class Runtime {
643766
from: Config.Path,
644767
moduleName?: string,
645768
options?: InternalModuleOptions,
646-
isRequireActual?: boolean | null,
769+
isRequireActual = false,
647770
): T {
648771
const moduleID = this._resolver.getModuleID(
649772
this._virtualMocks,
@@ -770,17 +893,12 @@ export default class Runtime {
770893
moduleName,
771894
);
772895

773-
if (
774-
this._isolatedMockRegistry &&
775-
this._isolatedMockRegistry.get(moduleID)
776-
) {
777-
return this._isolatedMockRegistry.get(moduleID);
778-
} else if (this._mockRegistry.get(moduleID)) {
779-
return this._mockRegistry.get(moduleID);
780-
}
781-
782896
const mockRegistry = this._isolatedMockRegistry || this._mockRegistry;
783897

898+
if (mockRegistry.get(moduleID)) {
899+
return mockRegistry.get(moduleID);
900+
}
901+
784902
if (this._mockFactories.has(moduleID)) {
785903
// has check above makes this ok
786904
const module = this._mockFactories.get(moduleID)!();
@@ -896,7 +1014,7 @@ export default class Runtime {
8961014
}
8971015

8981016
try {
899-
if (this._shouldMock(from, moduleName)) {
1017+
if (this._shouldMock(from, moduleName, this._explicitShouldMock)) {
9001018
return this.requireMock<T>(from, moduleName);
9011019
} else {
9021020
return this.requireModule<T>(from, moduleName);
@@ -952,6 +1070,7 @@ export default class Runtime {
9521070
this._moduleRegistry.clear();
9531071
this._esmoduleRegistry.clear();
9541072
this._cjsNamedExports.clear();
1073+
this._moduleMockRegistry.clear();
9551074

9561075
if (this._environment) {
9571076
if (this._environment.global) {
@@ -1043,6 +1162,26 @@ export default class Runtime {
10431162
this._mockFactories.set(moduleID, mockFactory);
10441163
}
10451164

1165+
private setModuleMock(
1166+
from: string,
1167+
moduleName: string,
1168+
mockFactory: () => Promise<unknown> | unknown,
1169+
options?: {virtual?: boolean},
1170+
): void {
1171+
if (options?.virtual) {
1172+
const mockPath = this._resolver.getModulePath(from, moduleName);
1173+
1174+
this._virtualModuleMocks.set(mockPath, true);
1175+
}
1176+
const moduleID = this._resolver.getModuleID(
1177+
this._virtualModuleMocks,
1178+
from,
1179+
moduleName,
1180+
);
1181+
this._explicitShouldMockModule.set(moduleID, true);
1182+
this._moduleMockFactories.set(moduleID, mockFactory);
1183+
}
1184+
10461185
restoreAllMocks(): void {
10471186
this._moduleMocker.restoreAllMocks();
10481187
}
@@ -1063,12 +1202,15 @@ export default class Runtime {
10631202
this._internalModuleRegistry.clear();
10641203
this._mainModule = null;
10651204
this._mockFactories.clear();
1205+
this._moduleMockFactories.clear();
10661206
this._mockMetaDataCache.clear();
10671207
this._shouldMockModuleCache.clear();
10681208
this._shouldUnmockTransitiveDependenciesCache.clear();
10691209
this._explicitShouldMock.clear();
1210+
this._explicitShouldMockModule.clear();
10701211
this._transitiveShouldMock.clear();
10711212
this._virtualMocks.clear();
1213+
this._virtualModuleMocks.clear();
10721214
this._cacheFS.clear();
10731215
this._unmockList = undefined;
10741216

@@ -1516,8 +1658,11 @@ export default class Runtime {
15161658
);
15171659
}
15181660

1519-
private _shouldMock(from: Config.Path, moduleName: string): boolean {
1520-
const explicitShouldMock = this._explicitShouldMock;
1661+
private _shouldMock(
1662+
from: Config.Path,
1663+
moduleName: string,
1664+
explicitShouldMock: Map<string, boolean>,
1665+
): boolean {
15211666
const moduleID = this._resolver.getModuleID(
15221667
this._virtualMocks,
15231668
from,
@@ -1687,6 +1832,24 @@ export default class Runtime {
16871832
this.setMock(from, moduleName, mockFactory, options);
16881833
return jestObject;
16891834
};
1835+
const mockModule: Jest['mockModule'] = (
1836+
moduleName,
1837+
mockFactory,
1838+
options,
1839+
) => {
1840+
if (mockFactory !== undefined) {
1841+
this.setModuleMock(from, moduleName, mockFactory, options);
1842+
return jestObject;
1843+
}
1844+
1845+
const moduleID = this._resolver.getModuleID(
1846+
this._virtualMocks,
1847+
from,
1848+
moduleName,
1849+
);
1850+
this._explicitShouldMockModule.set(moduleID, true);
1851+
return jestObject;
1852+
};
16901853
const clearAllMocks = () => {
16911854
this.clearAllMocks();
16921855
return jestObject;
@@ -1785,6 +1948,7 @@ export default class Runtime {
17851948
isMockFunction: this._moduleMocker.isMockFunction,
17861949
isolateModules,
17871950
mock,
1951+
mockModule,
17881952
requireActual: this.requireActual.bind(this, from),
17891953
requireMock: this.requireMock.bind(this, from),
17901954
resetAllMocks,

0 commit comments

Comments
 (0)