Extracted for its error paths, not its size. Every step can fail or return nothing, and each outcome has to leave the operator with a status that says where the chain stopped — a workflow reporting "afgerond" after a step produced nothing tells them a result exists when it does not. Inline in the component, none of that was exercised; it now has fourteen tests covering each stopping point, both rejection kinds, the reuse path and the preconditions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
101 lines
3.8 KiB
Python
101 lines
3.8 KiB
Python
"""Helpers for asserting frontend wiring from the backend test suite.
|
|
|
|
Most of this suite checks frontend behaviour by reading TSX files and
|
|
asserting that literal substrings occur in them. That reds the suite on every
|
|
rename and every copy change while proving nothing about behaviour: renaming a
|
|
button label is not a regression, and ``source.count("useEffect(") == 1`` is a
|
|
formatting rule, not a contract.
|
|
|
|
These helpers keep the useful half of that idea — that a documented product
|
|
contract must remain wired somewhere in the frontend — and drop the brittle
|
|
half. Assert on identifiers, API paths and prop names, which only change when
|
|
the wiring genuinely changes. Do not assert on user-visible copy; put that in
|
|
a frontend component test where the rendered output can be checked properly.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
FRONTEND_SRC = ROOT / "frontend" / "src"
|
|
|
|
|
|
# The map workspace is one feature split across several modules: the container
|
|
# component, its domain layer and its pure helpers. A contract belongs to the
|
|
# feature, not to whichever file currently holds it, so splitting a 4.000-line
|
|
# component must not red the suite.
|
|
MAP_WORKSPACE_SOURCES = (
|
|
"components/map/MapWorkspace.tsx",
|
|
"components/map/mapWorkspaceThemes.ts",
|
|
"components/map/mapWorkspaceUtils.ts",
|
|
"hooks/useMapImageOverlays.ts",
|
|
"hooks/useMapRectangleSelection.ts",
|
|
"hooks/useFullGisWorkflow.ts",
|
|
"components/map/MapExplorerView.tsx",
|
|
"components/map/MapAdvancedWorkbench.tsx",
|
|
)
|
|
|
|
|
|
def read_frontend(relative_path: str) -> str:
|
|
"""Read one frontend source file relative to ``frontend/src``."""
|
|
|
|
return (FRONTEND_SRC / relative_path).read_text(encoding="utf-8")
|
|
|
|
|
|
def read_frontend_area(*relative_paths: str) -> str:
|
|
"""Read several related sources as one body of code.
|
|
|
|
Files that do not exist are skipped, so this survives a module being split
|
|
further or merged back.
|
|
"""
|
|
|
|
parts: list[str] = []
|
|
for relative_path in relative_paths:
|
|
path = FRONTEND_SRC / relative_path
|
|
if path.is_file():
|
|
parts.append(path.read_text(encoding="utf-8"))
|
|
return chr(10).join(parts)
|
|
|
|
|
|
def read_map_workspace() -> str:
|
|
"""The whole map workspace feature, whichever modules it is split into."""
|
|
|
|
return read_frontend_area(*MAP_WORKSPACE_SOURCES)
|
|
|
|
|
|
def assert_wired(source: str, *identifiers: str, context: str = "frontend source") -> None:
|
|
"""Every identifier must appear in ``source``.
|
|
|
|
Use for symbols, hook names, prop names and API paths — things a refactor
|
|
renames deliberately — never for sentences shown to a user.
|
|
"""
|
|
|
|
missing = [identifier for identifier in identifiers if identifier not in source]
|
|
assert not missing, f"{context} no longer wires: {missing}"
|
|
|
|
|
|
def assert_calls(source: str, function_name: str, *, first_argument: str) -> None:
|
|
"""Assert ``function_name`` is called with ``first_argument`` as argument 1.
|
|
|
|
Tolerates whatever the remaining arguments have been refactored into, which
|
|
is the part that keeps changing while the wiring stays the same.
|
|
"""
|
|
|
|
pattern = rf"{re.escape(function_name)}\(\s*{re.escape(first_argument)}\s*[,)]"
|
|
assert re.search(pattern, source), f"{function_name}({first_argument}, …) is no longer called"
|
|
|
|
|
|
def assert_mentions(source: str, *phrases: str, context: str = "frontend source") -> None:
|
|
"""Case-insensitive check that a concept is still surfaced to the operator.
|
|
|
|
A deliberately weak assertion: it survives rewording but still fails if a
|
|
whole explanation is deleted. Prefer ``assert_wired`` where an identifier
|
|
exists to check instead.
|
|
"""
|
|
|
|
folded = source.casefold()
|
|
missing = [phrase for phrase in phrases if phrase.casefold() not in folded]
|
|
assert not missing, f"{context} no longer mentions: {missing}"
|