From 3fa52b3a65f7c3bab9ec52b8cc3d667be2c3ddf8 Mon Sep 17 00:00:00 2001 From: PJ Date: Sat, 6 Jun 2026 12:51:53 +0530 Subject: [PATCH] test(replay-ui): pin action selector parse and row formatting --- replay-ui/src/__tests__/action-format.test.ts | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 replay-ui/src/__tests__/action-format.test.ts diff --git a/replay-ui/src/__tests__/action-format.test.ts b/replay-ui/src/__tests__/action-format.test.ts new file mode 100644 index 0000000..17b8cae --- /dev/null +++ b/replay-ui/src/__tests__/action-format.test.ts @@ -0,0 +1,84 @@ +import { describe, it, expect } from "bun:test"; +import { + formatActionRow, + formatElapsed, + parseSelector, + tagFromSelector, +} from "../lib/action-format"; +import type { StepSummary } from "../types"; + +function summary(over: Partial): StepSummary { + return { + index: 0, + timestamp: "1970-01-01T00:00:00.000Z", + has_violations: false, + has_exceptions: false, + ...over, + }; +} + +// Bug class: selector parsing mislabels every action row — splitting on the +// wrong colon, treating a value-with-colon as the kind, or dropping the prefix +// ellipsis would render the wrong target tag for every step. +describe("parseSelector", () => { + const cases: { input: string; out: ReturnType }[] = [ + { input: "id:login", out: { kind: "id", value: "login" } }, + { input: "text:Sign In", out: { kind: "text", value: "Sign In" } }, + { input: "textPrefix:Hello", out: { kind: "textPrefix", value: "Hello" } }, + { input: "id:com.app:id/btn", out: { kind: "id", value: "com.app:id/btn" } }, + { input: "bogus:x", out: null }, + { input: ":leading", out: null }, + { input: "no-colon", out: null }, + ]; + for (const { input, out } of cases) { + it(`parses ${input}`, () => { + expect(parseSelector(input)).toEqual(out); + }); + } +}); + +describe("tagFromSelector", () => { + it("appends ellipsis only for prefix selectors", () => { + expect(tagFromSelector("textPrefix:Hel")).toBe("Hel..."); + expect(tagFromSelector("text:Hello")).toBe("Hello"); + expect(tagFromSelector("plain")).toBe("plain"); + }); +}); + +describe("formatActionRow", () => { + it("observes when no action kind, with and without screen", () => { + expect(formatActionRow(summary({}))).toEqual({ + verb: "Observe", + target: "", + targetIsTag: false, + }); + expect(formatActionRow(summary({ screen: "Home" }))).toEqual({ + verb: "Observe", + target: "@ Home", + targetIsTag: false, + }); + }); + + it("treats a selector label as a tag and a coordinate label as literal", () => { + expect( + formatActionRow(summary({ action_kind: "Tap", action_label: "id:btn" })), + ).toEqual({ verb: "Click", target: "btn", targetIsTag: true }); + expect( + formatActionRow(summary({ action_kind: "Tap", action_label: "(10, 20)" })), + ).toEqual({ verb: "Click", target: "(10, 20)", targetIsTag: false }); + }); + + it("maps known verbs and falls back to the raw kind", () => { + expect(formatActionRow(summary({ action_kind: "InputText", action_label: "hi" })).verb).toBe("Type"); + expect(formatActionRow(summary({ action_kind: "Swipe" })).verb).toBe("Swipe"); + expect(formatActionRow(summary({ action_kind: "Custom" })).verb).toBe("Custom"); + }); +}); + +describe("formatElapsed", () => { + it("formats mm:ss.mmm and clamps negatives to zero", () => { + expect(formatElapsed(0)).toBe("00:00.000"); + expect(formatElapsed(65_432)).toBe("01:05.432"); + expect(formatElapsed(-50)).toBe("00:00.000"); + }); +});