gendesign/frontend/src/components/site-finder/__tests__/UtilityInfrastructureLayer.test.ts
Light1YT 854fa69a8b
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 54s
CI / openapi-codegen-check (pull_request) Successful in 1m49s
feat(site-finder): render OSM utility-infrastructure layer on analyze map (#1746)
Section2 "Сети" map now shows the OSM engineering-networks layer
(osm_utility_infrastructure_ekb, endpoint /parcels/{cad}/utility-infrastructure)
alongside the NSPD connection-points + ЗОУИТ layers from #1751, on the same map.

- useUtilityInfrastructure hook (react-query, mirrors useConnectionPoints)
- UtilityInfrastructureLayer: Point→CircleMarker, LineString/Polygon(+Multi)→GeoJSON,
  per-kind colour (power/water/gas/heat/communication/sewage), popups
- UtilityLayerControlPanel: collapsible legend, per-kind toggle + count
- SiteMap: optional utilityInfrastructure prop + visibleKinds state (CP/ЗОУИТ unchanged)
- 12 unit tests (geometry parsing + [lon,lat]→[lat,lon] swap + grouping)

Refs #1746
2026-06-27 03:40:51 +05:00

152 lines
5.1 KiB
TypeScript

/**
* Unit tests for the #1746 OSM utility-infrastructure layer pure logic: kind
* classification, geometry_geojson parsing (the three-geometry-type contract +
* graceful-skip), and grouping.
*
* The layer renders Leaflet <CircleMarker>/<GeoJSON> (jsdom can't mount those
* meaningfully), so we test the data-shaping that decides WHAT gets drawn and
* HOW it's colored — that's where the bugs live (power-lines must NOT be
* skipped; [lon,lat]→[lat,lat] swap; bad parse → crash or ocean geometry).
*/
import type { UtilityInfrastructureFeature } from "@/hooks/useUtilityInfrastructure";
import {
classifyUtilityKind,
groupUtilityByKind,
parseUtilityGeometry,
} from "../UtilityInfrastructureLayer";
function feat(
partial: Partial<UtilityInfrastructureFeature>,
): UtilityInfrastructureFeature {
return {
osm_id: 1,
osm_type: "way",
infrastructure_kind: "power",
name: null,
source_tag: null,
distance_m: 0,
geometry_geojson: {},
...partial,
};
}
// geometry_geojson приходит уже распарсенным объектом (ST_AsGeoJSON на бэке).
const POINT_GEOM = { type: "Point", coordinates: [60.6, 56.83] };
const LINESTRING_GEOM = {
type: "LineString",
coordinates: [
[60.6, 56.83],
[60.61, 56.84],
],
};
const POLYGON_GEOM = {
type: "Polygon",
coordinates: [
[
[60.6, 56.83],
[60.61, 56.83],
[60.61, 56.84],
[60.6, 56.83],
],
],
};
describe("classifyUtilityKind", () => {
it("passes through known kinds", () => {
expect(classifyUtilityKind("power")).toBe("power");
expect(classifyUtilityKind("water")).toBe("water");
expect(classifyUtilityKind("gas")).toBe("gas");
expect(classifyUtilityKind("heat")).toBe("heat");
expect(classifyUtilityKind("communication")).toBe("communication");
expect(classifyUtilityKind("sewage")).toBe("sewage");
});
it("maps unknown / null kind to 'other' (graceful)", () => {
expect(classifyUtilityKind("telecom")).toBe("other");
expect(classifyUtilityKind(null)).toBe("other");
expect(classifyUtilityKind(undefined)).toBe("other");
});
});
describe("parseUtilityGeometry", () => {
it("parses Point geometry (towers / nodes)", () => {
expect(parseUtilityGeometry(POINT_GEOM)?.type).toBe("Point");
});
it("parses LineString geometry (power lines — the majority, must NOT skip)", () => {
expect(parseUtilityGeometry(LINESTRING_GEOM)?.type).toBe("LineString");
});
it("parses Polygon geometry (substations)", () => {
expect(parseUtilityGeometry(POLYGON_GEOM)?.type).toBe("Polygon");
});
it("parses MultiLineString / MultiPolygon geometry", () => {
expect(
parseUtilityGeometry({
type: "MultiLineString",
coordinates: [LINESTRING_GEOM.coordinates],
})?.type,
).toBe("MultiLineString");
expect(
parseUtilityGeometry({
type: "MultiPolygon",
coordinates: [POLYGON_GEOM.coordinates],
})?.type,
).toBe("MultiPolygon");
});
it("tolerates a GeoJSON string (future contract change)", () => {
expect(parseUtilityGeometry(JSON.stringify(POINT_GEOM))?.type).toBe(
"Point",
);
});
it("returns null for missing / empty / invalid input (graceful skip)", () => {
expect(parseUtilityGeometry(null)).toBeNull();
expect(parseUtilityGeometry(undefined)).toBeNull();
expect(parseUtilityGeometry("")).toBeNull();
expect(parseUtilityGeometry("{not json")).toBeNull();
expect(parseUtilityGeometry({})).toBeNull();
expect(parseUtilityGeometry({ type: "Point" })).toBeNull();
expect(
parseUtilityGeometry({ type: "GeometryCollection", geometries: [] }),
).toBeNull();
});
});
describe("groupUtilityByKind", () => {
it("groups all three geometry types and keeps power lines", () => {
const grouped = groupUtilityByKind([
feat({ infrastructure_kind: "power", geometry_geojson: LINESTRING_GEOM }),
feat({ infrastructure_kind: "power", geometry_geojson: POINT_GEOM }),
feat({ infrastructure_kind: "power", geometry_geojson: POLYGON_GEOM }),
feat({ infrastructure_kind: "water", geometry_geojson: POINT_GEOM }),
feat({ infrastructure_kind: "gas", geometry_geojson: LINESTRING_GEOM }),
]);
expect(grouped.get("power")).toHaveLength(3);
expect(grouped.get("water")).toHaveLength(1);
expect(grouped.get("gas")).toHaveLength(1);
});
it("drops features without valid geometry", () => {
const grouped = groupUtilityByKind([
feat({ infrastructure_kind: "power", geometry_geojson: LINESTRING_GEOM }),
feat({ infrastructure_kind: "power", geometry_geojson: {} }),
]);
expect(grouped.get("power")).toHaveLength(1);
});
it("buckets unknown kinds under 'other'", () => {
const grouped = groupUtilityByKind([
feat({ infrastructure_kind: "telecom", geometry_geojson: POINT_GEOM }),
]);
expect(grouped.get("other")).toHaveLength(1);
});
it("returns empty buckets (not undefined) when features is empty", () => {
const grouped = groupUtilityByKind([]);
expect(grouped.get("power")).toEqual([]);
expect(grouped.get("other")).toEqual([]);
});
});