Files
next-tjwater-frontend/tests/browser/scada-api-source.e2e.ts
T
jiang 31acd33a2e fix: probe all non-core GeoServer layers
Extend availability checks beyond pumps and tanks so unpublished valve and reservoir layers cannot leave stale controls.
2026-09-11 17:00:54 +08:00

134 lines
5.3 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")),
reservoir: Boolean(globalThis.__waterNetworkMap?.getLayer("reservoirs-symbol")),
pump: Boolean(globalThis.__waterNetworkMap?.getLayer("pumps-symbol")),
tank: Boolean(globalThis.__waterNetworkMap?.getLayer("tanks-symbol"))
}))).toEqual({ valve: true, reservoir: true, pump: true, tank: true });
});
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" />`
});
});
}