"""SPA-shell pages (/calendar, /day, /score, /compare, /legend, /alerts) and static-asset serving -- moved here from backend/tests/web/test_api.py (repo-split Stage 7a: frontend now owns these, not backend). These routes are static shells/assets that don't call the backend, so the hermetic `client` fixture serves them.""" B = "/thermograph" # matches THERMOGRAPH_BASE set in tests/conftest.py def test_calendar_serves_with_origin_filled_in(client): r = client.get(f"{B}/calendar") assert r.status_code == 200 assert "text/html" in r.headers["content-type"] assert "__ORIGIN__" not in r.text assert client.head(f"{B}/calendar").status_code == 200 def test_other_spa_shells_serve(client): for path in ("/day", "/score", "/compare", "/legend", "/alerts"): r = client.get(f"{B}{path}") assert r.status_code == 200, path assert "__ORIGIN__" not in r.text, path def test_static_asset_serves(client): r = client.get(f"{B}/app.js") assert r.status_code == 200 assert "javascript" in r.headers["content-type"] def test_old_static_index_is_gone(client): """index.html must not linger behind the static mount as indexable duplicate content now that / is server-rendered (moved here from backend/tests/web/test_homepage.py, repo-split Stage 7a -- frontend's own StaticFiles mount is what actually answers this now).""" assert client.get(f"{B}/index.html").status_code == 404 # --- product-event instrumentation flag --------------------------------------- # track.js sends nothing unless it finds data-tg-events on . The whole # rollout story rests on that stamp being absent by default, so assert both # halves: off unless THERMOGRAPH_EVENTS is set, and applied to BOTH HTML paths # (the Jinja-rendered content pages and the static SPA shells). def test_event_flag_is_off_by_default_on_every_html_path(client): for path in ("/", "/calendar", "/day", "/score", "/compare", "/alerts"): r = client.get(f"{B}{path}") assert r.status_code == 200, path assert "data-tg-events" not in r.text, path def test_event_flag_stamps_both_html_paths_when_enabled(monkeypatch): """Rebuilt in-process rather than reusing `client`: the flag is read at import and the SPA shells memoize their prepped template.""" import importlib import content as content_mod from fastapi.testclient import TestClient monkeypatch.setenv("THERMOGRAPH_EVENTS", "1") content_mod = importlib.reload(content_mod) assert content_mod.EVENTS_ENABLED import app as app_mod app_mod = importlib.reload(app_mod) flagged = TestClient(app_mod.app) for path in ("/calendar", "/compare"): assert 'data-tg-events="1"' in flagged.get(f"{B}{path}").text, path # Leave the modules as the rest of the suite expects to find them. monkeypatch.delenv("THERMOGRAPH_EVENTS") importlib.reload(content_mod) importlib.reload(app_mod) def test_consent_control_is_present_on_every_page(client): """Withdrawal must be reachable from anywhere, so the footer carries the control on every server-rendered page. consent.js leaves it empty when instrumentation is disabled, so it costs nothing when off.""" for path in ("/", "/privacy", "/climate"): r = client.get(f"{B}{path}") assert r.status_code == 200, path assert "data-consent-control" in r.text, path def test_consent_script_loads_before_the_tracker(client): """consent.js owns the banner and the opt-in gate; track.js imports it. It is listed first so the gate is installed even on a page whose tracker chain never loads.""" for path, after in (("/", "digest.js"), ("/calendar", "track.js")): body = client.get(f"{B}{path}").text tag = lambda name: body.index(f'src="{B}/{name}"' if path == "/" else f'src="{name}"') assert tag("consent.js") < tag(after), path def test_privacy_page_describes_what_is_actually_collected(client): """The page is a public commitment; if this test has to change, the copy and the code have drifted apart. Asserts the load-bearing promises only.""" body = client.get(f"{B}/privacy").text for promise in ( "session storage", # where the id lives "30 minutes", # idle rotation "2 hours", # absolute cap "never linked to your account", "30 days", # session-row retention "400 days", # anonymous aggregate retention "90 days", # failed-search retention "Global Privacy Control", "only if you say yes", # opt-in, not opt-out ): assert promise in body, promise # The v1 claim that this change makes untrue must be gone. assert "no per-visitor identifier" not in body def test_footer_no_longer_claims_no_tracking(client): """A consented session id is an identifier; the blanket claim would be a lie.""" body = client.get(f"{B}/").text assert "No tracking." not in body assert "No third-party trackers." in body