diff --git a/native-lib/node/tests/tck/compare.ts b/native-lib/node/tests/tck/compare.ts index 495d3ba..9be27c8 100644 --- a/native-lib/node/tests/tck/compare.ts +++ b/native-lib/node/tests/tck/compare.ts @@ -73,7 +73,10 @@ function compareJson(actual: string, expected: string): CompareResult { } catch (err) { return fail(`expected is not valid JSON: ${err}`); } - return deepEqual(a, e) ? ok : fail(`JSON mismatch:\n actual: ${actual.trim()}\n expected: ${expected.trim()}`); + // Normalize CRLF → LF within string values so platform line endings don't + // cause a mismatch — e.g. an embedded CSV value the writer emits with CRLF on + // Windows but LF elsewhere. Matches the CLI's AssertionHelper EOL handling. + return deepEqual(a, e, true) ? ok : fail(`JSON mismatch:\n actual: ${actual.trim()}\n expected: ${expected.trim()}`); } const xmlParser = new XMLParser({ @@ -141,15 +144,23 @@ function stripAllWhitespace(s: string): string { return s.replace(/\s+/g, ""); } -/** Structural deep equality for JSON values (objects compared key-insensitively to order). */ -export function deepEqual(a: unknown, b: unknown): boolean { +/** + * Structural deep equality for JSON values (objects compared key-insensitively + * to order). When `normEol` is true, string values are compared with CRLF + * normalized to LF so platform line endings inside a value don't cause a + * mismatch. + */ +export function deepEqual(a: unknown, b: unknown, normEol = false): boolean { + if (typeof a === "string" && typeof b === "string") { + return normEol ? a.replace(/\r\n/g, "\n") === b.replace(/\r\n/g, "\n") : a === b; + } if (a === b) return true; if (a === null || b === null) return a === b; if (typeof a !== typeof b) return false; if (Array.isArray(a) || Array.isArray(b)) { if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false; - return a.every((v, i) => deepEqual(v, b[i])); + return a.every((v, i) => deepEqual(v, b[i], normEol)); } if (typeof a === "object" && typeof b === "object") { @@ -158,7 +169,7 @@ export function deepEqual(a: unknown, b: unknown): boolean { const ak = Object.keys(ao); const bk = Object.keys(bo); if (ak.length !== bk.length) return false; - return ak.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && deepEqual(ao[k], bo[k])); + return ak.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && deepEqual(ao[k], bo[k], normEol)); } return false; diff --git a/native-lib/node/tests/unit/tck-compare.test.ts b/native-lib/node/tests/unit/tck-compare.test.ts index 90699f2..dd9894b 100644 --- a/native-lib/node/tests/unit/tck-compare.test.ts +++ b/native-lib/node/tests/unit/tck-compare.test.ts @@ -28,6 +28,21 @@ describe("compareOutput — json (structural)", () => { expect(r.match).toBe(false); expect(r.detail).toMatch(/actual is not valid JSON/); }); + + it("ignores CRLF vs LF inside a string value (Windows line endings)", () => { + // Regression: a JSON output embedding CSV — the writer emits CRLF on Windows + // but the fixture has LF. The line breaks are escaped \r\n / \n inside the + // JSON string, so they must be normalized during value comparison. + const actual = buf(JSON.stringify({ csv: "a|b\r\nx|y\r\n" })); + const expected = buf(JSON.stringify({ csv: "a|b\nx|y\n" })); + expect(compareOutput("json", actual, expected).match).toBe(true); + }); + + it("still distinguishes genuinely different string values", () => { + const actual = buf(JSON.stringify({ v: "hello" })); + const expected = buf(JSON.stringify({ v: "world" })); + expect(compareOutput("json", actual, expected).match).toBe(false); + }); }); describe("compareOutput — xml (structural)", () => {