diff --git a/nextjs-app/__tests__/components/OfstedSection.test.tsx b/nextjs-app/__tests__/components/OfstedSection.test.tsx new file mode 100644 index 0000000..3f16072 --- /dev/null +++ b/nextjs-app/__tests__/components/OfstedSection.test.tsx @@ -0,0 +1,84 @@ +import { render, screen } from '@testing-library/react'; +import { OfstedSection } from '@/components/school/OfstedSection'; +import { ofstedLegacyAreas } from '@/lib/utils'; +import type { OfstedInspection } from '@/lib/types'; + +const empty = { + 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, sixth_form_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, report_url: null, + report_card: {}, +} as OfstedInspection; + +// `page` mimics how each school page used to mount the section; the secondary +// page's branch hard-coded four areas and dropped the sixth form (audit M2). +function renderSection(o: Partial, page: Record = {}) { + const ofsted = { ...empty, ...o } as OfstedInspection; + render( + , + ); +} + +it('shows no overall grade, not an older one, after a no-grade inspection (Rabbsfarm)', () => { + renderSection({ + inspection_date: '2025-06-17', quality_of_education: 3, behaviour_attitudes: 3, + personal_development: 2, leadership_management: 3, early_years_provision: 2, + current_grade: null, latest_visit: { date: '2025-06-17', kind: 'graded', outcome: null }, + }); + expect(screen.getByText(/Inspected 17 June 2025/)).toBeInTheDocument(); + expect(screen.getByText('No overall grade')).toBeInTheDocument(); + expect(screen.queryByText('Good', { selector: 'span' })).not.toBeInTheDocument(); + expect(screen.queryByText('Not rated')).not.toBeInTheDocument(); + expect(screen.getAllByText('Requires Improvement')).toHaveLength(3); +}); + +it('keeps the sixth-form judgement when there is no overall grade (audit M2)', () => { + renderSection({ + inspection_date: '2025-04-01', quality_of_education: 1, behaviour_attitudes: 1, + personal_development: 1, leadership_management: 1, sixth_form_provision: 2, + current_grade: null, latest_visit: { date: '2025-04-01', kind: 'graded', outcome: null }, + }, { variant: 'secondary' }); + expect(screen.getByText(/sixth form/i)).toBeInTheDocument(); + expect(screen.getByText('Good')).toBeInTheDocument(); +}); + +it('dates a grade by its source and prints a later visit separately (Washwood Heath)', () => { + renderSection({ + inspection_date: '2020-03-03', overall_effectiveness: 2, quality_of_education: 2, + current_grade: { grade: 2, date: '2020-03-03', basis: 'graded' }, + latest_visit: { date: '2025-05-21', kind: 'ungraded', outcome: 'Standards maintained' }, + }); + expect(screen.getByText(/Inspected 21 May 2025/)).toBeInTheDocument(); + expect(screen.getByText('Graded inspection, 3 March 2020')).toBeInTheDocument(); + expect(screen.getByText(/Latest visit: Ungraded inspection, 21 May 2025: Standards maintained/)).toBeInTheDocument(); +}); + +it('says a grade was confirmed at an ungraded visit (Robins Lane)', () => { + renderSection({ + inspection_date: '2020-01-07', overall_effectiveness: 2, + current_grade: { grade: 2, date: '2024-07-18', basis: 'confirmed' }, + latest_visit: { date: '2024-07-18', kind: 'ungraded', outcome: 'School remains Good' }, + }); + expect(screen.getByText('Confirmed at an ungraded inspection, 18 July 2024')).toBeInTheDocument(); + expect(screen.queryByText(/Latest visit/)).not.toBeInTheDocument(); +}); + +it('prints an ungraded outcome when there is no grade (Oakgrove)', () => { + renderSection({ + current_grade: null, + latest_visit: { date: '2024-11-13', kind: 'ungraded', outcome: 'Standards maintained' }, + }); + expect(screen.getByText('No overall grade')).toBeInTheDocument(); + expect(screen.getByText(/Ungraded inspection, 13 November 2024: Standards maintained/)).toBeInTheDocument(); +}); diff --git a/nextjs-app/__tests__/support/schoolFixtures.ts b/nextjs-app/__tests__/support/schoolFixtures.ts index 7eb4c55..332a3cf 100644 --- a/nextjs-app/__tests__/support/schoolFixtures.ts +++ b/nextjs-app/__tests__/support/schoolFixtures.ts @@ -191,7 +191,8 @@ export const primaryFixture = { personal_development: 2, leadership_management: 2, previous_overall: 3, - grade_source: 'graded', + current_grade: { grade: 2, date: '2023-05-17', basis: 'graded' }, + latest_visit: { date: '2023-05-17', kind: 'graded', outcome: null }, }), census, admissions: makeAdmissions({ year: 2024 }), @@ -295,7 +296,8 @@ export const allThroughFixture = { behaviour_attitudes: 1, personal_development: 1, leadership_management: 1, - grade_source: 'graded', + current_grade: { grade: 1, date: '2022-10-04', basis: 'graded' }, + latest_visit: { date: '2022-10-04', kind: 'graded', outcome: null }, }), census, admissions: makeAdmissions({ year: 2024, school_phase: 'Secondary' }), diff --git a/nextjs-app/components/school/OfstedSection.tsx b/nextjs-app/components/school/OfstedSection.tsx index 7d2d202..e393ca3 100644 --- a/nextjs-app/components/school/OfstedSection.tsx +++ b/nextjs-app/components/school/OfstedSection.tsx @@ -1,16 +1,18 @@ /** * OfstedSection — shared between the primary and secondary detail pages. * - * The two versions were ~80% identical, but that figure masked a real fork in - * the no-overall-grade case: the primary page shows a "Not rated" badge, while - * the secondary page shows a four-area OEIF panel. The disclaimer copy also - * differs slightly. Both are preserved exactly via the `variant` prop rather - * than reconciled, because this refactor must not change either page. Merging - * them is a follow-up decision for a human, not a side effect of a move. + * The headline is the school's current Ofsted status (backend: + * fact_ofsted_latest): the report card; or the overall grade still in force, + * with the inspection that awarded or confirmed it; or "No overall grade". + * A later visit that is not the grade's source gets its own line. Both pages + * render the same branches, so neither drops an area judgement (audit M2). + * Rule: docs/superpowers/specs/2026-10-05-ofsted-current-status-design.md * * Server component. */ +import { ofstedDisplay } from '@/lib/compareLogic'; +import { formatOfstedDate, gradeSourceLine, latestVisitLine, showLatestVisitLine } from '@/lib/ofstedStatus'; import type { OfstedInspection } from '@/lib/types'; import { Section, sectionStyles as styles } from './sectionShared'; @@ -40,14 +42,13 @@ export interface OfstedSectionProps { ofstedInspectedDate: string | null; oeifAllSameGrade: boolean; oeifAreas: { label: string; value: number }[]; - variant?: 'primary' | 'secondary'; } export function OfstedSection({ ofsted, urn, isReportCard, ofstedInspectedDate, - oeifAllSameGrade, oeifAreas, variant = 'primary', + oeifAllSameGrade, oeifAreas, }: OfstedSectionProps) { - const isSecondary = variant === 'secondary'; + const display = ofstedDisplay(ofsted); return (
@@ -55,7 +56,7 @@ export function OfstedSection({ {isReportCard ? 'Ofsted Report Card' : 'Ofsted Rating'} {ofstedInspectedDate && ( - {isSecondary && ' '}Inspected {new Date(ofstedInspectedDate).toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })} + Inspected {new Date(ofstedInspectedDate).toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })} )} - ) : (!isSecondary || ofsted.overall_effectiveness) ? ( - /* ── Old OEIF layout ── */ + ) : ( <>
- - {ofsted.overall_effectiveness ? OFSTED_LABELS[ofsted.overall_effectiveness] : 'Not rated'} - - {ofsted.previous_overall != null && - ofsted.previous_overall !== ofsted.overall_effectiveness && ( - - Previously: {OFSTED_LABELS[ofsted.previous_overall]} - + {display.kind === 'graded' || display.kind === 'confirmed' ? ( + <> + + {display.gradeLabel} + + {ofsted.previous_overall != null && ofsted.previous_overall !== display.grade && ( + + Previously: {OFSTED_LABELS[ofsted.previous_overall]} + + )} + + ) : ( + No overall grade )}

- {ofsted.grade_source === 'ungraded_carried_forward' - ? 'This overall grade is carried forward from an earlier inspection. Ofsted has since visited without issuing a new overall grade. From September 2024, Ofsted no longer makes an overall effectiveness judgement.' - : isSecondary - ? 'From September 2024, Ofsted no longer makes an overall effectiveness judgement in inspections.' - : 'From September 2024, Ofsted no longer makes an overall effectiveness judgement in inspections of state-funded schools.'} + {ofsted.current_grade + ? gradeSourceLine(ofsted.current_grade) + : 'Ofsted stopped giving overall grades in September 2024.'}

- {oeifAllSameGrade ? ( -

- Rated {OFSTED_LABELS[ofsted.overall_effectiveness!]} across all inspected areas: Quality of Teaching, Behaviour, Pupils' Development and Leadership. -

- ) : ( -
- {oeifAreas.map(({ label, value }) => ( -
-
{label}
-
- {OFSTED_LABELS[value]} -
-
- ))} -
+ {showLatestVisitLine(ofsted) && ofsted.latest_visit && ( +

Latest visit: {latestVisitLine(ofsted.latest_visit)}

)} - - ) : ( - /* ── Secondary only: inspected since Sept 2024, no overall grade ── */ - <> -

- From September 2024, Ofsted no longer gives a single overall grade. -

-
- {[ - { label: 'Quality of Education', value: ofsted.quality_of_education }, - { label: 'Behaviour & Attitudes', value: ofsted.behaviour_attitudes }, - { label: 'Personal Development', value: ofsted.personal_development }, - { label: 'Leadership & Management', value: ofsted.leadership_management }, - ].filter(({ value }) => value != null).map(({ label, value }) => ( -
-
{label}
-
- {OFSTED_LABELS[value!]} -
+ {oeifAllSameGrade && (display.kind === 'graded' || display.kind === 'confirmed') ? ( +

+ Rated {display.gradeLabel} across all inspected areas: Quality of Teaching, Behaviour, Pupils' Development and Leadership. +

+ ) : oeifAreas.length > 0 ? ( + <> + {ofsted.inspection_date && ofsted.inspection_date !== ofsted.latest_visit?.date && ( +

+ Area judgements from the graded inspection, {formatOfstedDate(ofsted.inspection_date)}. +

+ )} +
+ {oeifAreas.map(({ label, value }) => ( +
+
{label}
+
+ {OFSTED_LABELS[value]} +
+
+ ))}
- ))} -
+ + ) : null} )}
diff --git a/nextjs-app/components/school/PrimarySchoolSections.tsx b/nextjs-app/components/school/PrimarySchoolSections.tsx index 319ee86..623027b 100644 --- a/nextjs-app/components/school/PrimarySchoolSections.tsx +++ b/nextjs-app/components/school/PrimarySchoolSections.tsx @@ -55,15 +55,17 @@ export function PrimarySchoolSections({ const secondaryAvg = nationalAvg?.secondary ?? {}; const isReportCard = !!(ofsted?.report_card && Object.keys(ofsted.report_card).length > 0); - // Report cards are dated by their own inspection (rc_inspection_date), never - // the legacy inspection_date (report cards exist only from Nov 2025). + // 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. const ofstedInspectedDate = isReportCard ? ofsted?.rc_inspection_date ?? null - : ofsted?.inspection_date ?? null; + : ofsted?.latest_visit?.date ?? null; const oeifAreas = ofsted ? ofstedLegacyAreas(ofsted) : []; const oeifAllSameGrade = !!ofsted && !isReportCard && + ofsted.overall_effectiveness != null && oeifAreas.length >= 3 && oeifAreas.every((a) => a.value === ofsted.overall_effectiveness); @@ -77,7 +79,6 @@ export function PrimarySchoolSections({ ofstedInspectedDate={ofstedInspectedDate} oeifAllSameGrade={oeifAllSameGrade} oeifAreas={oeifAreas} - variant="primary" /> )} diff --git a/nextjs-app/components/school/SecondarySchoolSections.tsx b/nextjs-app/components/school/SecondarySchoolSections.tsx index 7fcbe98..9526e8d 100644 --- a/nextjs-app/components/school/SecondarySchoolSections.tsx +++ b/nextjs-app/components/school/SecondarySchoolSections.tsx @@ -63,11 +63,12 @@ export function SecondarySchoolSections({ const isReportCard = !!(ofsted?.report_card && Object.keys(ofsted.report_card).length > 0); const ofstedInspectedDate = isReportCard ? ofsted?.rc_inspection_date ?? null - : ofsted?.inspection_date ?? null; + : ofsted?.latest_visit?.date ?? null; const oeifAreas = ofsted ? ofstedLegacyAreas(ofsted) : []; const oeifAllSameGrade = !!ofsted && !isReportCard && + ofsted.overall_effectiveness != null && oeifAreas.length >= 3 && oeifAreas.every((a) => a.value === ofsted.overall_effectiveness); @@ -81,7 +82,6 @@ export function SecondarySchoolSections({ ofstedInspectedDate={ofstedInspectedDate} oeifAllSameGrade={oeifAllSameGrade} oeifAreas={oeifAreas} - variant="secondary" /> )}