From 8ecdf42c63e2947e6b62770cf543a0d4eeffaa3d Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Fri, 25 Apr 2025 10:13:50 -0300 Subject: [PATCH 01/28] lib: restructure assert to use clasd implementation for basics assert --- lib/assert.js | 175 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 153 insertions(+), 22 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index 6dedcd0b971046..62cb686744d831 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -75,11 +75,159 @@ function lazyLoadComparison() { // The assert module provides functions that throw // AssertionError's when particular conditions are not met. The // assert module must conform to the following interface. +// const assert = {}; -const assert = module.exports = ok; +function assert(value, message) { + assert.ok(value, message); +} const NO_EXCEPTION_SENTINEL = {}; +class Assert { + constructor(options = {}) {} + + /** + * Pure assertion tests whether a value is truthy, as determined + * by !!value. + * @param {...any} args + * @returns {void} + */ + static ok(...args) { + innerOk(Assert.ok, args.length, ...args); + } + + /** + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @param {string} [operator] + * @param {Function} [stackStartFn] + */ + static fail(actual, expected, message, operator, stackStartFn) { + const argsLen = arguments.length; + + let internalMessage = false; + if (actual == null && argsLen <= 1) { + internalMessage = true; + message = 'Failed'; + } else if (argsLen === 1) { + message = actual; + actual = undefined; + } else { + if (warned === false) { + warned = true; + process.emitWarning( + 'assert.fail() with more than one argument is deprecated. ' + + 'Please use assert.strictEqual() instead or only pass a message.', + 'DeprecationWarning', + 'DEP0094', + ); + } + if (argsLen === 2) + operator = '!='; + } + + if (message instanceof Error) throw message; + + const errArgs = { + actual, + expected, + operator: operator === undefined ? 'fail' : operator, + stackStartFn: stackStartFn || Assert.fail, + message, + }; + const err = new AssertionError(errArgs); + if (internalMessage) { + err.generatedMessage = true; + } + throw err; + } + + /** + * The equality assertion tests shallow, coercive equality with ==. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ + /* eslint-disable no-restricted-properties */ + static equal(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + // eslint-disable-next-line eqeqeq + if (actual != expected && (!NumberIsNaN(actual) || !NumberIsNaN(expected))) { + innerFail({ + actual, + expected, + message, + operator: '==', + stackStartFn: Assert.equal, + }); + } + }; + + /** + * The non-equality assertion tests for whether two objects are not + * equal with !=. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ + static notEqual(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + // eslint-disable-next-line eqeqeq + if (actual == expected || (NumberIsNaN(actual) && NumberIsNaN(expected))) { + innerFail({ + actual, + expected, + message, + operator: '!=', + stackStartFn: Assert.notEqual, + }); + } + }; + + + + /** + * The strict equivalence assertion tests a strict equality relation. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ + static strictEqual(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + if (!ObjectIs(actual, expected)) { + innerFail({ + actual, + expected, + message, + operator: 'strictEqual', + stackStartFn: Assert.strictEqual, + }); + } + }; +} + +assert.Assert = Assert; + +assert.ok = Assert.ok; +assert.fail = Assert.fail; +assert.equal = Assert.equal; +assert.notEqual = Assert.notEqual; + +assert.strictEqual = Assert.strictEqual; + +// The AssertionError is defined in internal/error. +assert.AssertionError = AssertionError; + // All of the following functions must throw an AssertionError // when a corresponding condition is not met, with a message that // may be undefined if not provided. All assertion methods provide @@ -277,27 +425,8 @@ function notDeepStrictEqual(actual, expected, message) { } } -/** - * The strict equivalence assertion tests a strict equality relation. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ -assert.strictEqual = function strictEqual(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - if (!ObjectIs(actual, expected)) { - innerFail({ - actual, - expected, - message, - operator: 'strictEqual', - stackStartFn: strictEqual, - }); - } -}; + +// assert.strictEqual = function strictEqual(actual, expected, message) { /** * The strict non-equivalence assertion tests for any strict inequality. @@ -815,3 +944,5 @@ assert.strict = ObjectAssign(strict, assert, { }); assert.strict.strict = assert.strict; + +module.exports = assert; From 3539371527905ddb5d5c9d3a84c445524705d70d Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sun, 4 May 2025 14:57:13 -0300 Subject: [PATCH 02/28] lib: change static methods and add other compare methods --- lib/assert.js | 180 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 164 insertions(+), 16 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index 62cb686744d831..77d7aafe850018 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -92,8 +92,8 @@ class Assert { * @param {...any} args * @returns {void} */ - static ok(...args) { - innerOk(Assert.ok, args.length, ...args); + ok(...args) { + innerOk(this.ok, args.length, ...args); } /** @@ -103,7 +103,7 @@ class Assert { * @param {string} [operator] * @param {Function} [stackStartFn] */ - static fail(actual, expected, message, operator, stackStartFn) { + fail(actual, expected, message, operator, stackStartFn) { const argsLen = arguments.length; let internalMessage = false; @@ -133,7 +133,7 @@ class Assert { actual, expected, operator: operator === undefined ? 'fail' : operator, - stackStartFn: stackStartFn || Assert.fail, + stackStartFn: stackStartFn || this.fail, message, }; const err = new AssertionError(errArgs); @@ -151,7 +151,7 @@ class Assert { * @returns {void} */ /* eslint-disable no-restricted-properties */ - static equal(actual, expected, message) { + equal(actual, expected, message) { if (arguments.length < 2) { throw new ERR_MISSING_ARGS('actual', 'expected'); } @@ -162,7 +162,7 @@ class Assert { expected, message, operator: '==', - stackStartFn: Assert.equal, + stackStartFn: this.equal, }); } }; @@ -175,7 +175,7 @@ class Assert { * @param {string | Error} [message] * @returns {void} */ - static notEqual(actual, expected, message) { + notEqual(actual, expected, message) { if (arguments.length < 2) { throw new ERR_MISSING_ARGS('actual', 'expected'); } @@ -186,12 +186,105 @@ class Assert { expected, message, operator: '!=', - stackStartFn: Assert.notEqual, + stackStartFn: this.notEqual, }); } }; + /** + * The deep equivalence assertion tests a deep equality relation. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ + deepEqual(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + if (isDeepEqual === undefined) lazyLoadComparison(); + if (!isDeepEqual(actual, expected)) { + innerFail({ + actual, + expected, + message, + operator: 'deepEqual', + stackStartFn: this.deepEqual, + }); + } + }; + + /** + * The deep non-equivalence assertion tests for any deep inequality. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ + notDeepEqual(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + if (isDeepEqual === undefined) lazyLoadComparison(); + if (isDeepEqual(actual, expected)) { + innerFail({ + actual, + expected, + message, + operator: 'notDeepEqual', + stackStartFn: this.notDeepEqual, + }); + } + }; + /* eslint-enable */ + /** + * The deep strict equivalence assertion tests a deep strict equality + * relation. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ + deepStrictEqual(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + if (isDeepEqual === undefined) lazyLoadComparison(); + if (!isDeepStrictEqual(actual, expected)) { + innerFail({ + actual, + expected, + message, + operator: 'deepStrictEqual', + stackStartFn: this.deepStrictEqual, + }); + } + }; + + /** + * The deep strict non-equivalence assertion tests for any deep strict + * inequality. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ + notDeepStrictEqual(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + if (isDeepEqual === undefined) lazyLoadComparison(); + if (isDeepStrictEqual(actual, expected)) { + innerFail({ + actual, + expected, + message, + operator: 'notDeepStrictEqual', + stackStartFn: this.notDeepStrictEqual, + }); + } + } /** * The strict equivalence assertion tests a strict equality relation. @@ -200,7 +293,7 @@ class Assert { * @param {string | Error} [message] * @returns {void} */ - static strictEqual(actual, expected, message) { + strictEqual(actual, expected, message) { if (arguments.length < 2) { throw new ERR_MISSING_ARGS('actual', 'expected'); } @@ -210,20 +303,75 @@ class Assert { expected, message, operator: 'strictEqual', - stackStartFn: Assert.strictEqual, + stackStartFn: this.strictEqual, + }); + } + }; + + /** + * The strict non-equivalence assertion tests for any strict inequality. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ + notStrictEqual(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + if (ObjectIs(actual, expected)) { + innerFail({ + actual, + expected, + message, + operator: 'notStrictEqual', + stackStartFn: this.notStrictEqual, + }); + } + }; + + /** + * The strict equivalence assertion test between two objects + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ + partialDeepStrictEqual( + actual, + expected, + message, + ) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + if (isDeepEqual === undefined) lazyLoadComparison(); + if (!isPartialStrictEqual(actual, expected)) { + innerFail({ + actual, + expected, + message, + operator: 'partialDeepStrictEqual', + stackStartFn: this.partialDeepStrictEqual, }); } }; } -assert.Assert = Assert; -assert.ok = Assert.ok; -assert.fail = Assert.fail; -assert.equal = Assert.equal; -assert.notEqual = Assert.notEqual; +const assert = new Assert(); -assert.strictEqual = Assert.strictEqual; +assert.ok = assert.ok.bind(assert); +assert.fail = assert.fail.bind(assert); +assert.equal = assert.equal.bind(assert); +assert.notEqual = assert.notEqual.bind(assert); +assert.deepEqual = assert.deepEqual.bind(assert); +assert.notDeepEqual = assert.notDeepEqual.bind(assert); +assert.deepStrictEqual = assert.deepStrictEqual.bind(assert); +assert.notDeepStrictEqual = assert.notDeepStrictEqual.bind(assert); +assert.strictEqual = assert.strictEqual.bind(assert); +assert.notStrictEqual = assert.strictEqual.bind(assert); +assert.partialDeepStrictEqual = assert.partialDeepStrictEqual.bind(assert); // The AssertionError is defined in internal/error. assert.AssertionError = AssertionError; From 3d1c2f9c608c3869dee00cc8dde18d06fa2c096c Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Mon, 5 May 2025 17:16:58 -0300 Subject: [PATCH 03/28] lib: update assert module to use instance methods and preserve original behavior --- lib/assert.js | 187 ++++++++++++++++++----------------- test/parallel/test-assert.js | 15 +++ 2 files changed, 111 insertions(+), 91 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index 77d7aafe850018..a795a7b7438ee1 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -75,16 +75,49 @@ function lazyLoadComparison() { // The assert module provides functions that throw // AssertionError's when particular conditions are not met. The // assert module must conform to the following interface. -// const assert = {}; - -function assert(value, message) { - assert.ok(value, message); -} +const assert = module.exports = ok; const NO_EXCEPTION_SENTINEL = {}; class Assert { - constructor(options = {}) {} + constructor(options = {}) { + this.options = options; + } + + #internalMatch(string, regexp, message, fn) { + if (!isRegExp(regexp)) { + throw new ERR_INVALID_ARG_TYPE( + 'regexp', 'RegExp', regexp, + ); + } + const match = fn === Assert.prototype.match; + if (typeof string !== 'string' || + RegExpPrototypeExec(regexp, string) !== null !== match) { + if (message instanceof Error) { + throw message; + } + + const generatedMessage = !message; + + // 'The input was expected to not match the regular expression ' + + message ||= (typeof string !== 'string' ? + 'The "string" argument must be of type string. Received type ' + + `${typeof string} (${inspect(string)})` : + (match ? + 'The input did not match the regular expression ' : + 'The input was expected to not match the regular expression ') + + `${inspect(regexp)}. Input:\n\n${inspect(string)}\n`); + const err = new AssertionError({ + actual: string, + expected: regexp, + message, + operator: fn.name, + stackStartFn: fn, + }); + err.generatedMessage = generatedMessage; + throw err; + } + } /** * Pure assertion tests whether a value is truthy, as determined @@ -150,7 +183,6 @@ class Assert { * @param {string | Error} [message] * @returns {void} */ - /* eslint-disable no-restricted-properties */ equal(actual, expected, message) { if (arguments.length < 2) { throw new ERR_MISSING_ARGS('actual', 'expected'); @@ -191,7 +223,7 @@ class Assert { } }; - /** + /** * The deep equivalence assertion tests a deep equality relation. * @param {any} actual * @param {any} expected @@ -236,9 +268,8 @@ class Assert { }); } }; - /* eslint-enable */ - /** + /** * The deep strict equivalence assertion tests a deep strict equality * relation. * @param {any} actual @@ -356,25 +387,58 @@ class Assert { }); } }; -} + /** + * Expects the `string` input to match the regular expression. + * @param {string} string + * @param {RegExp} regexp + * @param {string | Error} [message] + * @returns {void} + */ + match(string, regexp, message) { + this.#internalMatch(string, regexp, message, Assert.prototype.match); + }; + + /** + * Expects the `string` input not to match the regular expression. + * @param {string} string + * @param {RegExp} regexp + * @param {string | Error} [message] + * @returns {void} + */ + doesNotMatch(string, regexp, message) { + this.#internalMatch(string, regexp, message, Assert.prototype.doesNotMatch); + }; -const assert = new Assert(); + /** + * Expects the function `promiseFn` to throw an error. + * @param {() => any} promiseFn + * @param {...any} [args] + * @returns {void} + */ + throws(promiseFn, ...args) { + expectsError(Assert.prototype.throws, getActual(promiseFn), ...args); + }; +} -assert.ok = assert.ok.bind(assert); -assert.fail = assert.fail.bind(assert); -assert.equal = assert.equal.bind(assert); -assert.notEqual = assert.notEqual.bind(assert); -assert.deepEqual = assert.deepEqual.bind(assert); -assert.notDeepEqual = assert.notDeepEqual.bind(assert); -assert.deepStrictEqual = assert.deepStrictEqual.bind(assert); -assert.notDeepStrictEqual = assert.notDeepStrictEqual.bind(assert); -assert.strictEqual = assert.strictEqual.bind(assert); -assert.notStrictEqual = assert.strictEqual.bind(assert); -assert.partialDeepStrictEqual = assert.partialDeepStrictEqual.bind(assert); +const assertInstance = new Assert(); +['ok', 'fail', 'equal', 'notEqual', 'deepEqual', 'notDeepEqual', + 'deepStrictEqual', 'notDeepStrictEqual', 'strictEqual', + 'notStrictEqual', 'partialDeepStrictEqual', 'match', 'doesNotMatch', 'throws'].forEach((name) => { + assertInstance[name] = assertInstance[name].bind(assertInstance); +}); -// The AssertionError is defined in internal/error. -assert.AssertionError = AssertionError; +/** + * Pure assertion tests whether a value is truthy, as determined + * by !!value. + * @param {...any} args + * @returns {void} + */ +function ok(...args) { + innerOk(ok, args.length, ...args); +} +ObjectAssign(assert, assertInstance); +assert.ok = ok; // All of the following functions must throw an AssertionError // when a corresponding condition is not met, with a message that @@ -918,16 +982,6 @@ function expectsNoError(stackStartFn, actual, error, message) { throw actual; } -/** - * Expects the function `promiseFn` to throw an error. - * @param {() => any} promiseFn - * @param {...any} [args] - * @returns {void} - */ -assert.throws = function throws(promiseFn, ...args) { - expectsError(throws, getActual(promiseFn), ...args); -}; - /** * Expects `promiseFn` function or its value to reject. * @param {() => Promise} promiseFn @@ -1018,62 +1072,7 @@ assert.ifError = function ifError(err) { } }; -function internalMatch(string, regexp, message, fn) { - if (!isRegExp(regexp)) { - throw new ERR_INVALID_ARG_TYPE( - 'regexp', 'RegExp', regexp, - ); - } - const match = fn === assert.match; - if (typeof string !== 'string' || - RegExpPrototypeExec(regexp, string) !== null !== match) { - if (message instanceof Error) { - throw message; - } - - const generatedMessage = !message; - - // 'The input was expected to not match the regular expression ' + - message ||= (typeof string !== 'string' ? - 'The "string" argument must be of type string. Received type ' + - `${typeof string} (${inspect(string)})` : - (match ? - 'The input did not match the regular expression ' : - 'The input was expected to not match the regular expression ') + - `${inspect(regexp)}. Input:\n\n${inspect(string)}\n`); - const err = new AssertionError({ - actual: string, - expected: regexp, - message, - operator: fn.name, - stackStartFn: fn, - }); - err.generatedMessage = generatedMessage; - throw err; - } -} - -/** - * Expects the `string` input to match the regular expression. - * @param {string} string - * @param {RegExp} regexp - * @param {string | Error} [message] - * @returns {void} - */ -assert.match = function match(string, regexp, message) { - internalMatch(string, regexp, message, match); -}; - -/** - * Expects the `string` input not to match the regular expression. - * @param {string} string - * @param {RegExp} regexp - * @param {string | Error} [message] - * @returns {void} - */ -assert.doesNotMatch = function doesNotMatch(string, regexp, message) { - internalMatch(string, regexp, message, doesNotMatch); -}; +assert.CallTracker = deprecate(CallTracker, 'assert.CallTracker is deprecated.', 'DEP0173'); /** * Expose a strict only variant of assert. @@ -1084,6 +1083,9 @@ function strict(...args) { innerOk(strict, args.length, ...args); } +assert.AssertionError = AssertionError; +// Assert.AssertionError = AssertionError; + assert.strict = ObjectAssign(strict, assert, { equal: assert.strictEqual, deepEqual: assert.deepStrictEqual, @@ -1091,6 +1093,9 @@ assert.strict = ObjectAssign(strict, assert, { notDeepEqual: assert.notDeepStrictEqual, }); + +assert.strict.Assert = Assert; assert.strict.strict = assert.strict; module.exports = assert; +module.exports.Assert = Assert; diff --git a/test/parallel/test-assert.js b/test/parallel/test-assert.js index ae3df139cd68b7..328bbb407478ec 100644 --- a/test/parallel/test-assert.js +++ b/test/parallel/test-assert.js @@ -23,6 +23,7 @@ const { invalidArgTypeHelper } = require('../common'); const assert = require('assert'); +const { Assert } = require('assert'); const { inspect } = require('util'); const { test } = require('node:test'); const vm = require('vm'); @@ -1595,5 +1596,19 @@ test('assert/strict exists', () => { assert.strictEqual(require('assert/strict'), assert.strict); }); +test('Assert class', () => { + const assertInstance = new Assert(); + + assertInstance.ok(assert.AssertionError.prototype instanceof Error, + 'assert.AssertionError instanceof Error'); + + assertInstance.throws(() => assert(false), assertInstance.AssertionError, 'ok(false)'); + assertInstance.ok(true); + assertInstance.throws(() => assertInstance.equal(true, false), + assertInstance.AssertionError, 'equal(true, false)'); + assertInstance.notEqual(true, false); + assertInstance.notStrictEqual(2, '2'); +}); + /* eslint-enable no-restricted-syntax */ /* eslint-enable no-restricted-properties */ From 6921bce250bdf02cdf5dc338c9eadd0812b67c64 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Wed, 7 May 2025 17:18:55 -0300 Subject: [PATCH 04/28] test: add Assert class tests with additional error scenarios --- test/parallel/test-assert.js | 45 +++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/test/parallel/test-assert.js b/test/parallel/test-assert.js index 328bbb407478ec..0e9771dc6cacaf 100644 --- a/test/parallel/test-assert.js +++ b/test/parallel/test-assert.js @@ -1596,18 +1596,51 @@ test('assert/strict exists', () => { assert.strictEqual(require('assert/strict'), assert.strict); }); -test('Assert class', () => { +test('Assert class basic instance', () => { const assertInstance = new Assert(); assertInstance.ok(assert.AssertionError.prototype instanceof Error, - 'assert.AssertionError instanceof Error'); - - assertInstance.throws(() => assert(false), assertInstance.AssertionError, 'ok(false)'); + 'assert.AssertionError instanceof Error'); assertInstance.ok(true); - assertInstance.throws(() => assertInstance.equal(true, false), - assertInstance.AssertionError, 'equal(true, false)'); + assertInstance.equal(undefined, undefined); assertInstance.notEqual(true, false); + assertInstance.throws( + () => assertInstance.deepEqual(/a/), + { code: 'ERR_MISSING_ARGS' } + ); + assertInstance.throws( + () => assertInstance.notDeepEqual('test'), + { code: 'ERR_MISSING_ARGS' } + ); assertInstance.notStrictEqual(2, '2'); + assertInstance.throws(() => assertInstance.strictEqual(2, '2'), + assertInstance.AssertionError, 'strictEqual(2, \'2\')'); + assertInstance.throws( + () => { + assertInstance.partialDeepStrictEqual({ a: true }, { a: false }, 'custom message'); + }, + { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: 'custom message\n+ actual - expected\n\n {\n+ a: true\n- a: false\n }\n' + } + ); + assertInstance.throws( + () => assertInstance.match(/abc/, 'string'), + { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "regexp" argument must be an instance of RegExp. ' + + "Received type string ('string')" + } + ); + assertInstance.throws( + () => assertInstance.doesNotMatch(/abc/, 'string'), + { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "regexp" argument must be an instance of RegExp. ' + + "Received type string ('string')" + } + ); }); /* eslint-enable no-restricted-syntax */ From 226748c490f374edf4f5c9db159d46e3cb11601b Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Thu, 8 May 2025 13:08:18 -0300 Subject: [PATCH 05/28] test: update snapshot fixtures --- lib/assert.js | 92 +++++++++++++++++-- test/fixtures/errors/error_exit.snapshot | 2 +- .../test-runner/output/describe_it.snapshot | 3 + .../test-runner/output/dot_reporter.snapshot | 3 + .../output/junit_reporter.snapshot | 3 + .../test-runner/output/output.snapshot | 3 + .../test-runner/output/output_cli.snapshot | 3 + .../output/source_mapped_locations.snapshot | 3 + .../test-runner/output/spec_reporter.snapshot | 3 + .../output/spec_reporter_cli.snapshot | 3 + test/parallel/test-assert.js | 14 +-- test/parallel/test-runner-assert.js | 2 + 12 files changed, 116 insertions(+), 18 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index a795a7b7438ee1..1391d0b5207795 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -119,6 +119,58 @@ class Assert { } } + // #expectsError(stackStartFn, actual, error, message) { + // if (typeof error === 'string') { + // if (arguments.length === 4) { + // throw new ERR_INVALID_ARG_TYPE('error', + // ['Object', 'Error', 'Function', 'RegExp'], + // error); + // } + // if (typeof actual === 'object' && actual !== null) { + // if (actual.message === error) { + // throw new ERR_AMBIGUOUS_ARGUMENT( + // 'error/message', + // `The error message "${actual.message}" is identical to the message.`, + // ); + // } + // } else if (actual === error) { + // throw new ERR_AMBIGUOUS_ARGUMENT( + // 'error/message', + // `The error "${actual}" is identical to the message.`, + // ); + // } + // message = error; + // error = undefined; + // } else if (error != null && + // typeof error !== 'object' && + // typeof error !== 'function') { + // throw new ERR_INVALID_ARG_TYPE('error', + // ['Object', 'Error', 'Function', 'RegExp'], + // error); + // } + + // if (actual === NO_EXCEPTION_SENTINEL) { + // let details = ''; + // if (error?.name) { + // details += ` (${error.name})`; + // } + // details += message ? `: ${message}` : '.'; + // const fnType = stackStartFn === Assert.prototype.rejects ? 'rejection' : 'exception'; + // innerFail({ + // actual: undefined, + // expected: error, + // operator: stackStartFn.name, + // message: `Missing expected ${fnType}${details}`, + // stackStartFn, + // }); + // } + + // if (!error) + // return; + + // expectedException(actual, error, message, stackStartFn); + // } + /** * Pure assertion tests whether a value is truthy, as determined * by !!value. @@ -410,21 +462,31 @@ class Assert { this.#internalMatch(string, regexp, message, Assert.prototype.doesNotMatch); }; - /** - * Expects the function `promiseFn` to throw an error. - * @param {() => any} promiseFn - * @param {...any} [args] - * @returns {void} - */ - throws(promiseFn, ...args) { - expectsError(Assert.prototype.throws, getActual(promiseFn), ...args); - }; + // /** + // * Expects the function `promiseFn` to throw an error. + // * @param {() => any} promiseFn + // * @param {...any} [args] + // * @returns {void} + // */ + // throws(promiseFn, ...args) { + // this.#expectsError(Assert.prototype.throws, getActual(promiseFn), ...args); + // }; + + // /** + // * Expects `promiseFn` function or its value to reject. + // * @param {() => Promise} promiseFn + // * @param {...any} [args] + // * @returns {Promise} + // */ + // async rejects(promiseFn, ...args) { + // this.#expectsError(Assert.prototype.rejects, await waitForActual(promiseFn), ...args); + // }; } const assertInstance = new Assert(); ['ok', 'fail', 'equal', 'notEqual', 'deepEqual', 'notDeepEqual', 'deepStrictEqual', 'notDeepStrictEqual', 'strictEqual', - 'notStrictEqual', 'partialDeepStrictEqual', 'match', 'doesNotMatch', 'throws'].forEach((name) => { + 'notStrictEqual', 'partialDeepStrictEqual', 'match', 'doesNotMatch'].forEach((name) => { assertInstance[name] = assertInstance[name].bind(assertInstance); }); @@ -982,6 +1044,16 @@ function expectsNoError(stackStartFn, actual, error, message) { throw actual; } +/** + * Expects the function `promiseFn` to throw an error. + * @param {() => any} promiseFn + * @param {...any} [args] + * @returns {void} + */ +assert.throws = function throws(promiseFn, ...args) { + expectsError(throws, getActual(promiseFn), ...args); +}; + /** * Expects `promiseFn` function or its value to reject. * @param {() => Promise} promiseFn diff --git a/test/fixtures/errors/error_exit.snapshot b/test/fixtures/errors/error_exit.snapshot index 778165dc25c4fc..fff8f868e6f876 100644 --- a/test/fixtures/errors/error_exit.snapshot +++ b/test/fixtures/errors/error_exit.snapshot @@ -7,7 +7,7 @@ AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: 1 !== 2 - at Object. (*error_exit.js:*:*) { + at new AssertionError (node:internal*assert*assertion_error:*:*) { generatedMessage: true, code: 'ERR_ASSERTION', actual: 1, diff --git a/test/fixtures/test-runner/output/describe_it.snapshot b/test/fixtures/test-runner/output/describe_it.snapshot index 67d4af7f1b9f45..2cda9f1a6c7ae6 100644 --- a/test/fixtures/test-runner/output/describe_it.snapshot +++ b/test/fixtures/test-runner/output/describe_it.snapshot @@ -154,6 +154,9 @@ not ok 14 - async assertion fail * * * + * + * + * ... # Subtest: resolve pass ok 15 - resolve pass diff --git a/test/fixtures/test-runner/output/dot_reporter.snapshot b/test/fixtures/test-runner/output/dot_reporter.snapshot index 5abbb979667cfd..1e06b8eea8b607 100644 --- a/test/fixtures/test-runner/output/dot_reporter.snapshot +++ b/test/fixtures/test-runner/output/dot_reporter.snapshot @@ -61,6 +61,9 @@ Failed tests: * * * + * + * + * * { generatedMessage: true, code: 'ERR_ASSERTION', diff --git a/test/fixtures/test-runner/output/junit_reporter.snapshot b/test/fixtures/test-runner/output/junit_reporter.snapshot index 3b1d15022af704..b0fb1af4dc2224 100644 --- a/test/fixtures/test-runner/output/junit_reporter.snapshot +++ b/test/fixtures/test-runner/output/junit_reporter.snapshot @@ -119,6 +119,9 @@ true !== false * * * + * + * + * * { generatedMessage: true, code: 'ERR_ASSERTION', diff --git a/test/fixtures/test-runner/output/output.snapshot b/test/fixtures/test-runner/output/output.snapshot index ffbe91759bb859..9dc6a2c0d5755f 100644 --- a/test/fixtures/test-runner/output/output.snapshot +++ b/test/fixtures/test-runner/output/output.snapshot @@ -157,6 +157,9 @@ not ok 13 - async assertion fail * * * + * + * + * ... # Subtest: resolve pass ok 14 - resolve pass diff --git a/test/fixtures/test-runner/output/output_cli.snapshot b/test/fixtures/test-runner/output/output_cli.snapshot index 7f989f14c619cf..8fc99aa548c5ce 100644 --- a/test/fixtures/test-runner/output/output_cli.snapshot +++ b/test/fixtures/test-runner/output/output_cli.snapshot @@ -157,6 +157,9 @@ not ok 13 - async assertion fail * * * + * + * + * ... # Subtest: resolve pass ok 14 - resolve pass diff --git a/test/fixtures/test-runner/output/source_mapped_locations.snapshot b/test/fixtures/test-runner/output/source_mapped_locations.snapshot index 8cf210da817aae..6fc9d3c455b379 100644 --- a/test/fixtures/test-runner/output/source_mapped_locations.snapshot +++ b/test/fixtures/test-runner/output/source_mapped_locations.snapshot @@ -22,6 +22,9 @@ not ok 1 - fails * * * + * + * + * ... 1..1 # tests 1 diff --git a/test/fixtures/test-runner/output/spec_reporter.snapshot b/test/fixtures/test-runner/output/spec_reporter.snapshot index 6c11b9ba6d4a39..35530531cf8cdf 100644 --- a/test/fixtures/test-runner/output/spec_reporter.snapshot +++ b/test/fixtures/test-runner/output/spec_reporter.snapshot @@ -166,6 +166,9 @@ * * * + * + * + * * { generatedMessage: true, code: 'ERR_ASSERTION', diff --git a/test/fixtures/test-runner/output/spec_reporter_cli.snapshot b/test/fixtures/test-runner/output/spec_reporter_cli.snapshot index a428b1140ac812..8f57e8714d4f6f 100644 --- a/test/fixtures/test-runner/output/spec_reporter_cli.snapshot +++ b/test/fixtures/test-runner/output/spec_reporter_cli.snapshot @@ -169,6 +169,9 @@ * * * + * + * + * * { generatedMessage: true, code: 'ERR_ASSERTION', diff --git a/test/parallel/test-assert.js b/test/parallel/test-assert.js index 0e9771dc6cacaf..98f010c8ea4634 100644 --- a/test/parallel/test-assert.js +++ b/test/parallel/test-assert.js @@ -1604,18 +1604,18 @@ test('Assert class basic instance', () => { assertInstance.ok(true); assertInstance.equal(undefined, undefined); assertInstance.notEqual(true, false); - assertInstance.throws( + assert.throws( () => assertInstance.deepEqual(/a/), { code: 'ERR_MISSING_ARGS' } ); - assertInstance.throws( + assert.throws( () => assertInstance.notDeepEqual('test'), { code: 'ERR_MISSING_ARGS' } ); assertInstance.notStrictEqual(2, '2'); - assertInstance.throws(() => assertInstance.strictEqual(2, '2'), - assertInstance.AssertionError, 'strictEqual(2, \'2\')'); - assertInstance.throws( + assert.throws(() => assertInstance.strictEqual(2, '2'), + assertInstance.AssertionError, 'strictEqual(2, \'2\')'); + assert.throws( () => { assertInstance.partialDeepStrictEqual({ a: true }, { a: false }, 'custom message'); }, @@ -1625,7 +1625,7 @@ test('Assert class basic instance', () => { message: 'custom message\n+ actual - expected\n\n {\n+ a: true\n- a: false\n }\n' } ); - assertInstance.throws( + assert.throws( () => assertInstance.match(/abc/, 'string'), { code: 'ERR_INVALID_ARG_TYPE', @@ -1633,7 +1633,7 @@ test('Assert class basic instance', () => { "Received type string ('string')" } ); - assertInstance.throws( + assert.throws( () => assertInstance.doesNotMatch(/abc/, 'string'), { code: 'ERR_INVALID_ARG_TYPE', diff --git a/test/parallel/test-runner-assert.js b/test/parallel/test-runner-assert.js index 2c495baca0afd2..236f1851d6d262 100644 --- a/test/parallel/test-runner-assert.js +++ b/test/parallel/test-runner-assert.js @@ -7,6 +7,8 @@ test('expected methods are on t.assert', (t) => { const uncopiedKeys = [ 'AssertionError', 'strict', + 'Assert', + 'options', ]; const assertKeys = Object.keys(assert).filter((key) => !uncopiedKeys.includes(key)); const expectedKeys = ['snapshot', 'fileSnapshot'].concat(assertKeys).sort(); From 435dc0b298d5b1f78c8af1294ca9d9b631551890 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Thu, 8 May 2025 14:58:11 -0300 Subject: [PATCH 06/28] lib: update Assert class to use instance methods for error handling --- lib/assert.js | 217 ++++++++++-------------------- test/parallel/test-assert.js | 14 +- test/parallel/test-fs-promises.js | 2 +- 3 files changed, 80 insertions(+), 153 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index 1391d0b5207795..0a4c5c7992f52c 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -119,57 +119,57 @@ class Assert { } } - // #expectsError(stackStartFn, actual, error, message) { - // if (typeof error === 'string') { - // if (arguments.length === 4) { - // throw new ERR_INVALID_ARG_TYPE('error', - // ['Object', 'Error', 'Function', 'RegExp'], - // error); - // } - // if (typeof actual === 'object' && actual !== null) { - // if (actual.message === error) { - // throw new ERR_AMBIGUOUS_ARGUMENT( - // 'error/message', - // `The error message "${actual.message}" is identical to the message.`, - // ); - // } - // } else if (actual === error) { - // throw new ERR_AMBIGUOUS_ARGUMENT( - // 'error/message', - // `The error "${actual}" is identical to the message.`, - // ); - // } - // message = error; - // error = undefined; - // } else if (error != null && - // typeof error !== 'object' && - // typeof error !== 'function') { - // throw new ERR_INVALID_ARG_TYPE('error', - // ['Object', 'Error', 'Function', 'RegExp'], - // error); - // } - - // if (actual === NO_EXCEPTION_SENTINEL) { - // let details = ''; - // if (error?.name) { - // details += ` (${error.name})`; - // } - // details += message ? `: ${message}` : '.'; - // const fnType = stackStartFn === Assert.prototype.rejects ? 'rejection' : 'exception'; - // innerFail({ - // actual: undefined, - // expected: error, - // operator: stackStartFn.name, - // message: `Missing expected ${fnType}${details}`, - // stackStartFn, - // }); - // } - - // if (!error) - // return; - - // expectedException(actual, error, message, stackStartFn); - // } + #expectsError(stackStartFn, actual, error, message) { + if (typeof error === 'string') { + if (arguments.length === 4) { + throw new ERR_INVALID_ARG_TYPE('error', + ['Object', 'Error', 'Function', 'RegExp'], + error); + } + if (typeof actual === 'object' && actual !== null) { + if (actual.message === error) { + throw new ERR_AMBIGUOUS_ARGUMENT( + 'error/message', + `The error message "${actual.message}" is identical to the message.`, + ); + } + } else if (actual === error) { + throw new ERR_AMBIGUOUS_ARGUMENT( + 'error/message', + `The error "${actual}" is identical to the message.`, + ); + } + message = error; + error = undefined; + } else if (error != null && + typeof error !== 'object' && + typeof error !== 'function') { + throw new ERR_INVALID_ARG_TYPE('error', + ['Object', 'Error', 'Function', 'RegExp'], + error); + } + + if (actual === NO_EXCEPTION_SENTINEL) { + let details = ''; + if (error?.name) { + details += ` (${error.name})`; + } + details += message ? `: ${message}` : '.'; + const fnType = stackStartFn === Assert.prototype.rejects ? 'rejection' : 'exception'; + innerFail({ + actual: undefined, + expected: error, + operator: stackStartFn.name, + message: `Missing expected ${fnType}${details}`, + stackStartFn, + }); + } + + if (!error) + return; + + expectedException(actual, error, message, stackStartFn); + } /** * Pure assertion tests whether a value is truthy, as determined @@ -462,31 +462,32 @@ class Assert { this.#internalMatch(string, regexp, message, Assert.prototype.doesNotMatch); }; - // /** - // * Expects the function `promiseFn` to throw an error. - // * @param {() => any} promiseFn - // * @param {...any} [args] - // * @returns {void} - // */ - // throws(promiseFn, ...args) { - // this.#expectsError(Assert.prototype.throws, getActual(promiseFn), ...args); - // }; - - // /** - // * Expects `promiseFn` function or its value to reject. - // * @param {() => Promise} promiseFn - // * @param {...any} [args] - // * @returns {Promise} - // */ - // async rejects(promiseFn, ...args) { - // this.#expectsError(Assert.prototype.rejects, await waitForActual(promiseFn), ...args); - // }; + /** + * Expects the function `promiseFn` to throw an error. + * @param {() => any} promiseFn + * @param {...any} [args] + * @returns {void} + */ + throws(promiseFn, ...args) { + this.#expectsError(Assert.prototype.throws, getActual(promiseFn), ...args); + }; + + /** + * Expects `promiseFn` function or its value to reject. + * @param {() => Promise} promiseFn + * @param {...any} [args] + * @returns {Promise} + */ + async rejects(promiseFn, ...args) { + this.#expectsError(Assert.prototype.rejects, await waitForActual(promiseFn), ...args); + }; } const assertInstance = new Assert(); ['ok', 'fail', 'equal', 'notEqual', 'deepEqual', 'notDeepEqual', 'deepStrictEqual', 'notDeepStrictEqual', 'strictEqual', - 'notStrictEqual', 'partialDeepStrictEqual', 'match', 'doesNotMatch'].forEach((name) => { + 'notStrictEqual', 'partialDeepStrictEqual', 'match', 'doesNotMatch', + 'throws', 'rejects'].forEach((name) => { assertInstance[name] = assertInstance[name].bind(assertInstance); }); @@ -947,58 +948,6 @@ async function waitForActual(promiseFn) { return NO_EXCEPTION_SENTINEL; } -function expectsError(stackStartFn, actual, error, message) { - if (typeof error === 'string') { - if (arguments.length === 4) { - throw new ERR_INVALID_ARG_TYPE('error', - ['Object', 'Error', 'Function', 'RegExp'], - error); - } - if (typeof actual === 'object' && actual !== null) { - if (actual.message === error) { - throw new ERR_AMBIGUOUS_ARGUMENT( - 'error/message', - `The error message "${actual.message}" is identical to the message.`, - ); - } - } else if (actual === error) { - throw new ERR_AMBIGUOUS_ARGUMENT( - 'error/message', - `The error "${actual}" is identical to the message.`, - ); - } - message = error; - error = undefined; - } else if (error != null && - typeof error !== 'object' && - typeof error !== 'function') { - throw new ERR_INVALID_ARG_TYPE('error', - ['Object', 'Error', 'Function', 'RegExp'], - error); - } - - if (actual === NO_EXCEPTION_SENTINEL) { - let details = ''; - if (error?.name) { - details += ` (${error.name})`; - } - details += message ? `: ${message}` : '.'; - const fnType = stackStartFn === assert.rejects ? 'rejection' : 'exception'; - innerFail({ - actual: undefined, - expected: error, - operator: stackStartFn.name, - message: `Missing expected ${fnType}${details}`, - stackStartFn, - }); - } - - if (!error) - return; - - expectedException(actual, error, message, stackStartFn); -} - function hasMatchingError(actual, expected) { if (typeof expected !== 'function') { if (isRegExp(expected)) { @@ -1044,26 +993,6 @@ function expectsNoError(stackStartFn, actual, error, message) { throw actual; } -/** - * Expects the function `promiseFn` to throw an error. - * @param {() => any} promiseFn - * @param {...any} [args] - * @returns {void} - */ -assert.throws = function throws(promiseFn, ...args) { - expectsError(throws, getActual(promiseFn), ...args); -}; - -/** - * Expects `promiseFn` function or its value to reject. - * @param {() => Promise} promiseFn - * @param {...any} [args] - * @returns {Promise} - */ -assert.rejects = async function rejects(promiseFn, ...args) { - expectsError(rejects, await waitForActual(promiseFn), ...args); -}; - /** * Asserts that the function `fn` does not throw an error. * @param {() => any} fn @@ -1156,7 +1085,6 @@ function strict(...args) { } assert.AssertionError = AssertionError; -// Assert.AssertionError = AssertionError; assert.strict = ObjectAssign(strict, assert, { equal: assert.strictEqual, @@ -1165,7 +1093,6 @@ assert.strict = ObjectAssign(strict, assert, { notDeepEqual: assert.notDeepStrictEqual, }); - assert.strict.Assert = Assert; assert.strict.strict = assert.strict; diff --git a/test/parallel/test-assert.js b/test/parallel/test-assert.js index 98f010c8ea4634..0e9771dc6cacaf 100644 --- a/test/parallel/test-assert.js +++ b/test/parallel/test-assert.js @@ -1604,18 +1604,18 @@ test('Assert class basic instance', () => { assertInstance.ok(true); assertInstance.equal(undefined, undefined); assertInstance.notEqual(true, false); - assert.throws( + assertInstance.throws( () => assertInstance.deepEqual(/a/), { code: 'ERR_MISSING_ARGS' } ); - assert.throws( + assertInstance.throws( () => assertInstance.notDeepEqual('test'), { code: 'ERR_MISSING_ARGS' } ); assertInstance.notStrictEqual(2, '2'); - assert.throws(() => assertInstance.strictEqual(2, '2'), - assertInstance.AssertionError, 'strictEqual(2, \'2\')'); - assert.throws( + assertInstance.throws(() => assertInstance.strictEqual(2, '2'), + assertInstance.AssertionError, 'strictEqual(2, \'2\')'); + assertInstance.throws( () => { assertInstance.partialDeepStrictEqual({ a: true }, { a: false }, 'custom message'); }, @@ -1625,7 +1625,7 @@ test('Assert class basic instance', () => { message: 'custom message\n+ actual - expected\n\n {\n+ a: true\n- a: false\n }\n' } ); - assert.throws( + assertInstance.throws( () => assertInstance.match(/abc/, 'string'), { code: 'ERR_INVALID_ARG_TYPE', @@ -1633,7 +1633,7 @@ test('Assert class basic instance', () => { "Received type string ('string')" } ); - assert.throws( + assertInstance.throws( () => assertInstance.doesNotMatch(/abc/, 'string'), { code: 'ERR_INVALID_ARG_TYPE', diff --git a/test/parallel/test-fs-promises.js b/test/parallel/test-fs-promises.js index 796ad3224c4dba..d2f523742798e4 100644 --- a/test/parallel/test-fs-promises.js +++ b/test/parallel/test-fs-promises.js @@ -58,7 +58,7 @@ assert.strictEqual( code: 'ENOENT', name: 'Error', message: /^ENOENT: no such file or directory, access/, - stack: /at async ok\.rejects/ + stack: /at async Assert\.rejects/ } ).then(common.mustCall()); From 5c3513298791c6eec357c5835b0e7ca322c4902c Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Thu, 8 May 2025 17:54:36 -0300 Subject: [PATCH 07/28] lib: add doesNotThrow and doesNotReject methods in Assert class --- lib/assert.js | 103 ++++++++++++++++++----------------- test/parallel/test-assert.js | 31 +++++++++++ 2 files changed, 83 insertions(+), 51 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index 0a4c5c7992f52c..76648d8421d689 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -82,6 +82,7 @@ const NO_EXCEPTION_SENTINEL = {}; class Assert { constructor(options = {}) { this.options = options; + this.AssertionError = AssertionError; } #internalMatch(string, regexp, message, fn) { @@ -171,12 +172,37 @@ class Assert { expectedException(actual, error, message, stackStartFn); } + #expectsNoError(stackStartFn, actual, error, message) { + if (actual === NO_EXCEPTION_SENTINEL) + return; + + if (typeof error === 'string') { + message = error; + error = undefined; + } + + if (!error || hasMatchingError(actual, error)) { + const details = message ? `: ${message}` : '.'; + const fnType = stackStartFn === Assert.prototype.doesNotReject ? + 'rejection' : 'exception'; + innerFail({ + actual, + expected: error, + operator: stackStartFn.name, + message: `Got unwanted ${fnType}${details}\n` + + `Actual message: "${actual?.message}"`, + stackStartFn, + }); + } + throw actual; + } + /** - * Pure assertion tests whether a value is truthy, as determined - * by !!value. - * @param {...any} args - * @returns {void} - */ + * Pure assertion tests whether a value is truthy, as determined + * by !!value. + * @param {...any} args + * @returns {void} + */ ok(...args) { innerOk(this.ok, args.length, ...args); } @@ -481,13 +507,33 @@ class Assert { async rejects(promiseFn, ...args) { this.#expectsError(Assert.prototype.rejects, await waitForActual(promiseFn), ...args); }; + + /** + * Asserts that the function `fn` does not throw an error. + * @param {() => any} fn + * @param {...any} [args] + * @returns {void} + */ + doesNotThrow(fn, ...args) { + this.#expectsNoError(Assert.prototype.doesNotThrow, getActual(fn), ...args); + }; + + /** + * Expects `fn` or its value to not reject. + * @param {() => Promise} fn + * @param {...any} [args] + * @returns {Promise} + */ + async doesNotReject(fn, ...args) { + this.#expectsNoError(Assert.prototype.doesNotReject, await waitForActual(fn), ...args); + }; } const assertInstance = new Assert(); ['ok', 'fail', 'equal', 'notEqual', 'deepEqual', 'notDeepEqual', 'deepStrictEqual', 'notDeepStrictEqual', 'strictEqual', 'notStrictEqual', 'partialDeepStrictEqual', 'match', 'doesNotMatch', - 'throws', 'rejects'].forEach((name) => { + 'throws', 'rejects', 'doesNotThrow', 'doesNotReject'].forEach((name) => { assertInstance[name] = assertInstance[name].bind(assertInstance); }); @@ -968,51 +1014,6 @@ function hasMatchingError(actual, expected) { return ReflectApply(expected, {}, [actual]) === true; } -function expectsNoError(stackStartFn, actual, error, message) { - if (actual === NO_EXCEPTION_SENTINEL) - return; - - if (typeof error === 'string') { - message = error; - error = undefined; - } - - if (!error || hasMatchingError(actual, error)) { - const details = message ? `: ${message}` : '.'; - const fnType = stackStartFn === assert.doesNotReject ? - 'rejection' : 'exception'; - innerFail({ - actual, - expected: error, - operator: stackStartFn.name, - message: `Got unwanted ${fnType}${details}\n` + - `Actual message: "${actual?.message}"`, - stackStartFn, - }); - } - throw actual; -} - -/** - * Asserts that the function `fn` does not throw an error. - * @param {() => any} fn - * @param {...any} [args] - * @returns {void} - */ -assert.doesNotThrow = function doesNotThrow(fn, ...args) { - expectsNoError(doesNotThrow, getActual(fn), ...args); -}; - -/** - * Expects `fn` or its value to not reject. - * @param {() => Promise} fn - * @param {...any} [args] - * @returns {Promise} - */ -assert.doesNotReject = async function doesNotReject(fn, ...args) { - expectsNoError(doesNotReject, await waitForActual(fn), ...args); -}; - /** * Throws `value` if the value is not `null` or `undefined`. * @param {any} err diff --git a/test/parallel/test-assert.js b/test/parallel/test-assert.js index 0e9771dc6cacaf..f1c48bd8d4caab 100644 --- a/test/parallel/test-assert.js +++ b/test/parallel/test-assert.js @@ -1641,6 +1641,37 @@ test('Assert class basic instance', () => { "Received type string ('string')" } ); + + { + function thrower(errorConstructor) { + throw new errorConstructor({}); + } + + let threw = false; + try { + assertInstance.doesNotThrow(() => thrower(TypeError), assertInstance.AssertionError); + } catch (e) { + threw = true; + assertInstance.ok(e instanceof TypeError); + } + assertInstance.ok(threw, 'assertInstance.doesNotThrow with an explicit error is eating extra errors'); + } + { + let threw = false; + const rangeError = new RangeError('my range'); + + try { + assertInstance.doesNotThrow(() => { + throw new TypeError('wrong type'); + }, TypeError, rangeError); + } catch (e) { + threw = true; + assertInstance.ok(e.message.includes(rangeError.message)); + assertInstance.ok(e instanceof assertInstance.AssertionError); + assertInstance.ok(!e.stack.includes('doesNotThrow'), e); + } + assertInstance.ok(threw); + } }); /* eslint-enable no-restricted-syntax */ From eecf3ccb918203a9fca74efe1e0e4a841b83edef Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Fri, 9 May 2025 15:24:43 -0300 Subject: [PATCH 08/28] lib: add ifError method to Assert class --- lib/assert.js | 124 +++++++++--------- .../errors/if-error-has-good-stack.snapshot | 8 +- 2 files changed, 65 insertions(+), 67 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index 76648d8421d689..1229e3e39a207b 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -527,13 +527,73 @@ class Assert { async doesNotReject(fn, ...args) { this.#expectsNoError(Assert.prototype.doesNotReject, await waitForActual(fn), ...args); }; + + /** + * Throws `value` if the value is not `null` or `undefined`. + * @param {any} err + * @returns {void} + */ + ifError(err) { + if (err !== null && err !== undefined) { + let message = 'ifError got unwanted exception: '; + if (typeof err === 'object' && typeof err.message === 'string') { + if (err.message.length === 0 && err.constructor) { + message += err.constructor.name; + } else { + message += err.message; + } + } else { + message += inspect(err); + } + + const newErr = new AssertionError({ + actual: err, + expected: null, + operator: 'ifError', + message, + stackStartFn: this.ifError, + }); + + // Make sure we actually have a stack trace! + const origStack = err.stack; + + if (typeof origStack === 'string') { + // This will remove any duplicated frames from the error frames taken + // from within `ifError` and add the original error frames to the newly + // created ones. + const origStackStart = StringPrototypeIndexOf(origStack, '\n at'); + if (origStackStart !== -1) { + const originalFrames = StringPrototypeSplit( + StringPrototypeSlice(origStack, origStackStart + 1), + '\n', + ); + // Filter all frames existing in err.stack. + let newFrames = StringPrototypeSplit(newErr.stack, '\n'); + for (const errFrame of originalFrames) { + // Find the first occurrence of the frame. + const pos = ArrayPrototypeIndexOf(newFrames, errFrame); + if (pos !== -1) { + // Only keep new frames. + newFrames = ArrayPrototypeSlice(newFrames, 0, pos); + break; + } + } + const stackStart = ArrayPrototypeJoin(newFrames, '\n'); + const stackEnd = ArrayPrototypeJoin(originalFrames, '\n'); + newErr.stack = `${stackStart}\n${stackEnd}`; + } + } + + throw newErr; + } + }; } const assertInstance = new Assert(); ['ok', 'fail', 'equal', 'notEqual', 'deepEqual', 'notDeepEqual', 'deepStrictEqual', 'notDeepStrictEqual', 'strictEqual', 'notStrictEqual', 'partialDeepStrictEqual', 'match', 'doesNotMatch', - 'throws', 'rejects', 'doesNotThrow', 'doesNotReject'].forEach((name) => { + 'throws', 'rejects', 'doesNotThrow', 'doesNotReject', 'ifError'].forEach((name) => { assertInstance[name] = assertInstance[name].bind(assertInstance); }); @@ -1014,68 +1074,6 @@ function hasMatchingError(actual, expected) { return ReflectApply(expected, {}, [actual]) === true; } -/** - * Throws `value` if the value is not `null` or `undefined`. - * @param {any} err - * @returns {void} - */ -assert.ifError = function ifError(err) { - if (err !== null && err !== undefined) { - let message = 'ifError got unwanted exception: '; - if (typeof err === 'object' && typeof err.message === 'string') { - if (err.message.length === 0 && err.constructor) { - message += err.constructor.name; - } else { - message += err.message; - } - } else { - message += inspect(err); - } - - const newErr = new AssertionError({ - actual: err, - expected: null, - operator: 'ifError', - message, - stackStartFn: ifError, - }); - - // Make sure we actually have a stack trace! - const origStack = err.stack; - - if (typeof origStack === 'string') { - // This will remove any duplicated frames from the error frames taken - // from within `ifError` and add the original error frames to the newly - // created ones. - const origStackStart = StringPrototypeIndexOf(origStack, '\n at'); - if (origStackStart !== -1) { - const originalFrames = StringPrototypeSplit( - StringPrototypeSlice(origStack, origStackStart + 1), - '\n', - ); - // Filter all frames existing in err.stack. - let newFrames = StringPrototypeSplit(newErr.stack, '\n'); - for (const errFrame of originalFrames) { - // Find the first occurrence of the frame. - const pos = ArrayPrototypeIndexOf(newFrames, errFrame); - if (pos !== -1) { - // Only keep new frames. - newFrames = ArrayPrototypeSlice(newFrames, 0, pos); - break; - } - } - const stackStart = ArrayPrototypeJoin(newFrames, '\n'); - const stackEnd = ArrayPrototypeJoin(originalFrames, '\n'); - newErr.stack = `${stackStart}\n${stackEnd}`; - } - } - - throw newErr; - } -}; - -assert.CallTracker = deprecate(CallTracker, 'assert.CallTracker is deprecated.', 'DEP0173'); - /** * Expose a strict only variant of assert. * @param {...any} args diff --git a/test/fixtures/errors/if-error-has-good-stack.snapshot b/test/fixtures/errors/if-error-has-good-stack.snapshot index 9296b25f10b7c6..30477c483de7f8 100644 --- a/test/fixtures/errors/if-error-has-good-stack.snapshot +++ b/test/fixtures/errors/if-error-has-good-stack.snapshot @@ -1,12 +1,12 @@ node:assert:* - throw newErr; - ^ + throw newErr; + ^ AssertionError [ERR_ASSERTION]: ifError got unwanted exception: test error + at new AssertionError (node:internal*assert*assertion_error:*:*) + at Assert.ifError (node:assert:*:*) at z (*if-error-has-good-stack.js:*:*) at y (*if-error-has-good-stack.js:*:*) - at x (*if-error-has-good-stack.js:*:*) - at Object. (*if-error-has-good-stack.js:*:*) at c (*if-error-has-good-stack.js:*:*) at b (*if-error-has-good-stack.js:*:*) at a (*if-error-has-good-stack.js:*:*) From 4eb5923282314988298bef244dad29d203e8ee89 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Fri, 9 May 2025 15:50:38 -0300 Subject: [PATCH 09/28] lib: refactor expectedException function into a private method in Assert class --- lib/assert.js | 214 +++++++++++++++++++++++++------------------------- 1 file changed, 107 insertions(+), 107 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index 1229e3e39a207b..4075af8c0c28e8 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -120,6 +120,112 @@ class Assert { } } + #expectedException(actual, expected, message, fn) { + let generatedMessage = false; + let throwError = false; + + if (typeof expected !== 'function') { + // Handle regular expressions. + if (isRegExp(expected)) { + const str = String(actual); + if (RegExpPrototypeExec(expected, str) !== null) + return; + + if (!message) { + generatedMessage = true; + message = 'The input did not match the regular expression ' + + `${inspect(expected)}. Input:\n\n${inspect(str)}\n`; + } + throwError = true; + // Handle primitives properly. + } else if (typeof actual !== 'object' || actual === null) { + const err = new AssertionError({ + actual, + expected, + message, + operator: 'deepStrictEqual', + stackStartFn: fn, + }); + err.operator = fn.name; + throw err; + } else { + // Handle validation objects. + const keys = ObjectKeys(expected); + // Special handle errors to make sure the name and the message are + // compared as well. + if (expected instanceof Error) { + ArrayPrototypePush(keys, 'name', 'message'); + } else if (keys.length === 0) { + throw new ERR_INVALID_ARG_VALUE('error', + expected, 'may not be an empty object'); + } + if (isDeepEqual === undefined) lazyLoadComparison(); + for (const key of keys) { + if (typeof actual[key] === 'string' && + isRegExp(expected[key]) && + RegExpPrototypeExec(expected[key], actual[key]) !== null) { + continue; + } + compareExceptionKey(actual, expected, key, message, keys, fn); + } + return; + } + // Guard instanceof against arrow functions as they don't have a prototype. + // Check for matching Error classes. + } else if (expected.prototype !== undefined && actual instanceof expected) { + return; + } else if (ObjectPrototypeIsPrototypeOf(Error, expected)) { + if (!message) { + generatedMessage = true; + message = 'The error is expected to be an instance of ' + + `"${expected.name}". Received `; + if (isError(actual)) { + const name = (actual.constructor?.name) || + actual.name; + if (expected.name === name) { + message += 'an error with identical name but a different prototype.'; + } else { + message += `"${name}"`; + } + if (actual.message) { + message += `\n\nError message:\n\n${actual.message}`; + } + } else { + message += `"${inspect(actual, { depth: -1 })}"`; + } + } + throwError = true; + } else { + // Check validation functions return value. + const res = ReflectApply(expected, {}, [actual]); + if (res !== true) { + if (!message) { + generatedMessage = true; + const name = expected.name ? `"${expected.name}" ` : ''; + message = `The ${name}validation function is expected to return` + + ` "true". Received ${inspect(res)}`; + + if (isError(actual)) { + message += `\n\nCaught error:\n\n${actual}`; + } + } + throwError = true; + } + } + + if (throwError) { + const err = new AssertionError({ + actual, + expected, + message, + operator: fn.name, + stackStartFn: fn, + }); + err.generatedMessage = generatedMessage; + throw err; + } + } + #expectsError(stackStartFn, actual, error, message) { if (typeof error === 'string') { if (arguments.length === 4) { @@ -169,7 +275,7 @@ class Assert { if (!error) return; - expectedException(actual, error, message, stackStartFn); + this.#expectedException(actual, error, message, stackStartFn); } #expectsNoError(stackStartFn, actual, error, message) { @@ -903,112 +1009,6 @@ function compareExceptionKey(actual, expected, key, message, keys, fn) { } } -function expectedException(actual, expected, message, fn) { - let generatedMessage = false; - let throwError = false; - - if (typeof expected !== 'function') { - // Handle regular expressions. - if (isRegExp(expected)) { - const str = String(actual); - if (RegExpPrototypeExec(expected, str) !== null) - return; - - if (!message) { - generatedMessage = true; - message = 'The input did not match the regular expression ' + - `${inspect(expected)}. Input:\n\n${inspect(str)}\n`; - } - throwError = true; - // Handle primitives properly. - } else if (typeof actual !== 'object' || actual === null) { - const err = new AssertionError({ - actual, - expected, - message, - operator: 'deepStrictEqual', - stackStartFn: fn, - }); - err.operator = fn.name; - throw err; - } else { - // Handle validation objects. - const keys = ObjectKeys(expected); - // Special handle errors to make sure the name and the message are - // compared as well. - if (expected instanceof Error) { - ArrayPrototypePush(keys, 'name', 'message'); - } else if (keys.length === 0) { - throw new ERR_INVALID_ARG_VALUE('error', - expected, 'may not be an empty object'); - } - if (isDeepEqual === undefined) lazyLoadComparison(); - for (const key of keys) { - if (typeof actual[key] === 'string' && - isRegExp(expected[key]) && - RegExpPrototypeExec(expected[key], actual[key]) !== null) { - continue; - } - compareExceptionKey(actual, expected, key, message, keys, fn); - } - return; - } - // Guard instanceof against arrow functions as they don't have a prototype. - // Check for matching Error classes. - } else if (expected.prototype !== undefined && actual instanceof expected) { - return; - } else if (ObjectPrototypeIsPrototypeOf(Error, expected)) { - if (!message) { - generatedMessage = true; - message = 'The error is expected to be an instance of ' + - `"${expected.name}". Received `; - if (isError(actual)) { - const name = (actual.constructor?.name) || - actual.name; - if (expected.name === name) { - message += 'an error with identical name but a different prototype.'; - } else { - message += `"${name}"`; - } - if (actual.message) { - message += `\n\nError message:\n\n${actual.message}`; - } - } else { - message += `"${inspect(actual, { depth: -1 })}"`; - } - } - throwError = true; - } else { - // Check validation functions return value. - const res = ReflectApply(expected, {}, [actual]); - if (res !== true) { - if (!message) { - generatedMessage = true; - const name = expected.name ? `"${expected.name}" ` : ''; - message = `The ${name}validation function is expected to return` + - ` "true". Received ${inspect(res)}`; - - if (isError(actual)) { - message += `\n\nCaught error:\n\n${actual}`; - } - } - throwError = true; - } - } - - if (throwError) { - const err = new AssertionError({ - actual, - expected, - message, - operator: fn.name, - stackStartFn: fn, - }); - err.generatedMessage = generatedMessage; - throw err; - } -} - function getActual(fn) { validateFunction(fn, 'fn'); try { From e1f06e4cbe3847124e43a535e2f46a8c780b2d29 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Fri, 9 May 2025 16:02:21 -0300 Subject: [PATCH 10/28] lib: refactor compareExceptionKey function into a private method in Assert class --- lib/assert.js | 58 +++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index 4075af8c0c28e8..df05f9dae1c0cf 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -120,6 +120,34 @@ class Assert { } } + #compareExceptionKey(actual, expected, key, message, keys, fn) { + if (!(key in actual) || !isDeepStrictEqual(actual[key], expected[key])) { + if (!message) { + // Create placeholder objects to create a nice output. + const a = new Comparison(actual, keys); + const b = new Comparison(expected, keys, actual); + + const err = new AssertionError({ + actual: a, + expected: b, + operator: 'deepStrictEqual', + stackStartFn: fn, + }); + err.actual = actual; + err.expected = expected; + err.operator = fn.name; + throw err; + } + innerFail({ + actual, + expected, + message, + operator: fn.name, + stackStartFn: fn, + }); + } + } + #expectedException(actual, expected, message, fn) { let generatedMessage = false; let throwError = false; @@ -166,7 +194,7 @@ class Assert { RegExpPrototypeExec(expected[key], actual[key]) !== null) { continue; } - compareExceptionKey(actual, expected, key, message, keys, fn); + this.#compareExceptionKey(actual, expected, key, message, keys, fn); } return; } @@ -981,34 +1009,6 @@ class Comparison { } } -function compareExceptionKey(actual, expected, key, message, keys, fn) { - if (!(key in actual) || !isDeepStrictEqual(actual[key], expected[key])) { - if (!message) { - // Create placeholder objects to create a nice output. - const a = new Comparison(actual, keys); - const b = new Comparison(expected, keys, actual); - - const err = new AssertionError({ - actual: a, - expected: b, - operator: 'deepStrictEqual', - stackStartFn: fn, - }); - err.actual = actual; - err.expected = expected; - err.operator = fn.name; - throw err; - } - innerFail({ - actual, - expected, - message, - operator: fn.name, - stackStartFn: fn, - }); - } -} - function getActual(fn) { validateFunction(fn, 'fn'); try { From 8e6a8c2bfb8ecad97b4b55ab4c7751a8f586cd15 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Fri, 9 May 2025 16:14:46 -0300 Subject: [PATCH 11/28] lib: add private #waitForActual method to Assert class for promise handling --- lib/assert.js | 54 +++++++++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index df05f9dae1c0cf..154f9cc1c1e5f4 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -331,6 +331,31 @@ class Assert { throw actual; } + async #waitForActual(promiseFn) { + let resultPromise; + if (typeof promiseFn === 'function') { + // Return a rejected promise if `promiseFn` throws synchronously. + resultPromise = promiseFn(); + // Fail in case no promise is returned. + if (!checkIsPromise(resultPromise)) { + throw new ERR_INVALID_RETURN_VALUE('instance of Promise', + 'promiseFn', resultPromise); + } + } else if (checkIsPromise(promiseFn)) { + resultPromise = promiseFn; + } else { + throw new ERR_INVALID_ARG_TYPE( + 'promiseFn', ['Function', 'Promise'], promiseFn); + } + + try { + await resultPromise; + } catch (e) { + return e; + } + return NO_EXCEPTION_SENTINEL; + } + /** * Pure assertion tests whether a value is truthy, as determined * by !!value. @@ -639,7 +664,7 @@ class Assert { * @returns {Promise} */ async rejects(promiseFn, ...args) { - this.#expectsError(Assert.prototype.rejects, await waitForActual(promiseFn), ...args); + this.#expectsError(Assert.prototype.rejects, await this.#waitForActual(promiseFn), ...args); }; /** @@ -659,7 +684,7 @@ class Assert { * @returns {Promise} */ async doesNotReject(fn, ...args) { - this.#expectsNoError(Assert.prototype.doesNotReject, await waitForActual(fn), ...args); + this.#expectsNoError(Assert.prototype.doesNotReject, await this.#waitForActual(fn), ...args); }; /** @@ -1029,31 +1054,6 @@ function checkIsPromise(obj) { typeof obj.catch === 'function'); } -async function waitForActual(promiseFn) { - let resultPromise; - if (typeof promiseFn === 'function') { - // Return a rejected promise if `promiseFn` throws synchronously. - resultPromise = promiseFn(); - // Fail in case no promise is returned. - if (!checkIsPromise(resultPromise)) { - throw new ERR_INVALID_RETURN_VALUE('instance of Promise', - 'promiseFn', resultPromise); - } - } else if (checkIsPromise(promiseFn)) { - resultPromise = promiseFn; - } else { - throw new ERR_INVALID_ARG_TYPE( - 'promiseFn', ['Function', 'Promise'], promiseFn); - } - - try { - await resultPromise; - } catch (e) { - return e; - } - return NO_EXCEPTION_SENTINEL; -} - function hasMatchingError(actual, expected) { if (typeof expected !== 'function') { if (isRegExp(expected)) { From 787c15218561a54e164f9775e0448614d9468460 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Fri, 9 May 2025 16:27:39 -0300 Subject: [PATCH 12/28] lib: add innerFail as private method to Assert class --- lib/assert.js | 30 ++++++++++++++---------- test/fixtures/errors/error_exit.snapshot | 4 ++-- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index 154f9cc1c1e5f4..23811ad6fdcf59 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -85,6 +85,12 @@ class Assert { this.AssertionError = AssertionError; } + #innerFail(obj) { + if (obj.message instanceof Error) throw obj.message; + + throw new AssertionError(obj); + } + #internalMatch(string, regexp, message, fn) { if (!isRegExp(regexp)) { throw new ERR_INVALID_ARG_TYPE( @@ -138,7 +144,7 @@ class Assert { err.operator = fn.name; throw err; } - innerFail({ + this.#innerFail({ actual, expected, message, @@ -291,7 +297,7 @@ class Assert { } details += message ? `: ${message}` : '.'; const fnType = stackStartFn === Assert.prototype.rejects ? 'rejection' : 'exception'; - innerFail({ + this.#innerFail({ actual: undefined, expected: error, operator: stackStartFn.name, @@ -319,7 +325,7 @@ class Assert { const details = message ? `: ${message}` : '.'; const fnType = stackStartFn === Assert.prototype.doesNotReject ? 'rejection' : 'exception'; - innerFail({ + this.#innerFail({ actual, expected: error, operator: stackStartFn.name, @@ -426,7 +432,7 @@ class Assert { } // eslint-disable-next-line eqeqeq if (actual != expected && (!NumberIsNaN(actual) || !NumberIsNaN(expected))) { - innerFail({ + this.#innerFail({ actual, expected, message, @@ -450,7 +456,7 @@ class Assert { } // eslint-disable-next-line eqeqeq if (actual == expected || (NumberIsNaN(actual) && NumberIsNaN(expected))) { - innerFail({ + this.#innerFail({ actual, expected, message, @@ -473,7 +479,7 @@ class Assert { } if (isDeepEqual === undefined) lazyLoadComparison(); if (!isDeepEqual(actual, expected)) { - innerFail({ + this.#innerFail({ actual, expected, message, @@ -496,7 +502,7 @@ class Assert { } if (isDeepEqual === undefined) lazyLoadComparison(); if (isDeepEqual(actual, expected)) { - innerFail({ + this.#innerFail({ actual, expected, message, @@ -520,7 +526,7 @@ class Assert { } if (isDeepEqual === undefined) lazyLoadComparison(); if (!isDeepStrictEqual(actual, expected)) { - innerFail({ + this.#innerFail({ actual, expected, message, @@ -544,7 +550,7 @@ class Assert { } if (isDeepEqual === undefined) lazyLoadComparison(); if (isDeepStrictEqual(actual, expected)) { - innerFail({ + this.#innerFail({ actual, expected, message, @@ -566,7 +572,7 @@ class Assert { throw new ERR_MISSING_ARGS('actual', 'expected'); } if (!ObjectIs(actual, expected)) { - innerFail({ + this.#innerFail({ actual, expected, message, @@ -588,7 +594,7 @@ class Assert { throw new ERR_MISSING_ARGS('actual', 'expected'); } if (ObjectIs(actual, expected)) { - innerFail({ + this.#innerFail({ actual, expected, message, @@ -615,7 +621,7 @@ class Assert { } if (isDeepEqual === undefined) lazyLoadComparison(); if (!isPartialStrictEqual(actual, expected)) { - innerFail({ + this.#innerFail({ actual, expected, message, diff --git a/test/fixtures/errors/error_exit.snapshot b/test/fixtures/errors/error_exit.snapshot index fff8f868e6f876..de4ba32c4560a1 100644 --- a/test/fixtures/errors/error_exit.snapshot +++ b/test/fixtures/errors/error_exit.snapshot @@ -1,7 +1,7 @@ Exiting with code=1 node:assert:* - throw new AssertionError(obj); - ^ + throw new AssertionError(obj); + ^ AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: From 9e72716e6d55aea8cef4b68f33294e92f6168eb9 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Fri, 9 May 2025 16:43:04 -0300 Subject: [PATCH 13/28] lib: add getActual as priivate method to Assert class --- lib/assert.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index 23811ad6fdcf59..b0aea8a4306585 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -362,6 +362,16 @@ class Assert { return NO_EXCEPTION_SENTINEL; } + #getActual(fn) { + validateFunction(fn, 'fn'); + try { + fn(); + } catch (e) { + return e; + } + return NO_EXCEPTION_SENTINEL; + } + /** * Pure assertion tests whether a value is truthy, as determined * by !!value. @@ -660,7 +670,7 @@ class Assert { * @returns {void} */ throws(promiseFn, ...args) { - this.#expectsError(Assert.prototype.throws, getActual(promiseFn), ...args); + this.#expectsError(Assert.prototype.throws, this.#getActual(promiseFn), ...args); }; /** @@ -680,7 +690,7 @@ class Assert { * @returns {void} */ doesNotThrow(fn, ...args) { - this.#expectsNoError(Assert.prototype.doesNotThrow, getActual(fn), ...args); + this.#expectsNoError(Assert.prototype.doesNotThrow, this.#getActual(fn), ...args); }; /** @@ -1040,16 +1050,6 @@ class Comparison { } } -function getActual(fn) { - validateFunction(fn, 'fn'); - try { - fn(); - } catch (e) { - return e; - } - return NO_EXCEPTION_SENTINEL; -} - function checkIsPromise(obj) { // Accept native ES6 promises and promises that are implemented in a similar // way. Do not accept thenables that use a function as `obj` and that have no From a4f291b6fbb3341412201a739a2075c54d8dd92d Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Fri, 9 May 2025 16:50:26 -0300 Subject: [PATCH 14/28] lib: move checkIsPromise function to private method in Assert class --- lib/assert.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index b0aea8a4306585..89c412fafce5eb 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -343,11 +343,11 @@ class Assert { // Return a rejected promise if `promiseFn` throws synchronously. resultPromise = promiseFn(); // Fail in case no promise is returned. - if (!checkIsPromise(resultPromise)) { + if (!this.#checkIsPromise(resultPromise)) { throw new ERR_INVALID_RETURN_VALUE('instance of Promise', 'promiseFn', resultPromise); } - } else if (checkIsPromise(promiseFn)) { + } else if (this.#checkIsPromise(promiseFn)) { resultPromise = promiseFn; } else { throw new ERR_INVALID_ARG_TYPE( @@ -372,6 +372,16 @@ class Assert { return NO_EXCEPTION_SENTINEL; } + #checkIsPromise(obj) { + // Accept native ES6 promises and promises that are implemented in a similar + // way. Do not accept thenables that use a function as `obj` and that have no + // `catch` handler. + return isPromise(obj) || + (obj !== null && typeof obj === 'object' && + typeof obj.then === 'function' && + typeof obj.catch === 'function'); + } + /** * Pure assertion tests whether a value is truthy, as determined * by !!value. @@ -1050,16 +1060,6 @@ class Comparison { } } -function checkIsPromise(obj) { - // Accept native ES6 promises and promises that are implemented in a similar - // way. Do not accept thenables that use a function as `obj` and that have no - // `catch` handler. - return isPromise(obj) || - (obj !== null && typeof obj === 'object' && - typeof obj.then === 'function' && - typeof obj.catch === 'function'); -} - function hasMatchingError(actual, expected) { if (typeof expected !== 'function') { if (isRegExp(expected)) { From 863075e0afbfbb245b2bb8b29301f03bef52e4ef Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Fri, 9 May 2025 17:13:44 -0300 Subject: [PATCH 15/28] lib: move hasMatchingError as private method to Assert --- lib/assert.js | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/lib/assert.js b/lib/assert.js index 89c412fafce5eb..786fca7da03325 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -79,12 +79,35 @@ const assert = module.exports = ok; const NO_EXCEPTION_SENTINEL = {}; +class Comparison { + constructor(obj, keys, actual) { + for (const key of keys) { + if (key in obj) { + if (actual !== undefined && + typeof actual[key] === 'string' && + isRegExp(obj[key]) && + RegExpPrototypeExec(obj[key], actual[key]) !== null) { + this[key] = actual[key]; + } else { + this[key] = obj[key]; + } + } + } + } +} + class Assert { constructor(options = {}) { this.options = options; this.AssertionError = AssertionError; } + // All of the following functions must throw an AssertionError + // when a corresponding condition is not met, with a message that + // may be undefined if not provided. All assertion methods provide + // both the actual and expected values to the assertion error for + // display purposes. + #innerFail(obj) { if (obj.message instanceof Error) throw obj.message; @@ -321,7 +344,7 @@ class Assert { error = undefined; } - if (!error || hasMatchingError(actual, error)) { + if (!error || this.#hasMatchingError(actual, error)) { const details = message ? `: ${message}` : '.'; const fnType = stackStartFn === Assert.prototype.doesNotReject ? 'rejection' : 'exception'; @@ -337,6 +360,26 @@ class Assert { throw actual; } + #hasMatchingError(actual, expected) { + if (typeof expected !== 'function') { + if (isRegExp(expected)) { + const str = String(actual); + return RegExpPrototypeExec(expected, str) !== null; + } + throw new ERR_INVALID_ARG_TYPE( + 'expected', ['Function', 'RegExp'], expected, + ); + } + // Guard instanceof against arrow functions as they don't have a prototype. + if (expected.prototype !== undefined && actual instanceof expected) { + return true; + } + if (ObjectPrototypeIsPrototypeOf(Error, expected)) { + return false; + } + return ReflectApply(expected, {}, [actual]) === true; + } + async #waitForActual(promiseFn) { let resultPromise; if (typeof promiseFn === 'function') { From 27269f9d761d7bc0fbeb909191b10ba9ef5611da Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Fri, 9 May 2025 18:31:38 -0300 Subject: [PATCH 16/28] test: move Assert class instance tests to separate file --- lib/assert.js | 1 - test/parallel/test-assert-class.js | 106 +++++++++++++++++++++++++++++ test/parallel/test-assert.js | 79 --------------------- 3 files changed, 106 insertions(+), 80 deletions(-) create mode 100644 test/parallel/test-assert-class.js diff --git a/lib/assert.js b/lib/assert.js index 786fca7da03325..b17f6cade2fc8b 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -1144,5 +1144,4 @@ assert.strict = ObjectAssign(strict, assert, { assert.strict.Assert = Assert; assert.strict.strict = assert.strict; -module.exports = assert; module.exports.Assert = Assert; diff --git a/test/parallel/test-assert-class.js b/test/parallel/test-assert-class.js new file mode 100644 index 00000000000000..8a3550aa4e37ff --- /dev/null +++ b/test/parallel/test-assert-class.js @@ -0,0 +1,106 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const { Assert } = require('assert'); +const { test } = require('node:test'); + +// Disable colored output to prevent color codes from breaking assertion +// message comparisons. This should only be an issue when process.stdout +// is a TTY. +if (process.stdout.isTTY) { + process.env.NODE_DISABLE_COLORS = '1'; +} + +test('Assert class basic instance', () => { + const assertInstance = new Assert(); + + assertInstance.ok(assert.AssertionError.prototype instanceof Error, + 'assert.AssertionError instanceof Error'); + assertInstance.ok(true); + assertInstance.throws( + () => { assertInstance.fail(); }, + { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: 'Failed', + operator: 'fail', + actual: undefined, + expected: undefined, + generatedMessage: true, + stack: /Failed/ + } + ); + assertInstance.equal(undefined, undefined); + assertInstance.notEqual(true, false); + assertInstance.throws( + () => assertInstance.deepEqual(/a/), + { code: 'ERR_MISSING_ARGS' } + ); + assertInstance.throws( + () => assertInstance.notDeepEqual('test'), + { code: 'ERR_MISSING_ARGS' } + ); + assertInstance.notStrictEqual(2, '2'); + assertInstance.throws(() => assertInstance.strictEqual(2, '2'), + assertInstance.AssertionError, 'strictEqual(2, \'2\')'); + assertInstance.throws( + () => { + assertInstance.partialDeepStrictEqual({ a: true }, { a: false }, 'custom message'); + }, + { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: 'custom message\n+ actual - expected\n\n {\n+ a: true\n- a: false\n }\n' + } + ); + assertInstance.throws( + () => assertInstance.match(/abc/, 'string'), + { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "regexp" argument must be an instance of RegExp. ' + + "Received type string ('string')" + } + ); + assertInstance.throws( + () => assertInstance.doesNotMatch(/abc/, 'string'), + { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "regexp" argument must be an instance of RegExp. ' + + "Received type string ('string')" + } + ); + + /* eslint-disable no-restricted-syntax */ + { + function thrower(errorConstructor) { + throw new errorConstructor({}); + } + + let threw = false; + try { + assertInstance.doesNotThrow(() => thrower(TypeError), assertInstance.AssertionError); + } catch (e) { + threw = true; + assertInstance.ok(e instanceof TypeError); + } + assertInstance.ok(threw, 'assertInstance.doesNotThrow with an explicit error is eating extra errors'); + } + { + let threw = false; + const rangeError = new RangeError('my range'); + + try { + assertInstance.doesNotThrow(() => { + throw new TypeError('wrong type'); + }, TypeError, rangeError); + } catch (e) { + threw = true; + assertInstance.ok(e.message.includes(rangeError.message)); + assertInstance.ok(e instanceof assertInstance.AssertionError); + assertInstance.ok(!e.stack.includes('doesNotThrow'), e); + } + assertInstance.ok(threw); + } + /* eslint-enable no-restricted-syntax */ +}); diff --git a/test/parallel/test-assert.js b/test/parallel/test-assert.js index f1c48bd8d4caab..ae3df139cd68b7 100644 --- a/test/parallel/test-assert.js +++ b/test/parallel/test-assert.js @@ -23,7 +23,6 @@ const { invalidArgTypeHelper } = require('../common'); const assert = require('assert'); -const { Assert } = require('assert'); const { inspect } = require('util'); const { test } = require('node:test'); const vm = require('vm'); @@ -1596,83 +1595,5 @@ test('assert/strict exists', () => { assert.strictEqual(require('assert/strict'), assert.strict); }); -test('Assert class basic instance', () => { - const assertInstance = new Assert(); - - assertInstance.ok(assert.AssertionError.prototype instanceof Error, - 'assert.AssertionError instanceof Error'); - assertInstance.ok(true); - assertInstance.equal(undefined, undefined); - assertInstance.notEqual(true, false); - assertInstance.throws( - () => assertInstance.deepEqual(/a/), - { code: 'ERR_MISSING_ARGS' } - ); - assertInstance.throws( - () => assertInstance.notDeepEqual('test'), - { code: 'ERR_MISSING_ARGS' } - ); - assertInstance.notStrictEqual(2, '2'); - assertInstance.throws(() => assertInstance.strictEqual(2, '2'), - assertInstance.AssertionError, 'strictEqual(2, \'2\')'); - assertInstance.throws( - () => { - assertInstance.partialDeepStrictEqual({ a: true }, { a: false }, 'custom message'); - }, - { - code: 'ERR_ASSERTION', - name: 'AssertionError', - message: 'custom message\n+ actual - expected\n\n {\n+ a: true\n- a: false\n }\n' - } - ); - assertInstance.throws( - () => assertInstance.match(/abc/, 'string'), - { - code: 'ERR_INVALID_ARG_TYPE', - message: 'The "regexp" argument must be an instance of RegExp. ' + - "Received type string ('string')" - } - ); - assertInstance.throws( - () => assertInstance.doesNotMatch(/abc/, 'string'), - { - code: 'ERR_INVALID_ARG_TYPE', - message: 'The "regexp" argument must be an instance of RegExp. ' + - "Received type string ('string')" - } - ); - - { - function thrower(errorConstructor) { - throw new errorConstructor({}); - } - - let threw = false; - try { - assertInstance.doesNotThrow(() => thrower(TypeError), assertInstance.AssertionError); - } catch (e) { - threw = true; - assertInstance.ok(e instanceof TypeError); - } - assertInstance.ok(threw, 'assertInstance.doesNotThrow with an explicit error is eating extra errors'); - } - { - let threw = false; - const rangeError = new RangeError('my range'); - - try { - assertInstance.doesNotThrow(() => { - throw new TypeError('wrong type'); - }, TypeError, rangeError); - } catch (e) { - threw = true; - assertInstance.ok(e.message.includes(rangeError.message)); - assertInstance.ok(e instanceof assertInstance.AssertionError); - assertInstance.ok(!e.stack.includes('doesNotThrow'), e); - } - assertInstance.ok(threw); - } -}); - /* eslint-enable no-restricted-syntax */ /* eslint-enable no-restricted-properties */ From 2311b6d7f8eec02dfac6507cb7919c3ed7918994 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Fri, 6 Jun 2025 19:46:11 -0300 Subject: [PATCH 17/28] lib: rebase Assert.fail method --- lib/assert.js | 324 ++------------------------------------------------ 1 file changed, 8 insertions(+), 316 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index b17f6cade2fc8b..d8163e2c99c820 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -436,43 +436,21 @@ class Assert { } /** - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @param {string} [operator] - * @param {Function} [stackStartFn] + * Throws an AssertionError with the given message. + * @param {any | Error} [message] */ - fail(actual, expected, message, operator, stackStartFn) { - const argsLen = arguments.length; + fail(message) { + if (isError(message)) throw message; let internalMessage = false; - if (actual == null && argsLen <= 1) { - internalMessage = true; + if (message === undefined) { message = 'Failed'; - } else if (argsLen === 1) { - message = actual; - actual = undefined; - } else { - if (warned === false) { - warned = true; - process.emitWarning( - 'assert.fail() with more than one argument is deprecated. ' + - 'Please use assert.strictEqual() instead or only pass a message.', - 'DeprecationWarning', - 'DEP0094', - ); - } - if (argsLen === 2) - operator = '!='; + internalMessage = true; } - if (message instanceof Error) throw message; - const errArgs = { - actual, - expected, - operator: operator === undefined ? 'fail' : operator, - stackStartFn: stackStartFn || this.fail, + operator: 'fail', + stackStartFn: this.fail, message, }; const err = new AssertionError(errArgs); @@ -837,292 +815,6 @@ function ok(...args) { ObjectAssign(assert, assertInstance); assert.ok = ok; -// All of the following functions must throw an AssertionError -// when a corresponding condition is not met, with a message that -// may be undefined if not provided. All assertion methods provide -// both the actual and expected values to the assertion error for -// display purposes. - -function innerFail(obj) { - if (obj.message instanceof Error) throw obj.message; - - throw new AssertionError(obj); -} - -/** - * Throws an AssertionError with the given message. - * @param {any | Error} [message] - */ -function fail(message) { - if (isError(message)) throw message; - - let internalMessage = false; - if (message === undefined) { - message = 'Failed'; - internalMessage = true; - } - - const errArgs = { - operator: 'fail', - stackStartFn: fail, - message, - }; - const err = new AssertionError(errArgs); - if (internalMessage) { - err.generatedMessage = true; - } - throw err; -} - -assert.fail = fail; - -// The AssertionError is defined in internal/error. -assert.AssertionError = AssertionError; - -/** - * Pure assertion tests whether a value is truthy, as determined - * by !!value. - * @param {...any} args - * @returns {void} - */ -function ok(...args) { - innerOk(ok, args.length, ...args); -} -assert.ok = ok; - -/** - * The equality assertion tests shallow, coercive equality with ==. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ -/* eslint-disable no-restricted-properties */ -assert.equal = function equal(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - // eslint-disable-next-line eqeqeq - if (actual != expected && (!NumberIsNaN(actual) || !NumberIsNaN(expected))) { - innerFail({ - actual, - expected, - message, - operator: '==', - stackStartFn: equal, - }); - } -}; - -/** - * The non-equality assertion tests for whether two objects are not - * equal with !=. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ -assert.notEqual = function notEqual(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - // eslint-disable-next-line eqeqeq - if (actual == expected || (NumberIsNaN(actual) && NumberIsNaN(expected))) { - innerFail({ - actual, - expected, - message, - operator: '!=', - stackStartFn: notEqual, - }); - } -}; - -/** - * The deep equivalence assertion tests a deep equality relation. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ -assert.deepEqual = function deepEqual(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - if (isDeepEqual === undefined) lazyLoadComparison(); - if (!isDeepEqual(actual, expected)) { - innerFail({ - actual, - expected, - message, - operator: 'deepEqual', - stackStartFn: deepEqual, - }); - } -}; - -/** - * The deep non-equivalence assertion tests for any deep inequality. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ -assert.notDeepEqual = function notDeepEqual(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - if (isDeepEqual === undefined) lazyLoadComparison(); - if (isDeepEqual(actual, expected)) { - innerFail({ - actual, - expected, - message, - operator: 'notDeepEqual', - stackStartFn: notDeepEqual, - }); - } -}; -/* eslint-enable */ - -/** - * The deep strict equivalence assertion tests a deep strict equality - * relation. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ -assert.deepStrictEqual = function deepStrictEqual(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - if (isDeepEqual === undefined) lazyLoadComparison(); - if (!isDeepStrictEqual(actual, expected)) { - innerFail({ - actual, - expected, - message, - operator: 'deepStrictEqual', - stackStartFn: deepStrictEqual, - }); - } -}; - -/** - * The deep strict non-equivalence assertion tests for any deep strict - * inequality. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ -assert.notDeepStrictEqual = notDeepStrictEqual; -function notDeepStrictEqual(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - if (isDeepEqual === undefined) lazyLoadComparison(); - if (isDeepStrictEqual(actual, expected)) { - innerFail({ - actual, - expected, - message, - operator: 'notDeepStrictEqual', - stackStartFn: notDeepStrictEqual, - }); - } -} - - -// assert.strictEqual = function strictEqual(actual, expected, message) { - -/** - * The strict non-equivalence assertion tests for any strict inequality. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ -assert.notStrictEqual = function notStrictEqual(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - if (ObjectIs(actual, expected)) { - innerFail({ - actual, - expected, - message, - operator: 'notStrictEqual', - stackStartFn: notStrictEqual, - }); - } -}; - -/** - * The strict equivalence assertion test between two objects - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ -assert.partialDeepStrictEqual = function partialDeepStrictEqual( - actual, - expected, - message, -) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - if (isDeepEqual === undefined) lazyLoadComparison(); - if (!isPartialStrictEqual(actual, expected)) { - innerFail({ - actual, - expected, - message, - operator: 'partialDeepStrictEqual', - stackStartFn: partialDeepStrictEqual, - }); - } -}; - -class Comparison { - constructor(obj, keys, actual) { - for (const key of keys) { - if (key in obj) { - if (actual !== undefined && - typeof actual[key] === 'string' && - isRegExp(obj[key]) && - RegExpPrototypeExec(obj[key], actual[key]) !== null) { - this[key] = actual[key]; - } else { - this[key] = obj[key]; - } - } - } - } -} - -function hasMatchingError(actual, expected) { - if (typeof expected !== 'function') { - if (isRegExp(expected)) { - const str = String(actual); - return RegExpPrototypeExec(expected, str) !== null; - } - throw new ERR_INVALID_ARG_TYPE( - 'expected', ['Function', 'RegExp'], expected, - ); - } - // Guard instanceof against arrow functions as they don't have a prototype. - if (expected.prototype !== undefined && actual instanceof expected) { - return true; - } - if (ObjectPrototypeIsPrototypeOf(Error, expected)) { - return false; - } - return ReflectApply(expected, {}, [actual]) === true; -} - /** * Expose a strict only variant of assert. * @param {...any} args From 9131e0519986396ece3d5265414e301fc4b814f4 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sat, 7 Jun 2025 17:02:09 -0300 Subject: [PATCH 18/28] lib: enhance Assert class to support full diffs in error messages --- lib/assert.js | 44 ++++++--- lib/internal/assert/assertion_error.js | 26 +++--- test/fixtures/errors/error_exit.snapshot | 5 +- .../errors/if-error-has-good-stack.snapshot | 3 +- .../output/assertion-color-tty.snapshot | 3 +- .../test-runner/output/dot_reporter.snapshot | 6 +- .../output/junit_reporter.snapshot | 6 +- .../test-runner/output/spec_reporter.snapshot | 6 +- .../output/spec_reporter_cli.snapshot | 6 +- test/parallel/test-assert-class.js | 91 +++++++++++++++++++ 10 files changed, 159 insertions(+), 37 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index d8163e2c99c820..6563693e49924e 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -96,10 +96,28 @@ class Comparison { } } +/** + * Assert options. + * @typedef {object} AssertOptions + * @property {'full'|'simple'} [diff='simple'] - If set to 'full', shows the full diff in assertion errors. + */ + +/** + * The Assert class provides assertion methods. + * @class + * @param {AssertOptions} [options] - Optional configuration for assertions. + */ class Assert { constructor(options = {}) { - this.options = options; this.AssertionError = AssertionError; + this.options = { ...options, diff: options.diff ?? 'simple' }; + } + + #buildAssertionErrorOptions(obj) { + if (this.options.diff === 'full') { + return { ...obj, diff: this.options.diff }; + } + return obj; } // All of the following functions must throw an AssertionError @@ -111,7 +129,7 @@ class Assert { #innerFail(obj) { if (obj.message instanceof Error) throw obj.message; - throw new AssertionError(obj); + throw new AssertionError(this.#buildAssertionErrorOptions(obj)); } #internalMatch(string, regexp, message, fn) { @@ -137,13 +155,13 @@ class Assert { 'The input did not match the regular expression ' : 'The input was expected to not match the regular expression ') + `${inspect(regexp)}. Input:\n\n${inspect(string)}\n`); - const err = new AssertionError({ + const err = new AssertionError(this.#buildAssertionErrorOptions({ actual: string, expected: regexp, message, operator: fn.name, stackStartFn: fn, - }); + })); err.generatedMessage = generatedMessage; throw err; } @@ -156,12 +174,12 @@ class Assert { const a = new Comparison(actual, keys); const b = new Comparison(expected, keys, actual); - const err = new AssertionError({ + const err = new AssertionError(this.#buildAssertionErrorOptions({ actual: a, expected: b, operator: 'deepStrictEqual', stackStartFn: fn, - }); + })); err.actual = actual; err.expected = expected; err.operator = fn.name; @@ -196,13 +214,13 @@ class Assert { throwError = true; // Handle primitives properly. } else if (typeof actual !== 'object' || actual === null) { - const err = new AssertionError({ + const err = new AssertionError(this.#buildAssertionErrorOptions({ actual, expected, message, operator: 'deepStrictEqual', stackStartFn: fn, - }); + })); err.operator = fn.name; throw err; } else { @@ -271,13 +289,13 @@ class Assert { } if (throwError) { - const err = new AssertionError({ + const err = new AssertionError(this.#buildAssertionErrorOptions({ actual, expected, message, operator: fn.name, stackStartFn: fn, - }); + })); err.generatedMessage = generatedMessage; throw err; } @@ -453,7 +471,7 @@ class Assert { stackStartFn: this.fail, message, }; - const err = new AssertionError(errArgs); + const err = new AssertionError(this.#buildAssertionErrorOptions(errArgs)); if (internalMessage) { err.generatedMessage = true; } @@ -752,13 +770,13 @@ class Assert { message += inspect(err); } - const newErr = new AssertionError({ + const newErr = new AssertionError(this.#buildAssertionErrorOptions({ actual: err, expected: null, operator: 'ifError', message, stackStartFn: this.ifError, - }); + })); // Make sure we actually have a stack trace! const origStack = err.stack; diff --git a/lib/internal/assert/assertion_error.js b/lib/internal/assert/assertion_error.js index d654ca5038bbab..c13d29c873266b 100644 --- a/lib/internal/assert/assertion_error.js +++ b/lib/internal/assert/assertion_error.js @@ -178,7 +178,7 @@ function isSimpleDiff(actual, inspectedActual, expected, inspectedExpected) { return typeof actual !== 'object' || actual === null || typeof expected !== 'object' || expected === null; } -function createErrDiff(actual, expected, operator, customMessage) { +function createErrDiff(actual, expected, operator, customMessage, diffType = 'simple') { operator = checkOperator(actual, expected, operator); let skipped = false; @@ -202,7 +202,7 @@ function createErrDiff(actual, expected, operator, customMessage) { } else if (inspectedActual === inspectedExpected) { // Handles the case where the objects are structurally the same but different references operator = 'notIdentical'; - if (inspectedSplitActual.length > 50) { + if (inspectedSplitActual.length > 50 && diffType !== 'full') { message = `${ArrayPrototypeJoin(ArrayPrototypeSlice(inspectedSplitActual, 0, 50), '\n')}\n...}`; skipped = true; } else { @@ -231,7 +231,8 @@ function createErrDiff(actual, expected, operator, customMessage) { return `${headerMessage}${skippedMessage}\n${message}\n`; } -function addEllipsis(string) { +function addEllipsis(string, diff) { + if (diff === 'full') return string; const lines = StringPrototypeSplit(string, '\n', 11); if (lines.length > 10) { lines.length = 10; @@ -252,6 +253,7 @@ class AssertionError extends Error { details, // Compatibility with older versions. stackStartFunction, + diff = 'simple', } = options; let { actual, @@ -263,7 +265,7 @@ class AssertionError extends Error { if (message != null) { if (kMethodsWithCustomMessageDiff.includes(operator)) { - super(createErrDiff(actual, expected, operator, message)); + super(createErrDiff(actual, expected, operator, message, diff)); } else { super(String(message)); } @@ -283,7 +285,7 @@ class AssertionError extends Error { } if (kMethodsWithCustomMessageDiff.includes(operator)) { - super(createErrDiff(actual, expected, operator, message)); + super(createErrDiff(actual, expected, operator, message, diff)); } else if (operator === 'notDeepStrictEqual' || operator === 'notStrictEqual') { // In case the objects are equal but the operator requires unequal, show @@ -300,8 +302,7 @@ class AssertionError extends Error { } // Only remove lines in case it makes sense to collapse those. - // TODO: Accept env to always show the full error. - if (res.length > 50) { + if (res.length > 50 && diff !== 'full') { res[46] = `${colors.blue}...${colors.white}`; while (res.length > 47) { ArrayPrototypePop(res); @@ -320,15 +321,15 @@ class AssertionError extends Error { const knownOperator = kReadableOperator[operator]; if (operator === 'notDeepEqual' && res === other) { res = `${knownOperator}\n\n${res}`; - if (res.length > 1024) { + if (res.length > 1024 && diff !== 'full') { res = `${StringPrototypeSlice(res, 0, 1021)}...`; } super(res); } else { - if (res.length > kMaxLongStringLength) { + if (res.length > kMaxLongStringLength && diff !== 'full') { res = `${StringPrototypeSlice(res, 0, 509)}...`; } - if (other.length > kMaxLongStringLength) { + if (other.length > kMaxLongStringLength && diff !== 'full') { other = `${StringPrototypeSlice(other, 0, 509)}...`; } if (operator === 'deepEqual') { @@ -378,6 +379,7 @@ class AssertionError extends Error { this.stack; // eslint-disable-line no-unused-expressions // Reset the name. this.name = 'AssertionError'; + this.diff = diff; } toString() { @@ -390,10 +392,10 @@ class AssertionError extends Error { const tmpExpected = this.expected; if (typeof this.actual === 'string') { - this.actual = addEllipsis(this.actual); + this.actual = addEllipsis(this.actual, this.diff); } if (typeof this.expected === 'string') { - this.expected = addEllipsis(this.expected); + this.expected = addEllipsis(this.expected, this.diff); } // This limits the `actual` and `expected` property default inspection to diff --git a/test/fixtures/errors/error_exit.snapshot b/test/fixtures/errors/error_exit.snapshot index de4ba32c4560a1..9283959072d217 100644 --- a/test/fixtures/errors/error_exit.snapshot +++ b/test/fixtures/errors/error_exit.snapshot @@ -1,6 +1,6 @@ Exiting with code=1 node:assert:* - throw new AssertionError(obj); + throw new AssertionError(this.#buildAssertionErrorOptions(obj)); ^ AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: @@ -12,7 +12,8 @@ AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: code: 'ERR_ASSERTION', actual: 1, expected: 2, - operator: 'strictEqual' + operator: 'strictEqual', + diff: 'simple' } Node.js * diff --git a/test/fixtures/errors/if-error-has-good-stack.snapshot b/test/fixtures/errors/if-error-has-good-stack.snapshot index 30477c483de7f8..9f0e83d24d3efc 100644 --- a/test/fixtures/errors/if-error-has-good-stack.snapshot +++ b/test/fixtures/errors/if-error-has-good-stack.snapshot @@ -19,7 +19,8 @@ AssertionError [ERR_ASSERTION]: ifError got unwanted exception: test error at a (*if-error-has-good-stack.js:*:*) at Object. (*if-error-has-good-stack.js:*:*), expected: null, - operator: 'ifError' + operator: 'ifError', + diff: 'simple' } Node.js * diff --git a/test/fixtures/test-runner/output/assertion-color-tty.snapshot b/test/fixtures/test-runner/output/assertion-color-tty.snapshot index a74016febc5df4..4409d6f5e3e939 100644 --- a/test/fixtures/test-runner/output/assertion-color-tty.snapshot +++ b/test/fixtures/test-runner/output/assertion-color-tty.snapshot @@ -21,5 +21,6 @@ code: [32m'ERR_ASSERTION'[39m, actual: [32m'!Hello World'[39m, expected: [32m'Hello World!'[39m, - operator: [32m'strictEqual'[39m + operator: [32m'strictEqual'[39m, + diff: [32m'simple'[39m } diff --git a/test/fixtures/test-runner/output/dot_reporter.snapshot b/test/fixtures/test-runner/output/dot_reporter.snapshot index 1e06b8eea8b607..55689fa40ce51e 100644 --- a/test/fixtures/test-runner/output/dot_reporter.snapshot +++ b/test/fixtures/test-runner/output/dot_reporter.snapshot @@ -69,7 +69,8 @@ Failed tests: code: 'ERR_ASSERTION', actual: true, expected: false, - operator: 'strictEqual' + operator: 'strictEqual', + diff: 'simple' } ✖ reject fail (*ms) Error: rejected from reject fail @@ -216,7 +217,8 @@ Failed tests: code: 'ERR_ASSERTION', actual: [Object], expected: [Object], - operator: 'deepEqual' + operator: 'deepEqual', + diff: 'simple' } ✖ invalid subtest fail (*ms) 'test could not be started because its parent finished' diff --git a/test/fixtures/test-runner/output/junit_reporter.snapshot b/test/fixtures/test-runner/output/junit_reporter.snapshot index b0fb1af4dc2224..2b3eaec238c422 100644 --- a/test/fixtures/test-runner/output/junit_reporter.snapshot +++ b/test/fixtures/test-runner/output/junit_reporter.snapshot @@ -127,7 +127,8 @@ true !== false code: 'ERR_ASSERTION', actual: true, expected: false, - operator: 'strictEqual' + operator: 'strictEqual', + diff: 'simple' } } @@ -491,7 +492,8 @@ should loosely deep-equal code: 'ERR_ASSERTION', actual: [Object], expected: [Object], - operator: 'deepEqual' + operator: 'deepEqual', + diff: 'simple' } } diff --git a/test/fixtures/test-runner/output/spec_reporter.snapshot b/test/fixtures/test-runner/output/spec_reporter.snapshot index 35530531cf8cdf..e3145efa25e0e0 100644 --- a/test/fixtures/test-runner/output/spec_reporter.snapshot +++ b/test/fixtures/test-runner/output/spec_reporter.snapshot @@ -174,7 +174,8 @@ code: 'ERR_ASSERTION', actual: true, expected: false, - operator: 'strictEqual' + operator: 'strictEqual', + diff: 'simple' } * @@ -357,7 +358,8 @@ code: 'ERR_ASSERTION', actual: [Object], expected: [Object], - operator: 'deepEqual' + operator: 'deepEqual', + diff: 'simple' } * diff --git a/test/fixtures/test-runner/output/spec_reporter_cli.snapshot b/test/fixtures/test-runner/output/spec_reporter_cli.snapshot index 8f57e8714d4f6f..c90c6df670639a 100644 --- a/test/fixtures/test-runner/output/spec_reporter_cli.snapshot +++ b/test/fixtures/test-runner/output/spec_reporter_cli.snapshot @@ -177,7 +177,8 @@ code: 'ERR_ASSERTION', actual: true, expected: false, - operator: 'strictEqual' + operator: 'strictEqual', + diff: 'simple' } * @@ -360,7 +361,8 @@ code: 'ERR_ASSERTION', actual: { foo: 1, bar: 1, boo: [ 1 ], baz: { date: 1970-01-01T00:00:00.000Z, null: null, number: 1, string: 'Hello', undefined: undefined } }, expected: { boo: [ 1 ], baz: { date: 1970-01-01T00:00:00.000Z, null: null, number: 1, string: 'Hello', undefined: undefined }, circular: { bar: 2, c: [Circular *1] } }, - operator: 'deepEqual' + operator: 'deepEqual', + diff: 'simple' } * diff --git a/test/parallel/test-assert-class.js b/test/parallel/test-assert-class.js index 8a3550aa4e37ff..0f222210c78d59 100644 --- a/test/parallel/test-assert-class.js +++ b/test/parallel/test-assert-class.js @@ -3,6 +3,7 @@ require('../common'); const assert = require('assert'); const { Assert } = require('assert'); +const { inspect } = require('util'); const { test } = require('node:test'); // Disable colored output to prevent color codes from breaking assertion @@ -104,3 +105,93 @@ test('Assert class basic instance', () => { } /* eslint-enable no-restricted-syntax */ }); + +test('Assert class with full diff', () => { + const assertInstance = new Assert({ diff: 'full' }); + + const longStringOfAs = 'A'.repeat(1025); + const longStringOFBs = 'B'.repeat(1025); + + assertInstance.throws(() => { + assertInstance.strictEqual(longStringOfAs, longStringOFBs); + }, (err) => { + assertInstance.strictEqual(err.code, 'ERR_ASSERTION'); + assertInstance.strictEqual(err.message, + `Expected values to be strictly equal:\n+ actual - expected\n\n` + + `+ '${longStringOfAs}'\n- '${longStringOFBs}'\n`); + assertInstance.ok(inspect(err).includes(`actual: '${longStringOfAs}'`)); + assertInstance.ok(inspect(err).includes(`expected: '${longStringOFBs}'`)); + return true; + }); + + assertInstance.throws(() => { + assertInstance.notStrictEqual(longStringOfAs, longStringOfAs); + }, (err) => { + assertInstance.strictEqual(err.code, 'ERR_ASSERTION'); + assertInstance.strictEqual(err.message, + `Expected "actual" to be strictly unequal to:\n\n` + + `'${longStringOfAs}'`); + assertInstance.ok(inspect(err).includes(`actual: '${longStringOfAs}'`)); + assertInstance.ok(inspect(err).includes(`expected: '${longStringOfAs}'`)); + return true; + }); + + assertInstance.throws(() => { + assertInstance.deepEqual(longStringOfAs, longStringOFBs); + }, (err) => { + assertInstance.strictEqual(err.code, 'ERR_ASSERTION'); + assertInstance.strictEqual( + err.message, + `Expected values to be loosely deep-equal:\n\n` + + `'${longStringOfAs}'\n\nshould loosely deep-equal\n\n'${longStringOFBs}'` + ); + assertInstance.ok(inspect(err).includes(`actual: '${longStringOfAs}'`)); + assertInstance.ok(inspect(err).includes(`expected: '${longStringOFBs}'`)); + return true; + }); +}); + +test('Assert class with simple diff', () => { + const assertInstance = new Assert({ diff: 'simple' }); + + const longStringOfAs = 'A'.repeat(1025); + const longStringOFBs = 'B'.repeat(1025); + + assertInstance.throws(() => { + assertInstance.strictEqual(longStringOfAs, longStringOFBs); + }, (err) => { + assertInstance.strictEqual(err.code, 'ERR_ASSERTION'); + assertInstance.strictEqual(err.message, + `Expected values to be strictly equal:\n+ actual - expected\n\n` + + `+ '${longStringOfAs}'\n- '${longStringOFBs}'\n`); + assertInstance.ok(inspect(err).includes(`actual: '${longStringOfAs.slice(0, 513)}...`)); + assertInstance.ok(inspect(err).includes(`expected: '${longStringOFBs.slice(0, 513)}...`)); + return true; + }); + + assertInstance.throws(() => { + assertInstance.notStrictEqual(longStringOfAs, longStringOfAs); + }, (err) => { + assertInstance.strictEqual(err.code, 'ERR_ASSERTION'); + assertInstance.strictEqual(err.message, + `Expected "actual" to be strictly unequal to:\n\n` + + `'${longStringOfAs}'`); + assertInstance.ok(inspect(err).includes(`actual: '${longStringOfAs.slice(0, 513)}...`)); + assertInstance.ok(inspect(err).includes(`expected: '${longStringOfAs.slice(0, 513)}...`)); + return true; + }); + + assertInstance.throws(() => { + assertInstance.deepEqual(longStringOfAs, longStringOFBs); + }, (err) => { + assertInstance.strictEqual(err.code, 'ERR_ASSERTION'); + assertInstance.strictEqual( + err.message, + `Expected values to be loosely deep-equal:\n\n` + + `'${longStringOfAs.slice(0, 508)}...\n\nshould loosely deep-equal\n\n'${longStringOFBs.slice(0, 508)}...` + ); + assertInstance.ok(inspect(err).includes(`actual: '${longStringOfAs.slice(0, 513)}...`)); + assertInstance.ok(inspect(err).includes(`expected: '${longStringOFBs.slice(0, 513)}...`)); + return true; + }); +}); From 1c22af463c148d318dcc77b1881872aca5a88bb4 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sun, 8 Jun 2025 12:34:19 -0300 Subject: [PATCH 19/28] test: enhance assert.throws usage with nested assertion error handling --- test/message/assert_throws_stack.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/message/assert_throws_stack.js b/test/message/assert_throws_stack.js index 36bc5734cae37f..01a84cf7964f49 100644 --- a/test/message/assert_throws_stack.js +++ b/test/message/assert_throws_stack.js @@ -3,4 +3,9 @@ require('../common'); const assert = require('assert').strict; -assert.throws(() => { throw new Error('foo'); }, { bar: true }); +assert.throws( + () => { + assert.throws(() => { throw new Error('foo'); }, { bar: true }); + }, + assert.AssertionError, +); From 00a293e038c0f747bf8ffafac6af53c6b48663e3 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sun, 8 Jun 2025 16:15:39 -0300 Subject: [PATCH 20/28] test: update assert throws stack out file --- test/message/assert_throws_stack.js | 7 +------ test/message/assert_throws_stack.out | 7 ++++--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/test/message/assert_throws_stack.js b/test/message/assert_throws_stack.js index 01a84cf7964f49..36bc5734cae37f 100644 --- a/test/message/assert_throws_stack.js +++ b/test/message/assert_throws_stack.js @@ -3,9 +3,4 @@ require('../common'); const assert = require('assert').strict; -assert.throws( - () => { - assert.throws(() => { throw new Error('foo'); }, { bar: true }); - }, - assert.AssertionError, -); +assert.throws(() => { throw new Error('foo'); }, { bar: true }); diff --git a/test/message/assert_throws_stack.out b/test/message/assert_throws_stack.out index 897ddf36a04eb0..e4cf409b07f44c 100644 --- a/test/message/assert_throws_stack.out +++ b/test/message/assert_throws_stack.out @@ -22,8 +22,8 @@ AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: code: 'ERR_ASSERTION', actual: Error: foo at assert.throws.bar (*assert_throws_stack.js:*) - at getActual (node:assert:*) - at strict.throws (node:assert:*) + at #getActual (node:assert:*) + at Assert.throws (node:assert:*) at Object. (*assert_throws_stack.js:*:*) at * at * @@ -32,7 +32,8 @@ AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: at * at *, expected: { bar: true }, - operator: 'throws' + operator: 'throws', + diff: 'simple' } Node.js * From 0d3a968b9dda486c177664e5757050fe308a7cd4 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sun, 8 Jun 2025 19:54:51 -0300 Subject: [PATCH 21/28] test: align indentation in assert_throws_stack.out --- test/message/assert_throws_stack.out | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/message/assert_throws_stack.out b/test/message/assert_throws_stack.out index e4cf409b07f44c..61fb186edbd32c 100644 --- a/test/message/assert_throws_stack.out +++ b/test/message/assert_throws_stack.out @@ -1,6 +1,6 @@ node:assert:* - throw err; - ^ + throw err; + ^ AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: + actual - expected From 231f0c98212ae1068f68474acc6bb0380f59c584 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Mon, 9 Jun 2025 20:03:36 -0300 Subject: [PATCH 22/28] lib: convert Assert class to a constructor function type --- lib/assert.js | 1336 ++++++++--------- test/fixtures/errors/error_exit.snapshot | 6 +- .../errors/if-error-has-good-stack.snapshot | 8 +- .../test-runner/output/describe_it.snapshot | 3 - .../test-runner/output/dot_reporter.snapshot | 3 - .../output/junit_reporter.snapshot | 3 - .../test-runner/output/output.snapshot | 3 - .../test-runner/output/output_cli.snapshot | 3 - .../output/source_mapped_locations.snapshot | 3 - .../test-runner/output/spec_reporter.snapshot | 3 - .../output/spec_reporter_cli.snapshot | 3 - test/message/assert_throws_stack.out | 7 +- 12 files changed, 677 insertions(+), 704 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index 6563693e49924e..b0b4c5bf3c6cc3 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -75,27 +75,11 @@ function lazyLoadComparison() { // The assert module provides functions that throw // AssertionError's when particular conditions are not met. The // assert module must conform to the following interface. + const assert = module.exports = ok; const NO_EXCEPTION_SENTINEL = {}; -class Comparison { - constructor(obj, keys, actual) { - for (const key of keys) { - if (key in obj) { - if (actual !== undefined && - typeof actual[key] === 'string' && - isRegExp(obj[key]) && - RegExpPrototypeExec(obj[key], actual[key]) !== null) { - this[key] = actual[key]; - } else { - this[key] = obj[key]; - } - } - } - } -} - /** * Assert options. * @typedef {object} AssertOptions @@ -103,735 +87,739 @@ class Comparison { */ /** - * The Assert class provides assertion methods. - * @class + * @class Assert * @param {AssertOptions} [options] - Optional configuration for assertions. + * @throws {ERR_INVALID_ARG_TYPE} If not called with `new`. */ -class Assert { - constructor(options = {}) { - this.AssertionError = AssertionError; - this.options = { ...options, diff: options.diff ?? 'simple' }; +function Assert(options = {}) { + if (!new.target) { + throw new ERR_INVALID_ARG_TYPE('Assert', 'constructor', Assert); } + this.AssertionError = AssertionError; + this.options = ObjectAssign({ diff: 'simple' }, options); +} - #buildAssertionErrorOptions(obj) { - if (this.options.diff === 'full') { - return { ...obj, diff: this.options.diff }; - } - return obj; +function _buildAssertionErrorOptions(self, obj) { + if (self?.options?.diff === 'full') { + return { ...obj, diff: self.options.diff }; } + return obj; +} - // All of the following functions must throw an AssertionError - // when a corresponding condition is not met, with a message that - // may be undefined if not provided. All assertion methods provide - // both the actual and expected values to the assertion error for - // display purposes. +// All of the following functions must throw an AssertionError +// when a corresponding condition is not met, with a message that +// may be undefined if not provided. All assertion methods provide +// both the actual and expected values to the assertion error for +// display purposes. - #innerFail(obj) { - if (obj.message instanceof Error) throw obj.message; +function innerFail(obj) { + if (obj.message instanceof Error) throw obj.message; - throw new AssertionError(this.#buildAssertionErrorOptions(obj)); + throw new AssertionError(_buildAssertionErrorOptions(this, obj)); +} + +/** + * Throws an AssertionError with the given message. + * @param {any | Error} [message] + */ +Assert.prototype.fail = function fail(message) { + if (isError(message)) throw message; + + let internalMessage = false; + if (message === undefined) { + message = 'Failed'; + internalMessage = true; } - #internalMatch(string, regexp, message, fn) { - if (!isRegExp(regexp)) { - throw new ERR_INVALID_ARG_TYPE( - 'regexp', 'RegExp', regexp, - ); - } - const match = fn === Assert.prototype.match; - if (typeof string !== 'string' || - RegExpPrototypeExec(regexp, string) !== null !== match) { - if (message instanceof Error) { - throw message; - } + const errArgs = { + operator: 'fail', + stackStartFn: fail, + message, + }; + const err = new AssertionError(_buildAssertionErrorOptions(this, errArgs)); + if (internalMessage) { + err.generatedMessage = true; + } + throw err; +}; - const generatedMessage = !message; - - // 'The input was expected to not match the regular expression ' + - message ||= (typeof string !== 'string' ? - 'The "string" argument must be of type string. Received type ' + - `${typeof string} (${inspect(string)})` : - (match ? - 'The input did not match the regular expression ' : - 'The input was expected to not match the regular expression ') + - `${inspect(regexp)}. Input:\n\n${inspect(string)}\n`); - const err = new AssertionError(this.#buildAssertionErrorOptions({ - actual: string, - expected: regexp, - message, - operator: fn.name, - stackStartFn: fn, - })); - err.generatedMessage = generatedMessage; - throw err; - } +// The AssertionError is defined in internal/error. +assert.AssertionError = AssertionError; + +/** + * Pure assertion tests whether a value is truthy, as determined + * by !!value. + * @param {...any} args + * @returns {void} + */ +function ok(...args) { + innerOk(ok, args.length, ...args); +} +Assert.prototype.ok = ok; +assert.ok = ok; + +/** + * The equality assertion tests shallow, coercive equality with ==. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ +Assert.prototype.equal = function equal(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); } + // eslint-disable-next-line eqeqeq + if (actual != expected && (!NumberIsNaN(actual) || !NumberIsNaN(expected))) { + innerFail.call(this, { + actual, + expected, + message, + operator: '==', + stackStartFn: equal, + }); + } +}; - #compareExceptionKey(actual, expected, key, message, keys, fn) { - if (!(key in actual) || !isDeepStrictEqual(actual[key], expected[key])) { - if (!message) { - // Create placeholder objects to create a nice output. - const a = new Comparison(actual, keys); - const b = new Comparison(expected, keys, actual); - - const err = new AssertionError(this.#buildAssertionErrorOptions({ - actual: a, - expected: b, - operator: 'deepStrictEqual', - stackStartFn: fn, - })); - err.actual = actual; - err.expected = expected; - err.operator = fn.name; - throw err; +/** + * The non-equality assertion tests for whether two objects are not + * equal with !=. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ +Assert.prototype.notEqual = function notEqual(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + // eslint-disable-next-line eqeqeq + if (actual == expected || (NumberIsNaN(actual) && NumberIsNaN(expected))) { + innerFail.call(this, { + actual, + expected, + message, + operator: '!=', + stackStartFn: notEqual, + }); + } +}; + +/** + * The deep equivalence assertion tests a deep equality relation. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ +Assert.prototype.deepEqual = function deepEqual(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + if (isDeepEqual === undefined) lazyLoadComparison(); + if (!isDeepEqual(actual, expected)) { + innerFail.call(this, { + actual, + expected, + message, + operator: 'deepEqual', + stackStartFn: deepEqual, + }); + } +}; + +/** + * The deep non-equivalence assertion tests for any deep inequality. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ +Assert.prototype.notDeepEqual = function notDeepEqual(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + if (isDeepEqual === undefined) lazyLoadComparison(); + if (isDeepEqual(actual, expected)) { + innerFail.call(this, { + actual, + expected, + message, + operator: 'notDeepEqual', + stackStartFn: notDeepEqual, + }); + } +}; + +/** + * The deep strict equivalence assertion tests a deep strict equality + * relation. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ +Assert.prototype.deepStrictEqual = function deepStrictEqual(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + if (isDeepEqual === undefined) lazyLoadComparison(); + if (!isDeepStrictEqual(actual, expected)) { + innerFail.call(this, { + actual, + expected, + message, + operator: 'deepStrictEqual', + stackStartFn: deepStrictEqual, + }); + } +}; + +/** + * The deep strict non-equivalence assertion tests for any deep strict + * inequality. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ +Assert.prototype.notDeepStrictEqual = notDeepStrictEqual; +function notDeepStrictEqual(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + if (isDeepEqual === undefined) lazyLoadComparison(); + if (isDeepStrictEqual(actual, expected)) { + innerFail.call(this, { + actual, + expected, + message, + operator: 'notDeepStrictEqual', + stackStartFn: notDeepStrictEqual, + }); + } +} + +/** + * The strict equivalence assertion tests a strict equality relation. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ +Assert.prototype.strictEqual = function strictEqual(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + if (!ObjectIs(actual, expected)) { + innerFail.call(this, { + actual, + expected, + message, + operator: 'strictEqual', + stackStartFn: strictEqual, + }); + } +}; + +/** + * The strict non-equivalence assertion tests for any strict inequality. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ +Assert.prototype.notStrictEqual = function notStrictEqual(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + if (ObjectIs(actual, expected)) { + innerFail.call(this, { + actual, + expected, + message, + operator: 'notStrictEqual', + stackStartFn: notStrictEqual, + }); + } +}; + +/** + * The strict equivalence assertion test between two objects + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ +Assert.prototype.partialDeepStrictEqual = function partialDeepStrictEqual( + actual, + expected, + message, +) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + if (isDeepEqual === undefined) lazyLoadComparison(); + if (!isPartialStrictEqual(actual, expected)) { + innerFail.call(this, { + actual, + expected, + message, + operator: 'partialDeepStrictEqual', + stackStartFn: partialDeepStrictEqual, + }); + } +}; + +class Comparison { + constructor(obj, keys, actual) { + for (const key of keys) { + if (key in obj) { + if (actual !== undefined && + typeof actual[key] === 'string' && + isRegExp(obj[key]) && + RegExpPrototypeExec(obj[key], actual[key]) !== null) { + this[key] = actual[key]; + } else { + this[key] = obj[key]; + } } - this.#innerFail({ - actual, - expected, - message, - operator: fn.name, - stackStartFn: fn, - }); } } +} - #expectedException(actual, expected, message, fn) { - let generatedMessage = false; - let throwError = false; +function compareExceptionKey(actual, expected, key, message, keys, fn) { + if (!(key in actual) || !isDeepStrictEqual(actual[key], expected[key])) { + if (!message) { + // Create placeholder objects to create a nice output. + const a = new Comparison(actual, keys); + const b = new Comparison(expected, keys, actual); - if (typeof expected !== 'function') { - // Handle regular expressions. - if (isRegExp(expected)) { - const str = String(actual); - if (RegExpPrototypeExec(expected, str) !== null) - return; + const err = new AssertionError(_buildAssertionErrorOptions(this, { + actual: a, + expected: b, + operator: 'deepStrictEqual', + stackStartFn: fn, + })); + err.actual = actual; + err.expected = expected; + err.operator = fn.name; + throw err; + } + innerFail.call(this, { + actual, + expected, + message, + operator: fn.name, + stackStartFn: fn, + }); + } +} - if (!message) { - generatedMessage = true; - message = 'The input did not match the regular expression ' + - `${inspect(expected)}. Input:\n\n${inspect(str)}\n`; - } - throwError = true; - // Handle primitives properly. - } else if (typeof actual !== 'object' || actual === null) { - const err = new AssertionError(this.#buildAssertionErrorOptions({ - actual, - expected, - message, - operator: 'deepStrictEqual', - stackStartFn: fn, - })); - err.operator = fn.name; - throw err; - } else { - // Handle validation objects. - const keys = ObjectKeys(expected); - // Special handle errors to make sure the name and the message are - // compared as well. - if (expected instanceof Error) { - ArrayPrototypePush(keys, 'name', 'message'); - } else if (keys.length === 0) { - throw new ERR_INVALID_ARG_VALUE('error', - expected, 'may not be an empty object'); - } - if (isDeepEqual === undefined) lazyLoadComparison(); - for (const key of keys) { - if (typeof actual[key] === 'string' && - isRegExp(expected[key]) && - RegExpPrototypeExec(expected[key], actual[key]) !== null) { - continue; - } - this.#compareExceptionKey(actual, expected, key, message, keys, fn); - } +function expectedException(actual, expected, message, fn) { + let generatedMessage = false; + let throwError = false; + + if (typeof expected !== 'function') { + // Handle regular expressions. + if (isRegExp(expected)) { + const str = String(actual); + if (RegExpPrototypeExec(expected, str) !== null) return; - } - // Guard instanceof against arrow functions as they don't have a prototype. - // Check for matching Error classes. - } else if (expected.prototype !== undefined && actual instanceof expected) { - return; - } else if (ObjectPrototypeIsPrototypeOf(Error, expected)) { + if (!message) { generatedMessage = true; - message = 'The error is expected to be an instance of ' + - `"${expected.name}". Received `; - if (isError(actual)) { - const name = (actual.constructor?.name) || - actual.name; - if (expected.name === name) { - message += 'an error with identical name but a different prototype.'; - } else { - message += `"${name}"`; - } - if (actual.message) { - message += `\n\nError message:\n\n${actual.message}`; - } - } else { - message += `"${inspect(actual, { depth: -1 })}"`; - } + message = 'The input did not match the regular expression ' + + `${inspect(expected)}. Input:\n\n${inspect(str)}\n`; } throwError = true; - } else { - // Check validation functions return value. - const res = ReflectApply(expected, {}, [actual]); - if (res !== true) { - if (!message) { - generatedMessage = true; - const name = expected.name ? `"${expected.name}" ` : ''; - message = `The ${name}validation function is expected to return` + - ` "true". Received ${inspect(res)}`; - - if (isError(actual)) { - message += `\n\nCaught error:\n\n${actual}`; - } - } - throwError = true; - } - } - - if (throwError) { - const err = new AssertionError(this.#buildAssertionErrorOptions({ + // Handle primitives properly. + } else if (typeof actual !== 'object' || actual === null) { + const err = new AssertionError(_buildAssertionErrorOptions(this, { actual, expected, message, - operator: fn.name, + operator: 'deepStrictEqual', stackStartFn: fn, })); - err.generatedMessage = generatedMessage; + err.operator = fn.name; throw err; - } - } - - #expectsError(stackStartFn, actual, error, message) { - if (typeof error === 'string') { - if (arguments.length === 4) { - throw new ERR_INVALID_ARG_TYPE('error', - ['Object', 'Error', 'Function', 'RegExp'], - error); + } else { + // Handle validation objects. + const keys = ObjectKeys(expected); + // Special handle errors to make sure the name and the message are + // compared as well. + if (expected instanceof Error) { + ArrayPrototypePush(keys, 'name', 'message'); + } else if (keys.length === 0) { + throw new ERR_INVALID_ARG_VALUE('error', + expected, 'may not be an empty object'); } - if (typeof actual === 'object' && actual !== null) { - if (actual.message === error) { - throw new ERR_AMBIGUOUS_ARGUMENT( - 'error/message', - `The error message "${actual.message}" is identical to the message.`, - ); + if (isDeepEqual === undefined) lazyLoadComparison(); + for (const key of keys) { + if (typeof actual[key] === 'string' && + isRegExp(expected[key]) && + RegExpPrototypeExec(expected[key], actual[key]) !== null) { + continue; } - } else if (actual === error) { - throw new ERR_AMBIGUOUS_ARGUMENT( - 'error/message', - `The error "${actual}" is identical to the message.`, - ); + compareExceptionKey(actual, expected, key, message, keys, fn); } - message = error; - error = undefined; - } else if (error != null && - typeof error !== 'object' && - typeof error !== 'function') { - throw new ERR_INVALID_ARG_TYPE('error', - ['Object', 'Error', 'Function', 'RegExp'], - error); - } - - if (actual === NO_EXCEPTION_SENTINEL) { - let details = ''; - if (error?.name) { - details += ` (${error.name})`; - } - details += message ? `: ${message}` : '.'; - const fnType = stackStartFn === Assert.prototype.rejects ? 'rejection' : 'exception'; - this.#innerFail({ - actual: undefined, - expected: error, - operator: stackStartFn.name, - message: `Missing expected ${fnType}${details}`, - stackStartFn, - }); - } - - if (!error) - return; - - this.#expectedException(actual, error, message, stackStartFn); - } - - #expectsNoError(stackStartFn, actual, error, message) { - if (actual === NO_EXCEPTION_SENTINEL) return; - - if (typeof error === 'string') { - message = error; - error = undefined; - } - - if (!error || this.#hasMatchingError(actual, error)) { - const details = message ? `: ${message}` : '.'; - const fnType = stackStartFn === Assert.prototype.doesNotReject ? - 'rejection' : 'exception'; - this.#innerFail({ - actual, - expected: error, - operator: stackStartFn.name, - message: `Got unwanted ${fnType}${details}\n` + - `Actual message: "${actual?.message}"`, - stackStartFn, - }); - } - throw actual; - } - - #hasMatchingError(actual, expected) { - if (typeof expected !== 'function') { - if (isRegExp(expected)) { - const str = String(actual); - return RegExpPrototypeExec(expected, str) !== null; - } - throw new ERR_INVALID_ARG_TYPE( - 'expected', ['Function', 'RegExp'], expected, - ); - } - // Guard instanceof against arrow functions as they don't have a prototype. - if (expected.prototype !== undefined && actual instanceof expected) { - return true; } - if (ObjectPrototypeIsPrototypeOf(Error, expected)) { - return false; - } - return ReflectApply(expected, {}, [actual]) === true; - } - - async #waitForActual(promiseFn) { - let resultPromise; - if (typeof promiseFn === 'function') { - // Return a rejected promise if `promiseFn` throws synchronously. - resultPromise = promiseFn(); - // Fail in case no promise is returned. - if (!this.#checkIsPromise(resultPromise)) { - throw new ERR_INVALID_RETURN_VALUE('instance of Promise', - 'promiseFn', resultPromise); + // Guard instanceof against arrow functions as they don't have a prototype. + // Check for matching Error classes. + } else if (expected.prototype !== undefined && actual instanceof expected) { + return; + } else if (ObjectPrototypeIsPrototypeOf(Error, expected)) { + if (!message) { + generatedMessage = true; + message = 'The error is expected to be an instance of ' + + `"${expected.name}". Received `; + if (isError(actual)) { + const name = (actual.constructor?.name) || + actual.name; + if (expected.name === name) { + message += 'an error with identical name but a different prototype.'; + } else { + message += `"${name}"`; + } + if (actual.message) { + message += `\n\nError message:\n\n${actual.message}`; + } + } else { + message += `"${inspect(actual, { depth: -1 })}"`; } - } else if (this.#checkIsPromise(promiseFn)) { - resultPromise = promiseFn; - } else { - throw new ERR_INVALID_ARG_TYPE( - 'promiseFn', ['Function', 'Promise'], promiseFn); } + throwError = true; + } else { + // Check validation functions return value. + const res = ReflectApply(expected, {}, [actual]); + if (res !== true) { + if (!message) { + generatedMessage = true; + const name = expected.name ? `"${expected.name}" ` : ''; + message = `The ${name}validation function is expected to return` + + ` "true". Received ${inspect(res)}`; - try { - await resultPromise; - } catch (e) { - return e; + if (isError(actual)) { + message += `\n\nCaught error:\n\n${actual}`; + } + } + throwError = true; } - return NO_EXCEPTION_SENTINEL; } - #getActual(fn) { - validateFunction(fn, 'fn'); - try { - fn(); - } catch (e) { - return e; - } - return NO_EXCEPTION_SENTINEL; - } - - #checkIsPromise(obj) { - // Accept native ES6 promises and promises that are implemented in a similar - // way. Do not accept thenables that use a function as `obj` and that have no - // `catch` handler. - return isPromise(obj) || - (obj !== null && typeof obj === 'object' && - typeof obj.then === 'function' && - typeof obj.catch === 'function'); - } - - /** - * Pure assertion tests whether a value is truthy, as determined - * by !!value. - * @param {...any} args - * @returns {void} - */ - ok(...args) { - innerOk(this.ok, args.length, ...args); - } - - /** - * Throws an AssertionError with the given message. - * @param {any | Error} [message] - */ - fail(message) { - if (isError(message)) throw message; - - let internalMessage = false; - if (message === undefined) { - message = 'Failed'; - internalMessage = true; - } - - const errArgs = { - operator: 'fail', - stackStartFn: this.fail, + if (throwError) { + const err = new AssertionError(_buildAssertionErrorOptions(this, { + actual, + expected, message, - }; - const err = new AssertionError(this.#buildAssertionErrorOptions(errArgs)); - if (internalMessage) { - err.generatedMessage = true; - } + operator: fn.name, + stackStartFn: fn, + })); + err.generatedMessage = generatedMessage; throw err; } +} - /** - * The equality assertion tests shallow, coercive equality with ==. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ - equal(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - // eslint-disable-next-line eqeqeq - if (actual != expected && (!NumberIsNaN(actual) || !NumberIsNaN(expected))) { - this.#innerFail({ - actual, - expected, - message, - operator: '==', - stackStartFn: this.equal, - }); - } - }; +function getActual(fn) { + validateFunction(fn, 'fn'); + try { + fn(); + } catch (e) { + return e; + } + return NO_EXCEPTION_SENTINEL; +} - /** - * The non-equality assertion tests for whether two objects are not - * equal with !=. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ - notEqual(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - // eslint-disable-next-line eqeqeq - if (actual == expected || (NumberIsNaN(actual) && NumberIsNaN(expected))) { - this.#innerFail({ - actual, - expected, - message, - operator: '!=', - stackStartFn: this.notEqual, - }); - } - }; +function checkIsPromise(obj) { + // Accept native ES6 promises and promises that are implemented in a similar + // way. Do not accept thenables that use a function as `obj` and that have no + // `catch` handler. + return isPromise(obj) || + (obj !== null && typeof obj === 'object' && + typeof obj.then === 'function' && + typeof obj.catch === 'function'); +} - /** - * The deep equivalence assertion tests a deep equality relation. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ - deepEqual(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - if (isDeepEqual === undefined) lazyLoadComparison(); - if (!isDeepEqual(actual, expected)) { - this.#innerFail({ - actual, - expected, - message, - operator: 'deepEqual', - stackStartFn: this.deepEqual, - }); - } - }; +async function waitForActual(promiseFn) { + let resultPromise; + if (typeof promiseFn === 'function') { + // Return a rejected promise if `promiseFn` throws synchronously. + resultPromise = promiseFn(); + // Fail in case no promise is returned. + if (!checkIsPromise(resultPromise)) { + throw new ERR_INVALID_RETURN_VALUE('instance of Promise', + 'promiseFn', resultPromise); + } + } else if (checkIsPromise(promiseFn)) { + resultPromise = promiseFn; + } else { + throw new ERR_INVALID_ARG_TYPE( + 'promiseFn', ['Function', 'Promise'], promiseFn); + } - /** - * The deep non-equivalence assertion tests for any deep inequality. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ - notDeepEqual(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - if (isDeepEqual === undefined) lazyLoadComparison(); - if (isDeepEqual(actual, expected)) { - this.#innerFail({ - actual, - expected, - message, - operator: 'notDeepEqual', - stackStartFn: this.notDeepEqual, - }); - } - }; + try { + await resultPromise; + } catch (e) { + return e; + } + return NO_EXCEPTION_SENTINEL; +} - /** - * The deep strict equivalence assertion tests a deep strict equality - * relation. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ - deepStrictEqual(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); +function expectsError(stackStartFn, actual, error, message) { + if (typeof error === 'string') { + if (arguments.length === 4) { + throw new ERR_INVALID_ARG_TYPE('error', + ['Object', 'Error', 'Function', 'RegExp'], + error); } - if (isDeepEqual === undefined) lazyLoadComparison(); - if (!isDeepStrictEqual(actual, expected)) { - this.#innerFail({ - actual, - expected, - message, - operator: 'deepStrictEqual', - stackStartFn: this.deepStrictEqual, - }); + if (typeof actual === 'object' && actual !== null) { + if (actual.message === error) { + throw new ERR_AMBIGUOUS_ARGUMENT( + 'error/message', + `The error message "${actual.message}" is identical to the message.`, + ); + } + } else if (actual === error) { + throw new ERR_AMBIGUOUS_ARGUMENT( + 'error/message', + `The error "${actual}" is identical to the message.`, + ); } - }; + message = error; + error = undefined; + } else if (error != null && + typeof error !== 'object' && + typeof error !== 'function') { + throw new ERR_INVALID_ARG_TYPE('error', + ['Object', 'Error', 'Function', 'RegExp'], + error); + } - /** - * The deep strict non-equivalence assertion tests for any deep strict - * inequality. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ - notDeepStrictEqual(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - if (isDeepEqual === undefined) lazyLoadComparison(); - if (isDeepStrictEqual(actual, expected)) { - this.#innerFail({ - actual, - expected, - message, - operator: 'notDeepStrictEqual', - stackStartFn: this.notDeepStrictEqual, - }); - } + if (actual === NO_EXCEPTION_SENTINEL) { + let details = ''; + if (error?.name) { + details += ` (${error.name})`; + } + details += message ? `: ${message}` : '.'; + const fnType = stackStartFn === Assert.prototype.rejects ? 'rejection' : 'exception'; + innerFail.call(this, { + actual: undefined, + expected: error, + operator: stackStartFn.name, + message: `Missing expected ${fnType}${details}`, + stackStartFn, + }); } - /** - * The strict equivalence assertion tests a strict equality relation. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ - strictEqual(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - if (!ObjectIs(actual, expected)) { - this.#innerFail({ - actual, - expected, - message, - operator: 'strictEqual', - stackStartFn: this.strictEqual, - }); - } - }; + if (!error) + return; - /** - * The strict non-equivalence assertion tests for any strict inequality. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ - notStrictEqual(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - if (ObjectIs(actual, expected)) { - this.#innerFail({ - actual, - expected, - message, - operator: 'notStrictEqual', - stackStartFn: this.notStrictEqual, - }); - } - }; + expectedException.call(this, actual, error, message, stackStartFn); +} - /** - * The strict equivalence assertion test between two objects - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ - partialDeepStrictEqual( - actual, - expected, - message, - ) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - if (isDeepEqual === undefined) lazyLoadComparison(); - if (!isPartialStrictEqual(actual, expected)) { - this.#innerFail({ - actual, - expected, - message, - operator: 'partialDeepStrictEqual', - stackStartFn: this.partialDeepStrictEqual, - }); +function hasMatchingError(actual, expected) { + if (typeof expected !== 'function') { + if (isRegExp(expected)) { + const str = String(actual); + return RegExpPrototypeExec(expected, str) !== null; } - }; + throw new ERR_INVALID_ARG_TYPE( + 'expected', ['Function', 'RegExp'], expected, + ); + } + // Guard instanceof against arrow functions as they don't have a prototype. + if (expected.prototype !== undefined && actual instanceof expected) { + return true; + } + if (ObjectPrototypeIsPrototypeOf(Error, expected)) { + return false; + } + return ReflectApply(expected, {}, [actual]) === true; +} - /** - * Expects the `string` input to match the regular expression. - * @param {string} string - * @param {RegExp} regexp - * @param {string | Error} [message] - * @returns {void} - */ - match(string, regexp, message) { - this.#internalMatch(string, regexp, message, Assert.prototype.match); - }; +function expectsNoError(stackStartFn, actual, error, message) { + if (actual === NO_EXCEPTION_SENTINEL) + return; - /** - * Expects the `string` input not to match the regular expression. - * @param {string} string - * @param {RegExp} regexp - * @param {string | Error} [message] - * @returns {void} - */ - doesNotMatch(string, regexp, message) { - this.#internalMatch(string, regexp, message, Assert.prototype.doesNotMatch); - }; + if (typeof error === 'string') { + message = error; + error = undefined; + } - /** - * Expects the function `promiseFn` to throw an error. - * @param {() => any} promiseFn - * @param {...any} [args] - * @returns {void} - */ - throws(promiseFn, ...args) { - this.#expectsError(Assert.prototype.throws, this.#getActual(promiseFn), ...args); - }; + if (!error || hasMatchingError(actual, error)) { + const details = message ? `: ${message}` : '.'; + const fnType = stackStartFn === Assert.prototype.doesNotReject ? + 'rejection' : 'exception'; + innerFail.call(this, { + actual, + expected: error, + operator: stackStartFn.name, + message: `Got unwanted ${fnType}${details}\n` + + `Actual message: "${actual?.message}"`, + stackStartFn, + }); + } + throw actual; +} - /** - * Expects `promiseFn` function or its value to reject. - * @param {() => Promise} promiseFn - * @param {...any} [args] - * @returns {Promise} - */ - async rejects(promiseFn, ...args) { - this.#expectsError(Assert.prototype.rejects, await this.#waitForActual(promiseFn), ...args); - }; +/** + * Expects the function `promiseFn` to throw an error. + * @param {() => any} promiseFn + * @param {...any} [args] + * @returns {void} + */ +Assert.prototype.throws = function throws(promiseFn, ...args) { + expectsError(throws, getActual(promiseFn), ...args); +}; - /** - * Asserts that the function `fn` does not throw an error. - * @param {() => any} fn - * @param {...any} [args] - * @returns {void} - */ - doesNotThrow(fn, ...args) { - this.#expectsNoError(Assert.prototype.doesNotThrow, this.#getActual(fn), ...args); - }; +/** + * Expects `promiseFn` function or its value to reject. + * @param {() => Promise} promiseFn + * @param {...any} [args] + * @returns {Promise} + */ +Assert.prototype.rejects = async function rejects(promiseFn, ...args) { + expectsError(rejects, await waitForActual(promiseFn), ...args); +}; - /** - * Expects `fn` or its value to not reject. - * @param {() => Promise} fn - * @param {...any} [args] - * @returns {Promise} - */ - async doesNotReject(fn, ...args) { - this.#expectsNoError(Assert.prototype.doesNotReject, await this.#waitForActual(fn), ...args); - }; +/** + * Asserts that the function `fn` does not throw an error. + * @param {() => any} fn + * @param {...any} [args] + * @returns {void} + */ +Assert.prototype.doesNotThrow = function doesNotThrow(fn, ...args) { + expectsNoError(doesNotThrow, getActual(fn), ...args); +}; - /** - * Throws `value` if the value is not `null` or `undefined`. - * @param {any} err - * @returns {void} - */ - ifError(err) { - if (err !== null && err !== undefined) { - let message = 'ifError got unwanted exception: '; - if (typeof err === 'object' && typeof err.message === 'string') { - if (err.message.length === 0 && err.constructor) { - message += err.constructor.name; - } else { - message += err.message; - } +/** + * Expects `fn` or its value to not reject. + * @param {() => Promise} fn + * @param {...any} [args] + * @returns {Promise} + */ +Assert.prototype.doesNotReject = async function doesNotReject(fn, ...args) { + expectsNoError(doesNotReject, await waitForActual(fn), ...args); +}; + +/** + * Throws `value` if the value is not `null` or `undefined`. + * @param {any} err + * @returns {void} + */ +Assert.prototype.ifError = function ifError(err) { + if (err !== null && err !== undefined) { + let message = 'ifError got unwanted exception: '; + if (typeof err === 'object' && typeof err.message === 'string') { + if (err.message.length === 0 && err.constructor) { + message += err.constructor.name; } else { - message += inspect(err); + message += err.message; } + } else { + message += inspect(err); + } - const newErr = new AssertionError(this.#buildAssertionErrorOptions({ - actual: err, - expected: null, - operator: 'ifError', - message, - stackStartFn: this.ifError, - })); - - // Make sure we actually have a stack trace! - const origStack = err.stack; - - if (typeof origStack === 'string') { - // This will remove any duplicated frames from the error frames taken - // from within `ifError` and add the original error frames to the newly - // created ones. - const origStackStart = StringPrototypeIndexOf(origStack, '\n at'); - if (origStackStart !== -1) { - const originalFrames = StringPrototypeSplit( - StringPrototypeSlice(origStack, origStackStart + 1), - '\n', - ); - // Filter all frames existing in err.stack. - let newFrames = StringPrototypeSplit(newErr.stack, '\n'); - for (const errFrame of originalFrames) { - // Find the first occurrence of the frame. - const pos = ArrayPrototypeIndexOf(newFrames, errFrame); - if (pos !== -1) { - // Only keep new frames. - newFrames = ArrayPrototypeSlice(newFrames, 0, pos); - break; - } + const newErr = new AssertionError(_buildAssertionErrorOptions(this, { + actual: err, + expected: null, + operator: 'ifError', + message, + stackStartFn: ifError, + })); + + // Make sure we actually have a stack trace! + const origStack = err.stack; + + if (typeof origStack === 'string') { + // This will remove any duplicated frames from the error frames taken + // from within `ifError` and add the original error frames to the newly + // created ones. + const origStackStart = StringPrototypeIndexOf(origStack, '\n at'); + if (origStackStart !== -1) { + const originalFrames = StringPrototypeSplit( + StringPrototypeSlice(origStack, origStackStart + 1), + '\n', + ); + // Filter all frames existing in err.stack. + let newFrames = StringPrototypeSplit(newErr.stack, '\n'); + for (const errFrame of originalFrames) { + // Find the first occurrence of the frame. + const pos = ArrayPrototypeIndexOf(newFrames, errFrame); + if (pos !== -1) { + // Only keep new frames. + newFrames = ArrayPrototypeSlice(newFrames, 0, pos); + break; } - const stackStart = ArrayPrototypeJoin(newFrames, '\n'); - const stackEnd = ArrayPrototypeJoin(originalFrames, '\n'); - newErr.stack = `${stackStart}\n${stackEnd}`; } + const stackStart = ArrayPrototypeJoin(newFrames, '\n'); + const stackEnd = ArrayPrototypeJoin(originalFrames, '\n'); + newErr.stack = `${stackStart}\n${stackEnd}`; } - - throw newErr; } - }; + + throw newErr; + } +}; + +function internalMatch(string, regexp, message, fn) { + if (!isRegExp(regexp)) { + throw new ERR_INVALID_ARG_TYPE( + 'regexp', 'RegExp', regexp, + ); + } + const match = fn === Assert.prototype.match; + if (typeof string !== 'string' || + RegExpPrototypeExec(regexp, string) !== null !== match) { + if (message instanceof Error) { + throw message; + } + + const generatedMessage = !message; + + // 'The input was expected to not match the regular expression ' + + message ||= (typeof string !== 'string' ? + 'The "string" argument must be of type string. Received type ' + + `${typeof string} (${inspect(string)})` : + (match ? + 'The input did not match the regular expression ' : + 'The input was expected to not match the regular expression ') + + `${inspect(regexp)}. Input:\n\n${inspect(string)}\n`); + const err = new AssertionError(_buildAssertionErrorOptions(this, { + actual: string, + expected: regexp, + message, + operator: fn.name, + stackStartFn: fn, + })); + err.generatedMessage = generatedMessage; + throw err; + } } -const assertInstance = new Assert(); -['ok', 'fail', 'equal', 'notEqual', 'deepEqual', 'notDeepEqual', - 'deepStrictEqual', 'notDeepStrictEqual', 'strictEqual', - 'notStrictEqual', 'partialDeepStrictEqual', 'match', 'doesNotMatch', - 'throws', 'rejects', 'doesNotThrow', 'doesNotReject', 'ifError'].forEach((name) => { - assertInstance[name] = assertInstance[name].bind(assertInstance); -}); +/** + * Expects the `string` input to match the regular expression. + * @param {string} string + * @param {RegExp} regexp + * @param {string | Error} [message] + * @returns {void} + */ +Assert.prototype.match = function match(string, regexp, message) { + internalMatch(string, regexp, message, match); +}; /** - * Pure assertion tests whether a value is truthy, as determined - * by !!value. - * @param {...any} args + * Expects the `string` input not to match the regular expression. + * @param {string} string + * @param {RegExp} regexp + * @param {string | Error} [message] * @returns {void} */ -function ok(...args) { - innerOk(ok, args.length, ...args); -} -ObjectAssign(assert, assertInstance); -assert.ok = ok; +Assert.prototype.doesNotMatch = function doesNotMatch(string, regexp, message) { + internalMatch(string, regexp, message, doesNotMatch); +}; /** * Expose a strict only variant of assert. @@ -842,7 +830,15 @@ function strict(...args) { innerOk(strict, args.length, ...args); } -assert.AssertionError = AssertionError; +const assertInstance = new Assert(); +[ + 'ok', 'fail', 'equal', 'notEqual', 'deepEqual', 'notDeepEqual', + 'deepStrictEqual', 'notDeepStrictEqual', 'strictEqual', + 'notStrictEqual', 'partialDeepStrictEqual', 'match', 'doesNotMatch', + 'throws', 'rejects', 'doesNotThrow', 'doesNotReject', 'ifError', +].forEach((name) => { + assert[name] = assertInstance[name].bind(assertInstance); +}); assert.strict = ObjectAssign(strict, assert, { equal: assert.strictEqual, diff --git a/test/fixtures/errors/error_exit.snapshot b/test/fixtures/errors/error_exit.snapshot index 9283959072d217..35b4405cddf4c4 100644 --- a/test/fixtures/errors/error_exit.snapshot +++ b/test/fixtures/errors/error_exit.snapshot @@ -1,13 +1,13 @@ Exiting with code=1 node:assert:* - throw new AssertionError(this.#buildAssertionErrorOptions(obj)); - ^ + throw new AssertionError(_buildAssertionErrorOptions(this, obj)); + ^ AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: 1 !== 2 - at new AssertionError (node:internal*assert*assertion_error:*:*) { + at Object. (*error_exit.js:*:*) { generatedMessage: true, code: 'ERR_ASSERTION', actual: 1, diff --git a/test/fixtures/errors/if-error-has-good-stack.snapshot b/test/fixtures/errors/if-error-has-good-stack.snapshot index 9f0e83d24d3efc..ba76800b970028 100644 --- a/test/fixtures/errors/if-error-has-good-stack.snapshot +++ b/test/fixtures/errors/if-error-has-good-stack.snapshot @@ -1,12 +1,12 @@ node:assert:* - throw newErr; - ^ + throw newErr; + ^ AssertionError [ERR_ASSERTION]: ifError got unwanted exception: test error - at new AssertionError (node:internal*assert*assertion_error:*:*) - at Assert.ifError (node:assert:*:*) at z (*if-error-has-good-stack.js:*:*) at y (*if-error-has-good-stack.js:*:*) + at x (*if-error-has-good-stack.js:*:*) + at Object. (*if-error-has-good-stack.js:*:*) at c (*if-error-has-good-stack.js:*:*) at b (*if-error-has-good-stack.js:*:*) at a (*if-error-has-good-stack.js:*:*) diff --git a/test/fixtures/test-runner/output/describe_it.snapshot b/test/fixtures/test-runner/output/describe_it.snapshot index 2cda9f1a6c7ae6..67d4af7f1b9f45 100644 --- a/test/fixtures/test-runner/output/describe_it.snapshot +++ b/test/fixtures/test-runner/output/describe_it.snapshot @@ -154,9 +154,6 @@ not ok 14 - async assertion fail * * * - * - * - * ... # Subtest: resolve pass ok 15 - resolve pass diff --git a/test/fixtures/test-runner/output/dot_reporter.snapshot b/test/fixtures/test-runner/output/dot_reporter.snapshot index 55689fa40ce51e..deba4abfcb5e28 100644 --- a/test/fixtures/test-runner/output/dot_reporter.snapshot +++ b/test/fixtures/test-runner/output/dot_reporter.snapshot @@ -61,9 +61,6 @@ Failed tests: * * * - * - * - * * { generatedMessage: true, code: 'ERR_ASSERTION', diff --git a/test/fixtures/test-runner/output/junit_reporter.snapshot b/test/fixtures/test-runner/output/junit_reporter.snapshot index 2b3eaec238c422..c4701e80226aaf 100644 --- a/test/fixtures/test-runner/output/junit_reporter.snapshot +++ b/test/fixtures/test-runner/output/junit_reporter.snapshot @@ -119,9 +119,6 @@ true !== false * * * - * - * - * * { generatedMessage: true, code: 'ERR_ASSERTION', diff --git a/test/fixtures/test-runner/output/output.snapshot b/test/fixtures/test-runner/output/output.snapshot index 9dc6a2c0d5755f..ffbe91759bb859 100644 --- a/test/fixtures/test-runner/output/output.snapshot +++ b/test/fixtures/test-runner/output/output.snapshot @@ -157,9 +157,6 @@ not ok 13 - async assertion fail * * * - * - * - * ... # Subtest: resolve pass ok 14 - resolve pass diff --git a/test/fixtures/test-runner/output/output_cli.snapshot b/test/fixtures/test-runner/output/output_cli.snapshot index 8fc99aa548c5ce..7f989f14c619cf 100644 --- a/test/fixtures/test-runner/output/output_cli.snapshot +++ b/test/fixtures/test-runner/output/output_cli.snapshot @@ -157,9 +157,6 @@ not ok 13 - async assertion fail * * * - * - * - * ... # Subtest: resolve pass ok 14 - resolve pass diff --git a/test/fixtures/test-runner/output/source_mapped_locations.snapshot b/test/fixtures/test-runner/output/source_mapped_locations.snapshot index 6fc9d3c455b379..8cf210da817aae 100644 --- a/test/fixtures/test-runner/output/source_mapped_locations.snapshot +++ b/test/fixtures/test-runner/output/source_mapped_locations.snapshot @@ -22,9 +22,6 @@ not ok 1 - fails * * * - * - * - * ... 1..1 # tests 1 diff --git a/test/fixtures/test-runner/output/spec_reporter.snapshot b/test/fixtures/test-runner/output/spec_reporter.snapshot index e3145efa25e0e0..3df07dfc631e39 100644 --- a/test/fixtures/test-runner/output/spec_reporter.snapshot +++ b/test/fixtures/test-runner/output/spec_reporter.snapshot @@ -166,9 +166,6 @@ * * * - * - * - * * { generatedMessage: true, code: 'ERR_ASSERTION', diff --git a/test/fixtures/test-runner/output/spec_reporter_cli.snapshot b/test/fixtures/test-runner/output/spec_reporter_cli.snapshot index c90c6df670639a..f3d15c8179dad2 100644 --- a/test/fixtures/test-runner/output/spec_reporter_cli.snapshot +++ b/test/fixtures/test-runner/output/spec_reporter_cli.snapshot @@ -169,9 +169,6 @@ * * * - * - * - * * { generatedMessage: true, code: 'ERR_ASSERTION', diff --git a/test/message/assert_throws_stack.out b/test/message/assert_throws_stack.out index 61fb186edbd32c..7cb062c3f91cd7 100644 --- a/test/message/assert_throws_stack.out +++ b/test/message/assert_throws_stack.out @@ -1,6 +1,6 @@ node:assert:* - throw err; - ^ + throw err; + ^ AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: + actual - expected @@ -9,6 +9,7 @@ AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: - Comparison { - bar: true - } + at Object. (*assert_throws_stack.js:*:*) at * at * @@ -22,7 +23,7 @@ AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: code: 'ERR_ASSERTION', actual: Error: foo at assert.throws.bar (*assert_throws_stack.js:*) - at #getActual (node:assert:*) + at getActual (node:assert:*) at Assert.throws (node:assert:*) at Object. (*assert_throws_stack.js:*:*) at * From a5eb04cc9ad8038bf5ed92a367bee7d449df6a2c Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Tue, 10 Jun 2025 07:59:22 -0300 Subject: [PATCH 23/28] test: add assertion for Assert constructor requiring new --- test/parallel/test-assert-class.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/parallel/test-assert-class.js b/test/parallel/test-assert-class.js index 0f222210c78d59..7e1ece9b21900d 100644 --- a/test/parallel/test-assert-class.js +++ b/test/parallel/test-assert-class.js @@ -13,6 +13,17 @@ if (process.stdout.isTTY) { process.env.NODE_DISABLE_COLORS = '1'; } +test('Assert constructor requires new', () => { + assert.throws( + () => Assert(), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /Assert/, + } + ); +}); + test('Assert class basic instance', () => { const assertInstance = new Assert(); From 932b0a34645f40d73b5015135253454788808e49 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Tue, 10 Jun 2025 16:47:28 -0300 Subject: [PATCH 24/28] lib: bind assert methods to instance with correct name property --- lib/assert.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index b0b4c5bf3c6cc3..c079d944799e56 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -28,6 +28,7 @@ const { Error, NumberIsNaN, ObjectAssign, + ObjectDefineProperty, ObjectIs, ObjectKeys, ObjectPrototypeIsPrototypeOf, @@ -830,14 +831,16 @@ function strict(...args) { innerOk(strict, args.length, ...args); } -const assertInstance = new Assert(); +const assertInstance = new Assert(); [ 'ok', 'fail', 'equal', 'notEqual', 'deepEqual', 'notDeepEqual', 'deepStrictEqual', 'notDeepStrictEqual', 'strictEqual', 'notStrictEqual', 'partialDeepStrictEqual', 'match', 'doesNotMatch', 'throws', 'rejects', 'doesNotThrow', 'doesNotReject', 'ifError', ].forEach((name) => { - assert[name] = assertInstance[name].bind(assertInstance); + const bound = assertInstance[name].bind(assertInstance); + ObjectDefineProperty(bound, 'name', { value: name }); + assert[name] = bound; }); assert.strict = ObjectAssign(strict, assert, { From 42fb0680c71909f0edf2da36adfbf3bf778d14eb Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Tue, 10 Jun 2025 17:51:55 -0300 Subject: [PATCH 25/28] lib: enhance Assert constructor to validate options.diff --- lib/assert.js | 28 ++++++++++++++++++++++++---- test/parallel/test-assert-class.js | 18 ++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index c079d944799e56..13880c4758e8d8 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -62,6 +62,8 @@ const { validateFunction, } = require('internal/validators'); +const kOptions = Symbol('options'); + let isDeepEqual; let isDeepStrictEqual; let isPartialStrictEqual; @@ -96,13 +98,31 @@ function Assert(options = {}) { if (!new.target) { throw new ERR_INVALID_ARG_TYPE('Assert', 'constructor', Assert); } + + const allowedDiffs = ['simple', 'full']; + if ( + options.diff !== undefined && + !allowedDiffs.includes(options.diff) + ) { + throw new ERR_INVALID_ARG_VALUE( + 'options.diff', + options.diff, + `must be one of ${allowedDiffs.map((d) => `"${d}"`).join(', ')}` + ); + } + this.AssertionError = AssertionError; - this.options = ObjectAssign({ diff: 'simple' }, options); + ObjectDefineProperty(this, kOptions, { + value: options, + enumerable: false, + configurable: false, + writable: false, + }); } function _buildAssertionErrorOptions(self, obj) { - if (self?.options?.diff === 'full') { - return { ...obj, diff: self.options.diff }; + if (self?.[kOptions]?.diff === 'full') { + return { ...obj, diff: self?.[kOptions]?.diff }; } return obj; } @@ -831,7 +851,7 @@ function strict(...args) { innerOk(strict, args.length, ...args); } -const assertInstance = new Assert(); +const assertInstance = new Assert({ diff: 'simple' }); [ 'ok', 'fail', 'equal', 'notEqual', 'deepEqual', 'notDeepEqual', 'deepStrictEqual', 'notDeepStrictEqual', 'strictEqual', diff --git a/test/parallel/test-assert-class.js b/test/parallel/test-assert-class.js index 7e1ece9b21900d..d763e853b4f79a 100644 --- a/test/parallel/test-assert-class.js +++ b/test/parallel/test-assert-class.js @@ -117,6 +117,24 @@ test('Assert class basic instance', () => { /* eslint-enable no-restricted-syntax */ }); +test('Assert class with valid diff options', () => { + assert.doesNotThrow(() => new Assert({ diff: 'simple' })); + assert.doesNotThrow(() => new Assert({ diff: 'full' })); + assert.doesNotThrow(() => new Assert()); + assert.doesNotThrow(() => new Assert({ diff: undefined })); +}); + +test('Assert class with invalid diff option', () => { + assert.throws( + () => new Assert({ diff: 'invalid' }), + { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + message: /must be one of "simple", "full"/, + } + ); +}); + test('Assert class with full diff', () => { const assertInstance = new Assert({ diff: 'full' }); From 74c90dedf74449071ceb37768b40e9c4584aad71 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Tue, 10 Jun 2025 19:08:58 -0300 Subject: [PATCH 26/28] lib: simplify error handling with diff option --- lib/assert.js | 79 ++++++++++++++---------- test/fixtures/errors/error_exit.snapshot | 2 +- test/parallel/test-assert-class.js | 9 +-- 3 files changed, 48 insertions(+), 42 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index 13880c4758e8d8..24ead3d24eb2cc 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -38,6 +38,7 @@ const { StringPrototypeIndexOf, StringPrototypeSlice, StringPrototypeSplit, + Symbol, } = primordials; const { @@ -107,12 +108,13 @@ function Assert(options = {}) { throw new ERR_INVALID_ARG_VALUE( 'options.diff', options.diff, - `must be one of ${allowedDiffs.map((d) => `"${d}"`).join(', ')}` + `must be one of ${allowedDiffs.map((d) => `"${d}"`).join(', ')}`, ); } this.AssertionError = AssertionError; ObjectDefineProperty(this, kOptions, { + __proto__: null, value: options, enumerable: false, configurable: false, @@ -120,13 +122,6 @@ function Assert(options = {}) { }); } -function _buildAssertionErrorOptions(self, obj) { - if (self?.[kOptions]?.diff === 'full') { - return { ...obj, diff: self?.[kOptions]?.diff }; - } - return obj; -} - // All of the following functions must throw an AssertionError // when a corresponding condition is not met, with a message that // may be undefined if not provided. All assertion methods provide @@ -136,7 +131,7 @@ function _buildAssertionErrorOptions(self, obj) { function innerFail(obj) { if (obj.message instanceof Error) throw obj.message; - throw new AssertionError(_buildAssertionErrorOptions(this, obj)); + throw new AssertionError(obj); } /** @@ -156,8 +151,9 @@ Assert.prototype.fail = function fail(message) { operator: 'fail', stackStartFn: fail, message, + diff: this?.[kOptions]?.diff, }; - const err = new AssertionError(_buildAssertionErrorOptions(this, errArgs)); + const err = new AssertionError(errArgs); if (internalMessage) { err.generatedMessage = true; } @@ -192,12 +188,13 @@ Assert.prototype.equal = function equal(actual, expected, message) { } // eslint-disable-next-line eqeqeq if (actual != expected && (!NumberIsNaN(actual) || !NumberIsNaN(expected))) { - innerFail.call(this, { + innerFail({ actual, expected, message, operator: '==', stackStartFn: equal, + diff: this?.[kOptions]?.diff, }); } }; @@ -216,12 +213,13 @@ Assert.prototype.notEqual = function notEqual(actual, expected, message) { } // eslint-disable-next-line eqeqeq if (actual == expected || (NumberIsNaN(actual) && NumberIsNaN(expected))) { - innerFail.call(this, { + innerFail({ actual, expected, message, operator: '!=', stackStartFn: notEqual, + diff: this?.[kOptions]?.diff, }); } }; @@ -239,12 +237,13 @@ Assert.prototype.deepEqual = function deepEqual(actual, expected, message) { } if (isDeepEqual === undefined) lazyLoadComparison(); if (!isDeepEqual(actual, expected)) { - innerFail.call(this, { + innerFail({ actual, expected, message, operator: 'deepEqual', stackStartFn: deepEqual, + diff: this?.[kOptions]?.diff, }); } }; @@ -262,12 +261,13 @@ Assert.prototype.notDeepEqual = function notDeepEqual(actual, expected, message) } if (isDeepEqual === undefined) lazyLoadComparison(); if (isDeepEqual(actual, expected)) { - innerFail.call(this, { + innerFail({ actual, expected, message, operator: 'notDeepEqual', stackStartFn: notDeepEqual, + diff: this?.[kOptions]?.diff, }); } }; @@ -286,12 +286,13 @@ Assert.prototype.deepStrictEqual = function deepStrictEqual(actual, expected, me } if (isDeepEqual === undefined) lazyLoadComparison(); if (!isDeepStrictEqual(actual, expected)) { - innerFail.call(this, { + innerFail({ actual, expected, message, operator: 'deepStrictEqual', stackStartFn: deepStrictEqual, + diff: this?.[kOptions]?.diff, }); } }; @@ -311,12 +312,13 @@ function notDeepStrictEqual(actual, expected, message) { } if (isDeepEqual === undefined) lazyLoadComparison(); if (isDeepStrictEqual(actual, expected)) { - innerFail.call(this, { + innerFail({ actual, expected, message, operator: 'notDeepStrictEqual', stackStartFn: notDeepStrictEqual, + diff: this?.[kOptions]?.diff, }); } } @@ -333,12 +335,13 @@ Assert.prototype.strictEqual = function strictEqual(actual, expected, message) { throw new ERR_MISSING_ARGS('actual', 'expected'); } if (!ObjectIs(actual, expected)) { - innerFail.call(this, { + innerFail({ actual, expected, message, operator: 'strictEqual', stackStartFn: strictEqual, + diff: this?.[kOptions]?.diff, }); } }; @@ -355,12 +358,13 @@ Assert.prototype.notStrictEqual = function notStrictEqual(actual, expected, mess throw new ERR_MISSING_ARGS('actual', 'expected'); } if (ObjectIs(actual, expected)) { - innerFail.call(this, { + innerFail({ actual, expected, message, operator: 'notStrictEqual', stackStartFn: notStrictEqual, + diff: this?.[kOptions]?.diff, }); } }; @@ -382,12 +386,13 @@ Assert.prototype.partialDeepStrictEqual = function partialDeepStrictEqual( } if (isDeepEqual === undefined) lazyLoadComparison(); if (!isPartialStrictEqual(actual, expected)) { - innerFail.call(this, { + innerFail({ actual, expected, message, operator: 'partialDeepStrictEqual', stackStartFn: partialDeepStrictEqual, + diff: this?.[kOptions]?.diff, }); } }; @@ -416,23 +421,25 @@ function compareExceptionKey(actual, expected, key, message, keys, fn) { const a = new Comparison(actual, keys); const b = new Comparison(expected, keys, actual); - const err = new AssertionError(_buildAssertionErrorOptions(this, { + const err = new AssertionError({ actual: a, expected: b, operator: 'deepStrictEqual', stackStartFn: fn, - })); + diff: this?.[kOptions]?.diff, + }); err.actual = actual; err.expected = expected; err.operator = fn.name; throw err; } - innerFail.call(this, { + innerFail({ actual, expected, message, operator: fn.name, stackStartFn: fn, + diff: this?.[kOptions]?.diff, }); } } @@ -456,13 +463,14 @@ function expectedException(actual, expected, message, fn) { throwError = true; // Handle primitives properly. } else if (typeof actual !== 'object' || actual === null) { - const err = new AssertionError(_buildAssertionErrorOptions(this, { + const err = new AssertionError({ actual, expected, message, operator: 'deepStrictEqual', stackStartFn: fn, - })); + diff: this?.[kOptions]?.diff, + }); err.operator = fn.name; throw err; } else { @@ -531,13 +539,14 @@ function expectedException(actual, expected, message, fn) { } if (throwError) { - const err = new AssertionError(_buildAssertionErrorOptions(this, { + const err = new AssertionError({ actual, expected, message, operator: fn.name, stackStartFn: fn, - })); + diff: this?.[kOptions]?.diff, + }); err.generatedMessage = generatedMessage; throw err; } @@ -625,12 +634,13 @@ function expectsError(stackStartFn, actual, error, message) { } details += message ? `: ${message}` : '.'; const fnType = stackStartFn === Assert.prototype.rejects ? 'rejection' : 'exception'; - innerFail.call(this, { + innerFail({ actual: undefined, expected: error, operator: stackStartFn.name, message: `Missing expected ${fnType}${details}`, stackStartFn, + diff: this?.[kOptions]?.diff, }); } @@ -673,13 +683,14 @@ function expectsNoError(stackStartFn, actual, error, message) { const details = message ? `: ${message}` : '.'; const fnType = stackStartFn === Assert.prototype.doesNotReject ? 'rejection' : 'exception'; - innerFail.call(this, { + innerFail({ actual, expected: error, operator: stackStartFn.name, message: `Got unwanted ${fnType}${details}\n` + `Actual message: "${actual?.message}"`, stackStartFn, + diff: this?.[kOptions]?.diff, }); } throw actual; @@ -743,13 +754,14 @@ Assert.prototype.ifError = function ifError(err) { message += inspect(err); } - const newErr = new AssertionError(_buildAssertionErrorOptions(this, { + const newErr = new AssertionError({ actual: err, expected: null, operator: 'ifError', message, stackStartFn: ifError, - })); + diff: this?.[kOptions]?.diff, + }); // Make sure we actually have a stack trace! const origStack = err.stack; @@ -808,13 +820,14 @@ function internalMatch(string, regexp, message, fn) { 'The input did not match the regular expression ' : 'The input was expected to not match the regular expression ') + `${inspect(regexp)}. Input:\n\n${inspect(string)}\n`); - const err = new AssertionError(_buildAssertionErrorOptions(this, { + const err = new AssertionError({ actual: string, expected: regexp, message, operator: fn.name, stackStartFn: fn, - })); + diff: this?.[kOptions]?.diff, + }); err.generatedMessage = generatedMessage; throw err; } @@ -859,7 +872,7 @@ const assertInstance = new Assert({ diff: 'simple' }); 'throws', 'rejects', 'doesNotThrow', 'doesNotReject', 'ifError', ].forEach((name) => { const bound = assertInstance[name].bind(assertInstance); - ObjectDefineProperty(bound, 'name', { value: name }); + ObjectDefineProperty(bound, 'name', { __proto__: null, value: name }); assert[name] = bound; }); diff --git a/test/fixtures/errors/error_exit.snapshot b/test/fixtures/errors/error_exit.snapshot index 35b4405cddf4c4..9594e08b4dadf9 100644 --- a/test/fixtures/errors/error_exit.snapshot +++ b/test/fixtures/errors/error_exit.snapshot @@ -1,6 +1,6 @@ Exiting with code=1 node:assert:* - throw new AssertionError(_buildAssertionErrorOptions(this, obj)); + throw new AssertionError(obj); ^ AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: diff --git a/test/parallel/test-assert-class.js b/test/parallel/test-assert-class.js index d763e853b4f79a..24beefc1c19cfb 100644 --- a/test/parallel/test-assert-class.js +++ b/test/parallel/test-assert-class.js @@ -25,7 +25,7 @@ test('Assert constructor requires new', () => { }); test('Assert class basic instance', () => { - const assertInstance = new Assert(); + const assertInstance = new Assert({ diff: undefined }); assertInstance.ok(assert.AssertionError.prototype instanceof Error, 'assert.AssertionError instanceof Error'); @@ -117,13 +117,6 @@ test('Assert class basic instance', () => { /* eslint-enable no-restricted-syntax */ }); -test('Assert class with valid diff options', () => { - assert.doesNotThrow(() => new Assert({ diff: 'simple' })); - assert.doesNotThrow(() => new Assert({ diff: 'full' })); - assert.doesNotThrow(() => new Assert()); - assert.doesNotThrow(() => new Assert({ diff: undefined })); -}); - test('Assert class with invalid diff option', () => { assert.throws( () => new Assert({ diff: 'invalid' }), From a1db7bc034157329d9efaf069db8b8657a7113a1 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Tue, 10 Jun 2025 20:21:06 -0300 Subject: [PATCH 27/28] doc: add diff option to Assert and AssertionError --- doc/api/assert.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/doc/api/assert.md b/doc/api/assert.md index 64ba28687e8704..4428b4d128f2c5 100644 --- a/doc/api/assert.md +++ b/doc/api/assert.md @@ -149,6 +149,8 @@ added: v0.1.21 * `operator` {string} The `operator` property on the error instance. * `stackStartFn` {Function} If provided, the generated stack trace omits frames before this function. + * `diff` {string} If set to `'full'`, shows the full diff in assertion errors. Defaults to `'simple'`. + Accepted values: `'simple'`, `'full'`. A subclass of {Error} that indicates the failure of an assertion. @@ -215,10 +217,33 @@ try { } ``` +## Class: assert.Assert + + + +The `Assert` class allows creating independent assertion instances with custom options. + +### `new assert.Assert([options])` + +* `options` {Object} + * `diff` {string} If set to `'full'`, shows the full diff in assertion errors. Defaults to `'simple'`. + Accepted values: `'simple'`, `'full'`. + +Creates a new assertion instance. The `diff` option controls the verbosity of diffs in assertion error messages. + +```js +const { Assert } = require('node:assert'); +const assertInstance = new Assert({ diff: 'full' }); +assertInstance.deepStrictEqual({ a: 1 }, { a: 2 }); +// Shows a full diff in the error message. +``` + ## `assert(value[, message])` * `value` {any} The input that is checked for being truthy. From 2971f6c7d8c87d44fdf289836e264b1d7070bf84 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Tue, 10 Jun 2025 20:23:57 -0300 Subject: [PATCH 28/28] doc: update version number for assert function introduction --- doc/api/assert.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/assert.md b/doc/api/assert.md index 4428b4d128f2c5..a376e8cc268138 100644 --- a/doc/api/assert.md +++ b/doc/api/assert.md @@ -243,7 +243,7 @@ assertInstance.deepStrictEqual({ a: 1 }, { a: 2 }); ## `assert(value[, message])` * `value` {any} The input that is checked for being truthy.