From e61ebe6a1a17c5121e62df4a04907d7f87e77de8 Mon Sep 17 00:00:00 2001 From: Nev Date: Thu, 12 Feb 2026 00:09:31 -0800 Subject: [PATCH] fix(deepEqual): prevent null return breaking nested Promise/function comparisons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes issue from PR #273 where _deepEquals() returning null from _strictEquals for non-reference-equal Promises/functions/WeakMaps caused nested comparisons to fail incorrectly. Callers check (=== false), treating null as "equal". Two bugs fixed: 1. Added _strictEqualsBool wrapper to convert null → false for types that should only be equal if reference-equal (Promise, function, symbol, WeakMap, WeakSet) 2. Fixed _getTypeComparer() objType lookup bug where undefined objType accessed _typeEquals["undefined"] returning wrong comparer instead of falling back to theType Changes: - core/src/assert/funcs/equal.ts: * Added _strictEqualsBool wrapper function * Updated _typeEquals map for promise/function/symbol/weakmap/weakset * Fixed _getTypeComparer to check (objType && _typeEquals[objType]) - core/test/src/assert/assert.equals.test.ts: * Added regression test for Promise/function in nested objects/arrays --- core/src/assert/funcs/equal.ts | 21 ++++++++---- core/test/src/assert/assert.equals.test.ts | 37 ++++++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/core/src/assert/funcs/equal.ts b/core/src/assert/funcs/equal.ts index fd8abc4..1037a90 100644 --- a/core/src/assert/funcs/equal.ts +++ b/core/src/assert/funcs/equal.ts @@ -177,6 +177,15 @@ interface IEqualOptions { visiting: any[]; } +/** + * Wraps _strictEquals to ensure it always returns a boolean (never null). + * For types that should only be equal if reference-equal (Promise, function, WeakMap, etc.), + * this treats non-reference-equal values as not equal. + */ +const _strictEqualsBool = (value: any, expected: any, options: IEqualOptions): boolean => { + return _strictEquals(value, expected) === true; +}; + const _typeEquals: { [key: string]: (value: any, expected: any, options: IEqualOptions) => boolean } = { "string": _valueOfEquals, "number": _valueOfEquals, @@ -184,11 +193,11 @@ const _typeEquals: { [key: string]: (value: any, expected: any, options: IEqualO "undefined": _valueOfEquals, "date": _valueOfEquals, - "promise": _strictEquals, - "function": _strictEquals, - "symbol": _strictEquals, - "weakmap": _strictEquals, - "weakset": _strictEquals, + "promise": _strictEqualsBool, + "function": _strictEqualsBool, + "symbol": _strictEqualsBool, + "weakmap": _strictEqualsBool, + "weakset": _strictEqualsBool, "error": _matchKeys(["name", "message", "code"]), @@ -215,7 +224,7 @@ function _getTypeComparer(value: any): (value: any, expected: any, options: IEqu objType = strLower(objToString(value).slice(8, -1)); } - let compareFn = _typeEquals[objType] || _typeEquals[theType]; + let compareFn = (objType && _typeEquals[objType]) || _typeEquals[theType]; if (!compareFn) { try { if (isObject(value) && isFunction(value[Symbol.iterator])) { diff --git a/core/test/src/assert/assert.equals.test.ts b/core/test/src/assert/assert.equals.test.ts index 484ecf4..4317c74 100644 --- a/core/test/src/assert/assert.equals.test.ts +++ b/core/test/src/assert/assert.equals.test.ts @@ -1163,4 +1163,41 @@ describe("assert.deepStrictEqual", function () { assert.deepStrictEqual([{ a: 1 }], [{ a: "1" }]); }, "expected [{a:1}] to deeply and strictly equal [{a:\"1\"}]"); }); + + it("Objects/arrays containing different Promises/functions should fail", function () { + // Regression test for issue #273 + // _deepEquals() must not return null for types like Promise/function/WeakMap/etc + // in nested comparisons, as null is treated as "equal" (not === false) + + const promise1 = Promise.resolve(1); + const promise2 = Promise.resolve(2); + const func1 = () => 1; + const func2 = () => 2; + + // Different promises in nested objects should fail + checkError(function () { + assert.deepStrictEqual({ p: promise1 }, { p: promise2 }); + }, /expected .* to deeply and strictly equal .*/); + + // Different functions in nested objects should fail + checkError(function () { + assert.deepStrictEqual({ f: func1 }, { f: func2 }); + }, /expected .* to deeply and strictly equal .*/); + + // Different promises in arrays should fail + checkError(function () { + assert.deepStrictEqual([promise1], [promise2]); + }, /expected .* to deeply and strictly equal .*/); + + // Different functions in arrays should fail + checkError(function () { + assert.deepStrictEqual([func1], [func2]); + }, /expected .* to deeply and strictly equal .*/); + + // Same reference should pass + assert.deepStrictEqual({ p: promise1 }, { p: promise1 }); + assert.deepStrictEqual({ f: func1 }, { f: func1 }); + assert.deepStrictEqual([promise1], [promise1]); + assert.deepStrictEqual([func1], [func1]); + }); });