fix(replay-ui): size overlay viewBox from hierarchy root bounds

Tap points are recorded in the hierarchy's coordinate space (iOS points,
Android pixels, web CSS px) while screenshots are device pixels, so the
overlay rendered at 1/3 position on iOS 3x screens. Derive the viewBox
from the root element bounds; natural image size stays the fallback.
This commit is contained in:
pj committed 2026-06-06 00:31:10 +05:30
1 parent 9958b0ddc8
commit d84dc74b0b
3 files changed
+71 -2

No files matched your search

@@ -0,0 +1,36 @@
import { describe, it, expect } from "bun:test";
import { deviceSpaceOf } from "../lib/device-space";
import type { Hierarchy } from "../types";
function hierarchyWithRoot(right: number, bottom: number): Hierarchy {
return {
elements: [
{
bounds: { left: 0, top: 0, right, bottom },
},
],
};
}
describe("deviceSpaceOf", () => {
it("returns the root bounds extent", () => {
expect(deviceSpaceOf(hierarchyWithRoot(393, 852))).toEqual({
width: 393,
height: 852,
});
});
it("returns undefined without a hierarchy", () => {
expect(deviceSpaceOf(undefined)).toBeUndefined();
});
it("returns undefined for an empty hierarchy", () => {
expect(deviceSpaceOf({ elements: [] })).toBeUndefined();
});
it("returns undefined for non-positive root bounds", () => {
expect(deviceSpaceOf(hierarchyWithRoot(0, 852))).toBeUndefined();
expect(deviceSpaceOf(hierarchyWithRoot(393, 0))).toBeUndefined();
expect(deviceSpaceOf(hierarchyWithRoot(-1, -1))).toBeUndefined();
});
});
+16
View File
@@ -0,0 +1,16 @@
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.
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 };
}
+19 -2
View File
@@ -14,6 +14,7 @@ import Tabs, { type TabDefinition } from "../components/Tabs";
import { useStep } from "../hooks/useStep"; import { useStep } from "../hooks/useStep";
import { useKeyboardNav } from "../hooks/useKeyboardNav"; import { useKeyboardNav } from "../hooks/useKeyboardNav";
import { useTheme } from "../hooks/useTheme"; import { useTheme } from "../hooks/useTheme";
import { deviceSpaceOf } from "../lib/device-space";
function basename(specPath: string): string { function basename(specPath: string): string {
const index = specPath.lastIndexOf("/"); const index = specPath.lastIndexOf("/");
@@ -129,12 +130,21 @@ export default function RunDetail() {
const witnessesBefore = currentStep?.witnesses; const witnessesBefore = currentStep?.witnesses;
const witnessesAfter = nextStep?.witnesses ?? witnessesBefore; const witnessesAfter = nextStep?.witnesses ?? witnessesBefore;
const exceptionsForStep = currentStep?.exceptions; const exceptionsForStep = currentStep?.exceptions;
const beforeSpace = deviceSpaceOf(currentStep?.hierarchy);
const afterSpace = deviceSpaceOf(nextStep?.hierarchy ?? currentStep?.hierarchy);
const beforeTabs: TabDefinition[] = [ const beforeTabs: TabDefinition[] = [
{ {
id: "screenshot", id: "screenshot",
label: "Screenshot", label: "Screenshot",
content: <Screenshot src={beforeScreenshot} action={currentStep?.next_action} />, content: (
<Screenshot
src={beforeScreenshot}
action={currentStep?.next_action}
deviceWidth={beforeSpace?.width}
deviceHeight={beforeSpace?.height}
/>
),
}, },
{ {
id: "snapshots", id: "snapshots",
@@ -194,7 +204,14 @@ export default function RunDetail() {
{ {
id: "screenshot", id: "screenshot",
label: "Screenshot", label: "Screenshot",
content: <Screenshot src={afterScreenshot} action={undefined} />, content: (
<Screenshot
src={afterScreenshot}
action={undefined}
deviceWidth={afterSpace?.width}
deviceHeight={afterSpace?.height}
/>
),
}, },
{ {
id: "snapshots", id: "snapshots",