Files
school_compare/nextjs-app/__tests__/lib/lastDistanceOffered.test.ts
T

231 lines
8.9 KiB
TypeScript
Raw Normal View History

/**
* Last distance offered — formatting and the caveats attached to the figure.
*
* The assertions about the route note and the year are not cosmetic. A cut-off
* shown without its year, or a banded school's widest cut-off shown as if it
* were the only one, tells a parent something false about their chances of a
* place — so both are pinned here rather than left to the component.
*/
import { formatCutoffDistance, formatEntryYear } from '@/lib/utils';
import {
describeCutoff, cutoffYearRows, cutoffTrendSummary, cutoffCoverageNote,
describeCutoffAbsence, compareToCutoffs, entryYearOf, CUTOFF_UNCERTAINTY_M,
} from '@/components/school/lastDistanceOffered';
import type { SchoolAdmissionDistance } from '@/lib/types';
const distance = (over: Partial<SchoolAdmissionDistance> = {}): SchoolAdmissionDistance => ({
year: 2025,
distance_m: 500,
route_count: 1,
la_name: 'Camden',
distance_unit_raw: 'miles',
...over,
});
describe('formatCutoffDistance', () => {
it('leads with miles, the unit councils publish in', () => {
expect(formatCutoffDistance(500)).toEqual({ primary: '0.31 miles', secondary: '500 m' });
expect(formatCutoffDistance(1609.344)).toEqual({ primary: '1.00 miles', secondary: '1.6 km' });
});
it('switches to kilometres for the support figure above a kilometre', () => {
expect(formatCutoffDistance(3472.96)!.secondary).toBe('3.5 km');
});
it('leads with metres below 100 m, where a miles figure carries nothing', () => {
expect(formatCutoffDistance(27)).toEqual({ primary: '27 m', secondary: '0.02 miles' });
});
it('returns null rather than a zero cut-off', () => {
// 0.0 miles appears in the source where a school filled on a higher
// criterion. Rendered as "0.00 miles" it would read as the opposite.
expect(formatCutoffDistance(0)).toBeNull();
expect(formatCutoffDistance(null)).toBeNull();
expect(formatCutoffDistance(undefined)).toBeNull();
expect(formatCutoffDistance(Number.NaN)).toBeNull();
});
});
describe('formatEntryYear', () => {
it('names the intake, not the academic year', () => {
// formatAcademicYear would render 2025 as "2025/26", which reads as a
// school year rather than the September a child started.
expect(formatEntryYear(2025)).toBe('September 2025');
expect(formatEntryYear(null)).toBe('');
});
});
describe('describeCutoff', () => {
it('always carries the entry year alongside the figure', () => {
const d = describeCutoff(distance({ distance_m: 772.49, year: 2024 }));
expect(d).not.toBeNull();
expect(d!.primary).toBe('0.48 miles');
expect(d!.entryYear).toBe('September 2024');
});
it('says nothing about routes for a school with one', () => {
expect(describeCutoff(distance({ route_count: 1 }))!.routeNote).toBeNull();
expect(describeCutoff(distance({ route_count: null }))!.routeNote).toBeNull();
});
it('warns that a banded school\'s figure is the widest of several', () => {
const note = describeCutoff(distance({ route_count: 4 }))!.routeNote;
expect(note).toContain('4 admission routes');
expect(note).toContain('shorter cut-off');
});
it('is null when there is nothing publishable', () => {
expect(describeCutoff(null)).toBeNull();
expect(describeCutoff(undefined)).toBeNull();
expect(describeCutoff(distance({ distance_m: null }))).toBeNull();
});
});
// ── Year-by-year history ───────────────────────────────────────────────
const d = (year: number, distance_m: number | null, route_count = 1): SchoolAdmissionDistance => ({
year, distance_m, route_count, la_name: 'Camden', distance_unit_raw: 'miles',
});
describe('entryYearOf', () => {
it('reduces EES academic codes and plain entry years to one key', () => {
// Without this the two histories never join and every year looks unpublished.
expect(entryYearOf(202425)).toBe(2024);
expect(entryYearOf(2024)).toBe(2024);
});
});
describe('cutoffYearRows', () => {
it('emits a row for every year in the span, including the empty ones', () => {
const rows = cutoffYearRows([d(2021, 900), d(2024, 700)]);
expect(rows.map((r) => r.year)).toEqual([2024, 2023, 2022, 2021]);
expect(rows.map((r) => r.status)).toEqual([
'published', 'not-published', 'not-published', 'published',
]);
});
it('separates "nothing published" from "was not oversubscribed"', () => {
// The two look identical in the distance data and mean opposite things.
const rows = cutoffYearRows(
[d(2021, 900), d(2023, 700)],
[{ year: 2022, oversubscribed: false }],
);
expect(rows.find((r) => r.year === 2022)!.status).toBe('not-oversubscribed');
});
it('matches EES six-digit years against plain cut-off years', () => {
const rows = cutoffYearRows(
[d(2023, 700)],
[{ year: 202324, places_offered: 60, oversubscribed: true }],
);
expect(rows.find((r) => r.year === 2023)!.placesOffered).toBe(60);
});
it('does not stretch the span back over admissions years with no cut-offs', () => {
// EES reaches back further than councils publish; padding the chart with a
// decade of blanks would bury the years that carry a figure.
const rows = cutoffYearRows(
[d(2024, 700)],
[{ year: 2015, oversubscribed: true }, { year: 2024, oversubscribed: true }],
);
expect(rows.map((r) => r.year)).toEqual([2024]);
});
it('is safe when the backend does not send the field at all', () => {
expect(cutoffYearRows(undefined as never)).toEqual([]);
expect(cutoffYearRows([], [])).toEqual([]);
});
});
describe('cutoffTrendSummary', () => {
const rowsFor = (pts: [number, number][]) => cutoffYearRows(pts.map(([y, m]) => d(y, m)));
it('says nothing below four published points', () => {
expect(cutoffTrendSummary(rowsFor([[2022, 900], [2023, 800], [2024, 700]]))).toBeNull();
});
it('names both endpoints and their years rather than passing a verdict', () => {
const s = cutoffTrendSummary(rowsFor([
[2021, 1000], [2022, 900], [2023, 800], [2024, 500],
]));
expect(s).toContain('2021');
expect(s).toContain('2024');
expect(s).toContain('tightened');
});
it('does not call a small wobble a direction', () => {
const s = cutoffTrendSummary(rowsFor([
[2021, 1000], [2022, 1010], [2023, 990], [2024, 1030],
]));
expect(s).toContain('stayed broadly the same');
});
});
describe('cutoffCoverageNote', () => {
it('warns while the record is too thin to read as a trend', () => {
expect(cutoffCoverageNote(cutoffYearRows([d(2024, 700)]))).toContain('Only 1 year');
expect(cutoffCoverageNote(cutoffYearRows([
d(2021, 1000), d(2022, 900), d(2023, 800), d(2024, 700),
]))).toBeNull();
});
});
describe('describeCutoffAbsence', () => {
it('explains a selective school by how it admits, not by missing data', () => {
const s = describeCutoffAbsence({ localAuthority: 'Kent', admissionsPolicy: 'Selective' });
expect(s).toContain('entrance test');
expect(s).not.toContain('has not published');
});
it('reads a consistently undersubscribed school as good news', () => {
const s = describeCutoffAbsence({
localAuthority: 'Camden',
admissionsHistory: [
{ year: 2022, oversubscribed: false },
{ year: 2023, oversubscribed: false },
{ year: 2024, oversubscribed: false },
],
});
expect(s).toContain('has not needed a distance cut-off');
});
it('otherwise names the authority that would hold the figure', () => {
expect(describeCutoffAbsence({ localAuthority: 'Camden' }))
.toContain('Camden has not published');
});
});
describe('compareToCutoffs', () => {
const rows = cutoffYearRows(
[d(2022, 1000), d(2024, 800)],
[{ year: 2023, oversubscribed: false }],
);
it('marks a clearly nearer home as inside every comparable year', () => {
const r = compareToCutoffs(300, rows);
expect(r.insideCount).toBe(2);
expect(r.comparableCount).toBe(2);
expect(r.headline).toContain('inside the cut-off in all 2 years');
});
it('marks a clearly further home as outside', () => {
const r = compareToCutoffs(4000, rows);
expect(r.insideCount).toBe(0);
expect(r.headline).toContain('outside the cut-off in every year');
});
it('refuses to call a result inside the measurement error', () => {
// A postcode centroid covers several addresses; claiming a place on a 20 m
// margin would be inventing precision the inputs do not have.
const r = compareToCutoffs(800 - CUTOFF_UNCERTAINTY_M / 2, rows);
expect(r.years.find((y) => y.year === 2024)!.verdict).toBe('too-close');
expect(r.detail).toContain('too close to call');
});
it('never counts an unpublished year as a pass or a fail', () => {
const r = compareToCutoffs(300, rows);
expect(r.years.find((y) => y.year === 2023)!.verdict).toBe('unknown');
expect(r.detail).toContain('no published figure');
});
});