From a4d6020663040e24e82733c8bea7ae664ddb5f82 Mon Sep 17 00:00:00 2001 From: PJ Date: Sat, 6 Jun 2026 12:51:22 +0530 Subject: [PATCH] refactor(replay-ui): extract action selector/format into lib module --- replay-ui/src/lib/action-format.ts | 92 +++++++++++++++++++++++++++++ replay-ui/src/panels/ActionList.tsx | 90 +--------------------------- 2 files changed, 93 insertions(+), 89 deletions(-) create mode 100644 replay-ui/src/lib/action-format.ts diff --git a/replay-ui/src/lib/action-format.ts b/replay-ui/src/lib/action-format.ts new file mode 100644 index 0000000..d21831f --- /dev/null +++ b/replay-ui/src/lib/action-format.ts @@ -0,0 +1,92 @@ +import type { StepSummary } from "../types"; + +export interface FormattedRow { + verb: string; + target: string; + targetIsTag: boolean; +} + +const SELECTOR_PREFIXES = [ + "id", + "text", + "textPrefix", + "desc", + "descPrefix", + "class", + "classPrefix", + "package", +]; + +export function parseSelector( + selector: string, +): { kind: string; value: string } | null { + const colonIndex = selector.indexOf(":"); + if (colonIndex <= 0) { + return null; + } + const kind = selector.slice(0, colonIndex); + const value = selector.slice(colonIndex + 1); + if (!SELECTOR_PREFIXES.includes(kind)) { + return null; + } + return { kind, value }; +} + +export function tagFromSelector(selector: string): string { + const parsed = parseSelector(selector); + if (!parsed) { + return selector; + } + if (parsed.kind.endsWith("Prefix")) { + return `${parsed.value}...`; + } + return parsed.value; +} + +export function formatActionRow(step: StepSummary): FormattedRow { + const kind = step.action_kind; + const label = step.action_label ?? ""; + + if (!kind) { + if (step.screen) { + return { verb: "Observe", target: `@ ${step.screen}`, targetIsTag: false }; + } + return { verb: "Observe", target: "", targetIsTag: false }; + } + + switch (kind) { + case "Tap": { + if (!label) { + return { verb: "Click", target: "", targetIsTag: false }; + } + if (label.startsWith("(") && label.endsWith(")")) { + return { verb: "Click", target: label, targetIsTag: false }; + } + if (parseSelector(label)) { + return { verb: "Click", target: tagFromSelector(label), targetIsTag: true }; + } + return { verb: "Click", target: label, targetIsTag: false }; + } + case "InputText": + return { verb: "Type", target: label, targetIsTag: false }; + case "Swipe": + return { verb: "Swipe", target: label, targetIsTag: true }; + case "PressKey": + return { verb: "Press", target: label, targetIsTag: true }; + case "Wait": + return { verb: "Wait", target: label, targetIsTag: true }; + default: + return { verb: kind, target: label, targetIsTag: false }; + } +} + +export function formatElapsed(millis: number): string { + const safe = Math.max(0, Math.floor(millis)); + const totalSeconds = Math.floor(safe / 1000); + const mm = Math.floor(totalSeconds / 60); + const ss = totalSeconds % 60; + const ms = safe % 1000; + const pad2 = (n: number) => String(n).padStart(2, "0"); + const pad3 = (n: number) => String(n).padStart(3, "0"); + return `${pad2(mm)}:${pad2(ss)}.${pad3(ms)}`; +} diff --git a/replay-ui/src/panels/ActionList.tsx b/replay-ui/src/panels/ActionList.tsx index 66d2e14..ffa030f 100644 --- a/replay-ui/src/panels/ActionList.tsx +++ b/replay-ui/src/panels/ActionList.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef } from "react"; import type { KeyboardEvent } from "react"; import type { Step, StepSummary } from "../types"; +import { formatActionRow, formatElapsed } from "../lib/action-format"; import "./ActionList.css"; export interface ActionListProps { @@ -11,95 +12,6 @@ export interface ActionListProps { selectedStep?: Step; } -interface FormattedRow { - verb: string; - target: string; - targetIsTag: boolean; -} - -const SELECTOR_PREFIXES = [ - "id", - "text", - "textPrefix", - "desc", - "descPrefix", - "class", - "classPrefix", - "package", -]; - -function parseSelector(selector: string): { kind: string; value: string } | null { - const colonIndex = selector.indexOf(":"); - if (colonIndex <= 0) { - return null; - } - const kind = selector.slice(0, colonIndex); - const value = selector.slice(colonIndex + 1); - if (!SELECTOR_PREFIXES.includes(kind)) { - return null; - } - return { kind, value }; -} - -function tagFromSelector(selector: string): string { - const parsed = parseSelector(selector); - if (!parsed) { - return selector; - } - if (parsed.kind.endsWith("Prefix")) { - return `${parsed.value}...`; - } - return parsed.value; -} - -export function formatActionRow(step: StepSummary): FormattedRow { - const kind = step.action_kind; - const label = step.action_label ?? ""; - - if (!kind) { - if (step.screen) { - return { verb: "Observe", target: `@ ${step.screen}`, targetIsTag: false }; - } - return { verb: "Observe", target: "", targetIsTag: false }; - } - - switch (kind) { - case "Tap": { - if (!label) { - return { verb: "Click", target: "", targetIsTag: false }; - } - if (label.startsWith("(") && label.endsWith(")")) { - return { verb: "Click", target: label, targetIsTag: false }; - } - if (parseSelector(label)) { - return { verb: "Click", target: tagFromSelector(label), targetIsTag: true }; - } - return { verb: "Click", target: label, targetIsTag: false }; - } - case "InputText": - return { verb: "Type", target: label, targetIsTag: false }; - case "Swipe": - return { verb: "Swipe", target: label, targetIsTag: true }; - case "PressKey": - return { verb: "Press", target: label, targetIsTag: true }; - case "Wait": - return { verb: "Wait", target: label, targetIsTag: true }; - default: - return { verb: kind, target: label, targetIsTag: false }; - } -} - -export function formatElapsed(millis: number): string { - const safe = Math.max(0, Math.floor(millis)); - const totalSeconds = Math.floor(safe / 1000); - const mm = Math.floor(totalSeconds / 60); - const ss = totalSeconds % 60; - const ms = safe % 1000; - const pad2 = (n: number) => String(n).padStart(2, "0"); - const pad3 = (n: number) => String(n).padStart(3, "0"); - return `${pad2(mm)}:${pad2(ss)}.${pad3(ms)}`; -} - function renderTarget(target: string, isTag: boolean) { if (!target) { return null;