ZouitLayer (GeoJSON-полигоны из geom_geojson, severity blocker/warning/info по keyword, popup тип+имя) + ZouitLayerControlPanel (collapsible toggle per-severity, счётчик mappable) + SiteMap integration (default blocker+warning ON, info OFF, фон под POI). Graceful: нет geom_geojson → слой/панель скрыты, текстовый NspdZouitOverlapsBlock остаётся. 10 vitest. Closes #255
138 lines
4.6 KiB
TypeScript
138 lines
4.6 KiB
TypeScript
/**
|
||
* Unit tests for the #255 ЗОУИТ-layer pure logic: severity classification,
|
||
* geom_geojson parsing (the graceful-skip contract), and grouping.
|
||
*
|
||
* The layer renders Leaflet <GeoJSON> (jsdom can't mount that meaningfully), so
|
||
* we test the data-shaping that decides WHAT gets drawn and HOW it's colored —
|
||
* that's where the bugs live (wrong severity → wrong color; bad parse → ocean
|
||
* polygon or crash).
|
||
*/
|
||
import type { NspdZouitOverlap } from "@/types/nspd";
|
||
import {
|
||
classifyZouit,
|
||
groupZouitBySeverity,
|
||
parseZouitGeometry,
|
||
} from "../ZouitLayer";
|
||
|
||
function overlap(partial: Partial<NspdZouitOverlap>): NspdZouitOverlap {
|
||
return {
|
||
group_key: "cad_zouit",
|
||
layer: "",
|
||
subcategory: null,
|
||
name: null,
|
||
...partial,
|
||
};
|
||
}
|
||
|
||
// A minimal valid GeoJSON Polygon string (как ST_AsGeoJSON отдаёт).
|
||
const POLYGON_STR = JSON.stringify({
|
||
type: "Polygon",
|
||
coordinates: [
|
||
[
|
||
[60.6, 56.83],
|
||
[60.61, 56.83],
|
||
[60.61, 56.84],
|
||
[60.6, 56.83],
|
||
],
|
||
],
|
||
});
|
||
|
||
describe("classifyZouit", () => {
|
||
it("classifies pipeline / electricity / gas zones as blocker", () => {
|
||
expect(
|
||
classifyZouit(overlap({ type_zone: "Охранная зона трубопровода" })),
|
||
).toBe("blocker");
|
||
expect(
|
||
classifyZouit(
|
||
overlap({ type_zone: "Охранная зона электросетевого хозяйства" }),
|
||
),
|
||
).toBe("blocker");
|
||
expect(
|
||
classifyZouit(overlap({ type_zone: "Охранная зона газопровода" })),
|
||
).toBe("blocker");
|
||
});
|
||
|
||
it("classifies СЗЗ and heat-network zones as warning", () => {
|
||
expect(
|
||
classifyZouit(
|
||
overlap({ type_zone: "Санитарно-защитная зона предприятия" }),
|
||
),
|
||
).toBe("warning");
|
||
expect(
|
||
classifyZouit(overlap({ type_zone: "Охранная зона тепловой сети" })),
|
||
).toBe("warning");
|
||
});
|
||
|
||
it("classifies servitude / unknown zones as info", () => {
|
||
expect(
|
||
classifyZouit(
|
||
overlap({ type_zone: "Публичный сервитут в целях размещения сети" }),
|
||
),
|
||
).toBe("info");
|
||
expect(classifyZouit(overlap({ type_zone: null, name: null }))).toBe(
|
||
"info",
|
||
);
|
||
});
|
||
|
||
it("falls back to name when type_zone is absent (nspd-dump path)", () => {
|
||
expect(
|
||
classifyZouit(overlap({ name: "Охранная зона трубопровода с кад. №…" })),
|
||
).toBe("blocker");
|
||
});
|
||
});
|
||
|
||
describe("parseZouitGeometry", () => {
|
||
it("parses a valid GeoJSON Polygon string", () => {
|
||
const geom = parseZouitGeometry(POLYGON_STR);
|
||
expect(geom?.type).toBe("Polygon");
|
||
});
|
||
|
||
it("returns null for missing / empty / invalid input (graceful skip)", () => {
|
||
expect(parseZouitGeometry(null)).toBeNull();
|
||
expect(parseZouitGeometry(undefined)).toBeNull();
|
||
expect(parseZouitGeometry("")).toBeNull();
|
||
expect(parseZouitGeometry("{not json")).toBeNull();
|
||
});
|
||
|
||
it("returns null for non-polygon geometry (Point/LineString not drawn)", () => {
|
||
expect(
|
||
parseZouitGeometry(
|
||
JSON.stringify({ type: "Point", coordinates: [60.6, 56.83] }),
|
||
),
|
||
).toBeNull();
|
||
});
|
||
|
||
it("accepts an already-parsed object (contract tolerance)", () => {
|
||
// Контракт — строка (ST_AsGeoJSON), но parseZouitGeometry терпит и объект на
|
||
// случай смены контракта. Прогоняем object-путь через unknown-cast.
|
||
const obj = JSON.parse(POLYGON_STR) as unknown;
|
||
expect(parseZouitGeometry(obj as string)?.type).toBe("Polygon");
|
||
});
|
||
});
|
||
|
||
describe("groupZouitBySeverity", () => {
|
||
it("groups only mappable overlaps and drops geometry-less ones", () => {
|
||
const grouped = groupZouitBySeverity([
|
||
overlap({
|
||
type_zone: "Охранная зона газопровода",
|
||
geom_geojson: POLYGON_STR,
|
||
}),
|
||
overlap({
|
||
type_zone: "Охранная зона тепловой сети",
|
||
geom_geojson: POLYGON_STR,
|
||
}),
|
||
// Без geom_geojson — в карту не идёт.
|
||
overlap({ type_zone: "Публичный сервитут", geom_geojson: null }),
|
||
]);
|
||
expect(grouped.get("blocker")).toHaveLength(1);
|
||
expect(grouped.get("warning")).toHaveLength(1);
|
||
expect(grouped.get("info")).toHaveLength(0);
|
||
});
|
||
|
||
it("returns empty buckets (not undefined) when overlaps is empty", () => {
|
||
const grouped = groupZouitBySeverity([]);
|
||
expect(grouped.get("blocker")).toEqual([]);
|
||
expect(grouped.get("warning")).toEqual([]);
|
||
expect(grouped.get("info")).toEqual([]);
|
||
});
|
||
});
|