diff --git a/nextjs-app/__tests__/components/SpecialSchoolNote.test.tsx b/nextjs-app/__tests__/components/SpecialSchoolNote.test.tsx new file mode 100644 index 0000000..62c7f25 --- /dev/null +++ b/nextjs-app/__tests__/components/SpecialSchoolNote.test.tsx @@ -0,0 +1,33 @@ +/** + * SpecialSchoolNote Component Tests + */ + +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import { SpecialSchoolNote } from '@/components/SpecialSchoolNote'; + +describe('SpecialSchoolNote', () => { + it('renders nothing for a mainstream school', () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); + + it('describes a special school as teaching pupils with SEND', () => { + render(); + expect(screen.getByText(/This is a special school/i)).toBeInTheDocument(); + expect(screen.getByText(/special educational needs/i)).toBeInTheDocument(); + }); + + it('does NOT call PRU pupils SEND — they are educated outside a mainstream school', () => { + render(); + expect(screen.getByText(/This is a pupil referral unit/i)).toBeInTheDocument(); + expect(screen.getByText(/educated outside a mainstream school/i)).toBeInTheDocument(); + expect(screen.queryByText(/special educational needs/i)).not.toBeInTheDocument(); + }); + + it('does NOT call alternative-provision pupils SEND', () => { + render(); + expect(screen.getByText(/This is an alternative provision setting/i)).toBeInTheDocument(); + expect(screen.queryByText(/special educational needs/i)).not.toBeInTheDocument(); + }); +}); diff --git a/nextjs-app/components/SchoolDetailView.module.css b/nextjs-app/components/SchoolDetailView.module.css index cfe453b..b2b419a 100644 --- a/nextjs-app/components/SchoolDetailView.module.css +++ b/nextjs-app/components/SchoolDetailView.module.css @@ -1558,21 +1558,3 @@ .closingStrip strong { color: #8a6200; } - -/* Special-school context note: soft, informational (teal), not a warning. It - explains why the mainstream attainment measures and England comparison are - dropped for special schools / PRUs / AP. */ -.specialNote { - background: var(--bg-secondary, #f3ede4); - border-left: 4px solid var(--accent-teal, #2d7d7d); - border-radius: 0 6px 6px 0; - padding: 0.7rem 1rem; - margin: 0 0 1rem; - font-size: 0.9rem; - line-height: 1.5; - color: var(--text-secondary, #5c564d); - max-width: 72ch; -} -.specialNote strong { - color: var(--text-primary, #1a1612); -} diff --git a/nextjs-app/components/SchoolDetailView.tsx b/nextjs-app/components/SchoolDetailView.tsx index 99f7788..6038f4f 100644 --- a/nextjs-app/components/SchoolDetailView.tsx +++ b/nextjs-app/components/SchoolDetailView.tsx @@ -21,6 +21,7 @@ import { formatPercentage, formatProgress, formatAcademicYear, isProposedToClose, ofstedLegacyAreas, isSpecialSchool, } from '@/lib/utils'; import { DeltaChip } from './DeltaChip'; +import { SpecialSchoolNote } from './SpecialSchoolNote'; import { summariseAdmissions } from '@/lib/compareLogic'; const PerformanceChart = dynamic( @@ -656,20 +657,10 @@ export function SchoolDetailView({ : 'End-of-primary-school tests taken by Year 6 pupils. England averages shown for comparison.'}

- {/* Special schools sit the same national assessments but teach pupils - with SEND, so very few reach the mainstream "expected standard". - Explain that up front and drop the England comparison below, so a - 0% headline never reads as a failing grade against a benchmark - that doesn't fit the school. */} - {isSpecial && ( -
- This is a special school. Its pupils have special educational - needs and work towards individual targets. They sit the same national tests, but - very few reach the mainstream “expected standard” these measures report, - so a comparison with the England average isn’t a meaningful guide to the - school. Where available, the progress pupils make (below) is a fairer measure. -
- )} + {/* Explains up front why the England comparison is dropped below, so + a 0% headline never reads as a failing grade against a benchmark + that doesn't fit. Type-aware copy (special vs PRU vs AP). */} + {/* ── Primary / KS2 content ── */} {hasKS2Results && ( diff --git a/nextjs-app/components/SchoolRow.tsx b/nextjs-app/components/SchoolRow.tsx index dbafd05..16e73ad 100644 --- a/nextjs-app/components/SchoolRow.tsx +++ b/nextjs-app/components/SchoolRow.tsx @@ -38,24 +38,22 @@ export function SchoolRow({ school.religious_denomination && school.religious_denomination !== 'Does not apply'; - // Special schools / PRUs / AP: the mainstream RWM measure and its England - // comparison aren't a fair judgement (their pupils have SEND), so a "0% · - // −62 vs national" row misrepresents them. Also guard a placeholder all-zero - // row (every subject 0 — a special/suppressed signature), matching - // SchoolDetailView's ks2Placeholder. A genuine 0% combined (some pupils met - // individual subjects but not all three) is NOT all-zero, so it stays - // comparable and shows its real figure. + // A placeholder all-zero row (every subject 0 — a special/suppressed + // signature, matching SchoolDetailView's ks2Placeholder) isn't a real score, + // so its figure is hidden. A genuine 0% combined (some pupils met individual + // subjects but not all three) is NOT all-zero and stays shown. const rwmPlaceholder = school.rwm_expected_pct === 0 && (school.reading_expected_pct ?? 0) === 0 && (school.writing_expected_pct ?? 0) === 0 && (school.maths_expected_pct ?? 0) === 0; - const rwmComparable = - school.rwm_expected_pct != null && !rwmPlaceholder && !isSpecialSchool(school); - - // vs-national delta + // The school's OWN figure and its year-over-year trend are same-school + // measures — shown whenever there's a real value, special schools included. + const showRwmValue = school.rwm_expected_pct != null && !rwmPlaceholder; + // The vs-England delta is a mainstream benchmark: additionally dropped for + // special schools / PRUs / AP, whose pupils aren't measured against it fairly. const rwmDelta = - rwmComparable && nationalAvgRwm != null + showRwmValue && !isSpecialSchool(school) && nationalAvgRwm != null ? Math.round((school.rwm_expected_pct as number) - nationalAvgRwm) : null; @@ -102,9 +100,9 @@ export function SchoolRow({
- {rwmComparable ? formatPercentage(school.rwm_expected_pct, 0) : '—'} + {showRwmValue ? formatPercentage(school.rwm_expected_pct, 0) : '—'} - {rwmComparable && school.prev_rwm_expected_pct != null && ( + {showRwmValue && school.prev_rwm_expected_pct != null && ( - {isSpecial && ( -
- This is a special school. Its pupils have special educational - needs and work towards individual targets. They sit the same GCSEs, but their - headline attainment is far below the mainstream average by design, so a comparison - with the England average isn’t a meaningful guide to the school. -
- )} + {p8Suspended && (
diff --git a/nextjs-app/components/SecondarySchoolRow.tsx b/nextjs-app/components/SecondarySchoolRow.tsx index 0477d36..e10ca0a 100644 --- a/nextjs-app/components/SecondarySchoolRow.tsx +++ b/nextjs-app/components/SecondarySchoolRow.tsx @@ -55,15 +55,14 @@ export function SecondarySchoolRow({ const ofstedBadge = buildOfstedListBadge(school); const phase = getPhaseStyle(school.phase); const att8 = school.attainment_8_score; - // Special schools / PRUs / AP: Attainment 8 vs the LA average isn't a fair - // comparison (their pupils have SEND), so drop the delta and the number - // rather than show them trailing "the average" by design. Attainment 8 is a - // single 0–80 score with no subject breakdown to test for a placeholder, so - // this keys off establishment type only — a genuine (if extreme) 0.0 at a - // mainstream school still shows its real value. - const att8Comparable = att8 != null && !isSpecialSchool(school); + // The school's own Attainment 8 is a same-school figure — shown whenever it + // exists (special schools included; their type tag on line 2 gives context). + // Only the vs-LA-average delta, a benchmark comparison, is dropped for + // special schools / PRUs / AP, whose pupils aren't measured against it fairly. const laDelta = - att8Comparable && laAvgAttainment8 != null ? (att8 as number) - laAvgAttainment8 : null; + att8 != null && !isSpecialSchool(school) && laAvgAttainment8 != null + ? att8 - laAvgAttainment8 + : null; const admissionsTag = detectAdmissionsTag(school); const sixthForm = hasSixthForm(school); @@ -113,7 +112,7 @@ export function SecondarySchoolRow({
- {att8Comparable ? (att8 as number).toFixed(1) : '—'} + {att8 != null ? att8.toFixed(1) : '—'} Attainment 8 diff --git a/nextjs-app/components/SpecialSchoolNote.module.css b/nextjs-app/components/SpecialSchoolNote.module.css new file mode 100644 index 0000000..138b39e --- /dev/null +++ b/nextjs-app/components/SpecialSchoolNote.module.css @@ -0,0 +1,16 @@ +/* Special-school / PRU / AP context note: soft, informational (teal), not a + warning. Shared by both detail views so the styling can't drift. */ +.note { + background: var(--bg-secondary, #f3ede4); + border-left: 4px solid var(--accent-teal, #2d7d7d); + border-radius: 0 6px 6px 0; + padding: 0.7rem 1rem; + margin: 0 0 1rem; + font-size: 0.9rem; + line-height: 1.5; + color: var(--text-secondary, #5c564d); + max-width: 72ch; +} +.note strong { + color: var(--text-primary, #1a1612); +} diff --git a/nextjs-app/components/SpecialSchoolNote.tsx b/nextjs-app/components/SpecialSchoolNote.tsx new file mode 100644 index 0000000..f737328 --- /dev/null +++ b/nextjs-app/components/SpecialSchoolNote.tsx @@ -0,0 +1,58 @@ +/** + * SpecialSchoolNote — the context note shown on special-school / PRU / AP + * detail pages explaining why the mainstream England-average comparison is + * dropped. Renders nothing for mainstream schools. + * + * The copy is type-aware: only genuine special schools have pupils with special + * educational needs. Pupil referral units and alternative provision teach + * pupils educated outside a mainstream setting (e.g. after exclusion, or for + * medical reasons) who are not necessarily SEND — so their note says so rather + * than mischaracterising them. Suppressing the England comparison is reasonable + * for all three. + */ + +import { isSpecialSchool } from '@/lib/utils'; +import styles from './SpecialSchoolNote.module.css'; + +type SpecialKind = 'special' | 'pru' | 'ap'; + +function specialKind(schoolType: string | null | undefined): SpecialKind { + const t = (schoolType ?? '').toLowerCase(); + if (/pupil referral/.test(t)) return 'pru'; + if (/alternative provision/.test(t)) return 'ap'; + return 'special'; +} + +export function SpecialSchoolNote({ school }: { school: { school_type?: string | null } }) { + if (!isSpecialSchool(school)) return null; + const kind = specialKind(school.school_type); + + return ( +
+ {kind === 'special' && ( + <> + This is a special school. Its pupils have special educational needs and + work towards individual targets. They sit the same national assessments, but very few + reach the mainstream “expected standard” these measures report — so a + comparison with the England average isn’t a meaningful guide to the school. + + )} + {kind === 'pru' && ( + <> + This is a pupil referral unit. It teaches pupils educated outside a + mainstream school — for example after exclusion, or for medical or behavioural reasons. + The mainstream “expected standard” and the England-average comparison + aren’t a meaningful guide to the school. + + )} + {kind === 'ap' && ( + <> + This is an alternative provision setting. It teaches pupils educated + outside a mainstream school. The mainstream “expected standard” and the + England-average comparison aren’t a meaningful guide to the school. + + )}{' '} + Where available, the progress its pupils make is a fairer measure. +
+ ); +}