From ef8a534da13970bf459ef368f7c6330bb8e2cf66 Mon Sep 17 00:00:00 2001 From: Shaurya Singh Date: Tue, 26 May 2026 14:12:55 -0700 Subject: [PATCH 1/2] fix(assert): hint at reference-equality when diff is empty (#6878) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `assertEquals` throws "Values are not equal" but renders an empty diff when actual/expected stringify identically yet are deemed unequal — the common case is a function property compared by reference. The empty diff leaves users hunting for invisible differences. Detect the case (diff result has zero `added`/`removed` entries) and append a one-line hint explaining that functions, Promises, Requests, Blobs, and other built-ins are compared by reference, so two distinct instances are never equal even when their representations match. Skip the hint on real textual diffs and on string-vs-string comparisons (which always produce visible add/remove if they differ). Two new tests cover the hint firing path and the negative case. Full assert suite (141 tests) stays green. Direction matches @lionel-rowe's suggestion in the issue thread. --- assert/equals.ts | 12 +++++++++++ assert/equals_test.ts | 49 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/assert/equals.ts b/assert/equals.ts index b8370584425b..a8c5b2c1eaa7 100644 --- a/assert/equals.ts +++ b/assert/equals.ts @@ -64,5 +64,17 @@ export function assertEquals( const diffMsg = buildMessage(diffResult, { stringDiff }, arguments[3]) .join("\n"); message = `${message}\n${diffMsg}`; + // #6878: if the diff shows no removed/added lines, the values stringify + // identically but were still deemed unequal — typically because at least + // one nested property is a function (or Promise / Request / Blob / etc.) + // that gets compared by reference. The empty diff is confusing, so append + // a hint pointing at the likely cause. + if (!stringDiff && diffResult.every((r) => r.type !== "added" && r.type !== "removed")) { + message = `${message}\n` + + " Note: values stringify identically but are not structurally equal. " + + "Functions, Promises, Requests, Blobs, and other built-ins are compared by " + + "reference, so two distinct instances are never equal even when their " + + "representations match."; + } throw new AssertionError(message); } diff --git a/assert/equals_test.ts b/assert/equals_test.ts index 8f23661d6b2f..6cfb7de95f5a 100644 --- a/assert/equals_test.ts +++ b/assert/equals_test.ts @@ -1,5 +1,10 @@ // Copyright 2018-2026 the Deno authors. MIT license. -import { assertEquals, AssertionError, assertThrows } from "./mod.ts"; +import { + assertEquals, + AssertionError, + assertStringIncludes, + assertThrows, +} from "./mod.ts"; import { bold, gray, @@ -207,6 +212,48 @@ Deno.test({ }, }); +Deno.test({ + name: + "assertEquals() hints at reference-equality when objects with function props stringify identically (#6878)", + fn() { + let caught: AssertionError | undefined; + try { + assertEquals( + { x: 1, y: () => 2 }, + { x: 1, y: () => 2 }, + ); + } catch (e) { + caught = e as AssertionError; + } + if (!caught) throw new Error("Expected assertEquals to throw"); + assertStringIncludes(caught.message, "Values are not equal"); + assertStringIncludes( + caught.message, + "stringify identically but are not structurally equal", + ); + assertStringIncludes(caught.message, "compared by reference"); + }, +}); + +Deno.test({ + name: + "assertEquals() does not append reference-equality hint when there is a real textual diff", + fn() { + let caught: AssertionError | undefined; + try { + assertEquals({ x: 1 }, { x: 2 }); + } catch (e) { + caught = e as AssertionError; + } + if (!caught) throw new Error("Expected assertEquals to throw"); + assertEquals( + caught.message.includes("stringify identically"), + false, + "Hint should only fire when the diff is empty", + ); + }, +}); + Deno.test({ name: "assertEquals() matches same Set with object keys", fn() { From c379203a7de840af5b114e93ac4f8ea96df97ac9 Mon Sep 17 00:00:00 2001 From: Shaurya Singh Date: Tue, 26 May 2026 14:29:52 -0700 Subject: [PATCH 2/2] fmt: wrap long if-condition per deno fmt --- assert/equals.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/assert/equals.ts b/assert/equals.ts index a8c5b2c1eaa7..7e800851d616 100644 --- a/assert/equals.ts +++ b/assert/equals.ts @@ -69,7 +69,10 @@ export function assertEquals( // one nested property is a function (or Promise / Request / Blob / etc.) // that gets compared by reference. The empty diff is confusing, so append // a hint pointing at the likely cause. - if (!stringDiff && diffResult.every((r) => r.type !== "added" && r.type !== "removed")) { + if ( + !stringDiff && + diffResult.every((r) => r.type !== "added" && r.type !== "removed") + ) { message = `${message}\n` + " Note: values stringify identically but are not structurally equal. " + "Functions, Promises, Requests, Blobs, and other built-ins are compared by " +