Skip to content

Commit 6478bb2

Browse files
committed
feat(runtime): add jest.mockModule
1 parent bba34bd commit 6478bb2

File tree

6 files changed

+200
-19
lines changed

6 files changed

+200
-19
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ FAIL __tests__/index.js
4141
12 | module.exports = () => 'test';
4242
13 |
4343
44-
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:558:17)
44+
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:557:17)
4545
at Object.require (index.js:10:1)
4646
`;
4747

@@ -70,6 +70,6 @@ FAIL __tests__/index.js
7070
12 | module.exports = () => 'test';
7171
13 |
7272
73-
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:558:17)
73+
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:557:17)
7474
at Object.require (index.js:10:1)
7575
`;

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: 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

Lines changed: 11 additions & 0 deletions
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

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/resolver.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: 177 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ export default class Runtime {
156156
private _currentlyExecutingModulePath: string;
157157
private readonly _environment: JestEnvironment;
158158
private readonly _explicitShouldMock: Map<string, boolean>;
159+
private readonly _explicitShouldMockModule: Map<string, boolean>;
159160
private _fakeTimersImplementation:
160161
| LegacyFakeTimers<unknown>
161162
| ModernFakeTimers
@@ -170,6 +171,8 @@ export default class Runtime {
170171
>;
171172
private _mockRegistry: Map<string, any>;
172173
private _isolatedMockRegistry: Map<string, any> | null;
174+
private _moduleMockRegistry: Map<string, VMModule>;
175+
private readonly _moduleMockFactories: Map<string, () => unknown>;
173176
private readonly _moduleMocker: ModuleMocker;
174177
private _isolatedModuleRegistry: ModuleRegistry | null;
175178
private _moduleRegistry: ModuleRegistry;
@@ -193,6 +196,7 @@ export default class Runtime {
193196
private readonly _transitiveShouldMock: Map<string, boolean>;
194197
private _unmockList: RegExp | undefined;
195198
private readonly _virtualMocks: Map<string, boolean>;
199+
private readonly _virtualModuleMocks: Map<string, boolean>;
196200
private _moduleImplementation?: typeof nativeModule.Module;
197201
private readonly jestObjectCaches: Map<string, Jest>;
198202
private jestGlobals?: JestGlobals;
@@ -212,11 +216,14 @@ export default class Runtime {
212216
this._currentlyExecutingModulePath = '';
213217
this._environment = environment;
214218
this._explicitShouldMock = new Map();
219+
this._explicitShouldMockModule = new Map();
215220
this._internalModuleRegistry = new Map();
216221
this._isCurrentlyExecutingManualMock = null;
217222
this._mainModule = null;
218223
this._mockFactories = new Map();
219224
this._mockRegistry = new Map();
225+
this._moduleMockRegistry = new Map();
226+
this._moduleMockFactories = new Map();
220227
invariant(
221228
this._environment.moduleMocker,
222229
'`moduleMocker` must be set on an environment when created',
@@ -236,6 +243,7 @@ export default class Runtime {
236243
this._fileTransforms = new Map();
237244
this._fileTransformsMutex = new Map();
238245
this._virtualMocks = new Map();
246+
this._virtualModuleMocks = new Map();
239247
this.jestObjectCaches = new Map();
240248

241249
this._mockMetaDataCache = new Map();
@@ -499,6 +507,16 @@ export default class Runtime {
499507

500508
const [path, query] = specifier.split('?');
501509

510+
if (
511+
this._shouldMock(
512+
referencingIdentifier,
513+
path,
514+
this._explicitShouldMockModule,
515+
)
516+
) {
517+
return this.importMock(referencingIdentifier, path, context);
518+
}
519+
502520
const resolved = this._resolveModule(referencingIdentifier, path);
503521

504522
if (
@@ -539,6 +557,8 @@ export default class Runtime {
539557
async unstable_importModule(
540558
from: Config.Path,
541559
moduleName?: string,
560+
// TODO: implement this
561+
_isImportActual = false,
542562
): Promise<void> {
543563
invariant(
544564
runtimeSupportsVmModules,
@@ -588,6 +608,109 @@ export default class Runtime {
588608
return evaluateSyntheticModule(module);
589609
}
590610

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

@@ -619,7 +742,7 @@ export default class Runtime {
619742
from: Config.Path,
620743
moduleName?: string,
621744
options?: InternalModuleOptions,
622-
isRequireActual?: boolean | null,
745+
isRequireActual = false,
623746
): T {
624747
const moduleID = this._resolver.getModuleID(
625748
this._virtualMocks,
@@ -743,17 +866,12 @@ export default class Runtime {
743866
moduleName,
744867
);
745868

746-
if (
747-
this._isolatedMockRegistry &&
748-
this._isolatedMockRegistry.get(moduleID)
749-
) {
750-
return this._isolatedMockRegistry.get(moduleID);
751-
} else if (this._mockRegistry.get(moduleID)) {
752-
return this._mockRegistry.get(moduleID);
753-
}
754-
755869
const mockRegistry = this._isolatedMockRegistry || this._mockRegistry;
756870

871+
if (mockRegistry.get(moduleID)) {
872+
return mockRegistry.get(moduleID);
873+
}
874+
757875
if (this._mockFactories.has(moduleID)) {
758876
// has check above makes this ok
759877
const module = this._mockFactories.get(moduleID)!();
@@ -869,7 +987,7 @@ export default class Runtime {
869987
}
870988

871989
try {
872-
if (this._shouldMock(from, moduleName)) {
990+
if (this._shouldMock(from, moduleName, this._explicitShouldMock)) {
873991
return this.requireMock<T>(from, moduleName);
874992
} else {
875993
return this.requireModule<T>(from, moduleName);
@@ -925,6 +1043,7 @@ export default class Runtime {
9251043
this._moduleRegistry.clear();
9261044
this._esmoduleRegistry.clear();
9271045
this._cjsNamedExports.clear();
1046+
this._moduleMockRegistry.clear();
9281047

9291048
if (this._environment) {
9301049
if (this._environment.global) {
@@ -1014,6 +1133,26 @@ export default class Runtime {
10141133
this._mockFactories.set(moduleID, mockFactory);
10151134
}
10161135

1136+
private setModuleMock(
1137+
from: string,
1138+
moduleName: string,
1139+
mockFactory: () => Promise<unknown> | unknown,
1140+
options?: {virtual?: boolean},
1141+
): void {
1142+
if (options?.virtual) {
1143+
const mockPath = this._resolver.getModulePath(from, moduleName);
1144+
1145+
this._virtualModuleMocks.set(mockPath, true);
1146+
}
1147+
const moduleID = this._resolver.getModuleID(
1148+
this._virtualModuleMocks,
1149+
from,
1150+
moduleName,
1151+
);
1152+
this._explicitShouldMockModule.set(moduleID, true);
1153+
this._moduleMockFactories.set(moduleID, mockFactory);
1154+
}
1155+
10171156
restoreAllMocks(): void {
10181157
this._moduleMocker.restoreAllMocks();
10191158
}
@@ -1034,12 +1173,15 @@ export default class Runtime {
10341173
this._internalModuleRegistry.clear();
10351174
this._mainModule = null;
10361175
this._mockFactories.clear();
1176+
this._moduleMockFactories.clear();
10371177
this._mockMetaDataCache.clear();
10381178
this._shouldMockModuleCache.clear();
10391179
this._shouldUnmockTransitiveDependenciesCache.clear();
10401180
this._explicitShouldMock.clear();
1181+
this._explicitShouldMockModule.clear();
10411182
this._transitiveShouldMock.clear();
10421183
this._virtualMocks.clear();
1184+
this._virtualModuleMocks.clear();
10431185
this._cacheFS.clear();
10441186
this._unmockList = undefined;
10451187

@@ -1479,8 +1621,11 @@ export default class Runtime {
14791621
);
14801622
}
14811623

1482-
private _shouldMock(from: Config.Path, moduleName: string): boolean {
1483-
const explicitShouldMock = this._explicitShouldMock;
1624+
private _shouldMock(
1625+
from: Config.Path,
1626+
moduleName: string,
1627+
explicitShouldMock: Map<string, boolean>,
1628+
): boolean {
14841629
const moduleID = this._resolver.getModuleID(
14851630
this._virtualMocks,
14861631
from,
@@ -1650,6 +1795,24 @@ export default class Runtime {
16501795
this.setMock(from, moduleName, mockFactory, options);
16511796
return jestObject;
16521797
};
1798+
const mockModule: Jest['mockModule'] = (
1799+
moduleName,
1800+
mockFactory,
1801+
options,
1802+
) => {
1803+
if (mockFactory !== undefined) {
1804+
this.setModuleMock(from, moduleName, mockFactory, options);
1805+
return jestObject;
1806+
}
1807+
1808+
const moduleID = this._resolver.getModuleID(
1809+
this._virtualMocks,
1810+
from,
1811+
moduleName,
1812+
);
1813+
this._explicitShouldMockModule.set(moduleID, true);
1814+
return jestObject;
1815+
};
16531816
const clearAllMocks = () => {
16541817
this.clearAllMocks();
16551818
return jestObject;
@@ -1748,6 +1911,7 @@ export default class Runtime {
17481911
isMockFunction: this._moduleMocker.isMockFunction,
17491912
isolateModules,
17501913
mock,
1914+
mockModule,
17511915
requireActual: this.requireActual.bind(this, from),
17521916
requireMock: this.requireMock.bind(this, from),
17531917
resetAllMocks,

0 commit comments

Comments
 (0)