lib: restructure assert to become a class

PR-URL: https://github.com/nodejs/node/pull/58253
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
Miguel Marcondes Filho 2025-08-05 11:15:02 -03:00 committed by GitHub
parent 3090def635
commit 4f5d11e6fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 823 additions and 49 deletions

View file

@ -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,6 +217,51 @@ try {
}
```
## Class: `assert.Assert`
<!-- YAML
added: REPLACEME
-->
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'`.
* `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.
```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.
```
**Important**: When destructuring assertion methods from an `Assert` instance,
the methods lose their connection to the instance's configuration options (such as `diff` and `strict` settings).
The destructured methods will fall back to default behavior instead.
```js
const myAssert = new Assert({ diff: 'full' });
// This works as expected - uses 'full' diff
myAssert.strictEqual({ a: 1 }, { b: { c: 1 } });
// This loses the 'full' diff setting - falls back to default 'simple' diff
const { strictEqual } = myAssert;
strictEqual({ a: 1 }, { b: { c: 1 } });
```
When destructured, methods lose access to the instance's `this` context and revert to default assertion behavior
(diff: 'simple', non-strict mode).
To maintain custom options when using destructured methods, avoid
destructuring and call methods directly on the instance.
## `assert(value[, message])`
<!-- YAML

View file

@ -21,6 +21,7 @@
'use strict';
const {
ArrayPrototypeForEach,
ArrayPrototypeIndexOf,
ArrayPrototypeJoin,
ArrayPrototypePush,
@ -28,6 +29,7 @@ const {
Error,
NumberIsNaN,
ObjectAssign,
ObjectDefineProperty,
ObjectIs,
ObjectKeys,
ObjectPrototypeIsPrototypeOf,
@ -37,11 +39,13 @@ const {
StringPrototypeIndexOf,
StringPrototypeSlice,
StringPrototypeSplit,
Symbol,
} = primordials;
const {
codes: {
ERR_AMBIGUOUS_ARGUMENT,
ERR_CONSTRUCT_CALL_REQUIRED,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_RETURN_VALUE,
@ -54,13 +58,16 @@ const {
isPromise,
isRegExp,
} = require('internal/util/types');
const { isError } = require('internal/util');
const { isError, setOwnProperty } = require('internal/util');
const { innerOk } = require('internal/assert/utils');
const {
validateFunction,
validateOneOf,
} = require('internal/validators');
const kOptions = Symbol('options');
let isDeepEqual;
let isDeepStrictEqual;
let isPartialStrictEqual;
@ -80,12 +87,60 @@ const assert = module.exports = ok;
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.
*/
/**
* @class Assert
* @param {AssertOptions} [options] - Optional configuration for assertions.
* @throws {ERR_CONSTRUCT_CALL_REQUIRED} If not called with `new`.
*/
function Assert(options) {
if (!new.target) {
throw new ERR_CONSTRUCT_CALL_REQUIRED('Assert');
}
options = ObjectAssign({ __proto__: null, strict: true }, options);
const allowedDiffs = ['simple', 'full'];
if (options.diff !== undefined) {
validateOneOf(options.diff, 'options.diff', allowedDiffs);
}
this.AssertionError = AssertionError;
ObjectDefineProperty(this, kOptions, {
__proto__: null,
value: options,
enumerable: false,
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
// 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.
// DESTRUCTURING WARNING: All Assert.prototype methods use optional chaining
// (this?.[kOptions]) to safely access instance configuration. When methods are
// destructured from an Assert instance (e.g., const {strictEqual} = myAssert),
// they lose their `this` context and will use default behavior instead of the
// instance's custom options.
function innerFail(obj) {
if (obj.message instanceof Error) throw obj.message;
@ -96,7 +151,7 @@ function innerFail(obj) {
* Throws an AssertionError with the given message.
* @param {any | Error} [message]
*/
function fail(message) {
Assert.prototype.fail = function fail(message) {
if (isError(message)) throw message;
let internalMessage = false;
@ -105,19 +160,22 @@ function fail(message) {
internalMessage = true;
}
// IMPORTANT: When adding new references to `this`, ensure they use optional chaining
// (this?.[kOptions]?.diff) to handle cases where the method is destructured from an
// Assert instance and loses its context. Destructured methods will fall back
// to default behavior when `this` is undefined.
const errArgs = {
operator: 'fail',
stackStartFn: fail,
message,
diff: this?.[kOptions]?.diff,
};
const err = new AssertionError(errArgs);
if (internalMessage) {
err.generatedMessage = true;
}
throw err;
}
assert.fail = fail;
};
// The AssertionError is defined in internal/error.
assert.AssertionError = AssertionError;
@ -131,7 +189,17 @@ assert.AssertionError = AssertionError;
function ok(...args) {
innerOk(ok, args.length, ...args);
}
assert.ok = ok;
/**
* Pure assertion tests whether a value is truthy, as determined
* by !!value.
* Duplicated as the other `ok` function is supercharged and exposed as default export.
* @param {...any} args
* @returns {void}
*/
Assert.prototype.ok = function ok(...args) {
innerOk(ok, args.length, ...args);
};
/**
* The equality assertion tests shallow, coercive equality with ==.
@ -140,8 +208,7 @@ assert.ok = ok;
* @param {string | Error} [message]
* @returns {void}
*/
/* eslint-disable no-restricted-properties */
assert.equal = function equal(actual, expected, message) {
Assert.prototype.equal = function equal(actual, expected, message) {
if (arguments.length < 2) {
throw new ERR_MISSING_ARGS('actual', 'expected');
}
@ -153,6 +220,7 @@ assert.equal = function equal(actual, expected, message) {
message,
operator: '==',
stackStartFn: equal,
diff: this?.[kOptions]?.diff,
});
}
};
@ -165,7 +233,7 @@ assert.equal = function equal(actual, expected, message) {
* @param {string | Error} [message]
* @returns {void}
*/
assert.notEqual = function notEqual(actual, expected, message) {
Assert.prototype.notEqual = function notEqual(actual, expected, message) {
if (arguments.length < 2) {
throw new ERR_MISSING_ARGS('actual', 'expected');
}
@ -177,6 +245,7 @@ assert.notEqual = function notEqual(actual, expected, message) {
message,
operator: '!=',
stackStartFn: notEqual,
diff: this?.[kOptions]?.diff,
});
}
};
@ -188,7 +257,7 @@ assert.notEqual = function notEqual(actual, expected, message) {
* @param {string | Error} [message]
* @returns {void}
*/
assert.deepEqual = function deepEqual(actual, expected, message) {
Assert.prototype.deepEqual = function deepEqual(actual, expected, message) {
if (arguments.length < 2) {
throw new ERR_MISSING_ARGS('actual', 'expected');
}
@ -200,6 +269,7 @@ assert.deepEqual = function deepEqual(actual, expected, message) {
message,
operator: 'deepEqual',
stackStartFn: deepEqual,
diff: this?.[kOptions]?.diff,
});
}
};
@ -211,7 +281,7 @@ assert.deepEqual = function deepEqual(actual, expected, message) {
* @param {string | Error} [message]
* @returns {void}
*/
assert.notDeepEqual = function notDeepEqual(actual, expected, message) {
Assert.prototype.notDeepEqual = function notDeepEqual(actual, expected, message) {
if (arguments.length < 2) {
throw new ERR_MISSING_ARGS('actual', 'expected');
}
@ -223,10 +293,10 @@ assert.notDeepEqual = function notDeepEqual(actual, expected, message) {
message,
operator: 'notDeepEqual',
stackStartFn: notDeepEqual,
diff: this?.[kOptions]?.diff,
});
}
};
/* eslint-enable */
/**
* The deep strict equivalence assertion tests a deep strict equality
@ -236,7 +306,7 @@ assert.notDeepEqual = function notDeepEqual(actual, expected, message) {
* @param {string | Error} [message]
* @returns {void}
*/
assert.deepStrictEqual = function deepStrictEqual(actual, expected, message) {
Assert.prototype.deepStrictEqual = function deepStrictEqual(actual, expected, message) {
if (arguments.length < 2) {
throw new ERR_MISSING_ARGS('actual', 'expected');
}
@ -248,6 +318,7 @@ assert.deepStrictEqual = function deepStrictEqual(actual, expected, message) {
message,
operator: 'deepStrictEqual',
stackStartFn: deepStrictEqual,
diff: this?.[kOptions]?.diff,
});
}
};
@ -260,7 +331,7 @@ assert.deepStrictEqual = function deepStrictEqual(actual, expected, message) {
* @param {string | Error} [message]
* @returns {void}
*/
assert.notDeepStrictEqual = notDeepStrictEqual;
Assert.prototype.notDeepStrictEqual = notDeepStrictEqual;
function notDeepStrictEqual(actual, expected, message) {
if (arguments.length < 2) {
throw new ERR_MISSING_ARGS('actual', 'expected');
@ -273,6 +344,7 @@ function notDeepStrictEqual(actual, expected, message) {
message,
operator: 'notDeepStrictEqual',
stackStartFn: notDeepStrictEqual,
diff: this?.[kOptions]?.diff,
});
}
}
@ -284,7 +356,7 @@ function notDeepStrictEqual(actual, expected, message) {
* @param {string | Error} [message]
* @returns {void}
*/
assert.strictEqual = function strictEqual(actual, expected, message) {
Assert.prototype.strictEqual = function strictEqual(actual, expected, message) {
if (arguments.length < 2) {
throw new ERR_MISSING_ARGS('actual', 'expected');
}
@ -295,6 +367,7 @@ assert.strictEqual = function strictEqual(actual, expected, message) {
message,
operator: 'strictEqual',
stackStartFn: strictEqual,
diff: this?.[kOptions]?.diff,
});
}
};
@ -306,7 +379,7 @@ assert.strictEqual = function strictEqual(actual, expected, message) {
* @param {string | Error} [message]
* @returns {void}
*/
assert.notStrictEqual = function notStrictEqual(actual, expected, message) {
Assert.prototype.notStrictEqual = function notStrictEqual(actual, expected, message) {
if (arguments.length < 2) {
throw new ERR_MISSING_ARGS('actual', 'expected');
}
@ -317,6 +390,7 @@ assert.notStrictEqual = function notStrictEqual(actual, expected, message) {
message,
operator: 'notStrictEqual',
stackStartFn: notStrictEqual,
diff: this?.[kOptions]?.diff,
});
}
};
@ -328,7 +402,7 @@ assert.notStrictEqual = function notStrictEqual(actual, expected, message) {
* @param {string | Error} [message]
* @returns {void}
*/
assert.partialDeepStrictEqual = function partialDeepStrictEqual(
Assert.prototype.partialDeepStrictEqual = function partialDeepStrictEqual(
actual,
expected,
message,
@ -344,6 +418,7 @@ assert.partialDeepStrictEqual = function partialDeepStrictEqual(
message,
operator: 'partialDeepStrictEqual',
stackStartFn: partialDeepStrictEqual,
diff: this?.[kOptions]?.diff,
});
}
};
@ -377,6 +452,7 @@ function compareExceptionKey(actual, expected, key, message, keys, fn) {
expected: b,
operator: 'deepStrictEqual',
stackStartFn: fn,
diff: this?.[kOptions]?.diff,
});
err.actual = actual;
err.expected = expected;
@ -389,6 +465,7 @@ function compareExceptionKey(actual, expected, key, message, keys, fn) {
message,
operator: fn.name,
stackStartFn: fn,
diff: this?.[kOptions]?.diff,
});
}
}
@ -418,6 +495,7 @@ function expectedException(actual, expected, message, fn) {
message,
operator: 'deepStrictEqual',
stackStartFn: fn,
diff: this?.[kOptions]?.diff,
});
err.operator = fn.name;
throw err;
@ -493,6 +571,7 @@ function expectedException(actual, expected, message, fn) {
message,
operator: fn.name,
stackStartFn: fn,
diff: this?.[kOptions]?.diff,
});
err.generatedMessage = generatedMessage;
throw err;
@ -580,20 +659,21 @@ function expectsError(stackStartFn, actual, error, message) {
details += ` (${error.name})`;
}
details += message ? `: ${message}` : '.';
const fnType = stackStartFn === assert.rejects ? 'rejection' : 'exception';
const fnType = stackStartFn === Assert.prototype.rejects ? 'rejection' : 'exception';
innerFail({
actual: undefined,
expected: error,
operator: stackStartFn.name,
message: `Missing expected ${fnType}${details}`,
stackStartFn,
diff: this?.[kOptions]?.diff,
});
}
if (!error)
return;
expectedException(actual, error, message, stackStartFn);
expectedException.call(this, actual, error, message, stackStartFn);
}
function hasMatchingError(actual, expected) {
@ -627,7 +707,7 @@ function expectsNoError(stackStartFn, actual, error, message) {
if (!error || hasMatchingError(actual, error)) {
const details = message ? `: ${message}` : '.';
const fnType = stackStartFn === assert.doesNotReject ?
const fnType = stackStartFn === Assert.prototype.doesNotReject ?
'rejection' : 'exception';
innerFail({
actual,
@ -636,6 +716,7 @@ function expectsNoError(stackStartFn, actual, error, message) {
message: `Got unwanted ${fnType}${details}\n` +
`Actual message: "${actual?.message}"`,
stackStartFn,
diff: this?.[kOptions]?.diff,
});
}
throw actual;
@ -647,7 +728,7 @@ function expectsNoError(stackStartFn, actual, error, message) {
* @param {...any} [args]
* @returns {void}
*/
assert.throws = function throws(promiseFn, ...args) {
Assert.prototype.throws = function throws(promiseFn, ...args) {
expectsError(throws, getActual(promiseFn), ...args);
};
@ -657,7 +738,7 @@ assert.throws = function throws(promiseFn, ...args) {
* @param {...any} [args]
* @returns {Promise<void>}
*/
assert.rejects = async function rejects(promiseFn, ...args) {
Assert.prototype.rejects = async function rejects(promiseFn, ...args) {
expectsError(rejects, await waitForActual(promiseFn), ...args);
};
@ -667,7 +748,7 @@ assert.rejects = async function rejects(promiseFn, ...args) {
* @param {...any} [args]
* @returns {void}
*/
assert.doesNotThrow = function doesNotThrow(fn, ...args) {
Assert.prototype.doesNotThrow = function doesNotThrow(fn, ...args) {
expectsNoError(doesNotThrow, getActual(fn), ...args);
};
@ -677,7 +758,7 @@ assert.doesNotThrow = function doesNotThrow(fn, ...args) {
* @param {...any} [args]
* @returns {Promise<void>}
*/
assert.doesNotReject = async function doesNotReject(fn, ...args) {
Assert.prototype.doesNotReject = async function doesNotReject(fn, ...args) {
expectsNoError(doesNotReject, await waitForActual(fn), ...args);
};
@ -686,7 +767,7 @@ assert.doesNotReject = async function doesNotReject(fn, ...args) {
* @param {any} err
* @returns {void}
*/
assert.ifError = function ifError(err) {
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') {
@ -705,6 +786,7 @@ assert.ifError = function ifError(err) {
operator: 'ifError',
message,
stackStartFn: ifError,
diff: this?.[kOptions]?.diff,
});
// Make sure we actually have a stack trace!
@ -747,7 +829,7 @@ function internalMatch(string, regexp, message, fn) {
'regexp', 'RegExp', regexp,
);
}
const match = fn === assert.match;
const match = fn === Assert.prototype.match;
if (typeof string !== 'string' ||
RegExpPrototypeExec(regexp, string) !== null !== match) {
if (message instanceof Error) {
@ -770,6 +852,7 @@ function internalMatch(string, regexp, message, fn) {
message,
operator: fn.name,
stackStartFn: fn,
diff: this?.[kOptions]?.diff,
});
err.generatedMessage = generatedMessage;
throw err;
@ -783,7 +866,7 @@ function internalMatch(string, regexp, message, fn) {
* @param {string | Error} [message]
* @returns {void}
*/
assert.match = function match(string, regexp, message) {
Assert.prototype.match = function match(string, regexp, message) {
internalMatch(string, regexp, message, match);
};
@ -794,7 +877,7 @@ assert.match = function match(string, regexp, message) {
* @param {string | Error} [message]
* @returns {void}
*/
assert.doesNotMatch = function doesNotMatch(string, regexp, message) {
Assert.prototype.doesNotMatch = function doesNotMatch(string, regexp, message) {
internalMatch(string, regexp, message, doesNotMatch);
};
@ -807,6 +890,17 @@ function strict(...args) {
innerOk(strict, args.length, ...args);
}
// TODO(aduh95): take `ok` from `Assert.prototype` instead of a self-ref in a next major.
assert.ok = assert;
ArrayPrototypeForEach([
'fail', 'equal', 'notEqual', 'deepEqual', 'notDeepEqual',
'deepStrictEqual', 'notDeepStrictEqual', 'strictEqual',
'notStrictEqual', 'partialDeepStrictEqual', 'match', 'doesNotMatch',
'throws', 'rejects', 'doesNotThrow', 'doesNotReject', 'ifError',
], (name) => {
setOwnProperty(assert, name, Assert.prototype[name]);
});
assert.strict = ObjectAssign(strict, assert, {
equal: assert.strictEqual,
deepEqual: assert.deepStrictEqual,
@ -814,4 +908,7 @@ assert.strict = ObjectAssign(strict, assert, {
notDeepEqual: assert.notDeepStrictEqual,
});
assert.strict.Assert = Assert;
assert.strict.strict = assert.strict;
assert.Assert = Assert;

View file

@ -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 {
@ -252,6 +252,7 @@ class AssertionError extends Error {
details,
// Compatibility with older versions.
stackStartFunction,
diff = 'simple',
} = options;
let {
actual,
@ -263,7 +264,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 +284,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 +301,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 +320,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 +378,7 @@ class AssertionError extends Error {
this.stack; // eslint-disable-line no-unused-expressions
// Reset the name.
this.name = 'AssertionError';
this.diff = diff;
}
toString() {

View file

@ -1161,6 +1161,7 @@ E('ERR_CHILD_PROCESS_STDIO_MAXBUFFER', '%s maxBuffer length exceeded',
RangeError);
E('ERR_CONSOLE_WRITABLE_STREAM',
'Console expects a writable stream instance for %s', TypeError);
E('ERR_CONSTRUCT_CALL_REQUIRED', 'Class constructor %s cannot be invoked without `new`', TypeError);
E('ERR_CONTEXT_NOT_INITIALIZED', 'context used is not initialized', Error);
E('ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED',
'Custom engines not supported by this OpenSSL', Error);

View file

@ -12,7 +12,8 @@ AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
code: 'ERR_ASSERTION',
actual: 1,
expected: 2,
operator: 'strictEqual'
operator: 'strictEqual',
diff: 'simple'
}
Node.js *

View file

@ -19,7 +19,8 @@ AssertionError [ERR_ASSERTION]: ifError got unwanted exception: test error
at a (*if-error-has-good-stack.js:*:*)
at Object.<anonymous> (*if-error-has-good-stack.js:*:*),
expected: null,
operator: 'ifError'
operator: 'ifError',
diff: 'simple'
}
Node.js *

View file

@ -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
}

View file

@ -66,7 +66,8 @@ Failed tests:
code: 'ERR_ASSERTION',
actual: true,
expected: false,
operator: 'strictEqual'
operator: 'strictEqual',
diff: 'simple'
}
✖ reject fail (*ms)
Error: rejected from reject fail
@ -215,7 +216,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'

View file

@ -124,7 +124,8 @@ true !== false
code: 'ERR_ASSERTION',
actual: true,
expected: false,
operator: 'strictEqual'
operator: 'strictEqual',
diff: 'simple'
}
}
</failure>
@ -491,7 +492,8 @@ should loosely deep-equal
code: 'ERR_ASSERTION',
actual: [Object],
expected: [Object],
operator: 'deepEqual'
operator: 'deepEqual',
diff: 'simple'
}
}
</failure>

View file

@ -171,7 +171,8 @@
code: 'ERR_ASSERTION',
actual: true,
expected: false,
operator: 'strictEqual'
operator: 'strictEqual',
diff: 'simple'
}
*
@ -356,7 +357,8 @@
code: 'ERR_ASSERTION',
actual: [Object],
expected: [Object],
operator: 'deepEqual'
operator: 'deepEqual',
diff: 'simple'
}
*

View file

@ -174,7 +174,8 @@
code: 'ERR_ASSERTION',
actual: true,
expected: false,
operator: 'strictEqual'
operator: 'strictEqual',
diff: 'simple'
}
*
@ -359,7 +360,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: <ref *1> { bar: 2, c: [Circular *1] } },
operator: 'deepEqual'
operator: 'deepEqual',
diff: 'simple'
}
*

View file

@ -9,6 +9,7 @@ AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
- Comparison {
- bar: true
- }
at Object.<anonymous> (*assert_throws_stack.js:*:*)
at *
at *
@ -32,7 +33,8 @@ AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
at *
at *,
expected: { bar: true },
operator: 'throws'
operator: 'throws',
diff: 'simple'
}
Node.js *

View file

@ -0,0 +1,133 @@
'use strict';
require('../common');
const assert = require('assert');
const { Assert } = require('assert');
const { test } = require('node:test');
// Disable colored output to prevent color codes from breaking assertion
// message comparisons. This should only be an issue when process.stdout
// is a TTY.
if (process.stdout.isTTY) {
process.env.NODE_DISABLE_COLORS = '1';
}
test('Assert class destructuring behavior - diff option', () => {
const assertInstanceFull = new Assert({ diff: 'full' });
const assertInstanceSimple = new Assert({ diff: 'simple' });
assertInstanceFull.throws(
() => assertInstanceFull.strictEqual({ a: 1 }, { a: 2 }),
(err) => {
assert.strictEqual(err.diff, 'full');
return true;
}
);
assertInstanceSimple.throws(
() => assertInstanceSimple.strictEqual({ a: 1 }, { a: 2 }),
(err) => {
assert.strictEqual(err.diff, 'simple');
return true;
}
);
const { strictEqual: strictEqualSimple } = assertInstanceSimple;
const { strictEqual: strictEqualFull } = assertInstanceFull;
const { deepStrictEqual: deepStrictEqualFull } = assertInstanceFull;
const { equal: equalFull } = assertInstanceFull;
assert.throws(
() => strictEqualSimple({ a: 1 }, { a: 2 }),
(err) => {
assert.strictEqual(err.diff, 'simple');
return true;
}
);
assert.throws(
() => strictEqualFull({ a: 1 }, { a: 2 }),
(err) => {
assert.strictEqual(err.diff, 'simple');
return true;
}
);
assert.throws(
() => deepStrictEqualFull({ a: 1 }, { a: 2 }),
(err) => {
assert.strictEqual(err.diff, 'simple');
return true;
}
);
assert.throws(
() => equalFull({ a: 1 }, { a: 2 }),
(err) => {
assert.strictEqual(err.diff, 'simple');
return true;
}
);
});
test('Assert class destructuring behavior - strict option', () => {
const assertInstanceNonStrict = new Assert({ strict: false });
const assertInstanceStrict = new Assert({ strict: true });
assertInstanceNonStrict.equal(2, '2');
assert.throws(
() => assertInstanceStrict.equal(2, '2'),
assert.AssertionError
);
const { equal: equalNonStrict } = assertInstanceNonStrict;
const { equal: equalStrict } = assertInstanceStrict;
equalNonStrict(2, '2');
assert.throws(
() => equalStrict(2, '2'),
assert.AssertionError
);
});
test('Assert class destructuring behavior - comprehensive methods', () => {
const myAssert = new Assert({ diff: 'full', strict: false });
const {
fail,
equal,
strictEqual,
deepStrictEqual,
throws,
match,
doesNotMatch
} = myAssert;
assert.throws(() => fail('test message'), (err) => {
assert.strictEqual(err.diff, 'simple');
assert.strictEqual(err.message, 'test message');
return true;
});
assert.throws(() => equal({ a: 1 }, { a: 2 }), (err) => {
assert.strictEqual(err.diff, 'simple');
return true;
});
assert.throws(() => strictEqual({ a: 1 }, { a: 2 }), (err) => {
assert.strictEqual(err.diff, 'simple');
return true;
});
assert.throws(() => deepStrictEqual({ a: 1 }, { a: 2 }), (err) => {
assert.strictEqual(err.diff, 'simple');
return true;
});
throws(() => { throw new Error('test'); }, Error);
match('hello world', /world/);
doesNotMatch('hello world', /xyz/);
});

View file

@ -0,0 +1,480 @@
'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 constructor requires new', () => {
assert.throws(() => Assert(), {
code: 'ERR_CONSTRUCT_CALL_REQUIRED',
name: 'TypeError',
});
});
test('Assert class non strict', () => {
const assertInstance = new Assert({ diff: undefined, strict: false });
assertInstance.ok(
assert.AssertionError.prototype instanceof Error,
'assert.AssertionError instanceof Error'
);
assert.strictEqual(typeof assertInstance.ok, 'function');
assert.strictEqual(assertInstance.ok.strictEqual, undefined);
assert.strictEqual(typeof assertInstance.strictEqual, 'function');
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.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.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 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' }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
message: "The property 'options.diff' must be one of: 'simple', 'full'. Received 'invalid'",
});
});
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 });
// 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.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.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.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 });
// 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.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.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.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;
}
);
}
});

View file

@ -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();