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, { 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: /^水泵/ })).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, { 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.poll(() => page.evaluate(() => ({ pump: Boolean(globalThis.__waterNetworkMap?.getLayer("pumps-symbol")), tank: Boolean(globalThis.__waterNetworkMap?.getLayer("tanks-symbol")) }))).toEqual({ pump: true, tank: true }); }); test("loads required map layers while optional layer probes are still pending", async ({ page }) => { let releaseProbe = () => {}; const probeGate = new Promise((resolve) => { releaseProbe = resolve; }); await mockRuntimeAndMap(page, { 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: { pumps: number; tanks: number }, options: { probeGate?: Promise } = {} ) { 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?.endsWith(":pumps") ? "pumps" : "tanks"; await route.fulfill({ contentType: "application/xml", body: `` }); }); }