fix(search): no LA comparison for independent schools (H2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
TudorandClaude Opus 5.5 committed 2026-10-06 12:36:00 +01:00
1 parent e659867590
commit c7ddf0505d
6 files changed
+87 -6

No files matched your search

@@ -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/);
});
@@ -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(
<SecondarySchoolRow
school={{ ...base, school_type: 'Academy converter', attainment_8_score: 60 }}
laAvgAttainment8={54.5}
/>,
);
expect(screen.getByText(/\+5\.5 vs LA avg/)).toBeInTheDocument();
});
it('keeps the comparison for a school whose type is unknown', () => {
render(
<SecondarySchoolRow
school={{ ...base, school_type: null, attainment_8_score: 60 } as unknown as School}
laAvgAttainment8={54.5}
/>,
);
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(
<SecondarySchoolRow
school={{ ...base, school_type: 'Other independent school', attainment_8_score: 20.4 }}
laAvgAttainment8={54.5}
/>,
);
expect(screen.getByText('20.4')).toBeInTheDocument();
expect(screen.queryByText(/vs LA avg/)).not.toBeInTheDocument();
});
});
+17
View File
@@ -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);
});
});
+2 -2
View File
@@ -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' : '';
+6 -4
View File
@@ -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;
+10
View File
@@ -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