Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
d596803
doc: add path rules and validation for export targets in package.json
0hmX Jun 9, 2025
893999e
src: replace V8 Fast API todo comment with note comment
dario-piotrowicz Jun 9, 2025
a4d7560
test: close FileHandle objects in tests explicitly
jasnell Jun 9, 2025
713fbad
test_runner: support object property mocking
idango10 Jun 9, 2025
4347ce3
src: add new CopyUtimes function to reduce code duplication
dario-piotrowicz Jun 9, 2025
dc10238
deps: update zlib to 1.3.1-470d3a2
nodejs-github-bot Jun 10, 2025
80eac14
deps: update simdjson to 3.13.0
nodejs-github-bot Jun 10, 2025
3aa2762
deps: update sqlite to 3.50.1
nodejs-github-bot Jun 10, 2025
181014a
test: cleanup status files
panva Jun 10, 2025
e9c6fa5
src: replace std::array with static arrays in contextify
mertcanaltin Jun 10, 2025
85f062c
test: deflake async-hooks/test-improper-order on AIX
bakigul1 Jun 3, 2025
7e34aa4
test: skip tests failing when run under root
LiviaMedeiros Jun 10, 2025
839964e
fs: allow correct handling of burst in fs-events with AsyncIterator
pipobscure Jun 10, 2025
450f481
deps: update amaro to 1.0.0
nodejs-github-bot Jun 11, 2025
76e3c8a
test: update WPT for es-exceptions to 2f96fa1996
nodejs-github-bot Jun 11, 2025
aa657f0
test: split indirect eval import tests
legendecas Jun 11, 2025
a3c7a63
module: allow cycles in require() in the CJS handling in ESM loader
joyeecheung Jun 11, 2025
9b28f40
module: remove experimental warning from type stripping
marco-ippolito Jun 11, 2025
fa089d6
test: update WPT for dom/abort to dc928169ee
nodejs-github-bot Jun 11, 2025
94e53d4
test: update WPT for urlpattern to 3ffda23e5a
nodejs-github-bot Jun 1, 2025
b1f60d2
http2: add diagnostics channel 'http2.server.stream.close'
RaisinTen Jun 11, 2025
b11da11
http2: fix DEP0194 message
climba03003 Jun 11, 2025
afbaf92
tools: improve release proposal linter
aduh95 Jun 11, 2025
6390f70
lib,src: support DOMException ser-des
legendecas Jun 11, 2025
b6760b3
esm: syncify default path of `ModuleLoader.load`
JakobJingleheimer Jun 11, 2025
ffff8ce
typings: add ZSTD_COMPRESS, ZSTD_DECOMPRESS to internalBinding
nektro Jun 12, 2025
45f7d16
module: refactor commonjs typescript loader
marco-ippolito Jun 12, 2025
ff8a369
module: fix typescript import.meta.main
marco-ippolito Jun 12, 2025
c1f9791
tools: edit commit-queue workflow file
aduh95 Jun 12, 2025
1cc77c7
doc: punctuation fix for Node-API versioning clarification
jiacai2050 Jun 12, 2025
268c8c1
tools: remove config.status under `make distclean`
Renegade334 Jun 12, 2025
b22e970
tools: switch to `@stylistic/eslint-plugin`
targos Jun 13, 2025
ec808b3
test: use `common.skipIfInspectorDisabled()` to skip tests
dario-piotrowicz Jun 13, 2025
dfb0144
src: enhance error messages for unknown options
pmarchini Jun 13, 2025
e6a1787
tools: bump brace-expansion from 1.1.11 to 1.1.12 in /tools/eslint
dependabot[bot] Jun 14, 2025
dc2f23e
tools: bump `brace-expansion` in `/tools/clang-format`
dependabot[bot] Jun 14, 2025
ef0230a
url: add fileURLToPathBuffer API
jasnell Jun 12, 2025
b7e488c
test: refactor repl tab complete tests
dario-piotrowicz Jun 15, 2025
c39d570
test: reduce the use of private symbols in test-events-once.js
kt3k Jun 15, 2025
17df800
typings: add Atomics primordials
Renegade334 Jun 4, 2025
704b1fa
test: add tests for REPL custom evals
dario-piotrowicz Jun 15, 2025
841609a
doc: add islandryu to collaborators
islandryu Jun 16, 2025
fb4378b
fs: add UV_ENOSPC to list of things to pass to err directly
jackyzha0 Jun 16, 2025
6f7b895
util: inspect: do not crash on an Error stack pointing to itself
SamVerschueren Jun 16, 2025
82b18ba
repl: fix tab completion not working with computer string properties
dario-piotrowicz Jun 17, 2025
c9e7b5e
test_runner: prefer `Atomics` primordials
Renegade334 Jun 4, 2025
af18c0e
fs: remove IIFE in glob
LiviaMedeiros May 21, 2025
f125310
doc: run license-builder
github-actions[bot] Jun 18, 2025
62b5879
fs: avoid computing time coefficient constants in runtime
LiviaMedeiros Jun 18, 2025
7bb1246
doc: add history entry for TS support in hooks
aduh95 Jun 18, 2025
d94b184
test: deflake test-buffer-large-size-buffer-alloc
lpinca Jun 18, 2025
68671f4
deps: upgrade npm to 11.4.2
npm-cli-bot Jun 20, 2025
6e0ee39
test: skip the test if the buffer allocation fails
lpinca Jun 20, 2025
b5ff3f4
tools: include toolchain.gypi in abseil.gyp
legendecas Jun 20, 2025
be93091
inspector: add protocol methods retrieving sent/received data
legendecas Jun 20, 2025
e2e88d4
doc: fix wrong RFC number in http2
deokjinkim Jun 20, 2025
fa6854f
deps: update amaro to 1.1.0
nodejs-github-bot Jun 20, 2025
96c78d7
fs: make `Dir` disposers idempotent
Renegade334 Jun 20, 2025
4b61f10
deps: V8: cherry-pick e3df60f3f5ab
legendecas Jun 19, 2025
081c708
lib: make domexception a native error
legendecas Jun 12, 2025
a0b1378
src: pass resource on permission checks for spawn
RafaelGSS Jun 21, 2025
8eec789
test: correct SIMD support comment
richardlau Jun 21, 2025
9df1cfe
tools: make nodedownload module compatible with Python 3.14
frenzymadness Jun 22, 2025
3faa4e8
test: deflake test-buffer-large-size-buffer-alloc-unsafe
lpinca Jun 22, 2025
d9c2b70
test: refactor repl save-load tests
dario-piotrowicz Jun 22, 2025
d0e42ff
repl: avoid deprecated `require.extensions` in tab completion
gulbaki Jun 23, 2025
6ef7329
Revert "test_runner: automatically wait for subtests to finish"
romainmenke May 11, 2025
8b0c5ed
Revert "test_runner: remove promises returned by test()"
romainmenke May 11, 2025
dce1995
Revert "test_runner: remove promises returned by t.test()"
romainmenke May 11, 2025
7cdda92
test: fix test-timeout-flag after revert of auto subtest wait
pmarchini May 14, 2025
2ba2c93
build: fix typo 'Stoage' to 'Storage' in help text
ganglike248 Jun 23, 2025
7643ce9
deps: update acorn to 8.15.0
nodejs-github-bot Jun 23, 2025
97fbfd8
doc: fix stability 1.x links excluding the decimal digit
dario-piotrowicz Jun 23, 2025
11811c1
deps: update nghttp2 to 1.66.0
nodejs-github-bot Jun 24, 2025
7498383
lib: make `validateInternalField()` throw `ERR_INVALID_THIS`
LiviaMedeiros Jun 19, 2025
20089e2
lib: rename `validateInternalField` into `validateThisInternalField`
LiviaMedeiros Jun 19, 2025
8c20896
src: add FromV8Value<T>() for integral and enum types
Aditi-1400 Jun 24, 2025
4c2c100
meta: add @nodejs/inspector as codeowner
legendecas Jun 24, 2025
c9dc0a8
http: fix keep-alive not timing out after post-request empty line
islandryu Jun 24, 2025
a1529d5
test_runner: automatically wait for subtests to finish
cjihrig Jan 19, 2025
7419750
2025-06-24, Version 24.3.0 (Current)
RafaelGSS Jun 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
test_runner: support object property mocking
PR-URL: #58438
Fixes: #58322
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
  • Loading branch information
