- For imported CJS, if it's not customized by asynchronous hooks, make sure it won't use the quirky re-invented require in all cases. - When the imported CJS module is customized by synchronous hooks, in the synthetic module evalutation step, avoid calling the respective default step again. - Make the branching of loadCJSModuleWithModuleLoad() and loadCJSModuleWithSpecialRequire() more explicit, and fold the tentative fs read in the 'commonjs' translator into the share createCJSModuleWrap() helper instead of checking it twice in the same path. Signed-off-by: Joyee Cheung <joyeec9h3@gmail.com> PR-URL: https://github.com/nodejs/node/pull/62920 Fixes: https://github.com/nodejs/node/issues/63060 Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
1031 lines
28 KiB
JavaScript
1031 lines
28 KiB
JavaScript
'use strict';
|
|
const {
|
|
ArrayPrototypeFilter,
|
|
ArrayPrototypePush,
|
|
ArrayPrototypeSlice,
|
|
Error,
|
|
FunctionPrototypeBind,
|
|
FunctionPrototypeCall,
|
|
ObjectAssign,
|
|
ObjectDefineProperty,
|
|
ObjectGetOwnPropertyDescriptor,
|
|
ObjectGetPrototypeOf,
|
|
ObjectKeys,
|
|
Proxy,
|
|
ReflectApply,
|
|
ReflectConstruct,
|
|
ReflectGet,
|
|
SafeMap,
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* 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, 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,
|
|
format,
|
|
fullPath,
|
|
moduleExports,
|
|
sharedState,
|
|
specifier,
|
|
}) {
|
|
const config = {
|
|
__proto__: null,
|
|
cache,
|
|
moduleExports,
|
|
caller,
|
|
};
|
|
|
|
sharedState.mockMap.set(baseURL, config);
|
|
sharedState.mockMap.set(fullPath, config);
|
|
|
|
this.#sharedState = sharedState;
|
|
this.#restore = {
|
|
__proto__: null,
|
|
baseURL,
|
|
cached: fullPath in Module._cache,
|
|
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];
|
|
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;
|
|
}
|
|
|
|
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);
|
|
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;
|
|
const ctx = new MockModuleContext({
|
|
__proto__: null,
|
|
baseURL: baseURL.href,
|
|
cache,
|
|
caller,
|
|
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 keys = ObjectKeys(from);
|
|
|
|
for (let i = 0; i < keys.length; ++i) {
|
|
const key = keys[i];
|
|
const descriptor = ObjectGetOwnPropertyDescriptor(from, key);
|
|
ObjectDefineProperty(to, key, descriptor);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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,
|
|
MockTracker,
|
|
};
|