diff --git a/ui/src/components/transcript/RunTranscriptView.test.tsx b/ui/src/components/transcript/RunTranscriptView.test.tsx index 3e5a42c7bbb..e1a869caa34 100644 --- a/ui/src/components/transcript/RunTranscriptView.test.tsx +++ b/ui/src/components/transcript/RunTranscriptView.test.tsx @@ -1,11 +1,32 @@ -// @vitest-environment node +// @vitest-environment jsdom +import { act, type ReactNode } from "react"; import { describe, expect, it, vi } from "vitest"; import { renderToStaticMarkup } from "react-dom/server"; +import { createRoot, type Root } from "react-dom/client"; import type { TranscriptEntry } from "../../adapters"; import { ThemeProvider } from "../../context/ThemeContext"; import { RunTranscriptView, normalizeTranscript } from "./RunTranscriptView"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + +function render(ui: ReactNode) { + const container = document.createElement("div"); + document.body.appendChild(container); + const root: Root = createRoot(container); + act(() => { + root.render(ui); + }); + return { + container, + unmount: () => { + act(() => root.unmount()); + container.remove(); + }, + }; +} + describe("RunTranscriptView", () => { it("keeps running command stdout inside the command fold instead of a standalone stdout block", () => { const entries: TranscriptEntry[] = [ @@ -111,29 +132,36 @@ describe("RunTranscriptView", () => { expect(html).not.toContain("result"); }); - it("adds timestamp and age tooltip to raw mode labels", () => { + it("adds timestamp and age tooltip to raw mode labels lazily on hover", () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-03-12T00:03:00.000Z")); + const { container, unmount } = render( + + + , + ); try { - const html = renderToStaticMarkup( - - - , - ); + const label = container.querySelector("span[data-timestamp]"); + expect(label?.textContent).toBe("stdout"); + expect(label?.getAttribute("title")).toBeNull(); - expect(html).toContain("Timestamp:"); - expect(html).toContain("Ago: 3m ago"); - expect(html).toContain(">stdout"); + act(() => { + label?.dispatchEvent(new MouseEvent("mouseover", { bubbles: true })); + }); + + expect(label?.getAttribute("title")).toContain("Timestamp:"); + expect(label?.getAttribute("title")).toContain("Ago: 3m ago"); } finally { + unmount(); vi.useRealTimers(); } }); diff --git a/ui/src/components/transcript/RunTranscriptView.tsx b/ui/src/components/transcript/RunTranscriptView.tsx index 887f42f41b6..b20deeae374 100644 --- a/ui/src/components/transcript/RunTranscriptView.tsx +++ b/ui/src/components/transcript/RunTranscriptView.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react"; +import { useMemo, useState, type MouseEvent as ReactMouseEvent } from "react"; import type { TranscriptEntry } from "../../adapters"; import { MarkdownBody } from "../MarkdownBody"; import { timeAgo } from "../../lib/timeAgo"; @@ -142,6 +142,13 @@ function formatRawEntryTimestampTooltip(ts: string): string { return `Timestamp: ${formatDateTime(ts)}; Ago: ${timeAgo(ts)}`; } +function handleRawTimestampLabelHover(event: ReactMouseEvent) { + if (event.currentTarget.title) return; + const ts = event.currentTarget.dataset.timestamp; + if (!ts) return; + event.currentTarget.title = formatRawEntryTimestampTooltip(ts); +} + function humanizeLabel(value: string): string { return value .replace(/[_-]+/g, " ") @@ -1373,7 +1380,8 @@ function RawTranscriptView({ > {entry.kind}