import { act, cleanup, renderHook, waitFor } from "@testing-library/react"; import { afterEach, expect, it, vi } from "vitest"; import type { RefObject } from "react"; import type { Map as MapLibreMap } from "maplibre-gl"; import type { WorkbenchMapController } from "../map/workbench-map-controller"; import type { WorkbenchSyncState } from "./use-workbench-sync"; import { useInvestigationPresentation } from "./use-investigation-presentation"; const sync = { subscribePresentationEvents: vi.fn(() => () => undefined) } as unknown as WorkbenchSyncState; const mapRef: RefObject = { current: null }; const controller = { queryPresentationFeatures: vi.fn() } as unknown as WorkbenchMapController; const padding = () => ({ top: 0, right: 0, bottom: 0, left: 0 }); const onClearLayers = vi.fn(); const options = { scope: "preview" as const, enabled: false, task: null, projectId: "one", sync, mapRef, mapReady: false, controller, getPadding: padding, onClearLayers }; afterEach(() => { cleanup(); vi.useRealTimers(); }); it("restores the last successful result when an unfinished display is aborted", async () => { const { result } = renderHook(() => useInvestigationPresentation(options)); await act(() => result.current.runPreview({ blocks: [], records: [{ value: "saved" }], commands: [] })); const abort = new AbortController(); let pending!: Promise; act(() => { pending = result.current.executeCommand({ action: "open_chart", chart_id: "pending", evidence_refs: ["test"], chart: { title: "Unfinished", chart_type: "line", x_data: ["10:00"], series: [{ name: "pressure", data: [20] }] } }, abort.signal, "preview").catch(() => undefined); }); await waitFor(() => expect(result.current.charts).toHaveLength(1)); await act(async () => { abort.abort(); await pending; }); expect(result.current.charts).toEqual([]); expect(result.current.records).toEqual([{ value: "saved" }]); expect(result.current.history.entries.at(-1)?.status).toBe("cancelled"); }); it("retries a cancelled live event in the same history row", async () => { const { result } = renderHook(() => useInvestigationPresentation(options)); const command = { action: "open_chart" as const, chart_id: "retry", evidence_refs: ["test"], chart: { title: "Recoverable", chart_type: "line" as const, x_data: ["10:00"], series: [{ name: "pressure", data: [20] }], } }; const event = { task_id: "50000000-0000-4000-8000-000000000001", run_id: "55000000-0000-4000-8000-000000000001", execution_generation: 1, event_seq: 1, command }; const abort = new AbortController(); let pending!: Promise; act(() => { pending = result.current.executeCommand(command, abort.signal, "live", event).catch(() => undefined); }); await waitFor(() => expect(result.current.charts).toHaveLength(1)); await act(async () => { abort.abort(); await pending; }); const id = result.current.history.entries[0].id; act(() => result.current.history.onSelect(id)); await act(() => result.current.executeCommand(command, new AbortController().signal, "live", event)); expect(result.current.history.entries).toHaveLength(1); expect(result.current.history.entries[0]).toMatchObject({ id, status: "completed" }); act(() => result.current.history.onSelect(null)); expect(result.current.charts[0].chart.title).toBe("Recoverable"); }); it("does not restore an old partial display after the project changes", async () => { const { result, rerender } = renderHook(({ projectId }) => useInvestigationPresentation({ ...options, projectId }), { initialProps: { projectId: "one" } }); await act(() => result.current.runPreview({ blocks: [], records: [{ value: "old project" }], commands: [] })); const abort = new AbortController(); let pending!: Promise; act(() => { pending = result.current.executeCommand({ action: "open_chart", chart_id: "pending", evidence_refs: ["test"], chart: { title: "Unfinished", chart_type: "line", x_data: ["10:00"], series: [{ name: "pressure", data: [20] }] } }, abort.signal, "preview").catch(() => undefined); }); await waitFor(() => expect(result.current.charts).toHaveLength(1)); rerender({ projectId: "two" }); await act(async () => { abort.abort(); await pending; }); expect(result.current.charts).toEqual([]); expect(result.current.records).toEqual([]); expect(result.current.history.entries).toEqual([]); }); it("advances saved snapshots on its timer, ignores new results and stops when the user clears overlays", async () => { vi.useFakeTimers(); const { result } = renderHook(() => useInvestigationPresentation(options)); await act(() => result.current.runPreview({ blocks: [], records: [{ value: 1 }], commands: [] })); await act(() => result.current.runPreview({ blocks: [], records: [{ value: 2 }], commands: [] })); act(() => result.current.history.playback.onToggle()); expect(result.current.records).toEqual([{ value: 1 }]); expect(result.current.history.playback.playing).toBe(true); await act(() => result.current.runPreview({ blocks: [], records: [{ value: 3 }], commands: [] })); expect(result.current.records).toEqual([{ value: 1 }]); act(() => vi.advanceTimersByTime(1500)); expect(result.current.records).toEqual([{ value: 2 }]); act(() => vi.advanceTimersByTime(1500)); expect(result.current.history.playback.playing).toBe(false); expect(result.current.records).toEqual([{ value: 2 }]); act(() => result.current.history.playback.onToggle()); expect(result.current.history.playback.playing).toBe(true); act(() => result.current.clearMapOverlays()); expect(result.current.history.playback.playing).toBe(false); expect(result.current.records).toEqual([{ value: 3 }]); act(() => vi.advanceTimersByTime(3000)); expect(result.current.records).toEqual([{ value: 3 }]); });