"""Phase 177 E2E (Playwright): the tab icon carries the effective palette — the route, the pre-paint tag, the live preview, the restore contract. Source: TODO.md L3 — "We need the favicon to change colors to match the theme." (owner roadmap confirmation 2026-10-13, A1–A4.) Run in isolation (DB must be up: ``podman compose up -d db``): uv run pytest tests/e2e/test_themed_favicon.py -v --no-cov Test → contract mapping (one story, one phase, one isolated file): 1. ``test_default_state_serves_the_golden_route`` — the default (no ``ui_settings`` row) deployment: ``GET /favicon.svg`` is 200 ``image/svg+xml`` + ``no-cache`` with the body byte-identical to the golden art (``FAVICON_GOLDEN`` from ``tests.unit.test_theming`` — ONE source); the five pages' served HTML reference the route (``href="/favicon.svg"`` — bare, never ``/assets/favicon.svg``); the deleted static file is 404. 2. ``test_themed_state_serves_the_effective_palette`` — with a saved palette (the 3 driving colors distinct): the route's SVG carries the three hexes in their EXACT roles (attribute-parsed, never substring) and is byte-equal to the server renderer's output; a fresh page load STILL injects the ``#bor-theme`` tag immediately before ```` with the full 17-var content + the phase-91 CSP hash (the tag/CSP contract is untouched by the favicon work), and the browser's first paint is the themed paint (the computed ``--surface`` / ``--brand``). 3. ``test_live_preview_repaints_the_favicon`` — an admin's live (unsaved) pick of the three driving fields sets ``link[rel="icon"]`` to a ``data:image/svg+xml`` URI that, decoded, carries the three picked hexes in their exact roles (attribute-parsed). Headless Chromium never fetches favicons (probe-verified — zero request events, not even for the initial ``/favicon.svg``), so the suite pins the href state + the decoded art as the repaint contract and guards the CSP path NEGATIVELY: no CSP violation is ever reported against a ``data:`` URI (the phase-123 ``img-src 'self' data:`` carve-out makes a ``data:`` favicon legal — CSP3 governs ``link[rel=icon]`` by ``img-src``; a blocked fetch would surface as a ``requestfailed`` with a CSP error or a console violation, which the suite asserts does not happen). A non-driving pick (``#theme-bg``) never touches the href. 4. ``test_save_restores_the_route_url`` — Save: the href is EXACTLY ``/favicon.svg`` and the route now serves the saved palette's bytes (the settled palette governs). 5. ``test_reset_restores_the_route_url`` — after a Save, Reset: the href is ``/favicon.svg`` and the route serves the built-in golden bytes. 6. ``test_view_refresh_restores_the_route_url`` — a user-initiated re-show (the router's ``bor:view-refresh`` dispatch — the phase-77 hook) restores the href after a live pick. Zero external requests across the whole suite (AGENTS rule 6 — the house E2E assertion idiom): a ``page.on("request")`` collector asserts every http(s) URL is the local origin (a ``data:`` URI is exempt by design — the phase-123 carve-out). DB isolation: the shared e2e Postgres keeps ``ui_settings`` (the single row the caching middleware + the favicon route read for EVERY request — a leftover themed row would repaint other suites' pages and icons) and ``api_tokens`` rows across suites. An autouse fixture truncates ``ui_settings`` and deletes the ``e2e-``-labeled tokens before AND after every test (never a TRUNCATE on ``api_tokens`` — the shared DB may hold the owner's real tokens). Per-module app env (the tuning/tokens/archive-upload pattern): the module-scoped ``app_server`` override boots the same env block as the shared conftest server with the branding vars pinned to the CODE defaults (an operator's local ``.env`` may carry the owner's name/placeholder/footer, and "the effective strings start at the template defaults" must hold regardless — the phase-61/62 leak-guard pattern, extended to ``BOR_APP_NAME``) and ``BOR_GIT_SOURCES`` forced empty (the dev ``.env``'s git repo must not render as env rows in this suite's app). The phase-92 file (test_theme_save_and_coverage.py) is the copy source — the two modules' scaffolds stay in lockstep so a future conftest refactor touches both at once. """ from __future__ import annotations import json import os import subprocess import sys import xml.etree.ElementTree as ET from collections.abc import Iterator from urllib.parse import unquote import httpx import pytest from playwright.sync_api import Page, expect from sqlalchemy import text from app.config import Settings from app.core import theming from app.db import SessionLocal from e2e.auth_helpers import login from e2e.conftest import ( ADMIN_PASSWORD, APP_PORT, REPO, SESSION_SECRET, USE_REAL_LLM, _wait_http, ) from tests.unit.test_theming import FAVICON_GOLDEN APP_URL = f"http://127.0.0.1:{APP_PORT}" # The three driving fields (A1 — the artwork's color roles: hexagon # fill, hexagon stroke + center dot, 4 spokes) with E2E values # distinct from every built-in. DRIVING: dict[str, str] = { "surface": "#102030", "brand": "#4f46e5", "brand_ink": "#a5b4fc", } #: The E2E-stable color-input ids (the form's own markup — the #: static E2E-stable-selectors house convention). INPUT_IDS = { field: f"#theme-{field.replace('_', '-')}" for field in theming.COLOR_FIELDS } #: The five templates whose served HTML must reference the route. PAGES = ("/", "/document.html", "/login.html", "/shared.html", "/doc-edit.html") #: The data: URI prefix the live preview sets (the phase-123 #: img-src 'self' data: carve-out makes it CSP-legal). DATA_PREFIX = "data:image/svg+xml," # --------------------------------------------------------------------------- # In-test constants (single sources of truth — never duplicated) # --------------------------------------------------------------------------- def _expected_tag(colors: dict[str, str]) -> str: """The EXACT inline tag ``theme_style_tag`` renders for ``colors``: one ``:root`` override, all 17 vars in COLOR_FIELDS order, no whitespace (the byte the middleware injects).""" declarations = "".join( f"--{k.replace('_', '-')}:{colors[k]};" for k in theming.COLOR_FIELDS ) return f'' def _expected_favicon_bytes(colors: dict[str, str]) -> bytes: """The route's body for a palette: the SERVER renderer's output (imported — ONE source, never a test-side mirror of the art).""" return theming.favicon_svg(colors).encode("utf-8") def _attr(element: ET.Element, name: str) -> str: """An SVG attribute, asserted non-null (a missing role attribute is a test failure, not a parseable value).""" value = element.get(name) assert value is not None, f"missing the {name} attribute" return value def _parse_roles(svg: str) -> dict[str, str]: """The three color roles, ATTRIBUTE-parsed (the unit pin's idiom — never substring): the hexagon's fill (surface), the hexagon's stroke (brand), the center circle's fill (brand), the spokes' stroke (brand_ink).""" ns = "{http://www.w3.org/2000/svg}" root = ET.fromstring(svg) assert root.tag == f"{ns}svg", root.tag hexagon, circle, spokes = list(root) return { "surface": _attr(hexagon, "fill"), "brand": _attr(hexagon, "stroke"), "brand_circle": _attr(circle, "fill"), "brand_ink": _attr(spokes, "stroke"), } def _login_to_theme(page: Page, app_url: str) -> None: """Log in straight into the Theme view AND wait for its mount-time ``GET /api/ui-settings`` to ANSWER — the settle signal. Phase 190 (task 02) moved it with the tab's string fieldset: the settle used to be read off the (now removed) ``#theme-app-name`` input, the only input the static markup shipped empty. The network is the source of truth now.""" with page.expect_response( lambda r: "/api/ui-settings" in r.url, timeout=30_000 ) as info: login(page, app_url, next="/theme.html") assert info.value.status == 200, "the mount's GET /api/ui-settings failed" expect(page.locator("#theme-content")).to_be_visible(timeout=15_000) def _favicon_href(page: Page) -> str: """The tab icon's current href (the attribute's raw state).""" return page.evaluate( "() => document.querySelector('link[rel=icon]').getAttribute('href')" ) def _wait_data_href(page: Page) -> None: """The href is a ``data:`` URI (the live preview has landed).""" page.wait_for_function( f"() => (document.querySelector('link[rel=icon]')" f".getAttribute('href') || '').startsWith('{DATA_PREFIX}')", timeout=15_000, ) def _wait_route_href(page: Page) -> None: """The href is EXACTLY the route URL (the restore has landed — the settled palette governs).""" page.wait_for_function( "() => (document.querySelector('link[rel=icon]')" '.getAttribute(\'href\') || \'\') === \'/favicon.svg\'', timeout=15_000, ) def _seed_theme_via_api(app_url: str, cookies: dict[str, str]) -> None: """Admin ``PUT /api/ui-settings`` with the 3 driving colors (the rest null → the built-ins — the route's bytes need only the three colors in the artwork).""" body: dict[str, object] = {} for field in theming.STRING_FIELDS: body[field] = None for field in theming.COLOR_FIELDS: body[field] = None body.update(DRIVING) r = httpx.put(f"{app_url}/api/ui-settings", json=body, cookies=cookies, timeout=10) assert r.status_code == 200, r.text # --------------------------------------------------------------------------- # Per-module app env (the tuning/tokens/archive-upload pattern) # --------------------------------------------------------------------------- @pytest.fixture(scope="module") def app_server(mock_llm: int) -> Iterator[str]: """The real app under test — per-module env: the branding vars are pinned to the CODE defaults (the effective strings start at the template defaults regardless of an operator's local ``.env`` — the phase-61/62 leak-guard pattern the shared conftest server applies to its two string vars; this one pins all three, including ``BOR_APP_NAME``, which the shared server leaves to the process) and ``BOR_GIT_SOURCES`` is forced empty (the dev ``.env``'s git repo must not render as env rows in this suite's app).""" env = dict(os.environ) env.pop("DEBUGPY", None) env["BOR_ENVIRONMENT"] = "e2e" env["BOR_STATIC_DIR"] = str(REPO / "frontend") env["BOR_LLM_BASE_URL"] = ( "https://aipi.reeseapps.com/v1" if USE_REAL_LLM else f"http://127.0.0.1:{mock_llm}/v1" ) # Mock-calibrated threshold (conftest pattern) — no chat turn is # ever sent in this suite, but the app boots with the same shape. env["BOR_RELEVANCE_THRESHOLD"] = "0.30" env["BOR_LLM_RETRY_DELAY"] = "0" env["BOR_LLM_RETRIES"] = str(Settings.model_fields["llm_retries"].default) env.setdefault( "BOR_DATABASE_URL", "postgresql+psycopg://reese:reese@localhost:5432/brain_of_reese", ) # Phase 16: admin auth must be set or create_app() refuses to boot. env["BOR_ADMIN_PASSWORD"] = ADMIN_PASSWORD env["BOR_SESSION_SECRET"] = SESSION_SECRET env["BOR_DOCS_REPO"] = "" env["BOR_SUGGESTIONS"] = json.dumps( Settings.model_fields["suggestions"].default ) # The branding vars: "unset" = the template defaults (the code # defaults, derived from the class fields — the local ``.env`` # may carry the owner's values, and this suite's assertions need # the TEMPLATE defaults, not the owner's). env["BOR_APP_NAME"] = Settings.model_fields["app_name"].default env["BOR_INPUT_PLACEHOLDER"] = ( Settings.model_fields["input_placeholder"].default ) env["BOR_FOOTER_TEXT"] = Settings.model_fields["footer_text"].default env["BOR_GIT_SOURCES"] = "" proc = subprocess.Popen( [sys.executable, "-m", "uvicorn", "app.main:app", "--host", "127.0.0.1", "--port", str(APP_PORT), "--log-level", "warning"], cwd=REPO, env=env, ) try: _wait_http(f"{APP_URL}/api/health") yield APP_URL finally: proc.terminate() try: proc.wait(timeout=10) except subprocess.TimeoutExpired: proc.kill() @pytest.fixture(scope="module") def app_url(app_server: str) -> str: return app_server # --------------------------------------------------------------------------- # DB isolation + helpers # --------------------------------------------------------------------------- def _clean_ui_state() -> None: """Fresh theme + token state per test: truncate the single-row ``ui_settings`` (the middleware + the favicon route read it for EVERY request — a leftover themed row would repaint other suites' pages and icons) and delete this suite's issued tokens (label-scoped on ``e2e-`` — never a TRUNCATE: the shared DB may hold the owner's real tokens).""" with SessionLocal() as db: db.execute(text("TRUNCATE ui_settings")) db.execute(text("DELETE FROM api_tokens WHERE label LIKE 'e2e-%'")) db.commit() @pytest.fixture(autouse=True) def _clean(db_ready: None) -> Iterator[None]: _clean_ui_state() yield _clean_ui_state() def _cookies(page: Page) -> dict[str, str]: """The session cookies the browser context holds (the test's API side sees exactly what that browser sees).""" return { c["name"]: c["value"] for c in page.context.cookies() if "name" in c and "value" in c } def _zero_external( page: Page, ) -> tuple[list[str], list[str], list[str], list[str]]: """AGENTS rule 6 (the house E2E assertion idiom): every http(s) request is the local origin; ``data:`` URIs are exempt by design (the phase-123 ``img-src 'self' data:`` carve-out). Also collects the CSP-failure signals: a failed ``data:`` fetch (a blocked fetch carries a CSP error in its failure message) and console CSP violations ("Refused to …" / "CSP" against a ``data:`` URI). Returns (externals, data_requests, failed_data, csp_violations). Headless Chromium never fetches favicons (probe-verified), so ``data_requests`` is expected to stay empty — the negative assertions (``failed_data`` + ``csp_violations``) are the CSP guard: a future change that blocks the ``data:`` URI surfaces here, and a real (non-headless) browser run additionally sees the fetch in ``data_requests``.""" externals: list[str] = [] data_requests: list[str] = [] failed_data: list[str] = [] csp_violations: list[str] = [] def _watch(request: object) -> None: url = getattr(request, "url", "") if url.startswith("data:"): data_requests.append(url) return if url.startswith(("http://", "https://")) and "127.0.0.1" not in url: externals.append(url) def _watch_failed(request: object) -> None: url = getattr(request, "url", "") failure = getattr(request, "failure", None) if url.startswith("data:") and failure: failed_data.append(str(failure)) def _watch_console(message: object) -> None: msg = getattr(message, "text", "") if ("CSP" in msg or "Refused to" in msg) and "data:" in msg: csp_violations.append(msg) page.on("request", _watch) page.on("requestfailed", _watch_failed) page.on("console", _watch_console) return externals, data_requests, failed_data, csp_violations def _assert_zero_external( externals: list[str], failed_data: list[str], csp_violations: list[str] ) -> None: """The suite's tail assertion: zero external requests, no ``data:`` URI CSP-blocked (a blocked fetch carries a CSP error in its failure message), and no CSP violation reported against a ``data:`` URI (the phase-123 carve-out holds).""" assert not externals, f"external requests: {externals}" assert not failed_data, f"data: URIs blocked (CSP?): {failed_data}" assert not csp_violations, f"CSP violations: {csp_violations}" # --------------------------------------------------------------------------- # 1. Default state: the golden route, the five page references, the # deleted static file # --------------------------------------------------------------------------- def test_default_state_serves_the_golden_route(app_url: str, db_ready: None) -> None: """The default (no ``ui_settings`` row) deployment: the route is byte-identical to the golden art (imported — ONE source), with the svg media type + ``no-cache``; the five pages' served HTML reference the route (bare — no ``?v=``, never ``/assets/favicon.svg``); the deleted static file is 404.""" r = httpx.get(f"{app_url}/favicon.svg", timeout=10) assert r.status_code == 200 assert r.headers["content-type"].startswith("image/svg+xml") assert r.headers["cache-control"] == "no-cache" assert r.content == FAVICON_GOLDEN.encode("utf-8") for path in PAGES: page = httpx.get(app_url + path, timeout=10) assert page.status_code == 200, path assert ( '' in page.text ), f"{path} must reference the themed route" assert "/assets/favicon.svg" not in page.text, ( f"{path} must never reference the deleted static file" ) assert "favicon.svg?v=" not in page.text, ( f"{path} must keep the favicon link bare (no ?v=)" ) gone = httpx.get(app_url + "/assets/favicon.svg", timeout=10) assert gone.status_code == 404 # --------------------------------------------------------------------------- # 2. Themed state: the route carries the effective palette; the # pre-paint tag + the first paint are untouched (phase 91) # --------------------------------------------------------------------------- def test_themed_state_serves_the_effective_palette( page: Page, app_url: str, db_ready: None ) -> None: """With a saved palette (the 3 driving colors distinct): the route's SVG carries the three hexes in their EXACT roles (attribute-parsed) and is byte-equal to the server renderer; a fresh page load STILL injects the ``#bor-theme`` tag (17-var content, immediately before ```` + the phase-91 CSP hash — the tag/CSP contract is untouched by the favicon work) and the browser's first paint is the themed paint.""" page.set_default_timeout(30_000) login(page, app_url) _seed_theme_via_api(app_url, _cookies(page)) # The route: the three hexes in their exact roles… r = httpx.get(f"{app_url}/favicon.svg", timeout=10) assert r.status_code == 200 roles = _parse_roles(r.text) assert roles["surface"] == DRIVING["surface"] assert roles["brand"] == DRIVING["brand"] assert roles["brand_circle"] == DRIVING["brand"] assert roles["brand_ink"] == DRIVING["brand_ink"] # …and byte-equal to the server renderer's output (ONE source). effective = {**theming.BUILTIN_COLORS, **DRIVING} assert r.content == _expected_favicon_bytes(effective) # The pre-paint tag non-regression (phase 91): the RAW served # HTML carries the 17-var tag immediately before ````… tag = _expected_tag(effective) r = httpx.get(app_url + "/", timeout=10) assert r.status_code == 200 assert r.text.count(tag) == 1, "the 17-var tag must be injected exactly once" start = r.text.index(tag) assert start + len(tag) == r.text.index(""), ( "the tag must end exactly where begins" ) # …with the phase-91 CSP hash (the favicon work never touched # the tag/CSP contract). assert "style-src 'self' 'sha256-" in r.headers.get( "content-security-policy", "" ), "the themed page must carry the phase-91 CSP hash" # A fresh page load: the first paint IS the themed paint. page.goto(app_url + "/") page.wait_for_function( """(d) => { const cs = getComputedStyle(document.documentElement); return cs.getPropertyValue('--surface').trim() === d.surface && cs.getPropertyValue('--brand').trim() === d.brand; }""", arg=DRIVING, timeout=15_000, ) # --------------------------------------------------------------------------- # 3. Live preview: an unsaved pick repaints the favicon via the # CSP-legal data: URI; a non-driving pick never touches it # --------------------------------------------------------------------------- def test_live_preview_repaints_the_favicon( page: Page, app_url: str, db_ready: None ) -> None: """An admin's live (unsaved) pick of the three driving fields sets ``link[rel="icon"]`` to a ``data:`` URI that, decoded, carries the three picked hexes in their exact roles; the browser FETCHES the ``data:`` URI (the phase-123 carve-out — a blocked fetch would be a ``requestfailed`` with a CSP error); a non-driving pick (``#theme-bg``) never touches the href.""" page.set_default_timeout(30_000) externals, _data_requests, failed_data, csp_violations = _zero_external(page) _login_to_theme(page, app_url) # A clean start: the href is the route (no row → the golden # art served by the route). assert _favicon_href(page) == "/favicon.svg" # Set the three driving fields (page.fill fires the input event # — the house _fill_theme_form idiom). for field in ("surface", "brand", "brand_ink"): page.fill(INPUT_IDS[field], DRIVING[field]) _wait_data_href(page) # The href is a data: URI, and — decoded — carries the three # picked hexes in their exact roles (attribute-parsed). href = _favicon_href(page) assert href.startswith(DATA_PREFIX) svg = unquote(href[len(DATA_PREFIX):]) roles = _parse_roles(svg) assert roles["surface"] == DRIVING["surface"] assert roles["brand"] == DRIVING["brand"] assert roles["brand_circle"] == DRIVING["brand"] assert roles["brand_ink"] == DRIVING["brand_ink"] # A non-driving pick never touches the href. page.fill(INPUT_IDS["bg"], "#000000") assert _favicon_href(page) == href, "a non-driving pick must not touch the href" # The CSP guard (negative — headless never fetches favicons): # no failed data: fetch, no CSP violation against the URI. _assert_zero_external(externals, failed_data, csp_violations) # --------------------------------------------------------------------------- # 4. Restore: Save — the href is EXACTLY /favicon.svg and the route # serves the saved palette's bytes (the settled palette governs) # --------------------------------------------------------------------------- def test_save_restores_the_route_url( page: Page, app_url: str, db_ready: None ) -> None: """After a live pick, Save: the §7.4 lifecycle lands and the href is EXACTLY ``/favicon.svg``; the route now serves the saved palette's bytes (the settled palette governs — the route re-reads the DB per request, no process cache).""" page.set_default_timeout(30_000) externals, _d, failed_data, csp = _zero_external(page) _login_to_theme(page, app_url) # A live pick first — so the save has a data: href to restore. for field in ("surface", "brand", "brand_ink"): page.fill(INPUT_IDS[field], DRIVING[field]) _wait_data_href(page) # Save through the real form. page.click("#theme-save") expect(page.locator("#theme-result")).to_have_text( "Theme saved.", timeout=30_000 ) expect(page.locator("#theme-result")).to_have_attribute("role", "status") # The restore: the href is EXACTLY the route URL. _wait_route_href(page) assert _favicon_href(page) == "/favicon.svg" # The settled palette governs: the route serves the saved # palette's bytes (the PUT saved the 17 form colors — the 3 # driving colors + the 14 built-ins (→ NULL); the brand strings are # the Settings panel's since phase 190 (A1) and this body carried # none, so the effective palette is the 3 + the built-ins). effective = {**theming.BUILTIN_COLORS, **DRIVING} r = httpx.get(f"{app_url}/favicon.svg", timeout=10) assert r.status_code == 200 assert r.content == _expected_favicon_bytes(effective) _assert_zero_external(externals, failed_data, csp) # --------------------------------------------------------------------------- # 5. Restore: Reset — the href is /favicon.svg and the route serves # the built-in golden bytes # --------------------------------------------------------------------------- def test_reset_restores_the_route_url( page: Page, app_url: str, db_ready: None ) -> None: """After a Save (the route is themed), a fresh live pick, then Reset: the §7.4 lifecycle lands and the href is ``/favicon.svg``; the route serves the built-in golden bytes (the reset nulled the row — the built-ins govern).""" page.set_default_timeout(30_000) externals, _d, failed_data, csp = _zero_external(page) _login_to_theme(page, app_url) # Save first — the route is themed (the reset then has a real # state to settle back from). for field in ("surface", "brand", "brand_ink"): page.fill(INPUT_IDS[field], DRIVING[field]) _wait_data_href(page) page.click("#theme-save") expect(page.locator("#theme-result")).to_have_text( "Theme saved.", timeout=30_000 ) _wait_route_href(page) r = httpx.get(f"{app_url}/favicon.svg", timeout=10) assert r.content == _expected_favicon_bytes( {**theming.BUILTIN_COLORS, **DRIVING} ) # A fresh live pick — the href is the data: one the reset # restores (a distinct value, so the repaint is guaranteed). page.fill(INPUT_IDS["surface"], "#0a1525") _wait_data_href(page) # Reset to defaults: the §7.4 lifecycle (the all-null PUT — the # API's documented "defaults" operation). page.click("#theme-reset") expect(page.locator("#theme-result")).to_have_text( "Reset to the built-in theme.", timeout=30_000 ) expect(page.locator("#theme-result")).to_have_attribute("role", "status") # The restore: the href is EXACTLY the route URL… _wait_route_href(page) assert _favicon_href(page) == "/favicon.svg" # …and the route serves the built-in golden bytes. r = httpx.get(f"{app_url}/favicon.svg", timeout=10) assert r.status_code == 200 assert r.content == FAVICON_GOLDEN.encode("utf-8") _assert_zero_external(externals, failed_data, csp) # --------------------------------------------------------------------------- # 6. Restore: the bor:view-refresh re-show (the phase-77 hook) # --------------------------------------------------------------------------- def test_view_refresh_restores_the_route_url( page: Page, app_url: str, db_ready: None ) -> None: """After a live pick, a user-initiated re-show (leave the Theme view, come back — the router dispatches ``bor:view-refresh`` on the already-mounted section): the handler re-runs the load, reconciles, and the href is restored to ``/favicon.svg`` (the settled palette governs — in this test's row-less state, the golden bytes).""" page.set_default_timeout(30_000) externals, _d, failed_data, csp = _zero_external(page) _login_to_theme(page, app_url) # A live pick — the href is the data: one the re-show restores. for field in ("surface", "brand", "brand_ink"): page.fill(INPUT_IDS[field], DRIVING[field]) _wait_data_href(page) # A user-initiated re-show: away (the RAG view — a real view # switch), then back — the router's phase-77 hook dispatches # bor:view-refresh on the mounted Theme section. page.click("#nav-sources") expect(page.locator("#view-rag")).to_be_visible(timeout=15_000) page.click("#nav-theme") expect(page.locator("#view-theme")).to_be_visible(timeout=15_000) # The restore: the href is EXACTLY the route URL. _wait_route_href(page) assert _favicon_href(page) == "/favicon.svg" # The settled palette governs: no row was saved, so the route # still serves the golden bytes. r = httpx.get(f"{app_url}/favicon.svg", timeout=10) assert r.status_code == 200 assert r.content == FAVICON_GOLDEN.encode("utf-8") _assert_zero_external(externals, failed_data, csp)