import type { Map as MapLibreMap } from "maplibre-gl"; import { describe, expect, it, vi } from "vitest"; import { SCADA_HIT_LAYER_IDS, SCADA_ICON_EXPRESSION, SCADA_IMAGE_IDS, SCADA_MAP_ICON_PATHS, registerScadaImages, scadaFallbackLayers, scadaLayers } from "./scada"; import { waterNetworkLayers } from "./layers"; import { MAP_STYLE_TOKENS } from "./map-colors"; import { SUPPLY_LAYER_VISUALS } from "./map-layer-visuals"; describe("SCADA map presentation", () => { it("maps the authoritative device types and falls back to unknown", () => { expect(SCADA_ICON_EXPRESSION).toEqual([ "match", ["downcase", ["to-string", ["coalesce", ["get", "device_type"], ""]]], "pipe_flow", SCADA_IMAGE_IDS.flow, "pressure", SCADA_IMAGE_IDS.pressure, SCADA_IMAGE_IDS.pressure ]); }); it("keeps the hit target last in normal and fallback presentations", () => { expect(scadaLayers.slice(-2).map((layer) => layer.id)).toEqual(SCADA_HIT_LAYER_IDS); expect(scadaFallbackLayers.slice(-2).map((layer) => layer.id)).toEqual(SCADA_HIT_LAYER_IDS); }); it("renders every SCADA layer from GeoJSON without a GeoServer source-layer", () => { [...scadaLayers, ...scadaFallbackLayers].forEach((layer) => { expect("source" in layer ? layer.source : undefined).toBe("scada"); expect(layer).not.toHaveProperty("source-layer"); }); }); it("keeps the icon and selected ring consistent with junction selection", () => { const symbolLayer = scadaLayers.find((layer) => layer.id === "scada-pressure-symbol"); const outerLayer = scadaLayers.find((layer) => layer.id === "scada-pressure-selected-outer"); const selectedLayer = scadaLayers.find((layer) => layer.id === "scada-pressure-selected"); const junctionOuterLayer = waterNetworkLayers.find((layer) => layer.id === "junctions-selected-outer"); const junctionSelectedLayer = waterNetworkLayers.find((layer) => layer.id === "junctions-selected"); const junctionOuterPaint = junctionOuterLayer?.paint as Record | undefined; const junctionSelectedPaint = junctionSelectedLayer?.paint as Record | undefined; expect(symbolLayer).toMatchObject({ type: "symbol", layout: { "icon-image": SCADA_IMAGE_IDS.pressure, "icon-size": SUPPLY_LAYER_VISUALS.scada.iconSize } }); expect(outerLayer).toMatchObject({ type: "circle", paint: { "circle-radius": SUPPLY_LAYER_VISUALS.scada.selectedRadius, "circle-stroke-width": junctionOuterPaint?.["circle-stroke-width"] } }); expect(selectedLayer).toMatchObject({ type: "circle", paint: { "circle-stroke-width": junctionSelectedPaint?.["circle-stroke-width"] } }); expect(JSON.stringify((selectedLayer as { paint?: unknown })?.paint)).toContain("highlighted"); }); it("uses distinct colors for authoritative pressure and pipe flow types", () => { const pressureFill = scadaLayers.find((layer) => layer.id === "scada-pressure-fill"); const flowFill = scadaLayers.find((layer) => layer.id === "scada-flow-fill"); expect(JSON.stringify((pressureFill as { paint?: unknown })?.paint)).toContain(MAP_STYLE_TOKENS.supply.scadaPressure); expect(JSON.stringify((flowFill as { paint?: unknown })?.paint)).toContain(MAP_STYLE_TOKENS.supply.scadaFlow); expect(JSON.stringify(pressureFill && "filter" in pressureFill ? pressureFill.filter : undefined)).toContain("pressure"); expect(JSON.stringify(flowFill && "filter" in flowFill ? flowFill.filter : undefined)).toContain("pipe_flow"); }); it("uses MapLibre-compatible PNG assets", () => { expect(Object.values(SCADA_MAP_ICON_PATHS)).toHaveLength(2); Object.values(SCADA_MAP_ICON_PATHS).forEach((path) => expect(path).toMatch(/\.png$/)); }); it("registers all map images at two-times pixel density", async () => { const image = { width: 64, height: 64 }; const map = createImageMap(() => Promise.resolve({ data: image })); await expect(registerScadaImages(map)).resolves.toEqual({ status: "ready", failedImageIds: [] }); expect(map.loadImage).toHaveBeenCalledTimes(2); expect(map.addImage).toHaveBeenCalledTimes(2); expect(map.addImage).toHaveBeenCalledWith(expect.any(String), image, { pixelRatio: 2 }); }); it("falls back to the pressure image when flow fails", async () => { const image = { width: 64, height: 64 }; const map = createImageMap((path) => path.includes("scada-flow") ? Promise.reject(new Error("decode failed")) : Promise.resolve({ data: image })); await expect(registerScadaImages(map)).resolves.toEqual({ status: "degraded", failedImageIds: [SCADA_IMAGE_IDS.flow] }); expect(map.addImage).toHaveBeenCalledWith(SCADA_IMAGE_IDS.flow, image, { pixelRatio: 2 }); }); it("returns unavailable instead of rejecting when the fallback image fails", async () => { const map = createImageMap(() => Promise.reject(new Error("unsupported image"))); await expect(registerScadaImages(map)).resolves.toEqual({ status: "unavailable", failedImageIds: Object.values(SCADA_IMAGE_IDS) }); expect(scadaFallbackLayers.map((layer) => layer.id).slice(0, 3)).toEqual([ "scada-pressure-casing", "scada-pressure-fill", "scada-pressure-fallback" ]); }); }); function createImageMap(loadImage: (path: string) => Promise<{ data: { width: number; height: number } }>) { const loadedImageIds = new Set(); return { loadImage: vi.fn(loadImage), hasImage: vi.fn((imageId: string) => loadedImageIds.has(imageId)), addImage: vi.fn((imageId: string) => loadedImageIds.add(imageId)) } as unknown as Pick & { loadImage: ReturnType; addImage: ReturnType; }; } it("does not access a disposed map after SCADA image loading completes", async () => { const controller = new AbortController(); let resolve!: (value: { data: { width: number; height: number } }) => void; const map = createImageMap(() => new Promise((done) => { resolve = done; })); const pending = registerScadaImages(map, controller.signal); controller.abort(); resolve({ data: { width: 64, height: 64 } }); await expect(pending).resolves.toMatchObject({ status: "cancelled" }); expect(map.addImage).not.toHaveBeenCalled(); expect(map.loadImage).toHaveBeenCalledTimes(1); });