Files
TJWaterFrontend_Refine/src/components/threeDimensional/sceneData.test.ts
T

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