1'use strict'; 2const { 3 ArrayPrototypePush, 4 ArrayPrototypeSlice, 5 Error, 6 FunctionPrototypeCall, 7 ObjectDefineProperty, 8 ObjectGetOwnPropertyDescriptor, 9 ObjectGetPrototypeOf, 10 Proxy, 11 ReflectApply, 12 ReflectConstruct, 13 ReflectGet, 14 SafeMap, 15} = primordials; 16const { 17 codes: { 18 ERR_INVALID_ARG_TYPE, 19 ERR_INVALID_ARG_VALUE, 20 }, 21} = require('internal/errors'); 22const { kEmptyObject } = require('internal/util'); 23const { 24 validateBoolean, 25 validateFunction, 26 validateInteger, 27 validateObject, 28} = require('internal/validators'); 29const { MockTimers } = require('internal/test_runner/mock/mock_timers'); 30 31function kDefaultFunction() {} 32 33class MockFunctionContext { 34 #calls; 35 #mocks; 36 #implementation; 37 #restore; 38 #times; 39 40 constructor(implementation, restore, times) { 41 this.#calls = []; 42 this.#mocks = new SafeMap(); 43 this.#implementation = implementation; 44 this.#restore = restore; 45 this.#times = times; 46 } 47 48 /** 49 * Gets an array of recorded calls made to the mock function. 50 * @returns {Array} An array of recorded calls. 51 */ 52 get calls() { 53 return ArrayPrototypeSlice(this.#calls, 0); 54 } 55 56 /** 57 * Retrieves the number of times the mock function has been called. 58 * @returns {number} The call count. 59 */ 60 callCount() { 61 return this.#calls.length; 62 } 63 64 /** 65 * Sets a new implementation for the mock function. 66 * @param {Function} implementation - The new implementation for the mock function. 67 */ 68 mockImplementation(implementation) { 69 validateFunction(implementation, 'implementation'); 70 this.#implementation = implementation; 71 } 72 73 /** 74 * Replaces the implementation of the function only once. 75 * @param {Function} implementation - The substitute function. 76 * @param {number} [onCall] - The call index to be replaced. 77 */ 78 mockImplementationOnce(implementation, onCall) { 79 validateFunction(implementation, 'implementation'); 80 const nextCall = this.#calls.length; 81 const call = onCall ?? nextCall; 82 validateInteger(call, 'onCall', nextCall); 83 this.#mocks.set(call, implementation); 84 } 85 86 /** 87 * Restores the original function that was mocked. 88 */ 89 restore() { 90 const { descriptor, object, original, methodName } = this.#restore; 91 92 if (typeof methodName === 'string') { 93 // This is an object method spy. 94 ObjectDefineProperty(object, methodName, descriptor); 95 } else { 96 // This is a bare function spy. There isn't much to do here but make 97 // the mock call the original function. 98 this.#implementation = original; 99 } 100 } 101 102 /** 103 * Resets the recorded calls to the mock function 104 */ 105 resetCalls() { 106 this.#calls = []; 107 } 108 109 /** 110 * Tracks a call made to the mock function. 111 * @param {object} call - The call details. 112 */ 113 trackCall(call) { 114 ArrayPrototypePush(this.#calls, call); 115 } 116 117 /** 118 * Gets the next implementation to use for the mock function. 119 * @returns {Function} The next implementation. 120 */ 121 nextImpl() { 122 const nextCall = this.#calls.length; 123 const mock = this.#mocks.get(nextCall); 124 const impl = mock ?? this.#implementation; 125 126 if (nextCall + 1 === this.#times) { 127 this.restore(); 128 } 129 130 this.#mocks.delete(nextCall); 131 return impl; 132 } 133} 134 135const { nextImpl, restore, trackCall } = MockFunctionContext.prototype; 136delete MockFunctionContext.prototype.trackCall; 137delete MockFunctionContext.prototype.nextImpl; 138 139class MockTracker { 140 #mocks = []; 141 #timers; 142 143 /** 144 * Returns the mock timers of this MockTracker instance. 145 * @returns {MockTimers} The mock timers instance. 146 */ 147 get timers() { 148 this.#timers ??= new MockTimers(); 149 return this.#timers; 150 } 151 152 /** 153 * Creates a mock function tracker. 154 * @param {Function} [original] - The original function to be tracked. 155 * @param {Function} [implementation] - An optional replacement function for the original one. 156 * @param {object} [options] - Additional tracking options. 157 * @param {number} [options.times=Infinity] - The maximum number of times the mock function can be called. 158 * @returns {ProxyConstructor} The mock function tracker. 159 */ 160 fn( 161 original = function() {}, 162 implementation = original, 163 options = kEmptyObject, 164 ) { 165 if (original !== null && typeof original === 'object') { 166 options = original; 167 original = function() {}; 168 implementation = original; 169 } else if (implementation !== null && typeof implementation === 'object') { 170 options = implementation; 171 implementation = original; 172 } 173 174 validateFunction(original, 'original'); 175 validateFunction(implementation, 'implementation'); 176 validateObject(options, 'options'); 177 const { times = Infinity } = options; 178 validateTimes(times, 'options.times'); 179 const ctx = new MockFunctionContext(implementation, { __proto__: null, original }, times); 180 return this.#setupMock(ctx, original); 181 } 182 183 /** 184 * Creates a method tracker for a specified object or function. 185 * @param {(object | Function)} objectOrFunction - The object or function containing the method to be tracked. 186 * @param {string} methodName - The name of the method to be tracked. 187 * @param {Function} [implementation] - An optional replacement function for the original method. 188 * @param {object} [options] - Additional tracking options. 189 * @param {boolean} [options.getter=false] - Indicates whether this is a getter method. 190 * @param {boolean} [options.setter=false] - Indicates whether this is a setter method. 191 * @param {number} [options.times=Infinity] - The maximum number of times the mock method can be called. 192 * @returns {ProxyConstructor} The mock method tracker. 193 */ 194 method( 195 objectOrFunction, 196 methodName, 197 implementation = kDefaultFunction, 198 options = kEmptyObject, 199 ) { 200 validateStringOrSymbol(methodName, 'methodName'); 201 if (typeof objectOrFunction !== 'function') { 202 validateObject(objectOrFunction, 'object'); 203 } 204 205 if (implementation !== null && typeof implementation === 'object') { 206 options = implementation; 207 implementation = kDefaultFunction; 208 } 209 210 validateFunction(implementation, 'implementation'); 211 validateObject(options, 'options'); 212 213 const { 214 getter = false, 215 setter = false, 216 times = Infinity, 217 } = options; 218 219 validateBoolean(getter, 'options.getter'); 220 validateBoolean(setter, 'options.setter'); 221 validateTimes(times, 'options.times'); 222 223 if (setter && getter) { 224 throw new ERR_INVALID_ARG_VALUE( 225 'options.setter', setter, "cannot be used with 'options.getter'", 226 ); 227 } 228 const descriptor = findMethodOnPrototypeChain(objectOrFunction, methodName); 229 230 let original; 231 232 if (getter) { 233 original = descriptor?.get; 234 } else if (setter) { 235 original = descriptor?.set; 236 } else { 237 original = descriptor?.value; 238 } 239 240 if (typeof original !== 'function') { 241 throw new ERR_INVALID_ARG_VALUE( 242 'methodName', original, 'must be a method', 243 ); 244 } 245 246 const restore = { __proto__: null, descriptor, object: objectOrFunction, methodName }; 247 const impl = implementation === kDefaultFunction ? 248 original : implementation; 249 const ctx = new MockFunctionContext(impl, restore, times); 250 const mock = this.#setupMock(ctx, original); 251 const mockDescriptor = { 252 __proto__: null, 253 configurable: descriptor.configurable, 254 enumerable: descriptor.enumerable, 255 }; 256 257 if (getter) { 258 mockDescriptor.get = mock; 259 mockDescriptor.set = descriptor.set; 260 } else if (setter) { 261 mockDescriptor.get = descriptor.get; 262 mockDescriptor.set = mock; 263 } else { 264 mockDescriptor.writable = descriptor.writable; 265 mockDescriptor.value = mock; 266 } 267 268 ObjectDefineProperty(objectOrFunction, methodName, mockDescriptor); 269 270 return mock; 271 } 272 273 /** 274 * Mocks a getter method of an object. 275 * This is a syntax sugar for the MockTracker.method with options.getter set to true 276 * @param {object} object - The target object. 277 * @param {string} methodName - The name of the getter method to be mocked. 278 * @param {Function} [implementation] - An optional replacement function for the targeted method. 279 * @param {object} [options] - Additional tracking options. 280 * @param {boolean} [options.getter=true] - Indicates whether this is a getter method. 281 * @param {boolean} [options.setter=false] - Indicates whether this is a setter method. 282 * @param {number} [options.times=Infinity] - The maximum number of times the mock method can be called. 283 * @returns {ProxyConstructor} The mock method tracker. 284 */ 285 getter( 286 object, 287 methodName, 288 implementation = kDefaultFunction, 289 options = kEmptyObject, 290 ) { 291 if (implementation !== null && typeof implementation === 'object') { 292 options = implementation; 293 implementation = kDefaultFunction; 294 } else { 295 validateObject(options, 'options'); 296 } 297 298 const { getter = true } = options; 299 300 if (getter === false) { 301 throw new ERR_INVALID_ARG_VALUE( 302 'options.getter', getter, 'cannot be false', 303 ); 304 } 305 306 return this.method(object, methodName, implementation, { 307 __proto__: null, 308 ...options, 309 getter, 310 }); 311 } 312 313 /** 314 * Mocks a setter method of an object. 315 * This function is a syntax sugar for MockTracker.method with options.setter set to true. 316 * @param {object} object - The target object. 317 * @param {string} methodName - The setter method to be mocked. 318 * @param {Function} [implementation] - An optional replacement function for the targeted method. 319 * @param {object} [options] - Additional tracking options. 320 * @param {boolean} [options.getter=false] - Indicates whether this is a getter method. 321 * @param {boolean} [options.setter=true] - Indicates whether this is a setter method. 322 * @param {number} [options.times=Infinity] - The maximum number of times the mock method can be called. 323 * @returns {ProxyConstructor} The mock method tracker. 324 */ 325 setter( 326 object, 327 methodName, 328 implementation = kDefaultFunction, 329 options = kEmptyObject, 330 ) { 331 if (implementation !== null && typeof implementation === 'object') { 332 options = implementation; 333 implementation = kDefaultFunction; 334 } else { 335 validateObject(options, 'options'); 336 } 337 338 const { setter = true } = options; 339 340 if (setter === false) { 341 throw new ERR_INVALID_ARG_VALUE( 342 'options.setter', setter, 'cannot be false', 343 ); 344 } 345 346 return this.method(object, methodName, implementation, { 347 __proto__: null, 348 ...options, 349 setter, 350 }); 351 } 352 353 /** 354 * Resets the mock tracker, restoring all mocks and clearing timers. 355 */ 356 reset() { 357 this.restoreAll(); 358 this.#timers?.reset(); 359 this.#mocks = []; 360 } 361 362 /** 363 * Restore all mocks created by this MockTracker instance. 364 */ 365 restoreAll() { 366 for (let i = 0; i < this.#mocks.length; i++) { 367 FunctionPrototypeCall(restore, this.#mocks[i]); 368 } 369 } 370 371 #setupMock(ctx, fnToMatch) { 372 const mock = new Proxy(fnToMatch, { 373 __proto__: null, 374 apply(_fn, thisArg, argList) { 375 const fn = FunctionPrototypeCall(nextImpl, ctx); 376 let result; 377 let error; 378 379 try { 380 result = ReflectApply(fn, thisArg, argList); 381 } catch (err) { 382 error = err; 383 throw err; 384 } finally { 385 FunctionPrototypeCall(trackCall, ctx, { 386 __proto__: null, 387 arguments: argList, 388 error, 389 result, 390 // eslint-disable-next-line no-restricted-syntax 391 stack: new Error(), 392 target: undefined, 393 this: thisArg, 394 }); 395 } 396 397 return result; 398 }, 399 construct(target, argList, newTarget) { 400 const realTarget = FunctionPrototypeCall(nextImpl, ctx); 401 let result; 402 let error; 403 404 try { 405 result = ReflectConstruct(realTarget, argList, newTarget); 406 } catch (err) { 407 error = err; 408 throw err; 409 } finally { 410 FunctionPrototypeCall(trackCall, ctx, { 411 __proto__: null, 412 arguments: argList, 413 error, 414 result, 415 // eslint-disable-next-line no-restricted-syntax 416 stack: new Error(), 417 target, 418 this: result, 419 }); 420 } 421 422 return result; 423 }, 424 get(target, property, receiver) { 425 if (property === 'mock') { 426 return ctx; 427 } 428 429 return ReflectGet(target, property, receiver); 430 }, 431 }); 432 433 ArrayPrototypePush(this.#mocks, ctx); 434 return mock; 435 } 436} 437 438function validateStringOrSymbol(value, name) { 439 if (typeof value !== 'string' && typeof value !== 'symbol') { 440 throw new ERR_INVALID_ARG_TYPE(name, ['string', 'symbol'], value); 441 } 442} 443 444function validateTimes(value, name) { 445 if (value === Infinity) { 446 return; 447 } 448 449 validateInteger(value, name, 1); 450} 451 452function findMethodOnPrototypeChain(instance, methodName) { 453 let host = instance; 454 let descriptor; 455 456 while (host !== null) { 457 descriptor = ObjectGetOwnPropertyDescriptor(host, methodName); 458 459 if (descriptor) { 460 break; 461 } 462 463 host = ObjectGetPrototypeOf(host); 464 } 465 466 return descriptor; 467} 468 469module.exports = { MockTracker }; 470