Files
6krrt/tests/test_admin_nav_is_complete.py

101 lines
4.1 KiB
Python

"""Every admin page links to every other admin page.
Two pages had drifted in opposite directions -- providers.html was missing
Proficiency and proficiency.html was missing Providers -- so you could not
navigate between them in either direction without editing the URL. Neither
gap was noticeable from the page you were on, which is why a test is worth
more here than care.
The page list is derived from the routes the router actually serves, so a
new admin page fails this until it is linked from the others, rather than
quietly launching unreachable.
The links render client-side now: navbar.js builds them from its
NAV_LINKS array into the empty <ul id="nav-links"> every page ships, so
statically parsing each page for anchors would report every link missing
on every page. The invariant the old parse guarded -- a complete list,
present everywhere -- moved with the list itself: it is defined exactly
once (in navbar.js, as complete as the route table) and every page carries
the mount that renders it. Hardcoded <li class="nav-item"> entries are
how the pages drifted originally, so their return fails this too. The
active-page highlighting lives in tests/test_admin_js_units.py, which
runs navbar.js's real activeNavHref through node.
"""
from __future__ import annotations
import re
from pathlib import Path
import pytest
FRONTEND = Path(__file__).resolve().parent.parent / "admin" / "frontend"
NAVBAR_JS = FRONTEND / "navbar.js"
# href -> file that serves it. Kept explicit rather than globbed so that
# adding a page is a deliberate two-line edit here, not an accident.
PAGES = {
"/admin/": "index.html",
"/admin/models": "models.html",
"/admin/profiles": "profiles.html",
"/admin/proficiency": "proficiency.html",
"/admin/decisions": "decisions.html",
"/admin/quota": "quota.html",
"/admin/controls": "controls.html",
"/admin/providers": "providers.html",
}
# The nav renders every page except the landing page, which is reached by
# the logo -- a second door to the same place, in the row where width is
# scarcest. Order is the order the bar displays.
NAV_HREFS = list(PAGES)[1:]
NAV_LINKS_BEGIN = "/* NAV_LINKS:BEGIN */"
NAV_LINKS_END = "/* NAV_LINKS:END */"
NAV_RE = re.compile(r'<header class="navbar.*?</header>', re.DOTALL)
def _nav_hrefs() -> list[str]:
"""The hrefs navbar.js renders, in file order, read from NAV_LINKS."""
js = NAVBAR_JS.read_text(encoding="utf-8")
begin = js.find(NAV_LINKS_BEGIN)
end = js.find(NAV_LINKS_END)
assert begin != -1, "navbar.js: NAV_LINKS block not found"
assert end > begin, "navbar.js: NAV_LINKS:END absent or before begin"
return re.findall(r"href:\s*'([^']+)'", js[begin:end])
def test_navbar_js_owns_the_complete_link_list():
"""The one list is as complete as the route table, in bar order.
A page added to PAGES without a NAV_LINKS entry fails here while it
is still a two-line fix, instead of launching silently unreachable.
"""
assert _nav_hrefs() == NAV_HREFS, (
f"NAV_LINKS drifted from the served pages; expected {NAV_HREFS}"
)
@pytest.mark.parametrize("filename", sorted(PAGES.values()))
def test_every_page_ships_the_mount_and_the_script(filename):
"""Each page carries the empty #nav-links ul and the script that fills it.
Old failure shape, new shape: instead of parsing anchors (they only
exist after navbar.js runs in the browser), completeness per page is
the mount plus the script tag -- and the absence of any hardcoded
nav-item <li>, because those are what drifted across eight copies.
"""
html = (FRONTEND / filename).read_text(encoding="utf-8")
nav = NAV_RE.search(html)
assert nav, f'{filename}: no <header class="navbar"> block found'
assert '<ul class="navbar-nav" id="nav-links"></ul>' in nav.group(0), (
f"{filename}: the empty #nav-links mount is missing from the navbar"
)
assert '<script src="navbar.js"></script>' in html, (
f"{filename}: navbar.js is not included"
)
assert '<li class="nav-item' not in nav.group(0), (
f"{filename}: hardcoded nav entries are back -- render them from navbar.js"
)