The scene previously returned early when realtime rows were absent and selected map-tier assets for network mode, hiding SCADA markers and station interiors and floors. Use SCADA timestamps as valid frames, render node-aligned monitor markers, and pin the full network context in the manifest regression test. Keep the timeline close action inside its drag header.
336 lines
8.9 KiB
TypeScript
336 lines
8.9 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("builds a SCADA-only frame when the matching simulation frame is absent", () => {
|
|
const frame = buildSceneFrame({
|
|
queryTime: new Date("2026-09-14T09:30:00.000Z"),
|
|
model,
|
|
nodeRows: [],
|
|
linkRows: [],
|
|
pressureDevices: [
|
|
{ device_id: "S-1", device_type: "pressure", node_id: "J-1" },
|
|
],
|
|
scadaRows: [
|
|
{
|
|
time: "2026-09-14T09:30:00.000Z",
|
|
device_id: "S-1",
|
|
monitored_value: 30.8,
|
|
cleaned_value: 30.9,
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(frame.resultTime).toBe("2026-09-14T09:30:00.000Z");
|
|
expect(frame.payload?.nodes["J-1"]).toEqual({
|
|
pressure: 30.9,
|
|
source: "scada",
|
|
deviceId: "S-1",
|
|
});
|
|
expect(frame.payload?.links).toEqual({});
|
|
expect(frame.stats).toMatchObject({
|
|
simulationNodes: 0,
|
|
simulationLinks: 0,
|
|
scadaOverrides: 1,
|
|
});
|
|
expect(frame.warnings).toEqual([
|
|
"当前时刻无在线模拟结果,已显示 SCADA 监测点。",
|
|
]);
|
|
});
|
|
|
|
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 数据暂不可用,当前仅显示模拟结果。",
|
|
]);
|
|
});
|
|
|
|
it("keeps the SCADA-only warning returned by the frame builder", async () => {
|
|
mockApiFetch
|
|
.mockResolvedValueOnce({ ok: true, json: async () => [] })
|
|
.mockResolvedValueOnce({ ok: true, json: async () => [] })
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ FLOW_UNITS: "LPS", PRESSURE_UNITS: "METERS" }),
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => [
|
|
{
|
|
time: "2026-09-14T09:30:00.000Z",
|
|
device_id: "S-1",
|
|
monitored_value: 30.8,
|
|
cleaned_value: 30.9,
|
|
},
|
|
],
|
|
});
|
|
|
|
const frame = await fetchSceneFrame({
|
|
queryTime: new Date("2026-09-14T09:30:00.000Z"),
|
|
model,
|
|
pressureDevices: [
|
|
{ device_id: "S-1", device_type: "pressure", node_id: "J-1" },
|
|
],
|
|
signal: new AbortController().signal,
|
|
});
|
|
|
|
expect(frame.stats.scadaOverrides).toBe(1);
|
|
expect(frame.warnings).toEqual([
|
|
"当前时刻无在线模拟结果,已显示 SCADA 监测点。",
|
|
]);
|
|
});
|
|
});
|