Skip to content

Commit 90d6908

Browse files
authoredSep 7, 2021
feat(runtime): add minimal support for mocking ESM (#11818)
1 parent 3620885 commit 90d6908

File tree

5 files changed

+128
-14
lines changed

5 files changed

+128
-14
lines changed
 

‎CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
### Features
44

5+
- `[jest-runtime]` Add experimental, limited (and undocumented) support for mocking ECMAScript Modules ([#11818](https://github.com/facebook/jest/pull/11818))
6+
57
### Fixes
68

79
- `[jest-types]` Export the `PrettyFormatOptions` interface ([#11801](https://github.com/facebook/jest/pull/11801))

‎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.unstable_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 the provided module factory when it is being imported.
142+
*/
143+
unstable_mockModule<T = unknown>(
144+
moduleName: string,
145+
moduleFactory: () => Promise<T> | T,
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

+106-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 (
@@ -612,6 +630,46 @@ export default class Runtime {
612630
return evaluateSyntheticModule(module);
613631
}
614632

633+
private async importMock<T = unknown>(
634+
from: Config.Path,
635+
moduleName: string,
636+
context: VMContext,
637+
): Promise<T> {
638+
const moduleID = this._resolver.getModuleID(
639+
this._virtualModuleMocks,
640+
from,
641+
moduleName,
642+
);
643+
644+
if (this._moduleMockRegistry.has(moduleID)) {
645+
return this._moduleMockRegistry.get(moduleID);
646+
}
647+
648+
if (this._moduleMockFactories.has(moduleID)) {
649+
const invokedFactory: any = await this._moduleMockFactories.get(
650+
moduleID,
651+
// has check above makes this ok
652+
)!();
653+
654+
const module = new SyntheticModule(
655+
Object.keys(invokedFactory),
656+
function () {
657+
Object.entries(invokedFactory).forEach(([key, value]) => {
658+
// @ts-expect-error: TS doesn't know what `this` is
659+
this.setExport(key, value);
660+
});
661+
},
662+
{context, identifier: moduleName},
663+
);
664+
665+
this._moduleMockRegistry.set(moduleID, module);
666+
667+
return evaluateSyntheticModule(module);
668+
}
669+
670+
throw new Error('Attempting to import a mock without a factory');
671+
}
672+
615673
private getExportsOfCjs(modulePath: Config.Path) {
616674
const cachedNamedExports = this._cjsNamedExports.get(modulePath);
617675

@@ -643,7 +701,7 @@ export default class Runtime {
643701
from: Config.Path,
644702
moduleName?: string,
645703
options?: InternalModuleOptions,
646-
isRequireActual?: boolean | null,
704+
isRequireActual = false,
647705
): T {
648706
const moduleID = this._resolver.getModuleID(
649707
this._virtualMocks,
@@ -770,17 +828,12 @@ export default class Runtime {
770828
moduleName,
771829
);
772830

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-
782831
const mockRegistry = this._isolatedMockRegistry || this._mockRegistry;
783832

833+
if (mockRegistry.get(moduleID)) {
834+
return mockRegistry.get(moduleID);
835+
}
836+
784837
if (this._mockFactories.has(moduleID)) {
785838
// has check above makes this ok
786839
const module = this._mockFactories.get(moduleID)!();
@@ -896,7 +949,7 @@ export default class Runtime {
896949
}
897950

898951
try {
899-
if (this._shouldMock(from, moduleName)) {
952+
if (this._shouldMock(from, moduleName, this._explicitShouldMock)) {
900953
return this.requireMock<T>(from, moduleName);
901954
} else {
902955
return this.requireModule<T>(from, moduleName);
@@ -952,6 +1005,7 @@ export default class Runtime {
9521005
this._moduleRegistry.clear();
9531006
this._esmoduleRegistry.clear();
9541007
this._cjsNamedExports.clear();
1008+
this._moduleMockRegistry.clear();
9551009

9561010
if (this._environment) {
9571011
if (this._environment.global) {
@@ -1043,6 +1097,26 @@ export default class Runtime {
10431097
this._mockFactories.set(moduleID, mockFactory);
10441098
}
10451099

1100+
private setModuleMock(
1101+
from: string,
1102+
moduleName: string,
1103+
mockFactory: () => Promise<unknown> | unknown,
1104+
options?: {virtual?: boolean},
1105+
): void {
1106+
if (options?.virtual) {
1107+
const mockPath = this._resolver.getModulePath(from, moduleName);
1108+
1109+
this._virtualModuleMocks.set(mockPath, true);
1110+
}
1111+
const moduleID = this._resolver.getModuleID(
1112+
this._virtualModuleMocks,
1113+
from,
1114+
moduleName,
1115+
);
1116+
this._explicitShouldMockModule.set(moduleID, true);
1117+
this._moduleMockFactories.set(moduleID, mockFactory);
1118+
}
1119+
10461120
restoreAllMocks(): void {
10471121
this._moduleMocker.restoreAllMocks();
10481122
}
@@ -1063,12 +1137,15 @@ export default class Runtime {
10631137
this._internalModuleRegistry.clear();
10641138
this._mainModule = null;
10651139
this._mockFactories.clear();
1140+
this._moduleMockFactories.clear();
10661141
this._mockMetaDataCache.clear();
10671142
this._shouldMockModuleCache.clear();
10681143
this._shouldUnmockTransitiveDependenciesCache.clear();
10691144
this._explicitShouldMock.clear();
1145+
this._explicitShouldMockModule.clear();
10701146
this._transitiveShouldMock.clear();
10711147
this._virtualMocks.clear();
1148+
this._virtualModuleMocks.clear();
10721149
this._cacheFS.clear();
10731150
this._unmockList = undefined;
10741151

@@ -1516,8 +1593,11 @@ export default class Runtime {
15161593
);
15171594
}
15181595

1519-
private _shouldMock(from: Config.Path, moduleName: string): boolean {
1520-
const explicitShouldMock = this._explicitShouldMock;
1596+
private _shouldMock(
1597+
from: Config.Path,
1598+
moduleName: string,
1599+
explicitShouldMock: Map<string, boolean>,
1600+
): boolean {
15211601
const moduleID = this._resolver.getModuleID(
15221602
this._virtualMocks,
15231603
from,
@@ -1687,6 +1767,18 @@ export default class Runtime {
16871767
this.setMock(from, moduleName, mockFactory, options);
16881768
return jestObject;
16891769
};
1770+
const mockModule: Jest['unstable_mockModule'] = (
1771+
moduleName,
1772+
mockFactory,
1773+
options,
1774+
) => {
1775+
if (typeof mockFactory !== 'function') {
1776+
throw new Error('`unstable_mockModule` must be passed a mock factory');
1777+
}
1778+
1779+
this.setModuleMock(from, moduleName, mockFactory, options);
1780+
return jestObject;
1781+
};
16901782
const clearAllMocks = () => {
16911783
this.clearAllMocks();
16921784
return jestObject;
@@ -1821,6 +1913,7 @@ export default class Runtime {
18211913
setTimeout,
18221914
spyOn,
18231915
unmock,
1916+
unstable_mockModule: mockModule,
18241917
useFakeTimers,
18251918
useRealTimers,
18261919
};

0 commit comments

Comments
 (0)
Please sign in to comment.