import { buildSceneFrame, fetchPressureDevices, fetchSceneFrame, supportsThreeDimensionalScene, type SceneModelIndex, } from "./sceneData"; const mockApiFetch = jest.fn(); jest.mock("@/lib/apiFetch", () => ({ apiFetch: (...args: unknown[]) => mockApiFetch(...args), })); const model: SceneModelIndex = { modelId: "zjb-water-network-v23", nodeIds: new Set(["J-1", "J-2"]), linkIds: new Set(["P-1", "P-2"]), }; describe("buildSceneFrame", () => { beforeEach(() => { mockApiFetch.mockReset(); }); it("enables the scene only for the normalized ZJB project code", () => { expect(supportsThreeDimensionalScene(" ZJB ")).toBe(true); expect(supportsThreeDimensionalScene("tjwater_v2")).toBe(false); expect(supportsThreeDimensionalScene(null)).toBe(false); }); it("combines one complete simulation frame and overrides pressure with cleaned SCADA", () => { const frame = buildSceneFrame({ queryTime: new Date("2026-09-11T03:00:00.000Z"), model, nodeRows: [ { time: "2026-09-11T03:00:00.000Z", node_id: "J-1", pressure: 31.2, }, { time: "2026-09-11T03:00:00.000Z", node_id: "J-2", pressure: 29.7, }, ], linkRows: [ { time: "2026-09-11T03:00:00.000Z", link_id: "P-1", velocity: 0.82, flow: -18.4, status: 1, }, ], pressureDevices: [ { device_id: "S-1", device_type: "pressure", node_id: "J-1" }, ], scadaRows: [ { time: "2026-09-11T03:00:01.000Z", device_id: "S-1", monitored_value: 30.8, cleaned_value: 30.9, }, ], }); expect(frame.resultTime).toBe("2026-09-11T03:00:00.000Z"); expect(frame.payload?.nodes["J-1"]).toEqual({ pressure: 30.9, source: "scada", deviceId: "S-1", simulationPressure: 31.2, }); expect(frame.payload?.links["P-1"]).toEqual({ velocity: 0.82, flow: -66.24, direction: -1, status: "open", }); expect(frame.stats).toMatchObject({ simulationNodes: 2, simulationLinks: 1, scadaOverrides: 1, missingNodes: 0, missingLinks: 1, }); }); it("uses monitored SCADA when cleaned data is absent", () => { const frame = buildSceneFrame({ queryTime: new Date("2026-09-11T03:00:00.000Z"), model, nodeRows: [ { time: "2026-09-11T03:00:00.000Z", node_id: "J-1", pressure: 31.2, }, ], linkRows: [ { time: "2026-09-11T03:00:00.000Z", link_id: "P-1", velocity: 0.2, flow: 2, status: 0, }, ], pressureDevices: [ { device_id: "S-1", device_type: "pressure", node_id: "J-1" }, ], scadaRows: [ { time: "2026-09-11T03:00:00.000Z", device_id: "S-1", monitored_value: 30.8, cleaned_value: null, }, ], }); expect(frame.payload?.nodes["J-1"]?.pressure).toBe(30.8); expect(frame.payload?.links["P-1"]?.status).toBe("closed"); }); it("keeps the selected time and returns no payload when a complete frame is absent", () => { const frame = buildSceneFrame({ queryTime: new Date("2026-09-11T03:00:00.000Z"), model, nodeRows: [ { time: "2026-09-11T02:45:00.000Z", node_id: "J-1", pressure: 31.2, }, ], linkRows: [], pressureDevices: [], scadaRows: [], }); expect(frame.selectedTime).toBe("2026-09-11T03:00:00.000Z"); expect(frame.resultTime).toBeNull(); expect(frame.payload).toBeNull(); expect(frame.stats.missingNodes).toBe(2); expect(frame.stats.missingLinks).toBe(2); }); it("ignores IDs outside the scene model instead of rejecting the frame", () => { const frame = buildSceneFrame({ queryTime: new Date("2026-09-11T03:00:00.000Z"), model, nodeRows: [ { time: "2026-09-11T03:00:00.000Z", node_id: "J-UNKNOWN", pressure: 31.2, }, ], linkRows: [ { time: "2026-09-11T03:00:00.000Z", link_id: "P-UNKNOWN", velocity: 0.2, flow: 2, status: 2, }, ], pressureDevices: [], scadaRows: [], }); expect(frame.payload?.nodes).toEqual({}); expect(frame.payload?.links).toEqual({}); expect(frame.stats.ignoredElements).toBe(2); }); it("reads pressure mappings from the paginated SCADA device response", async () => { mockApiFetch.mockResolvedValue({ ok: true, json: async () => ({ items: [ { device_id: "S-1", device_type: "pressure", node_id: " J-1 " }, { device_id: "S-2", device_type: "flow", node_id: "J-2" }, { device_id: "S-3", device_type: "pressure", node_id: null }, ], total: 3, limit: 1000, offset: 0, }), }); const devices = await fetchPressureDevices(new AbortController().signal); expect(devices).toEqual([ { device_id: "S-1", device_type: "pressure", node_id: "J-1", measurement_unit: "m", }, ]); expect(mockApiFetch).toHaveBeenCalledWith( expect.stringContaining("/api/v1/scada-devices?limit=1000&offset=0"), expect.objectContaining({ signal: expect.any(AbortSignal) }), ); }); it("keeps simulation results when SCADA readings are temporarily unavailable", async () => { mockApiFetch .mockResolvedValueOnce({ ok: true, json: async () => [ { time: "2026-09-11T03:00:00.000Z", node_id: "J-1", pressure: 31.2, }, ], }) .mockResolvedValueOnce({ ok: true, json: async () => [ { time: "2026-09-11T03:00:00.000Z", link_id: "P-1", velocity: 0.8, flow: 12, status: 1, }, ], }) .mockResolvedValueOnce({ ok: true, json: async () => ({ FLOW_UNITS: "LPS", PRESSURE_UNITS: "METERS" }), }) .mockResolvedValueOnce({ ok: false, status: 503, text: async () => "SCADA service unavailable", }); const frame = await fetchSceneFrame({ queryTime: new Date("2026-09-11T03:00:00.000Z"), model, pressureDevices: [ { device_id: "S-1", device_type: "pressure", node_id: "J-1" }, ], signal: new AbortController().signal, }); expect(frame.payload?.nodes["J-1"]).toEqual({ pressure: 31.2, source: "simulation", }); expect(frame.stats.scadaOverrides).toBe(0); expect(frame.warnings).toEqual([ "SCADA 数据暂不可用,当前仅显示模拟结果。", ]); }); });