diff --git a/nextjs-app/__tests__/components/LeafletMapInner.test.tsx b/nextjs-app/__tests__/components/LeafletMapInner.test.tsx index 5a47ae0..e910061 100644 --- a/nextjs-app/__tests__/components/LeafletMapInner.test.tsx +++ b/nextjs-app/__tests__/components/LeafletMapInner.test.tsx @@ -105,3 +105,20 @@ it('puts the card back when the pins are rebuilt for a reason other than the sch expect(container.querySelector('.sc-popup')).toHaveTextContent('Southmead Primary School'); expect(container.querySelectorAll('.sc-pin--selected')).toHaveLength(1); }); + +it('compares a state secondary with its LA average on the card, but not an independent one', () => { + const state: School = { ...base, urn: 3, school_name: 'Holland Park School', school_type: 'Academy converter', + phase: 'Secondary', local_authority: 'Kensington and Chelsea', attainment_8_score: 60, + latitude: 51.5, longitude: -0.2, distance: 0.3 }; + const independent: School = { ...state, urn: 4, school_name: 'Abbey Gate College', + school_type: 'Other independent school', attainment_8_score: 20.4 }; + const laAverages = { 'Kensington and Chelsea': 54.5 }; + + const { container, rerender } = renderMap({ schools: [state, independent], laAverages, selectedUrn: 3 }); + expect(container.querySelector('.sc-popup')).toHaveTextContent('60.0 Att 8 +5.5 vs LA'); + + rerender({ schools: [state, independent], laAverages, selectedUrn: 4 }); + const card = container.querySelector('.sc-popup')!; + expect(card).toHaveTextContent('20.4 Att 8'); + expect(card).not.toHaveTextContent(/vs LA/); +}); diff --git a/nextjs-app/__tests__/components/SecondarySchoolRow.test.tsx b/nextjs-app/__tests__/components/SecondarySchoolRow.test.tsx index 9503a84..2ed339b 100644 --- a/nextjs-app/__tests__/components/SecondarySchoolRow.test.tsx +++ b/nextjs-app/__tests__/components/SecondarySchoolRow.test.tsx @@ -116,3 +116,38 @@ describe('SecondarySchoolRow shares the school page flags', () => { expect(screen.getByText('Fee-paying')).toBeInTheDocument(); }); }); + +describe('SecondarySchoolRow LA comparison', () => { + it('compares a state school with its LA average', () => { + render( + , + ); + expect(screen.getByText(/\+5\.5 vs LA avg/)).toBeInTheDocument(); + }); + + it('keeps the comparison for a school whose type is unknown', () => { + render( + , + ); + expect(screen.getByText(/\+5\.5 vs LA avg/)).toBeInTheDocument(); + }); + + it("shows an independent school's Attainment 8 without an LA comparison", () => { + // DfE's LA average covers state-funded schools; an independent's + // Attainment 8 leaves out IGCSEs (audit H2). + render( + , + ); + expect(screen.getByText('20.4')).toBeInTheDocument(); + expect(screen.queryByText(/vs LA avg/)).not.toBeInTheDocument(); + }); +}); diff --git a/nextjs-app/__tests__/lib/utils.test.ts b/nextjs-app/__tests__/lib/utils.test.ts index ab03561..ea9f983 100644 --- a/nextjs-app/__tests__/lib/utils.test.ts +++ b/nextjs-app/__tests__/lib/utils.test.ts @@ -391,3 +391,20 @@ describe('singleSexLabel', () => { expect(singleSexLabel(undefined)).toBeNull(); }); }); + +describe('isIndependentSchool', () => { + const { isIndependentSchool } = require('@/lib/utils'); + + it('matches both GIAS independent types', () => { + expect(isIndependentSchool({ school_type: 'Other independent school' })).toBe(true); + expect(isIndependentSchool({ school_type: 'Other independent special school' })).toBe(true); + }); + + it('does not match state-funded types or a missing type', () => { + for (const t of ['Academy converter', 'Community school', 'Free schools', 'Non-maintained special school']) { + expect(isIndependentSchool({ school_type: t })).toBe(false); + } + expect(isIndependentSchool({ school_type: null })).toBe(false); + expect(isIndependentSchool({})).toBe(false); + }); +}); diff --git a/nextjs-app/components/LeafletMapInner.tsx b/nextjs-app/components/LeafletMapInner.tsx index e7d1bec..ded0513 100644 --- a/nextjs-app/components/LeafletMapInner.tsx +++ b/nextjs-app/components/LeafletMapInner.tsx @@ -9,7 +9,7 @@ import { useEffect, useRef, useState } from 'react'; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; import type { School } from '@/lib/types'; -import { schoolUrl, isSpecialSchool, buildOfstedListBadge, listRwmValue } from '@/lib/utils'; +import { schoolUrl, isSpecialSchool, isIndependentSchool, buildOfstedListBadge, listRwmValue } from '@/lib/utils'; interface LeafletMapInnerProps { schools: School[]; @@ -70,7 +70,7 @@ function metricHtml(school: School, { nationalAvgRwm, laAverages }: CardContext) const score = school.attainment_8_score; const laAvg = school.local_authority ? (laAverages?.[school.local_authority] ?? null) : null; let delta = ''; - if (!special && laAvg != null) { + if (!special && !isIndependentSchool(school) && laAvg != null) { const diff = Math.round((score - laAvg) * 10) / 10; // Att8 runs 0–90 in 0.1 steps; ±0.5 is meaningful, where RWM needs ±2. const cls = diff >= 0.5 ? 'sc-up' : diff <= -0.5 ? 'sc-down' : ''; diff --git a/nextjs-app/components/SecondarySchoolRow.tsx b/nextjs-app/components/SecondarySchoolRow.tsx index 2c5181e..bc3f48e 100644 --- a/nextjs-app/components/SecondarySchoolRow.tsx +++ b/nextjs-app/components/SecondarySchoolRow.tsx @@ -11,7 +11,7 @@ 'use client'; import type { School } from '@/lib/types'; -import { buildOfstedListBadge, getPhaseStyle, schoolUrl, formatAgeRange, isProposedToClose, isSpecialSchool } from '@/lib/utils'; +import { buildOfstedListBadge, getPhaseStyle, schoolUrl, formatAgeRange, isProposedToClose, isSpecialSchool, isIndependentSchool } from '@/lib/utils'; import { schoolFlags, schoolTypeLabel } from '@/lib/schoolFacts'; import styles from './SecondarySchoolRow.module.css'; @@ -45,10 +45,12 @@ export function SecondarySchoolRow({ const att8 = school.attainment_8_score; // 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. + // Only the vs-LA-average delta, a benchmark comparison, is dropped: for + // special schools / PRUs / AP, whose pupils aren't measured against it + // fairly, and for independent schools, because DfE's LA average covers + // state-funded schools and an independent's Attainment 8 leaves out IGCSEs. const laDelta = - att8 != null && !isSpecialSchool(school) && laAvgAttainment8 != null + att8 != null && !isSpecialSchool(school) && !isIndependentSchool(school) && laAvgAttainment8 != null ? att8 - laAvgAttainment8 : null; diff --git a/nextjs-app/lib/utils.ts b/nextjs-app/lib/utils.ts index 75c873a..9d3440d 100644 --- a/nextjs-app/lib/utils.ts +++ b/nextjs-app/lib/utils.ts @@ -758,6 +758,16 @@ export function isSpecialSchool(school: { school_type?: string | null }): boolea return /\bspecial\b/.test(t) || /pupil referral/.test(t) || /alternative provision/.test(t); } +/** + * Independent (fee-paying) schools: GIAS types "Other independent school" and + * "Other independent special school". DfE's Attainment 8 for them leaves out + * IGCSEs, and DfE's LA averages cover state-funded schools only, so callers + * drop the "vs LA avg" comparison for them (audit H2). + */ +export function isIndependentSchool(school: { school_type?: string | null }): boolean { + return /\bindependent\b/i.test(school.school_type ?? ''); +} + /** * Whether GIAS records a religious character. "None", "Does not apply" and * "Not applicable" are the register's ways of saying it has none; the place