PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m4s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 16s
PR Checks / Build Frontend (no push) (pull_request) Successful in 45s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Failing after 3m17s
Earlier years are to become a paid feature, so they stop being published.
The load-bearing part is that this is a change to the API, not only to the
page. /api/schools/{urn} is public and unauthenticated: leaving
admission_distance_history in the payload while declining to render it would
have handed the whole record to anyone who opened the network tab. It is
withheld at the source, and the page follows.
Nothing changes upstream. The tap, the plausibility band and
fact_admission_distance are untouched and still load every published year, so
restoring history for entitled callers is a change to one function in
data_loader rather than a re-collection.
What the reader now gets is the latest figure on the Admissions tile, and a
Distance section that answers the question the number alone cannot: whether
their own address falls inside it. Retitled to "How far away are you?", which
is what it now does — the previous title described a record that is no longer
there.
Removed with the history: the trend chart, the year-by-year table, the
per-year verdict strip, the trend summary and the coverage note, along with
their CSS. The section goes from 743px to 417px.
One consequence worth naming. A run of years used to soften a single close
call — a home just outside one year's cut-off was usually inside another. With
one year published, the "too close to call" band is the entire safety margin
between a parent and a place they do not have, so the verdict now names its
year, and the three outcomes are tinted apart rather than distinguished by
wording alone.
The existing stylesheet test earned its keep here: the three verdict classes
were referenced before they were written, and it caught them. Unstyled, a
"beyond the cut-off" result would have been indistinguishable from an "inside"
one — the exact failure the longhand class map was written to prevent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDvkyqqHABm4bmth2kjAxE
181 lines
7.4 KiB
TypeScript
181 lines
7.4 KiB
TypeScript
/**
|
|
* Last distance offered, rendered on both detail templates.
|
|
*
|
|
* The figure is the one number on these pages that a parent may act on — it is
|
|
* easy to read as "we live inside the catchment, we will get a place". These
|
|
* tests pin the things that stop it being read that way: the year is always
|
|
* present, the caveat is always present, and the blanket "not available"
|
|
* sentence appears only when it is actually true.
|
|
*/
|
|
|
|
import { screen, fireEvent } from '@testing-library/react';
|
|
import { renderSchoolDetail, renderSecondarySchoolDetail } from '../support/renderSchoolDetail';
|
|
import { primaryFixture, secondaryFixture } from '../support/schoolFixtures';
|
|
import type { SchoolAdmissionDistance } from '@/lib/types';
|
|
|
|
const cutoff = (over: Partial<SchoolAdmissionDistance> = {}): SchoolAdmissionDistance => ({
|
|
year: 2025,
|
|
distance_m: 500,
|
|
route_count: 1,
|
|
la_name: 'Camden',
|
|
distance_unit_raw: 'miles',
|
|
...over,
|
|
});
|
|
|
|
describe('primary detail page', () => {
|
|
it('shows the figure with the year it belongs to', () => {
|
|
renderSchoolDetail({ ...primaryFixture, admissionDistance: cutoff({ distance_m: 772.49, year: 2024 }) });
|
|
|
|
expect(screen.getByText('0.48 miles')).toBeInTheDocument();
|
|
expect(screen.getByText(/Last distance offered/)).toHaveTextContent('September 2024');
|
|
});
|
|
|
|
it('never shows the figure without saying it is not a catchment', () => {
|
|
renderSchoolDetail({ ...primaryFixture, admissionDistance: cutoff() });
|
|
|
|
expect(screen.getByText(/not a fixed catchment/)).toBeInTheDocument();
|
|
expect(screen.getByText(/moves every year/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('flags that a banded school\'s figure is the widest of several routes', () => {
|
|
renderSchoolDetail({ ...primaryFixture, admissionDistance: cutoff({ route_count: 4 }) });
|
|
|
|
expect(screen.getByText(/4 admission routes/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders nothing distance-related when the LA publishes none', () => {
|
|
renderSchoolDetail({ ...primaryFixture, admissionDistance: null });
|
|
|
|
expect(screen.queryByText(/Last distance offered/)).not.toBeInTheDocument();
|
|
expect(screen.queryByText(/not a fixed catchment/)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('carries the figure even with no EES admissions row', () => {
|
|
// The two sources are independent; this school has a cut-off and no
|
|
// admissions figures. Before this feature the section did not render at all.
|
|
renderSchoolDetail({
|
|
...primaryFixture,
|
|
admissions: null,
|
|
admissionsHistory: [],
|
|
admissionDistance: cutoff({ distance_m: 1421.05 }),
|
|
});
|
|
|
|
expect(screen.getByText('0.88 miles')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('secondary detail page', () => {
|
|
it('shows the figure with the year it belongs to', () => {
|
|
renderSecondarySchoolDetail({ ...secondaryFixture, admissionDistance: cutoff({ distance_m: 3472.96 }) });
|
|
|
|
expect(screen.getByText('2.16 miles')).toBeInTheDocument();
|
|
expect(screen.getByText(/Last distance offered/)).toHaveTextContent('September 2025');
|
|
});
|
|
|
|
it('drops the blanket "not available" line once a distance exists', () => {
|
|
renderSecondarySchoolDetail({ ...secondaryFixture, admissionDistance: cutoff() });
|
|
|
|
expect(screen.queryByText(/has not published a cut-off distance/)).not.toBeInTheDocument();
|
|
expect(screen.getByText(/not a fixed catchment/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('names the authority that would hold the data when there is none', () => {
|
|
// The old copy asserted "Historical distance cut-off data is not available
|
|
// for this school" on every secondary page, including the ones whose
|
|
// council does publish it.
|
|
renderSecondarySchoolDetail({ ...secondaryFixture, admissionDistance: null });
|
|
|
|
expect(screen.getByText(/has not published a cut-off distance/)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
// ── The Distance section ───────────────────────────────────────────────
|
|
|
|
describe('Distance section', () => {
|
|
it('appears for a school with a figure and coordinates', () => {
|
|
const { container } = renderSchoolDetail({
|
|
...primaryFixture,
|
|
schoolInfo: { ...primaryFixture.schoolInfo, latitude: 51.5, longitude: -0.12 },
|
|
admissionDistance: cutoff({ distance_m: 700, year: 2026 }),
|
|
});
|
|
|
|
expect(container.querySelector('#distance')).toBeInTheDocument();
|
|
expect(screen.getByText('How far away are you?')).toBeInTheDocument();
|
|
});
|
|
|
|
it('stays away when the school has no coordinates to measure from', () => {
|
|
const { container } = renderSchoolDetail({
|
|
...primaryFixture,
|
|
schoolInfo: { ...primaryFixture.schoolInfo, latitude: null, longitude: null },
|
|
admissionDistance: cutoff({ distance_m: 700, year: 2026 }),
|
|
});
|
|
|
|
expect(container.querySelector('#distance')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('stays away when no figure has been published', () => {
|
|
const { container } = renderSchoolDetail({
|
|
...primaryFixture,
|
|
schoolInfo: { ...primaryFixture.schoolInfo, latitude: 51.5, longitude: -0.12 },
|
|
admissionDistance: null,
|
|
});
|
|
|
|
expect(container.querySelector('#distance')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('shows no year-by-year record — that is held back as a paid feature', () => {
|
|
// The page must not leak the history through a table, a chart or a strip of
|
|
// per-year verdicts. The API no longer sends it either; this guards the
|
|
// render side so a future component cannot quietly put it back.
|
|
renderSchoolDetail({
|
|
...primaryFixture,
|
|
schoolInfo: { ...primaryFixture.schoolInfo, latitude: 51.5, longitude: -0.12 },
|
|
admissionDistance: cutoff({ distance_m: 700, year: 2026 }),
|
|
});
|
|
|
|
// Scoped to the section: the page has other tables (the history section's).
|
|
const section = document.querySelector('#distance')!;
|
|
expect(section.querySelector('table')).toBeNull();
|
|
expect(screen.queryByText(/Last distance offered, by year/)).not.toBeInTheDocument();
|
|
expect(screen.queryByText(/Not published/)).not.toBeInTheDocument();
|
|
expect(screen.queryByText(/too few to read as a trend/)).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('secondary Distance section', () => {
|
|
it('renders on the secondary template too', () => {
|
|
const { container } = renderSecondarySchoolDetail({
|
|
...secondaryFixture,
|
|
schoolInfo: { ...secondaryFixture.schoolInfo, latitude: 51.5, longitude: -0.12 },
|
|
admissionDistance: cutoff({ distance_m: 3472.96, year: 2026 }),
|
|
});
|
|
|
|
expect(container.querySelector('#distance')).toBeInTheDocument();
|
|
});
|
|
|
|
it('explains a selective school by how it admits rather than as missing data', () => {
|
|
renderSecondarySchoolDetail({
|
|
...secondaryFixture,
|
|
schoolInfo: { ...secondaryFixture.schoolInfo, admissions_policy: 'Selective' },
|
|
admissionDistance: null,
|
|
});
|
|
|
|
expect(screen.getByText(/ranked by the entrance test/)).toBeInTheDocument();
|
|
expect(screen.queryByText(/has not published a cut-off distance/)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('reads a consistently undersubscribed school as good news', () => {
|
|
renderSecondarySchoolDetail({
|
|
...secondaryFixture,
|
|
admissionDistance: null,
|
|
admissionsHistory: [
|
|
{ year: 2022, oversubscribed: false },
|
|
{ year: 2023, oversubscribed: false },
|
|
{ year: 2024, oversubscribed: false },
|
|
],
|
|
});
|
|
|
|
expect(screen.getByText(/has not needed a distance cut-off/)).toBeInTheDocument();
|
|
});
|
|
});
|