fix: probe all non-core GeoServer layers

Extend availability checks beyond pumps and tanks so unpublished valve and reservoir layers cannot leave stale controls.
This commit is contained in:
2026-09-11 17:00:54 +08:00
parent cf699d6207
commit 31acd33a2e
4 changed files with 33 additions and 15 deletions
@@ -40,13 +40,15 @@ describe("GeoServer data-dependent layer availability", () => {
it("keeps only optional layers that contain features", async () => { it("keeps only optional layers that contain features", async () => {
const fetchMock = vi const fetchMock = vi
.fn() .fn()
.mockResolvedValueOnce(new Response('<wfs:FeatureCollection numberMatched="0" />'))
.mockResolvedValueOnce(new Response('<wfs:FeatureCollection numberMatched="0" />'))
.mockResolvedValueOnce(new Response('<wfs:FeatureCollection numberMatched="3" />')) .mockResolvedValueOnce(new Response('<wfs:FeatureCollection numberMatched="3" />'))
.mockResolvedValueOnce(new Response('<wfs:FeatureCollection numberMatched="0" />')); .mockResolvedValueOnce(new Response('<wfs:FeatureCollection numberMatched="0" />'));
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
await expect(resolveGeoServerLayerAvailability()).resolves.toEqual({ await expect(resolveGeoServerLayerAvailability()).resolves.toEqual({
availableSourceIds: ["pipes", "junctions", "valves", "reservoirs", "pumps"], availableSourceIds: ["pipes", "junctions", "pumps"],
emptySourceIds: ["tanks"], emptySourceIds: ["valves", "reservoirs", "tanks"],
failedSourceIds: [] failedSourceIds: []
}); });
}); });
@@ -64,13 +66,15 @@ describe("GeoServer data-dependent layer availability", () => {
const fetchMock = vi const fetchMock = vi
.fn() .fn()
.mockResolvedValueOnce(new Response("missing", { status: 503 })) .mockResolvedValueOnce(new Response("missing", { status: 503 }))
.mockResolvedValueOnce(new Response("<ServiceException />"))
.mockResolvedValueOnce(new Response("missing", { status: 503 }))
.mockResolvedValueOnce(new Response("<ServiceException />")); .mockResolvedValueOnce(new Response("<ServiceException />"));
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
await expect(resolveGeoServerLayerAvailability()).resolves.toEqual({ await expect(resolveGeoServerLayerAvailability()).resolves.toEqual({
availableSourceIds: ["pipes", "junctions", "valves", "reservoirs"], availableSourceIds: ["pipes", "junctions"],
emptySourceIds: [], emptySourceIds: [],
failedSourceIds: ["pumps", "tanks"] failedSourceIds: ["valves", "reservoirs", "pumps", "tanks"]
}); });
}); });
}); });
+2 -2
View File
@@ -23,8 +23,8 @@ describe("createWaterNetworkSources", () => {
expect(SUPPLY_LAYER_CATALOG.map((layer) => [layer.id, layer.available, layer.availability])).toEqual([ expect(SUPPLY_LAYER_CATALOG.map((layer) => [layer.id, layer.available, layer.availability])).toEqual([
["pipes", true, "required"], ["pipes", true, "required"],
["junctions", true, "required"], ["junctions", true, "required"],
["valves", true, "required"], ["valves", true, "probe"],
["reservoirs", true, "required"], ["reservoirs", true, "probe"],
["scada", true, "required"], ["scada", true, "required"],
["pumps", true, "probe"], ["pumps", true, "probe"],
["tanks", true, "probe"] ["tanks", true, "probe"]
+2 -2
View File
@@ -68,7 +68,7 @@ export const SUPPLY_LAYER_CATALOG = [
sourceLayer: SOURCE_LAYERS.valves, sourceLayer: SOURCE_LAYERS.valves,
geometry: "point", geometry: "point",
available: true, available: true,
availability: "required", availability: "probe",
label: "阀门", label: "阀门",
icon: "valve" icon: "valve"
}, },
@@ -77,7 +77,7 @@ export const SUPPLY_LAYER_CATALOG = [
sourceLayer: SOURCE_LAYERS.reservoirs, sourceLayer: SOURCE_LAYERS.reservoirs,
geometry: "point", geometry: "point",
available: true, available: true,
availability: "required", availability: "probe",
label: "水库", label: "水库",
icon: "reservoir" icon: "reservoir"
}, },
+21 -7
View File
@@ -2,7 +2,7 @@ import { expect, test, type Page } from "@playwright/test";
import { mockScadaApi } from "./support/mock-scada-api"; import { mockScadaApi } from "./support/mock-scada-api";
test("loads SCADA from the backend API and aligns map features by device_id", async ({ page }) => { test("loads SCADA from the backend API and aligns map features by device_id", async ({ page }) => {
await mockRuntimeAndMap(page, { pumps: 0, tanks: 0 }); await mockRuntimeAndMap(page, { valves: 0, reservoirs: 0, pumps: 0, tanks: 0 });
await mockScadaApi(page, [{ await mockScadaApi(page, [{
device_id: "SCADA-1", device_id: "SCADA-1",
device_type: "pressure", device_type: "pressure",
@@ -38,13 +38,16 @@ test("loads SCADA from the backend API and aligns map features by device_id", as
await expect(layerTool).toBeEnabled(); await expect(layerTool).toBeEnabled();
await layerTool.click(); 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: /^阀门/ })).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);
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 }) => { test("shows data-dependent layer controls only when GeoServer reports features", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 }); await page.setViewportSize({ width: 390, height: 844 });
await mockRuntimeAndMap(page, { pumps: 2, tanks: 1 }); await mockRuntimeAndMap(page, { valves: 1, reservoirs: 1, pumps: 2, tanks: 1 });
await mockScadaApi(page); await mockScadaApi(page);
await page.goto("/", { waitUntil: "domcontentloaded" }); await page.goto("/", { waitUntil: "domcontentloaded" });
@@ -52,12 +55,16 @@ test("shows data-dependent layer controls only when GeoServer reports features",
const layerTool = page.getByRole("button", { name: /图层:管理地图图层/ }); const layerTool = page.getByRole("button", { name: /图层:管理地图图层/ });
await expect(layerTool).toBeEnabled(); await expect(layerTool).toBeEnabled();
await layerTool.click(); 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(page.getByRole("button", { name: /^水箱/ })).toBeVisible(); await expect(page.getByRole("button", { name: /^水箱/ })).toBeVisible();
await expect.poll(() => page.evaluate(() => ({ 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")), pump: Boolean(globalThis.__waterNetworkMap?.getLayer("pumps-symbol")),
tank: Boolean(globalThis.__waterNetworkMap?.getLayer("tanks-symbol")) tank: Boolean(globalThis.__waterNetworkMap?.getLayer("tanks-symbol"))
}))).toEqual({ pump: true, tank: true }); }))).toEqual({ valve: true, reservoir: true, pump: true, tank: true });
}); });
test("loads required map layers while optional layer probes are still pending", async ({ page }) => { test("loads required map layers while optional layer probes are still pending", async ({ page }) => {
@@ -65,7 +72,11 @@ test("loads required map layers while optional layer probes are still pending",
const probeGate = new Promise<void>((resolve) => { const probeGate = new Promise<void>((resolve) => {
releaseProbe = resolve; releaseProbe = resolve;
}); });
await mockRuntimeAndMap(page, { pumps: 1, tanks: 0 }, { probeGate }); await mockRuntimeAndMap(
page,
{ valves: 0, reservoirs: 0, pumps: 1, tanks: 0 },
{ probeGate }
);
await mockScadaApi(page); await mockScadaApi(page);
await page.goto("/", { waitUntil: "domcontentloaded" }); await page.goto("/", { waitUntil: "domcontentloaded" });
@@ -86,7 +97,7 @@ test("loads required map layers while optional layer probes are still pending",
async function mockRuntimeAndMap( async function mockRuntimeAndMap(
page: Page, page: Page,
featureCounts: { pumps: number; tanks: number }, featureCounts: Record<"valves" | "reservoirs" | "pumps" | "tanks", number>,
options: { probeGate?: Promise<void> } = {} options: { probeGate?: Promise<void> } = {}
) { ) {
await page.route("**/runtime-config.js", async (route) => { await page.route("**/runtime-config.js", async (route) => {
@@ -110,10 +121,13 @@ async function mockRuntimeAndMap(
await page.route("https://scada-map.invalid/geoserver/tjwater_next/ows?**", async (route) => { await page.route("https://scada-map.invalid/geoserver/tjwater_next/ows?**", async (route) => {
await options.probeGate; await options.probeGate;
const typeNames = new URL(route.request().url()).searchParams.get("typeNames"); const typeNames = new URL(route.request().url()).searchParams.get("typeNames");
const sourceId = typeNames?.endsWith(":pumps") ? "pumps" : "tanks"; const sourceId = typeNames?.split(":").at(-1);
const featureCount = sourceId && sourceId in featureCounts
? featureCounts[sourceId as keyof typeof featureCounts]
: 0;
await route.fulfill({ await route.fulfill({
contentType: "application/xml", contentType: "application/xml",
body: `<wfs:FeatureCollection numberMatched="${featureCounts[sourceId]}" numberReturned="0" />` body: `<wfs:FeatureCollection numberMatched="${featureCount}" numberReturned="0" />`
}); });
}); });
} }