Files
next-tjwater-frontend/src/features/workbench/hooks/use-investigation-presentation.test.tsx
T

98 lines
5.7 KiB
TypeScript

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<MapLibreMap | null> = { 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<unknown>;
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<unknown>;
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<unknown>;
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 }]);
});