145 lines
6.5 KiB
TypeScript
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);
|
|
});
|