From 82809a30cc6b5687026dea18d7da07601546525e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 15 Jul 2026 14:24:45 +0000 Subject: [PATCH 1/2] fix(cli): detect opacity-swapped raster slices --- .../cli/src/commands/layout-audit.browser.js | 5 +-- .../src/commands/layout-audit.browser.test.ts | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/layout-audit.browser.js b/packages/cli/src/commands/layout-audit.browser.js index 4eb5184d92..080354a26a 100644 --- a/packages/cli/src/commands/layout-audit.browser.js +++ b/packages/cli/src/commands/layout-audit.browser.js @@ -1426,7 +1426,8 @@ }; // Frozen-sweep guard (#U10, checkPipeline.ts): a compact per-sample - // fingerprint of every visible element's box + opacity, in DOM order. Node + // fingerprint of every visible element's identity + box + opacity, in DOM + // order. Node // calls this once per seeked grid point and compares the strings across the // whole run — if every sample produces the identical string, the seek never // actually moved anything and the whole audit run is unreliable. Deliberately @@ -1472,7 +1473,7 @@ const parts = elements.map((element) => { const rect = toRect(element.getBoundingClientRect()); const opacity = round(opacityChain(element)); - return `${rect.left},${rect.top},${rect.width},${rect.height},${opacity}`; + return `${selectorFor(element)},${rect.left},${rect.top},${rect.width},${rect.height},${opacity}`; }); for (const media of root.querySelectorAll("canvas, video")) { if (!isVisibleElement(media)) continue; diff --git a/packages/cli/src/commands/layout-audit.browser.test.ts b/packages/cli/src/commands/layout-audit.browser.test.ts index a7b36d2d36..1ecdf7a27c 100644 --- a/packages/cli/src/commands/layout-audit.browser.test.ts +++ b/packages/cli/src/commands/layout-audit.browser.test.ts @@ -63,6 +63,41 @@ describe("layout-audit.browser", () => { expect(after).not.toBe(before); }); + it("changes the sweep fingerprint when same-sized raster slices swap visibility", () => { + document.body.innerHTML = ` +
+ + +
+ `; + installGeometry({ + root: rect({ left: 0, top: 0, width: 640, height: 360 }), + "slice-a": rect({ left: 80, top: 60, width: 480, height: 240 }), + "slice-b": rect({ left: 80, top: 60, width: 480, height: 240 }), + }); + const nativeGetComputedStyle = window.getComputedStyle.bind(window); + vi.spyOn(window, "getComputedStyle").mockImplementation((element) => { + const computed = nativeGetComputedStyle(element); + const inlineOpacity = (element as HTMLElement).style.opacity; + if (!inlineOpacity) return computed; + return new Proxy(computed, { + get(target, property, receiver) { + return property === "opacity" ? inlineOpacity : Reflect.get(target, property, receiver); + }, + }); + }); + + installAuditScript(); + const collect = (window as unknown as { __hyperframesLayoutGeometry: () => string }) + .__hyperframesLayoutGeometry; + const before = collect(); + document.getElementById("slice-a")!.style.opacity = "0"; + document.getElementById("slice-b")!.style.opacity = "1"; + const after = collect(); + + expect(after).not.toBe(before); + }); + it("uses authored canvas dimensions when the root bounding rect is degenerate", () => { document.body.innerHTML = `
From 1a0222d4c4528d1e03a5b9acf6eb0c16b4b61180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 15 Jul 2026 18:09:07 +0000 Subject: [PATCH 2/2] fix(cli): disambiguate class-only raster slices --- .../cli/src/commands/layout-audit.browser.js | 2 +- .../src/commands/layout-audit.browser.test.ts | 71 +++++++++++-------- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/packages/cli/src/commands/layout-audit.browser.js b/packages/cli/src/commands/layout-audit.browser.js index 080354a26a..a23fd31b45 100644 --- a/packages/cli/src/commands/layout-audit.browser.js +++ b/packages/cli/src/commands/layout-audit.browser.js @@ -1473,7 +1473,7 @@ const parts = elements.map((element) => { const rect = toRect(element.getBoundingClientRect()); const opacity = round(opacityChain(element)); - return `${selectorFor(element)},${rect.left},${rect.top},${rect.width},${rect.height},${opacity}`; + return `${uniqueSelectorFor(element)},${rect.left},${rect.top},${rect.width},${rect.height},${opacity}`; }); for (const media of root.querySelectorAll("canvas, video")) { if (!isVisibleElement(media)) continue; diff --git a/packages/cli/src/commands/layout-audit.browser.test.ts b/packages/cli/src/commands/layout-audit.browser.test.ts index 1ecdf7a27c..51078fa473 100644 --- a/packages/cli/src/commands/layout-audit.browser.test.ts +++ b/packages/cli/src/commands/layout-audit.browser.test.ts @@ -63,40 +63,55 @@ describe("layout-audit.browser", () => { expect(after).not.toBe(before); }); - it("changes the sweep fingerprint when same-sized raster slices swap visibility", () => { - document.body.innerHTML = ` + it.each([ + ["ids", 'id="slice-a"', 'id="slice-b"'], + ["data layout names", 'data-layout-name="slice-a"', 'data-layout-name="slice-b"'], + ["shared classes", 'class="raster-slice"', 'class="raster-slice"'], + ["structural positions", "", ""], + ])( + "changes the sweep fingerprint when same-sized raster slices with %s swap visibility", + (_identity, firstAttributes, secondAttributes) => { + document.body.innerHTML = `
- - + +
`; - installGeometry({ - root: rect({ left: 0, top: 0, width: 640, height: 360 }), - "slice-a": rect({ left: 80, top: 60, width: 480, height: 240 }), - "slice-b": rect({ left: 80, top: 60, width: 480, height: 240 }), - }); - const nativeGetComputedStyle = window.getComputedStyle.bind(window); - vi.spyOn(window, "getComputedStyle").mockImplementation((element) => { - const computed = nativeGetComputedStyle(element); - const inlineOpacity = (element as HTMLElement).style.opacity; - if (!inlineOpacity) return computed; - return new Proxy(computed, { - get(target, property, receiver) { - return property === "opacity" ? inlineOpacity : Reflect.get(target, property, receiver); - }, + const [firstSlice, secondSlice] = Array.from(document.querySelectorAll("img")) as [ + HTMLImageElement, + HTMLImageElement, + ]; + const sliceRect = rect({ left: 80, top: 60, width: 480, height: 240 }); + installGeometry({ + root: rect({ left: 0, top: 0, width: 640, height: 360 }), + "slice-a": sliceRect, + "slice-b": sliceRect, + headline: sliceRect, + "": sliceRect, + }); + const nativeGetComputedStyle = window.getComputedStyle.bind(window); + vi.spyOn(window, "getComputedStyle").mockImplementation((element) => { + const computed = nativeGetComputedStyle(element); + const inlineOpacity = (element as HTMLElement).style.opacity; + if (!inlineOpacity) return computed; + return new Proxy(computed, { + get(target, property, receiver) { + return property === "opacity" ? inlineOpacity : Reflect.get(target, property, receiver); + }, + }); }); - }); - installAuditScript(); - const collect = (window as unknown as { __hyperframesLayoutGeometry: () => string }) - .__hyperframesLayoutGeometry; - const before = collect(); - document.getElementById("slice-a")!.style.opacity = "0"; - document.getElementById("slice-b")!.style.opacity = "1"; - const after = collect(); + installAuditScript(); + const collect = (window as unknown as { __hyperframesLayoutGeometry: () => string }) + .__hyperframesLayoutGeometry; + const before = collect(); + firstSlice.style.opacity = "0"; + secondSlice.style.opacity = "1"; + const after = collect(); - expect(after).not.toBe(before); - }); + expect(after).not.toBe(before); + }, + ); it("uses authored canvas dimensions when the root bounding rect is degenerate", () => { document.body.innerHTML = `