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(); });