From 5dd42302513cdebc1215399e89d9293ba1587bb1 Mon Sep 17 00:00:00 2001 From: PJ Date: Mon, 13 Jul 2026 14:14:32 +0530 Subject: [PATCH] fix(replay): derive device space from screen extent, not first node the first positive-bounds element is often a short status-bar node (320x24 on android); using it gave a 320/24 aspect ratio that squashed the screenshot overlay into a grey horizontal band. use the max extent across elements (like the runner's screenBounds) instead. --- replay-ui/src/__tests__/device-space.test.ts | 14 +++++++++++++ replay-ui/src/lib/device-space.ts | 21 ++++++++++++-------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/replay-ui/src/__tests__/device-space.test.ts b/replay-ui/src/__tests__/device-space.test.ts index db3f453..0d6f927 100644 --- a/replay-ui/src/__tests__/device-space.test.ts +++ b/replay-ui/src/__tests__/device-space.test.ts @@ -44,4 +44,18 @@ describe("deviceSpaceOf", () => { }; expect(deviceSpaceOf(hierarchy)).toEqual({ width: 402, height: 874 }); }); + + it("uses the screen extent, not a short status-bar node listed first", () => { + // Regression: a 320x24 status bar precedes the 320x640 screen. Picking the + // first positive-bounds element gave a 320/24 aspect ratio, squashing the + // screenshot overlay into a grey horizontal band. + const hierarchy: Hierarchy = { + elements: [ + { bounds: { left: 0, top: 0, right: 320, bottom: 24 } }, + { bounds: { left: 0, top: 0, right: 320, bottom: 640 } }, + { bounds: { left: 20, top: 271, right: 286, bottom: 319 } }, + ], + }; + expect(deviceSpaceOf(hierarchy)).toEqual({ width: 320, height: 640 }); + }); }); diff --git a/replay-ui/src/lib/device-space.ts b/replay-ui/src/lib/device-space.ts index 5c89828..cfd90c6 100644 --- a/replay-ui/src/lib/device-space.ts +++ b/replay-ui/src/lib/device-space.ts @@ -3,18 +3,23 @@ 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. 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). +// device bounds, not the image's natural pixel size. +// +// Use the maximum extent across all elements (mirroring the runner's +// screenBounds), NOT the first positive-bounds element: the first element is +// often a status-bar/decor node a few px tall (e.g. 320x24 on Android), which +// would give a wildly wrong aspect ratio and squash the overlay into a band. export function deviceSpaceOf( hierarchy?: Hierarchy, ): { width: number; height: number } | undefined { + let width = 0; + let height = 0; for (const element of hierarchy?.elements ?? []) { - const width = element.bounds.right; - const height = element.bounds.bottom; - if (width > 0 && height > 0) { - return { width, height }; - } + if (element.bounds.right > width) width = element.bounds.right; + if (element.bounds.bottom > height) height = element.bounds.bottom; + } + if (width > 0 && height > 0) { + return { width, height }; } return undefined; }