"""Tests for the homepage: the cache-only "unusual right now" precompute, the server-rendered surfaces, and the digest signup. The precompute must never touch the network — a sweep over ~1000 cities that fetched would burst the archive quota — so the tests here assert that the fetching climate helpers are not called at all. """ import datetime import json import pathlib import time import pytest from fastapi.testclient import TestClient import app as appmod import cities import climate import content import homepage B = "/thermograph" @pytest.fixture def client(): return TestClient(appmod.app) @pytest.fixture def no_feed(monkeypatch): """The homepage with no precomputed feed at all.""" monkeypatch.setattr(homepage, "load", lambda: None) return None def _pick(name, pct, cls, grade, tail, slug=None): return { "slug": slug or (name.lower().replace(" ", "-") + "-xx"), "name": name, "display": f"{name}, Somewhere", "lat": 1.0, "lon": 2.0, "cell_id": "1_2", "date": datetime.date.today().isoformat(), "metric": "tmax", "metric_label": "High temp", "window_label": "mid-July", "value": 99.0, "percentile": pct, "grade": grade, "cls": cls, "departure": abs(pct - 50), "tail": tail, } # --- the precompute ---------------------------------------------------------- def test_build_never_fetches_upstream(monkeypatch): """The sweep uses the cache-only readers, never the fetching ones.""" def boom(*a, **k): raise AssertionError("the homepage sweep must not fetch upstream") monkeypatch.setattr(climate, "get_history", boom) monkeypatch.setattr(climate, "get_recent_forecast", boom) feed = homepage.build(limit=20) assert set(feed) >= {"generated_at", "date", "considered", "picks", "ranked"} def test_build_grades_cached_cities(monkeypatch, history, recent): monkeypatch.setattr(climate, "load_cached_history", lambda cell: history.clone()) monkeypatch.setattr(climate, "load_cached_recent_forecast", lambda cell: recent.clone()) feed = homepage.build(limit=5) assert feed["considered"] == 5 for row in feed["ranked"]: assert row["percentile"] is not None assert row["cls"] and not row["cls"].startswith("--") # token name, no prefix assert row["tail"] in ("warm", "cold") def test_missing_cell_cache_is_skipped_not_fatal(monkeypatch): monkeypatch.setattr(climate, "load_cached_history", lambda cell: None) monkeypatch.setattr(climate, "load_cached_recent_forecast", lambda cell: None) feed = homepage.build(limit=10) assert feed["considered"] == 0 and feed["ranked"] == [] def test_load_survives_missing_and_corrupt_file(monkeypatch, tmp_path): monkeypatch.setattr(homepage, "FEED_PATH", str(tmp_path / "nope.json")) assert homepage.load() is None bad = tmp_path / "homepage.json" bad.write_text("{not json") monkeypatch.setattr(homepage, "FEED_PATH", str(bad)) assert homepage.load() is None bad.write_text('{"unexpected": true}') assert homepage.load() is None def test_refresh_writes_atomically(monkeypatch, tmp_path): monkeypatch.setattr(homepage, "FEED_PATH", str(tmp_path / "homepage.json")) monkeypatch.setattr(climate, "load_cached_history", lambda cell: None) homepage.refresh(limit=3) written = json.loads((tmp_path / "homepage.json").read_text()) assert written["ranked"] == [] # No temp files left behind. assert [p.name for p in tmp_path.iterdir()] == ["homepage.json"] def test_staleness(monkeypatch): today = datetime.date.today().isoformat() assert not homepage.is_stale({"date": today, "generated_at": time.time()}) assert homepage.is_stale({"date": today, "generated_at": time.time() - 7 * 3600}) assert homepage.is_stale({"date": "1999-01-01", "generated_at": time.time()}) # --- the rendered page ------------------------------------------------------- def test_homepage_is_complete_without_js(client, no_feed): """A crawler (or a reader with JS off) gets the whole page as real HTML.""" r = client.get(f"{B}/") assert r.status_code == 200 html = r.text assert "How unusual is your weather?" in html assert "How it works" in html assert "Climate context for 1,000 cities" in html assert "Free. No ads. No tracking." in html # The interactive tool's DOM contract survives the move to Jinja. for dom_id in ('id="find-btn"', 'id="date-input"', 'id="panel"', 'id="placeholder"', 'id="results"'): assert dom_id in html # Exactly one h1: the hero headline. The brand degrades to a

. assert html.count(" so the hero owns the page's only h1. The lockup styling is written against `.brand h1`, so the class MUST be matched there too — otherwise the brand silently falls back to block layout, which baseline-aligns the mark instead of centring it and drops the monospace wordmark. Nothing else catches this: the page still renders, and every other test still passes, while the header looks wrong. """ html = client.get(f"{B}/").text assert 'class="site-name"' in html assert "