diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4f783c2..e78f0f1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,6 +17,7 @@ jobs: - 22.x - 24.x - 25.x + - 26.x runs-on: ${{ matrix.runner }} steps: - name: Harden Runner diff --git a/implementors/node/assert.js b/implementors/node/assert.js index 8320281..3a6be80 100644 --- a/implementors/node/assert.js +++ b/implementors/node/assert.js @@ -7,16 +7,17 @@ import { match, } from 'node:assert/strict'; -const assert = Object.assign((value, message) => ok(value, message), { - ok: (value, message) => ok(value, message), - strictEqual: (actual, expected, message) => - strictEqual(actual, expected, message), - notStrictEqual: (actual, expected, message) => - notStrictEqual(actual, expected, message), - deepStrictEqual: (actual, expected, message) => - deepStrictEqual(actual, expected, message), - throws: (fn, error, message) => throws(fn, error, message), - match: (string, regex, message) => match(string, regex, message), +// Forward with rest arguments rather than named parameters: an omitted trailing +// message has to stay omitted. Node.js 26 reads the message as a variadic tuple, +// so an explicitly passed `undefined` is a message of the wrong type there and +// the assertion fails with ERR_INVALID_ARG_TYPE instead of the value comparison. +const assert = Object.assign((...args) => ok(...args), { + ok: (...args) => ok(...args), + strictEqual: (...args) => strictEqual(...args), + notStrictEqual: (...args) => notStrictEqual(...args), + deepStrictEqual: (...args) => deepStrictEqual(...args), + throws: (...args) => throws(...args), + match: (...args) => match(...args), }); Object.assign(globalThis, { assert }); diff --git a/tests/harness/assert.js b/tests/harness/assert.js index 65bf43c..a1848ae 100644 --- a/tests/harness/assert.js +++ b/tests/harness/assert.js @@ -105,3 +105,49 @@ if (!threw) throw new Error('assert.match must throw when input is not a string' threw = false; try { assert.match('hello', 'hello'); } catch { threw = true; } if (!threw) throw new Error('assert.match must throw when pattern is not a RegExp'); + +// The message is optional on every method. A failure without one must still +// report the comparison, and a failure with one must report that message. +function failureOf(label, fn) { + try { + fn(); + } catch (error) { + return error; + } + throw new Error(`${label} was expected to throw`); +} + +const optionalMessage = [ + ['assert', () => assert(false), (m) => assert(false, m)], + ['assert.ok', () => assert.ok(false), (m) => assert.ok(false, m)], + ['assert.strictEqual', () => assert.strictEqual(1, 2), (m) => assert.strictEqual(1, 2, m)], + ['assert.notStrictEqual', () => assert.notStrictEqual(1, 1), (m) => assert.notStrictEqual(1, 1, m)], + [ + 'assert.deepStrictEqual', + () => assert.deepStrictEqual({ a: 1 }, { a: 2 }), + (m) => assert.deepStrictEqual({ a: 1 }, { a: 2 }, m), + ], + ['assert.match', () => assert.match('hello', /world/), (m) => assert.match('hello', /world/, m)], + ['assert.throws', () => assert.throws(() => {}, /oops/), (m) => assert.throws(() => {}, /oops/, m)], +]; + +const customMessage = 'a message the caller chose'; + +for (const [label, withoutMessage, withMessage] of optionalMessage) { + // Only a substring check: the strict assert methods append their own value + // comparison to a caller supplied message. + const described = failureOf(label, () => withMessage(customMessage)); + if (!described.message.includes(customMessage)) { + throw new Error( + `${label} must fail with the message it was given but failed with "${described.message}"`, + ); + } + + const bare = failureOf(label, withoutMessage); + if (bare.name !== described.name) { + throw new Error( + `${label} must fail the same way with and without a message, but without one it failed ` + + `with "${bare.name}: ${bare.message}" instead of ${described.name}`, + ); + } +}