Files
next-tjwater-frontend/tests/browser/scada-api-source.e2e.ts
T
jiang bf44cc0f25
Generic Container CI/CD / test-build-publish (push) Successful in 59s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 59s
fix: synchronize dynamic GeoServer layer lifecycle
Optional sources became dynamic while dependent labels and controller state remained static, allowing stale requests to restore removed layers. Register dependent layers through the same lifecycle and invalidate target request generations before release.
2026-09-11 18:25:41 +08:00

162 lines
6.4 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
import { mockScadaApi } from "./support/mock-scada-api";
test("loads SCADA from the backend API and aligns map features by device_id", async ({ page }) => {
await mockRuntimeAndMap(page, { valves: 0, reservoirs: 0, pumps: 0, tanks: 0 });
await mockScadaApi(page, [{
device_id: "SCADA-1",
device_type: "pressure",
node_id: "J-1",
link_id: null,
api_query_id: "query-1",
transmission_mode: "realtime",
transmission_frequency: "2s",
reliability: 100,
x: 1,
y: 2,
longitude: 121.5,
latitude: 30.9
}]);
const deviceRequestPromise = page.waitForRequest((request) =>
request.url().includes("/api/v1/scada-devices")
);
await page.goto("/", { waitUntil: "domcontentloaded" });
const deviceRequest = await deviceRequestPromise;
expect(deviceRequest.headers()["x-project-id"]).toBe("project-1");
await expect.poll(async () => page.evaluate(() => {
const map = globalThis.__waterNetworkMap;
if (!map) return [];
return map.querySourceFeatures("scada").map((feature) => ({
id: String(feature.id),
deviceId: String(feature.properties?.device_id)
}));
})).toContainEqual({ id: "SCADA-1", deviceId: "SCADA-1" });
const layerTool = page.getByRole("button", { name: /图层:管理地图图层/ });
await expect(layerTool).toBeEnabled();
await layerTool.click();
await expect(page.getByRole("button", { name: /^管线/ })).toBeVisible();
await expect(page.getByRole("button", { name: /^节点/ })).toBeVisible();
await expect(page.getByRole("button", { name: /^阀门/ })).toHaveCount(0);
await expect(page.getByRole("button", { name: /^水库/ })).toHaveCount(0);
await expect(page.getByRole("button", { name: /^水泵/ })).toHaveCount(0);
await expect(page.getByRole("button", { name: /^水箱/ })).toHaveCount(0);
});
test("shows data-dependent layer controls only when GeoServer reports features", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await mockRuntimeAndMap(page, { valves: 1, reservoirs: 1, pumps: 2, tanks: 1 });
await mockScadaApi(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
const layerTool = page.getByRole("button", { name: /图层:管理地图图层/ });
await expect(layerTool).toBeEnabled();
await layerTool.click();
await expect(page.getByRole("button", { name: /^阀门/ })).toBeVisible();
await expect(page.getByRole("button", { name: /^水库/ })).toBeVisible();
await expect(page.getByRole("button", { name: /^水泵/ })).toBeVisible();
await expect(page.getByRole("button", { name: /^水箱/ })).toBeVisible();
await expect.poll(() => page.evaluate(() => ({
valve: Boolean(globalThis.__waterNetworkMap?.getLayer("valves-symbol")),
valveLabel: Boolean(globalThis.__waterNetworkMap?.getLayer("workbench-value-label-valves")),
reservoir: Boolean(globalThis.__waterNetworkMap?.getLayer("reservoirs-symbol")),
reservoirLabel: Boolean(globalThis.__waterNetworkMap?.getLayer("workbench-value-label-reservoirs")),
pump: Boolean(globalThis.__waterNetworkMap?.getLayer("pumps-symbol")),
tank: Boolean(globalThis.__waterNetworkMap?.getLayer("tanks-symbol"))
}))).toEqual({
valve: true,
valveLabel: true,
reservoir: true,
reservoirLabel: true,
pump: true,
tank: true
});
await expect.poll(() => page.evaluate(() => {
const layerIds = globalThis.__waterNetworkMap?.getStyle().layers.map((layer) => layer.id) ?? [];
return {
valveLabel: layerIds.indexOf("workbench-value-label-valves"),
simulationLabel: layerIds.indexOf("simulation-burst-label")
};
})).toMatchObject({
valveLabel: expect.any(Number),
simulationLabel: expect.any(Number)
});
const layerOrder = await page.evaluate(() => {
const layerIds = globalThis.__waterNetworkMap?.getStyle().layers.map((layer) => layer.id) ?? [];
return [
layerIds.indexOf("workbench-value-label-valves"),
layerIds.indexOf("simulation-burst-label")
];
});
expect(layerOrder[0]).toBeGreaterThanOrEqual(0);
expect(layerOrder[0]).toBeLessThan(layerOrder[1]);
});
test("loads required map layers while optional layer probes are still pending", async ({ page }) => {
let releaseProbe = () => {};
const probeGate = new Promise<void>((resolve) => {
releaseProbe = resolve;
});
await mockRuntimeAndMap(
page,
{ valves: 0, reservoirs: 0, pumps: 1, tanks: 0 },
{ probeGate }
);
await mockScadaApi(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
try {
await expect.poll(() => page.evaluate(() => ({
pipes: Boolean(globalThis.__waterNetworkMap?.getLayer("pipes-casing")),
pumps: Boolean(globalThis.__waterNetworkMap?.getLayer("pumps-symbol"))
}))).toEqual({ pipes: true, pumps: false });
} finally {
releaseProbe();
}
await expect.poll(() => page.evaluate(() =>
Boolean(globalThis.__waterNetworkMap?.getLayer("pumps-symbol"))
)).toBe(true);
});
async function mockRuntimeAndMap(
page: Page,
featureCounts: Record<"valves" | "reservoirs" | "pumps" | "tanks", number>,
options: { probeGate?: Promise<void> } = {}
) {
await page.route("**/runtime-config.js", async (route) => {
await route.fulfill({
contentType: "application/javascript",
body: `globalThis.__TJWATER_CONFIG__ = {
TJWATER_AUTH_MODE: "disabled",
TJWATER_MAPBOX_ACCESS_TOKEN: "",
TJWATER_MAP_URL: "https://scada-map.invalid/geoserver",
TJWATER_GEOSERVER_WORKSPACE: "tjwater_next",
TJWATER_SERVER_API_BASE_URL: "https://tjwater-api.invalid",
TJWATER_AGENT_API_BASE_URL: "http://127.0.0.1:8787",
TJWATER_ENABLE_DEV_PANEL: "false",
TJWATER_ENABLE_MSW: "false"
};`
});
});
await page.route("https://scada-map.invalid/**", async (route) => {
await route.fulfill({ status: 204, body: "" });
});
await page.route("https://scada-map.invalid/geoserver/tjwater_next/ows?**", async (route) => {
await options.probeGate;
const typeNames = new URL(route.request().url()).searchParams.get("typeNames");
const sourceId = typeNames?.split(":").at(-1);
const featureCount = sourceId && sourceId in featureCounts
? featureCounts[sourceId as keyof typeof featureCounts]
: 0;
await route.fulfill({
contentType: "application/xml",
body: `<wfs:FeatureCollection numberMatched="${featureCount}" numberReturned="0" />`
});
});
}