The month and season records tables showed only the record high (of the daytime high) and the record low (of the overnight low) — one extreme each. Show both ends of both metrics: for the daytime high, its record hottest (▲) and the coldest a day ever stayed (▼, its record-low high); for the overnight low, its mildest night (▲, its record-high low) and its record low (▼). All four already came out of all_time_records; they were just being dropped. Restructure each table to Period · Daytime high · Overnight low, with each metric cell carrying two tinted, dated chips (▲ warmest / ▼ coldest). Fits mobile in three columns; the all-time cards already showed both extremes, so this brings the per-period tables in line.
157 lines
6.4 KiB
Python
157 lines
6.4 KiB
Python
"""Tests for the crawlable SEO content pages: the city set, robots/sitemap, and
|
||
that the server-rendered pages contain the stats in the HTML (with the weather
|
||
layer faked, like test_api.py)."""
|
||
import pytest
|
||
from fastapi.testclient import TestClient
|
||
|
||
import app as appmod
|
||
import cities
|
||
import climate
|
||
|
||
# BASE defaults to /thermograph in tests (THERMOGRAPH_BASE unset).
|
||
B = "/thermograph"
|
||
SLUG = "london-england-gb" # present in cities.json
|
||
|
||
|
||
@pytest.fixture
|
||
def client(monkeypatch, history, recent):
|
||
monkeypatch.setattr(climate, "load_cached_history", lambda cell: history.clone())
|
||
monkeypatch.setattr(climate, "get_recent_forecast", lambda cell: recent.clone())
|
||
monkeypatch.setattr(climate, "get_history", lambda cell: (history.clone(), {"cached": True}))
|
||
return TestClient(appmod.app)
|
||
|
||
|
||
def test_city_set_slugs_unique_and_lookup():
|
||
slugs = cities.all_slugs()
|
||
assert len(slugs) == len(set(slugs)) >= 100
|
||
assert cities.get(SLUG) is not None
|
||
assert cities.get("does-not-exist") is None
|
||
|
||
|
||
def test_robots_txt(client):
|
||
r = client.get(f"{B}/robots.txt")
|
||
assert r.status_code == 200
|
||
assert "Sitemap:" in r.text and "/sitemap.xml" in r.text
|
||
assert "Disallow: /thermograph/api/" in r.text
|
||
|
||
|
||
def test_sitemap_lists_city_urls(client):
|
||
r = client.get(f"{B}/sitemap.xml")
|
||
assert r.status_code == 200
|
||
assert "<urlset" in r.text
|
||
assert f"/climate/{SLUG}</loc>" in r.text
|
||
assert f"/climate/{SLUG}/july</loc>" in r.text
|
||
assert f"/climate/{SLUG}/records</loc>" in r.text
|
||
|
||
|
||
def test_city_page_renders_stats_in_html(client):
|
||
r = client.get(f"{B}/climate/{SLUG}")
|
||
assert r.status_code == 200
|
||
b = r.text
|
||
assert "London" in b and "climate" in b.lower()
|
||
assert 'rel="canonical"' in b and f"/climate/{SLUG}" in b
|
||
assert '"@type":"Dataset"' in b
|
||
assert "average temperatures by month" in b.lower()
|
||
# a real number rendered (°F appears in the normals/today text)
|
||
assert "°F" in b
|
||
|
||
|
||
def test_city_404(client):
|
||
assert client.get(f"{B}/climate/nope-not-a-city").status_code == 404
|
||
|
||
|
||
def test_curated_events_have_valid_slugs():
|
||
import city_events
|
||
slugs = set(cities.all_slugs())
|
||
assert len(city_events.EVENTS) >= 20
|
||
assert all(s in slugs for s in city_events.EVENTS), \
|
||
[s for s in city_events.EVENTS if s not in slugs]
|
||
|
||
|
||
def test_curated_event_renders(client):
|
||
b = client.get(f"{B}/climate/new-orleans-louisiana-us").text
|
||
assert "Notable weather in New Orleans" in b
|
||
assert "Hurricane Katrina" in b
|
||
|
||
|
||
def test_uncurated_city_has_no_event_section(client):
|
||
b = client.get(f"{B}/climate/shanghai-cn").text
|
||
assert "city-event" not in b # falls back to the flavor blurb
|
||
|
||
|
||
def test_city_travel_cta_prefills_compare(client):
|
||
b = client.get(f"{B}/climate/{SLUG}").text
|
||
assert "Thinking of visiting" in b
|
||
assert "/compare#loc=" in b # the city is pre-loaded on the compare page
|
||
|
||
|
||
def test_city_blurb_renders_with_attribution(client, monkeypatch):
|
||
monkeypatch.setattr(cities, "flavor",
|
||
lambda slug: {"extract": "Testville is a lovely place to test.",
|
||
"url": "https://en.wikipedia.org/wiki/Testville", "title": "Testville"})
|
||
b = client.get(f"{B}/climate/{SLUG}").text
|
||
assert "Testville is a lovely place to test." in b
|
||
assert "via Wikipedia" in b # CC BY-SA attribution link
|
||
|
||
|
||
def test_month_and_records(client):
|
||
assert client.get(f"{B}/climate/{SLUG}/july").status_code == 200
|
||
assert client.get(f"{B}/climate/{SLUG}/records").status_code == 200
|
||
assert client.get(f"{B}/climate/{SLUG}/notamonth").status_code == 404
|
||
|
||
|
||
def test_records_page_has_monthly_and_seasonal(client):
|
||
b = client.get(f"{B}/climate/{SLUG}/records").text
|
||
# Monthly records: all 12 months, each linking to its month page.
|
||
assert "Records by month" in b
|
||
for month in ("January", "July", "December"):
|
||
assert month in b
|
||
assert f"/climate/{SLUG}/january" in b
|
||
# Seasonal records: labelled for London's Northern Hemisphere, with month spans.
|
||
assert "Records by season" in b
|
||
assert "Northern Hemisphere" in b
|
||
assert 'Winter <span class="season-span">(Dec–Feb)</span>' in b
|
||
assert 'Summer <span class="season-span">(Jun–Aug)</span>' in b
|
||
# All-time table still present, plus structured data and real °F values.
|
||
assert "All-time records" in b
|
||
assert '"@type":"Dataset"' in b and "°F" in b
|
||
# Title/description advertise the new coverage.
|
||
assert "monthly" in b.lower() and "seasonal" in b.lower()
|
||
|
||
|
||
def test_records_seasons_flip_in_southern_hemisphere(client):
|
||
# São Paulo is south of the equator, so Dec–Feb is summer, not winter.
|
||
b = client.get(f"{B}/climate/sao-paulo-br/records").text
|
||
assert "Southern Hemisphere" in b
|
||
assert 'Summer <span class="season-span">(Dec–Feb)</span>' in b
|
||
assert 'Winter <span class="season-span">(Jun–Aug)</span>' in b
|
||
|
||
|
||
def test_climate_pages_are_colour_coded(client):
|
||
# Temperatures wear the diverging cold→hot palette (a heat map, not a plain grid),
|
||
# and the city page carries the monthly temperature-range strip.
|
||
city = client.get(f"{B}/climate/{SLUG}").text
|
||
assert "t-cell t-" in city # tinted temperature cells in the normals table
|
||
assert "range-fill" in city and "--c1:var(--" in city # the monthly range strip's gradient bars
|
||
recs = client.get(f"{B}/climate/{SLUG}/records").text
|
||
assert "t-inline t-" in recs # tinted record chips
|
||
month = client.get(f"{B}/climate/{SLUG}/july").text
|
||
assert "t-inline t-" in month # tinted hero numbers + records
|
||
|
||
|
||
def test_records_show_both_extremes_per_metric(client):
|
||
# Each metric column shows BOTH its record warmest (▲) and coldest (▼) — so the
|
||
# daytime high carries its record-low high, and the overnight low its record-high low.
|
||
b = client.get(f"{B}/climate/{SLUG}/records").text
|
||
assert "Daytime high" in b and "Overnight low" in b
|
||
assert "▲" in b and "▼" in b
|
||
# 12 months × 2 metric columns × 2 extremes = 48 chips just in the monthly table.
|
||
assert b.count('class="rec-ext"') >= 48
|
||
|
||
|
||
def test_hub_glossary_about(client):
|
||
assert client.get(f"{B}/climate").status_code == 200
|
||
assert client.get(f"{B}/glossary").status_code == 200
|
||
assert client.get(f"{B}/glossary/percentile").status_code == 200
|
||
assert client.get(f"{B}/glossary/not-a-term").status_code == 404
|
||
assert client.get(f"{B}/about").status_code == 200
|