diff --git a/nextjs-app/__tests__/components/CompareOfsted.test.tsx b/nextjs-app/__tests__/components/CompareOfsted.test.tsx index 039e22a..a142a82 100644 --- a/nextjs-app/__tests__/components/CompareOfsted.test.tsx +++ b/nextjs-app/__tests__/components/CompareOfsted.test.tsx @@ -69,6 +69,18 @@ const data: Record = { }; describe('CompareOfsted', () => { + it('words a school without an Ofsted record from the evidence (audit H4)', () => { + const isi = { ...school(4, 'ISI School'), inspectorate: 'ISI' } as School; + const listed = { ...school(5, 'New Academy'), ofsted_listed: true } as School; + render(); + expect(screen.getAllByText('Inspected by ISI').length).toBeGreaterThan(0); + expect(screen.getAllByText('Not yet inspected').length).toBeGreaterThan(0); + expect(screen.queryByText(/No inspection outcome in our dataset/)).toBeNull(); + }); + it('renders the three regimes without inventing an overall grade for report cards', () => { render(); diff --git a/nextjs-app/__tests__/components/InspectionNotice.test.tsx b/nextjs-app/__tests__/components/InspectionNotice.test.tsx new file mode 100644 index 0000000..d0adc82 --- /dev/null +++ b/nextjs-app/__tests__/components/InspectionNotice.test.tsx @@ -0,0 +1,20 @@ +import { render, screen } from '@testing-library/react'; +import { InspectionNotice } from '@/components/school/InspectionNotice'; + +it('names ISI and links its reports for an ISI school (audit H4)', () => { + render(); + expect(screen.getByText('Inspected by the Independent Schools Inspectorate (ISI)')).toBeInTheDocument(); + expect(screen.getByRole('link', { name: /ISI reports/ })).toHaveAttribute('href', 'https://www.isi.net/reports/'); +}); + +it('says "Not yet inspected" for a school Ofsted lists without an inspection', () => { + render(); + expect(screen.getByText('Not yet inspected')).toBeInTheDocument(); + expect(screen.getByRole('link', { name: /Ofsted reports/ })).toHaveAttribute( + 'href', 'https://reports.ofsted.gov.uk/inspection-reports/find-inspection-report/provider/ELS/150031'); +}); + +it('renders nothing without inspection data, including an unknown listing', () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); +}); diff --git a/nextjs-app/__tests__/components/OfstedSection.test.tsx b/nextjs-app/__tests__/components/OfstedSection.test.tsx index 3f16072..f4fec22 100644 --- a/nextjs-app/__tests__/components/OfstedSection.test.tsx +++ b/nextjs-app/__tests__/components/OfstedSection.test.tsx @@ -82,3 +82,14 @@ it('prints an ungraded outcome when there is no grade (Oakgrove)', () => { expect(screen.getByText('No overall grade')).toBeInTheDocument(); expect(screen.getByText(/Ungraded inspection, 13 November 2024: Standards maintained/)).toBeInTheDocument(); }); + +it('labels a further education record and prints its short inspection (audit H4)', () => { + renderSection({ + framework: 'Further education and skills', + current_grade: { grade: 2, date: '2019-03-05', basis: 'graded' }, + latest_visit: { date: '2023-02-01', kind: 'short', outcome: null }, + }); + expect(screen.getByText('Further education and skills')).toBeInTheDocument(); + expect(screen.getByText('Good')).toBeInTheDocument(); + expect(screen.getByText('Latest visit: Short inspection, 1 February 2023')).toBeInTheDocument(); +}); diff --git a/nextjs-app/__tests__/lib/compareLogic.test.ts b/nextjs-app/__tests__/lib/compareLogic.test.ts index ce217a4..9f04bb9 100644 --- a/nextjs-app/__tests__/lib/compareLogic.test.ts +++ b/nextjs-app/__tests__/lib/compareLogic.test.ts @@ -19,6 +19,7 @@ import { summariseAdmissions, summariseReportCard, verdict, + noInspectionLabel, } from '@/lib/compareLogic'; import type { OfstedInspection, SchoolAdmissions } from '@/lib/types'; @@ -359,3 +360,14 @@ describe('basket limits per comparison group', () => { expect(kept).toEqual([0, 1, 2, 3, 4, 7, 8, 9]); }); }); + +it('shows an FE report card with only a safeguarding result as a report card (audit H4)', () => { + const display = ofstedDisplay(ofsted({ rc_inspection_date: '2026-05-12', rc_safeguarding_met: true, report_card: {} })); + expect(display.kind).toBe('report_card'); +}); + +it('words the no-record compare cell from the evidence', () => { + expect(noInspectionLabel({ inspectorate: 'ISI' })).toBe('Inspected by ISI'); + expect(noInspectionLabel({ ofsted_listed: true })).toBe('Not yet inspected'); + expect(noInspectionLabel({ ofsted_listed: null })).toBe('No inspection in our dataset'); +}); diff --git a/nextjs-app/components/compare/CompareAtAGlance.tsx b/nextjs-app/components/compare/CompareAtAGlance.tsx index 446742e..c66f645 100644 --- a/nextjs-app/components/compare/CompareAtAGlance.tsx +++ b/nextjs-app/components/compare/CompareAtAGlance.tsx @@ -14,6 +14,7 @@ import { summariseAdmissions, verdict, type ReportCardSummary, + noInspectionLabel, } from '@/lib/compareLogic'; import type { Benchmarks, ComparisonData, NationalAverages, School } from '@/lib/types'; import { Cell, Chip, Measure, Section, SectionGrid, sectionStyles as s } from './sectionShared'; @@ -91,7 +92,7 @@ export function CompareAtAGlance({ No overall grade )} - {display.kind === 'none' && No inspection in our dataset} + {display.kind === 'none' && {noInspectionLabel(data[String(school.urn)]?.school_info ?? school)}} ); })} diff --git a/nextjs-app/components/compare/CompareOfsted.tsx b/nextjs-app/components/compare/CompareOfsted.tsx index 16135b4..8a267cc 100644 --- a/nextjs-app/components/compare/CompareOfsted.tsx +++ b/nextjs-app/components/compare/CompareOfsted.tsx @@ -12,6 +12,7 @@ import { ofstedDisplay, rcAreaLabel, type OfstedDisplay, + noInspectionLabel, } from '@/lib/compareLogic'; import { gradeSourceLine } from '@/lib/ofstedStatus'; import type { ComparisonData, OfstedCurrentGrade, OfstedInspection, School } from '@/lib/types'; @@ -41,9 +42,11 @@ function yearsSince(iso: string | null): number | null { return (Date.now() - d.getTime()) / (365.25 * 24 * 3600 * 1000); } -function ResultCell({ display, current }: { display: OfstedDisplay; current: OfstedCurrentGrade | null }) { +function ResultCell({ display, current, school }: { + display: OfstedDisplay; current: OfstedCurrentGrade | null; school: School; +}) { if (display.kind === 'none') { - return No inspection outcome in our dataset; + return {noInspectionLabel(school)}; } if (display.kind === 'report_card') { return ( @@ -175,7 +178,7 @@ export function CompareOfsted({ {schools.map((school, i) => ( - + ))} @@ -213,7 +216,7 @@ export function CompareOfsted({ schoolName={school.school_name} /> ) : ( - No inspection in our dataset + {noInspectionLabel(data[String(school.urn)]?.school_info ?? school)} )} ); @@ -228,7 +231,7 @@ export function CompareOfsted({ {schools.map((school, i) => { const url = data[String(school.urn)]?.ofsted?.ofsted_page_url ?? - `https://reports.ofsted.gov.uk/provider/21/${school.urn}`; + `https://reports.ofsted.gov.uk/inspection-reports/find-inspection-report/provider/ELS/${school.urn}`; return ( {/* Short visible label to save space on mobile (the coloured diff --git a/nextjs-app/components/school/InspectionNotice.tsx b/nextjs-app/components/school/InspectionNotice.tsx new file mode 100644 index 0000000..e7b9c16 --- /dev/null +++ b/nextjs-app/components/school/InspectionNotice.tsx @@ -0,0 +1,46 @@ +/** + * InspectionNotice — the school page's inspection section when there is no + * Ofsted inspection record (audit H4): the inspectorate GIAS names (ISI), or + * Ofsted's own "not yet inspected" when its data lists the school. Nothing + * otherwise: no claim the data cannot support. Server component. + */ + +import type { School } from '@/lib/types'; +import { Section, sectionStyles as styles } from './sectionShared'; + +const OFSTED_REPORTS = 'https://reports.ofsted.gov.uk/inspection-reports/find-inspection-report/provider/ELS/'; +const ISI_REPORTS = 'https://www.isi.net/reports/'; + +export function InspectionNotice({ school }: { school: Pick }) { + if (school.inspectorate === 'ISI') { + return ( +
+

+ Inspection + + ISI reports ↗ + +

+

Inspected by the Independent Schools Inspectorate (ISI)

+
+ ); + } + if (school.ofsted_listed) { + return ( +
+

+ Ofsted Rating + + Ofsted reports ↗ + +

+
+ Not yet inspected +
+
+ ); + } + return null; +} diff --git a/nextjs-app/components/school/OfstedSection.tsx b/nextjs-app/components/school/OfstedSection.tsx index e393ca3..f739349 100644 --- a/nextjs-app/components/school/OfstedSection.tsx +++ b/nextjs-app/components/school/OfstedSection.tsx @@ -12,7 +12,7 @@ */ import { ofstedDisplay } from '@/lib/compareLogic'; -import { formatOfstedDate, gradeSourceLine, latestVisitLine, showLatestVisitLine } from '@/lib/ofstedStatus'; +import { FE_FRAMEWORK, formatOfstedDate, gradeSourceLine, latestVisitLine, showLatestVisitLine } from '@/lib/ofstedStatus'; import type { OfstedInspection } from '@/lib/types'; import { Section, sectionStyles as styles } from './sectionShared'; @@ -71,6 +71,10 @@ export function OfstedSection({ + {ofsted.framework === FE_FRAMEWORK && ( +

{FE_FRAMEWORK}

+ )} + {isReportCard ? ( /* ── New Report Card layout ── */ <> diff --git a/nextjs-app/components/school/PrimarySchoolSections.tsx b/nextjs-app/components/school/PrimarySchoolSections.tsx index 623027b..66069bd 100644 --- a/nextjs-app/components/school/PrimarySchoolSections.tsx +++ b/nextjs-app/components/school/PrimarySchoolSections.tsx @@ -19,6 +19,8 @@ import type { import { ofstedLegacyAreas } from '@/lib/utils'; import type { SchoolFlags } from '@/lib/schoolSections'; import { OfstedSection } from './OfstedSection'; +import { InspectionNotice } from './InspectionNotice'; +import { hasReportCard } from '@/lib/ofstedStatus'; import { ResultsSection } from './ResultsSection'; import { AdmissionsSection } from './AdmissionsSection'; import { DistanceSection } from './DistanceSection'; @@ -54,7 +56,8 @@ export function PrimarySchoolSections({ const primaryAvg = nationalAvg?.primary ?? {}; const secondaryAvg = nationalAvg?.secondary ?? {}; - const isReportCard = !!(ofsted?.report_card && Object.keys(ofsted.report_card).length > 0); + // By date as well as area labels: an FE report card has only a safeguarding result (audit H4). + const isReportCard = !!ofsted && hasReportCard(ofsted); // Report cards are dated by their own inspection (rc_inspection_date); // anything else by the school's latest visit, never the older inspection a // grade may come from. @@ -71,7 +74,7 @@ export function PrimarySchoolSections({ return ( <> - {ofsted && ( + {ofsted ? ( - )} + ) : } {flags.hasAnyResults && flags.latestResults && ( 0); + // By date as well as area labels: an FE report card has only a safeguarding result (audit H4). + const isReportCard = !!ofsted && hasReportCard(ofsted); const ofstedInspectedDate = isReportCard ? ofsted?.rc_inspection_date ?? null : ofsted?.latest_visit?.date ?? null; @@ -74,7 +77,7 @@ export function SecondarySchoolSections({ return (
- {ofsted && ( + {ofsted ? ( - )} + ) : } {flags.hasResults && flags.latestResults && ( ): string { + if (school.inspectorate === 'ISI') return 'Inspected by ISI'; + if (school.ofsted_listed) return 'Not yet inspected'; + return 'No inspection in our dataset'; +} + export function ofstedDisplay( ofsted: OfstedInspection | null | undefined, ): OfstedDisplay { @@ -111,7 +122,7 @@ export function ofstedDisplay( // A report card is the newest inspection format; when present it wins — // never derive or prefer an overall grade alongside it. - if (ofsted.report_card && Object.keys(ofsted.report_card).length > 0) { + if (hasReportCard(ofsted)) { return { kind: 'report_card', summary: summariseReportCard(ofsted) }; } diff --git a/nextjs-app/lib/types.ts b/nextjs-app/lib/types.ts index d90adb7..87bde48 100644 --- a/nextjs-app/lib/types.ts +++ b/nextjs-app/lib/types.ts @@ -97,7 +97,8 @@ export interface School { // ============================================================================ export interface OfstedInspection { - framework: 'OEIF' | 'ReportCard' | null; + /** Ofsted's event grouping (e.g. "Schools - S5"), or "Further education and skills" for FE records (audit H4). */ + framework: string | null; inspection_date: string | null; /** Start date of the report-card inspection itself (Nov 2025+); null otherwise. */ rc_inspection_date?: string | null;