Files
next-tjwater-frontend/src/features/workbench/hooks/use-scada-alert-notices.test.tsx
T

98 lines
5.5 KiB
TypeScript

import { act, cleanup, renderHook } from "@testing-library/react";
import { afterEach, expect, it, vi } from "vitest";
import { dismissMapNotice, showMapNotice } from "@/features/map/core";
import type { WorkbenchAlert } from "../types";
import { useScadaAlertNotices } from "./use-scada-alert-notices";
vi.mock("@/features/map/core", () => ({ showMapNotice: vi.fn(), dismissMapNotice: vi.fn() }));
const alert: WorkbenchAlert = {
id: "incident-1", title: "SCADA 指标异常", description: "已启动调查,详细诊断见调查报告。",
time: "08:00", severity: "warning", conditionId: "operational-task:task-1",
source: { kind: "scada_replay", replayId: "replay-1", ruleId: "scada_diagnosis", batchSequence: 2 }
};
afterEach(() => { cleanup(); sessionStorage.clear(); vi.clearAllMocks(); });
it("restores old notifications quietly and announces each newly persisted SCADA incident once", () => {
const onOpen = vi.fn();
const { result, rerender } = renderHook(({ alerts }: { alerts: WorkbenchAlert[] | null }) =>
useScadaAlertNotices("project-1", alerts, onOpen), { initialProps: { alerts: null as WorkbenchAlert[] | null } });
rerender({ alerts: [alert] });
expect(showMapNotice).not.toHaveBeenCalled();
expect(result.current.atmosphere?.alert).toEqual(alert);
const next = { ...alert, id: "incident-2", conditionId: "operational-task:task-2" };
rerender({ alerts: [alert, next] });
rerender({ alerts: [alert, { ...next }] });
expect(showMapNotice).toHaveBeenCalledTimes(1);
expect(result.current.atmosphere).toMatchObject({ preview: false, count: 2 });
act(() => result.current.dismissAtmosphere(result.current.atmosphere!.id));
rerender({ alerts: [alert, { ...next }] });
expect(result.current.atmosphere).toBeNull();
expect(showMapNotice).toHaveBeenCalledWith(expect.objectContaining({
title: alert.title, tone: "warning", position: "top-center", actionLabel: "查看调查", duration: Infinity
}));
vi.mocked(showMapNotice).mock.calls[0][0].onAction?.();
expect(onOpen).toHaveBeenCalledWith(next);
expect(dismissMapNotice).not.toHaveBeenCalled();
});
it("retains linked notices after recovery and never reannounces the same incident", () => {
const { result, rerender } = renderHook(({ alerts }) => useScadaAlertNotices("project-1", alerts, vi.fn()),
{ initialProps: { alerts: [] as WorkbenchAlert[] } });
rerender({ alerts: [alert] });
expect(result.current.atmosphere?.alert).toEqual(alert);
rerender({ alerts: [] });
expect(result.current.atmosphere).toBeNull();
expect(dismissMapNotice).not.toHaveBeenCalled();
rerender({ alerts: [alert] });
expect(showMapNotice).toHaveBeenCalledTimes(1);
});
it("clears recovered data-quality notices without task links", () => {
const { rerender } = renderHook(({ alerts }) => useScadaAlertNotices("project-1", alerts, vi.fn()),
{ initialProps: { alerts: [] as WorkbenchAlert[] } });
rerender({ alerts: [{ ...alert, conditionId: undefined }] });
rerender({ alerts: [] });
expect(dismissMapNotice).toHaveBeenCalledWith("scada-alert:project-1:incident-1");
});
it("clears popups when leaving a project and keeps a different project's initial alerts quiet", () => {
const { result, rerender, unmount } = renderHook(({ project, alerts }) =>
useScadaAlertNotices(project, alerts, vi.fn()),
{ initialProps: { project: "project-1", alerts: [] as WorkbenchAlert[] } });
rerender({ project: "project-1", alerts: [alert] });
rerender({ project: "project-2", alerts: [alert] });
expect(result.current.atmosphere?.projectId).toBe("project-2");
expect(dismissMapNotice).toHaveBeenCalledWith("scada-alert:project-1:incident-1");
expect(showMapNotice).toHaveBeenCalledTimes(1);
rerender({ project: "project-2", alerts: [alert, { ...alert, id: "incident-2", severity: "critical" }] });
expect(showMapNotice).toHaveBeenLastCalledWith(expect.objectContaining({ tone: "error" }));
unmount();
expect(dismissMapNotice).toHaveBeenCalledWith("scada-alert:project-2:incident-2");
});
it("ignores other sources and leaves data quality notices without a fabricated investigation link", () => {
const { result, rerender } = renderHook(({ alerts }) => useScadaAlertNotices("project-1", alerts, vi.fn()),
{ initialProps: { alerts: [] as WorkbenchAlert[] } });
rerender({ alerts: [{ ...alert, source: undefined }] });
expect(showMapNotice).not.toHaveBeenCalled();
rerender({ alerts: [{ ...alert, conditionId: undefined }] });
expect(showMapNotice).toHaveBeenLastCalledWith(expect.objectContaining({ actionLabel: undefined, onAction: undefined }));
expect(result.current.atmosphere).toBeNull();
rerender({ alerts: [{ ...alert, id: "quality-only", source: { ...alert.source!, kind: "scada_replay", replayId: "replay-1", ruleId: "quality-P1", batchSequence: 2 } }] });
expect(result.current.atmosphere).toBeNull();
});
it("previews locally and an older dismissal cannot clear a newer real incident", () => {
const { result, rerender } = renderHook(({ project, alerts }) => useScadaAlertNotices(project, alerts, vi.fn()),
{ initialProps: { project: "project-1", alerts: [] as WorkbenchAlert[] } });
act(() => result.current.previewAtmosphere());
const preview = result.current.atmosphere!;
expect(preview).toMatchObject({ preview: true, alert: null, projectId: "project-1" });
expect(showMapNotice).not.toHaveBeenCalled();
rerender({ project: "project-1", alerts: [alert] });
act(() => result.current.dismissAtmosphere(preview.id));
expect(result.current.atmosphere?.alert).toEqual(alert);
rerender({ project: "project-2", alerts: [] });
expect(result.current.atmosphere).toBeNull();
});