Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nodejs/node.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcjihrig <cjihrig@gmail.com>2022-04-05 01:36:40 +0300
committercjihrig <cjihrig@gmail.com>2022-11-08 02:25:54 +0300
commit7c6682957b3c5f86d0616cebc0ad09cc2a1fd50d (patch)
tree4c225bb187aed436f36d617981addfc2611d32e4
parent6ef4368db4cffb536d4040961ffabdd2c1fc2cd0 (diff)
test_runner: support function mocking
This commit allows tests in the test runner to mock functions and methods. PR-URL: https://github.com/nodejs/node/pull/45326 Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
-rw-r--r--doc/api/test.md360
-rw-r--r--lib/internal/test_runner/mock.js295
-rw-r--r--lib/internal/test_runner/test.js8
-rw-r--r--lib/test.js19
-rw-r--r--test/parallel/test-runner-mocking.js801
-rw-r--r--tools/doc/type-parser.mjs2
6 files changed, 1483 insertions, 2 deletions
diff --git a/doc/api/test.md b/doc/api/test.md
index e9e4113fa25..8d3e84e48c7 100644
--- a/doc/api/test.md
+++ b/doc/api/test.md
@@ -352,6 +352,89 @@ Otherwise, the test is considered to be a failure. Test files must be
executable by Node.js, but are not required to use the `node:test` module
internally.
+## Mocking
+
+The `node:test` module supports mocking during testing via a top-level `mock`
+object. The following example creates a spy on a function that adds two numbers
+together. The spy is then used to assert that the function was called as
+expected.
+
+```mjs
+import assert from 'node:assert';
+import { mock, test } from 'node:test';
+
+test('spies on a function', () => {
+ const sum = mock.fn((a, b) => {
+ return a + b;
+ });
+
+ assert.strictEqual(sum.mock.calls.length, 0);
+ assert.strictEqual(sum(3, 4), 7);
+ assert.strictEqual(sum.mock.calls.length, 1);
+
+ const call = sum.mock.calls[0];
+ assert.deepStrictEqual(call.arguments, [3, 4]);
+ assert.strictEqual(call.result, 7);
+ assert.strictEqual(call.error, undefined);
+
+ // Reset the globally tracked mocks.
+ mock.reset();
+});
+```
+
+```cjs
+'use strict';
+const assert = require('node:assert');
+const { mock, test } = require('node:test');
+
+test('spies on a function', () => {
+ const sum = mock.fn((a, b) => {
+ return a + b;
+ });
+
+ assert.strictEqual(sum.mock.calls.length, 0);
+ assert.strictEqual(sum(3, 4), 7);
+ assert.strictEqual(sum.mock.calls.length, 1);
+
+ const call = sum.mock.calls[0];
+ assert.deepStrictEqual(call.arguments, [3, 4]);
+ assert.strictEqual(call.result, 7);
+ assert.strictEqual(call.error, undefined);
+
+ // Reset the globally tracked mocks.
+ mock.reset();
+});
+```
+
+The same mocking functionality is also exposed on the [`TestContext`][] object
+of each test. The following example creates a spy on an object method using the
+API exposed on the `TestContext`. The benefit of mocking via the test context is
+that the test runner will automatically restore all mocked functionality once
+the test finishes.
+
+```js
+test('spies on an object method', (t) => {
+ const number = {
+ value: 5,
+ add(a) {
+ return this.value + a;
+ },
+ };
+
+ t.mock.method(number, 'add');
+ assert.strictEqual(number.add.mock.calls.length, 0);
+ assert.strictEqual(number.add(3), 8);
+ assert.strictEqual(number.add.mock.calls.length, 1);
+
+ const call = number.add.mock.calls[0];
+
+ assert.deepStrictEqual(call.arguments, [3]);
+ assert.strictEqual(call.result, 8);
+ assert.strictEqual(call.target, undefined);
+ assert.strictEqual(call.this, number);
+});
+```
+
## `run([options])`
<!-- YAML
@@ -644,6 +727,281 @@ describe('tests', async () => {
});
```
+## Class: `MockFunctionContext`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+The `MockFunctionContext` class is used to inspect or manipulate the behavior of
+mocks created via the [`MockTracker`][] APIs.
+
+### `ctx.calls`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* {Array}
+
+A getter that returns a copy of the internal array used to track calls to the
+mock. Each entry in the array is an object with the following properties.
+
+* `arguments` {Array} An array of the arguments passed to the mock function.
+* `error` {any} If the mocked function threw then this property contains the
+ thrown value. **Default:** `undefined`.
+* `result` {any} The value returned by the mocked function.
+* `stack` {Error} An `Error` object whose stack can be used to determine the
+ callsite of the mocked function invocation.
+* `target` {Function|undefined} If the mocked function is a constructor, this
+ field contains the class being constructed. Otherwise this will be
+ `undefined`.
+* `this` {any} The mocked function's `this` value.
+
+### `ctx.callCount()`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* Returns: {integer} The number of times that this mock has been invoked.
+
+This function returns the number of times that this mock has been invoked. This
+function is more efficient than checking `ctx.calls.length` because `ctx.calls`
+is a getter that creates a copy of the internal call tracking array.
+
+### `ctx.mockImplementation(implementation)`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* `implementation` {Function|AsyncFunction} The function to be used as the
+ mock's new implementation.
+
+This function is used to change the behavior of an existing mock.
+
+The following example creates a mock function using `t.mock.fn()`, calls the
+mock function, and then changes the mock implementation to a different function.
+
+```js
+test('changes a mock behavior', (t) => {
+ let cnt = 0;
+
+ function addOne() {
+ cnt++;
+ return cnt;
+ }
+
+ function addTwo() {
+ cnt += 2;
+ return cnt;
+ }
+
+ const fn = t.mock.fn(addOne);
+
+ assert.strictEqual(fn(), 1);
+ fn.mock.mockImplementation(addTwo);
+ assert.strictEqual(fn(), 3);
+ assert.strictEqual(fn(), 5);
+});
+```
+
+### `ctx.mockImplementationOnce(implementation[, onCall])`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* `implementation` {Function|AsyncFunction} The function to be used as the
+ mock's implementation for the invocation number specified by `onCall`.
+* `onCall` {integer} The invocation number that will use `implementation`. 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 `onCall` 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.fn()`, calls the
+mock function, changes the mock implementation to a different function for the
+next invocation, and then resumes its previous behavior.
+
+```js
+test('changes a mock behavior once', (t) => {
+ let cnt = 0;
+
+ function addOne() {
+ cnt++;
+ return cnt;
+ }
+
+ function addTwo() {
+ cnt += 2;
+ return cnt;
+ }
+
+ const fn = t.mock.fn(addOne);
+
+ assert.strictEqual(fn(), 1);
+ fn.mock.mockImplementationOnce(addTwo);
+ assert.strictEqual(fn(), 3);
+ assert.strictEqual(fn(), 4);
+});
+```
+
+### `ctx.restore()`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+Resets the implementation of the mock function to its original behavior. The
+mock can still be used after calling this function.
+
+## Class: `MockTracker`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+The `MockTracker` class is used to manage mocking functionality. The test runner
+module provides a top level `mock` export which is a `MockTracker` instance.
+Each test also provides its own `MockTracker` instance via the test context's
+`mock` property.
+
+### `mock.fn([original[, implementation]][, options])`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* `original` {Function|AsyncFunction} An optional function to create a mock on.
+ **Default:** A no-op function.
+* `implementation` {Function|AsyncFunction} An optional function used as the
+ mock implementation for `original`. This is useful for creating mocks that
+ exhibit one behavior for a specified number of calls and then restore the
+ behavior of `original`. **Default:** The function specified by `original`.
+* `options` {Object} Optional configuration options for the mock function. The
+ following properties are supported:
+ * `times` {integer} The number of times that the mock will use the behavior of
+ `implementation`. Once the mock function has been called `times` times, it
+ will automatically restore the behavior of `original`. This value must be an
+ integer greater than zero. **Default:** `Infinity`.
+* Returns: {Proxy} The mocked function. The mocked function contains a special
+ `mock` property, which is an instance of [`MockFunctionContext`][], and can
+ be used for inspecting and changing the behavior of the mocked function.
+
+This function is used to create a mock function.
+
+The following example creates a mock function that increments a counter by one
+on each invocation. The `times` option is used to modify the mock behavior such
+that the first two invocations add two to the counter instead of one.
+
+```js
+test('mocks a counting function', (t) => {
+ let cnt = 0;
+
+ function addOne() {
+ cnt++;
+ return cnt;
+ }
+
+ function addTwo() {
+ cnt += 2;
+ return cnt;
+ }
+
+ const fn = t.mock.fn(addOne, addTwo, { times: 2 });
+
+ assert.strictEqual(fn(), 2);
+ assert.strictEqual(fn(), 4);
+ assert.strictEqual(fn(), 5);
+ assert.strictEqual(fn(), 6);
+});
+```
+
+### `mock.method(object, methodName[, implementation][, options])`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* `object` {Object} The object whose method is being mocked.
+* `methodName` {string|symbol} The identifier of the method on `object` to mock.
+ If `object[methodName]` is not a function, an error is thrown.
+* `implementation` {Function|AsyncFunction} An optional function used as the
+ mock implementation for `object[methodName]`. **Default:** The original method
+ specified by `object[methodName]`.
+* `options` {Object} Optional configuration options for the mock method. The
+ following properties are supported:
+ * `getter` {boolean} If `true`, `object[methodName]` is treated as a getter.
+ This option cannot be used with the `setter` option. **Default:** false.
+ * `setter` {boolean} If `true`, `object[methodName]` is treated as a setter.
+ This option cannot be used with the `getter` option. **Default:** false.
+ * `times` {integer} The number of times that the mock will use the behavior of
+ `implementation`. Once the mocked method has been called `times` times, it
+ will automatically restore the original behavior. This value must be an
+ integer greater than zero. **Default:** `Infinity`.
+* Returns: {Proxy} The mocked method. The mocked method contains a special
+ `mock` property, which is an instance of [`MockFunctionContext`][], and can
+ be used for inspecting and changing the behavior of the mocked method.
+
+This function is used to create a mock on an existing object method. The
+following example demonstrates how a mock is created on an existing object
+method.
+
+```js
+test('spies on an object method', (t) => {
+ const number = {
+ value: 5,
+ subtract(a) {
+ return this.value - a;
+ },
+ };
+
+ t.mock.method(number, 'subtract');
+ assert.strictEqual(number.subtract.mock.calls.length, 0);
+ assert.strictEqual(number.subtract(3), 2);
+ assert.strictEqual(number.subtract.mock.calls.length, 1);
+
+ const call = number.subtract.mock.calls[0];
+
+ assert.deepStrictEqual(call.arguments, [3]);
+ assert.strictEqual(call.result, 2);
+ assert.strictEqual(call.error, undefined);
+ assert.strictEqual(call.target, undefined);
+ assert.strictEqual(call.this, number);
+});
+```
+
+### `mock.reset()`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+This function restores the default behavior of all mocks that were previously
+created by this `MockTracker` and disassociates the mocks from the
+`MockTracker` instance. Once disassociated, the mocks can still be used, but the
+`MockTracker` instance can no longer be used to reset their behavior or
+otherwise interact with them.
+
+After each test completes, this function is called on the test context's
+`MockTracker`. If the global `MockTracker` is used extensively, calling this
+function manually is recommended.
+
+### `mock.restoreAll()`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+This function restores the default behavior of all mocks that were previously
+created by this `MockTracker`. Unlike `mock.reset()`, `mock.restoreAll()` does
+not disassociate the mocks from the `MockTracker` instance.
+
## Class: `TapStream`
<!-- YAML
@@ -979,6 +1337,8 @@ added:
[`--test-name-pattern`]: cli.md#--test-name-pattern
[`--test-only`]: cli.md#--test-only
[`--test`]: cli.md#--test
+[`MockFunctionContext`]: #class-mockfunctioncontext
+[`MockTracker`]: #class-mocktracker
[`SuiteContext`]: #class-suitecontext
[`TestContext`]: #class-testcontext
[`context.diagnostic`]: #contextdiagnosticmessage
diff --git a/lib/internal/test_runner/mock.js b/lib/internal/test_runner/mock.js
new file mode 100644
index 00000000000..6b1d7e0c570
--- /dev/null
+++ b/lib/internal/test_runner/mock.js
@@ -0,0 +1,295 @@
+'use strict';
+const {
+ ArrayPrototypePush,
+ ArrayPrototypeSlice,
+ Error,
+ FunctionPrototypeCall,
+ ObjectDefineProperty,
+ ObjectGetOwnPropertyDescriptor,
+ Proxy,
+ ReflectApply,
+ ReflectConstruct,
+ ReflectGet,
+ SafeMap,
+} = primordials;
+const {
+ codes: {
+ ERR_INVALID_ARG_TYPE,
+ ERR_INVALID_ARG_VALUE,
+ },
+} = require('internal/errors');
+const { kEmptyObject } = require('internal/util');
+const {
+ validateBoolean,
+ validateFunction,
+ validateInteger,
+ validateObject,
+} = require('internal/validators');
+
+function kDefaultFunction() {}
+
+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;
+ }
+
+ get calls() {
+ return ArrayPrototypeSlice(this.#calls, 0);
+ }
+
+ callCount() {
+ return this.#calls.length;
+ }
+
+ mockImplementation(implementation) {
+ validateFunction(implementation, 'implementation');
+ this.#implementation = implementation;
+ }
+
+ mockImplementationOnce(implementation, onCall) {
+ validateFunction(implementation, 'implementation');
+ const nextCall = this.#calls.length;
+ const call = onCall ?? nextCall;
+ validateInteger(call, 'onCall', nextCall);
+ this.#mocks.set(call, implementation);
+ }
+
+ 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;
+ }
+ }
+
+ trackCall(call) {
+ ArrayPrototypePush(this.#calls, call);
+ }
+
+ 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, trackCall } = MockFunctionContext.prototype;
+delete MockFunctionContext.prototype.trackCall;
+delete MockFunctionContext.prototype.nextImpl;
+
+class MockTracker {
+ #mocks = [];
+
+ 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, { original }, times);
+ return this.#setupMock(ctx, original);
+ }
+
+ method(
+ object,
+ methodName,
+ implementation = kDefaultFunction,
+ options = kEmptyObject,
+ ) {
+ validateObject(object, 'object');
+ validateStringOrSymbol(methodName, 'methodName');
+
+ 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 = ObjectGetOwnPropertyDescriptor(object, 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 = { descriptor, object, 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(object, methodName, mockDescriptor);
+
+ return mock;
+ }
+
+ reset() {
+ this.restoreAll();
+ this.#mocks = [];
+ }
+
+ restoreAll() {
+ for (let i = 0; i < this.#mocks.length; i++) {
+ FunctionPrototypeCall(restore, this.#mocks[i]);
+ }
+ }
+
+ #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, {
+ 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, {
+ 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, ctx);
+ return mock;
+ }
+}
+
+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);
+}
+
+module.exports = { MockTracker };
diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js
index 6d30f3aaa66..2176b87fbf1 100644
--- a/lib/internal/test_runner/test.js
+++ b/lib/internal/test_runner/test.js
@@ -32,6 +32,7 @@ const {
AbortError,
} = require('internal/errors');
const { getOptionValue } = require('internal/options');
+const { MockTracker } = require('internal/test_runner/mock');
const { TapStream } = require('internal/test_runner/tap_stream');
const {
convertStringToRegExp,
@@ -112,6 +113,11 @@ class TestContext {
this.#test.diagnostic(message);
}
+ get mock() {
+ this.#test.mock ??= new MockTracker();
+ return this.#test.mock;
+ }
+
runOnly(value) {
this.#test.runOnlySubtests = !!value;
}
@@ -239,6 +245,7 @@ class Test extends AsyncResource {
this.#outerSignal?.addEventListener('abort', this.#abortHandler);
this.fn = fn;
+ this.mock = null;
this.name = name;
this.parent = parent;
this.cancelled = false;
@@ -593,6 +600,7 @@ class Test extends AsyncResource {
}
this.#outerSignal?.removeEventListener('abort', this.#abortHandler);
+ this.mock?.reset();
if (this.parent !== null) {
this.parent.activeSubtests--;
diff --git a/lib/test.js b/lib/test.js
index 767d0e83a5f..dc4045622a8 100644
--- a/lib/test.js
+++ b/lib/test.js
@@ -1,5 +1,5 @@
'use strict';
-const { ObjectAssign } = primordials;
+const { ObjectAssign, ObjectDefineProperty } = primordials;
const { test, describe, it, before, after, beforeEach, afterEach } = require('internal/test_runner/harness');
const { run } = require('internal/test_runner/runner');
@@ -14,3 +14,20 @@ ObjectAssign(module.exports, {
run,
test,
});
+
+let lazyMock;
+
+ObjectDefineProperty(module.exports, 'mock', {
+ __proto__: null,
+ configurable: true,
+ enumerable: true,
+ get() {
+ if (lazyMock === undefined) {
+ const { MockTracker } = require('internal/test_runner/mock');
+
+ lazyMock = new MockTracker();
+ }
+
+ return lazyMock;
+ },
+});
diff --git a/test/parallel/test-runner-mocking.js b/test/parallel/test-runner-mocking.js
new file mode 100644
index 00000000000..6d3233ec0ed
--- /dev/null
+++ b/test/parallel/test-runner-mocking.js
@@ -0,0 +1,801 @@
+'use strict';
+const common = require('../common');
+const assert = require('node:assert');
+const { mock, test } = require('node:test');
+
+test('spies on a function', (t) => {
+ const sum = t.mock.fn((arg1, arg2) => {
+ return arg1 + arg2;
+ });
+
+ assert.strictEqual(sum.mock.calls.length, 0);
+ assert.strictEqual(sum(3, 4), 7);
+ assert.strictEqual(sum.call(1000, 9, 1), 10);
+ assert.strictEqual(sum.mock.calls.length, 2);
+
+ let call = sum.mock.calls[0];
+ assert.deepStrictEqual(call.arguments, [3, 4]);
+ assert.strictEqual(call.error, undefined);
+ assert.strictEqual(call.result, 7);
+ assert.strictEqual(call.target, undefined);
+ assert.strictEqual(call.this, undefined);
+
+ call = sum.mock.calls[1];
+ assert.deepStrictEqual(call.arguments, [9, 1]);
+ assert.strictEqual(call.error, undefined);
+ assert.strictEqual(call.result, 10);
+ assert.strictEqual(call.target, undefined);
+ assert.strictEqual(call.this, 1000);
+});
+
+test('spies on a bound function', (t) => {
+ const bound = function(arg1, arg2) {
+ return this + arg1 + arg2;
+ }.bind(50);
+ const sum = t.mock.fn(bound);
+
+ assert.strictEqual(sum.mock.calls.length, 0);
+ assert.strictEqual(sum(3, 4), 57);
+ assert.strictEqual(sum(9, 1), 60);
+ assert.strictEqual(sum.mock.calls.length, 2);
+
+ let call = sum.mock.calls[0];
+ assert.deepStrictEqual(call.arguments, [3, 4]);
+ assert.strictEqual(call.result, 57);
+ assert.strictEqual(call.target, undefined);
+ assert.strictEqual(call.this, undefined);
+
+ call = sum.mock.calls[1];
+ assert.deepStrictEqual(call.arguments, [9, 1]);
+ assert.strictEqual(call.result, 60);
+ assert.strictEqual(call.target, undefined);
+ assert.strictEqual(call.this, undefined);
+});
+
+test('spies on a constructor', (t) => {
+ class ParentClazz {
+ constructor(c) {
+ this.c = c;
+ }
+ }
+
+ class Clazz extends ParentClazz {
+ #privateValue;
+
+ constructor(a, b) {
+ super(a + b);
+ this.a = a;
+ this.#privateValue = b;
+ }
+
+ getPrivateValue() {
+ return this.#privateValue;
+ }
+ }
+
+ const ctor = t.mock.fn(Clazz);
+ const instance = new ctor(42, 85);
+
+ assert(instance instanceof Clazz);
+ assert(instance instanceof ParentClazz);
+ assert.strictEqual(instance.a, 42);
+ assert.strictEqual(instance.getPrivateValue(), 85);
+ assert.strictEqual(instance.c, 127);
+ assert.strictEqual(ctor.mock.calls.length, 1);
+
+ const call = ctor.mock.calls[0];
+
+ assert.deepStrictEqual(call.arguments, [42, 85]);
+ assert.strictEqual(call.error, undefined);
+ assert.strictEqual(call.result, instance);
+ assert.strictEqual(call.target, Clazz);
+ assert.strictEqual(call.this, instance);
+});
+
+test('a no-op spy function is created by default', (t) => {
+ const fn = t.mock.fn();
+
+ assert.strictEqual(fn.mock.calls.length, 0);
+ assert.strictEqual(fn(3, 4), undefined);
+ assert.strictEqual(fn.mock.calls.length, 1);
+
+ const call = fn.mock.calls[0];
+ assert.deepStrictEqual(call.arguments, [3, 4]);
+ assert.strictEqual(call.result, undefined);
+ assert.strictEqual(call.target, undefined);
+ assert.strictEqual(call.this, undefined);
+});
+
+test('internal no-op function can be reused', (t) => {
+ const fn1 = t.mock.fn();
+ fn1.prop = true;
+ const fn2 = t.mock.fn();
+
+ fn1(1);
+ fn2(2);
+ fn1(3);
+
+ assert.notStrictEqual(fn1.mock, fn2.mock);
+ assert.strictEqual(fn1.mock.calls.length, 2);
+ assert.strictEqual(fn2.mock.calls.length, 1);
+ assert.strictEqual(fn1.prop, true);
+ assert.strictEqual(fn2.prop, undefined);
+});
+
+test('functions can be mocked multiple times at once', (t) => {
+ function sum(a, b) {
+ return a + b;
+ }
+
+ function difference(a, b) {
+ return a - b;
+ }
+
+ function product(a, b) {
+ return a * b;
+ }
+
+ const fn1 = t.mock.fn(sum, difference);
+ const fn2 = t.mock.fn(sum, product);
+
+ assert.strictEqual(fn1(5, 3), 2);
+ assert.strictEqual(fn2(5, 3), 15);
+ assert.strictEqual(fn2(4, 2), 8);
+ assert(!('mock' in sum));
+ assert(!('mock' in difference));
+ assert(!('mock' in product));
+ assert.notStrictEqual(fn1.mock, fn2.mock);
+ assert.strictEqual(fn1.mock.calls.length, 1);
+ assert.strictEqual(fn2.mock.calls.length, 2);
+});
+
+test('internal no-op function can be reused as methods', (t) => {
+ const obj = {
+ _foo: 5,
+ _bar: 9,
+ foo() {
+ return this._foo;
+ },
+ bar() {
+ return this._bar;
+ },
+ };
+
+ t.mock.method(obj, 'foo');
+ obj.foo.prop = true;
+ t.mock.method(obj, 'bar');
+ assert.strictEqual(obj.foo(), 5);
+ assert.strictEqual(obj.bar(), 9);
+ assert.strictEqual(obj.bar(), 9);
+ assert.notStrictEqual(obj.foo.mock, obj.bar.mock);
+ assert.strictEqual(obj.foo.mock.calls.length, 1);
+ assert.strictEqual(obj.bar.mock.calls.length, 2);
+ assert.strictEqual(obj.foo.prop, true);
+ assert.strictEqual(obj.bar.prop, undefined);
+});
+
+test('methods can be mocked multiple times but not at the same time', (t) => {
+ const obj = {
+ offset: 3,
+ sum(a, b) {
+ return this.offset + a + b;
+ },
+ };
+
+ function difference(a, b) {
+ return this.offset + (a - b);
+ }
+
+ function product(a, b) {
+ return this.offset + a * b;
+ }
+
+ const originalSum = obj.sum;
+ const fn1 = t.mock.method(obj, 'sum', difference);
+
+ assert.strictEqual(obj.sum(5, 3), 5);
+ assert.strictEqual(obj.sum(5, 1), 7);
+ assert.strictEqual(obj.sum, fn1);
+ assert.notStrictEqual(fn1.mock, undefined);
+ assert.strictEqual(originalSum.mock, undefined);
+ assert.strictEqual(difference.mock, undefined);
+ assert.strictEqual(product.mock, undefined);
+ assert.strictEqual(fn1.mock.calls.length, 2);
+
+ const fn2 = t.mock.method(obj, 'sum', product);
+
+ assert.strictEqual(obj.sum(5, 3), 18);
+ assert.strictEqual(obj.sum, fn2);
+ assert.notStrictEqual(fn1, fn2);
+ assert.strictEqual(fn2.mock.calls.length, 1);
+
+ obj.sum.mock.restore();
+ assert.strictEqual(obj.sum, fn1);
+ obj.sum.mock.restore();
+ assert.strictEqual(obj.sum, originalSum);
+ assert.strictEqual(obj.sum.mock, undefined);
+});
+
+test('spies on an object method', (t) => {
+ const obj = {
+ prop: 5,
+ method(a, b) {
+ return a + b + this.prop;
+ },
+ };
+
+ assert.strictEqual(obj.method(1, 3), 9);
+ t.mock.method(obj, 'method');
+ assert.strictEqual(obj.method.mock.calls.length, 0);
+ assert.strictEqual(obj.method(1, 3), 9);
+
+ const call = obj.method.mock.calls[0];
+
+ assert.deepStrictEqual(call.arguments, [1, 3]);
+ assert.strictEqual(call.result, 9);
+ assert.strictEqual(call.target, undefined);
+ assert.strictEqual(call.this, obj);
+
+ assert.strictEqual(obj.method.mock.restore(), undefined);
+ assert.strictEqual(obj.method(1, 3), 9);
+ assert.strictEqual(obj.method.mock, undefined);
+});
+
+test('spies on a getter', (t) => {
+ const obj = {
+ prop: 5,
+ get method() {
+ return this.prop;
+ },
+ };
+
+ assert.strictEqual(obj.method, 5);
+
+ const getter = t.mock.method(obj, 'method', { getter: true });
+
+ assert.strictEqual(getter.mock.calls.length, 0);
+ assert.strictEqual(obj.method, 5);
+
+ const call = getter.mock.calls[0];
+
+ assert.deepStrictEqual(call.arguments, []);
+ assert.strictEqual(call.result, 5);
+ assert.strictEqual(call.target, undefined);
+ assert.strictEqual(call.this, obj);
+
+ assert.strictEqual(getter.mock.restore(), undefined);
+ assert.strictEqual(obj.method, 5);
+});
+
+test('spies on a setter', (t) => {
+ const obj = {
+ prop: 100,
+ // eslint-disable-next-line accessor-pairs
+ set method(val) {
+ this.prop = val;
+ },
+ };
+
+ assert.strictEqual(obj.prop, 100);
+ obj.method = 88;
+ assert.strictEqual(obj.prop, 88);
+
+ const setter = t.mock.method(obj, 'method', { setter: true });
+
+ assert.strictEqual(setter.mock.calls.length, 0);
+ obj.method = 77;
+ assert.strictEqual(obj.prop, 77);
+ assert.strictEqual(setter.mock.calls.length, 1);
+
+ const call = setter.mock.calls[0];
+
+ assert.deepStrictEqual(call.arguments, [77]);
+ assert.strictEqual(call.result, undefined);
+ assert.strictEqual(call.target, undefined);
+ assert.strictEqual(call.this, obj);
+
+ assert.strictEqual(setter.mock.restore(), undefined);
+ assert.strictEqual(obj.prop, 77);
+ obj.method = 65;
+ assert.strictEqual(obj.prop, 65);
+});
+
+test('spy functions can be bound', (t) => {
+ const sum = t.mock.fn(function(arg1, arg2) {
+ return this + arg1 + arg2;
+ });
+ const bound = sum.bind(1000);
+
+ assert.strictEqual(bound(9, 1), 1010);
+ assert.strictEqual(sum.mock.calls.length, 1);
+
+ const call = sum.mock.calls[0];
+ assert.deepStrictEqual(call.arguments, [9, 1]);
+ assert.strictEqual(call.result, 1010);
+ assert.strictEqual(call.target, undefined);
+ assert.strictEqual(call.this, 1000);
+
+ assert.strictEqual(sum.mock.restore(), undefined);
+ assert.strictEqual(sum.bind(0)(2, 11), 13);
+});
+
+test('mocked functions report thrown errors', (t) => {
+ const testError = new Error('test error');
+ const fn = t.mock.fn(() => {
+ throw testError;
+ });
+
+ assert.throws(fn, /test error/);
+ assert.strictEqual(fn.mock.calls.length, 1);
+
+ const call = fn.mock.calls[0];
+
+ assert.deepStrictEqual(call.arguments, []);
+ assert.strictEqual(call.error, testError);
+ assert.strictEqual(call.result, undefined);
+ assert.strictEqual(call.target, undefined);
+ assert.strictEqual(call.this, undefined);
+});
+
+test('mocked constructors report thrown errors', (t) => {
+ const testError = new Error('test error');
+ class Clazz {
+ constructor() {
+ throw testError;
+ }
+ }
+
+ const ctor = t.mock.fn(Clazz);
+
+ assert.throws(() => {
+ new ctor();
+ }, /test error/);
+ assert.strictEqual(ctor.mock.calls.length, 1);
+
+ const call = ctor.mock.calls[0];
+
+ assert.deepStrictEqual(call.arguments, []);
+ assert.strictEqual(call.error, testError);
+ assert.strictEqual(call.result, undefined);
+ assert.strictEqual(call.target, Clazz);
+ assert.strictEqual(call.this, undefined);
+});
+
+test('mocks a function', (t) => {
+ const sum = (arg1, arg2) => arg1 + arg2;
+ const difference = (arg1, arg2) => arg1 - arg2;
+ const fn = t.mock.fn(sum, difference);
+
+ assert.strictEqual(fn.mock.calls.length, 0);
+ assert.strictEqual(fn(3, 4), -1);
+ assert.strictEqual(fn(9, 1), 8);
+ assert.strictEqual(fn.mock.calls.length, 2);
+
+ let call = fn.mock.calls[0];
+ assert.deepStrictEqual(call.arguments, [3, 4]);
+ assert.strictEqual(call.result, -1);
+ assert.strictEqual(call.target, undefined);
+ assert.strictEqual(call.this, undefined);
+
+ call = fn.mock.calls[1];
+ assert.deepStrictEqual(call.arguments, [9, 1]);
+ assert.strictEqual(call.result, 8);
+ assert.strictEqual(call.target, undefined);
+ assert.strictEqual(call.this, undefined);
+
+ assert.strictEqual(fn.mock.restore(), undefined);
+ assert.strictEqual(fn(2, 11), 13);
+});
+
+test('mocks a constructor', (t) => {
+ class ParentClazz {
+ constructor(c) {
+ this.c = c;
+ }
+ }
+
+ class Clazz extends ParentClazz {
+ #privateValue;
+
+ constructor(a, b) {
+ super(a + b);
+ this.a = a;
+ this.#privateValue = b;
+ }
+
+ getPrivateValue() {
+ return this.#privateValue;
+ }
+ }
+
+ class MockClazz {
+ #privateValue;
+
+ constructor(z) {
+ this.z = z;
+ }
+ }
+
+ const ctor = t.mock.fn(Clazz, MockClazz);
+ const instance = new ctor(42, 85);
+
+ assert(!(instance instanceof MockClazz));
+ assert(instance instanceof Clazz);
+ assert(instance instanceof ParentClazz);
+ assert.strictEqual(instance.a, undefined);
+ assert.strictEqual(instance.c, undefined);
+ assert.strictEqual(instance.z, 42);
+ assert.strictEqual(ctor.mock.calls.length, 1);
+
+ const call = ctor.mock.calls[0];
+
+ assert.deepStrictEqual(call.arguments, [42, 85]);
+ assert.strictEqual(call.result, instance);
+ assert.strictEqual(call.target, Clazz);
+ assert.strictEqual(call.this, instance);
+ assert.throws(() => {
+ instance.getPrivateValue();
+ }, /TypeError: Cannot read private member #privateValue /);
+});
+
+test('mocks an object method', (t) => {
+ const obj = {
+ prop: 5,
+ method(a, b) {
+ return a + b + this.prop;
+ },
+ };
+
+ function mockMethod(a) {
+ return a + this.prop;
+ }
+
+ assert.strictEqual(obj.method(1, 3), 9);
+ t.mock.method(obj, 'method', mockMethod);
+ assert.strictEqual(obj.method.mock.calls.length, 0);
+ assert.strictEqual(obj.method(1, 3), 6);
+
+ const call = obj.method.mock.calls[0];
+
+ assert.deepStrictEqual(call.arguments, [1, 3]);
+ assert.strictEqual(call.result, 6);
+ assert.strictEqual(call.target, undefined);
+ assert.strictEqual(call.this, obj);
+
+ assert.strictEqual(obj.method.mock.restore(), undefined);
+ assert.strictEqual(obj.method(1, 3), 9);
+ assert.strictEqual(obj.method.mock, undefined);
+});
+
+test('mocks a getter', (t) => {
+ const obj = {
+ prop: 5,
+ get method() {
+ return this.prop;
+ },
+ };
+
+ function mockMethod() {
+ return this.prop - 1;
+ }
+
+ assert.strictEqual(obj.method, 5);
+
+ const getter = t.mock.method(obj, 'method', mockMethod, { getter: true });
+
+ assert.strictEqual(getter.mock.calls.length, 0);
+ assert.strictEqual(obj.method, 4);
+
+ const call = getter.mock.calls[0];
+
+ assert.deepStrictEqual(call.arguments, []);
+ assert.strictEqual(call.result, 4);
+ assert.strictEqual(call.target, undefined);
+ assert.strictEqual(call.this, obj);
+
+ assert.strictEqual(getter.mock.restore(), undefined);
+ assert.strictEqual(obj.method, 5);
+});
+
+test('mocks a setter', (t) => {
+ const obj = {
+ prop: 100,
+ // eslint-disable-next-line accessor-pairs
+ set method(val) {
+ this.prop = val;
+ },
+ };
+
+ function mockMethod(val) {
+ this.prop = -val;
+ }
+
+ assert.strictEqual(obj.prop, 100);
+ obj.method = 88;
+ assert.strictEqual(obj.prop, 88);
+
+ const setter = t.mock.method(obj, 'method', mockMethod, { setter: true });
+
+ assert.strictEqual(setter.mock.calls.length, 0);
+ obj.method = 77;
+ assert.strictEqual(obj.prop, -77);
+ assert.strictEqual(setter.mock.calls.length, 1);
+
+ const call = setter.mock.calls[0];
+
+ assert.deepStrictEqual(call.arguments, [77]);
+ assert.strictEqual(call.result, undefined);
+ assert.strictEqual(call.target, undefined);
+ assert.strictEqual(call.this, obj);
+
+ assert.strictEqual(setter.mock.restore(), undefined);
+ assert.strictEqual(obj.prop, -77);
+ obj.method = 65;
+ assert.strictEqual(obj.prop, 65);
+});
+
+test('mocked functions match name and length', (t) => {
+ function getNameAndLength(fn) {
+ return {
+ name: Object.getOwnPropertyDescriptor(fn, 'name'),
+ length: Object.getOwnPropertyDescriptor(fn, 'length'),
+ };
+ }
+
+ function func1() {}
+ const func2 = function(a) {}; // eslint-disable-line func-style
+ const arrow = (a, b, c) => {};
+ const obj = { method(a, b) {} };
+
+ assert.deepStrictEqual(
+ getNameAndLength(func1),
+ getNameAndLength(t.mock.fn(func1))
+ );
+ assert.deepStrictEqual(
+ getNameAndLength(func2),
+ getNameAndLength(t.mock.fn(func2))
+ );
+ assert.deepStrictEqual(
+ getNameAndLength(arrow),
+ getNameAndLength(t.mock.fn(arrow))
+ );
+ assert.deepStrictEqual(
+ getNameAndLength(obj.method),
+ getNameAndLength(t.mock.method(obj, 'method', func1))
+ );
+});
+
+test('method() fails if method cannot be redefined', (t) => {
+ const obj = {
+ prop: 5,
+ };
+
+ Object.defineProperty(obj, 'method', {
+ configurable: false,
+ value(a, b) {
+ return a + b + this.prop;
+ }
+ });
+
+ function mockMethod(a) {
+ return a + this.prop;
+ }
+
+ assert.throws(() => {
+ t.mock.method(obj, 'method', mockMethod);
+ }, /Cannot redefine property: method/);
+ assert.strictEqual(obj.method(1, 3), 9);
+ assert.strictEqual(obj.method.mock, undefined);
+});
+
+test('method() fails if field is a property instead of a method', (t) => {
+ const obj = {
+ prop: 5,
+ method: 100,
+ };
+
+ function mockMethod(a) {
+ return a + this.prop;
+ }
+
+ assert.throws(() => {
+ t.mock.method(obj, 'method', mockMethod);
+ }, /The argument 'methodName' must be a method/);
+ assert.strictEqual(obj.method, 100);
+ assert.strictEqual(obj.method.mock, undefined);
+});
+
+test('mocks can be auto-restored', (t) => {
+ let cnt = 0;
+
+ function addOne() {
+ cnt++;
+ return cnt;
+ }
+
+ function addTwo() {
+ cnt += 2;
+ return cnt;
+ }
+
+ const fn = t.mock.fn(addOne, addTwo, { times: 2 });
+
+ assert.strictEqual(fn(), 2);
+ assert.strictEqual(fn(), 4);
+ assert.strictEqual(fn(), 5);
+ assert.strictEqual(fn(), 6);
+});
+
+test('mock implementation can be changed dynamically', (t) => {
+ let cnt = 0;
+
+ function addOne() {
+ cnt++;
+ return cnt;
+ }
+
+ function addTwo() {
+ cnt += 2;
+ return cnt;
+ }
+
+ function addThree() {
+ cnt += 3;
+ return cnt;
+ }
+
+ const fn = t.mock.fn(addOne);
+
+ assert.strictEqual(fn.mock.callCount(), 0);
+ assert.strictEqual(fn(), 1);
+ assert.strictEqual(fn(), 2);
+ assert.strictEqual(fn(), 3);
+ assert.strictEqual(fn.mock.callCount(), 3);
+
+ fn.mock.mockImplementation(addTwo);
+ assert.strictEqual(fn(), 5);
+ assert.strictEqual(fn(), 7);
+ assert.strictEqual(fn.mock.callCount(), 5);
+
+ fn.mock.restore();
+ assert.strictEqual(fn(), 8);
+ assert.strictEqual(fn(), 9);
+ assert.strictEqual(fn.mock.callCount(), 7);
+
+ assert.throws(() => {
+ fn.mock.mockImplementationOnce(common.mustNotCall(), 6);
+ }, /The value of "onCall" is out of range\. It must be >= 7/);
+
+ fn.mock.mockImplementationOnce(addThree, 7);
+ fn.mock.mockImplementationOnce(addTwo, 8);
+ assert.strictEqual(fn(), 12);
+ assert.strictEqual(fn(), 14);
+ assert.strictEqual(fn(), 15);
+ assert.strictEqual(fn.mock.callCount(), 10);
+ fn.mock.mockImplementationOnce(addThree);
+ assert.strictEqual(fn(), 18);
+ assert.strictEqual(fn(), 19);
+ assert.strictEqual(fn.mock.callCount(), 12);
+});
+
+test('local mocks are auto restored after the test finishes', async (t) => {
+ const obj = {
+ foo() {},
+ bar() {},
+ };
+ const originalFoo = obj.foo;
+ const originalBar = obj.bar;
+
+ assert.strictEqual(originalFoo, obj.foo);
+ assert.strictEqual(originalBar, obj.bar);
+
+ const mockFoo = t.mock.method(obj, 'foo');
+
+ assert.strictEqual(mockFoo, obj.foo);
+ assert.notStrictEqual(originalFoo, obj.foo);
+ assert.strictEqual(originalBar, obj.bar);
+
+ t.beforeEach(() => {
+ assert.strictEqual(mockFoo, obj.foo);
+ assert.strictEqual(originalBar, obj.bar);
+ });
+
+ t.afterEach(() => {
+ assert.strictEqual(mockFoo, obj.foo);
+ assert.notStrictEqual(originalBar, obj.bar);
+ });
+
+ await t.test('creates mocks that are auto restored', (t) => {
+ const mockBar = t.mock.method(obj, 'bar');
+
+ assert.strictEqual(mockFoo, obj.foo);
+ assert.strictEqual(mockBar, obj.bar);
+ assert.notStrictEqual(originalBar, obj.bar);
+ });
+
+ assert.strictEqual(mockFoo, obj.foo);
+ assert.strictEqual(originalBar, obj.bar);
+});
+
+test('uses top level mock', () => {
+ function sum(a, b) {
+ return a + b;
+ }
+
+ function difference(a, b) {
+ return a - b;
+ }
+
+ const fn = mock.fn(sum, difference);
+
+ assert.strictEqual(fn.mock.calls.length, 0);
+ assert.strictEqual(fn(3, 4), -1);
+ assert.strictEqual(fn.mock.calls.length, 1);
+ mock.reset();
+ assert.strictEqual(fn(3, 4), 7);
+ assert.strictEqual(fn.mock.calls.length, 2);
+});
+
+test('the getter and setter options cannot be used together', (t) => {
+ assert.throws(() => {
+ t.mock.method({}, 'method', { getter: true, setter: true });
+ }, /The property 'options\.setter' cannot be used with 'options\.getter'/);
+});
+
+test('method names must be strings or symbols', (t) => {
+ const symbol = Symbol();
+ const obj = {
+ method() {},
+ [symbol]() {},
+ };
+
+ t.mock.method(obj, 'method');
+ t.mock.method(obj, symbol);
+
+ assert.throws(() => {
+ t.mock.method(obj, {});
+ }, /The "methodName" argument must be one of type string or symbol/);
+});
+
+test('the times option must be an integer >= 1', (t) => {
+ assert.throws(() => {
+ t.mock.fn({ times: null });
+ }, /The "options\.times" property must be of type number/);
+
+ assert.throws(() => {
+ t.mock.fn({ times: 0 });
+ }, /The value of "options\.times" is out of range/);
+
+ assert.throws(() => {
+ t.mock.fn(() => {}, { times: 3.14159 });
+ }, /The value of "options\.times" is out of range/);
+});
+
+test('spies on a class prototype method', (t) => {
+ class Clazz {
+ constructor(c) {
+ this.c = c;
+ }
+
+ getC() {
+ return this.c;
+ }
+ }
+
+ const instance = new Clazz(85);
+
+ assert.strictEqual(instance.getC(), 85);
+ t.mock.method(Clazz.prototype, 'getC');
+
+ assert.strictEqual(instance.getC.mock.calls.length, 0);
+ assert.strictEqual(instance.getC(), 85);
+ assert.strictEqual(instance.getC.mock.calls.length, 1);
+ assert.strictEqual(Clazz.prototype.getC.mock.calls.length, 1);
+
+ const call = instance.getC.mock.calls[0];
+ assert.deepStrictEqual(call.arguments, []);
+ assert.strictEqual(call.result, 85);
+ assert.strictEqual(call.error, undefined);
+ assert.strictEqual(call.target, undefined);
+ assert.strictEqual(call.this, instance);
+});
diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs
index b660949ce3c..0053b62b31f 100644
--- a/tools/doc/type-parser.mjs
+++ b/tools/doc/type-parser.mjs
@@ -14,7 +14,7 @@ const jsPrimitives = {
const jsGlobalObjectsUrl = `${jsDocPrefix}Reference/Global_Objects/`;
const jsGlobalTypes = [
'AggregateError', 'Array', 'ArrayBuffer', 'DataView', 'Date', 'Error',
- 'EvalError', 'Function', 'Map', 'Object', 'Promise', 'RangeError',
+ 'EvalError', 'Function', 'Map', 'Object', 'Promise', 'Proxy', 'RangeError',
'ReferenceError', 'RegExp', 'Set', 'SharedArrayBuffer', 'SyntaxError',
'TypeError', 'TypedArray', 'URIError', 'Uint8Array',
];