MapWorkspace.tsx was 3.157 lines: a props interface, 1.200 lines of derived state and handlers, and two complete render paths — the map-first explorer and the advanced workbench behind it. It is now five modules, and the container is nineteen lines that choose between the two. The obstacle was the props signature. The explorer reads 97 derived values and the workbench 40, so passing them individually would have produced a 97-field interface — worse than the file it replaced. Extracting the derived state into a hook that returns one object solves it: MapWorkspaceViewModel is ReturnType<typeof useMapWorkspaceViewModel>, so the shape is derived from what the hook actually produces and cannot drift from it. Each view then names two typed objects, and the JSX moved unchanged. The contract tests found the one place where widening a negative assertion is wrong. "The map workspace performs no transport" was true of the old file and false of the whole feature, because the hooks call the API by design. It is now scoped to the presentational modules, which is what it always meant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
159 lines
6.3 KiB
Python
159 lines
6.3 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
from shapely.geometry import Polygon, shape
|
|
from tests.frontend_contract import read_feature
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
SCRIPTS = ROOT / "scripts"
|
|
|
|
|
|
def load_module(name: str, filename: str):
|
|
scripts_path = str(SCRIPTS)
|
|
if scripts_path not in sys.path:
|
|
sys.path.insert(0, scripts_path)
|
|
spec = importlib.util.spec_from_file_location(name, SCRIPTS / filename)
|
|
assert spec is not None
|
|
assert spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def test_kempen_scope_matches_official_28_municipality_policy_region() -> None:
|
|
scopes = load_module("geographic_scopes_test", "geographic_scopes.py")
|
|
scope = scopes.KEMPEN_TRANSPORT_REGION_SCOPE
|
|
|
|
assert scope.key == "kempen-transport-region"
|
|
assert scope.project_name == "Kempen Regional Workbench"
|
|
assert scope.scope_type == "transport_region"
|
|
assert len(scope.members) == 28
|
|
assert len(set(scope.nis_codes)) == 28
|
|
assert ("Mol", "13025") in {(member.name, member.nis_code) for member in scope.members}
|
|
assert ("Nijlen", "12026") in {(member.name, member.nis_code) for member in scope.members}
|
|
assert "vervoerregio-kempen" in scope.authority_url
|
|
assert "geen claim" in scope.limitation_message
|
|
|
|
|
|
def test_scope_union_preserves_member_identity_and_policy_limitation() -> None:
|
|
scopes = load_module("geographic_scopes_union_test", "geographic_scopes.py")
|
|
provisioner = load_module("provision_geographic_scope_test", "provision_geographic_scope.py")
|
|
scope = scopes.GeographicScope(
|
|
key="test-region",
|
|
display_name="Testregio",
|
|
project_name="Test Regional Workbench",
|
|
project_region="Test",
|
|
area_name="Testregio - operationele grens",
|
|
authority_name="Test authority",
|
|
authority_url="https://example.test/scope",
|
|
scope_type="policy_region",
|
|
limitation_message="Operationele testgrens; geen landschappelijke claim.",
|
|
members=(scopes.ScopeMember("Alpha", "10001"), scopes.ScopeMember("Beta", "10002")),
|
|
)
|
|
source_features = [
|
|
{
|
|
"type": "Feature",
|
|
"id": "alpha",
|
|
"geometry": Polygon([(4.0, 51.0), (4.1, 51.0), (4.1, 51.1), (4.0, 51.1)]).__geo_interface__,
|
|
"properties": {"NAAM": "Alpha", "NISCODE": "10001"},
|
|
},
|
|
{
|
|
"type": "Feature",
|
|
"id": "beta",
|
|
"geometry": Polygon([(4.1, 51.0), (4.2, 51.0), (4.2, 51.1), (4.1, 51.1)]).__geo_interface__,
|
|
"properties": {"NAAM": "Beta", "NISCODE": "10002"},
|
|
},
|
|
]
|
|
|
|
boundary, members, summary = provisioner.build_scope_payloads(
|
|
scope,
|
|
source_features,
|
|
source_url="https://example.test/vrbg",
|
|
generated_at="2026-07-14T00:00:00+00:00",
|
|
)
|
|
|
|
assert len(boundary["features"]) == 1
|
|
assert len(members["features"]) == 2
|
|
assert shape(boundary["features"][0]["geometry"]).is_valid
|
|
assert boundary["features"][0]["properties"]["member_nis_codes"] == ["10001", "10002"]
|
|
assert boundary["features"][0]["properties"]["scope_limitation"] == scope.limitation_message
|
|
assert [feature["properties"]["municipality"] for feature in members["features"]] == ["Alpha", "Beta"]
|
|
assert summary["member_count"] == 2
|
|
assert summary["area_km2"] > 0
|
|
|
|
|
|
def test_scope_api_pagination_respects_canonical_limit() -> None:
|
|
provisioner = load_module("provision_geographic_scope_paging_test", "provision_geographic_scope.py")
|
|
|
|
class Response:
|
|
ok = True
|
|
status_code = 200
|
|
text = ""
|
|
|
|
def __init__(self, payload):
|
|
self.payload = payload
|
|
|
|
def json(self):
|
|
return {"data": self.payload}
|
|
|
|
class Session:
|
|
def __init__(self) -> None:
|
|
self.offsets = []
|
|
|
|
def get(self, url, *, params, timeout):
|
|
del url, timeout
|
|
self.offsets.append(params["offset"])
|
|
offset = params["offset"]
|
|
page = [{"id": index} for index in range(offset, min(offset + 200, 401))]
|
|
return Response({"items": page, "total": 401})
|
|
|
|
session = Session()
|
|
items = provisioner.list_paginated_items(session, "http://backend/api/v1/projects", timeout=30)
|
|
|
|
assert len(items) == 401
|
|
assert session.offsets == [0, 200, 400]
|
|
|
|
|
|
def test_kempen_scope_operator_is_packaged_and_exposed_in_map_flow() -> None:
|
|
dockerfile = (ROOT / "deploy/unraid/Dockerfile.all-in-one").read_text(encoding="utf-8")
|
|
readiness = (ROOT / "scripts/run_readiness_check.sh").read_text(encoding="utf-8")
|
|
workspace = "\n".join(
|
|
(
|
|
read_feature("map_workspace"),
|
|
(ROOT / "frontend/src/components/map/mapWorkspaceUtils.ts").read_text(encoding="utf-8"),
|
|
)
|
|
)
|
|
app = read_feature("shell")
|
|
|
|
assert "COPY scripts/geographic_scopes.py" in dockerfile
|
|
assert "COPY scripts/provision_geographic_scope.py" in dockerfile
|
|
assert "py_compile scripts/provision_geographic_scope.py" in readiness
|
|
assert "Immutable scope dataset" in (ROOT / "scripts/provision_geographic_scope.py").read_text(encoding="utf-8")
|
|
assert "Kempen (28 gemeenten)" in workspace
|
|
assert 'aria-label="Regio"' not in workspace
|
|
assert 'aria-label="Ingeladen regiobereik"' in workspace
|
|
assert "Zoek optioneel een gemeente" in workspace
|
|
assert "projects={projects}" in app
|
|
map_props = app.split("<MapWorkspace", maxsplit=1)[1].split("/>", maxsplit=1)[0]
|
|
assert "onSelectProject={selectProject}" not in map_props
|
|
|
|
|
|
def test_project_switches_reset_scoped_state_and_prefer_the_regional_context() -> None:
|
|
bootstrap = read_feature("shell")
|
|
project_workspace = read_feature("shell")
|
|
map_state = read_feature("map_workspace")
|
|
dataset_workflow = read_feature("datasets")
|
|
|
|
selected_project_branch = bootstrap.split("if (!selectedProjectId)", maxsplit=1)[1]
|
|
assert "resetProjectData()" in selected_project_branch
|
|
assert "resetDatasetForProject()" in selected_project_branch
|
|
assert "projectDataRequestSequence" in project_workspace
|
|
assert "REGIONAL_WORKSPACE_PROJECT_NAME" in project_workspace
|
|
assert "vervoerregio|operationele grens" in map_state
|
|
assert "datasets.find(isOperationalScopeBoundaryDataset)" in dataset_workflow
|