Files
school_compare/nextjs-app/__tests__/lib/compareLogic.test.ts
T
TudorandClaude Fable 5 5944d88f0b
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 17s
PR Checks / Build Frontend (no push) (pull_request) Successful in 46s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 2m53s
fix(compare): expert sign-off must-fixes — phase-matched admissions, Ofsted sentinel codes, selective-school copy
M1: admissions rounds are now selected by the active phase tab
(admissionsForPhase) — an all-through school's Year 7 round no longer
masquerades as Reception odds beside pure primaries; honest per-cell and
section fallbacks name the round (Reception / Year 7).
M2: Ofsted sentinel codes (9 = not applicable) never render as judgement
chips, and the sixth-form judgement — previously dropped — now renders
for schools that have one.
M3: 'What this means' is phase- and type-aware: selective schools get
entrance-test framing, secondary faith schools a faith-criteria note, and
the primaries' distance template never appears on the secondary tab
(admissions_policy now exposed in compare school_info).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
2026-07-17 12:40:42 +01:00

306 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* compareLogic encodes the expert-reviewed comprehension rules for the
* compare screen: report-card summarisation (safeguarding never counted),
* three-regime Ofsted display, one consistent admissions chip metric,
* CI-based progress banding, verdict chips and dot-strip geometry.
*/
import {
OFSTED_LEGACY_GRADES,
admissionsForPhase,
ofstedDisplay,
progressBand,
rcAreaLabel,
stripPositions,
summariseAdmissions,
summariseReportCard,
verdict,
} from '@/lib/compareLogic';
import type { OfstedInspection, SchoolAdmissions } from '@/lib/types';
function ofsted(partial: Partial<OfstedInspection>): OfstedInspection {
return {
framework: null,
inspection_date: null,
inspection_type: null,
overall_effectiveness: null,
quality_of_education: null,
behaviour_attitudes: null,
personal_development: null,
leadership_management: null,
early_years_provision: null,
previous_overall: null,
rc_safeguarding_met: null,
rc_inclusion: null,
rc_curriculum_teaching: null,
rc_achievement: null,
rc_attendance_behaviour: null,
rc_personal_development: null,
rc_leadership_governance: null,
rc_early_years: null,
rc_sixth_form: null,
...partial,
};
}
const REPORT_CARD = {
rc_achievement: { code: 2, label: 'Strong standard' },
rc_curriculum_teaching: { code: 2, label: 'Strong standard' },
rc_personal_development: { code: 2, label: 'Strong standard' },
rc_leadership_governance: { code: 2, label: 'Strong standard' },
rc_inclusion: { code: 3, label: 'Expected standard' },
rc_early_years: { code: 3, label: 'Expected standard' },
rc_attendance_behaviour: { code: 4, label: 'Needs attention' },
};
describe('summariseReportCard', () => {
it('counts graded areas best-first and NAMES problem areas', () => {
const s = summariseReportCard(
ofsted({ report_card: REPORT_CARD, rc_safeguarding_met: true }),
);
expect(s.counts).toEqual([
{ label: 'Strong standard', count: 4 },
{ label: 'Expected standard', count: 2 },
]);
expect(s.problems).toEqual([
{ areaLabel: 'Attendance & behaviour', label: 'Needs attention' },
]);
expect(s.safeguarding).toBe('met');
expect(s.allClear).toBe(false);
});
it('never counts safeguarding as a graded area', () => {
const s = summariseReportCard(
ofsted({
report_card: { rc_achievement: { code: 3, label: 'Expected standard' } },
rc_safeguarding_met: true,
}),
);
const total = s.counts.reduce((n, c) => n + c.count, 0);
expect(total).toBe(1);
});
it('is allClear when everything is Expected standard or better and safeguarding met', () => {
const s = summariseReportCard(
ofsted({
report_card: {
rc_achievement: { code: 3, label: 'Expected standard' },
rc_inclusion: { code: 1, label: 'Exceptional' },
},
rc_safeguarding_met: true,
}),
);
expect(s.allClear).toBe(true);
expect(s.counts[0]).toEqual({ label: 'Exceptional', count: 1 });
});
it('passes labels through from the API — never invents wording', () => {
const s = summariseReportCard(
ofsted({ report_card: { rc_inclusion: { code: 4, label: 'Needs attention' } } }),
);
expect(JSON.stringify(s)).not.toContain('Attention needed');
});
});
describe('ofstedDisplay', () => {
it('prefers the report card over any legacy grade', () => {
const d = ofstedDisplay(
ofsted({ overall_effectiveness: 2, report_card: REPORT_CARD }),
);
expect(d.kind).toBe('report_card');
});
it('distinguishes graded from carried-forward grades', () => {
const graded = ofstedDisplay(
ofsted({ overall_effectiveness: 1, grade_source: 'graded' }),
);
expect(graded).toMatchObject({ kind: 'graded', gradeLabel: 'Outstanding', carriedForward: false });
const carried = ofstedDisplay(
ofsted({ overall_effectiveness: 2, grade_source: 'ungraded_carried_forward' }),
);
expect(carried).toMatchObject({ kind: 'carried_forward', gradeLabel: 'Good', carriedForward: true });
});
it('handles missing data', () => {
expect(ofstedDisplay(null).kind).toBe('none');
expect(ofstedDisplay(ofsted({})).kind).toBe('none');
});
it('identifies transitional inspections without overall grades', () => {
const transitional = ofstedDisplay(
ofsted({ overall_effectiveness: null, inspection_date: '2024-11-05' }),
);
expect(transitional.kind).toBe('transitional');
});
it('uses the four legacy grade words', () => {
expect(OFSTED_LEGACY_GRADES).toEqual({
1: 'Outstanding',
2: 'Good',
3: 'Requires improvement',
4: 'Inadequate',
});
});
});
describe('rcAreaLabel', () => {
it('maps rc keys to the mockups area labels', () => {
expect(rcAreaLabel('rc_attendance_behaviour')).toBe('Attendance & behaviour');
expect(rcAreaLabel('rc_curriculum_teaching')).toBe('Curriculum & teaching');
expect(rcAreaLabel('rc_leadership_governance')).toBe('Leadership & governance');
});
});
describe('summariseAdmissions', () => {
function admissions(partial: Partial<SchoolAdmissions>): SchoolAdmissions {
return {
year: 202627,
places_offered: null,
total_applications: null,
first_preference_offer_pct: null,
oversubscribed: null,
...partial,
};
}
it('97% → good chip with the mockup wording', () => {
const s = summariseAdmissions(
admissions({ first_preference_offer_pct: 96.98, total_applications: 457, places_offered: 180 }),
);
expect(s.chip).toEqual({ tone: 'good', text: '97% of first choices offered' });
expect(s.interest).toBe('Named on 457 forms · 180 places');
});
it('73% → warn chip "Over 1 in 4 first choices missed out"', () => {
const s = summariseAdmissions(admissions({ first_preference_offer_pct: 73.4 }));
expect(s.chip).toEqual({ tone: 'warn', text: 'Over 1 in 4 first choices missed out' });
});
it('100% → "All first choices offered"', () => {
const s = summariseAdmissions(admissions({ first_preference_offer_pct: 100 }));
expect(s.chip).toEqual({ tone: 'good', text: 'All first choices offered' });
});
it('no data → null chip and interest', () => {
const s = summariseAdmissions(null);
expect(s.chip).toBeNull();
expect(s.interest).toBeNull();
});
});
describe('admissionsForPhase', () => {
const row = (year: number, school_phase: string | null): SchoolAdmissions =>
({ year, school_phase, places_offered: 100, total_applications: 200, first_preference_offer_pct: 80 }) as SchoolAdmissions;
it('returns the latest round matching the active phase', () => {
const data = {
admissions: row(202627, 'Secondary'),
admissions_history: [row(202526, 'Secondary'), row(202526, 'Primary'), row(202425, 'Primary')],
};
expect(admissionsForPhase(data, true)?.year).toBe(202627);
expect(admissionsForPhase(data, false)?.year).toBe(202526);
expect(admissionsForPhase(data, false)?.school_phase).toBe('Primary');
});
it("never substitutes the other phase's round (all-through with Year 7 data only)", () => {
const data = {
admissions: row(202627, 'Secondary'),
admissions_history: [row(202526, 'Secondary')],
};
expect(admissionsForPhase(data, false)).toBeNull();
expect(admissionsForPhase(data, true)?.year).toBe(202627);
});
it('uses untagged legacy rows only when no row carries a phase', () => {
const untagged = { admissions: row(202627, null), admissions_history: [row(202526, null)] };
expect(admissionsForPhase(untagged, false)?.year).toBe(202627);
expect(admissionsForPhase(untagged, true)?.year).toBe(202627);
const mixed = { admissions: row(202627, 'Secondary'), admissions_history: [row(202526, null)] };
expect(admissionsForPhase(mixed, false)).toBeNull();
});
it('handles missing data', () => {
expect(admissionsForPhase(null, false)).toBeNull();
expect(admissionsForPhase({ admissions: null, admissions_history: [] }, true)).toBeNull();
});
});
describe('progressBand', () => {
it('CI entirely above zero → above', () => {
expect(progressBand(1.2, 0.4, 2.0)).toBe('above');
});
it('CI entirely below zero → below', () => {
expect(progressBand(-1.2, -2.0, -0.4)).toBe('below');
});
it('CI straddling zero → average', () => {
expect(progressBand(0.3, -0.5, 1.1)).toBe('average');
});
it('missing CI → null (no naive thresholding)', () => {
expect(progressBand(1.2, null, null)).toBeNull();
expect(progressBand(null, null, null)).toBeNull();
});
});
describe('verdict', () => {
it('above / close / below with a 2pp tolerance', () => {
expect(verdict(87, 62)).toBe('above');
expect(verdict(61, 62)).toBe('close');
expect(verdict(40, 62)).toBe('below');
});
});
describe('stripPositions', () => {
it('maps a custom domain', () => {
const pts = stripPositions([106], 100, 120);
expect(pts[0].pos).toBe(30);
});
it('flips a colliding label above', () => {
const pts = stripPositions([91, 92], 0, 100);
const sorted = [...pts].sort((a, b) => a.value - b.value);
expect(sorted[0].labelAbove).toBe(false);
expect(sorted[1].labelAbove).toBe(true);
});
it('skips nulls and keeps school indices', () => {
const pts = stripPositions([50, null, 70], 0, 100);
expect(pts).toHaveLength(2);
expect(pts.map((p) => p.schoolIndex)).toEqual([0, 2]);
});
it('clamps out-of-domain values', () => {
const pts = stripPositions([95], 100, 120);
expect(pts[0].pos).toBe(0);
});
});
describe('latestValues', () => {
const data = {
'1': {
yearly_data: [
{ year: 202324, rwm_expected_pct: 75 },
{ year: 202425, rwm_expected_pct: 87 },
],
},
'2': {
yearly_data: [
{ year: 202324, rwm_expected_pct: 82 },
{ year: 202425, rwm_expected_pct: null },
],
},
};
it('takes the latest non-null value per school in urn order', async () => {
const { latestValues } = await import('@/lib/compareLogic');
expect(latestValues(data, [1, 2], 'rwm_expected_pct')).toEqual([87, 82]);
});
it('returns null for unknown schools and metrics', async () => {
const { latestValues } = await import('@/lib/compareLogic');
expect(latestValues(data, [3], 'rwm_expected_pct')).toEqual([null]);
expect(latestValues(data, [1], 'nope')).toEqual([null]);
});
});