From 5bda2993d31966d6403bcaf7bbe219858006a821 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Thu, 11 Jun 2026 01:17:53 -0700 Subject: [PATCH 1/2] test(frontend): replace JSON structuredClone polyfill with faithful one The Jest setup polyfilled structuredClone for jsdom with a JSON.parse(JSON.stringify(...)) round-trip, which drops undefined-valued properties, stringifies Dates, degrades Map/Set to plain objects, and throws on cycles. Deep-clone tests guarding the deep-copy-state invariant therefore validated weaker semantics than production browsers. Replace the JSON round-trip with @ungap/structured-clone, a faithful implementation of the HTML structured clone algorithm, applied only when the environment does not already provide a native structuredClone. The polyfill runs inside the jsdom sandbox so cloned objects keep sandbox-realm prototypes and instanceof assertions work (a Node-realm clone such as worker_threads MessageChannel round-trip would break them). Unsupported transfer lists now fail loud instead of being silently ignored. Add regression tests asserting real structured clone semantics (undefined preservation, Date/Map/Set fidelity, cyclic references, throw on functions); all five fail against the previous JSON polyfill and pass now. Also update the stale test comment that claimed undefined preservation could not be asserted under jsdom. Closes #1160 --- frontend/package-lock.json | 7 ++- frontend/package.json | 1 + frontend/src/__tests__/setup.ts | 17 +++++- .../src/__tests__/ungap-structured-clone.d.ts | 12 ++++ frontend/src/__tests__/utils.test.ts | 58 +++++++++++++++++-- 5 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 frontend/src/__tests__/ungap-structured-clone.d.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index aade7bd29..adbfcc9e4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,6 +23,7 @@ "@types/jsdom": "^21.1.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", + "@ungap/structured-clone": "^1.3.1", "babel-loader": "^9.1.0", "copy-webpack-plugin": "^14.0.0", "css-loader": "^6.8.0", @@ -3140,9 +3141,9 @@ } }, "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", "dev": true, "license": "ISC" }, diff --git a/frontend/package.json b/frontend/package.json index fc809f766..f57d3cebb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,7 @@ "@types/jsdom": "^21.1.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", + "@ungap/structured-clone": "^1.3.1", "babel-loader": "^9.1.0", "copy-webpack-plugin": "^14.0.0", "css-loader": "^6.8.0", diff --git a/frontend/src/__tests__/setup.ts b/frontend/src/__tests__/setup.ts index c52860120..8159f6cd8 100644 --- a/frontend/src/__tests__/setup.ts +++ b/frontend/src/__tests__/setup.ts @@ -4,6 +4,7 @@ import '@testing-library/jest-dom'; import { TextEncoder, TextDecoder } from 'util'; import { webcrypto } from 'crypto'; +import structuredClonePolyfill from '@ungap/structured-clone'; // Polyfill TextEncoder/TextDecoder for Node.js environment global.TextEncoder = TextEncoder; @@ -61,8 +62,22 @@ Object.defineProperty(global, 'Chart', { // the Node.js global structuredClone to the window scope). The utils.ts // deepClone function delegates to structuredClone; without this the suite // throws "ReferenceError: structuredClone is not defined" (finding 11-N1). +// +// Prefer the environment-provided native implementation when present; only +// polyfill when absent. The polyfill is @ungap/structured-clone, a faithful +// implementation of the HTML structured clone algorithm (preserves +// undefined-valued properties, Date, Map, Set, RegExp, cycles; throws +// TypeError on functions), unlike the previous JSON round-trip which +// validated weaker semantics than production browsers (TEST-07). It must run +// inside the jsdom sandbox (not Node's realm) so cloned objects keep +// sandbox-realm prototypes and instanceof checks keep working. if (typeof globalThis.structuredClone === 'undefined') { - globalThis.structuredClone = (val: T): T => JSON.parse(JSON.stringify(val)) as T; + globalThis.structuredClone = ((value: T, options?: StructuredSerializeOptions): T => { + if (options?.transfer?.length) { + throw new Error('structuredClone polyfill does not support the transfer option'); + } + return structuredClonePolyfill(value); + }) as typeof structuredClone; } // Mock alert and confirm diff --git a/frontend/src/__tests__/ungap-structured-clone.d.ts b/frontend/src/__tests__/ungap-structured-clone.d.ts new file mode 100644 index 000000000..68b012946 --- /dev/null +++ b/frontend/src/__tests__/ungap-structured-clone.d.ts @@ -0,0 +1,12 @@ +// Type declarations for @ungap/structured-clone, which ships no .d.ts files. +// Used only by the Jest setup (setup.ts) to polyfill structuredClone with +// faithful HTML structured clone semantics under jest-environment-jsdom. +declare module '@ungap/structured-clone' { + interface StructuredCloneOptions { + transfer?: Transferable[]; + json?: boolean; + lossy?: boolean; + } + const structuredClonePolyfill: (value: T, options?: StructuredCloneOptions) => T; + export default structuredClonePolyfill; +} diff --git a/frontend/src/__tests__/utils.test.ts b/frontend/src/__tests__/utils.test.ts index 14d025d61..cf4b271df 100644 --- a/frontend/src/__tests__/utils.test.ts +++ b/frontend/src/__tests__/utils.test.ts @@ -501,10 +501,9 @@ describe('providerBadgeHtml', () => { // --------------------------------------------------------------------------- // Regression tests for finding 11-N1: deepClone defaults to structuredClone. -// Note: the jsdom test environment polyfills structuredClone with a JSON -// round-trip (see setup.ts) because jsdom does not expose Node's built-in -// structuredClone. As a result, undefined preservation can only be asserted -// when the native structuredClone is available (not in jsdom). +// The jsdom test environment polyfills structuredClone via Node's +// MessageChannel serializer (see setup.ts), which implements the real HTML +// structured clone algorithm, so full browser semantics are asserted here. // --------------------------------------------------------------------------- describe('deepClone (11-N1: structuredClone default)', () => { test('deep copy: mutation of clone does not affect source', () => { @@ -528,6 +527,57 @@ describe('deepClone (11-N1: structuredClone default)', () => { }); }); +// --------------------------------------------------------------------------- +// Regression tests for TEST-07: the test-environment structuredClone must +// implement real structured clone semantics, not a JSON round-trip. Each test +// below fails against the previous JSON.parse(JSON.stringify(...)) polyfill +// and passes against the browser/Node algorithm. +// --------------------------------------------------------------------------- +describe('deepClone (TEST-07: real structured clone semantics)', () => { + test('preserves undefined-valued properties (JSON round-trip drops them)', () => { + const obj: { a: number; b: number | undefined } = { a: 1, b: undefined }; + const clone = deepClone(obj); + expect('b' in clone).toBe(true); + expect(clone.b).toBeUndefined(); + }); + + test('preserves Date instances (JSON round-trip stringifies them)', () => { + const obj = { ts: new Date('2026-01-02T03:04:05.678Z') }; + const clone = deepClone(obj); + expect(clone.ts).toBeInstanceOf(Date); + expect(clone.ts.getTime()).toBe(obj.ts.getTime()); + expect(clone.ts).not.toBe(obj.ts); + }); + + test('preserves Map and Set (JSON round-trip degrades them to {})', () => { + const obj = { m: new Map([['k', 1]]), s: new Set([1, 2]) }; + const clone = deepClone(obj); + expect(clone.m).toBeInstanceOf(Map); + expect(clone.m.get('k')).toBe(1); + expect(clone.m).not.toBe(obj.m); + expect(clone.s).toBeInstanceOf(Set); + expect(clone.s.has(2)).toBe(true); + expect(clone.s).not.toBe(obj.s); + }); + + test('handles cyclic references (JSON round-trip throws)', () => { + interface Cyclic { + name: string; + self?: Cyclic; + } + const obj: Cyclic = { name: 'loop' }; + obj.self = obj; + const clone = deepClone(obj); + expect(clone).not.toBe(obj); + expect(clone.self).toBe(clone); + }); + + test('throws on functions like the browser implementation', () => { + // JSON round-trip silently drops functions; structured clone must throw. + expect(() => deepClone({ fn: (): number => 1 })).toThrow(); + }); +}); + describe('jsonClone (explicit JSON-serialisation variant)', () => { test('drops undefined values (expected JSON behaviour)', () => { const obj = { a: 1, b: undefined }; From df9bd84f83223aacc30576621c9e420deb247d03 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Fri, 26 Jun 2026 16:32:39 +0200 Subject: [PATCH 2/2] docs(test): correct structuredClone polyfill comments Adversarial review of #1241 found two stale/misleading comments about the new structuredClone polyfill: - utils.test.ts said "via Node's MessageChannel serializer" -- left over from an earlier draft that used MessageChannel; the actual polyfill is @ungap/structured-clone. Update to name the real package. - setup.ts claimed the polyfill "must run inside the jsdom sandbox" so instanceof works. In practice @ungap delegates to Node's native structuredClone when present; instanceof works because jsdom-on-jest shares Date/Map/Set primordials with Node, not because of any sandbox effect. Describe the real mechanism instead. No behavior change. --- frontend/src/__tests__/setup.ts | 8 +++++--- frontend/src/__tests__/utils.test.ts | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/src/__tests__/setup.ts b/frontend/src/__tests__/setup.ts index 8159f6cd8..329de7248 100644 --- a/frontend/src/__tests__/setup.ts +++ b/frontend/src/__tests__/setup.ts @@ -68,9 +68,11 @@ Object.defineProperty(global, 'Chart', { // implementation of the HTML structured clone algorithm (preserves // undefined-valued properties, Date, Map, Set, RegExp, cycles; throws // TypeError on functions), unlike the previous JSON round-trip which -// validated weaker semantics than production browsers (TEST-07). It must run -// inside the jsdom sandbox (not Node's realm) so cloned objects keep -// sandbox-realm prototypes and instanceof checks keep working. +// validated weaker semantics than production browsers (TEST-07). When the +// host Node has a native structuredClone, @ungap delegates to it; otherwise +// it uses its own serialize/deserialize. In jsdom-on-jest, Date/Map/Set +// primordials are shared with Node so `instanceof` keeps working on cloned +// values regardless of which path runs. if (typeof globalThis.structuredClone === 'undefined') { globalThis.structuredClone = ((value: T, options?: StructuredSerializeOptions): T => { if (options?.transfer?.length) { diff --git a/frontend/src/__tests__/utils.test.ts b/frontend/src/__tests__/utils.test.ts index cf4b271df..3c5f4418e 100644 --- a/frontend/src/__tests__/utils.test.ts +++ b/frontend/src/__tests__/utils.test.ts @@ -501,8 +501,8 @@ describe('providerBadgeHtml', () => { // --------------------------------------------------------------------------- // Regression tests for finding 11-N1: deepClone defaults to structuredClone. -// The jsdom test environment polyfills structuredClone via Node's -// MessageChannel serializer (see setup.ts), which implements the real HTML +// The jsdom test environment polyfills structuredClone via +// `@ungap/structured-clone` (see setup.ts), which implements the real HTML // structured clone algorithm, so full browser semantics are asserted here. // --------------------------------------------------------------------------- describe('deepClone (11-N1: structuredClone default)', () => {