fix(scada): avoid history refetch on map zoom
Generic Container CI/CD / test-build-publish (push) Successful in 1m22s
Frontend CI/CD v2 / build-test-publish-and-deploy (push) Successful in 1m23s

This commit is contained in:
2026-09-25 17:22:42 +08:00
parent 1b275206a9
commit c4a90be0d3
5 changed files with 178 additions and 14 deletions
@@ -0,0 +1,50 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { apiFetch } from "@/lib/apiFetch";
import SCADADataPanel from "./SCADADataPanel";
jest.mock("@/lib/apiFetch", () => ({ apiFetch: jest.fn() }));
jest.mock("@refinedev/core", () => ({
useGetIdentity: () => ({ data: { id: "test-user" } }),
useNotification: () => ({ open: jest.fn() }),
}));
jest.mock("@mui/x-date-pickers", () => ({
DateTimePicker: () => null,
LocalizationProvider: ({ children }: { children: React.ReactNode }) => children,
}));
jest.mock("echarts-for-react", () => () => null);
const response = (payload: unknown) =>
({ ok: true, json: async () => payload }) as Response;
describe("SCADADataPanel requests", () => {
beforeEach(() => {
jest.mocked(apiFetch).mockImplementation(async (url) =>
response(
String(url).includes("/scada-devices")
? [
{ device_id: "S-1", device_type: "pressure", node_id: "J-1", link_id: null },
{ device_id: "S-2", device_type: "pressure", node_id: "J-2", link_id: null },
]
: { series: [] },
),
);
});
afterEach(() => jest.clearAllMocks());
it("does not request history again when the same device IDs arrive in a new array", async () => {
const { rerender } = render(<SCADADataPanel deviceIds={["S-1"]} />);
await waitFor(() => expect(apiFetch).toHaveBeenCalledTimes(2));
rerender(<SCADADataPanel deviceIds={["S-1"]} />);
await waitFor(() => expect(apiFetch).toHaveBeenCalledTimes(2));
rerender(<SCADADataPanel deviceIds={["S-1", "S-2"]} />);
await waitFor(() => expect(apiFetch).toHaveBeenCalledTimes(4));
expect(jest.mocked(apiFetch).mock.calls[3][1]?.body).toContain('"device_ids":["S-2"]');
fireEvent.click(screen.getByRole("button", { name: "刷新" }));
await waitFor(() => expect(apiFetch).toHaveBeenCalledTimes(6));
});
});
+11 -5
View File
@@ -307,9 +307,15 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
const normalizedRange = useMemo(() => ensureValidRange(from, to), [from, to]);
const hasDevices = deviceIds.length > 0;
// Parent map updates can pass a new array with identical IDs. Keep the
// request dependency tied to the IDs themselves, not the array identity.
const deviceIdsKey = JSON.stringify(deviceIds);
const requestedDeviceIds = useMemo(
() => JSON.parse(deviceIdsKey) as string[],
[deviceIdsKey],
);
const hasDevices = requestedDeviceIds.length > 0;
const hasData = timeSeries.length > 0;
const deviceIdsKey = useMemo(() => deviceIds.join(","), [deviceIds]);
const dataset = useMemo(
() => buildDataset(timeSeries, deviceIds, fractionDigits, showCleaning),
@@ -348,7 +354,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
setError(null);
try {
const { from: rangeFrom, to: rangeTo } = normalizedRange;
const result = await customFetcher(deviceIds, {
const result = await customFetcher(requestedDeviceIds, {
from: rangeFrom.toDate(),
to: rangeTo.toDate(),
});
@@ -360,7 +366,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
setLoadingState("error");
}
},
[deviceIds, customFetcher, hasDevices, normalizedRange],
[requestedDeviceIds, customFetcher, hasDevices, normalizedRange],
);
// 处理数据清洗
@@ -447,7 +453,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
setTimeSeries([]);
setHistorySeries([]);
}
}, [deviceIdsKey, handleFetch, hasDevices]);
}, [handleFetch, hasDevices]);
// 当设备数量变化时,调整数据源选择
useEffect(() => {
@@ -1,5 +1,7 @@
import React from "react";
import { render, waitFor } from "@testing-library/react";
import { apiFetch } from "@/lib/apiFetch";
import { fetchHistoryData } from "./HistoryDataPanel";
import HistoryDataPanel, { fetchHistoryData } from "./HistoryDataPanel";
jest.mock("@/lib/apiFetch", () => ({
apiFetch: jest.fn(),
@@ -9,6 +11,12 @@ jest.mock("@components/olmap/core/MapComponent", () => ({
useData: () => null,
}));
jest.mock("@mui/x-date-pickers", () => ({
DateTimePicker: () => null,
LocalizationProvider: ({ children }: { children: React.ReactNode }) => children,
}));
jest.mock("echarts-for-react", () => () => null);
const jsonResponse = (payload: unknown, status = 200) =>
({
ok: status >= 200 && status < 300,
@@ -94,3 +102,39 @@ describe("fetchHistoryData", () => {
expect(apiFetch).toHaveBeenCalledTimes(1);
});
});
describe("HistoryDataPanel requests", () => {
beforeEach(() => {
jest.clearAllMocks();
jest.mocked(apiFetch).mockResolvedValue(jsonResponse({ series: [] }));
});
afterEach(() => jest.clearAllMocks());
it("does not reload unchanged elements when its parent rerenders", async () => {
const props = { type: "none" as const, onClose: jest.fn() };
const { rerender } = render(
React.createElement(HistoryDataPanel, {
...props,
featureInfos: [["J-1", "junction"]],
}),
);
await waitFor(() => expect(apiFetch).toHaveBeenCalledTimes(1));
rerender(
React.createElement(HistoryDataPanel, {
...props,
featureInfos: [["J-1", "junction"]],
}),
);
await waitFor(() => expect(apiFetch).toHaveBeenCalledTimes(1));
rerender(
React.createElement(HistoryDataPanel, {
...props,
featureInfos: [["J-2", "junction"]],
}),
);
await waitFor(() => expect(apiFetch).toHaveBeenCalledTimes(2));
});
});
@@ -258,12 +258,13 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
const normalizedRange = useMemo(() => ensureValidRange(from, to), [from, to]);
const hasDevices = deviceIds.length > 0;
const hasData = timeSeries.length > 0;
const featureInfosKey = useMemo(
() => JSON.stringify(featureInfos),
[featureInfos]
const featureInfosKey = JSON.stringify(featureInfos);
const requestedFeatureInfos = useMemo(
() => JSON.parse(featureInfosKey) as [string, string][],
[featureInfosKey],
);
const hasDevices = requestedFeatureInfos.length > 0;
const hasData = timeSeries.length > 0;
const dataset = useMemo(
() => buildDataset(timeSeries, historySeries, fractionDigits),
@@ -289,7 +290,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
try {
const { from: rangeFrom, to: rangeTo } = normalizedRange;
const result = await fetchHistoryData(
featureInfos,
requestedFeatureInfos,
{
from: rangeFrom.toDate(),
to: rangeTo.toDate(),
@@ -313,7 +314,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
setLoadingState("error");
}
},
[featureInfos, hasDevices, normalizedRange, runId, type],
[requestedFeatureInfos, hasDevices, normalizedRange, runId, type],
);
useEffect(
@@ -331,7 +332,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
setTimeSeries([]);
setHistorySeries([]);
}
}, [featureInfosKey, handleFetch, hasDevices]);
}, [handleFetch, hasDevices]);
const columns: GridColDef[] = useMemo(() => {
const base: GridColDef[] = [