'use strict';
const {
ArrayPrototypeFilter,
ArrayPrototypePush,
ArrayPrototypeSlice,
Error,
FunctionPrototypeBind,
FunctionPrototypeCall,
ObjectAssign,
ObjectDefineProperties,
ObjectDefineProperty,
ObjectGetOwnPropertyDescriptor,
ObjectGetOwnPropertyDescriptors,
ObjectGetPrototypeOf,
ObjectKeys,
ObjectSetPrototypeOf,
ObjectValues,
PromiseReject,
PromiseResolve,
Proxy,
ReflectApply,
ReflectConstruct,
ReflectGet,
SafeMap,
StringPrototypeIncludes,
StringPrototypeSlice,
StringPrototypeStartsWith,
} = primordials;
const {
codes: {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_STATE,
},
} = require('internal/errors');
const esmLoader = require('internal/modules/esm/loader');
const { getOptionValue } = require('internal/options');
const {
fileURLToPath,
isURL,
pathToFileURL,
URLParse,
} = require('internal/url');
const {
deprecateProperty,
emitExperimentalWarning,
getStructuredStack,
kEmptyObject,
} = require('internal/util');
let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
debug = fn;
});
const {
validateBoolean,
validateFunction,
validateInteger,
validateObject,
validateOneOf,
} = require('internal/validators');
const { MockTimers } = require('internal/test_runner/mock/mock_timers');
const {
Module,
} = require('internal/modules/cjs/loader');
const {
_load,
_nodeModulePaths,
_resolveFilename,
isBuiltin,
} = Module;
function kDefaultFunction() {}
const enableModuleMocking = getOptionValue('--experimental-test-module-mocks');
const kSupportedFormats = [
'builtin',
'commonjs-typescript',
'commonjs',
'json',
'module-typescript',
'module',
];
let sharedModuleState;
const deprecateNamedExports = deprecateProperty(
'namedExports',
'mock.module(): options.namedExports is deprecated. Use options.exports instead.',
);
const deprecateDefaultExport = deprecateProperty(
'defaultExport',
'mock.module(): options.defaultExport is deprecated. Use options.exports.default instead.',
);
const {
hooks: mockHooks,
mocks,
constants: { kBadExportsMessage, kMockSearchParam },
} = require('internal/test_runner/mock/loader');
class MockFunctionContext {
#calls;
#mocks;
#implementation;
#restore;
#times;
constructor(implementation, restore, times) {
this.#calls = [];
this.#mocks = new SafeMap();
this.#implementation = implementation;
this.#restore = restore;
this.#times = times;
}
/**
* Gets an array of recorded calls made to the mock function.
* @returns {Array} An array of recorded calls.
*/
get calls() {
return ArrayPrototypeSlice(this.#calls, 0);
}
/**
* Retrieves the number of times the mock function has been called.
* @returns {number} The call count.
*/
callCount() {
return this.#calls.length;
}
/**
* Sets a new implementation for the mock function.
* @param {Function} implementation - The new implementation for the mock function.
*/
mockImplementation(implementation) {
validateFunction(implementation, 'implementation');
this.#implementation = implementation;
}
/**
* Replaces the implementation of the function only once.
* @param {Function} implementation - The substitute function.
* @param {number} [onCall] - The call index to be replaced.
*/
mockImplementationOnce(implementation, onCall) {
validateFunction(implementation, 'implementation');
const nextCall = this.#calls.length;
const call = onCall ?? nextCall;
validateInteger(call, 'onCall', nextCall);
this.#mocks.set(call, implementation);
}
/**
* Sets a new implementation that always returns the given value.
* @param {any} value - The value to return.
*/
mockReturnValue(value) {
this.#implementation = function() { return value; };
}
/**
* Sets a new implementation that returns a promise resolved with the given value.
* @param {any} value - The value to resolve with.
*/
mockResolvedValue(value) {
this.#implementation = function() { return PromiseResolve(value); };
}
/**
* Sets a new implementation that returns a promise rejected with the given reason.
* @param {any} reason - The reason to reject with.
*/
mockRejectedValue(reason) {
this.#implementation = function() { return PromiseReject(reason); };
}
/**
* Sets a new implementation that always throws the given value.
* @param {any} value - The value to throw.
*/
mockThrownValue(value) {
this.#implementation = function() { throw value; };
}
/**
* Restores the original function that was mocked.
*/
restore() {
const { descriptor, object, original, methodName } = this.#restore;
if (typeof methodName === 'string') {
// This is an object method spy.
ObjectDefineProperty(object, methodName, { __proto__: null, ...descriptor });
} else {
// This is a bare function spy. There isn't much to do here but make
// the mock call the original function.
this.#implementation = original;
}
}
/**
* Resets the recorded calls to the mock function
*/
resetCalls() {
this.#calls = [];
}
/**
* Tracks a call made to the mock function.
* @param {object} call - The call details.
*/
trackCall(call) {
ArrayPrototypePush(this.#calls, call);
}
/**
* Gets the next implementation to use for the mock function.
* @returns {Function} The next implementation.
*/
nextImpl() {
const nextCall = this.#calls.length;
const mock = this.#mocks.get(nextCall);
const impl = mock ?? this.#implementation;
if (nextCall + 1 === this.#times) {
this.restore();
}
this.#mocks.delete(nextCall);
return impl;
}
}
const {
nextImpl,
restore: restoreFn,
trackCall,
} = MockFunctionContext.prototype;
delete MockFunctionContext.prototype.trackCall;
delete MockFunctionContext.prototype.nextImpl;
class MockModuleContext {
#restore;
#sharedState;
constructor({
baseURL,
cache,
caller,
cjsPath,
format,
fullPath,
moduleExports,
sharedState,
specifier,
}) {
const config = {
__proto__: null,
cache,
moduleExports,
caller,
};
sharedState.mockMap.set(baseURL, config);
sharedState.mockMap.set(fullPath, config);
// For dual packages (e.g., a package with a "exports" field that exposes
// both ESM and CJS entry points), the file selected by the ESM resolver
// (used to compute fullPath) may differ from the one selected by CJS
// require(). Register the CJS-resolved path so that require() also picks
// up the mock. See https://github.com/nodejs/node/issues/58231.
if (cjsPath !== null && cjsPath !== fullPath) {
sharedState.mockMap.set(cjsPath, config);
}
this.#sharedState = sharedState;
this.#restore = {
__proto__: null,
baseURL,
cached: fullPath in Module._cache,
cjsPath,
cjsCached: cjsPath !== null && cjsPath !== fullPath &&
cjsPath in Module._cache,
cjsValue: cjsPath !== null && cjsPath !== fullPath ?
Module._cache[cjsPath] : undefined,
format,
fullPath,
value: Module._cache[fullPath],
};
const mock = mocks.get(baseURL);
if (mock?.active) {
debug('already mocking "%s"', baseURL);
throw new ERR_INVALID_STATE(
`Cannot mock '${specifier}'. The module is already mocked.`,
);
} else {
const localVersion = mock?.localVersion ?? 0;
debug('new mock version %d for "%s"', localVersion, baseURL);
mocks.set(baseURL, {
__proto__: null,
url: baseURL,
cache,
exportNames: ArrayPrototypeFilter(ObjectKeys(moduleExports), (k) => k !== 'default'),
hasDefaultExport: 'default' in moduleExports,
format,
localVersion,
active: true,
});
}
delete Module._cache[fullPath];
if (cjsPath !== null && cjsPath !== fullPath) {
delete Module._cache[cjsPath];
}
sharedState.mockExports.set(baseURL, {
__proto__: null,
moduleExports,
});
}
restore() {
if (this.#restore === undefined) {
return;
}
// Delete the mock CJS cache entry. If the module was previously in the
// cache then restore the old value.
delete Module._cache[this.#restore.fullPath];
if (this.#restore.cached) {
Module._cache[this.#restore.fullPath] = this.#restore.value;
}
if (this.#restore.cjsPath !== null &&
this.#restore.cjsPath !== this.#restore.fullPath) {
delete Module._cache[this.#restore.cjsPath];
if (this.#restore.cjsCached) {
Module._cache[this.#restore.cjsPath] = this.#restore.cjsValue;
}
}
const mock = mocks.get(this.#restore.baseURL);
if (mock !== undefined) {
mock.active = false;
mock.localVersion++;
}
this.#sharedState.mockMap.delete(this.#restore.baseURL);
this.#sharedState.mockMap.delete(this.#restore.fullPath);
if (this.#restore.cjsPath !== null &&
this.#restore.cjsPath !== this.#restore.fullPath) {
this.#sharedState.mockMap.delete(this.#restore.cjsPath);
}
this.#restore = undefined;
}
}
const { restore: restoreModule } = MockModuleContext.prototype;
class MockPropertyContext {
#object;
#propertyName;
#value;
#originalValue;
#descriptor;
#accesses;
#onceValues;
constructor(object, propertyName, value) {
this.#onceValues = new SafeMap();
this.#accesses = [];
this.#object = object;
this.#propertyName = propertyName;
this.#originalValue = object[propertyName];
this.#value = arguments.length > 2 ? value : this.#originalValue;
this.#descriptor = ObjectGetOwnPropertyDescriptor(object, propertyName);
if (!this.#descriptor) {
throw new ERR_INVALID_ARG_VALUE(
'propertyName', propertyName, 'is not a property of the object',
);
}
const { configurable, enumerable } = this.#descriptor;
ObjectDefineProperty(object, propertyName, {
__proto__: null,
configurable,
enumerable,
get: () => {
const nextValue = this.#getAccessValue(this.#value);
const access = {
__proto__: null,
type: 'get',
value: nextValue,
// eslint-disable-next-line no-restricted-syntax
stack: new Error(),
};
ArrayPrototypePush(this.#accesses, access);
return nextValue;
},
set: this.mockImplementation.bind(this),
});
}
/**
* Gets an array of recorded accesses (get/set) to the property.
* @returns {Array} An array of access records.
*/
get accesses() {
return ArrayPrototypeSlice(this.#accesses, 0);
}
/**
* Retrieves the number of times the property was accessed (get or set).
* @returns {number} The total number of accesses.
*/
accessCount() {
return this.#accesses.length;
}
/**
* Sets a new value for the property.
* @param {any} value - The new value to be set.
* @throws {Error} If the property is not writable.
*/
mockImplementation(value) {
if (!this.#descriptor.writable) {
throw new ERR_INVALID_ARG_VALUE(
'propertyName', this.#propertyName, 'cannot be set',
);
}
const nextValue = this.#getAccessValue(value);
const access = {
__proto__: null,
type: 'set',
value: nextValue,
// eslint-disable-next-line no-restricted-syntax
stack: new Error(),
};
ArrayPrototypePush(this.#accesses, access);
this.#value = nextValue;
}
#getAccessValue(value) {
const accessIndex = this.#accesses.length;
let accessValue;
if (this.#onceValues.has(accessIndex)) {
accessValue = this.#onceValues.get(accessIndex);
this.#onceValues.delete(accessIndex);
} else {
accessValue = value;
}
return accessValue;
}
/**
* Sets a value to be used only for the next access (get or set), or a specific access index.
* @param {any} value - The value to be used once.
* @param {number} [onAccess] - The access index to be replaced.
*/
mockImplementationOnce(value, onAccess) {
const nextAccess = this.#accesses.length;
const accessIndex = onAccess ?? nextAccess;
validateInteger(accessIndex, 'onAccess', nextAccess);
this.#onceValues.set(accessIndex, value);
}
/**
* Resets the recorded accesses to the property.
*/
resetAccesses() {
this.#accesses = [];
}
/**
* Restores the original value of the property that was mocked.
*/
restore() {
ObjectDefineProperty(this.#object, this.#propertyName, {
__proto__: null,
...this.#descriptor,
value: this.#originalValue,
});
}
}
const { restore: restoreProperty } = MockPropertyContext.prototype;
class MockTracker {
#mocks = [];
#timers;
/**
* Returns the mock timers of this MockTracker instance.
* @returns {MockTimers} The mock timers instance.
*/
get timers() {
this.#timers ??= new MockTimers();
return this.#timers;
}
/**
* Creates a mock function tracker.
* @param {Function} [original] - The original function to be tracked.
* @param {Function} [implementation] - An optional replacement function for the original one.
* @param {object} [options] - Additional tracking options.
* @param {number} [options.times] - The maximum number of times the mock function can be called.
* @returns {ProxyConstructor} The mock function tracker.
*/
fn(
original = function() {},
implementation = original,
options = kEmptyObject,
) {
if (original !== null && typeof original === 'object') {
options = original;
original = function() {};
implementation = original;
} else if (implementation !== null && typeof implementation === 'object') {
options = implementation;
implementation = original;
}
validateFunction(original, 'original');
validateFunction(implementation, 'implementation');
validateObject(options, 'options');
const { times = Infinity } = options;
validateTimes(times, 'options.times');
const ctx = new MockFunctionContext(implementation, { __proto__: null, original }, times);
return this.#setupMock(ctx, original);
}
/**
* Creates a method tracker for a specified object or function.
* @param {(object | Function)} objectOrFunction - The object or function containing the method to be tracked.
* @param {string} methodName - The name of the method to be tracked.
* @param {Function} [implementation] - An optional replacement function for the original method.
* @param {object} [options] - Additional tracking options.
* @param {boolean} [options.getter] - Indicates whether this is a getter method.
* @param {boolean} [options.setter] - Indicates whether this is a setter method.
* @param {number} [options.times] - The maximum number of times the mock method can be called.
* @returns {ProxyConstructor} The mock method tracker.
*/
method(
objectOrFunction,
methodName,
implementation = kDefaultFunction,
options = kEmptyObject,
) {
validateStringOrSymbol(methodName, 'methodName');
if (typeof objectOrFunction !== 'function') {
validateObject(objectOrFunction, 'object');
}
if (implementation !== null && typeof implementation === 'object') {
options = implementation;
implementation = kDefaultFunction;
}
validateFunction(implementation, 'implementation');
validateObject(options, 'options');
const {
getter = false,
setter = false,
times = Infinity,
} = options;
validateBoolean(getter, 'options.getter');
validateBoolean(setter, 'options.setter');
validateTimes(times, 'options.times');
if (setter && getter) {
throw new ERR_INVALID_ARG_VALUE(
'options.setter', setter, "cannot be used with 'options.getter'",
);
}
const descriptor = findMethodOnPrototypeChain(objectOrFunction, methodName);
let original;
if (getter) {
original = descriptor?.get;
} else if (setter) {
original = descriptor?.set;
} else {
original = descriptor?.value;
}
if (typeof original !== 'function') {
throw new ERR_INVALID_ARG_VALUE(
'methodName', original, 'must be a method',
);
}
const restore = { __proto__: null, descriptor, object: objectOrFunction, methodName };
const impl = implementation === kDefaultFunction ?
original : implementation;
const ctx = new MockFunctionContext(impl, restore, times);
const mock = this.#setupMock(ctx, original);
const mockDescriptor = {
__proto__: null,
configurable: descriptor.configurable,
enumerable: descriptor.enumerable,
};
if (getter) {
mockDescriptor.get = mock;
mockDescriptor.set = descriptor.set;
} else if (setter) {
mockDescriptor.get = descriptor.get;
mockDescriptor.set = mock;
} else {
mockDescriptor.writable = descriptor.writable;
mockDescriptor.value = mock;
}
ObjectDefineProperty(objectOrFunction, methodName, mockDescriptor);
return mock;
}
/**
* Mocks a getter method of an object.
* This is a syntax sugar for the MockTracker.method with options.getter set to true
* @param {object} object - The target object.
* @param {string} methodName - The name of the getter method to be mocked.
* @param {Function} [implementation] - An optional replacement function for the targeted method.
* @param {object} [options] - Additional tracking options.
* @param {boolean} [options.getter] - Indicates whether this is a getter method.
* @param {boolean} [options.setter] - Indicates whether this is a setter method.
* @param {number} [options.times] - The maximum number of times the mock method can be called.
* @returns {ProxyConstructor} The mock method tracker.
*/
getter(
object,
methodName,
implementation = kDefaultFunction,
options = kEmptyObject,
) {
if (implementation !== null && typeof implementation === 'object') {
options = implementation;
implementation = kDefaultFunction;
} else {
validateObject(options, 'options');
}
const { getter = true } = options;
if (getter === false) {
throw new ERR_INVALID_ARG_VALUE(
'options.getter', getter, 'cannot be false',
);
}
return this.method(object, methodName, implementation, {
__proto__: null,
...options,
getter,
});
}
/**
* Mocks a setter method of an object.
* This function is a syntax sugar for MockTracker.method with options.setter set to true.
* @param {object} object - The target object.
* @param {string} methodName - The setter method to be mocked.
* @param {Function} [implementation] - An optional replacement function for the targeted method.
* @param {object} [options] - Additional tracking options.
* @param {boolean} [options.getter] - Indicates whether this is a getter method.
* @param {boolean} [options.setter] - Indicates whether this is a setter method.
* @param {number} [options.times] - The maximum number of times the mock method can be called.
* @returns {ProxyConstructor} The mock method tracker.
*/
setter(
object,
methodName,
implementation = kDefaultFunction,
options = kEmptyObject,
) {
if (implementation !== null && typeof implementation === 'object') {
options = implementation;
implementation = kDefaultFunction;
} else {
validateObject(options, 'options');
}
const { setter = true } = options;
if (setter === false) {
throw new ERR_INVALID_ARG_VALUE(
'options.setter', setter, 'cannot be false',
);
}
return this.method(object, methodName, implementation, {
__proto__: null,
...options,
setter,
});
}
module(specifier, options = kEmptyObject) {
emitExperimentalWarning('Module mocking');
if (typeof specifier !== 'string') {
if (!isURL(specifier))
throw new ERR_INVALID_ARG_TYPE('specifier', ['string', 'URL'], specifier);
specifier = `${specifier}`;
}
validateObject(options, 'options');
debug('module mock entry, specifier = "%s", options = %o', specifier, options);
const {
cache,
moduleExports,
} = normalizeModuleMockOptions(options);
const sharedState = setupSharedModuleState();
const mockSpecifier = StringPrototypeStartsWith(specifier, 'node:') ?
StringPrototypeSlice(specifier, 5) : specifier;
// Get the file that called this function. We need four stack frames:
// vm context -> getStructuredStack() -> this function -> actual caller.
const filename = getStructuredStack()[3]?.getFileName();
// If the caller is already a file URL, use it as is. Otherwise, convert it.
const hasFileProtocol = StringPrototypeStartsWith(filename, 'file://');
const caller = hasFileProtocol ? filename : pathToFileURL(filename).href;
const request = { __proto__: null, specifier: mockSpecifier, attributes: kEmptyObject };
const { format, url } = sharedState.moduleLoader.resolveSync(caller, request);
debug('module mock, url = "%s", format = "%s", caller = "%s"', url, format, caller);
if (format) { // Format is not yet known for ambiguous files when detection is enabled.
validateOneOf(format, 'format', kSupportedFormats);
}
const baseURL = URLParse(url);
if (!baseURL) {
throw new ERR_INVALID_ARG_VALUE(
'specifier', specifier, 'cannot compute URL',
);
}
if (baseURL.searchParams.has(kMockSearchParam)) {
throw new ERR_INVALID_STATE(
`Cannot mock '${specifier}'. The module is already mocked.`,
);
}
const fullPath = StringPrototypeStartsWith(url, 'file://') ?
fileURLToPath(url) : null;
// For dual packages, the ESM resolver may return a different file than
// CJS require() would for the same specifier (e.g., when a package's
// "exports" field points to different files for the "import" and
// "require" conditions). Compute the CJS-resolved path so that
// require() of a mocked module also picks up the mock.
// See https://github.com/nodejs/node/issues/58231.
const cjsPath = resolveAsCJS(mockSpecifier, caller, fullPath);
const ctx = new MockModuleContext({
__proto__: null,
baseURL: baseURL.href,
cache,
caller,
cjsPath,
format,
fullPath,
moduleExports,
sharedState,
specifier: mockSpecifier,
});
ArrayPrototypePush(this.#mocks, {
__proto__: null,
ctx,
restore: restoreModule,
});
return ctx;
}
/**
* Creates a property tracker for a specified object.
* @param {(object)} object - The object whose value is being tracked.
* @param {string} propertyName - The identifier of the property on object to be tracked.
* @param {any} value - An optional replacement value used as the mock value for object[valueName].
* @returns {ProxyConstructor} The mock property tracker.
*/
property(
object,
propertyName,
value,
) {
validateObject(object, 'object');
validateStringOrSymbol(propertyName, 'propertyName');
const ctx = arguments.length > 2 ?
new MockPropertyContext(object, propertyName, value) :
new MockPropertyContext(object, propertyName);
ArrayPrototypePush(this.#mocks, {
__proto__: null,
ctx,
restore: restoreProperty,
});
return new Proxy(object, {
__proto__: null,
get(target, property, receiver) {
if (property === 'mock') {
return ctx;
}
return ReflectGet(target, property, receiver);
},
});
}
/**
* Resets the mock tracker, restoring all mocks and clearing timers.
*/
reset() {
this.restoreAll();
this.#timers?.reset();
this.#mocks = [];
}
/**
* Restore all mocks created by this MockTracker instance.
*/
restoreAll() {
for (let i = 0; i < this.#mocks.length; i++) {
const { ctx, restore } = this.#mocks[i];
FunctionPrototypeCall(restore, ctx);
}
}
#setupMock(ctx, fnToMatch) {
const mock = new Proxy(fnToMatch, {
__proto__: null,
apply(_fn, thisArg, argList) {
const fn = FunctionPrototypeCall(nextImpl, ctx);
let result;
let error;
try {
result = ReflectApply(fn, thisArg, argList);
} catch (err) {
error = err;
throw err;
} finally {
FunctionPrototypeCall(trackCall, ctx, {
__proto__: null,
arguments: argList,
error,
result,
// eslint-disable-next-line no-restricted-syntax
stack: new Error(),
target: undefined,
this: thisArg,
});
}
return result;
},
construct(target, argList, newTarget) {
const realTarget = FunctionPrototypeCall(nextImpl, ctx);
let result;
let error;
try {
result = ReflectConstruct(realTarget, argList, newTarget);
} catch (err) {
error = err;
throw err;
} finally {
FunctionPrototypeCall(trackCall, ctx, {
__proto__: null,
arguments: argList,
error,
result,
// eslint-disable-next-line no-restricted-syntax
stack: new Error(),
target,
this: result,
});
}
return result;
},
get(target, property, receiver) {
if (property === 'mock') {
return ctx;
}
return ReflectGet(target, property, receiver);
},
});
ArrayPrototypePush(this.#mocks, {
__proto__: null,
ctx,
restore: restoreFn,
});
return mock;
}
}
function normalizeModuleMockOptions(options) {
const { cache = false } = options;
validateBoolean(cache, 'options.cache');
const hasExports = 'exports' in options;
const hasNamedExports = 'namedExports' in options;
const hasDefaultExport = 'defaultExport' in options;
deprecateNamedExports(options);
deprecateDefaultExport(options);
const moduleExports = { __proto__: null };
if (hasExports) {
validateObject(options.exports, 'options.exports');
}
if (hasNamedExports) {
validateObject(options.namedExports, 'options.namedExports');
}
if (hasExports && (hasNamedExports || hasDefaultExport)) {
let reason = "cannot be used with 'options.namedExports'";
if (hasDefaultExport) {
reason = hasNamedExports ?
"cannot be used with 'options.namedExports' or 'options.defaultExport'" :
"cannot be used with 'options.defaultExport'";
}
throw new ERR_INVALID_ARG_VALUE('options.exports', options.exports, reason);
}
if (hasExports) {
copyOwnProperties(options.exports, moduleExports);
}
if (hasNamedExports) {
copyOwnProperties(options.namedExports, moduleExports);
}
if (hasDefaultExport) {
ObjectDefineProperty(
moduleExports,
'default',
ObjectAssign({ __proto__: null }, ObjectGetOwnPropertyDescriptor(options, 'defaultExport')),
);
}
return {
__proto__: null,
cache,
moduleExports,
};
}
function copyOwnProperties(from, to) {
const descriptors = ObjectGetOwnPropertyDescriptors(from);
const descValues = ObjectValues(descriptors);
for (let i = 0; i < descValues.length; ++i) {
ObjectSetPrototypeOf(descValues[i], null);
}
ObjectDefineProperties(to, descriptors);
}
function setupSharedModuleState() {
if (sharedModuleState === undefined) {
const { mock } = require('test');
const mockExports = new SafeMap();
const { registerHooks } = require('internal/modules/customization_hooks');
const moduleLoader = esmLoader.getOrInitializeCascadedLoader();
registerHooks(mockHooks);
sharedModuleState = {
__proto__: null,
mockExports,
mockMap: new SafeMap(),
moduleLoader,
};
mock._mockExports = mockExports;
Module._load = FunctionPrototypeBind(cjsMockModuleLoad, sharedModuleState);
}
return sharedModuleState;
}
function cjsMockModuleLoad(request, parent, isMain) {
// Imported mocked URLs may re-enter Module._load with the mock query attached.
// Strip it to pass into methods that expect a normal request.
// TODO(joyeecheung): it might be better to strip the search params from the filename in
// the translator but that might have a bigger blast radius as other mocker might have also
// come to rely on this to create multiple cache identities for the same module.
try {
const parsedRequest = URLParse(request);
if (parsedRequest?.searchParams.has(kMockSearchParam)) {
parsedRequest.searchParams.delete(kMockSearchParam);
request = parsedRequest.href;
}
} catch {
// Not a valid URL, treat as a normal request.
}
let resolved;
if (isBuiltin(request)) {
resolved = ensureNodeScheme(request);
} else {
resolved = _resolveFilename(request, parent, isMain);
}
const config = this.mockMap.get(resolved);
if (config === undefined) {
return _load(request, parent, isMain);
}
const {
cache,
caller,
moduleExports,
} = config;
if (cache && Module._cache[resolved]) {
// The CJS cache entry is deleted when the mock is configured. If it has
// been repopulated, return the exports from that entry.
return Module._cache[resolved].exports;
}
const hasDefaultExport = 'default' in moduleExports;
// eslint-disable-next-line node-core/set-proto-to-null-in-object
const modExports = hasDefaultExport ? moduleExports.default : {};
const exportNames = ArrayPrototypeFilter(ObjectKeys(moduleExports), (k) => k !== 'default');
if ((typeof modExports !== 'object' || modExports === null) &&
exportNames.length > 0) {
// eslint-disable-next-line no-restricted-syntax
throw new Error(kBadExportsMessage);
}
for (let i = 0; i < exportNames.length; ++i) {
const name = exportNames[i];
const descriptor = ObjectGetOwnPropertyDescriptor(moduleExports, name);
ObjectDefineProperty(modExports, name, descriptor);
}
if (cache) {
const entry = new Module(resolved, caller);
entry.exports = modExports;
entry.filename = resolved;
entry.loaded = true;
entry.paths = _nodeModulePaths(entry.path);
Module._cache[resolved] = entry;
}
return modExports;
}
// Resolve `specifier` using CJS resolution rules so that mocks for dual
// packages (e.g., a package whose "exports" field points to different files
// for the "import" and "require" conditions) also intercept require().
// Returns an absolute file path on success, or null when the specifier cannot
// be resolved as CJS (for example, when the package is ESM-only or when it is
// a non-file URL such as data: or node:).
function resolveAsCJS(specifier, callerURL, esmFullPath) {
if (isBuiltin(specifier) ||
StringPrototypeStartsWith(specifier, 'node:') ||
StringPrototypeStartsWith(specifier, 'data:')) {
return null;
}
let parentPath;
if (StringPrototypeStartsWith(callerURL, 'file://')) {
try {
parentPath = fileURLToPath(callerURL);
} catch {
return null;
}
} else {
return null;
}
try {
const tmpModule = new Module(parentPath, null);
tmpModule.paths = _nodeModulePaths(parentPath);
const resolved = _resolveFilename(specifier, tmpModule, false);
if (typeof resolved !== 'string') {
return null;
}
// If the resolution matches what the ESM resolver picked, there is
// nothing additional to register.
if (resolved === esmFullPath) {
return esmFullPath;
}
// If the resolution returned something that is not a filesystem path
// (e.g., a builtin id without a slash or backslash), ignore it.
if (!StringPrototypeIncludes(resolved, '/') &&
!StringPrototypeIncludes(resolved, '\\')) {
return null;
}
return resolved;
} catch {
return null;
}
}
function validateStringOrSymbol(value, name) {
if (typeof value !== 'string' && typeof value !== 'symbol') {
throw new ERR_INVALID_ARG_TYPE(name, ['string', 'symbol'], value);
}
}
function validateTimes(value, name) {
if (value === Infinity) {
return;
}
validateInteger(value, name, 1);
}
function findMethodOnPrototypeChain(instance, methodName) {
let host = instance;
let descriptor;
while (host !== null) {
descriptor = ObjectGetOwnPropertyDescriptor(host, methodName);
if (descriptor) {
break;
}
host = ObjectGetPrototypeOf(host);
}
return descriptor;
}
function ensureNodeScheme(specifier) {
if (!StringPrototypeStartsWith(specifier, 'node:')) {
return `node:${specifier}`;
}
return specifier;
}
if (!enableModuleMocking) {
delete MockTracker.prototype.module;
}
module.exports = {
ensureNodeScheme,
MockFunctionContext,
MockTracker,
};