fix: pass resolve conditions when loading stub module (#15489)

This commit is contained in:
Trung Nguyen 2025-02-14 23:56:58 +07:00 committed by GitHub
parent dc9f98cae4
commit 4a4c031c4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 143 additions and 38 deletions

View File

@ -37,6 +37,7 @@
- `[jest-mock]` Add support for the Explicit Resource Management proposal to use the `using` keyword with `jest.spyOn(object, methodName)` ([#14895](https://github.com/jestjs/jest/pull/14895))
- `[jest-reporters]` Add support for [DEC mode 2026](https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036) ([#15008](https://github.com/jestjs/jest/pull/15008))
- `[jest-resolver]` Support `file://` URLs as paths ([#15154](https://github.com/jestjs/jest/pull/15154))
- `[jest-resolve,jest-runtime,jest-resolve-dependencies]` Pass the conditions when resolving stub modules ([#15489](https://github.com/jestjs/jest/pull/15489))
- `[jest-runtime]` Exposing new modern timers function `jest.advanceTimersToFrame()` from `@jest/fake-timers` ([#14598](https://github.com/jestjs/jest/pull/14598))
- `[jest-runtime]` Support `import.meta.filename` and `import.meta.dirname` (available from [Node 20.11](https://nodejs.org/en/blog/release/v20.11.0)) ([#14854](https://github.com/jestjs/jest/pull/14854))
- `[jest-runtime]` Support `import.meta.resolve` ([#14930](https://github.com/jestjs/jest/pull/14930))

View File

@ -41,7 +41,7 @@ exports[`moduleNameMapper wrong array configuration 1`] = `
12 | module.exports = () => 'test';
13 |
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:1182:17)
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:1184:17)
at Object.require (index.js:10:1)
at Object.require (__tests__/index.js:10:20)"
`;
@ -71,7 +71,7 @@ exports[`moduleNameMapper wrong configuration 1`] = `
12 | module.exports = () => 'test';
13 |
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:1182:17)
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:1184:17)
at Object.require (index.js:10:1)
at Object.require (__tests__/index.js:10:20)"
`;

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @jest-environment jest-environment-jsdom
*/
import {fn} from 'fake-dual-dep2';
test('returns correct message', () => {
expect(fn()).toBe('from browser');
});

View File

@ -0,0 +1,10 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
export function fn () {
return 'from browser';
}

View File

@ -0,0 +1,10 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
export function fn () {
return 'from node';
}

View File

@ -0,0 +1,10 @@
{
"name": "fake-dual-dep2",
"version": "1.0.0",
"exports": {
".": {
"node": "./node.mjs",
"browser": "./browser.mjs"
}
}
}

View File

@ -9,6 +9,9 @@
"testMatch": [
"<rootDir>/**/*.test.*"
],
"moduleNameMapper": {
"^fake-dual-dep2$": "fake-dual-dep2"
},
"transform": {}
}
}

View File

@ -36,6 +36,7 @@ export class DependencyResolver {
resolve(file: string, options?: ResolveModuleConfig): Array<string> {
const dependencies = this._hasteFS.getDependencies(file);
const fallbackOptions: ResolveModuleConfig = {conditions: undefined};
if (!dependencies) {
return [];
}
@ -51,11 +52,15 @@ export class DependencyResolver {
resolvedDependency = this._resolver.resolveModule(
file,
dependency,
options,
options ?? fallbackOptions,
);
} catch {
try {
resolvedDependency = this._resolver.getMockModule(file, dependency);
resolvedDependency = this._resolver.getMockModule(
file,
dependency,
options ?? fallbackOptions,
);
} catch {
// leave resolvedDependency as undefined if nothing can be found
}
@ -73,6 +78,7 @@ export class DependencyResolver {
resolvedMockDependency = this._resolver.getMockModule(
resolvedDependency,
path.basename(dependency),
options ?? fallbackOptions,
);
} catch {
// leave resolvedMockDependency as undefined if nothing can be found

View File

@ -742,7 +742,9 @@ describe('getMockModuleAsync', () => {
} as ResolverConfig);
const src = require.resolve('../');
await resolver.resolveModuleAsync(src, 'dependentModule');
await resolver.resolveModuleAsync(src, 'dependentModule', {
conditions: ['browser'],
});
expect(mockUserResolverAsync.async).toHaveBeenCalled();
expect(mockUserResolverAsync.async.mock.calls[0][0]).toBe(
@ -752,6 +754,10 @@ describe('getMockModuleAsync', () => {
'basedir',
path.dirname(src),
);
expect(mockUserResolverAsync.async.mock.calls[0][1]).toHaveProperty(
'conditions',
['browser'],
);
});
});

View File

@ -341,11 +341,11 @@ export default class Resolver {
resolveModule(
from: string,
moduleName: string,
options?: ResolveModuleConfig,
options: ResolveModuleConfig,
): string {
const dirname = path.dirname(from);
const module =
this.resolveStubModuleName(from, moduleName) ||
this.resolveStubModuleName(from, moduleName, options) ||
this.resolveModuleFromDirIfExists(dirname, moduleName, options);
if (module) return module;
@ -362,7 +362,7 @@ export default class Resolver {
): Promise<string> {
const dirname = path.dirname(from);
const module =
(await this.resolveStubModuleNameAsync(from, moduleName)) ||
(await this.resolveStubModuleNameAsync(from, moduleName, options)) ||
(await this.resolveModuleFromDirIfExistsAsync(
dirname,
moduleName,
@ -482,12 +482,16 @@ export default class Resolver {
);
}
getMockModule(from: string, name: string): string | null {
getMockModule(
from: string,
name: string,
options: Pick<ResolveModuleConfig, 'conditions'>,
): string | null {
const mock = this._moduleMap.getMockModule(name);
if (mock) {
return mock;
} else {
const moduleName = this.resolveStubModuleName(from, name);
const moduleName = this.resolveStubModuleName(from, name, options);
if (moduleName) {
return this.getModule(moduleName) || moduleName;
}
@ -495,12 +499,20 @@ export default class Resolver {
return null;
}
async getMockModuleAsync(from: string, name: string): Promise<string | null> {
async getMockModuleAsync(
from: string,
name: string,
options: Pick<ResolveModuleConfig, 'conditions'>,
): Promise<string | null> {
const mock = this._moduleMap.getMockModule(name);
if (mock) {
return mock;
} else {
const moduleName = await this.resolveStubModuleNameAsync(from, name);
const moduleName = await this.resolveStubModuleNameAsync(
from,
name,
options,
);
if (moduleName) {
return this.getModule(moduleName) || moduleName;
}
@ -536,7 +548,7 @@ export default class Resolver {
virtualMocks: Map<string, boolean>,
from: string,
moduleName = '',
options?: ResolveModuleConfig,
options: ResolveModuleConfig,
): string {
const stringifiedOptions = options ? JSON.stringify(options) : '';
const key = from + path.delimiter + moduleName + stringifiedOptions;
@ -552,7 +564,7 @@ export default class Resolver {
moduleName,
options,
);
const mockPath = this._getMockPath(from, moduleName);
const mockPath = this._getMockPath(from, moduleName, options);
const sep = path.delimiter;
const id =
@ -570,7 +582,7 @@ export default class Resolver {
virtualMocks: Map<string, boolean>,
from: string,
moduleName = '',
options?: ResolveModuleConfig,
options: ResolveModuleConfig,
): Promise<string> {
const stringifiedOptions = options ? JSON.stringify(options) : '';
const key = from + path.delimiter + moduleName + stringifiedOptions;
@ -589,7 +601,7 @@ export default class Resolver {
moduleName,
options,
);
const mockPath = await this._getMockPathAsync(from, moduleName);
const mockPath = await this._getMockPathAsync(from, moduleName, options);
const sep = path.delimiter;
const id =
@ -611,7 +623,7 @@ export default class Resolver {
virtualMocks: Map<string, boolean>,
from: string,
moduleName: string,
options?: ResolveModuleConfig,
options: ResolveModuleConfig,
): string | null {
if (this.isCoreModule(moduleName)) {
return moduleName;
@ -619,7 +631,7 @@ export default class Resolver {
if (moduleName.startsWith('data:')) {
return moduleName;
}
return this._isModuleResolved(from, moduleName)
return this._isModuleResolved(from, moduleName, options)
? this.getModule(moduleName)
: this._getVirtualMockPath(virtualMocks, from, moduleName, options);
}
@ -628,7 +640,7 @@ export default class Resolver {
virtualMocks: Map<string, boolean>,
from: string,
moduleName: string,
options?: ResolveModuleConfig,
options: ResolveModuleConfig,
): Promise<string | null> {
if (this.isCoreModule(moduleName)) {
return moduleName;
@ -639,32 +651,38 @@ export default class Resolver {
const isModuleResolved = await this._isModuleResolvedAsync(
from,
moduleName,
options,
);
return isModuleResolved
? this.getModule(moduleName)
: this._getVirtualMockPathAsync(virtualMocks, from, moduleName, options);
}
private _getMockPath(from: string, moduleName: string): string | null {
private _getMockPath(
from: string,
moduleName: string,
options: Pick<ResolveModuleConfig, 'conditions'>,
): string | null {
return this.isCoreModule(moduleName)
? null
: this.getMockModule(from, moduleName);
: this.getMockModule(from, moduleName, options);
}
private async _getMockPathAsync(
from: string,
moduleName: string,
options: Pick<ResolveModuleConfig, 'conditions'>,
): Promise<string | null> {
return this.isCoreModule(moduleName)
? null
: this.getMockModuleAsync(from, moduleName);
: this.getMockModuleAsync(from, moduleName, options);
}
private _getVirtualMockPath(
virtualMocks: Map<string, boolean>,
from: string,
moduleName: string,
options?: ResolveModuleConfig,
options: ResolveModuleConfig,
): string {
const virtualMockPath = this.getModulePath(from, moduleName);
return virtualMocks.get(virtualMockPath)
@ -688,23 +706,33 @@ export default class Resolver {
: from;
}
private _isModuleResolved(from: string, moduleName: string): boolean {
private _isModuleResolved(
from: string,
moduleName: string,
options: Pick<ResolveModuleConfig, 'conditions'>,
): boolean {
return !!(
this.getModule(moduleName) || this.getMockModule(from, moduleName)
this.getModule(moduleName) ||
this.getMockModule(from, moduleName, options)
);
}
private async _isModuleResolvedAsync(
from: string,
moduleName: string,
options: Pick<ResolveModuleConfig, 'conditions'>,
): Promise<boolean> {
return !!(
this.getModule(moduleName) ||
(await this.getMockModuleAsync(from, moduleName))
(await this.getMockModuleAsync(from, moduleName, options))
);
}
resolveStubModuleName(from: string, moduleName: string): string | null {
resolveStubModuleName(
from: string,
moduleName: string,
options: Pick<ResolveModuleConfig, 'conditions'>,
): string | null {
const dirname = path.dirname(from);
const {extensions, moduleDirectory, paths} = this._prepareForResolution(
@ -727,11 +755,11 @@ export default class Resolver {
let module: string | null = null;
for (const possibleModuleName of possibleModuleNames) {
const updatedName = mapModuleName(possibleModuleName);
module =
this.getModule(updatedName) ||
Resolver.findNodeModule(updatedName, {
basedir: dirname,
conditions: options?.conditions,
extensions,
moduleDirectory,
paths,
@ -763,6 +791,7 @@ export default class Resolver {
async resolveStubModuleNameAsync(
from: string,
moduleName: string,
options?: Pick<ResolveModuleConfig, 'conditions'>,
): Promise<string | null> {
const dirname = path.dirname(from);
@ -791,6 +820,7 @@ export default class Resolver {
this.getModule(updatedName) ||
(await Resolver.findNodeModuleAsync(updatedName, {
basedir: dirname,
conditions: options?.conditions,
extensions,
moduleDirectory,
paths,

View File

@ -925,11 +925,12 @@ export default class Runtime {
isRequireActual = false,
): T {
const isInternal = options?.isInternalModule ?? false;
const resolveModuleOptions = {conditions: this.cjsConditions};
const moduleID = this._resolver.getModuleID(
this._virtualMocks,
from,
moduleName,
{conditions: this.cjsConditions},
resolveModuleOptions,
);
let modulePath: string | undefined;
@ -937,7 +938,8 @@ export default class Runtime {
// to be more explicit.
const moduleResource = moduleName && this._resolver.getModule(moduleName);
const manualMock =
moduleName && this._resolver.getMockModule(from, moduleName);
moduleName &&
this._resolver.getMockModule(from, moduleName, resolveModuleOptions);
if (
!options?.isInternalModule &&
!isRequireActual &&
@ -1043,11 +1045,12 @@ export default class Runtime {
}
requireMock<T = unknown>(from: string, moduleName: string): T {
const options = {conditions: this.cjsConditions};
const moduleID = this._resolver.getModuleID(
this._virtualMocks,
from,
moduleName,
{conditions: this.cjsConditions},
options,
);
if (this._isolatedMockRegistry?.has(moduleID)) {
@ -1065,15 +1068,19 @@ export default class Runtime {
return module as T;
}
const manualMockOrStub = this._resolver.getMockModule(from, moduleName);
const manualMockOrStub = this._resolver.getMockModule(
from,
moduleName,
options,
);
let modulePath =
this._resolver.getMockModule(from, moduleName) ||
this._resolver.getMockModule(from, moduleName, options) ||
this._resolveCjsModule(from, moduleName);
let isManualMock =
manualMockOrStub &&
!this._resolver.resolveStubModuleName(from, moduleName);
!this._resolver.resolveStubModuleName(from, moduleName, options);
if (!isManualMock) {
// If the actual module file has a __mocks__ dir sitting immediately next
// to it, look to see if there is a manual mock for this file.
@ -1507,7 +1514,9 @@ export default class Runtime {
try {
return this._resolveCjsModule(from, moduleName);
} catch (error) {
const module = this._resolver.getMockModule(from, moduleName);
const module = this._resolver.getMockModule(from, moduleName, {
conditions: this.cjsConditions,
});
if (module) {
return module;
@ -1899,8 +1908,9 @@ export default class Runtime {
private _generateMock<T>(from: string, moduleName: string) {
const modulePath =
this._resolver.resolveStubModuleName(from, moduleName) ||
this._resolveCjsModule(from, moduleName);
this._resolver.resolveStubModuleName(from, moduleName, {
conditions: this.cjsConditions,
}) || this._resolveCjsModule(from, moduleName);
if (!this._mockMetaDataCache.has(modulePath)) {
// This allows us to handle circular dependencies while generating an
// automock
@ -1982,7 +1992,11 @@ export default class Runtime {
try {
modulePath = this._resolveCjsModule(from, moduleName);
} catch (error) {
const manualMock = this._resolver.getMockModule(from, moduleName);
const manualMock = this._resolver.getMockModule(
from,
moduleName,
options,
);
if (manualMock) {
this._shouldMockModuleCache.set(moduleID, true);
return true;
@ -2056,6 +2070,7 @@ export default class Runtime {
const manualMock = await this._resolver.getMockModuleAsync(
from,
moduleName,
options,
);
if (manualMock) {
this._shouldMockModuleCache.set(moduleID, true);