From 2b773bbf5d9cb8c4c0a61500c65ab3fb03f18340 Mon Sep 17 00:00:00 2001 From: PJ Date: Sat, 6 Jun 2026 00:53:08 +0530 Subject: [PATCH] fix(replay-ui): skip synthetic zero-bounds root in deviceSpaceOf The iOS hierarchy prepends a zero-bounds node before the real root window, so elements[0] returned undefined and the overlay fell back to the screenshot's pixel size. Take the first element with positive extent instead; pre-order puts the root window before any content. Verified against a real iOS trace in the replay UI. --- replay-ui/src/__tests__/device-space.test.ts | 11 +++++++++++ replay-ui/src/lib/device-space.ts | 18 +++++++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/replay-ui/src/__tests__/device-space.test.ts b/replay-ui/src/__tests__/device-space.test.ts index 91b7d04..db3f453 100644 --- a/replay-ui/src/__tests__/device-space.test.ts +++ b/replay-ui/src/__tests__/device-space.test.ts @@ -33,4 +33,15 @@ describe("deviceSpaceOf", () => { expect(deviceSpaceOf(hierarchyWithRoot(393, 0))).toBeUndefined(); expect(deviceSpaceOf(hierarchyWithRoot(-1, -1))).toBeUndefined(); }); + + it("skips the iOS synthetic zero-bounds root", () => { + const hierarchy: Hierarchy = { + elements: [ + { bounds: { left: 0, top: 0, right: 0, bottom: 0 } }, + { bounds: { left: 0, top: 0, right: 402, bottom: 874 } }, + { bounds: { left: 20, top: 100, right: 380, bottom: 150 } }, + ], + }; + expect(deviceSpaceOf(hierarchy)).toEqual({ width: 402, height: 874 }); + }); }); diff --git a/replay-ui/src/lib/device-space.ts b/replay-ui/src/lib/device-space.ts index e94b511..5c89828 100644 --- a/replay-ui/src/lib/device-space.ts +++ b/replay-ui/src/lib/device-space.ts @@ -3,14 +3,18 @@ import type { Hierarchy } from "../types"; // Tap points and resolved bounds in the trace share the hierarchy root's // coordinate space (iOS points, Android pixels, web CSS px). Screenshots may // be scaled (iOS 3x, web DPR>1), so the overlay viewBox must come from the -// root bounds, not the image's natural pixel size. +// root bounds, not the image's natural pixel size. Elements are in pre-order; +// the first one with positive extent is the root window (iOS prepends a +// synthetic zero-bounds node, so plain elements[0] is not enough). export function deviceSpaceOf( hierarchy?: Hierarchy, ): { width: number; height: number } | undefined { - const root = hierarchy?.elements?.[0]; - if (!root) return undefined; - const width = root.bounds.right; - const height = root.bounds.bottom; - if (!(width > 0) || !(height > 0)) return undefined; - return { width, height }; + for (const element of hierarchy?.elements ?? []) { + const width = element.bounds.right; + const height = element.bounds.bottom; + if (width > 0 && height > 0) { + return { width, height }; + } + } + return undefined; }