idango10 authored and targos committed Jun 16, 2025
commit 713fbad7b66c39d40c7bca63559de1e9ff47d4bc
119 changes: 119 additions & 0 deletions doc/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -2048,6 +2048,87 @@ added:

Resets the implementation of the mock module.

## Class: `MockPropertyContext`

<!-- YAML
added: REPLACEME
-->

The `MockPropertyContext` class is used to inspect or manipulate the behavior
of property mocks created via the [`MockTracker`][] APIs.

### `ctx.accesses`

* {Array}

A getter that returns a copy of the internal array used to track accesses (get/set) to
the mocked property. Each entry in the array is an object with the following properties:

* `type` {string} Either `'get'` or `'set'`, indicating the type of access.
* `value` {any} The value that was read (for `'get'`) or written (for `'set'`).
* `stack` {Error} An `Error` object whose stack can be used to determine the
callsite of the mocked function invocation.

### `ctx.accessCount()`

* Returns: {integer} The number of times that the property was accessed (read or written).

This function returns the number of times that the property was accessed.
This function is more efficient than checking `ctx.accesses.length` because
`ctx.accesses` is a getter that creates a copy of the internal access tracking array.

### `ctx.mockImplementation(value)`

* `value` {any} The new value to be set as the mocked property value.

This function is used to change the value returned by the mocked property getter.

