"""Unit: the shared header module contract (phase 19). The browser behavior is E2E-covered (tests/e2e/test_shared_header.py); here we pin the source-level wiring — the header.js exports, the cached whoami promise, the per-page HTML ids (anonymous-safe hidden-by-default controls), the sign-out binding move out of app.js, the non-chat New Chat bindings, and the viewer-bar CSS — so a silent regression is caught without a browser. """ from __future__ import annotations import re from pathlib import Path FRONTEND = Path(__file__).resolve().parents[2] / "frontend" ASSETS = FRONTEND / "assets" HEADER_JS = ASSETS / "header.js" APP_JS = ASSETS / "app.js" SOURCES_JS = ASSETS / "sources.js" DOCUMENT_JS = ASSETS / "document.js" LOGIN_JS = ASSETS / "login.js" TUNING_JS = ASSETS / "tuning.js" STYLES_CSS = ASSETS / "styles.css" INDEX_HTML = FRONTEND / "index.html" SOURCES_HTML = FRONTEND / "sources.html" DOCUMENT_HTML = FRONTEND / "document.html" LOGIN_HTML = FRONTEND / "login.html" TUNING_HTML = FRONTEND / "tuning.html" def _text(path: Path) -> str: assert path.is_file(), f"missing frontend file: {path}" return path.read_text(encoding="utf-8") def _script_srcs(path: Path) -> list[str]: return re.findall(r']*src="([^"]+)"', _text(path)) # ---------- header.js: the module itself ---------- def test_header_module_exports_the_three_functions() -> None: """header.js must export the three functions every page script imports (fetchIsAdmin / initSharedHeader / clearChatStorage).""" js = _text(HEADER_JS) assert "export function fetchIsAdmin" in js assert "export async function initSharedHeader" in js assert "export function clearChatStorage" in js def test_whoami_fetch_is_cached_in_a_module_level_promise() -> None: """The whoami fetch is cached in the module-level `adminPromise` marker — first call stores the promise, later calls return it, so a page makes exactly ONE /api/whoami request per load no matter how many consumers await it. Anonymous-safe: a failure resolves to false.""" js = _text(HEADER_JS) assert re.search(r"let\s+adminPromise\s*=\s*null", js), ( "module-level adminPromise marker missing" ) assert 'fetch("/api/whoami")' in js assert "if (!adminPromise)" in js, "fetchIsAdmin must reuse the stored promise" assert "return adminPromise" in js assert ".catch(() => false)" in js, "network failure must resolve to anonymous" def test_init_shared_header_toggles_only_elements_that_exist() -> None: """initSharedHeader awaits the cached whoami, toggles ONLY the controls present on the page (querySelector, null-safe), and returns the admin flag for reuse.""" js = _text(HEADER_JS) fn = js.find("function initSharedHeader") assert fn != -1 body = js[fn : js.find("\n}", fn)] assert "await fetchIsAdmin()" in body for selector in ("#sign-in-link", "#sign-out-btn", "#nav-sources", "#nav-tuning"): assert f'querySelector("{selector}")' in body assert "return admin" in body, "callers may reuse the flag" def test_clear_chat_storage_removes_the_phase14_key_silently() -> None: """clearChatStorage removes the SAME phase-14 key as app.js, inside a try/catch (private mode / storage errors are swallowed — the navigation still happens).""" js = _text(HEADER_JS) fn = js.find("function clearChatStorage") assert fn != -1 body = js[fn : js.find("\n}", fn)] assert 'localStorage.removeItem("bor.chat.v1")' in body assert "try" in body and "catch" in body def test_sign_out_binding_lives_in_the_shared_module() -> None: """The #sign-out-btn click binding (disable → POST /api/logout → reload) is owned by header.js at module import — exactly one implementation for every page that loads it.""" js = _text(HEADER_JS) assert "querySelector(\"#sign-out-btn\")" in js assert "signOutBtn.addEventListener" in js assert "signOutBtn.disabled = true" in js assert 'fetch("/api/logout", { method: "POST" })' in js assert "location.reload()" in js # ---------- HTML wiring: one shared bar on every page ---------- def test_nav_sources_ships_hidden_on_every_nav_page() -> None: """Phase 19 UX revision (owner permission 2026-08-23): the Sources nav link is hidden for anonymous — so it SHIPS with the hidden attribute (anonymous-safe default) on every page that has a nav (chat, sources, login).""" for html in (INDEX_HTML, SOURCES_HTML, LOGIN_HTML, TUNING_HTML): text = _text(html) assert re.search(r'id="nav-sources"[^>]*\bhidden\b', text), ( f"{html.name}: #nav-sources must ship hidden" ) def test_nav_tuning_ships_hidden_on_the_tuning_page() -> None: """Phase 27: the Global Tuning page reuses the shared header — the "Tuning" nav link is admin-only, so it SHIPS hidden (revealed by initSharedHeader once whoami says admin), is the page's active link (is-active + aria-current), and the page loads markdown.js (classic) + the tuning.js module with NO direct header.js