diff --git a/nextjs-app/__tests__/support/schoolFixtures.ts b/nextjs-app/__tests__/support/schoolFixtures.ts new file mode 100644 index 0000000..a9a7a31 --- /dev/null +++ b/nextjs-app/__tests__/support/schoolFixtures.ts @@ -0,0 +1,332 @@ +/** + * Fixtures for the school detail characterization tests. + * + * Four shapes, each pinning a branch the detail views take: + * primary — KS2 results, legacy OEIF Ofsted, 3 years of admissions + * (enough for the year/trend toggle) + * secondary — KS4 results, Report Card Ofsted, 1 admissions year + * (no toggle) + * allThrough — BOTH KS2 and KS4, so the primary content is gated back on + * special — a special school with an all-zero KS2 row, where the + * England comparison must be suppressed + * + * Field names come from lib/types.ts. If TypeScript rejects something here, + * the fixture is wrong — never widen the type to accommodate a fixture. + */ + +import type { + School, SchoolResult, AbsenceData, OfstedInspection, SchoolCensus, + SchoolAdmissions, SchoolDeprivation, SchoolFinance, NationalAverages, +} from '@/lib/types'; + +// --------------------------------------------------------------------------- +// Builders — SchoolResult has 57 required nullable fields, so every fixture +// starts from an all-null row and overrides only what it is testing. +// --------------------------------------------------------------------------- + +const emptyResult: SchoolResult = { + school_id: 1, + year: 2024, + total_pupils: null, eligible_pupils: null, + rwm_expected_pct: null, reading_expected_pct: null, writing_expected_pct: null, + maths_expected_pct: null, gps_expected_pct: null, science_expected_pct: null, + rwm_high_pct: null, reading_high_pct: null, writing_high_pct: null, + maths_high_pct: null, gps_high_pct: null, + reading_progress: null, writing_progress: null, maths_progress: null, + reading_avg_score: null, maths_avg_score: null, gps_avg_score: null, + disadvantaged_pct: null, eal_pct: null, sen_support_pct: null, sen_ehcp_pct: null, + stability_pct: null, + reading_absence_pct: null, gps_absence_pct: null, maths_absence_pct: null, + writing_absence_pct: null, science_absence_pct: null, + rwm_expected_boys_pct: null, rwm_expected_girls_pct: null, + rwm_high_boys_pct: null, rwm_high_girls_pct: null, + rwm_expected_disadvantaged_pct: null, rwm_expected_non_disadvantaged_pct: null, + disadvantaged_gap: null, + rwm_expected_3yr_pct: null, reading_avg_3yr: null, maths_avg_3yr: null, + attainment_8_score: null, progress_8_score: null, + progress_8_lower_ci: null, progress_8_upper_ci: null, + progress_8_english: null, progress_8_maths: null, + progress_8_ebacc: null, progress_8_open: null, + english_maths_strong_pass_pct: null, english_maths_standard_pass_pct: null, + ebacc_entry_pct: null, ebacc_strong_pass_pct: null, ebacc_standard_pass_pct: null, + ebacc_avg_score: null, gcse_grade_91_pct: null, prior_attainment_avg: null, +}; + +export function makeResult(overrides: Partial = {}): SchoolResult { + return { ...emptyResult, ...overrides }; +} + +function makeSchool(overrides: Partial = {}): School { + return { + urn: 123456, + school_name: 'Test Primary School', + local_authority: 'Westshire', + local_authority_code: 900, + school_type: 'Community school', + school_type_code: 'CY', + religious_denomination: 'None', + age_range: '4-11', + address1: '1 Test Lane', + address2: null, + town: 'Testville', + postcode: 'TE1 1ST', + address: '1 Test Lane, Testville, TE1 1ST', + latitude: 51.5, + longitude: -0.12, + phase: 'Primary', + gender: 'Mixed', + ...overrides, + }; +} + +const emptyOfsted: OfstedInspection = { + 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, + 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, +}; + +function makeOfsted(overrides: Partial = {}): OfstedInspection { + return { ...emptyOfsted, ...overrides }; +} + +function makeAdmissions(overrides: Partial = {}): SchoolAdmissions { + return { + year: 2024, + places_offered: 60, + total_applications: 210, + first_preference_applications: 90, + first_preference_offers: 54, + first_preference_offer_pct: 60, + oversubscribed: true, + ...overrides, + }; +} + +const census: SchoolCensus = { + year: 2024, + total_pupils: 420, + female_pupils: 205, + male_pupils: 215, + fsm_pct: 18.4, + eal_pct: 22.1, +}; + +const absence: AbsenceData = { + overall_absence_rate: 5.2, + persistent_absence_rate: 14.8, +}; + +const deprivation: SchoolDeprivation = { + lsoa_code: 'E01000001', + idaci_score: 0.21, + idaci_decile: 4, +}; + +const finance: SchoolFinance = { + year: 2024, + per_pupil_spend: 5400, + staff_cost_pct: 72.5, + teacher_cost_pct: 51.2, + support_staff_cost_pct: 21.3, + premises_cost_pct: 6.1, +}; + +// --------------------------------------------------------------------------- +// National averages +// --------------------------------------------------------------------------- + +export const nationalAveragesFixture: NationalAverages = { + year: 2024, + primary: { rwm_expected_pct: 61, rwm_high_pct: 8, reading_expected_pct: 74 }, + secondary: { attainment_8_score: 45.9, progress_8_score: 0, ebacc_entry_pct: 39 }, + by_year: [ + { year: 2023, primary: { rwm_expected_pct: 60 }, secondary: { attainment_8_score: 44.4 } }, + { year: 2024, primary: { rwm_expected_pct: 61 }, secondary: { attainment_8_score: 45.9 } }, + ], +}; + +// --------------------------------------------------------------------------- +// Fixture 1: ordinary primary — KS2 results, legacy OEIF, 3 admissions years +// --------------------------------------------------------------------------- + +export const primaryFixture = { + schoolInfo: makeSchool(), + yearlyData: [ + makeResult({ year: 2022, rwm_expected_pct: 54, reading_expected_pct: 68, writing_expected_pct: 62, maths_expected_pct: 65, total_pupils: 58 }), + makeResult({ year: 2023, rwm_expected_pct: 56, reading_expected_pct: 70, writing_expected_pct: 64, maths_expected_pct: 67, total_pupils: 60 }), + makeResult({ + year: 2024, + rwm_expected_pct: 58, rwm_high_pct: 9, + reading_expected_pct: 72, writing_expected_pct: 66, maths_expected_pct: 69, + reading_progress: 0.4, writing_progress: -0.2, maths_progress: 1.1, + disadvantaged_pct: 24.5, eal_pct: 22.1, sen_support_pct: 13.2, + total_pupils: 60, + }), + ], + absenceData: absence, + ofsted: makeOfsted({ + framework: 'OEIF', + inspection_date: '2023-05-17', + inspection_type: 'Section 5', + overall_effectiveness: 2, + quality_of_education: 2, + behaviour_attitudes: 1, + personal_development: 2, + leadership_management: 2, + previous_overall: 3, + grade_source: 'graded', + }), + census, + admissions: makeAdmissions({ year: 2024 }), + admissionsHistory: [ + makeAdmissions({ year: 2022, first_preference_offer_pct: 71 }), + makeAdmissions({ year: 2023, first_preference_offer_pct: 65 }), + makeAdmissions({ year: 2024, first_preference_offer_pct: 60 }), + ], + deprivation, + finance, +}; + +// --------------------------------------------------------------------------- +// Fixture 2: secondary — KS4 results, Report Card Ofsted, single admissions year +// --------------------------------------------------------------------------- + +export const secondaryFixture = { + schoolInfo: makeSchool({ + urn: 234567, + school_name: 'Test Secondary School', + phase: 'Secondary', + age_range: '11-16', + school_type: 'Academy converter', + school_type_code: 'AC', + }), + yearlyData: [ + makeResult({ year: 2023, attainment_8_score: 44.8, progress_8_score: 0.18 }), + makeResult({ + year: 2024, + attainment_8_score: 46.2, progress_8_score: 0.31, + progress_8_banding: 'Above average', + english_maths_standard_pass_pct: 68.4, + english_maths_strong_pass_pct: 47.1, + ebacc_entry_pct: 41.2, ebacc_standard_pass_pct: 28.6, + ebacc_strong_pass_pct: 19.4, ebacc_avg_score: 4.31, + disadvantaged_pct: 27.8, eal_pct: 19.3, sen_support_pct: 11.7, + }), + ], + absenceData: absence, + ofsted: makeOfsted({ + framework: 'ReportCard', + inspection_date: '2025-11-20', + rc_inspection_date: '2025-11-20', + rc_safeguarding_met: true, + rc_inclusion: 2, + rc_curriculum_teaching: 2, + rc_achievement: 3, + rc_attendance_behaviour: 2, + rc_personal_development: 1, + rc_leadership_governance: 2, + }), + census, + admissions: makeAdmissions({ year: 2024, school_phase: 'Secondary' }), + admissionsHistory: [makeAdmissions({ year: 2024, school_phase: 'Secondary' })], + deprivation, + finance, +}; + +// --------------------------------------------------------------------------- +// Fixture 3: all-through — BOTH key stages on the same row. +// The highest-value fixture: isAllThrough gates the KS2 content back on for a +// school that also has KS4 data, and this is the case most likely to break. +// --------------------------------------------------------------------------- + +export const allThroughFixture = { + schoolInfo: makeSchool({ + urn: 345678, + school_name: 'Test All-Through School', + phase: 'All-through', + age_range: '4-18', + has_sixth_form: true, + }), + yearlyData: [ + makeResult({ year: 2023, rwm_expected_pct: 55, attainment_8_score: 45.0 }), + makeResult({ + year: 2024, + rwm_expected_pct: 63, reading_expected_pct: 76, + writing_expected_pct: 70, maths_expected_pct: 71, + attainment_8_score: 48.7, progress_8_score: 0.42, + english_maths_standard_pass_pct: 71.2, + disadvantaged_pct: 20.1, eal_pct: 17.5, sen_support_pct: 9.8, + }), + ], + absenceData: absence, + ofsted: makeOfsted({ + framework: 'OEIF', + inspection_date: '2022-10-04', + inspection_type: 'Section 5', + overall_effectiveness: 1, + quality_of_education: 1, + behaviour_attitudes: 1, + personal_development: 1, + leadership_management: 1, + grade_source: 'graded', + }), + census, + admissions: makeAdmissions({ year: 2024, school_phase: 'Secondary' }), + admissionsHistory: [ + makeAdmissions({ year: 2023, school_phase: 'Secondary', first_preference_offer_pct: 58 }), + makeAdmissions({ year: 2024, school_phase: 'Secondary', first_preference_offer_pct: 52 }), + ], + deprivation, + finance, +}; + +// --------------------------------------------------------------------------- +// Fixture 4: special school — all-zero KS2 row. +// Guards the PR #70 regression: special/PRU/AP schools must not be shown an +// England comparison, because their pupils sit the same tests but very few +// reach the mainstream "expected standard". +// --------------------------------------------------------------------------- + +export const specialFixture = { + schoolInfo: makeSchool({ + urn: 456789, + school_name: 'Test Special School', + school_type: 'Community special school', + school_type_code: 'CYS', + age_range: '2-19', + }), + yearlyData: [ + makeResult({ + year: 2024, + rwm_expected_pct: 0, + reading_expected_pct: 0, + writing_expected_pct: 0, + maths_expected_pct: 0, + total_pupils: 12, + }), + ], + absenceData: absence, + ofsted: null, + census, + admissions: null, + admissionsHistory: [], + deprivation, + finance, +}; diff --git a/nextjs-app/jest.setup.js b/nextjs-app/jest.setup.js index 46662f2..256811b 100644 --- a/nextjs-app/jest.setup.js +++ b/nextjs-app/jest.setup.js @@ -27,6 +27,23 @@ Object.defineProperty(window, 'matchMedia', { })), }); +// jsdom implements neither of these; the school detail views use both +// (IntersectionObserver for the scroll-spy and the hero-CTA tracker, +// scrollTo for the "back to top" control). +global.IntersectionObserver = class { + constructor(callback) { + this.callback = callback; + } + observe() {} + unobserve() {} + disconnect() {} + takeRecords() { + return []; + } +}; + +window.scrollTo = jest.fn(); + // Mock localStorage const localStorageMock = { getItem: jest.fn(),