various fixes
This commit is contained in:
@@ -213,14 +213,17 @@ def test_admin_login_unlocks_sources_and_tuning(
|
||||
expect(page.locator("#docs-table")).to_be_visible()
|
||||
expect(page.locator("#docs-tbody tr")).to_have_count(13)
|
||||
|
||||
# Chat: the tuning UI is back — header toggle with count badge,
|
||||
# Sign out instead of Sign in, Tune under the answer.
|
||||
# Chat: the tuning UI is back — Sign out instead of Sign in, Tune
|
||||
# under the answer. The header toggle is NOT back: removed from the
|
||||
# navbar at owner request (2026-08-28), the panel still ships
|
||||
# hidden (note management lives on /tuning.html).
|
||||
page.goto(app_url)
|
||||
expect(page.locator("#sign-out-btn")).to_be_visible()
|
||||
expect(page.locator("#sign-in-link")).to_be_hidden()
|
||||
toggle = page.locator("#steering-toggle")
|
||||
expect(toggle).to_be_visible()
|
||||
expect(page.locator("#steering-count")).to_have_text("0")
|
||||
assert page.locator("#steering-toggle").count() == 0, (
|
||||
"the steering toggle was removed from the navbar (2026-08-28)"
|
||||
)
|
||||
expect(page.locator("#steering-panel")).to_be_hidden()
|
||||
|
||||
_ask(page, QUESTION)
|
||||
tune = page.locator(".msg.brain .tune-btn").last
|
||||
@@ -249,7 +252,10 @@ def test_logout_returns_to_anonymous(
|
||||
login(page, app_url, next="/") # straight into the chat
|
||||
expect(page).to_have_url(app_url + "/")
|
||||
expect(page.locator("#sign-out-btn")).to_be_visible()
|
||||
expect(page.locator("#steering-toggle")).to_be_visible()
|
||||
# The steering toggle was removed from the navbar (2026-08-28) —
|
||||
# absent for the admin too; the panel still ships hidden.
|
||||
assert page.locator("#steering-toggle").count() == 0
|
||||
expect(page.locator("#steering-panel")).to_be_hidden()
|
||||
|
||||
# One grounded turn as admin (persisted to localStorage by phase 14).
|
||||
_ask(page, QUESTION)
|
||||
|
||||
@@ -20,15 +20,18 @@ Per role, the VISIBLE inventory:
|
||||
|
||||
* admin: brand + nav [Chat, #nav-sources, #nav-git-sources, #nav-tuning]
|
||||
(four links, that order — the Git sources link joined in phase 35,
|
||||
owner permission 2026-08-26) + #steering-toggle + #sync-btn +
|
||||
#new-chat-btn + #sign-out-btn (with #sign-in-link hidden) — on all
|
||||
five pages, same id+class inventory, same DOM order;
|
||||
owner permission 2026-08-26) + #sync-btn + #new-chat-btn +
|
||||
#sign-out-btn (with #sign-in-link hidden) — on all five pages, same
|
||||
id+class inventory, same DOM order. The #steering-toggle was removed
|
||||
from the navbar at owner request (2026-08-28); note management lives
|
||||
on /tuning.html;
|
||||
* anonymous: brand + nav [Chat] (#nav-sources / #nav-git-sources /
|
||||
#nav-tuning hidden — locked A10 UI revision) + #new-chat-btn +
|
||||
#sign-in-link (with #sync-btn hidden, #sign-out-btn hidden) on all
|
||||
five pages — and the steering toggle + panel are ABSENT from the DOM
|
||||
(phase 16 "absent, not hidden" treatment, carried into phase 34 task
|
||||
01; test_admin_auth pins it).
|
||||
five pages — and the steering toggle (removed at owner request,
|
||||
2026-08-28) + panel are ABSENT from the DOM (the panel via the phase
|
||||
16 "absent, not hidden" treatment, carried into phase 34 task 01;
|
||||
test_admin_auth pins it).
|
||||
|
||||
Normalization for the inventory comparison: the current-page ``is-active``
|
||||
nav marker and the sign-in ``?next=`` value legitimately differ per page,
|
||||
@@ -40,26 +43,28 @@ page's bar (64px / 58px) and row 2 (``.doc-titlebar``) is present with
|
||||
(phase 13) is honored — ``back=`` accepted for same-origin relative
|
||||
URLs, rejected (→ /sources.html) otherwise.
|
||||
|
||||
Steering works off-chat: on /tuning.html (admin, zero notes) the toggle
|
||||
opens/closes #steering-panel with the empty state and a 0 count badge —
|
||||
no chat needed. Sync is present, not triggered: #sync-btn is visible on
|
||||
/tuning.html but is never clicked here (a real sync clones real repos —
|
||||
the full state machine is test_sync_button.py's job).
|
||||
Steering off-chat: on /tuning.html (admin, zero notes) the navbar
|
||||
carries no steering toggle (removed at owner request, 2026-08-28) — the
|
||||
header #steering-panel section still ships hidden and the Tuning
|
||||
page's own note list shows the empty state — no chat needed. Sync is
|
||||
present, not triggered: #sync-btn is visible on /tuning.html but is
|
||||
never clicked here (a real sync clones real repos — the full state
|
||||
machine is test_sync_button.py's job).
|
||||
|
||||
Determinism note: every assertion is settled-state — each page visit
|
||||
first waits for the whoami toggle to land (exactly one of Sign in /
|
||||
Sign out visible; the anonymous removal of the steering toggle happens
|
||||
Sign out visible; the anonymous removal of the steering panel happens
|
||||
in the SAME initSharedHeader pass) and, on the viewer, for the document
|
||||
title to render. The seed truncates steering_notes, so the count badge is
|
||||
0 on every admin page. No chat turn is ever submitted; #sync-btn is
|
||||
never clicked.
|
||||
title to render. The seed truncates steering_notes, so the Tuning page
|
||||
shows the empty state on every admin page. No chat turn is ever
|
||||
submitted; #sync-btn is never clicked.
|
||||
|
||||
Test → story mapping (Playwright Mapping Rule):
|
||||
1. ``test_admin_inventory_identical_on_all_five_pages``
|
||||
2. ``test_anonymous_inventory_identical_on_all_five_pages``
|
||||
3. ``test_viewer_row1_height_matches_chat_and_titlebar_present``
|
||||
4. ``test_viewer_back_link_honors_back_param``
|
||||
5. ``test_steering_panel_works_off_chat_on_tuning_page``
|
||||
5. ``test_steering_surface_off_chat_on_tuning_page``
|
||||
6. ``test_sync_button_present_on_tuning_page_without_triggering``
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@@ -152,7 +157,6 @@ _INVENTORY_JS = """() => {
|
||||
const sel = [
|
||||
".brand",
|
||||
".app-nav > a.nav-link",
|
||||
"#steering-toggle",
|
||||
"#sync-btn",
|
||||
"#new-chat-btn",
|
||||
"#sign-in-link",
|
||||
@@ -180,8 +184,8 @@ def _header_inventory(page: Page) -> list[str]:
|
||||
def _wait_settled(page: Page, admin: bool) -> None:
|
||||
"""Wait for initSharedHeader's whoami toggle to land: exactly one of
|
||||
Sign in / Sign out is visible (both ship hidden in the HTML). For
|
||||
anonymous visitors the steering toggle + panel removal happens in
|
||||
the SAME pass, so they are already gone when this returns."""
|
||||
anonymous visitors the steering panel removal happens in the SAME
|
||||
pass, so it is already gone when this returns."""
|
||||
if admin:
|
||||
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
|
||||
expect(page.locator("#sign-in-link")).to_be_hidden()
|
||||
@@ -220,7 +224,11 @@ def _visit(page: Page, app_url: str, name: str, url: str, admin: bool) -> list[s
|
||||
# revealed on every page, between Sources and Tuning.
|
||||
expect(page.locator("#nav-git-sources")).to_be_visible()
|
||||
expect(page.locator("#nav-tuning")).to_be_visible()
|
||||
expect(page.locator("#steering-toggle")).to_be_visible()
|
||||
# The steering toggle was removed from the navbar at owner
|
||||
# request (2026-08-28) — absent on every page, admin included.
|
||||
assert page.locator("#steering-toggle").count() == 0, (
|
||||
f"{name}: the steering toggle was removed from the navbar"
|
||||
)
|
||||
expect(page.locator("#sync-btn")).to_be_visible()
|
||||
expect(page.locator("#sign-out-btn")).to_be_visible()
|
||||
expect(page.locator("#sign-in-link")).to_be_hidden()
|
||||
@@ -233,9 +241,11 @@ def _visit(page: Page, app_url: str, name: str, url: str, admin: bool) -> list[s
|
||||
expect(page.locator("#sync-btn")).to_be_hidden()
|
||||
expect(page.locator("#sign-out-btn")).to_be_hidden()
|
||||
expect(page.locator("#sign-in-link")).to_be_visible()
|
||||
# …and the steering surface is ABSENT (phase 16 "absent, not
|
||||
# hidden", carried into the shared module by phase 34 task 01
|
||||
# — test_admin_auth pins the same contract).
|
||||
# …and the steering surface is ABSENT (the toggle was removed
|
||||
# from the navbar at owner request, 2026-08-28; the panel via
|
||||
# phase 16 "absent, not hidden", carried into the shared module
|
||||
# by phase 34 task 01 — test_admin_auth pins the same
|
||||
# contract).
|
||||
assert page.locator("#steering-toggle").count() == 0, (
|
||||
f"{name}: the steering toggle must be absent for anonymous"
|
||||
)
|
||||
@@ -289,7 +299,9 @@ def _admin_login_page_inventory(page: Page, app_url: str) -> list[str]:
|
||||
expect(page.locator("#nav-sources")).to_be_visible()
|
||||
expect(page.locator("#nav-git-sources")).to_be_visible()
|
||||
expect(page.locator("#nav-tuning")).to_be_visible()
|
||||
expect(page.locator("#steering-toggle")).to_be_visible()
|
||||
assert page.locator("#steering-toggle").count() == 0, (
|
||||
"login: the steering toggle was removed from the navbar"
|
||||
)
|
||||
expect(page.locator("#sync-btn")).to_be_visible()
|
||||
expect(page.locator("#sign-out-btn")).to_be_visible()
|
||||
expect(page.locator("#sign-in-link")).to_be_hidden()
|
||||
@@ -434,13 +446,14 @@ def test_viewer_back_link_honors_back_param(
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. Steering works off-chat: on /tuning.html (admin, zero notes) the
|
||||
# header toggle drives the panel — open/close cycle, empty state,
|
||||
# count badge 0. No chat turn is needed.
|
||||
# 5. Steering off-chat: on /tuning.html (admin, zero notes) the navbar
|
||||
# carries no steering toggle (removed at owner request, 2026-08-28)
|
||||
# — the header panel section still ships hidden and the Tuning
|
||||
# page's own note list shows the empty state. No chat turn needed.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_steering_panel_works_off_chat_on_tuning_page(
|
||||
def test_steering_surface_off_chat_on_tuning_page(
|
||||
page: Page, app_url: str, mock_llm: int, db_ready: None
|
||||
) -> None:
|
||||
page.set_viewport_size({"width": 1280, "height": 800})
|
||||
@@ -450,26 +463,16 @@ def test_steering_panel_works_off_chat_on_tuning_page(
|
||||
expect(page).to_have_url(app_url + TUNING_URL, timeout=30_000)
|
||||
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
|
||||
|
||||
# Settled admin state: the toggle is on the bar, the panel ships
|
||||
# hidden, and the count badge reads 0 (zero seeded notes).
|
||||
expect(page.locator("#steering-toggle")).to_be_visible()
|
||||
# Settled admin state: the navbar toggle is GONE (removed at owner
|
||||
# request, 2026-08-28), the header panel section still ships
|
||||
# hidden, and the Tuning page's own note list shows the empty
|
||||
# state (zero seeded notes).
|
||||
assert page.locator("#steering-toggle").count() == 0, (
|
||||
"the steering toggle was removed from the navbar"
|
||||
)
|
||||
expect(page.locator("#steering-panel")).to_be_hidden()
|
||||
expect(page.locator("#steering-count")).to_have_text("0")
|
||||
|
||||
# Open: the panel shows, the toggle's aria-expanded follows, and
|
||||
# the empty state is visible (the re-open refresh fetched 0 notes).
|
||||
page.click("#steering-toggle")
|
||||
expect(page.locator("#steering-panel")).to_be_visible()
|
||||
expect(page.locator("#steering-toggle")).to_have_attribute("aria-expanded", "true")
|
||||
expect(page.locator("#steering-empty")).to_be_visible()
|
||||
expect(page.locator("#steering-list .steering-note")).to_have_count(0)
|
||||
expect(page.locator("#steering-count")).to_have_text("0")
|
||||
|
||||
# Close: the cycle completes, the count badge still reads 0.
|
||||
page.click("#steering-toggle")
|
||||
expect(page.locator("#steering-panel")).to_be_hidden()
|
||||
expect(page.locator("#steering-toggle")).to_have_attribute("aria-expanded", "false")
|
||||
expect(page.locator("#steering-count")).to_have_text("0")
|
||||
expect(page.locator("#tune-list .tuning-note")).to_have_count(0)
|
||||
expect(page.locator("#tune-empty")).to_be_visible()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
+52
-62
@@ -13,8 +13,9 @@ stored in Postgres (``steering_notes``) → injected into the system prompt
|
||||
of every subsequent turn as the ``<tuning>`` section. The mock LLM
|
||||
echoes the first tuning note into its answer
|
||||
(`` (tuning: <first note line>)``), so prompt injection is observable in
|
||||
the UI deterministically. Notes are listed newest-first in the header
|
||||
"Tuning" panel, where each can be deleted.
|
||||
the UI deterministically. Notes are listed newest-first on the Tuning
|
||||
page (``/tuning.html``), where each can be deleted — the header "Tuning"
|
||||
toggle was removed from the navbar at owner request (2026-08-28).
|
||||
|
||||
Test → story mapping (Playwright Mapping Rule):
|
||||
1. ``test_tune_under_answer_persists_and_steers``
|
||||
@@ -45,8 +46,9 @@ QUESTION = "How is my Kubernetes cluster set up?"
|
||||
MOCK_ANSWER_MARKER = "Deterministic mock answer for E2E"
|
||||
NOTE = "STEEER-MARKER be concise"
|
||||
XSS_NOTE = "<script>window.__xss = true; alert('xss')</script>"
|
||||
#: index.html ships exactly three classic/module script tags: the
|
||||
#: phase-39 brand.js classic layer + markdown.js + the app.js module.
|
||||
#: Both index.html and tuning.html ship exactly three classic/module
|
||||
#: script tags: the phase-39 brand.js classic layer + markdown.js + the
|
||||
#: page module (app.js / tuning.js).
|
||||
BASE_SCRIPT_COUNT = 3
|
||||
|
||||
|
||||
@@ -113,11 +115,6 @@ def _tune_and_save(page: Page, note: str) -> None:
|
||||
expect(saved).to_contain_text("Saved — future answers will follow this.", timeout=15_000)
|
||||
|
||||
|
||||
def _open_panel(page: Page) -> None:
|
||||
page.click("#steering-toggle")
|
||||
expect(page.locator("#steering-panel")).to_be_visible()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Tune under an answer → persisted → next answer carries the note
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -147,14 +144,15 @@ def test_tune_under_answer_persists_and_steers(
|
||||
rows = db.scalars(select(SteeringNote)).all()
|
||||
assert [r.note for r in rows] == [NOTE]
|
||||
|
||||
# The header panel shows the note with an updated count badge.
|
||||
_open_panel(page)
|
||||
expect(page.locator("#steering-count")).to_have_text("1")
|
||||
expect(page.locator("#steering-list .steering-note")).to_have_count(1)
|
||||
expect(page.locator("#steering-list .steering-note-text")).to_have_text(NOTE)
|
||||
page.click("#steering-toggle") # close again
|
||||
# The Tuning page (the steering-notes manager — the navbar toggle
|
||||
# was removed at owner request, 2026-08-28) lists the note.
|
||||
page.goto(app_url + "/tuning.html")
|
||||
expect(page.locator("#tune-list .tuning-note")).to_have_count(1)
|
||||
expect(page.locator("#tune-list .tuning-note-text")).to_have_text(NOTE)
|
||||
|
||||
# The NEXT answer carries the note — it reached the system prompt.
|
||||
page.goto(app_url + "/")
|
||||
expect(page.locator("#send-btn")).to_be_enabled()
|
||||
_ask(page, QUESTION)
|
||||
bubble = page.locator(".msg.brain .bubble").last
|
||||
expect(bubble).to_contain_text(f"(tuning: {NOTE})")
|
||||
@@ -179,19 +177,21 @@ def test_delete_note_stops_steering(
|
||||
_ask(page, QUESTION)
|
||||
expect(page.locator(".msg.brain .bubble").last).to_contain_text(f"(tuning: {NOTE})")
|
||||
|
||||
# Delete the note from the panel.
|
||||
_open_panel(page)
|
||||
expect(page.locator("#steering-count")).to_have_text("1")
|
||||
page.locator("#steering-list .steering-delete").click()
|
||||
expect(page.locator("#steering-list .steering-note")).to_have_count(0)
|
||||
expect(page.locator("#steering-count")).to_have_text("0")
|
||||
expect(page.locator("#steering-empty")).to_be_visible()
|
||||
expect(page.locator("#steering-announcer")).to_contain_text("deleted")
|
||||
# Delete the note from the Tuning page (the header panel is no
|
||||
# longer reachable — the navbar toggle is gone).
|
||||
page.goto(app_url + "/tuning.html")
|
||||
expect(page.locator("#tune-list .tuning-note")).to_have_count(1)
|
||||
page.locator(".tuning-delete").click()
|
||||
expect(page.locator("#tune-list .tuning-note")).to_have_count(0, timeout=15_000)
|
||||
expect(page.locator("#tune-empty")).to_be_visible()
|
||||
expect(page.locator("#tune-announcer")).to_contain_text("deleted")
|
||||
|
||||
with SessionLocal() as db:
|
||||
assert db.scalars(select(SteeringNote)).all() == []
|
||||
|
||||
# The next answer no longer carries the marker.
|
||||
page.goto(app_url + "/")
|
||||
expect(page.locator("#send-btn")).to_be_enabled()
|
||||
_ask(page, QUESTION)
|
||||
bubble = page.locator(".msg.brain .bubble").last
|
||||
expect(bubble).to_contain_text(MOCK_ANSWER_MARKER)
|
||||
@@ -222,12 +222,13 @@ def test_note_rendered_as_text_xss_safe(
|
||||
_ask(page, QUESTION)
|
||||
_tune_and_save(page, XSS_NOTE)
|
||||
|
||||
# Panel: the payload is visible as LITERAL text…
|
||||
_open_panel(page)
|
||||
expect(page.locator("#steering-list .steering-note-text")).to_have_text(XSS_NOTE)
|
||||
# Tuning page: the payload is visible as LITERAL text…
|
||||
page.goto(app_url + "/tuning.html")
|
||||
expect(page.locator("#tune-list .tuning-note-text")).to_have_text(XSS_NOTE)
|
||||
|
||||
# …never as an executed element: no script tag anywhere, no dialog.
|
||||
assert page.locator("#steering-panel script").count() == 0
|
||||
# …never as an executed element: no script tag in the list, the page
|
||||
# still carries exactly its own three scripts, no dialog.
|
||||
assert page.locator("#tune-list script").count() == 0
|
||||
expect(page.locator("script")).to_have_count(BASE_SCRIPT_COUNT)
|
||||
assert dialogs == [], f"the note must never execute as script: {dialogs}"
|
||||
assert page.evaluate("() => window.__xss === undefined") is True
|
||||
@@ -239,34 +240,27 @@ def test_note_rendered_as_text_xss_safe(
|
||||
|
||||
|
||||
def test_tuning_panel_a11y(page: Page, app_url: str, db_ready: None) -> None:
|
||||
_reset_db(mock_port=0, seed=False) # no KB seeding needed for the panel a11y
|
||||
"""The steering surface's a11y now lives on the Tuning page (the
|
||||
header toggle was removed from the navbar at owner request,
|
||||
2026-08-28): the note list, the polite live region, and the labeled
|
||||
per-note delete (≥44px)."""
|
||||
_reset_db(mock_port=0, seed=False) # no KB seeding needed for the page a11y
|
||||
page.set_default_timeout(30_000)
|
||||
page.goto(app_url)
|
||||
login(page, app_url, next="/") # phase 16: the panel is admin-only
|
||||
login(page, app_url, next="/tuning.html") # phase 16: the notes are admin-only
|
||||
|
||||
toggle = page.locator("#steering-toggle")
|
||||
panel = page.locator("#steering-panel")
|
||||
announcer = page.locator("#steering-announcer")
|
||||
# The navbar no longer carries a steering toggle — absent for the
|
||||
# admin too — and the header panel section still ships hidden.
|
||||
assert page.locator("#steering-toggle").count() == 0
|
||||
expect(page.locator("#steering-panel")).to_be_hidden()
|
||||
|
||||
# Initial: closed, correctly wired, polite live region present.
|
||||
expect(toggle).to_have_attribute("aria-expanded", "false")
|
||||
expect(toggle).to_have_attribute("aria-controls", "steering-panel")
|
||||
expect(panel).to_have_attribute("role", "region")
|
||||
assert "Tuning notes" in (panel.get_attribute("aria-label") or "")
|
||||
expect(panel).to_be_hidden()
|
||||
# The page's own polite live region.
|
||||
announcer = page.locator("#tune-announcer")
|
||||
assert announcer.get_attribute("role") == "status"
|
||||
assert announcer.get_attribute("aria-live") == "polite"
|
||||
# Accessible name comes from its visible text (icon is aria-hidden).
|
||||
assert "Tuning" in toggle.inner_text()
|
||||
|
||||
# Open: expanded + the designed empty state.
|
||||
toggle.click()
|
||||
expect(toggle).to_have_attribute("aria-expanded", "true")
|
||||
expect(panel).to_be_visible()
|
||||
expect(page.locator("#steering-empty")).to_be_visible()
|
||||
expect(page.locator("#steering-count")).to_have_text("0")
|
||||
|
||||
# Add a note (API), then re-open the panel to refresh it.
|
||||
# Add a note (API), reload to refresh the list (tuning.js fetches
|
||||
# on boot).
|
||||
page.evaluate(
|
||||
"""async () => {
|
||||
const r = await fetch('/api/steering', {
|
||||
@@ -277,26 +271,22 @@ def test_tuning_panel_a11y(page: Page, app_url: str, db_ready: None) -> None:
|
||||
if (!r.ok) throw new Error('steering POST failed: ' + r.status);
|
||||
}"""
|
||||
)
|
||||
toggle.click() # close
|
||||
toggle.click() # re-open (refreshes the list)
|
||||
note_item = page.locator("#steering-list .steering-note")
|
||||
page.reload()
|
||||
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
|
||||
note_item = page.locator("#tune-list .tuning-note")
|
||||
expect(note_item).to_have_count(1)
|
||||
expect(note_item.locator(".steering-note-text")).to_have_text("a11y note one")
|
||||
expect(note_item.locator(".tuning-note-text")).to_have_text("a11y note one")
|
||||
|
||||
# The per-note delete is a real, labeled button (≥44px target).
|
||||
delete = page.locator("#steering-list .steering-delete")
|
||||
delete = page.locator(".tuning-delete")
|
||||
expect(delete).to_have_attribute("type", "button")
|
||||
assert (delete.get_attribute("aria-label") or "").startswith("Delete tuning note:")
|
||||
box = delete.bounding_box()
|
||||
assert box is not None and box["height"] >= 44
|
||||
|
||||
# Delete: list empties, count updates, the live region announces it.
|
||||
# Delete: list empties, the empty state shows, the live region
|
||||
# announces it.
|
||||
delete.click()
|
||||
expect(page.locator("#steering-list .steering-note")).to_have_count(0)
|
||||
expect(page.locator("#steering-count")).to_have_text("0")
|
||||
expect(page.locator("#tune-list .tuning-note")).to_have_count(0, timeout=15_000)
|
||||
expect(page.locator("#tune-empty")).to_be_visible()
|
||||
expect(announcer).to_contain_text("deleted")
|
||||
|
||||
# And the toggle closes cleanly again.
|
||||
toggle.click()
|
||||
expect(toggle).to_have_attribute("aria-expanded", "false")
|
||||
expect(panel).to_be_hidden()
|
||||
|
||||
@@ -6,27 +6,25 @@ toggle) to ANONYMOUS visitors — it shipped VISIBLE in all six pages'
|
||||
markup and ``assets/header.js`` removed it only after ``/api/whoami``
|
||||
resolved, so the button flashed for the whole whoami round-trip.
|
||||
|
||||
The fix mirrors the admin-only nav links (phase 19/29/35 contract):
|
||||
the toggle now SHIPS ``hidden`` in every page and ``initSharedHeader``
|
||||
unhides it only when whoami says admin; the anonymous end-state is
|
||||
unchanged (toggle + panel REMOVED from the DOM — phase 16 "absent,
|
||||
not hidden"). This suite proves the browser-level contract:
|
||||
Phase 40 fixed it with the ship-hidden / reveal-for-admin contract
|
||||
(the admin-only nav links). The owner then asked for the button to go
|
||||
away entirely (2026-08-28): the navbar ``#steering-toggle`` is now
|
||||
ABSENT from every page — for the admin AND anonymous — so the
|
||||
never-visible contract holds by construction, and note management
|
||||
lives on ``/tuning.html``. The anonymous end-state is unchanged: the
|
||||
``#steering-panel`` is REMOVED from the DOM (phase 16 "absent, not
|
||||
hidden") and ``/api/steering`` is never fetched. This suite proves the
|
||||
browser-level contract:
|
||||
|
||||
* a MutationObserver (installed via ``add_init_script`` before any
|
||||
page code runs) records every frame in which ``#steering-toggle``
|
||||
is both in the DOM and visible (``offsetParent !== null`` or
|
||||
``!hidden``) — an anonymous load records ZERO such frames, on
|
||||
every page, from first paint to the settled state;
|
||||
* after the whoami round-trip the toggle is ABSENT from the DOM for
|
||||
anonymous visitors (removed, not hidden);
|
||||
* the admin UX is untouched (phase 15/34 behavior): the toggle is
|
||||
revealed, clicking opens ``#steering-panel``
|
||||
(``aria-expanded="true"``) and the count badge matches the note
|
||||
list — including a self-check that the observer records the admin
|
||||
reveal, so the zero-frame anonymous assertions are not vacuous;
|
||||
* the ship-hidden nav-link contract this phase relies on
|
||||
(``#nav-sources`` / ``#nav-git-sources`` / ``#nav-tuning``) is
|
||||
intact: hidden for anonymous, revealed for admin.
|
||||
* ``#steering-toggle`` is absent from the DOM on every page, for
|
||||
anonymous AND admin — nothing can flash, because nothing ships;
|
||||
* the anonymous end-state survives the removal: ``#steering-panel``
|
||||
is removed from the DOM, not just hidden;
|
||||
* the admin UX around the removal is intact: the header panel section
|
||||
still ships hidden (never opened from the header anymore), and the
|
||||
ship-hidden nav-link contract this phase relies on
|
||||
(``#nav-sources`` / ``#nav-git-sources`` / ``#nav-tuning``) holds:
|
||||
hidden for anonymous, revealed for admin.
|
||||
|
||||
Story: ``.agent/user_stories/tuning-toggle-flash.md``
|
||||
Run in isolation (DB must be up: ``podman compose up -d db``):
|
||||
@@ -42,82 +40,27 @@ Test → story mapping (Playwright Mapping Rule):
|
||||
from __future__ import annotations
|
||||
|
||||
from playwright.sync_api import Page, expect
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.db import SessionLocal
|
||||
from e2e.auth_helpers import login
|
||||
|
||||
#: The pages the contract must hold on besides the chat page (mapping
|
||||
#: rule 2). document.html / git-sources.html are covered by the
|
||||
#: source-level unit pins (tests/unit/test_steering_toggle_visibility.py)
|
||||
#: — the four pages here are the ones an anonymous visitor actually
|
||||
#: lands on.
|
||||
#: source-level unit pins (tests/unit/test_steering_toggle_removal.py)
|
||||
#: — the three pages here are the other pages an anonymous visitor
|
||||
#: actually lands on.
|
||||
OTHER_PAGES = ("/sources.html", "/tuning.html", "/login.html")
|
||||
|
||||
ADMIN_NOTE = "PHASE40-E2E note — badge check"
|
||||
|
||||
#: Admin-only nav links — the ship-hidden / reveal-for-admin family the
|
||||
#: steering toggle now belongs to (phase 19/29/35 contract).
|
||||
#: steering toggle belonged to (phase 19/29/35 contract).
|
||||
NAV_IDS = ("#nav-sources", "#nav-git-sources", "#nav-tuning")
|
||||
|
||||
#: Runs in every new document BEFORE any page script (addInitScript):
|
||||
#: arms a MutationObserver over the whole DOM and records every frame
|
||||
#: in which #steering-toggle is attached AND visible — visible meaning
|
||||
#: rendered (offsetParent !== null) OR carrying no [hidden] attribute
|
||||
#: (!el.hidden). A shipped-VISIBLE toggle (the old bug) is recorded the
|
||||
#: moment the parser inserts it; a shipped-hidden toggle that is later
|
||||
#: revealed is recorded at the reveal mutation. The array is fresh per
|
||||
#: document, so each navigation asserts its own frames.
|
||||
VISIBILITY_OBSERVER_JS = """
|
||||
window.__tuningVisibleFrames = [];
|
||||
(() => {
|
||||
const visible = (el) =>
|
||||
!!el && el.isConnected && (el.offsetParent !== null || !el.hidden);
|
||||
const check = () => {
|
||||
if (visible(document.getElementById("steering-toggle"))) {
|
||||
window.__tuningVisibleFrames.push({
|
||||
at: Math.round(performance.now()),
|
||||
href: location.pathname,
|
||||
});
|
||||
}
|
||||
};
|
||||
const start = () => {
|
||||
check();
|
||||
new MutationObserver(check).observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ["hidden"],
|
||||
});
|
||||
};
|
||||
if (document.documentElement) start();
|
||||
else document.addEventListener("DOMContentLoaded", start);
|
||||
})();
|
||||
"""
|
||||
|
||||
|
||||
def install_visibility_observer(page: Page) -> None:
|
||||
"""Arm the never-visible frame counter on every document this page
|
||||
creates — the initial load, the post-login redirect, re-gotos."""
|
||||
page.add_init_script(VISIBILITY_OBSERVER_JS)
|
||||
|
||||
|
||||
def visible_frames(page: Page) -> list[dict[str, object]]:
|
||||
"""The frames the observer recorded in the CURRENT document."""
|
||||
return page.evaluate("() => window.__tuningVisibleFrames || []")
|
||||
|
||||
|
||||
def _assert_no_flash(page: Page, path: str) -> None:
|
||||
"""Zero visible frames + the phase-16 absent end-state, for one
|
||||
anonymously loaded page."""
|
||||
frames = visible_frames(page)
|
||||
assert frames == [], f"{path}: the toggle was visible {len(frames)}x: {frames!r}"
|
||||
def _assert_no_toggle(page: Page, path: str) -> None:
|
||||
"""The navbar toggle is absent from the DOM for one page — nothing
|
||||
to flash, for either role."""
|
||||
assert page.locator("#steering-toggle").count() == 0, (
|
||||
f"{path}: #steering-toggle must be REMOVED from the DOM for "
|
||||
"anonymous (phase 16 'absent, not hidden')"
|
||||
)
|
||||
assert page.locator("#steering-panel").count() == 0, (
|
||||
f"{path}: #steering-panel must be removed together with the toggle"
|
||||
f"{path}: #steering-toggle must be ABSENT from the DOM "
|
||||
"(removed from the navbar at owner request, 2026-08-28)"
|
||||
)
|
||||
|
||||
|
||||
@@ -125,14 +68,21 @@ def _wait_header_settled_anonymous(page: Page) -> None:
|
||||
"""Whoami resolved on the current document: the anonymous state
|
||||
reveals the Sign in link and keeps Sign out hidden (the pair is
|
||||
decided by the SAME initSharedHeader pass that removes the
|
||||
toggle)."""
|
||||
steering panel)."""
|
||||
page.wait_for_load_state("networkidle")
|
||||
expect(page.locator("#sign-in-link")).to_be_visible(timeout=15_000)
|
||||
expect(page.locator("#sign-out-btn")).to_be_hidden()
|
||||
|
||||
|
||||
def _wait_header_settled_admin(page: Page) -> None:
|
||||
"""Whoami resolved for a signed-in admin on the current document."""
|
||||
page.wait_for_load_state("networkidle")
|
||||
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
|
||||
expect(page.locator("#sign-in-link")).to_be_hidden()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Anonymous chat load: zero visible frames, toggle absent afterwards
|
||||
# 1. Anonymous chat load: zero toggle, panel removed (never visible)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -140,14 +90,19 @@ def test_anonymous_never_sees_toggle(
|
||||
page: Page, app_url: str, mock_llm: int, db_ready: None
|
||||
) -> None:
|
||||
page.set_default_timeout(30_000)
|
||||
install_visibility_observer(page)
|
||||
page.goto(app_url + "/")
|
||||
_wait_header_settled_anonymous(page)
|
||||
_assert_no_flash(page, "/")
|
||||
_assert_no_toggle(page, "/")
|
||||
# The phase-16 end-state survives the removal: the panel is REMOVED
|
||||
# from the DOM, not just hidden.
|
||||
assert page.locator("#steering-panel").count() == 0, (
|
||||
"/: #steering-panel must be removed from the DOM for anonymous "
|
||||
"(phase 16 'absent, not hidden')"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. Anonymous loads of the other pages: same zero-flash contract
|
||||
# 2. Anonymous loads of the other pages: same no-toggle contract
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -155,15 +110,19 @@ def test_anonymous_other_pages_never_flash(
|
||||
page: Page, app_url: str, mock_llm: int, db_ready: None
|
||||
) -> None:
|
||||
page.set_default_timeout(30_000)
|
||||
install_visibility_observer(page)
|
||||
for path in OTHER_PAGES:
|
||||
page.goto(app_url + path)
|
||||
_wait_header_settled_anonymous(page)
|
||||
_assert_no_flash(page, path)
|
||||
_assert_no_toggle(page, path)
|
||||
assert page.locator("#steering-panel").count() == 0, (
|
||||
f"{path}: #steering-panel must be removed from the DOM for "
|
||||
"anonymous (phase 16 'absent, not hidden')"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Admin: revealed, clickable, count badge matches the list
|
||||
# 3. Admin: the toggle is gone too — the panel still ships hidden and
|
||||
# the nav links are revealed
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -171,60 +130,26 @@ def test_admin_toggle_revealed_and_working(
|
||||
page: Page, app_url: str, mock_llm: int, db_ready: None
|
||||
) -> None:
|
||||
page.set_default_timeout(30_000)
|
||||
with SessionLocal() as db:
|
||||
db.execute(text("TRUNCATE steering_notes"))
|
||||
db.commit()
|
||||
|
||||
install_visibility_observer(page)
|
||||
login(page, app_url, next="/")
|
||||
page.wait_for_load_state("networkidle")
|
||||
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
|
||||
_wait_header_settled_admin(page)
|
||||
|
||||
toggle = page.locator("#steering-toggle")
|
||||
panel = page.locator("#steering-panel")
|
||||
|
||||
# Revealed — the [hidden] attribute is gone and the button renders.
|
||||
expect(toggle).to_be_visible()
|
||||
assert toggle.get_attribute("hidden") is None
|
||||
|
||||
# Observer self-check: the reveal IS a recorded visible frame, so
|
||||
# the zero-frame anonymous assertions above cannot be vacuous.
|
||||
assert visible_frames(page) != [], "the admin reveal was not observed"
|
||||
|
||||
# Click: the panel opens, aria-expanded tracks it (phase 15/34).
|
||||
toggle.click()
|
||||
expect(panel).to_be_visible()
|
||||
expect(toggle).to_have_attribute("aria-expanded", "true")
|
||||
|
||||
# Count badge matches the list — first the empty state…
|
||||
expect(page.locator("#steering-count")).to_have_text("0")
|
||||
expect(page.locator("#steering-list .steering-note")).to_have_count(0)
|
||||
expect(page.locator("#steering-empty")).to_be_visible()
|
||||
|
||||
# …then with one note created through the real admin API.
|
||||
page.evaluate(
|
||||
"""async (note) => {
|
||||
const r = await fetch("/api/steering", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({note}),
|
||||
});
|
||||
if (!r.ok) throw new Error("steering POST failed: " + r.status);
|
||||
}""",
|
||||
ADMIN_NOTE,
|
||||
)
|
||||
toggle.click() # close
|
||||
toggle.click() # re-open (refreshes the list)
|
||||
expect(panel).to_be_visible()
|
||||
expect(toggle).to_have_attribute("aria-expanded", "true")
|
||||
expect(page.locator("#steering-count")).to_have_text("1")
|
||||
expect(page.locator("#steering-list .steering-note")).to_have_count(1)
|
||||
expect(page.locator("#steering-list .steering-note-text")).to_have_text(ADMIN_NOTE)
|
||||
# The navbar toggle was removed at owner request (2026-08-28) —
|
||||
# absent for the admin as well; nothing in the header can flash
|
||||
# it back, because nothing ships it anymore.
|
||||
_assert_no_toggle(page, "/")
|
||||
# The header panel section survives (kept fresh by the chat page's
|
||||
# per-bubble Tune form) but ships hidden — no header control opens
|
||||
# it anymore; note management lives on /tuning.html.
|
||||
assert page.locator("#steering-panel").count() == 1
|
||||
expect(page.locator("#steering-panel")).to_be_hidden()
|
||||
# The surviving path to the notes: the admin-only Tuning nav link
|
||||
# is revealed.
|
||||
expect(page.locator("#nav-tuning")).to_be_visible()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. Nav-contract regression (phase 19/34): the ship-hidden family the
|
||||
# toggle now belongs to is intact
|
||||
# toggle used to belong to is intact
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -245,9 +170,8 @@ def test_nav_contract_regression(
|
||||
)
|
||||
|
||||
# Admin: the same links are revealed — the exact contract the
|
||||
# steering toggle now mirrors.
|
||||
# steering toggle mirrored before it was removed.
|
||||
login(page, app_url, next="/")
|
||||
page.wait_for_load_state("networkidle")
|
||||
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
|
||||
_wait_header_settled_admin(page)
|
||||
for nav in NAV_IDS:
|
||||
expect(page.locator(nav)).to_be_visible()
|
||||
|
||||
@@ -219,8 +219,9 @@ def test_header_z_index_is_20_so_the_dropdown_uses_21() -> None:
|
||||
def test_mobile_block_renders_the_44px_toggle() -> None:
|
||||
"""At ≤640px the toggle is a 44px×44px button (the phase-07 touch
|
||||
floor), ghost look like the other pills, a hover state in the
|
||||
.steering-toggle:hover family, and a SIZED icon (an unsized inline
|
||||
SVG defaults to 300px and would blow the bar out at 360px).
|
||||
shared pill hover family (brand-ink on brand-soft, as
|
||||
.nav-link:hover), and a SIZED icon (an unsized inline SVG defaults
|
||||
to 300px and would blow the bar out at 360px).
|
||||
:focus-visible needs no rule — the global 3px outline applies."""
|
||||
mobile = _media_block(_css(), "@media (max-width: 640px)")
|
||||
block = _rule_block(mobile, ".nav-toggle")
|
||||
@@ -239,7 +240,7 @@ def test_mobile_block_renders_the_44px_toggle() -> None:
|
||||
hover = _rule_block(mobile, ".nav-toggle:hover")
|
||||
assert "background: var(--brand-soft)" in hover
|
||||
assert "color: var(--brand-ink)" in hover, (
|
||||
"hover matches the .steering-toggle:hover family (≈6.9:1 pair)"
|
||||
"hover matches the shared pill hover family (≈6.9:1 pair)"
|
||||
)
|
||||
icon = _rule_block(mobile, ".nav-toggle svg")
|
||||
assert "width: 20px" in icon and "height: 20px" in icon, (
|
||||
|
||||
@@ -125,8 +125,9 @@ def test_nav_sources_ships_hidden_on_every_nav_page() -> None:
|
||||
def test_all_five_pages_share_the_header_control_order() -> None:
|
||||
"""Phase 34 task 03 (owner confirmation 2026-08-26): every page
|
||||
ships the IDENTICAL header control inventory in the IDENTICAL
|
||||
order — brand, nav [Chat, Sources, Tuning], #steering-toggle,
|
||||
#sync-btn, #new-chat-btn, Sign in, Sign out — inside the shared
|
||||
order — brand, nav [Chat, Sources, Tuning], #sync-btn, #new-chat-btn,
|
||||
Sign in, Sign out (the #steering-toggle was removed from the navbar
|
||||
at owner request, 2026-08-28) — inside the shared
|
||||
.header-inner row (the document viewer's row 1). Only the
|
||||
current-page is-active nav marker and the static ?next= fallback
|
||||
may differ per page (task 05's story E2E pins the rendered
|
||||
@@ -137,7 +138,6 @@ def test_all_five_pages_share_the_header_control_order() -> None:
|
||||
'href="/"',
|
||||
'id="nav-sources"',
|
||||
'id="nav-tuning"',
|
||||
'id="steering-toggle"',
|
||||
'id="sync-btn"',
|
||||
'id="new-chat-btn"',
|
||||
'id="sign-in-link"',
|
||||
@@ -288,16 +288,16 @@ def test_login_page_carries_the_full_header() -> None:
|
||||
"""Phase 34 task 03 (owner confirmation 2026-08-26): the noted
|
||||
boundary is reversed by owner decision — the login page finally
|
||||
carries the FULL shared header: the nav gains the #nav-tuning link
|
||||
(same ship-hidden markup as the other pages), plus the Tuning
|
||||
toggle, the admin-only Sync button, New chat, and the Sign in /
|
||||
Sign out pair (ship hidden — initSharedHeader reveals exactly one
|
||||
after whoami; the static ?next= fallback is the login page itself).
|
||||
(same ship-hidden markup as the other pages), plus the admin-only
|
||||
Sync button, New chat, and the Sign in / Sign out pair (ship hidden
|
||||
— initSharedHeader reveals exactly one after whoami; the static
|
||||
?next= fallback is the login page itself). The Tuning toggle is NOT
|
||||
among them — removed from the navbar at owner request (2026-08-28).
|
||||
No nav link is "current" on the auth page."""
|
||||
text = _text(LOGIN_HTML)
|
||||
for marker in (
|
||||
'id="nav-sources"',
|
||||
'id="nav-tuning"',
|
||||
'id="steering-toggle"',
|
||||
'id="sync-btn"',
|
||||
'id="new-chat-btn"',
|
||||
'id="sign-in-link"',
|
||||
@@ -410,12 +410,11 @@ def test_header_module_owns_the_steering_panel() -> None:
|
||||
"""header.js owns the steering panel behavior (moved from app.js in
|
||||
phase 34 task 01): the module-level null-safe element refs, the
|
||||
newest-first textContent render (XSS contract), the labeled
|
||||
per-note delete, the count badge, the announcer, and the toggle
|
||||
binding that runs at module import (like the sign-out binding)."""
|
||||
per-note delete, and the announcer — and NO toggle wiring anymore
|
||||
(the navbar #steering-toggle was removed at owner request,
|
||||
2026-08-28; note management lives on /tuning.html)."""
|
||||
js = _text(HEADER_JS)
|
||||
for selector in (
|
||||
"#steering-toggle",
|
||||
"#steering-count",
|
||||
"#steering-panel",
|
||||
"#steering-list",
|
||||
"#steering-empty",
|
||||
@@ -430,9 +429,8 @@ def test_header_module_owns_the_steering_panel() -> None:
|
||||
assert 'del.setAttribute("aria-label", `Delete tuning note: ${n.note}`)' in js
|
||||
assert "async function deleteSteeringNote" in js
|
||||
assert 'fetch(`/api/steering/${encodeURIComponent(id)}`, { method: "DELETE" })' in js
|
||||
assert "steeringToggle.addEventListener" in js, "the toggle binding is module-owned"
|
||||
assert 'setAttribute("aria-expanded"' in js
|
||||
assert "refreshSteering()" in js # re-open refreshes the list
|
||||
assert "refreshSteering()" in js # the admin boot refresh + the form's save
|
||||
assert "steeringToggle" not in js, "the navbar toggle is gone (2026-08-28)"
|
||||
|
||||
|
||||
def test_header_module_exports_refresh_and_announce_steering() -> None:
|
||||
@@ -455,17 +453,18 @@ def test_header_module_exports_refresh_and_announce_steering() -> None:
|
||||
|
||||
|
||||
def test_init_shared_header_gates_the_steering_surface() -> None:
|
||||
"""Inside initSharedHeader: admin → the list refreshes (count badge
|
||||
right before the panel is ever opened; only when the page ships the
|
||||
panel markup); anonymous → the toggle + panel are REMOVED from the
|
||||
DOM (phase-16 'absent, not hidden') and /api/steering is never
|
||||
fetched."""
|
||||
"""Inside initSharedHeader: admin → the list refreshes (the panel
|
||||
ships hidden and is only kept fresh; only when the page ships the
|
||||
panel markup); anonymous → the panel is REMOVED from the DOM
|
||||
(phase-16 'absent, not hidden') and /api/steering is never fetched.
|
||||
The navbar toggle was removed at owner request (2026-08-28) — no
|
||||
unhide / remove of a toggle may be left behind."""
|
||||
js = _text(HEADER_JS)
|
||||
fn = js.find("function initSharedHeader")
|
||||
assert fn != -1
|
||||
body = js[fn : js.find("\n}", fn)]
|
||||
assert "if (steeringPanel) refreshSteering();" in body
|
||||
assert "steeringToggle?.remove();" in body
|
||||
assert "steeringToggle" not in body
|
||||
assert "steeringPanel?.remove();" in body
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
"""Unit: the navbar steering toggle is gone (owner request, 2026-08-28).
|
||||
|
||||
The "Tuning" steering toggle (``#steering-toggle``) used to sit in
|
||||
every page's shared header — shipped hidden (phase 40), revealed for
|
||||
the admin, removed from the DOM for anonymous. The owner asked for the
|
||||
button to go away entirely: note management now lives on the
|
||||
standalone Tuning page (``/tuning.html``, phase 27). This file pins the
|
||||
removal at source level — the toggle + count badge are ABSENT from all
|
||||
six pages, the ``#steering-panel`` section still ships hidden (kept
|
||||
fresh by the chat page's per-bubble Tune form through header.js), the
|
||||
shared module owns no toggle wiring anymore, and the admin-only
|
||||
``#nav-tuning`` link — the surviving path to the notes — still ships
|
||||
hidden.
|
||||
"""
|
||||
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"
|
||||
|
||||
#: All six pages carry the shared header block (phase 34's five pages +
|
||||
#: phase 35's git-sources page).
|
||||
PAGES = (
|
||||
FRONTEND / "index.html",
|
||||
FRONTEND / "sources.html",
|
||||
FRONTEND / "document.html",
|
||||
FRONTEND / "git-sources.html",
|
||||
FRONTEND / "login.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 _init_body(js: str) -> str:
|
||||
"""The source of initSharedHeader in header.js."""
|
||||
fn = js.find("function initSharedHeader")
|
||||
assert fn != -1, "initSharedHeader must be defined"
|
||||
return js[fn : js.find("\n}", fn)]
|
||||
|
||||
|
||||
# ---------- the removal: absent from every page's navbar ----------
|
||||
|
||||
|
||||
def test_steering_toggle_removed_from_all_six_pages() -> None:
|
||||
"""The #steering-toggle button (and its #steering-count badge) is
|
||||
gone from the navbar of EVERY page — absent, not hidden."""
|
||||
for html in PAGES:
|
||||
text = _text(html)
|
||||
assert 'id="steering-toggle"' not in text, (
|
||||
f"{html.name}: #steering-toggle must be removed from the navbar"
|
||||
)
|
||||
assert 'class="steering-toggle"' not in text, (
|
||||
f"{html.name}: the .steering-toggle button must be removed"
|
||||
)
|
||||
assert 'id="steering-count"' not in text, (
|
||||
f"{html.name}: the #steering-count badge lived inside the toggle"
|
||||
)
|
||||
|
||||
|
||||
def test_steering_panel_still_ships_hidden_on_all_six_pages() -> None:
|
||||
"""The #steering-panel section survives the toggle removal (the chat
|
||||
page's per-bubble Tune form keeps it fresh through header.js) and
|
||||
still ships hidden, with its list / empty state / announcer."""
|
||||
for html in PAGES:
|
||||
text = _text(html)
|
||||
tag = re.search(r'<section[^>]*id="steering-panel"[^>]*>', text)
|
||||
assert tag, f"{html.name}: missing the #steering-panel section"
|
||||
assert re.search(r"\bhidden\b", tag.group(0)), "the panel ships hidden"
|
||||
assert 'id="steering-list"' in text, f"{html.name}: missing #steering-list"
|
||||
assert 'id="steering-empty"' in text, f"{html.name}: missing #steering-empty"
|
||||
assert re.search(r'<p[^>]*id="steering-announcer"[^>]*role="status"[^>]*>', text), (
|
||||
f"{html.name}: missing the #steering-announcer live region"
|
||||
)
|
||||
|
||||
|
||||
# ---------- header.js: no toggle wiring left, panel contract intact ----------
|
||||
|
||||
|
||||
def test_header_js_carries_no_toggle_wiring() -> None:
|
||||
"""header.js owns no #steering-toggle reference anymore: no element
|
||||
lookup, no admin unhide, no anonymous removal, no click binding, no
|
||||
open/close helper, no count badge."""
|
||||
js = _text(HEADER_JS)
|
||||
assert 'querySelector("#steering-toggle")' not in js
|
||||
assert "steeringToggle" not in js
|
||||
assert "setSteeringPanel" not in js
|
||||
assert 'querySelector("#steering-count")' not in js
|
||||
assert "steeringCount" not in js
|
||||
|
||||
|
||||
def test_header_js_keeps_the_panel_contract() -> None:
|
||||
"""What survives: inside initSharedHeader the admin branch still
|
||||
refreshes the panel list (fire-and-forget) and the anonymous branch
|
||||
still REMOVES the panel from the DOM (phase-16 "absent, not hidden")
|
||||
so /api/steering is never fetched for anonymous visitors."""
|
||||
body = _init_body(_text(HEADER_JS))
|
||||
admin = body.find("if (admin)")
|
||||
refresh = body.find("if (steeringPanel) refreshSteering();")
|
||||
assert admin != -1 and refresh != -1, "the admin refresh is missing"
|
||||
assert admin < refresh, "the refresh must live in the admin branch"
|
||||
assert "steeringPanel?.remove();" in body, (
|
||||
"the anonymous remove-from-DOM path must stay intact"
|
||||
)
|
||||
|
||||
|
||||
# ---------- the surviving path to the notes ----------
|
||||
|
||||
|
||||
def test_nav_tuning_still_ships_hidden_on_all_six_pages() -> None:
|
||||
"""The admin-only Tuning NAV LINK (#nav-tuning) — the surviving path
|
||||
to the steering notes now that the navbar toggle is gone — still
|
||||
ships hidden on every page (the phase-19/29/35 contract)."""
|
||||
for html in PAGES:
|
||||
tag = re.search(r'<a[^>]*id="nav-tuning"[^>]*>', _text(html))
|
||||
assert tag, f"{html.name}: missing the #nav-tuning nav link"
|
||||
assert re.search(r"\bhidden\b", tag.group(0)), (
|
||||
f"{html.name}: #nav-tuning must ship hidden"
|
||||
)
|
||||
@@ -1,150 +0,0 @@
|
||||
"""Unit: the tuning toggle ships hidden — the anonymous flash fix (phase 40).
|
||||
|
||||
The "Tuning" steering toggle (``#steering-toggle``) used to ship VISIBLE
|
||||
in every page's shared header and was removed by ``assets/header.js``
|
||||
only after ``/api/whoami`` resolved — so an anonymous user briefly saw
|
||||
the button on every page load (``TODO.md`` L3). The fix mirrors the
|
||||
admin-only NAV LINKS (phase 19/29/35 contract): the toggle now SHIPS
|
||||
with the ``hidden`` attribute in all six pages and ``initSharedHeader``
|
||||
unhides it only when whoami says admin. The browser behavior is
|
||||
E2E-covered (``tests/e2e/test_tuning_toggle_flash.py``); here we pin
|
||||
the source-level wiring — the ship-hidden markup on every page, the
|
||||
admin unhide line inside ``initSharedHeader``, the intact anonymous
|
||||
remove-from-DOM path (phase 16 "absent, not hidden"), and the
|
||||
``#nav-tuning`` hidden contract this phase relies on — 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"
|
||||
|
||||
#: All six pages carry the shared header block (phase 34's five pages +
|
||||
#: phase 35's git-sources page).
|
||||
PAGES = (
|
||||
FRONTEND / "index.html",
|
||||
FRONTEND / "sources.html",
|
||||
FRONTEND / "document.html",
|
||||
FRONTEND / "git-sources.html",
|
||||
FRONTEND / "login.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 _toggle_tag(html: Path) -> str:
|
||||
tag = re.search(r"<button[^>]*id=\"steering-toggle\"[^>]*>", _text(html))
|
||||
assert tag, f"{html.name}: missing the #steering-toggle button"
|
||||
return tag.group(0)
|
||||
|
||||
|
||||
def _toggle_body(html: Path) -> str:
|
||||
"""The full <button>…</button> block, for the icon/label/badge pins."""
|
||||
text = _text(html)
|
||||
start = text.find('id="steering-toggle"')
|
||||
assert start != -1, f"{html.name}: missing the #steering-toggle button"
|
||||
return text[start : text.find("</button>", start)]
|
||||
|
||||
|
||||
def _init_body(js: str) -> str:
|
||||
"""The source of initSharedHeader in header.js."""
|
||||
fn = js.find("function initSharedHeader")
|
||||
assert fn != -1, "initSharedHeader must be defined"
|
||||
return js[fn : js.find("\n}", fn)]
|
||||
|
||||
|
||||
# ---------- ship-hidden markup: zero flash for anonymous ----------
|
||||
|
||||
|
||||
def test_steering_toggle_ships_hidden_on_all_six_pages() -> None:
|
||||
"""#steering-toggle carries the ``hidden`` attribute in ALL SIX
|
||||
pages — the exact ship-hidden contract the admin-only nav links
|
||||
use, so an anonymous user never sees the "Tuning" button for a
|
||||
single frame, on any page."""
|
||||
for html in PAGES:
|
||||
tag = _toggle_tag(html)
|
||||
assert re.search(r"\bhidden\b", tag), (
|
||||
f"{html.name}: #steering-toggle must ship hidden"
|
||||
)
|
||||
|
||||
|
||||
def test_steering_toggle_keeps_its_existing_markup() -> None:
|
||||
"""Only the ``hidden`` attribute was added: type, class, the
|
||||
aria-expanded / aria-controls wiring, the decorative icon, the
|
||||
"Tuning" label, and the #steering-count badge stay byte-identical
|
||||
on every page — the admin UX is unchanged."""
|
||||
for html in PAGES:
|
||||
tag = _toggle_tag(html)
|
||||
assert 'type="button"' in tag
|
||||
assert 'class="steering-toggle"' in tag
|
||||
assert 'aria-expanded="false"' in tag
|
||||
assert 'aria-controls="steering-panel"' in tag
|
||||
body = _toggle_body(html)
|
||||
assert '<svg aria-hidden="true"' in body, f"{html.name}: icon is gone"
|
||||
assert '<span class="steering-label">Tuning</span>' in body, (
|
||||
f"{html.name}: label markup changed"
|
||||
)
|
||||
assert '<span class="steering-count" id="steering-count">0</span>' in body, (
|
||||
f"{html.name}: count badge markup changed"
|
||||
)
|
||||
|
||||
|
||||
def test_steering_panel_still_ships_hidden_on_all_six_pages() -> None:
|
||||
"""The #steering-panel section already shipped hidden and stays
|
||||
that way (this phase never touches the panel)."""
|
||||
for html in PAGES:
|
||||
tag = re.search(r'<section[^>]*id="steering-panel"[^>]*>', _text(html))
|
||||
assert tag, f"{html.name}: missing the #steering-panel section"
|
||||
assert re.search(r"\bhidden\b", tag.group(0)), "the panel ships hidden"
|
||||
|
||||
|
||||
# ---------- header.js: reveal-for-admin, anonymous removal intact ----------
|
||||
|
||||
|
||||
def test_header_js_unhides_the_toggle_for_admin() -> None:
|
||||
"""Inside initSharedHeader, the admin branch unhides the toggle —
|
||||
and that line sits BEFORE the refreshSteering() call (the count
|
||||
badge is right before the panel is ever opened)."""
|
||||
body = _init_body(_text(HEADER_JS))
|
||||
assert "if (steeringToggle) steeringToggle.hidden = false;" in body, (
|
||||
"the admin unhide is missing from initSharedHeader"
|
||||
)
|
||||
admin = body.find("if (admin)")
|
||||
unhide = body.find("steeringToggle.hidden = false")
|
||||
refresh = body.find("if (steeringPanel) refreshSteering();")
|
||||
assert -1 < admin < unhide, "the unhide must live in the admin branch"
|
||||
assert unhide < refresh, "the unhide must precede the refreshSteering() call"
|
||||
|
||||
|
||||
def test_anonymous_removal_path_is_intact() -> None:
|
||||
"""The phase-16 "absent, not hidden" contract is preserved: the
|
||||
anonymous branch still REMOVES the toggle + panel from the DOM —
|
||||
the new hidden attribute only closes the pre-whoami flash window,
|
||||
the end state (absent) is unchanged."""
|
||||
body = _init_body(_text(HEADER_JS))
|
||||
assert "steeringToggle?.remove();" in body
|
||||
assert "steeringPanel?.remove();" in body
|
||||
|
||||
|
||||
# ---------- the nav contract this phase relies on ----------
|
||||
|
||||
|
||||
def test_nav_tuning_still_ships_hidden_on_all_six_pages() -> None:
|
||||
"""The admin-only Tuning NAV LINK (#nav-tuning) — the contract the
|
||||
toggle now mirrors — still ships hidden on every page: one
|
||||
ship-hidden / reveal-for-admin family, nav link and toggle alike."""
|
||||
for html in PAGES:
|
||||
tag = re.search(r'<a[^>]*id="nav-tuning"[^>]*>', _text(html))
|
||||
assert tag, f"{html.name}: missing the #nav-tuning nav link"
|
||||
assert re.search(r"\bhidden\b", tag.group(0)), (
|
||||
f"{html.name}: #nav-tuning must ship hidden"
|
||||
)
|
||||
Reference in New Issue
Block a user