### `ctx.mockImplementationOnce(value[, onAccess])`

* `value` {any} The value to be used as the mock's
implementation for the invocation number specified by `onAccess`.
* `onAccess` {integer} The invocation number that will use `value`. If
the specified invocation has already occurred then an exception is thrown.
**Default:** The number of the next invocation.

This function is used to change the behavior of an existing mock for a single
invocation. Once invocation `onAccess` has occurred, the mock will revert to
whatever behavior it would have used had `mockImplementationOnce()` not been
called.

The following example creates a mock function using `t.mock.property()`, calls the
mock property, changes the mock implementation to a different value for the
next invocation, and then resumes its previous behavior.

```js
test('changes a mock behavior once', (t) => {
const obj = { foo: 1 };

const prop = t.mock.property(obj, 'foo', 5);

assert.strictEqual(obj.foo, 5);
prop.mock.mockImplementationOnce(25);
assert.strictEqual(obj.foo, 25);
assert.strictEqual(obj.foo, 5);
});
```

#### Caveat

For consistency with the rest of the mocking API, this function treats both property gets and sets
as accesses. If a property set occurs at the same access index, the "once" value will be consumed
by the set operation, and the mocked property value will be changed to the "once" value. This may
lead to unexpected behavior if you intend the "once" value to only be used for a get operation.

### `ctx.resetAccesses()`

Resets the access history of the mocked property.

### `ctx.restore()`

Resets the implementation of the mock property to its original behavior. The
mock can still be used after calling this function.

## Class: `MockTracker`

<!-- YAML
Expand Down Expand Up @@ -2252,6 +2333,43 @@ test('mocks a builtin module in both module systems', async (t) => {
});
```

### `mock.property(object, propertyName[, value])`

<!-- YAML
added: REPLACEME
-->

* `object` {Object} The object whose value is being mocked.
* `propertyName` {string|symbol} The identifier of the property on `object` to mock.
* `value` {any} An optional value used as the mock value
for `object[propertyName]`. **Default:** The original property value.
* Returns: {Proxy} A proxy to the mocked object. The mocked object contains a
special `mock` property, which is an instance of [`MockPropertyContext`][], and
can be used for inspecting and changing the behavior of the mocked property.

Creates a mock for a property value on an object. This allows you to track and control access to a specific property,
including how many times it is read (getter) or written (setter), and to restore the original value after mocking.

```js
test('mocks a property value', (t) => {
const obj = { foo: 42 };
const prop = t.mock.property(obj, 'foo', 100);

assert.strictEqual(obj.foo, 100);
assert.strictEqual(prop.mock.accessCount(), 1);
assert.strictEqual(prop.mock.accesses[0].type, 'get');
assert.strictEqual(prop.mock.accesses[0].value, 100);

obj.foo = 200;
assert.strictEqual(prop.mock.accessCount(), 2);
assert.strictEqual(prop.mock.accesses[1].type, 'set');
assert.strictEqual(prop.mock.accesses[1].value, 200);

prop.mock.restore();
assert.strictEqual(obj.foo, 42);
});
```

### `mock.reset()`

<!-- YAML
Expand Down Expand Up @@ -3790,6 +3908,7 @@ Can be used to abort test subtasks when the test has been aborted.
[`--test-update-snapshots`]: cli.md#--test-update-snapshots
[`--test`]: cli.md#--test
[`MockFunctionContext`]: #class-mockfunctioncontext
[`MockPropertyContext`]: #class-mockpropertycontext
[`MockTimers`]: #class-mocktimers
[`MockTracker.method`]: #mockmethodobject-methodname-implementation-options
[`MockTracker`]: #class-mocktracker
Expand Down
163 changes: 163 additions & 0 deletions lib/internal/test_runner/mock/mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,134 @@ class MockModuleContext {

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;
Expand Down Expand Up @@ -573,6 +701,41 @@ class MockTracker {
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.
*/
Expand Down
Loading