Files
next-tjwater-frontend/src/features/workbench/peripheral-sample/use-scada-playback.test.tsx
T

145 lines
6.5 KiB
TypeScript

import { act, cleanup, renderHook } from "@testing-library/react";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import type { ScadaReplay } from "../api/scada-replay-client";
import { useScadaPlayback } from "./use-scada-playback";
const start = Date.parse("2026-07-16T07:30:00+08:00");
const end = start + 3600_000;
function receipt(time: number, status = "paused") {
return { virtual_now: new Date(time).toISOString(), status, step_pending: false } as ScadaReplay;
}
function setup(advance = vi.fn<() => Promise<ScadaReplay | null>>()) {
const options = { active: true, enabled: true, busy: false, start, end, now: start,
intervalSeconds: 900, restart: false, advance };
return { ...renderHook((props) => useScadaPlayback(props), { initialProps: options }), options, advance };
}
beforeEach(() => vi.useFakeTimers());
afterEach(() => { cleanup(); vi.useRealTimers(); });
it("advances the display first and monitors once at each project boundary until the end", async () => {
let count = 0;
const advance = vi.fn(async () => {
count += 1;
return receipt(start + count * 900_000, count === 4 ? "completed" : "paused");
});
const { result } = setup(advance);
act(() => result.current.play());
await act(() => vi.advanceTimersByTimeAsync(2500));
expect(result.current.time).toBe(start + 450_000);
expect(advance).not.toHaveBeenCalled();
await act(() => vi.advanceTimersByTimeAsync(17500));
expect(advance).toHaveBeenCalledTimes(4);
expect(result.current.time).toBe(end);
expect(result.current.playing).toBe(false);
await act(() => vi.advanceTimersByTimeAsync(10000));
expect(advance).toHaveBeenCalledTimes(4);
});
it("waits for a slow diagnosis without overlapping requests or jumping ahead", async () => {
let resolve!: (value: ScadaReplay) => void;
const advance = vi.fn(() => new Promise<ScadaReplay>((done) => { resolve = done; }));
const { result } = setup(advance);
act(() => result.current.play());
await act(() => vi.advanceTimersByTimeAsync(20000));
expect(advance).toHaveBeenCalledTimes(1);
expect(result.current.time).toBe(start + 900_000);
act(() => result.current.pause());
await act(async () => resolve(receipt(start + 900_000)));
await act(() => vi.advanceTimersByTimeAsync(10000));
expect(advance).toHaveBeenCalledTimes(1);
expect(result.current.playing).toBe(false);
});
it("pauses when the panel becomes inactive and resumes from the same display time", async () => {
const { result, rerender, options, advance } = setup();
act(() => result.current.play());
await act(() => vi.advanceTimersByTimeAsync(2000));
expect(result.current.newRound).toBe(false);
const pausedAt = result.current.time;
rerender({ ...options, active: false });
await act(() => vi.advanceTimersByTimeAsync(10000));
expect(result.current.time).toBe(pausedAt);
expect(result.current.playing).toBe(false);
expect(advance).not.toHaveBeenCalled();
rerender(options);
act(() => result.current.play());
await act(() => vi.advanceTimersByTimeAsync(1000));
expect(result.current.time).toBe(pausedAt + 180_000);
});
it("pauses on a failed command and does not retry automatically", async () => {
const { result, advance } = setup(vi.fn(async () => null));
act(() => result.current.play());
await act(() => vi.advanceTimersByTimeAsync(15000));
expect(advance).toHaveBeenCalledTimes(1);
expect(result.current.playing).toBe(false);
});
it("uses the configured monitoring interval independently of the dial's five-minute ticks", async () => {
const { result, rerender, options, advance } = setup(vi.fn(async () => receipt(start + 300_000)));
rerender({ ...options, intervalSeconds: 300 });
act(() => result.current.play());
await act(() => vi.advanceTimersByTimeAsync(1700));
expect(advance).toHaveBeenCalledTimes(1);
expect(result.current.time).toBe(start + 300_000);
});
it("pauses for a hidden page and cleans up its automatic monitor on unmount", async () => {
const { result, unmount, advance } = setup();
act(() => result.current.play());
const visibility = vi.spyOn(document, "visibilityState", "get").mockReturnValue("hidden");
act(() => document.dispatchEvent(new Event("visibilitychange")));
await act(() => vi.advanceTimersByTimeAsync(10000));
expect(result.current.playing).toBe(false);
visibility.mockRestore();
act(() => result.current.play());
unmount();
await vi.advanceTimersByTimeAsync(10000);
expect(advance).not.toHaveBeenCalled();
});
it("resumes a restarted round before its first request without resetting the clock again", async () => {
const { result, rerender, options, advance } = setup();
rerender({ ...options, now: end, restart: true });
act(() => result.current.play());
await act(() => vi.advanceTimersByTimeAsync(2000));
expect(result.current.newRound).toBe(true);
const pausedAt = result.current.time;
act(() => result.current.pause());
act(() => result.current.play());
expect(result.current.time).toBe(pausedAt);
await act(() => vi.advanceTimersByTimeAsync(1000));
expect(result.current.time).toBe(pausedAt + 180_000);
expect(advance).not.toHaveBeenCalled();
});
it("keeps playing when the refreshed snapshot arrives before the command promise resolves", async () => {
let resolve!: (value: ScadaReplay) => void;
const { result, rerender, options } = setup(vi.fn(() => new Promise<ScadaReplay>((done) => { resolve = done; })));
act(() => result.current.play());
await act(() => vi.advanceTimersByTimeAsync(5000));
rerender({ ...options, now: start + 900_000 });
await act(async () => resolve(receipt(start + 900_000)));
expect(result.current.playing).toBe(true);
await act(() => vi.advanceTimersByTimeAsync(1000));
expect(result.current.time).toBe(start + 1080_000);
});
it("resets to a paused beginning without processing until play is pressed again", async () => {
const { result, rerender, options, advance } = setup(vi.fn(async () => receipt(start + 900_000)));
act(() => result.current.play());
await act(() => vi.advanceTimersByTimeAsync(2000));
expect(result.current.time).toBe(start + 360_000);
act(() => result.current.reset());
expect(result.current.time).toBe(start);
expect(result.current.playing).toBe(false);
expect(result.current.started).toBe(false);
expect(result.current.newRound).toBe(false);
rerender(options);
await act(() => vi.advanceTimersByTimeAsync(10000));
expect(advance).not.toHaveBeenCalled();
act(() => result.current.play());
await act(() => vi.advanceTimersByTimeAsync(5000));
expect(advance).toHaveBeenCalledTimes(1);
});