diff --git a/packages/cli/src/commands/layout-audit.browser.js b/packages/cli/src/commands/layout-audit.browser.js index 4eb5184d92..a23fd31b45 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 `${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 a7b36d2d36..51078fa473 100644 --- a/packages/cli/src/commands/layout-audit.browser.test.ts +++ b/packages/cli/src/commands/layout-audit.browser.test.ts @@ -63,6 +63,56 @@ describe("layout-audit.browser", () => { expect(after).not.toBe(before); }); + 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 = ` +
+ + +
+ `; + 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(); + firstSlice.style.opacity = "0"; + secondSlice.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 = `