Files
brain-of-reese/tests/unit/test_shared_header.py
T

289 lines
12 KiB
Python

"""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'<script[^>]*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 <script> tag
(single-evaluation design)."""
text = _text(TUNING_HTML)
tag = re.search(r'<a[^>]*id="nav-tuning"[^>]*>', text)
assert tag, "tuning.html must carry the #nav-tuning nav link"
assert 'class="nav-link is-active"' in tag.group(0), "the Tuning link is the active one"
assert 'aria-current="page"' in tag.group(0)
assert "hidden" in tag.group(0), "#nav-tuning must ship hidden (admin-only)"
srcs = _script_srcs(TUNING_HTML)
assert [s for s in srcs if "header.js" in s] == [], "no direct header.js <script> tag"
assert [s for s in srcs if "markdown.js" in s]
assert [s for s in srcs if "tuning.js" in s]
def test_nav_sources_is_absent_from_the_viewer() -> None:
"""The document viewer has no nav — no #nav-sources element there (the
module's missing-element no-op keeps it out)."""
assert 'id="nav-sources"' not in _text(DOCUMENT_HTML)
def test_sources_and_viewer_carry_the_shared_controls() -> None:
"""Sources AND the document viewer gain the New Chat button + the
Sign in / Sign out pair (both starting hidden — initSharedHeader
reveals exactly one after whoami), and their page scripts load
header.js (phase 23: via the page script's relative import, not a
direct script tag)."""
for html in (SOURCES_HTML, DOCUMENT_HTML):
text = _text(html)
assert 'id="new-chat-btn"' in text
assert re.search(r'id="sign-in-link"[^>]*\bhidden\b', text)
assert re.search(r'id="sign-out-btn"[^>]*\bhidden\b', text)
for js_file in (SOURCES_JS, DOCUMENT_JS):
assert 'from "./header.js"' in _text(js_file), (
f"{js_file.name}: page script must load the shared header module"
)
# Each page's Sign in link returns to ITS OWN page after login.
assert 'href="/login.html?next=/sources.html"' in _text(SOURCES_HTML)
assert 'href="/login.html?next=/document.html"' in _text(DOCUMENT_HTML)
def test_header_module_loads_before_the_page_script() -> None:
"""Phase 23 (owner-confirmed single-evaluation design): NO page loads
header.js with a direct <script> tag anymore. Each page script
imports it relatively (`from "./header.js"`) — a hoisted import that
the browser evaluates BEFORE the page script body runs, and that the
image bundler inlines into the page bundle. The sign-out binding and
the whoami cache therefore exist when the page script boots, and
header.js can never be evaluated twice on a page (a tag + import pair
would double-bind the sign-out listener)."""
cases = [
(INDEX_HTML, "app.js"),
(SOURCES_HTML, "sources.js"),
(DOCUMENT_HTML, "document.js"),
(LOGIN_HTML, "login.js"),
]
for html, page_script in cases:
srcs = _script_srcs(html)
assert [s for s in srcs if "header.js" in s] == [], (
f"{html.name}: no direct header.js <script> tag (single-evaluation design)"
)
assert [s for s in srcs if page_script in s], (
f"{html.name}: must load {page_script}"
)
js = _text(ASSETS / page_script)
assert 'from "./header.js"' in js, (
f"{page_script}: must import the shared header module relatively"
)
assert 'from "/assets/header.js"' not in js, (
f"{page_script}: absolute header import would break the esbuild bundle"
)
def test_login_page_carries_no_chat_controls() -> None:
"""Noted boundary (owner-confirmed): the login page is the auth page,
not an app page — no New Chat / Sign in / Sign out controls there;
header.js only toggles the Sources link."""
text = _text(LOGIN_HTML)
assert "new-chat-btn" not in text
assert "sign-in-link" not in text
assert "sign-out-btn" not in text
# ---------- page-script adaptations ----------
def test_app_js_delegates_the_shared_controls_to_header_module() -> None:
"""app.js imports the shared module, runs initSharedHeader() at boot
(BEFORE the phase-14 restore), takes its isAdmin from the cached
fetchIsAdmin(), and owns NO whoami fetch and NO sign-out binding
anymore (both moved to header.js). Phase 23: the import is relative
(`./header.js`) so esbuild can bundle it into the image."""
js = _text(APP_JS)
assert 'from "./header.js"' in js
assert "fetchIsAdmin" in js and "initSharedHeader" in js
assert "signOutBtn.addEventListener" not in js, (
"the sign-out binding moved to header.js"
)
assert 'fetch("/api/whoami")' not in js, (
"app.js must not fetch whoami itself — header.js caches it (one request/page)"
)
assert "loadAuthState" not in js, "loadAuthState was deleted in phase 19"
assert "function applyAuthState" in js, "chat-page tuning gate stays"
assert "isAdmin = await fetchIsAdmin();" in js
init_idx = js.find("await initSharedHeader();")
restore_idx = js.find("restoreConversation();")
assert -1 < init_idx < restore_idx, (
"header init must run before the phase-14 restore"
)
def test_login_js_uses_the_shared_fetch_is_admin() -> None:
"""login.js switches its whoami check to the shared cached promise
(one request per page) and calls initSharedHeader for the Sources
link; its already-admin → redirect behavior is unchanged. Phase 23:
the import is relative (`./header.js`)."""
js = _text(LOGIN_JS)
assert 'from "./header.js"' in js
assert "fetchIsAdmin" in js
assert "fetchIsAdmin()" in js
assert "initSharedHeader()" in js
assert 'fetch("/api/whoami")' not in js
assert "window.location.replace(safeNext())" in js
def test_non_chat_pages_bind_new_chat_to_the_chat_page() -> None:
"""On sources, the viewer, and the tuning page, New Chat means "go to
the chat, fresh": the binding clears the phase-14 key
(clearChatStorage) and navigates to "/" — and each page runs
initSharedHeader() at boot on the shared cached whoami. Phase 23:
the import is relative (`./header.js`)."""
for js_file in (SOURCES_JS, DOCUMENT_JS, TUNING_JS):
js = _text(js_file)
assert 'from "./header.js"' in js
assert "initSharedHeader()" in js
btn_idx = js.find("new-chat-btn")
clear_idx = js.find("clearChatStorage();")
nav_idx = js.find('window.location.href = "/"')
assert -1 < btn_idx < clear_idx < nav_idx, (
f"{js_file.name}: #new-chat-btn must clear storage then navigate to '/'"
)
assert 'fetch("/api/whoami")' not in js, (
f"{js_file.name}: whoami goes through the shared cached promise"
)
# ---------- viewer-bar CSS ----------
def test_viewer_bar_css_pushes_actions_right_and_title_clips() -> None:
"""styles.css defines .doc-header-actions (margin-left:auto flex
cluster) and the title block keeps min-width: 0 so
#doc-title/#doc-meta clip instead of overflowing the --header-h bar."""
css = _text(STYLES_CSS)
block = re.search(r"\.doc-header-actions\s*\{([^}]*)\}", css)
assert block, "styles.css must define .doc-header-actions"
body = block.group(1)
assert "margin-left: auto" in body
assert "display: flex" in body
assert re.search(r"\.doc-title-block\s*\{[^}]*min-width:\s*0", css), (
"the title block must keep clipping while the pills fit"
)