147 lines
6.3 KiB
TypeScript
147 lines
6.3 KiB
TypeScript
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<string, unknown> | undefined;
|
|
const junctionSelectedPaint = junctionSelectedLayer?.paint as Record<string, unknown> | 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<string>();
|
|
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<MapLibreMap, "loadImage" | "addImage" | "hasImage"> & {
|
|
loadImage: ReturnType<typeof vi.fn>;
|
|
addImage: ReturnType<typeof vi.fn>;
|
|
};
|
|
}
|
|
|
|
|
|
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);
|
|
});
|