Skip to content

Commit 32e43c2

Browse files
committed
fixup!
1 parent 591a6e3 commit 32e43c2

File tree

3 files changed

+150
-70
lines changed

3 files changed

+150
-70
lines changed

doc/api/assert.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,27 @@ strict methods. For example, [`assert.deepEqual()`][] will behave like
3939
In strict assertion mode, error messages for objects display a diff. In legacy
4040
assertion mode, error messages for objects display the objects, often truncated.
4141

42+
### Message parameter semantics
43+
44+
For assertion methods that accept an optional `message` parameter, the message
45+
may be provided in one of the following forms:
46+
47+
- **string**: Used as-is. If additional arguments are supplied after the
48+
`message` string, they are treated as printf-like substitutions (see
49+
[`util.format()`][]).
50+
- **Error**: If an `Error` instance is provided as `message`, that error is
51+
thrown directly instead of an `AssertionError`.
52+
- **function**: A function of the form `(actual, expected) => string`. It is
53+
called only when the assertion fails and should return a string to be used as
54+
the error message. Non-string return values are ignored and the default
55+
message is used instead.
56+
57+
If additional arguments are passed along with an `Error` or a function as
58+
`message`, the call is rejected with `ERR_AMBIGUOUS_ARGUMENT`.
59+
60+
If the first item is neither a string, `Error`, nor function, `ERR_INVALID_ARG_TYPE`
61+
is thrown.
62+
4263
To use strict assertion mode:
4364

