feat(ui): one consistent navbar on every page (TODO.md L3)
This commit is contained in:
@@ -8,16 +8,20 @@ Run in isolation (DB must be up: ``podman compose up -d db``):
|
||||
Contract under test (owner report 2026-08-23, phase 19) — ONE bar per
|
||||
page, the same controls everywhere:
|
||||
|
||||
* chat / sources: brand + nav [Chat, Sources — admin only] + New Chat
|
||||
+ Sign in / Sign out;
|
||||
* document viewer: back + title + New Chat + Sign in / Sign out (the
|
||||
viewer has no nav, so no Sources link at all);
|
||||
* chat / sources / viewer: brand + nav [Chat, Sources — admin only] +
|
||||
New Chat + Sign in / Sign out;
|
||||
* document viewer: the standard bar (row 1) + back + title + meta in a
|
||||
second titlebar row (phase 34, owner confirmation 2026-08-26 — the
|
||||
viewer's old "no nav" single-row bar is superseded; it now carries
|
||||
the SAME nav contract as every other page);
|
||||
* the "Sources" nav link (``#nav-sources``) is HIDDEN for anonymous
|
||||
users on every page that has a nav and shown for admin (phase-16 UX
|
||||
revision with owner permission; the soft-gate page and the A10 API
|
||||
split are untouched);
|
||||
users on every page and shown for admin (phase-16 UX revision with
|
||||
owner permission; the soft-gate page and the A10 API split are
|
||||
untouched) — now on the viewer as well (phase 34);
|
||||
* the bar height never moves: 64px desktop / 58px at ≤640px (phase-12
|
||||
``--header-h`` contract, bounding-box measurement convention).
|
||||
``--header-h`` contract, bounding-box measurement convention) — on
|
||||
the viewer this is ROW 1 (``.doc-header .app-header``); the
|
||||
titlebar row is content-sized.
|
||||
|
||||
Determinism note: every assertion is settled-state — ``assert_shared_bar``
|
||||
first waits for the whoami toggle to land (exactly one of Sign in /
|
||||
@@ -109,7 +113,10 @@ def _expected_h(page: Page) -> int:
|
||||
|
||||
|
||||
def _bar_selector(page_kind: str) -> str:
|
||||
return ".doc-header" if page_kind == "viewer" else ".app-header"
|
||||
"""The STANDARD bar element on each page kind. Phase 34: the
|
||||
viewer's header is two rows — the height contract applies to row 1
|
||||
(the standard bar), not the whole two-row <header>."""
|
||||
return ".doc-header .app-header" if page_kind == "viewer" else ".app-header"
|
||||
|
||||
|
||||
def assert_shared_bar(page: Page, admin: bool, page_kind: str) -> None:
|
||||
@@ -134,24 +141,26 @@ def assert_shared_bar(page: Page, admin: bool, page_kind: str) -> None:
|
||||
# New Chat is on the bar on every page kind (the owner's ask).
|
||||
expect(page.locator("#new-chat-btn")).to_be_visible()
|
||||
|
||||
if page_kind == "viewer":
|
||||
# The viewer has no nav — no Sources link in the DOM at all.
|
||||
assert page.locator("#nav-sources").count() == 0, (
|
||||
"the viewer bar must not carry a Sources nav link"
|
||||
)
|
||||
# The document itself has settled (rendered, not Loading…/not-found)
|
||||
# so the bar is being measured on the real page.
|
||||
expect(page.locator("#doc-title")).to_have_text(DOC_TITLE, timeout=15_000)
|
||||
else:
|
||||
# The Sources nav link: admin-only (phase-16 revision, owner
|
||||
# permission 2026-08-23) — hidden for anonymous, shown for admin.
|
||||
nav = page.locator("#nav-sources")
|
||||
assert nav.count() == 1, f"one #nav-sources expected on the {page_kind} page"
|
||||
# Phase 34: EVERY page kind — viewer included — carries the SAME
|
||||
# nav contract: the Chat link always visible; the admin-only
|
||||
# #nav-sources / #nav-tuning links (phase 19 / phase 29) ship
|
||||
# hidden and are revealed for admin (phase-16 UX revision, owner
|
||||
# permission 2026-08-23; the soft-gate page and the A10 API split
|
||||
# are untouched).
|
||||
expect(page.locator(".app-nav a[href='/']")).to_be_visible()
|
||||
for link_id in ("#nav-sources", "#nav-tuning"):
|
||||
nav = page.locator(link_id)
|
||||
assert nav.count() == 1, f"one {link_id} expected on the {page_kind} page"
|
||||
if admin:
|
||||
expect(nav).to_be_visible()
|
||||
else:
|
||||
expect(nav).to_be_hidden()
|
||||
|
||||
if page_kind == "viewer":
|
||||
# The document itself has settled (rendered, not Loading…/not-found)
|
||||
# so the bar is being measured on the real page.
|
||||
expect(page.locator("#doc-title")).to_have_text(DOC_TITLE, timeout=15_000)
|
||||
|
||||
# The bar height never moves: 64px desktop / 58px ≤640px (phase 12),
|
||||
# bounding-box measurement — the new pills must fit inside it.
|
||||
box = page.locator(_bar_selector(page_kind)).bounding_box()
|
||||
@@ -238,8 +247,9 @@ def test_sources_nav_hidden_for_anonymous_everywhere(
|
||||
assert nav.count() == 1
|
||||
expect(nav).to_be_hidden()
|
||||
|
||||
# The login page has no chat controls — header.js only toggles the
|
||||
# nav link there; for anonymous it stays hidden (it ships hidden).
|
||||
# The login page carries the full shared header too (phase 34);
|
||||
# for anonymous the admin-only nav links stay hidden (they ship
|
||||
# hidden and are revealed only for the admin).
|
||||
page.goto(app_url + "/login.html")
|
||||
page.wait_for_load_state("networkidle") # the whoami round-trip has settled
|
||||
nav = page.locator("#nav-sources")
|
||||
|
||||
Reference in New Issue
Block a user