/** * @file EventReplay.test.tsx * @description The Replay tab's controls: step, jump, arrow keys, a lifecycle * marker jumping the cursor, playback advancing on a timer, and the derived * state following the cursor. * @author Nguyễn Ngọc Trí Vĩ */ import { describe, it, expect, vi, afterEach } from "vitest"; import { act, fireEvent, render, screen } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import { EventReplay } from "../EventReplay"; import type { DashboardEvent } from "../../lib/types"; function ev(id: number, event_type: string, tool_name: string | null = null): DashboardEvent { return { id, session_id: "s1", agent_id: "a1", event_type, tool_name, summary: `${event_type} ${tool_name ?? ""}`.trim(), data: null, created_at: new Date(Date.UTC(2026, 9, 1, 10, 0, id)).toISOString(), } as DashboardEvent; } // Given newest first, as the events API answers: the replay orders them itself. const events = [ ev(4, "Stop"), ev(3, "PostToolUse", "Bash"), ev(2, "PreToolUse", "Bash"), ev(1, "SessionStart"), ]; function renderReplay() { return render( ); } const position = () => screen.getByText(/^\d+ \/ 4/); afterEach(() => vi.useRealTimers()); describe("EventReplay", () => { it("starts at the first event and steps with the buttons", () => { renderReplay(); expect(position().textContent).toMatch(/^1 \/ 4/); fireEvent.click(screen.getByTitle("Step forward (→)")); fireEvent.click(screen.getByTitle("Step forward (→)")); expect(position().textContent).toMatch(/^3 \/ 4/); expect(screen.getByText("Bash ×1")).toBeTruthy(); fireEvent.click(screen.getByTitle("Jump to end (End)")); expect(position().textContent).toMatch(/^4 \/ 4/); fireEvent.click(screen.getByTitle("Jump to start (Home)")); expect(position().textContent).toMatch(/^1 \/ 4/); }); it("moves on arrow keys and jumps from a lifecycle marker", () => { renderReplay(); const root = screen.getByTestId("event-replay"); fireEvent.keyDown(root, { key: "ArrowRight" }); expect(position().textContent).toMatch(/^2 \/ 4/); // The in-flight tool at the PreToolUse. expect(screen.getByText("Running tool").nextSibling?.textContent).toBe("Bash"); fireEvent.keyDown(root, { key: "End" }); expect(position().textContent).toMatch(/^4 \/ 4/); fireEvent.click(screen.getByRole("button", { name: "Session start · #1" })); expect(position().textContent).toMatch(/^1 \/ 4/); }); it("plays to the end at the chosen speed", () => { vi.useFakeTimers(); renderReplay(); fireEvent.click(screen.getByRole("button", { name: "5×" })); fireEvent.click(screen.getByTitle("Play (Space)")); act(() => { vi.advanceTimersByTime(100); }); expect(position().textContent).toMatch(/^2 \/ 4/); // One step per tick: each step schedules the next. for (let i = 0; i < 3; i++) { act(() => { vi.advanceTimersByTime(100); }); } expect(position().textContent).toMatch(/^4 \/ 4/); expect(screen.getByTitle("Play (Space)")).toBeTruthy(); }); });