Reported from staging: a Catholic primary showed six Catholic primaries, none of them close enough to be a real option, and omitted the community school down the road. Three causes, compounding. Ranking put tier before distance, so a faith match at 2.9 miles outranked a community school at 0.3. The ENOUGH=3 stopping rule — added so a cap of six would not drag in weak distant matches — filled the row from the best tier before it ever widened, which is what made every card Catholic. And a 3-mile tier-1 radius is sane for a secondary and most of a city for a primary, whose catchments are routinely under a mile. The premise was backwards. For a parent, distance is a constraint and intake is a preference; a school beyond a primary catchment is not a weaker option, it is not an option. So distance now decides the order and nothing else does. The hard filters are untouched — they were always where the defensibility lived. Similarity survives as chips on the card: reported, so a reader applies their own weighting, rather than ranked, so we apply ours for them. Reach is capped per phase (primary 2, secondary 6, post-16 10) as a sanity bound, not a target: ordering already handles density, so the cap only decides what happens where an area is sparse. A primary with nothing inside two miles now renders no section, which is the honest answer. Deleted: the tier system, the stopping rule, the tier-dependent lede, the `tier` field, the tier-3 fallback chip and its style. select_similar also stops taking is_secondary — it reads the phase from the subject's own row, so no caller can hand it one that disagrees with the data. The heading is now "Other schools nearby". The hard filters still guarantee a comparable set, but nothing ranks on likeness, so the heading no longer says it does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
211 lines
7.5 KiB
TypeScript
211 lines
7.5 KiB
TypeScript
import {
|
|
computeSchoolFlags, buildNavItems,
|
|
computeSecondaryFlags, buildSecondaryNavItems,
|
|
} from '@/lib/schoolSections';
|
|
import {
|
|
primaryFixture, secondaryFixture, allThroughFixture, specialFixture,
|
|
} from '../support/schoolFixtures';
|
|
|
|
describe('computeSchoolFlags', () => {
|
|
it('treats an all-through school as both primary-capable and secondary', () => {
|
|
const f = computeSchoolFlags(allThroughFixture);
|
|
expect(f.isAllThrough).toBe(true);
|
|
expect(f.isSecondary).toBe(true);
|
|
expect(f.hasKS2Results).toBe(true);
|
|
expect(f.hasKS4Results).toBe(true);
|
|
});
|
|
|
|
it('classifies an ordinary primary', () => {
|
|
const f = computeSchoolFlags(primaryFixture);
|
|
expect(f.isPrimary).toBe(true);
|
|
expect(f.isSecondary).toBe(false);
|
|
expect(f.hasKS2Results).toBe(true);
|
|
expect(f.hasKS4Results).toBe(false);
|
|
});
|
|
|
|
it('classifies an ordinary secondary', () => {
|
|
const f = computeSchoolFlags(secondaryFixture);
|
|
expect(f.isSecondary).toBe(true);
|
|
expect(f.isAllThrough).toBe(false);
|
|
expect(f.hasKS4Results).toBe(true);
|
|
});
|
|
|
|
it('suppresses the KS2 comparison for a special school', () => {
|
|
const f = computeSchoolFlags(specialFixture);
|
|
expect(f.isSpecial).toBe(true);
|
|
expect(f.suppressKs2Comparison).toBe(true);
|
|
expect(f.suppressKs4Comparison).toBe(true);
|
|
});
|
|
|
|
it('treats an all-zero KS2 row as a placeholder', () => {
|
|
const f = computeSchoolFlags(specialFixture);
|
|
expect(f.ks2Placeholder).toBe(true);
|
|
});
|
|
|
|
it('does not suppress comparison for an ordinary primary', () => {
|
|
const f = computeSchoolFlags(primaryFixture);
|
|
expect(f.isSpecial).toBe(false);
|
|
expect(f.suppressKs2Comparison).toBe(false);
|
|
});
|
|
|
|
it('detects a gender split only when census counts are present', () => {
|
|
expect(computeSchoolFlags(primaryFixture).hasGenderSplit).toBe(true);
|
|
expect(
|
|
computeSchoolFlags({ ...primaryFixture, census: null }).hasGenderSplit,
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('buildNavItems', () => {
|
|
it('omits sections with no data', () => {
|
|
const flags = computeSchoolFlags(specialFixture);
|
|
const ids = buildNavItems(flags, {
|
|
ofsted: null, admissions: null, admissionDistance: null, yearlyDataLength: 1,
|
|
}).map((n) => n.id);
|
|
|
|
expect(ids).not.toContain('ofsted');
|
|
expect(ids).not.toContain('admissions');
|
|
expect(ids).toContain('history');
|
|
});
|
|
|
|
it('labels the results section by phase', () => {
|
|
const label = (fixture: any) => {
|
|
const flags = computeSchoolFlags(fixture);
|
|
return buildNavItems(flags, {
|
|
ofsted: fixture.ofsted,
|
|
admissions: fixture.admissions,
|
|
admissionDistance: null,
|
|
yearlyDataLength: fixture.yearlyData.length,
|
|
}).find((n) => n.id === 'results')?.label;
|
|
};
|
|
|
|
expect(label(primaryFixture)).toBe('SATs');
|
|
expect(label(secondaryFixture)).toBe('GCSEs');
|
|
expect(label(allThroughFixture)).toBe('Results');
|
|
});
|
|
|
|
it('opens the admissions entry for a cut-off distance with no EES admissions', () => {
|
|
// 3% of the schools that render have one source and not the other. The nav
|
|
// condition and the section's render condition have to agree, or the sticky
|
|
// nav links to an anchor that was never rendered.
|
|
const flags = computeSchoolFlags(specialFixture);
|
|
const ids = buildNavItems(flags, {
|
|
ofsted: null,
|
|
admissions: null,
|
|
admissionDistance: { year: 2025, distance_m: 500, route_count: 1, la_name: 'Camden', distance_unit_raw: 'miles' },
|
|
yearlyDataLength: 1,
|
|
}).map((n) => n.id);
|
|
|
|
expect(ids).toContain('admissions');
|
|
});
|
|
|
|
it('keeps the engagement-led ordering', () => {
|
|
const flags = computeSchoolFlags(primaryFixture);
|
|
const ids = buildNavItems(flags, {
|
|
ofsted: primaryFixture.ofsted,
|
|
admissions: primaryFixture.admissions,
|
|
admissionDistance: null,
|
|
yearlyDataLength: primaryFixture.yearlyData.length,
|
|
}).map((n) => n.id);
|
|
|
|
expect(ids).toEqual([
|
|
'ofsted', 'results', 'admissions', 'inclusion',
|
|
'history', 'school-life', 'local-area', 'finances',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('computeSecondaryFlags', () => {
|
|
it('reads results and sixth form from the secondary fixture', () => {
|
|
const f = computeSecondaryFlags(secondaryFixture);
|
|
expect(f.hasResults).toBe(true);
|
|
expect(f.hasSixthForm).toBe(false);
|
|
expect(f.hasWellbeing).toBe(true);
|
|
});
|
|
|
|
it('reports sixth form when GIAS flags it', () => {
|
|
expect(computeSecondaryFlags(allThroughFixture).hasSixthForm).toBe(true);
|
|
});
|
|
|
|
it('suppresses the comparison for a special school', () => {
|
|
const f = computeSecondaryFlags(specialFixture);
|
|
expect(f.isSpecial).toBe(true);
|
|
expect(f.suppressComparison).toBe(true);
|
|
});
|
|
|
|
it('does not flag Progress 8 as suspended for pre-2024/25 cohorts', () => {
|
|
expect(computeSecondaryFlags(secondaryFixture).p8Suspended).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('buildSecondaryNavItems', () => {
|
|
it('uses the secondary section ids', () => {
|
|
const flags = computeSecondaryFlags(secondaryFixture);
|
|
const ids = buildSecondaryNavItems(flags, {
|
|
ofsted: secondaryFixture.ofsted,
|
|
admissions: secondaryFixture.admissions,
|
|
admissionDistance: null,
|
|
yearlyDataLength: secondaryFixture.yearlyData.length,
|
|
}).map((n) => n.id);
|
|
|
|
expect(ids).toEqual(['ofsted', 'gcse', 'admissions', 'history', 'wellbeing', 'finances']);
|
|
});
|
|
|
|
it('opens the admissions entry for a cut-off distance alone', () => {
|
|
const flags = computeSecondaryFlags(secondaryFixture);
|
|
const ids = buildSecondaryNavItems(flags, {
|
|
ofsted: null,
|
|
admissions: null,
|
|
admissionDistance: { year: 2025, distance_m: 2400, route_count: 4, la_name: 'Islington', distance_unit_raw: 'miles' },
|
|
yearlyDataLength: 1,
|
|
}).map((n) => n.id);
|
|
|
|
expect(ids).toContain('admissions');
|
|
});
|
|
|
|
it('gates History on more than one year, unlike the primary page', () => {
|
|
const flags = computeSecondaryFlags(secondaryFixture);
|
|
const ids = buildSecondaryNavItems(flags, {
|
|
ofsted: null, admissions: null, admissionDistance: null, yearlyDataLength: 1,
|
|
}).map((n) => n.id);
|
|
|
|
expect(ids).not.toContain('history');
|
|
});
|
|
});
|
|
|
|
describe('the nearby-schools nav item', () => {
|
|
const navInput = {
|
|
ofsted: null, admissions: null, admissionDistance: null,
|
|
hasLocation: true, yearlyDataLength: 1,
|
|
};
|
|
|
|
it('appears on both templates when the section renders', () => {
|
|
const primary = computeSchoolFlags(primaryFixture);
|
|
const secondary = computeSecondaryFlags(secondaryFixture);
|
|
const input = { ...navInput, hasNearbySchools: true };
|
|
|
|
expect(buildNavItems(primary, input).map((i) => i.id)).toContain('nearby');
|
|
expect(buildSecondaryNavItems(secondary, input).map((i) => i.id)).toContain('nearby');
|
|
});
|
|
|
|
it('is absent when the section does not render', () => {
|
|
const primary = computeSchoolFlags(primaryFixture);
|
|
const secondary = computeSecondaryFlags(secondaryFixture);
|
|
const input = { ...navInput, hasNearbySchools: false };
|
|
|
|
expect(buildNavItems(primary, input).map((i) => i.id)).not.toContain('nearby');
|
|
expect(buildSecondaryNavItems(secondary, input).map((i) => i.id)).not.toContain('nearby');
|
|
});
|
|
|
|
it('is absent when nothing says either way', () => {
|
|
const primary = computeSchoolFlags(primaryFixture);
|
|
expect(buildNavItems(primary, navInput).map((i) => i.id)).not.toContain('nearby');
|
|
});
|
|
|
|
it('comes last, because the section renders last', () => {
|
|
const primary = computeSchoolFlags(primaryFixture);
|
|
const ids = buildNavItems(primary, { ...navInput, hasNearbySchools: true }).map((i) => i.id);
|
|
expect(ids[ids.length - 1]).toBe('nearby');
|
|
});
|
|
});
|