Skip to content

Commit 4d6bf2b

Browse files
KhaledElmorsycpojer
authored andcommitted
fix(jest-runtime, jest-resolver): Allow core models to be mocked with "node:" URL paths (#14297)
Allow modules imported and mocked with 'node:*' URLs to be manually mocked by resolving just the path when generating module ID's for setting and checking mocks, and when resolving module names when importing mocks.
1 parent 69f0c89 commit 4d6bf2b

File tree

12 files changed

+136
-33
lines changed

12 files changed

+136
-33
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@
223223

224224
### Fixes
225225

226+
- `[jest-runtime, jest-resolve]` Allow core modules to be mocked with the `node:` URL scheme ([#14297](https://github.com/jestjs/jest/pull/14297))
226227
- `[jest-circus]` Prevent false test failures caused by promise rejections handled asynchronously ([#14110](https://github.com/jestjs/jest/pull/14110))
227228
- `[jest-config]` Handle frozen config object ([#14054](https://github.com/facebook/jest/pull/14054))
228229
- `[jest-config]` Allow `coverageDirectory` and `collectCoverageFrom` in project config ([#14180](https://github.com/jestjs/jest/pull/14180))

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 @@ exports[`moduleNameMapper wrong array configuration 1`] = `
4141
12 | module.exports = () => 'test';
4242
13 |
4343
44-
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:1177:17)
44+
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:1184:17)
4545
at Object.require (index.js:10:1)
4646
at Object.require (__tests__/index.js:10:20)"
4747
`;
@@ -71,7 +71,7 @@ exports[`moduleNameMapper wrong configuration 1`] = `
7171
12 | module.exports = () => 'test';
7272
13 |
7373
74-
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:1177:17)
74+
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:1184:17)
7575
at Object.require (index.js:10:1)
7676
at Object.require (__tests__/index.js:10:20)"
7777
`;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import runJest from '../runJest';
9+
10+
test('supports node url manual mocks', () => {
11+
const result = runJest('node-url-manual-mocks');
12+
expect(result.exitCode).toBe(0);
13+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
const {mock} = require('../testUtils');
8+
console.log(mock);
9+
module.exports = mock;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
const fs = require('node:fs');
8+
const {expectModuleMocked} = require('../testUtils');
9+
10+
jest.mock('node:fs');
11+
12+
it('correctly mocks the module', () => {
13+
expectModuleMocked(fs);
14+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
const fs = require('node:fs');
8+
const {expectModuleMocked} = require('../testUtils');
9+
10+
jest.mock('fs');
11+
12+
it('correctly mocks the module', () => {
13+
expectModuleMocked(fs);
14+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
const fs = require('fs');
8+
const {expectModuleMocked} = require('../testUtils');
9+
10+
jest.mock('node:fs');
11+
12+
it('correctly mocks the module', () => {
13+
expectModuleMocked(fs);
14+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"jest": {
3+
"testEnvironment": "node"
4+
}
5+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
const mock = {};
8+
module.exports.mock = mock;
9+
10+
module.exports.expectModuleMocked = module => {
11+
// eslint-disable-next-line no-undef
12+
expect(module).toBe(mock);
13+
};

jest.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export default {
3030
'packages/jest-runtime/src/__tests__/test_root.*',
3131
'website/.*',
3232
'e2e/runtime-internal-module-registry/__mocks__',
33+
'e2e/node-url-manual-mocks/__mocks__',
3334
],
3435
projects: ['<rootDir>', '<rootDir>/examples/*/'],
3536
snapshotFormat: {

packages/jest-resolve/src/resolver.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,12 @@ export default class Resolver {
459459
);
460460
}
461461

462+
normalizeCoreModuleSpecifier(specifier: string): string {
463+
return specifier.startsWith('node:')
464+
? specifier.slice('node:'.length)
465+
: specifier;
466+
}
467+
462468
getModule(name: string): string | null {
463469
return this._moduleMap.getModule(
464470
name,
@@ -491,9 +497,9 @@ export default class Resolver {
491497
if (mock) {
492498
return mock;
493499
} else {
494-
const moduleName = this.resolveStubModuleName(from, name, options);
495-
if (moduleName) {
496-
return this.getModule(moduleName) || moduleName;
500+
const resolvedName = this.resolveStubModuleName(from, name, options);
501+
if (resolvedName) {
502+
return this._moduleMap.getMockModule(resolvedName) ?? null;
497503
}
498504
}
499505
return null;
@@ -508,13 +514,13 @@ export default class Resolver {
508514
if (mock) {
509515
return mock;
510516
} else {
511-
const moduleName = await this.resolveStubModuleNameAsync(
517+
const resolvedName = await this.resolveStubModuleNameAsync(
512518
from,
513519
name,
514520
options,
515521
);
516-
if (moduleName) {
517-
return this.getModule(moduleName) || moduleName;
522+
if (resolvedName) {
523+
return this._moduleMap.getMockModule(resolvedName) ?? null;
518524
}
519525
}
520526
return null;
@@ -626,7 +632,7 @@ export default class Resolver {
626632
options: ResolveModuleConfig,
627633
): string | null {
628634
if (this.isCoreModule(moduleName)) {
629-
return moduleName;
635+
return this.normalizeCoreModuleSpecifier(moduleName);
630636
}
631637
if (moduleName.startsWith('data:')) {
632638
return moduleName;
@@ -793,6 +799,11 @@ export default class Resolver {
793799
moduleName: string,
794800
options?: Pick<ResolveModuleConfig, 'conditions'>,
795801
): Promise<string | null> {
802+
// Strip node URL scheme from core modules imported using it
803+
if (this.isCoreModule(moduleName)) {
804+
return this.normalizeCoreModuleSpecifier(moduleName);
805+
}
806+
796807
const dirname = path.dirname(from);
797808

798809
const {extensions, moduleDirectory, paths} = this._prepareForResolution(

packages/jest-runtime/src/index.ts

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,20 +1062,28 @@ export default class Runtime {
10621062
return module as T;
10631063
}
10641064

1065-
const manualMockOrStub = this._resolver.getMockModule(
1066-
from,
1067-
moduleName,
1068-
options,
1069-
);
1065+
/** Resolved mock module path from (potentially aliased) module name. */
1066+
const manualMockPath: string | null = (() => {
1067+
// Attempt to get manual mock path when moduleName is a:
1068+
1069+
// A. Core module specifier i.e. ['fs', 'node:fs']:
1070+
// Normalize then check for a root manual mock '<rootDir>/__mocks__/'
1071+
if (this._resolver.isCoreModule(moduleName)) {
1072+
const moduleWithoutNodePrefix =
1073+
this._resolver.normalizeCoreModuleSpecifier(moduleName);
1074+
return this._resolver.getMockModule(
1075+
from,
1076+
moduleWithoutNodePrefix,
1077+
options,
1078+
);
1079+
}
10701080

1071-
let modulePath =
1072-
this._resolver.getMockModule(from, moduleName, options) ||
1073-
this._resolveCjsModule(from, moduleName);
1081+
// B. Node module specifier i.e. ['jest', 'react']:
1082+
// Look for root manual mock
1083+
const rootMock = this._resolver.getMockModule(from, moduleName, options);
1084+
if (rootMock) return rootMock;
10741085

1075-
let isManualMock =
1076-
manualMockOrStub &&
1077-
!this._resolver.resolveStubModuleName(from, moduleName, options);
1078-
if (!isManualMock) {
1086+
// C. Relative/Absolute path:
10791087
// If the actual module file has a __mocks__ dir sitting immediately next
10801088
// to it, look to see if there is a manual mock for this file.
10811089
//
@@ -1087,7 +1095,7 @@ export default class Runtime {
10871095
// Where some other module does a relative require into each of the
10881096
// respective subDir{1,2} directories and expects a manual mock
10891097
// corresponding to that particular my_module.js file.
1090-
1098+
const modulePath = this._resolveCjsModule(from, moduleName);
10911099
const moduleDir = path.dirname(modulePath);
10921100
const moduleFileName = path.basename(modulePath);
10931101
const potentialManualMock = path.join(
@@ -1096,26 +1104,28 @@ export default class Runtime {
10961104
moduleFileName,
10971105
);
10981106
if (fs.existsSync(potentialManualMock)) {
1099-
isManualMock = true;
1100-
modulePath = potentialManualMock;
1107+
return potentialManualMock;
11011108
}
1102-
}
1103-
if (isManualMock) {
1109+
1110+
return null;
1111+
})();
1112+
1113+
if (manualMockPath) {
11041114
const localModule: InitialModule = {
11051115
children: [],
11061116
exports: {},
1107-
filename: modulePath,
1108-
id: modulePath,
1117+
filename: manualMockPath,
1118+
id: manualMockPath,
11091119
isPreloading: false,
11101120
loaded: false,
1111-
path: path.dirname(modulePath),
1121+
path: path.dirname(manualMockPath),
11121122
};
11131123

11141124
this._loadModule(
11151125
localModule,
11161126
from,
11171127
moduleName,
1118-
modulePath,
1128+
manualMockPath,
11191129
undefined,
11201130
mockRegistry,
11211131
);
@@ -1747,9 +1757,7 @@ export default class Runtime {
17471757

17481758
private _requireCoreModule(moduleName: string, supportPrefix: boolean) {
17491759
const moduleWithoutNodePrefix =
1750-
supportPrefix && moduleName.startsWith('node:')
1751-
? moduleName.slice('node:'.length)
1752-
: moduleName;
1760+
supportPrefix && this._resolver.normalizeCoreModuleSpecifier(moduleName);
17531761

17541762
if (moduleWithoutNodePrefix === 'process') {
17551763
return this._environment.global.process;

0 commit comments

Comments
 (0)