4465
```mjs
@@ -2357,7 +2378,7 @@ changes:
23572378

23582379
* `actual` {any}
23592380
* `expected` {any}
2360-
* `message` {string|Error}
2381+
* `message` {string|Error|Function}
23612382

23622383
Tests for partial deep equality between the `actual` and `expected` parameters.
23632384
"Deep" equality means that the enumerable "own" properties of child objects

lib/assert.js

Lines changed: 13 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ const {
6060
isRegExp,
6161
} = require('internal/util/types');
6262
const { isError, setOwnProperty } = require('internal/util');
63-
const { innerOk } = require('internal/assert/utils');
63+
const { innerOk, innerFail } = require('internal/assert/utils');
6464

6565
const {
6666
validateFunction,
@@ -144,53 +144,7 @@ function Assert(options) {
144144
// they lose their `this` context and will use default behavior instead of the
145145
// instance's custom options.
146146

147-
function innerFail(obj) {
148-
if (obj.message.length === 0) {
149-
obj.message = undefined;
150-
} else if (typeof obj.message[0] === 'string') {
151-
if (obj.message.length > 1) {
152-
obj.message = format(...obj.message);
153-
} else {
154-
obj.message = obj.message[0];
155-
}
156-
} else if (isError(obj.message[0])) {
157-
if (obj.message.length > 1) {
158-
throw new ERR_AMBIGUOUS_ARGUMENT(
159-
'message',
160-
`The error message was passed as error object "${ErrorPrototypeToString(obj.message[0])}" has trailing arguments that would be ignored.`,
161-
);
162-
}
163-
throw obj.message[0];
164-
} else if (typeof obj.message[0] === 'function') {
165-
if (obj.message.length > 1) {
166-
throw new ERR_AMBIGUOUS_ARGUMENT(
167-
'message',
168-
`The error message with function "${obj.message[0].name || 'anonymous'}" has trailing arguments that would be ignored.`,
169-
);
170-
}
171-
try {
172-
obj.message = obj.message[0](obj.actual, obj.expected);
173-
if (typeof obj.message !== 'string') {
174-
obj.message = undefined;
175-
}
176-
} catch {
177-
// Ignore and use default message instead
178-
obj.message = undefined;
179-
}
180-
} else {
181-
throw new ERR_INVALID_ARG_TYPE(
182-
'message',
183-
['string', 'function'],
184-
obj.message[0],
185-
);
186-
}
187-
188-
const error = new AssertionError(obj);
189-
if (obj.generatedMessage !== undefined) {
190-
error.generatedMessage = obj.generatedMessage;
191-
}
192-
throw error;
193-
}
147+
// See `internal/assert/utils.js` for MessageFactory/MessageTuple/InnerFailOptions typedefs
194148

195149
/**
196150
* Throws an AssertionError with the given message.
@@ -250,7 +204,7 @@ Assert.prototype.ok = function ok(...args) {
250204
* The equality assertion tests shallow, coercive equality with ==.
251205
* @param {any} actual
252206
* @param {any} expected
253-
* @param {string | Error | Function} [message]
207+
* @param {string | Error | MessageFactory} [message]
254208
* @returns {void}
255209
*/
256210
Assert.prototype.equal = function equal(actual, expected, ...message) {
@@ -275,7 +229,7 @@ Assert.prototype.equal = function equal(actual, expected, ...message) {
275229
* equal with !=.
276230
* @param {any} actual
277231
* @param {any} expected
278-
* @param {string | Error | Function} [message]
232+
* @param {string | Error | MessageFactory} [message]
279233
* @returns {void}
280234
*/
281235
Assert.prototype.notEqual = function notEqual(actual, expected, ...message) {
@@ -299,7 +253,7 @@ Assert.prototype.notEqual = function notEqual(actual, expected, ...message) {
299253
* The deep equivalence assertion tests a deep equality relation.
300254
* @param {any} actual
301255
* @param {any} expected
302-
* @param {string | Error | Function} [message]
256+
* @param {string | Error | MessageFactory} [message]
303257
* @returns {void}
304258
*/
305259
Assert.prototype.deepEqual = function deepEqual(actual, expected, ...message) {
@@ -323,7 +277,7 @@ Assert.prototype.deepEqual = function deepEqual(actual, expected, ...message) {
323277
* The deep non-equivalence assertion tests for any deep inequality.
324278
* @param {any} actual
325279
* @param {any} expected
326-
* @param {string | Error | Function} [message]
280+
* @param {string | Error | MessageFactory} [message]
327281
* @returns {void}
328282
*/
329283
Assert.prototype.notDeepEqual = function notDeepEqual(actual, expected, ...message) {
@@ -348,7 +302,7 @@ Assert.prototype.notDeepEqual = function notDeepEqual(actual, expected, ...messa
348302
* relation.
349303
* @param {any} actual
350304
* @param {any} expected
351-
* @param {string | Error | Function} [message]
305+
* @param {string | Error | MessageFactory} [message]
352306
* @returns {void}
353307
*/
354308
Assert.prototype.deepStrictEqual = function deepStrictEqual(actual, expected, ...message) {
@@ -373,7 +327,7 @@ Assert.prototype.deepStrictEqual = function deepStrictEqual(actual, expected, ..
373327
* inequality.
374328
* @param {any} actual
375329
* @param {any} expected
376-
* @param {string | Error | Function} [message]
330+
* @param {string | Error | MessageFactory} [message]
377331
* @returns {void}
378332
*/
379333
Assert.prototype.notDeepStrictEqual = notDeepStrictEqual;
@@ -398,7 +352,7 @@ function notDeepStrictEqual(actual, expected, ...message) {
398352
* The strict equivalence assertion tests a strict equality relation.
399353
* @param {any} actual
400354
* @param {any} expected
401-
* @param {string | Error | Function} [message]
355+
* @param {string | Error | MessageFactory} [message]
402356
* @returns {void}
403357
*/
404358
Assert.prototype.strictEqual = function strictEqual(actual, expected, ...message) {
@@ -421,7 +375,7 @@ Assert.prototype.strictEqual = function strictEqual(actual, expected, ...message
421375
* The strict non-equivalence assertion tests for any strict inequality.
422376
* @param {any} actual
423377
* @param {any} expected
424-
* @param {string | Error | Function} [message]
378+
* @param {string | Error | MessageFactory} [message]
425379
* @returns {void}
426380
*/
427381
Assert.prototype.notStrictEqual = function notStrictEqual(actual, expected, ...message) {
@@ -444,7 +398,7 @@ Assert.prototype.notStrictEqual = function notStrictEqual(actual, expected, ...m
444398
* The strict equivalence assertion test between two objects
445399
* @param {any} actual
446400
* @param {any} expected
447-
* @param {string | Error | Function} [message]
401+
* @param {string | Error | MessageFactory} [message]
448402
* @returns {void}
449403
*/
450404
Assert.prototype.partialDeepStrictEqual = function partialDeepStrictEqual(
@@ -904,7 +858,7 @@ function internalMatch(string, regexp, message, fn) {
904858
* Expects the `string` input to match the regular expression.
905859
* @param {string} string
906860
* @param {RegExp} regexp
907-
* @param {string | Error | Function} [message]
861+
* @param {string | Error | MessageFactory} [message]
908862
* @returns {void}
909863
*/
910864
Assert.prototype.match = function match(string, regexp, ...message) {
@@ -915,7 +869,7 @@ Assert.prototype.match = function match(string, regexp, ...message) {
915869
* Expects the `string` input not to match the regular expression.
916870
* @param {string} string
917871
* @param {RegExp} regexp
918-
* @param {string | Error | Function} [message]
872+
* @param {string | Error | MessageFactory} [message]
919873
* @returns {void}
920874
*/
921875
Assert.prototype.doesNotMatch = function doesNotMatch(string, regexp, ...message) {

lib/internal/assert/utils.js

Lines changed: 115 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33
const {
44
Error,
55
ErrorCaptureStackTrace,
6+
ErrorPrototypeToString,
67
StringPrototypeCharCodeAt,
78
StringPrototypeReplace,
89
} = primordials;
910

1011
const {
12+
codes: { ERR_AMBIGUOUS_ARGUMENT, ERR_INVALID_ARG_TYPE },
1113
isErrorStackTraceLimitWritable,
1214
} = require('internal/errors');
1315
const AssertionError = require('internal/assert/assertion_error');
1416
const { isError } = require('internal/util');
17+
const { format } = require('internal/util/inspect');
1518

1619
const {
1720
getErrorSourceExpression,
@@ -33,6 +36,45 @@ const meta = [
3336

3437
const escapeFn = (str) => meta[StringPrototypeCharCodeAt(str, 0)];
3538

39+
/**
40+
* A function that derives the failure message from the actual and expected values.
41+
* It is invoked only when the assertion fails.
42+
*
43+
* Other return values than a string are ignored.
44+
*
45+
* @callback MessageFactory
46+
* @param {any} actual
47+
* @param {any} expected
48+
* @returns {string}
49+
*/
50+
51+
/**
52+
* Raw message input is always passed internally as a tuple array.
53+
* Accepted shapes:
54+
* - []
55+
* - [string]
56+
* - [string, ...any[]] (printf-like substitutions)
57+
* - [Error]
58+
* - [MessageFactory]
59+
*
60+
* Additional elements after [Error] or [MessageFactory] are rejected with ERR_AMBIGUOUS_ARGUMENT.
61+
* A first element that is neither string, Error nor function is rejected with ERR_INVALID_ARG_TYPE.
62+
*
63+
* @typedef {[] | [string] | [string, ...any[]] | [Error] | [MessageFactory]} MessageTuple
64+
*/
65+
66+
/**
67+
* Options consumed by innerFail to construct and throw the AssertionError.
68+
* @typedef {object} InnerFailOptions
69+
* @property {any} actual
70+
* @property {any} expected
71+
* @property {MessageTuple} message
72+
* @property {string} operator
73+
* @property {Function} stackStartFn
74+
* @property {'simple' | 'full'} [diff]
75+
* @property {boolean} [generatedMessage]
76+
*/
77+
3678
function getErrMessage(fn) {
3779
const tmpLimit = Error.stackTraceLimit;
3880
const errorStackTraceLimitIsWritable = isErrorStackTraceLimitWritable();
@@ -52,32 +94,95 @@ function getErrMessage(fn) {
5294
}
5395
}
5496

55-
function innerOk(fn, argLen, value, message) {
97+
/**
98+
* @param {InnerFailOptions} obj
99+
*/
100+
function innerFail(obj) {
101+
if (obj.message.length === 0) {
102+
obj.message = undefined;
103+
} else if (typeof obj.message[0] === 'string') {
104+
if (obj.message.length > 1) {
105+
obj.message = format(...obj.message);
106+
} else {
107+
obj.message = obj.message[0];
108+
}
109+
} else if (isError(obj.message[0])) {
110+
if (obj.message.length > 1) {
111+
throw new ERR_AMBIGUOUS_ARGUMENT(
112+
'message',
113+
`The error message was passed as error object "${ErrorPrototypeToString(obj.message[0])}" has trailing arguments that would be ignored.`,
114+
);
115+
}
116+
throw obj.message[0];
117+
} else if (typeof obj.message[0] === 'function') {
118+
if (obj.message.length > 1) {
119+
throw new ERR_AMBIGUOUS_ARGUMENT(
120+
'message',
121+
`The error message with function "${obj.message[0].name || 'anonymous'}" has trailing arguments that would be ignored.`,
122+
);
123+
}
124+
try {
125+
obj.message = obj.message[0](obj.actual, obj.expected);
126+
if (typeof obj.message !== 'string') {
127+
obj.message = undefined;
128+
}
129+
} catch {
130+
// Ignore and use default message instead
131+
obj.message = undefined;
132+
}
133+
} else {
134+
throw new ERR_INVALID_ARG_TYPE(
135+
'message',
136+
['string', 'function'],
137+
obj.message[0],
138+
);
139+
}
140+
141+
const error = new AssertionError(obj);
142+
if (obj.generatedMessage !== undefined) {
143+
error.generatedMessage = obj.generatedMessage;
144+
}
145+
throw error;
146+
}
147+
148+
/**
149+
* Internal ok handler delegating to innerFail for message handling.
150+
* @param {Function} fn
151+
* @param {number} argLen
152+
* @param {any} value
153+
* @param {...any} message
154+
*/
155+
function innerOk(fn, argLen, value, ...message) {
56156
if (!value) {
57157
let generatedMessage = false;
158+
/** @type {any[]} */
159+
let messageArgs;
58160

59161
if (argLen === 0) {
60162
generatedMessage = true;
61-
message = 'No value argument passed to `assert.ok()`';
62-
} else if (message == null) {
163+
messageArgs = ['No value argument passed to `assert.ok()`'];
164+
} else if (message.length === 0 || message[0] == null) {
63165
generatedMessage = true;
64-
message = getErrMessage(fn);
65-
} else if (isError(message)) {
66-
throw message;
166+
messageArgs = [getErrMessage(fn)];
167+
} else if (typeof message[0] === 'string' || isError(message[0]) || typeof message[0] === 'function') {
168+
messageArgs = message;
169+
} else {
170+
// Accept any other type as custom message for assert.ok()
171+
messageArgs = [String(message[0])];
67172
}
68173

69-
const err = new AssertionError({
174+
innerFail({
70175
actual: value,
71176
expected: true,
72-
message,
177+
message: messageArgs,
73178
operator: '==',
74179
stackStartFn: fn,
180+
generatedMessage,
75181
});
76-
err.generatedMessage = generatedMessage;
77-
throw err;
78182
}
79183
}
80184

81185
module.exports = {
82186
innerOk,
187+
innerFail,
83188
};

0 commit comments

Comments
 (0)