PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m2s
PR Checks / Backend Smoke (pull_request) Successful in 8s
PR Checks / Build Backend (no push) (pull_request) Successful in 16s
PR Checks / Build Frontend (no push) (pull_request) Successful in 44s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Canceled after 1m21s
Someone on a place page is usually looking for a school they can name, so the order should serve scanning for it rather than ranking. /api/rankings keeps its league-table ordering; this is a place-page decision, not a site-wide one. Sorted case-insensitively, or a capitalised name would sort ahead of every lowercase one. The change made five pieces of copy untrue, so they go with it. The phase variant titled itself "— Ranked", and all four route families described themselves as "ranked by SATs and GCSE results". A page that opens by claiming an order it does not keep is worse than one that claims nothing. The ItemList markup carried `position` with no declared order, which reads as a ranking. It now declares ItemListOrderAscending, so the structured data says what the table does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
92 lines
3.2 KiB
Python
92 lines
3.2 KiB
Python
"""Tests for the places API (spec 2026-08-21)."""
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
def _schools_df() -> pd.DataFrame:
|
|
base = {
|
|
"local_authority": "Essex", "school_type": "Academy",
|
|
"phase": "Primary", "year": 202425, "ofsted_grade": 2.0,
|
|
"ofsted_date": None, "attainment_8_score": np.nan,
|
|
"town": "Brentwood", "postcode": "CM13 1AA", "status": "Open",
|
|
"address": "1 Test Street", "latitude": 51.6, "longitude": 0.3,
|
|
}
|
|
return pd.DataFrame([
|
|
{**base, "urn": 100000 + i, "school_name": f"Brentwood School {i}",
|
|
"rwm_expected_pct": 50.0 + i}
|
|
for i in range(6)
|
|
])
|
|
|
|
|
|
@pytest.fixture()
|
|
def client(monkeypatch):
|
|
from backend import app as app_module
|
|
|
|
monkeypatch.setattr(app_module, "load_school_data", _schools_df)
|
|
monkeypatch.setattr(app_module, "load_latest_school_data", _schools_df)
|
|
monkeypatch.setattr(app_module, "_place_registry", None)
|
|
return TestClient(app_module.app, raise_server_exceptions=False)
|
|
|
|
|
|
def test_registry_lists_each_published_place(client):
|
|
body = client.get("/api/places").json()
|
|
slugs = {(p["kind"], p["slug"]) for p in body["places"]}
|
|
assert ("town", "brentwood") in slugs
|
|
assert ("authority", "essex") in slugs
|
|
assert ("outcode", "cm13") in slugs
|
|
|
|
|
|
def test_registry_carries_a_count_per_place(client):
|
|
body = client.get("/api/places").json()
|
|
town = next(p for p in body["places"] if p["slug"] == "brentwood")
|
|
assert town["count"] == 6
|
|
|
|
|
|
def test_place_detail_returns_its_schools_alphabetically(client):
|
|
"""A place page is read by someone looking for a school they can name.
|
|
|
|
Scanning for it is what the order should serve, so the list is A-Z.
|
|
/api/rankings is where the league-table ordering lives.
|
|
"""
|
|
body = client.get("/api/places/town/brentwood").json()
|
|
assert body["place"]["name"] == "Brentwood"
|
|
names = [s["school_name"] for s in body["schools"]]
|
|
assert names == sorted(names, key=str.lower)
|
|
|
|
|
|
def test_place_ordering_ignores_case(client):
|
|
body = client.get("/api/places/town/brentwood").json()
|
|
names = [s["school_name"] for s in body["schools"]]
|
|
# A capitalised name must not sort ahead of every lowercase one.
|
|
assert names == sorted(names, key=str.lower)
|
|
|
|
|
|
def test_the_rankings_endpoint_still_ranks_by_metric(client):
|
|
# Alphabetical is a place-page decision, not a site-wide one.
|
|
body = client.get("/api/rankings?metric=rwm_expected_pct&phase=primary").json()
|
|
scores = [r["rwm_expected_pct"] for r in body.get("rankings", [])
|
|
if r.get("rwm_expected_pct") is not None]
|
|
assert scores == sorted(scores, reverse=True)
|
|
|
|
|
|
def test_place_detail_carries_the_local_average(client):
|
|
body = client.get("/api/places/town/brentwood").json()
|
|
# 50..55 inclusive
|
|
assert body["averages"]["rwm_expected_pct"] == pytest.approx(52.5)
|
|
|
|
|
|
def test_phase_filter_narrows_the_school_list(client):
|
|
body = client.get("/api/places/town/brentwood?phase=secondary").json()
|
|
assert body["schools"] == []
|
|
|
|
|
|
def test_unknown_place_404s(client):
|
|
assert client.get("/api/places/town/atlantis").status_code == 404
|
|
|
|
|
|
def test_unknown_kind_404s(client):
|
|
assert client.get("/api/places/planet/mars").status_code == 404
|