Skip to content

Commit 9d7f9cc

Browse files
committed
feat(runtime): add jest.mockModule
1 parent 26b2237 commit 9d7f9cc

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
@@ -146,6 +146,14 @@ export interface Jest {
146146
moduleFactory?: () => unknown,
147147
options?: {virtual?: boolean},
148148
): Jest;
149+
/**
150+
* Mocks a module with an auto-mocked version when it is being required.
151+
*/
152+
mockModule(
153+
moduleName: string,
154+
moduleFactory?: () => Promise<unknown> | unknown,
155+
options?: {virtual?: boolean},
156+
): Jest;
149157
/**
150158
* Returns the actual module instead of a mock, bypassing all checks on
151159
* 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
@@ -154,6 +154,7 @@ export default class Runtime {
154154
private _currentlyExecutingModulePath: string;
155155
private readonly _environment: JestEnvironment;
156156
private readonly _explicitShouldMock: Map<string, boolean>;
157+
private readonly _explicitShouldMockModule: Map<string, boolean>;
157158
private _fakeTimersImplementation:
158159
| LegacyFakeTimers<unknown>
159160
| ModernFakeTimers
@@ -168,6 +169,8 @@ export default class Runtime {
168169
>;
169170
private _mockRegistry: Map<string, any>;
170171
private _isolatedMockRegistry: Map<string, any> | null;
172+
private _moduleMockRegistry: Map<string, VMModule>;
173+
private readonly _moduleMockFactories: Map<string, () => unknown>;
171174
private readonly _moduleMocker: ModuleMocker;
172175
private _isolatedModuleRegistry: ModuleRegistry | null;
173176
private _moduleRegistry: ModuleRegistry;
@@ -190,6 +193,7 @@ export default class Runtime {
190193
private readonly _transitiveShouldMock: Map<string, boolean>;
191194
private _unmockList: RegExp | undefined;
192195
private readonly _virtualMocks: Map<string, boolean>;
196+
private readonly _virtualModuleMocks: Map<string, boolean>;
193197
private _moduleImplementation?: typeof nativeModule.Module;
194198
private readonly jestObjectCaches: Map<string, Jest>;
195199
private jestGlobals?: JestGlobals;
@@ -208,11 +212,14 @@ export default class Runtime {
208212
this._currentlyExecutingModulePath = '';
209213
this._environment = environment;
210214
this._explicitShouldMock = new Map();
215+
this._explicitShouldMockModule = new Map();
211216
this._internalModuleRegistry = new Map();
212217
this._isCurrentlyExecutingManualMock = null;
213218
this._mainModule = null;
214219
this._mockFactories = new Map();
215220
this._mockRegistry = new Map();
221+
this._moduleMockRegistry = new Map();
222+
this._moduleMockFactories = new Map();
216223
invariant(
217224
this._environment.moduleMocker,
218225
'`moduleMocker` must be set on an environment when created',
@@ -231,6 +238,7 @@ export default class Runtime {
231238
this._sourceMapRegistry = new Map();
232239
this._fileTransforms = new Map();
233240
this._virtualMocks = new Map();
241+
this._virtualModuleMocks = new Map();
234242
this.jestObjectCaches = new Map();
235243

236244
this._mockMetaDataCache = new Map();
@@ -452,6 +460,16 @@ export default class Runtime {
452460

453461
const [path, query] = specifier.split('?');
454462

463+
if (
464+
this._shouldMock(
465+
referencingIdentifier,
466+
path,
467+
this._explicitShouldMockModule,
468+
)
469+
) {
470+
return this.importMock(referencingIdentifier, path, context);
471+
}
472+
455473
const resolved = this._resolveModule(referencingIdentifier, path);
456474

457475
if (
@@ -492,6 +510,8 @@ export default class Runtime {
492510
async unstable_importModule(
493511
from: Config.Path,
494512
moduleName?: string,
513+
// TODO: implement this
514+
_isImportActual = false,
495515
): Promise<void> {
496516
invariant(
497517
runtimeSupportsVmModules,
@@ -541,6 +561,109 @@ export default class Runtime {
541561
return evaluateSyntheticModule(module);
542562
}
543563

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

@@ -572,7 +695,7 @@ export default class Runtime {
572695
from: Config.Path,
573696
moduleName?: string,
574697
options?: InternalModuleOptions,
575-
isRequireActual?: boolean | null,
698+
isRequireActual = false,
576699
): T {
577700
const moduleID = this._resolver.getModuleID(
578701
this._virtualMocks,
@@ -609,12 +732,10 @@ export default class Runtime {
609732

610733
if (options?.isInternalModule) {
611734
moduleRegistry = this._internalModuleRegistry;
735+
} else if (this._isolatedModuleRegistry) {
736+
moduleRegistry = this._isolatedModuleRegistry;
612737
} else {
613-
if (this._isolatedModuleRegistry) {
614-
moduleRegistry = this._isolatedModuleRegistry;
615-
} else {
616-
moduleRegistry = this._moduleRegistry;
617-
}
738+
moduleRegistry = this._moduleRegistry;
618739
}
619740

620741
const module = moduleRegistry.get(modulePath);
@@ -681,17 +802,12 @@ export default class Runtime {
681802
moduleName,
682803
);
683804

684-
if (
685-
this._isolatedMockRegistry &&
686-
this._isolatedMockRegistry.get(moduleID)
687-
) {
688-
return this._isolatedMockRegistry.get(moduleID);
689-
} else if (this._mockRegistry.get(moduleID)) {
690-
return this._mockRegistry.get(moduleID);
691-
}
692-
693805
const mockRegistry = this._isolatedMockRegistry || this._mockRegistry;
694806

807+
if (mockRegistry.get(moduleID)) {
808+
return mockRegistry.get(moduleID);
809+
}
810+
695811
if (this._mockFactories.has(moduleID)) {
696812
// has check above makes this ok
697813
const module = this._mockFactories.get(moduleID)!();
@@ -808,7 +924,7 @@ export default class Runtime {
808924
}
809925

810926
try {
811-
if (this._shouldMock(from, moduleName)) {
927+
if (this._shouldMock(from, moduleName, this._explicitShouldMock)) {
812928
return this.requireMock<T>(from, moduleName);
813929
} else {
814930
return this.requireModule<T>(from, moduleName);
@@ -864,6 +980,7 @@ export default class Runtime {
864980
this._moduleRegistry.clear();
865981
this._esmoduleRegistry.clear();
866982
this._cjsNamedExports.clear();
983+
this._moduleMockRegistry.clear();
867984

868985
if (this._environment) {
869986
if (this._environment.global) {
@@ -952,6 +1069,26 @@ export default class Runtime {
9521069
this._mockFactories.set(moduleID, mockFactory);
9531070
}
9541071

1072+
private setModuleMock(
1073+
from: string,
1074+
moduleName: string,
1075+
mockFactory: () => Promise<unknown> | unknown,
1076+
options?: {virtual?: boolean},
1077+
): void {
1078+
if (options?.virtual) {
1079+
const mockPath = this._resolver.getModulePath(from, moduleName);
1080+
1081+
this._virtualModuleMocks.set(mockPath, true);
1082+
}
1083+
const moduleID = this._resolver.getModuleID(
1084+
this._virtualModuleMocks,
1085+
from,
1086+
moduleName,
1087+
);
1088+
this._explicitShouldMockModule.set(moduleID, true);
1089+
this._moduleMockFactories.set(moduleID, mockFactory);
1090+
}
1091+
9551092
restoreAllMocks(): void {
9561093
this._moduleMocker.restoreAllMocks();
9571094
}
@@ -972,12 +1109,15 @@ export default class Runtime {
9721109
this._internalModuleRegistry.clear();
9731110
this._mainModule = null;
9741111
this._mockFactories.clear();
1112+
this._moduleMockFactories.clear();
9751113
this._mockMetaDataCache.clear();
9761114
this._shouldMockModuleCache.clear();
9771115
this._shouldUnmockTransitiveDependenciesCache.clear();
9781116
this._explicitShouldMock.clear();
1117+
this._explicitShouldMockModule.clear();
9791118
this._transitiveShouldMock.clear();
9801119
this._virtualMocks.clear();
1120+
this._virtualModuleMocks.clear();
9811121
this._cacheFS.clear();
9821122
this._unmockList = undefined;
9831123

@@ -1375,8 +1515,11 @@ export default class Runtime {
13751515
);
13761516
}
13771517

1378-
private _shouldMock(from: Config.Path, moduleName: string): boolean {
1379-
const explicitShouldMock = this._explicitShouldMock;
1518+
private _shouldMock(
1519+
from: Config.Path,
1520+
moduleName: string,
1521+
explicitShouldMock: Map<string, boolean>,
1522+
): boolean {
13801523
const moduleID = this._resolver.getModuleID(
13811524
this._virtualMocks,
13821525
from,
@@ -1544,6 +1687,24 @@ export default class Runtime {
15441687
this.setMock(from, moduleName, mockFactory, options);
15451688
return jestObject;
15461689
};
1690+
const mockModule: Jest['mockModule'] = (
1691+
moduleName,
1692+
mockFactory,
1693+
options,
1694+
) => {
1695+
if (mockFactory !== undefined) {
1696+
this.setModuleMock(from, moduleName, mockFactory, options);
1697+
return jestObject;
1698+
}
1699+
1700+
const moduleID = this._resolver.getModuleID(
1701+
this._virtualMocks,
1702+
from,
1703+
moduleName,
1704+
);
1705+
this._explicitShouldMockModule.set(moduleID, true);
1706+
return jestObject;
1707+
};
15471708
const clearAllMocks = () => {
15481709
this.clearAllMocks();
15491710
return jestObject;
@@ -1642,6 +1803,7 @@ export default class Runtime {
16421803
isMockFunction: this._moduleMocker.isMockFunction,
16431804
isolateModules,
16441805
mock,
1806+
mockModule,
16451807
requireActual: this.requireActual.bind(this, from),
16461808
requireMock: this.requireMock.bind(this, from),
16471809
resetAllMocks,

0 commit comments

Comments
 (0)