| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1971,6 +1971,87 @@ added: v22.3.0 | |||
| 1971 | 1971 | ||
| 1972 | 1972 | Resets the implementation of the mock module. | |
| 1973 | 1973 | ||
| 1974 | + ## Class: `MockPropertyContext` | ||
| 1975 | + | ||
| 1976 | + <!-- YAML | ||
| 1977 | + added: REPLACEME | ||
| 1978 | + --> | ||
| 1979 | + | ||
| 1980 | + The `MockPropertyContext` class is used to inspect or manipulate the behavior | ||
| 1981 | + of property mocks created via the [`MockTracker`][] APIs. | ||
| 1982 | + | ||
| 1983 | + ### `ctx.accesses` | ||
| 1984 | + | ||
| 1985 | + * {Array} | ||
| 1986 | + | ||
| 1987 | + A getter that returns a copy of the internal array used to track accesses (get/set) to | ||
| 1988 | + the mocked property. Each entry in the array is an object with the following properties: | ||
| 1989 | + | ||
| 1990 | + * `type` {string} Either `'get'` or `'set'`, indicating the type of access. | ||
| 1991 | + * `value` {any} The value that was read (for `'get'`) or written (for `'set'`). | ||
| 1992 | + * `stack` {Error} An `Error` object whose stack can be used to determine the | ||
| 1993 | + callsite of the mocked function invocation. | ||
| 1994 | + | ||
| 1995 | + ### `ctx.accessCount()` | ||
| 1996 | + | ||
| 1997 | + * Returns: {integer} The number of times that the property was accessed (read or written). | ||
| 1998 | + | ||
| 1999 | + This function returns the number of times that the property was accessed. | ||
| 2000 | + This function is more efficient than checking `ctx.accesses.length` because | ||
| 2001 | + `ctx.accesses` is a getter that creates a copy of the internal access tracking array. | ||
| 2002 | + | ||
| 2003 | + ### `ctx.mockImplementation(value)` | ||
| 2004 | + | ||
| 2005 | + * `value` {any} The new value to be set as the mocked property value. | ||
| 2006 | + | ||
| 2007 | + This function is used to change the value returned by the mocked property getter. | ||
| 2008 | + | ||
| 2009 | + ### `ctx.mockImplementationOnce(value[, onAccess])` | ||
| 2010 | + | ||
| 2011 | + * `value` {any} The value to be used as the mock's | ||
| 2012 | + implementation for the invocation number specified by `onAccess`. | ||
| 2013 | + * `onAccess` {integer} The invocation number that will use `value`. If | ||
| 2014 | + the specified invocation has already occurred then an exception is thrown. | ||
| 2015 | + **Default:** The number of the next invocation. | ||
| 2016 | + | ||
| 2017 | + This function is used to change the behavior of an existing mock for a single | ||
| 2018 | + invocation. Once invocation `onAccess` has occurred, the mock will revert to | ||
| 2019 | + whatever behavior it would have used had `mockImplementationOnce()` not been | ||
| 2020 | + called. | ||
| 2021 | + | ||
| 2022 | + The following example creates a mock function using `t.mock.property()`, calls the | ||
| 2023 | + mock property, changes the mock implementation to a different value for the | ||
| 2024 | + next invocation, and then resumes its previous behavior. | ||
| 2025 | + | ||
| 2026 | + ```js | ||
| 2027 | + test('changes a mock behavior once', (t) => { | ||
| 2028 | + const obj = { foo: 1 }; | ||
| 2029 | + | ||
| 2030 | + const prop = t.mock.property(obj, 'foo', 5); | ||
| 2031 | + | ||
| 2032 | + assert.strictEqual(obj.foo, 5); | ||
| 2033 | + prop.mock.mockImplementationOnce(25); | ||
| 2034 | + assert.strictEqual(obj.foo, 25); | ||
| 2035 | + assert.strictEqual(obj.foo, 5); | ||
| 2036 | + }); | ||
| 2037 | + ``` | ||
| 2038 | + | ||
| 2039 | + #### Caveat | ||
| 2040 | + | ||
| 2041 | + For consistency with the rest of the mocking API, this function treats both property gets and sets | ||
| 2042 | + as accesses. If a property set occurs at the same access index, the "once" value will be consumed | ||
| 2043 | + by the set operation, and the mocked property value will be changed to the "once" value. This may | ||
| 2044 | + lead to unexpected behavior if you intend the "once" value to only be used for a get operation. | ||
| 2045 | + | ||
| 2046 | + ### `ctx.resetAccesses()` | ||
| 2047 | + | ||
| 2048 | + Resets the access history of the mocked property. | ||
| 2049 | + | ||
| 2050 | + ### `ctx.restore()` | ||
| 2051 | + | ||
| 2052 | + Resets the implementation of the mock property to its original behavior. The | ||
| 2053 | + mock can still be used after calling this function. | ||
| 2054 | + | ||
| 1974 | 2055 | ## Class: `MockTracker` | |
| 1975 | 2056 | ||
| 1976 | 2057 | <!-- YAML | |
@@ -2173,6 +2254,43 @@ test('mocks a builtin module in both module systems', async (t) => { | |||
| 2173 | 2254 | }); | |
| 2174 | 2255 | ``` | |
| 2175 | 2256 | ||
| 2257 | + ### `mock.property(object, propertyName[, value])` | ||
| 2258 | + | ||
| 2259 | + <!-- YAML | ||
| 2260 | + added: REPLACEME | ||
| 2261 | + --> | ||
| 2262 | + | ||
| 2263 | + * `object` {Object} The object whose value is being mocked. | ||
| 2264 | + * `propertyName` {string|symbol} The identifier of the property on `object` to mock. | ||
| 2265 | + * `value` {any} An optional value used as the mock value | ||
| 2266 | + for `object[propertyName]`. **Default:** The original property value. | ||
| 2267 | + * Returns: {Proxy} A proxy to the mocked object. The mocked object contains a | ||
| 2268 | + special `mock` property, which is an instance of [`MockPropertyContext`][], and | ||
| 2269 | + can be used for inspecting and changing the behavior of the mocked property. | ||
| 2270 | + | ||
| 2271 | + Creates a mock for a property value on an object. This allows you to track and control access to a specific property, | ||
| 2272 | + including how many times it is read (getter) or written (setter), and to restore the original value after mocking. | ||
| 2273 | + | ||
| 2274 | + ```js | ||
| 2275 | + test('mocks a property value', (t) => { | ||
| 2276 | + const obj = { foo: 42 }; | ||
| 2277 | + const prop = t.mock.property(obj, 'foo', 100); | ||
| 2278 | + | ||
| 2279 | + assert.strictEqual(obj.foo, 100); | ||
| 2280 | + assert.strictEqual(prop.mock.accessCount(), 1); | ||
| 2281 | + assert.strictEqual(prop.mock.accesses[0].type, 'get'); | ||
| 2282 | + assert.strictEqual(prop.mock.accesses[0].value, 100); | ||
| 2283 | + | ||
| 2284 | + obj.foo = 200; | ||
| 2285 | + assert.strictEqual(prop.mock.accessCount(), 2); | ||
| 2286 | + assert.strictEqual(prop.mock.accesses[1].type, 'set'); | ||
| 2287 | + assert.strictEqual(prop.mock.accesses[1].value, 200); | ||
| 2288 | + | ||
| 2289 | + prop.mock.restore(); | ||
| 2290 | + assert.strictEqual(obj.foo, 42); | ||
| 2291 | + }); | ||
| 2292 | + ``` | ||
| 2293 | + | ||
| 2176 | 2294 | ### `mock.reset()` | |
| 2177 | 2295 | ||
| 2178 | 2296 | <!-- YAML | |
@@ -3703,6 +3821,7 @@ Can be used to abort test subtasks when the test has been aborted. | |||
| 3703 | 3821 | [`--test-update-snapshots`]: cli.md#--test-update-snapshots | |
| 3704 | 3822 | [`--test`]: cli.md#--test | |
| 3705 | 3823 | [`MockFunctionContext`]: #class-mockfunctioncontext | |
| 3824 | + [`MockPropertyContext`]: #class-mockpropertycontext | ||
| 3706 | 3825 | [`MockTimers`]: #class-mocktimers | |
| 3707 | 3826 | [`MockTracker.method`]: #mockmethodobject-methodname-implementation-options | |
| 3708 | 3827 | [`MockTracker`]: #class-mocktracker | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -284,6 +284,134 @@ class MockModuleContext { | |||
| 284 | 284 | ||
| 285 | 285 | const { restore: restoreModule } = MockModuleContext.prototype; | |
| 286 | 286 | ||
| 287 | + class MockPropertyContext { | ||
| 288 | + #object; | ||
| 289 | + #propertyName; | ||
| 290 | + #value; | ||
| 291 | + #originalValue; | ||
| 292 | + #descriptor; | ||
| 293 | + #accesses; | ||
| 294 | + #onceValues; | ||
| 295 | + | ||
| 296 | + constructor(object, propertyName, value) { | ||
| 297 | + this.#onceValues = new SafeMap(); | ||
| 298 | + this.#accesses = []; | ||
| 299 | + this.#object = object; | ||
| 300 | + this.#propertyName = propertyName; | ||
| 301 | + this.#originalValue = object[propertyName]; | ||
| 302 | + this.#value = arguments.length > 2 ? value : this.#originalValue; | ||
| 303 | + this.#descriptor = ObjectGetOwnPropertyDescriptor(object, propertyName); | ||
| 304 | + if (!this.#descriptor) { | ||
| 305 | + throw new ERR_INVALID_ARG_VALUE( | ||
| 306 | + 'propertyName', propertyName, 'is not a property of the object', | ||
| 307 | + ); | ||
| 308 | + } | ||
| 309 | + | ||
| 310 | + const { configurable, enumerable } = this.#descriptor; | ||
| 311 | + ObjectDefineProperty(object, propertyName, { | ||
| 312 | + __proto__: null, | ||
| 313 | + configurable, | ||
| 314 | + enumerable, | ||
| 315 | + get: () => { | ||
| 316 | + const nextValue = this.#getAccessValue(this.#value); | ||
| 317 | + const access = { | ||
| 318 | + __proto__: null, | ||
| 319 | + type: 'get', | ||
| 320 | + value: nextValue, | ||
| 321 | + // eslint-disable-next-line no-restricted-syntax | ||
| 322 | + stack: new Error(), | ||
| 323 | + }; | ||
| 324 | + ArrayPrototypePush(this.#accesses, access); | ||
| 325 | + return nextValue; | ||
| 326 | + }, | ||
| 327 | + set: this.mockImplementation.bind(this), | ||
| 328 | + }); | ||
| 329 | + } | ||
| 330 | + | ||
| 331 | + /** | ||
| 332 | + * Gets an array of recorded accesses (get/set) to the property. | ||
| 333 | + * @returns {Array} An array of access records. | ||
| 334 | + */ | ||
| 335 | + get accesses() { | ||
| 336 | + return ArrayPrototypeSlice(this.#accesses, 0); | ||
| 337 | + } | ||
| 338 | + | ||
| 339 | + /** | ||
| 340 | + * Retrieves the number of times the property was accessed (get or set). | ||
| 341 | + * @returns {number} The total number of accesses. | ||
| 342 | + */ | ||
| 343 | + accessCount() { | ||
| 344 | + return this.#accesses.length; | ||
| 345 | + } | ||
| 346 | + | ||
| 347 | + /** | ||
| 348 | + * Sets a new value for the property. | ||
| 349 | + * @param {any} value - The new value to be set. | ||
| 350 | + * @throws {Error} If the property is not writable. | ||
| 351 | + */ | ||
| 352 | + mockImplementation(value) { | ||
| 353 | + if (!this.#descriptor.writable) { | ||
| 354 | + throw new ERR_INVALID_ARG_VALUE( | ||
| 355 | + 'propertyName', this.#propertyName, 'cannot be set', | ||
| 356 | + ); | ||
| 357 | + } | ||
| 358 | + const nextValue = this.#getAccessValue(value); | ||
| 359 | + const access = { | ||
| 360 | + __proto__: null, | ||
| 361 | + type: 'set', | ||
| 362 | + value: nextValue, | ||
| 363 | + // eslint-disable-next-line no-restricted-syntax | ||
| 364 | + stack: new Error(), | ||
| 365 | + }; | ||
| 366 | + ArrayPrototypePush(this.#accesses, access); | ||
| 367 | + this.#value = nextValue; | ||
| 368 | + } | ||
| 369 | + | ||
| 370 | + #getAccessValue(value) { | ||
| 371 | + const accessIndex = this.#accesses.length; | ||
| 372 | + let accessValue; | ||
| 373 | + if (this.#onceValues.has(accessIndex)) { | ||
| 374 | + accessValue = this.#onceValues.get(accessIndex); | ||
| 375 | + this.#onceValues.delete(accessIndex); | ||
| 376 | + } else { | ||
| 377 | + accessValue = value; | ||
| 378 | + } | ||
| 379 | + return accessValue; | ||
| 380 | + } | ||
| 381 | + | ||
| 382 | + /** | ||
| 383 | + * Sets a value to be used only for the next access (get or set), or a specific access index. | ||
| 384 | + * @param {any} value - The value to be used once. | ||
| 385 | + * @param {number} [onAccess] - The access index to be replaced. | ||
| 386 | + */ | ||
| 387 | + mockImplementationOnce(value, onAccess) { | ||
| 388 | + const nextAccess = this.#accesses.length; | ||
| 389 | + const accessIndex = onAccess ?? nextAccess; | ||
| 390 | + validateInteger(accessIndex, 'onAccess', nextAccess); | ||
| 391 | + this.#onceValues.set(accessIndex, value); | ||
| 392 | + } | ||
| 393 | + | ||
| 394 | + /** | ||
| 395 | + * Resets the recorded accesses to the property. | ||
| 396 | + */ | ||
| 397 | + resetAccesses() { | ||
| 398 | + this.#accesses = []; | ||
| 399 | + } | ||
| 400 | + | ||
| 401 | + /** | ||
| 402 | + * Restores the original value of the property that was mocked. | ||
| 403 | + */ | ||
| 404 | + restore() { | ||
| 405 | + ObjectDefineProperty(this.#object, this.#propertyName, { | ||
| 406 | + __proto__: null, | ||
| 407 | + ...this.#descriptor, | ||
| 408 | + value: this.#originalValue, | ||
| 409 | + }); | ||
| 410 | + } | ||
| 411 | + } | ||
| 412 | + | ||
| 413 | + const { restore: restoreProperty } = MockPropertyContext.prototype; | ||
| 414 | + | ||
| 287 | 415 | class MockTracker { | |
| 288 | 416 | #mocks = []; | |
| 289 | 417 | #timers; | |
@@ -573,6 +701,41 @@ class MockTracker { | |||
| 573 | 701 | return ctx; | |
| 574 | 702 | } | |
| 575 | 703 | ||
| 704 | + /** | ||
| 705 | + * Creates a property tracker for a specified object. | ||
| 706 | + * @param {(object)} object - The object whose value is being tracked. | ||
| 707 | + * @param {string} propertyName - The identifier of the property on object to be tracked. | ||
| 708 | + * @param {any} value - An optional replacement value used as the mock value for object[valueName]. | ||
| 709 | + * @returns {ProxyConstructor} The mock property tracker. | ||
| 710 | + */ | ||
| 711 | + property( | ||
| 712 | + object, | ||
| 713 | + propertyName, | ||
| 714 | + value, | ||
| 715 | + ) { | ||
| 716 | + validateObject(object, 'object'); | ||
| 717 | + validateStringOrSymbol(propertyName, 'propertyName'); | ||
| 718 | + | ||
| 719 | + const ctx = arguments.length > 2 ? | ||
| 720 | + new MockPropertyContext(object, propertyName, value) : | ||
| 721 | + new MockPropertyContext(object, propertyName); | ||
| 722 | + ArrayPrototypePush(this.#mocks, { | ||
| 723 | + __proto__: null, | ||
| 724 | + ctx, | ||
| 725 | + restore: restoreProperty, | ||
| 726 | + }); | ||
| 727 | + | ||
| 728 | + return new Proxy(object, { | ||
| 729 | + __proto__: null, | ||
| 730 | + get(target, property, receiver) { | ||
| 731 | + if (property === 'mock') { | ||
| 732 | + return ctx; | ||
| 733 | + } | ||
| 734 | + return ReflectGet(target, property, receiver); | ||
| 735 | + }, | ||
| 736 | + }); | ||
| 737 | + } | ||
| 738 | + | ||
| 576 | 739 | /** | |
| 577 | 740 | * Resets the mock tracker, restoring all mocks and clearing timers. | |
| 578 | 741 | */ | |
| Back | FazBrowse Home | New Git URL |
0 commit comments