Files
next-tjwater-drainage-frontend/features/workbench/map/scada.test.ts
T

105 lines
4.2 KiB
TypeScript

import type { Map as MapLibreMap } from "maplibre-gl";
import { describe, expect, it, vi } from "vitest";
import {
SCADA_HIT_LAYER_ID,
SCADA_ICON_EXPRESSION,
SCADA_IMAGE_IDS,
SCADA_MAP_ICON_PATHS,
registerScadaImages,
scadaFallbackLayers,
scadaLayers
} from "./scada";
import { waterNetworkLayers } from "./layers";
describe("SCADA map presentation", () => {
it("maps the three authoritative device types and falls back to unknown", () => {
expect(SCADA_ICON_EXPRESSION).toEqual([
"match", ["to-number", ["get", "device_type_id"]],
72, SCADA_IMAGE_IDS.waterQuality,
58, SCADA_IMAGE_IDS.radarLevel,
45, SCADA_IMAGE_IDS.ultrasonicFlow,
SCADA_IMAGE_IDS.unknown
]);
});
it("keeps the hit target last in normal and fallback presentations", () => {
expect(scadaLayers.at(-1)?.id).toBe(SCADA_HIT_LAYER_ID);
expect(scadaFallbackLayers.at(-1)?.id).toBe(SCADA_HIT_LAYER_ID);
});
it("keeps the icon and selected ring consistent with junction selection", () => {
const symbolLayer = scadaLayers.find((layer) => layer.id === "scada-symbol");
const outerLayer = scadaLayers.find((layer) => layer.id === "scada-selected-outer");
const selectedLayer = scadaLayers.find((layer) => layer.id === "scada-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-size": 0.75 } });
expect(outerLayer).toMatchObject({
type: "circle",
paint: {
"circle-radius": 12.5,
"circle-stroke-width": junctionOuterPaint?.["circle-stroke-width"]
}
});
expect(selectedLayer).toMatchObject({
type: "circle",
paint: {
"circle-stroke-width": junctionSelectedPaint?.["circle-stroke-width"]
}
});
});
it("uses MapLibre-compatible PNG assets", () => {
expect(Object.values(SCADA_MAP_ICON_PATHS)).toHaveLength(4);
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(4);
expect(map.addImage).toHaveBeenCalledTimes(4);
expect(map.addImage).toHaveBeenCalledWith(expect.any(String), image, { pixelRatio: 2 });
});
it("falls back to the unknown image when one category fails", async () => {
const image = { width: 64, height: 64 };
const map = createImageMap((path) => path.includes("radar-level")
? Promise.reject(new Error("decode failed"))
: Promise.resolve({ data: image }));
await expect(registerScadaImages(map)).resolves.toEqual({
status: "degraded",
failedImageIds: [SCADA_IMAGE_IDS.radarLevel]
});
expect(map.addImage).toHaveBeenCalledWith(SCADA_IMAGE_IDS.radarLevel, 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[0]?.id).toBe("scada-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>;
};
}