265 lines
6.8 KiB
TypeScript
265 lines
6.8 KiB
TypeScript
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 数据暂不可用,当前仅显示模拟结果。",
|
|
]);
|
|
});
|
|
});
|