From 170623de089dcef64e0f41e0419025f08321f030 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Fri, 25 Apr 2025 10:13:50 -0300 Subject: [PATCH 01/10] lib: restructure assert to become a class --- lib/assert.js | 1310 +++++++++-------- lib/internal/assert/assertion_error.js | 26 +- test/fixtures/errors/error_exit.snapshot | 9 +- .../errors/if-error-has-good-stack.snapshot | 11 +- .../output/assertion-color-tty.snapshot | 3 +- .../test-runner/output/describe_it.snapshot | 3 + .../test-runner/output/dot_reporter.snapshot | 9 +- .../output/junit_reporter.snapshot | 9 +- .../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 | 9 +- .../output/spec_reporter_cli.snapshot | 9 +- test/message/assert_throws_stack.out | 11 +- test/parallel/test-assert-class.js | 197 +++ test/parallel/test-fs-promises.js | 2 +- test/parallel/test-runner-assert.js | 2 + 17 files changed, 948 insertions(+), 671 deletions(-) create mode 100644 test/parallel/test-assert-class.js diff --git a/lib/assert.js b/lib/assert.js index 6dedcd0b971046..6563693e49924e 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -75,728 +75,763 @@ 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 = {}; -// 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; +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]; + } + } + } } - 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 options. + * @typedef {object} AssertOptions + * @property {'full'|'simple'} [diff='simple'] - If set to 'full', shows the full diff in assertion errors. */ -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} + * The Assert class provides assertion methods. + * @class + * @param {AssertOptions} [options] - Optional configuration for assertions. */ -assert.deepEqual = function deepEqual(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); +class Assert { + constructor(options = {}) { + this.AssertionError = AssertionError; + this.options = { ...options, diff: options.diff ?? 'simple' }; } - 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, - }); + #buildAssertionErrorOptions(obj) { + if (this.options.diff === 'full') { + return { ...obj, diff: this.options.diff }; + } + return obj; } -}; -/* 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, - }); - } -}; + // 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. -/** - * 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, - }); - } -} + #innerFail(obj) { + if (obj.message instanceof Error) throw obj.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, - }); + throw new AssertionError(this.#buildAssertionErrorOptions(obj)); } -}; -/** - * 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]; - } + #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(this.#buildAssertionErrorOptions({ + actual: string, + expected: regexp, + message, + operator: fn.name, + stackStartFn: fn, + })); + err.generatedMessage = generatedMessage; + throw err; } } -} -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', + #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; + } + this.#innerFail({ + actual, + expected, + message, + operator: fn.name, stackStartFn: fn, }); - err.actual = actual; - err.expected = expected; - err.operator = fn.name; - throw err; } - innerFail({ - actual, - expected, - message, - operator: fn.name, - stackStartFn: fn, - }); } -} -function expectedException(actual, expected, message, fn) { - let generatedMessage = false; - let throwError = false; + #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 (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(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); + } + 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 input did not match the regular expression ' + - `${inspect(expected)}. Input:\n\n${inspect(str)}\n`; + 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; - // Handle primitives properly. - } else if (typeof actual !== 'object' || actual === null) { - const err = new AssertionError({ + } 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({ actual, expected, message, - operator: 'deepStrictEqual', + operator: fn.name, stackStartFn: fn, - }); - err.operator = fn.name; + })); + err.generatedMessage = generatedMessage; 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}`; + } + + #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 { - message += `"${inspect(actual, { depth: -1 })}"`; + } 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); } - 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}`; - } + if (actual === NO_EXCEPTION_SENTINEL) { + let details = ''; + if (error?.name) { + details += ` (${error.name})`; } - throwError = true; + 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 (throwError) { - const err = new AssertionError({ - actual, - expected, - message, - operator: fn.name, - stackStartFn: fn, - }); - err.generatedMessage = generatedMessage; - throw err; - } -} + if (!error) + return; -function getActual(fn) { - validateFunction(fn, 'fn'); - try { - fn(); - } catch (e) { - return e; + this.#expectedException(actual, error, message, stackStartFn); } - 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 - // `catch` handler. - return isPromise(obj) || - (obj !== null && typeof obj === 'object' && - typeof obj.then === 'function' && - typeof obj.catch === 'function'); -} + #expectsNoError(stackStartFn, actual, error, message) { + if (actual === NO_EXCEPTION_SENTINEL) + return; -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); - } + if (typeof error === 'string') { + message = error; + error = undefined; + } - try { - await resultPromise; - } catch (e) { - return e; + 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; } - 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.`, - ); + #hasMatchingError(actual, expected) { + if (typeof expected !== 'function') { + if (isRegExp(expected)) { + const str = String(actual); + return RegExpPrototypeExec(expected, str) !== null; } - } else if (actual === error) { - throw new ERR_AMBIGUOUS_ARGUMENT( - 'error/message', - `The error "${actual}" is identical to the message.`, + throw new ERR_INVALID_ARG_TYPE( + 'expected', ['Function', 'RegExp'], expected, ); } - 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); + // 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); + } + } else if (this.#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; } - 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, - }); + #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, + message, + }; + const err = new AssertionError(this.#buildAssertionErrorOptions(errArgs)); + if (internalMessage) { + err.generatedMessage = true; + } + throw err; } - if (!error) - return; + /** + * 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, + }); + } + }; - expectedException(actual, error, message, stackStartFn); -} + /** + * 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 hasMatchingError(actual, expected) { - if (typeof expected !== 'function') { - if (isRegExp(expected)) { - const str = String(actual); - return RegExpPrototypeExec(expected, str) !== null; + /** + * 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'); } - 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; -} + if (isDeepEqual === undefined) lazyLoadComparison(); + if (!isDeepEqual(actual, expected)) { + this.#innerFail({ + actual, + expected, + message, + operator: 'deepEqual', + stackStartFn: this.deepEqual, + }); + } + }; -function expectsNoError(stackStartFn, actual, error, message) { - if (actual === NO_EXCEPTION_SENTINEL) - return; + /** + * 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, + }); + } + }; - if (typeof error === 'string') { - message = error; - error = undefined; - } + /** + * 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)) { + this.#innerFail({ + actual, + expected, + message, + operator: 'deepStrictEqual', + stackStartFn: this.deepStrictEqual, + }); + } + }; - 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, - }); + /** + * 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, + }); + } } - 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); -}; + /** + * 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, + }); + } + }; -/** - * 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); -}; + /** + * 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, + }); + } + }; -/** - * 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); -}; + /** + * 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, + }); + } + }; -/** - * 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); -}; + /** + * 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); + }; -/** - * 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; + /** + * 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); + }; + + /** + * 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); + }; + + /** + * 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); + }; + + /** + * 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 `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); + }; + + /** + * 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 += err.message; + message += inspect(err); } - } 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 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 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; - } -}; -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; - } + throw newErr; + } + }; } -/** - * 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); -}; +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 not to match the regular expression. - * @param {string} string - * @param {RegExp} regexp - * @param {string | Error} [message] + * Pure assertion tests whether a value is truthy, as determined + * by !!value. + * @param {...any} args * @returns {void} */ -assert.doesNotMatch = function doesNotMatch(string, regexp, message) { - internalMatch(string, regexp, message, doesNotMatch); -}; +function ok(...args) { + innerOk(ok, args.length, ...args); +} +ObjectAssign(assert, assertInstance); +assert.ok = ok; /** * Expose a strict only variant of assert. @@ -807,6 +842,8 @@ function strict(...args) { innerOk(strict, args.length, ...args); } +assert.AssertionError = AssertionError; + assert.strict = ObjectAssign(strict, assert, { equal: assert.strictEqual, deepEqual: assert.deepStrictEqual, @@ -814,4 +851,7 @@ assert.strict = ObjectAssign(strict, assert, { notDeepEqual: assert.notDeepStrictEqual, }); +assert.strict.Assert = Assert; assert.strict.strict = assert.strict; + +module.exports.Assert = Assert; 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 778165dc25c4fc..9283959072d217 100644 --- a/test/fixtures/errors/error_exit.snapshot +++ b/test/fixtures/errors/error_exit.snapshot @@ -1,18 +1,19 @@ 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: 1 !== 2 - at Object. (*error_exit.js:*:*) { + at new AssertionError (node:internal*assert*assertion_error:*:*) { generatedMessage: true, 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 9296b25f10b7c6..9f0e83d24d3efc 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:*:*) @@ -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/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 4ef804951dc99f..05c7db38d85c56 100644 --- a/test/fixtures/test-runner/output/dot_reporter.snapshot +++ b/test/fixtures/test-runner/output/dot_reporter.snapshot @@ -61,12 +61,16 @@ Failed tests: * * * + * + * + * * { generatedMessage: true, code: 'ERR_ASSERTION', actual: true, expected: false, - operator: 'strictEqual' + operator: 'strictEqual', + diff: 'simple' } ✖ reject fail (*ms) Error: rejected from reject fail @@ -215,7 +219,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 d1868bd4b6eaa9..c74e5dcd0dd395 100644 --- a/test/fixtures/test-runner/output/junit_reporter.snapshot +++ b/test/fixtures/test-runner/output/junit_reporter.snapshot @@ -119,12 +119,16 @@ true !== false * * * + * + * + * * { generatedMessage: true, code: 'ERR_ASSERTION', actual: true, expected: false, - operator: 'strictEqual' + operator: 'strictEqual', + diff: 'simple' } } @@ -491,7 +495,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/output.snapshot b/test/fixtures/test-runner/output/output.snapshot index 044ac4137fa78d..91423254c1cde7 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 eaa085d97d06d1..120d3cecdc2797 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 17eb9d01451d32..7133bd643052d9 100644 --- a/test/fixtures/test-runner/output/spec_reporter.snapshot +++ b/test/fixtures/test-runner/output/spec_reporter.snapshot @@ -166,12 +166,16 @@ * * * + * + * + * * { generatedMessage: true, code: 'ERR_ASSERTION', actual: true, expected: false, - operator: 'strictEqual' + operator: 'strictEqual', + diff: 'simple' } * @@ -356,7 +360,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 2dd9e92deb1b38..eab1b66515aeb8 100644 --- a/test/fixtures/test-runner/output/spec_reporter_cli.snapshot +++ b/test/fixtures/test-runner/output/spec_reporter_cli.snapshot @@ -169,12 +169,16 @@ * * * + * + * + * * { generatedMessage: true, code: 'ERR_ASSERTION', actual: true, expected: false, - operator: 'strictEqual' + operator: 'strictEqual', + diff: 'simple' } * @@ -359,7 +363,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/message/assert_throws_stack.out b/test/message/assert_throws_stack.out index 897ddf36a04eb0..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 @@ -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 * diff --git a/test/parallel/test-assert-class.js b/test/parallel/test-assert-class.js new file mode 100644 index 00000000000000..0f222210c78d59 --- /dev/null +++ b/test/parallel/test-assert-class.js @@ -0,0 +1,197 @@ +'use strict'; + +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 +// 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 */ +}); + +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; + }); +}); 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()); 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 f3e1368fb12593d5ca87af8bac728217b72aabb4 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Mon, 9 Jun 2025 20:03:36 -0300 Subject: [PATCH 02/10] 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 05c7db38d85c56..c50cb99c16a0fa 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 c74e5dcd0dd395..37cea45d62bd84 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 91423254c1cde7..044ac4137fa78d 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 120d3cecdc2797..eaa085d97d06d1 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 7133bd643052d9..749f0dce43a450 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 eab1b66515aeb8..b3a352092c677a 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 244d7eb00735905d15e4ba4a2775ad3c4c5fac47 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Tue, 10 Jun 2025 07:59:22 -0300 Subject: [PATCH 03/10] 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 8d8c1e1e2f8504b56d8c173c9d14a650c0006fd4 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Tue, 10 Jun 2025 16:47:28 -0300 Subject: [PATCH 04/10] 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 d3b0035677488520965cfcdaa1ce99409426e7fb Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Tue, 10 Jun 2025 17:51:55 -0300 Subject: [PATCH 05/10] 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 0021039de106bd355c59a2066115c892f3515d70 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Tue, 10 Jun 2025 19:08:58 -0300 Subject: [PATCH 06/10] 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 1222ee54e259a8bde488e84a726d008f2798a6e7 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Tue, 10 Jun 2025 20:21:06 -0300 Subject: [PATCH 07/10] 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 cbb8446b5d5444..0e00f35e20d20a 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 9365a0f71895db008938a29e970c1e3976237b86 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Tue, 10 Jun 2025 20:23:57 -0300 Subject: [PATCH 08/10] 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 0e00f35e20d20a..3040ac69885e38 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. From fbb356c73f4839e0e7f4d47a50cdc0bfc0940103 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Wed, 18 Jun 2025 19:02:31 -0300 Subject: [PATCH 09/10] lib: add strict option to Assert --- doc/api/assert.md | 2 ++ lib/assert.js | 15 +++++++++++++-- test/parallel/test-assert-class.js | 23 +++++++++++++++++------ 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/doc/api/assert.md b/doc/api/assert.md index 3040ac69885e38..45f889712e6513 100644 --- a/doc/api/assert.md +++ b/doc/api/assert.md @@ -230,6 +230,8 @@ The `Assert` class allows creating independent assertion instances with custom o * `options` {Object} * `diff` {string} If set to `'full'`, shows the full diff in assertion errors. Defaults to `'simple'`. Accepted values: `'simple'`, `'full'`. + * `strict` {boolean} If set to `true`, non-strict methods behave like their + corresponding strict methods. Defaults to `true`. Creates a new assertion instance. The `diff` option controls the verbosity of diffs in assertion error messages. diff --git a/lib/assert.js b/lib/assert.js index 24ead3d24eb2cc..73359c953cb4c0 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -88,6 +88,8 @@ const NO_EXCEPTION_SENTINEL = {}; * Assert options. * @typedef {object} AssertOptions * @property {'full'|'simple'} [diff='simple'] - If set to 'full', shows the full diff in assertion errors. + * @property {boolean} [strict=true] - If set to true, non-strict methods behave like their corresponding + * strict methods. */ /** @@ -95,11 +97,13 @@ const NO_EXCEPTION_SENTINEL = {}; * @param {AssertOptions} [options] - Optional configuration for assertions. * @throws {ERR_INVALID_ARG_TYPE} If not called with `new`. */ -function Assert(options = {}) { +function Assert(options) { if (!new.target) { throw new ERR_INVALID_ARG_TYPE('Assert', 'constructor', Assert); } + options = ObjectAssign({ strict: true }, options); + const allowedDiffs = ['simple', 'full']; if ( options.diff !== undefined && @@ -120,6 +124,13 @@ function Assert(options = {}) { configurable: false, writable: false, }); + + if (options.strict) { + this.equal = this.strictEqual; + this.deepEqual = this.deepStrictEqual; + this.notEqual = this.notStrictEqual; + this.notDeepEqual = this.notDeepStrictEqual; + } } // All of the following functions must throw an AssertionError @@ -864,7 +875,7 @@ function strict(...args) { innerOk(strict, args.length, ...args); } -const assertInstance = new Assert({ diff: 'simple' }); +const assertInstance = new Assert({ diff: 'simple', strict: false }); [ '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 24beefc1c19cfb..9ea000987d2b1d 100644 --- a/test/parallel/test-assert-class.js +++ b/test/parallel/test-assert-class.js @@ -24,8 +24,8 @@ test('Assert constructor requires new', () => { ); }); -test('Assert class basic instance', () => { - const assertInstance = new Assert({ diff: undefined }); +test('Assert class non strict', () => { + const assertInstance = new Assert({ diff: undefined, strict: false }); assertInstance.ok(assert.AssertionError.prototype instanceof Error, 'assert.AssertionError instanceof Error'); @@ -44,6 +44,8 @@ test('Assert class basic instance', () => { } ); assertInstance.equal(undefined, undefined); + assertInstance.equal(null, undefined); + assertInstance.equal(2, '2'); assertInstance.notEqual(true, false); assertInstance.throws( () => assertInstance.deepEqual(/a/), @@ -117,6 +119,15 @@ test('Assert class basic instance', () => { /* eslint-enable no-restricted-syntax */ }); +test('Assert class strict', () => { + const assertInstance = new Assert(); + + assertInstance.equal(assertInstance.equal, assertInstance.strictEqual); + assertInstance.equal(assertInstance.deepEqual, assertInstance.deepStrictEqual); + assertInstance.equal(assertInstance.notEqual, assertInstance.notStrictEqual); + assertInstance.equal(assertInstance.notDeepEqual, assertInstance.notDeepStrictEqual); +}); + test('Assert class with invalid diff option', () => { assert.throws( () => new Assert({ diff: 'invalid' }), @@ -128,8 +139,8 @@ test('Assert class with invalid diff option', () => { ); }); -test('Assert class with full diff', () => { - const assertInstance = new Assert({ diff: 'full' }); +test('Assert class non strict with full diff', () => { + const assertInstance = new Assert({ diff: 'full', strict: false }); const longStringOfAs = 'A'.repeat(1025); const longStringOFBs = 'B'.repeat(1025); @@ -173,8 +184,8 @@ test('Assert class with full diff', () => { }); }); -test('Assert class with simple diff', () => { - const assertInstance = new Assert({ diff: 'simple' }); +test('Assert class non strict with simple diff', () => { + const assertInstance = new Assert({ diff: 'simple', strict: false }); const longStringOfAs = 'A'.repeat(1025); const longStringOFBs = 'B'.repeat(1025); From b664f8c97e680a9a6f50b567af96f45300b1a4d5 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Thu, 26 Jun 2025 12:14:28 -0300 Subject: [PATCH 10/10] lib: refactor addEllipsis function to remove unused parameter --- lib/internal/assert/assertion_error.js | 7 +- test/parallel/test-assert-class.js | 504 ++++++++++++++++++------- 2 files changed, 379 insertions(+), 132 deletions(-) diff --git a/lib/internal/assert/assertion_error.js b/lib/internal/assert/assertion_error.js index c13d29c873266b..5c15b96b12d1ea 100644 --- a/lib/internal/assert/assertion_error.js +++ b/lib/internal/assert/assertion_error.js @@ -231,8 +231,7 @@ function createErrDiff(actual, expected, operator, customMessage, diffType = 'si return `${headerMessage}${skippedMessage}\n${message}\n`; } -function addEllipsis(string, diff) { - if (diff === 'full') return string; +function addEllipsis(string) { const lines = StringPrototypeSplit(string, '\n', 11); if (lines.length > 10) { lines.length = 10; @@ -392,10 +391,10 @@ class AssertionError extends Error { const tmpExpected = this.expected; if (typeof this.actual === 'string') { - this.actual = addEllipsis(this.actual, this.diff); + this.actual = addEllipsis(this.actual); } if (typeof this.expected === 'string') { - this.expected = addEllipsis(this.expected, this.diff); + this.expected = addEllipsis(this.expected); } // This limits the `actual` and `expected` property default inspection to diff --git a/test/parallel/test-assert-class.js b/test/parallel/test-assert-class.js index 9ea000987d2b1d..62c910051776e2 100644 --- a/test/parallel/test-assert-class.js +++ b/test/parallel/test-assert-class.js @@ -14,24 +14,25 @@ if (process.stdout.isTTY) { } test('Assert constructor requires new', () => { - assert.throws( - () => Assert(), - { - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: /Assert/, - } - ); + assert.throws(() => Assert(), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /Assert/, + }); }); test('Assert class non strict', () => { const assertInstance = new Assert({ diff: undefined, strict: false }); - assertInstance.ok(assert.AssertionError.prototype instanceof Error, - 'assert.AssertionError instanceof Error'); + assertInstance.ok( + assert.AssertionError.prototype instanceof Error, + 'assert.AssertionError instanceof Error' + ); assertInstance.ok(true); assertInstance.throws( - () => { assertInstance.fail(); }, + () => { + assertInstance.fail(); + }, { code: 'ERR_ASSERTION', name: 'AssertionError', @@ -40,50 +41,52 @@ test('Assert class non strict', () => { actual: undefined, expected: undefined, generatedMessage: true, - stack: /Failed/ + stack: /Failed/, } ); assertInstance.equal(undefined, undefined); assertInstance.equal(null, undefined); assertInstance.equal(2, '2'); 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.deepEqual(/a/), - { code: 'ERR_MISSING_ARGS' } - ); - assertInstance.throws( - () => assertInstance.notDeepEqual('test'), - { code: 'ERR_MISSING_ARGS' } + () => assertInstance.strictEqual(2, '2'), + assertInstance.AssertionError, + "strictEqual(2, '2')" ); - 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'); + 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')" + 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 */ { @@ -93,21 +96,31 @@ test('Assert class non strict', () => { let threw = false; try { - assertInstance.doesNotThrow(() => thrower(TypeError), assertInstance.AssertionError); + 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'); + 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); + assertInstance.doesNotThrow( + () => { + throw new TypeError('wrong type'); + }, + TypeError, + rangeError + ); } catch (e) { threw = true; assertInstance.ok(e.message.includes(rangeError.message)); @@ -123,108 +136,343 @@ test('Assert class strict', () => { const assertInstance = new Assert(); assertInstance.equal(assertInstance.equal, assertInstance.strictEqual); - assertInstance.equal(assertInstance.deepEqual, assertInstance.deepStrictEqual); + assertInstance.equal( + assertInstance.deepEqual, + assertInstance.deepStrictEqual + ); assertInstance.equal(assertInstance.notEqual, assertInstance.notStrictEqual); - assertInstance.equal(assertInstance.notDeepEqual, assertInstance.notDeepStrictEqual); + assertInstance.equal( + assertInstance.notDeepEqual, + assertInstance.notDeepStrictEqual + ); }); 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"/, - } - ); + assert.throws(() => new Assert({ diff: 'invalid' }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + message: /must be one of "simple", "full"/, + }); }); +const longLinesOfAs = 'A\n'.repeat(100); +const longLinesOFBs = 'B\n'.repeat(100); +const truncatedAs = 'A\\n'.repeat(10) + '...'; +const truncatedBs = 'B\\n'.repeat(10) + '...'; + +const longStringOfAs = 'A'.repeat(10_000); +const longStringOfBs = 'B'.repeat(10_000); + +const longLinesOfAsWithEllipsis = longStringOfAs.substring(0, 9_488) + '...'; +const longLinesOFBsWithEllipsis = longStringOfBs.substring(0, 9_488) + '...'; test('Assert class non strict with full diff', () => { const assertInstance = new Assert({ diff: 'full', strict: false }); - 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; - }); + // long strings + { + assertInstance.throws( + () => { + assertInstance.strictEqual(longStringOfAs, longStringOfBs); + }, + (err) => { + assertInstance.strictEqual(err.code, 'ERR_ASSERTION'); + assertInstance.strictEqual(err.operator, 'strictEqual'); + assertInstance.strictEqual(err.diff, 'full'); + assertInstance.strictEqual(err.actual, longStringOfAs); + assertInstance.strictEqual(err.expected, longStringOfBs); - 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.strictEqual( + err.message, + `Expected values to be strictly equal:\n+ actual - expected\n\n` + + `+ '${longStringOfAs}'\n- '${longStringOfBs}'\n` + ); + assertInstance.ok( + inspect(err).includes(`actual: '${longLinesOfAsWithEllipsis}'`) + ); + assertInstance.ok( + inspect(err).includes(`expected: '${longLinesOFBsWithEllipsis}'`) + ); + return true; + } + ); + + assertInstance.throws( + () => { + assertInstance.notStrictEqual(longStringOfAs, longStringOfAs); + }, + (err) => { + assertInstance.strictEqual(err.code, 'ERR_ASSERTION'); + assertInstance.strictEqual(err.operator, 'notStrictEqual'); + assertInstance.strictEqual(err.diff, 'full'); + assertInstance.strictEqual(err.actual, longStringOfAs); + assertInstance.strictEqual(err.expected, longStringOfAs); - 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.strictEqual( + err.message, + `Expected "actual" to be strictly unequal to:\n\n` + + `'${longStringOfAs}'` + ); + assertInstance.ok( + inspect(err).includes(`actual: '${longLinesOfAsWithEllipsis}'`) + ); + assertInstance.ok( + inspect(err).includes(`expected: '${longLinesOfAsWithEllipsis}'`) + ); + return true; + } ); - assertInstance.ok(inspect(err).includes(`actual: '${longStringOfAs}'`)); - assertInstance.ok(inspect(err).includes(`expected: '${longStringOFBs}'`)); - return true; - }); + + assertInstance.throws( + () => { + assertInstance.deepEqual(longStringOfAs, longStringOfBs); + }, + (err) => { + assertInstance.strictEqual(err.code, 'ERR_ASSERTION'); + assertInstance.strictEqual(err.operator, 'deepEqual'); + assertInstance.strictEqual(err.diff, 'full'); + assertInstance.strictEqual(err.actual, longStringOfAs); + assertInstance.strictEqual(err.expected, longStringOfBs); + + 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: '${longLinesOfAsWithEllipsis}'`) + ); + assertInstance.ok( + inspect(err).includes(`expected: '${longLinesOFBsWithEllipsis}'`) + ); + return true; + } + ); + } + + // long lines + { + assertInstance.throws( + () => { + assertInstance.strictEqual(longLinesOfAs, longLinesOFBs); + }, + (err) => { + assertInstance.strictEqual(err.code, 'ERR_ASSERTION'); + assertInstance.strictEqual(err.operator, 'strictEqual'); + assertInstance.strictEqual(err.diff, 'full'); + assertInstance.strictEqual(err.actual, longLinesOfAs); + assertInstance.strictEqual(err.expected, longLinesOFBs); + + assertInstance.strictEqual(err.message.split('\n').length, 204); + assertInstance.strictEqual(err.actual.split('\n').length, 101); + assertInstance.ok( + err.message.includes('Expected values to be strictly equal') + ); + assertInstance.ok(inspect(err).includes(`actual: '${truncatedAs}`)); + assertInstance.ok(inspect(err).includes(`expected: '${truncatedBs}`)); + return true; + } + ); + + assertInstance.throws( + () => { + assertInstance.notStrictEqual(longLinesOfAs, longLinesOfAs); + }, + (err) => { + assertInstance.strictEqual(err.code, 'ERR_ASSERTION'); + assertInstance.strictEqual(err.operator, 'notStrictEqual'); + assertInstance.strictEqual(err.diff, 'full'); + assertInstance.strictEqual(err.actual, longLinesOfAs); + assertInstance.strictEqual(err.expected, longLinesOfAs); + + assertInstance.strictEqual(err.message.split('\n').length, 103); + assertInstance.strictEqual(err.actual.split('\n').length, 101); + assertInstance.ok( + err.message.includes(`Expected "actual" to be strictly unequal to:`) + ); + assertInstance.ok(inspect(err).includes(`actual: '${truncatedAs}`)); + assertInstance.ok(inspect(err).includes(`expected: '${truncatedAs}`)); + return true; + } + ); + + assertInstance.throws( + () => { + assertInstance.deepEqual(longLinesOfAs, longLinesOFBs); + }, + (err) => { + assertInstance.strictEqual(err.code, 'ERR_ASSERTION'); + assertInstance.strictEqual(err.operator, 'deepEqual'); + assertInstance.strictEqual(err.diff, 'full'); + assertInstance.strictEqual(err.actual, longLinesOfAs); + assertInstance.strictEqual(err.expected, longLinesOFBs); + + assertInstance.strictEqual(err.message.split('\n').length, 205); + assertInstance.strictEqual(err.actual.split('\n').length, 101); + assertInstance.ok( + err.message.includes(`Expected values to be loosely deep-equal:`) + ); + assertInstance.ok(inspect(err).includes(`actual: '${truncatedAs}`)); + assertInstance.ok(inspect(err).includes(`expected: '${truncatedBs}`)); + return true; + } + ); + } }); test('Assert class non strict with simple diff', () => { const assertInstance = new Assert({ diff: 'simple', strict: false }); - 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; - }); + // long strings + { + assertInstance.throws( + () => { + assertInstance.strictEqual(longStringOfAs, longStringOfBs); + }, + (err) => { + assertInstance.strictEqual(err.code, 'ERR_ASSERTION'); + assertInstance.strictEqual(err.operator, 'strictEqual'); + assertInstance.strictEqual(err.diff, 'simple'); + assertInstance.strictEqual(err.actual, longStringOfAs); + assertInstance.strictEqual(err.expected, longStringOfBs); - 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.strictEqual( + err.message, + `Expected values to be strictly equal:\n+ actual - expected\n\n` + + `+ '${longStringOfAs}'\n- '${longStringOfBs}'\n` + ); + assertInstance.ok( + inspect(err).includes(`actual: '${longLinesOfAsWithEllipsis}'`) + ); + assertInstance.ok( + inspect(err).includes(`expected: '${longLinesOFBsWithEllipsis}'`) + ); + return true; + } + ); + + assertInstance.throws( + () => { + assertInstance.notStrictEqual(longStringOfAs, longStringOfAs); + }, + (err) => { + assertInstance.strictEqual(err.code, 'ERR_ASSERTION'); + assertInstance.strictEqual(err.operator, 'notStrictEqual'); + assertInstance.strictEqual(err.diff, 'simple'); + assertInstance.strictEqual(err.actual, longStringOfAs); + assertInstance.strictEqual(err.expected, longStringOfAs); - 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.strictEqual( + err.message, + `Expected "actual" to be strictly unequal to:\n\n` + + `'${longStringOfAs}'` + ); + assertInstance.ok( + inspect(err).includes(`actual: '${longLinesOfAsWithEllipsis}'`) + ); + assertInstance.ok( + inspect(err).includes(`expected: '${longLinesOfAsWithEllipsis}'`) + ); + return true; + } ); - 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.deepEqual(longStringOfAs, longStringOfBs); + }, + (err) => { + assertInstance.strictEqual(err.code, 'ERR_ASSERTION'); + assertInstance.strictEqual(err.operator, 'deepEqual'); + assertInstance.strictEqual(err.diff, 'simple'); + assertInstance.strictEqual(err.actual, longStringOfAs); + assertInstance.strictEqual(err.expected, longStringOfBs); + + assertInstance.strictEqual( + err.message, + `Expected values to be loosely deep-equal:\n\n` + + `'${ + longStringOfAs.substring(0, 508) + '...' + }\n\nshould loosely deep-equal\n\n'${ + longStringOfBs.substring(0, 508) + '...' + }` + ); + assertInstance.ok( + inspect(err).includes(`actual: '${longLinesOfAsWithEllipsis}'`) + ); + assertInstance.ok( + inspect(err).includes(`expected: '${longLinesOFBsWithEllipsis}'`) + ); + return true; + } + ); + } + + // long lines + { + assertInstance.throws( + () => { + assertInstance.strictEqual(longLinesOfAs, longLinesOFBs); + }, + (err) => { + assertInstance.strictEqual(err.code, 'ERR_ASSERTION'); + assertInstance.strictEqual(err.operator, 'strictEqual'); + assertInstance.strictEqual(err.diff, 'simple'); + assertInstance.strictEqual(err.actual, longLinesOfAs); + assertInstance.strictEqual(err.expected, longLinesOFBs); + assertInstance.strictEqual(err.message.split('\n').length, 204); + assertInstance.strictEqual(err.actual.split('\n').length, 101); + + assertInstance.ok( + err.message.includes('Expected values to be strictly equal') + ); + assertInstance.ok(inspect(err).includes(`actual: '${truncatedAs}`)); + assertInstance.ok(inspect(err).includes(`expected: '${truncatedBs}`)); + return true; + } + ); + + assertInstance.throws( + () => { + assertInstance.notStrictEqual(longLinesOfAs, longLinesOfAs); + }, + (err) => { + assertInstance.strictEqual(err.code, 'ERR_ASSERTION'); + assertInstance.strictEqual(err.operator, 'notStrictEqual'); + assertInstance.strictEqual(err.diff, 'simple'); + assertInstance.strictEqual(err.actual, longLinesOfAs); + assertInstance.strictEqual(err.expected, longLinesOfAs); + + assertInstance.strictEqual(err.message.split('\n').length, 50); + assertInstance.strictEqual(err.actual.split('\n').length, 101); + assertInstance.ok( + err.message.includes(`Expected "actual" to be strictly unequal to:`) + ); + assertInstance.ok(inspect(err).includes(`actual: '${truncatedAs}`)); + assertInstance.ok(inspect(err).includes(`expected: '${truncatedAs}`)); + return true; + } + ); + + assertInstance.throws( + () => { + assertInstance.deepEqual(longLinesOfAs, longLinesOFBs); + }, + (err) => { + assertInstance.strictEqual(err.code, 'ERR_ASSERTION'); + assertInstance.strictEqual(err.operator, 'deepEqual'); + assertInstance.strictEqual(err.diff, 'simple'); + assertInstance.strictEqual(err.actual, longLinesOfAs); + assertInstance.strictEqual(err.expected, longLinesOFBs); + + assertInstance.strictEqual(err.message.split('\n').length, 109); + assertInstance.strictEqual(err.actual.split('\n').length, 101); + assertInstance.ok( + err.message.includes(`Expected values to be loosely deep-equal:`) + ); + assertInstance.ok(inspect(err).includes(`actual: '${truncatedAs}`)); + assertInstance.ok(inspect(err).includes(`expected: '${truncatedBs}`)); + return true; + } + ); + } });