fix(api): serialize schools that have no performance rows
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 9m39s
PR Checks / Backend Smoke (pull_request) Successful in 8s
PR Checks / Build Backend (no push) (pull_request) Successful in 30s
PR Checks / Build Frontend (no push) (pull_request) Successful in 42s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 2m0s

Schools without KS2/KS4 results (special post-16 institutions, sixth-form
centres, PRUs, new schools) come back from the marts LEFT JOIN with NaN in
every numeric column. school_info passed those raw pandas values straight
into JSONResponse, which renders with allow_nan=False, so the detail
endpoint 500d and the frontend turned that into a 404 on every such SEO
landing page.

Run school_info values through convert_to_native (the same treatment
yearly_data already gets), add backend unit tests plus a pytest step in PR
checks, and an e2e journey that finds a results-less school via the search
API and asserts its page renders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tudor
2026-07-07 09:22:54 +01:00
co-authored by Claude Opus 4.8
parent 929748d014
commit 87642b7b06
7 changed files with 116 additions and 5 deletions
+28
View File
@@ -60,6 +60,34 @@ test('school detail page renders name and performance data', async ({ page }) =>
await expect(page.locator('canvas:visible').first()).toBeVisible({ timeout: 15_000 });
});
test('school with no performance data still gets a working detail page', async ({ page }) => {
// Schools without KS2/KS4 results (special post-16 institutions, sixth-form
// centres, PRUs) used to 500 in the API — NaN GIAS fields broke JSON
// serialization — which the frontend rendered as a 404 on every such SEO
// landing page. Find one via the search API (year === null marks "no
// performance rows") and assert its page renders.
const candidates: number[] = [];
for (const q of ['post 16', 'specialist college', 'sixth form']) {
const resp = await page.request.get(
`/api/schools?search=${encodeURIComponent(q)}&per_page=20`
);
if (!resp.ok()) continue;
const body = await resp.json();
for (const s of body.schools ?? []) {
if (s.year === null && s.urn) candidates.push(s.urn);
}
if (candidates.length) break;
}
test.skip(candidates.length === 0, 'no results-less school in this dataset');
const detail = await page.request.get(`/api/schools/${candidates[0]}`);
expect(detail.status(), 'detail API must not 500 for a results-less school').toBe(200);
await page.goto(`/school/${candidates[0]}`);
await page.waitForURL(/\/school\/\d+-/); // redirected to canonical slug
await expect(page.locator('h1').first()).toBeVisible();
});
test('school hero map opens fullscreen on mobile without the Fullscreen API', async ({ page }) => {
// iOS Safari has no Element.requestFullscreen; the map must fall back to a
// CSS overlay. Simulate that by removing the API before any page script runs.