Files
school_compare/nextjs-app/__tests__/components/schoolDetail.characterization.test.tsx
T
TudorandClaude Opus 5.5 d1688ac150
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m14s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 19s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m19s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 1m20s
copy(web): replace em dashes in public copy
Rewrites every visitor-facing string that used an em dash, choosing the
punctuation for what the dash was doing: a colon before a list or
explanation, a comma for an aside, a full stop between two thoughts,
parentheses for an aside mid-sentence. Covers page titles and meta
descriptions, the home and admissions guide copy, school page headings
and notes, the compare page, metric labels and tooltips.

Two rewrites also fix the sentence around them: the closure banner no
longer repeats "proposed for closure", and the cut-off caveat's list of
priorities now parses.

A lone dash marking a missing value in a table cell stays: it is a data
convention, not prose. A Jest guard walks the source with the TypeScript
parser and fails on any other em dash in a string or JSX text node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 22:04:27 +01:00

178 lines
6.9 KiB
TypeScript

/**
* Characterization tests for the school detail pages.
*
* These describe behaviour that ALREADY EXISTS. They are written before the
* server/client split and must pass UNMODIFIED after it — that is the whole
* point. If one fails during the refactor, the refactor is wrong; fix the
* components, never these assertions.
*
* All rendering goes through renderSchoolDetail / renderSecondarySchoolDetail,
* the one file the refactor is permitted to change.
*/
import { screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {
primaryFixture, secondaryFixture, allThroughFixture, specialFixture,
} from '../support/schoolFixtures';
import { renderSchoolDetail, renderSecondarySchoolDetail } from '../support/renderSchoolDetail';
jest.mock('@/lib/analytics', () => ({
track: jest.fn(),
getNavigationSource: () => 'direct',
}));
// Chart.js needs a canvas jsdom does not provide, and Leaflet needs a real
// window. Both are already lazy client islands; their content is out of scope.
jest.mock('@/components/PerformanceChart', () => ({
PerformanceChart: () => <div data-testid="performance-chart" />,
}));
jest.mock('@/components/SatsChart', () => ({
__esModule: true,
default: () => <div data-testid="sats-chart" />,
}));
jest.mock('@/components/AdmissionsTrendChart', () => ({
__esModule: true,
default: () => <div data-testid="admissions-trend-chart" />,
}));
jest.mock('@/components/SchoolHeroMap', () => ({
SchoolHeroMap: () => <div data-testid="hero-map" />,
__esModule: true,
}));
describe('Ofsted section', () => {
it('renders the legacy OEIF grade, date and report link', () => {
renderSchoolDetail(primaryFixture);
expect(screen.getByText('Ofsted Rating')).toBeInTheDocument();
expect(screen.getByText(/Inspected 17 May 2023/)).toBeInTheDocument();
expect(screen.getAllByText('Good').length).toBeGreaterThan(0);
const link = screen.getByRole('link', { name: /Ofsted reports/ });
expect(link).toHaveAttribute(
'href',
expect.stringContaining(String(primaryFixture.schoolInfo.urn)),
);
});
it('shows the previous grade when it differs from the current one', () => {
renderSchoolDetail(primaryFixture);
expect(screen.getByText(/Previously:/)).toBeInTheDocument();
});
it('renders the Report Card layout with its category bands', () => {
renderSecondarySchoolDetail(secondaryFixture);
expect(screen.getByText('Ofsted Report Card')).toBeInTheDocument();
expect(screen.getByText(/From November 2025, Ofsted replaced single overall grades/)).toBeInTheDocument();
expect(screen.getByText('Safeguarding')).toBeInTheDocument();
expect(screen.getByText('Met')).toBeInTheDocument();
expect(screen.getByText('Achievement')).toBeInTheDocument();
});
});
describe('KS2 results', () => {
it('renders the combined RWM figure with its England comparison', async () => {
renderSchoolDetail(primaryFixture);
expect(screen.getByText('58%')).toBeInTheDocument();
// 58 vs an England average of 61 → a -3 pts chip and the hint line.
expect(await screen.findByText('England avg: 61%')).toBeInTheDocument();
expect(await screen.findByText('-3 pts')).toBeInTheDocument();
});
});
describe('KS4 results', () => {
// These figures appear in both the hero stats and the history table, so
// assert on presence rather than uniqueness.
it('renders Attainment 8, Progress 8 and EBacc figures', () => {
renderSecondarySchoolDetail(secondaryFixture);
expect(screen.getAllByText('46.2').length).toBeGreaterThan(0);
// formatProgress rounds to 1 decimal and signs positives: 0.31 → "+0.3"
expect(screen.getAllByText(/\+0\.3/).length).toBeGreaterThan(0);
expect(screen.getAllByText(/EBacc/i).length).toBeGreaterThan(0);
});
});
describe('all-through schools', () => {
// The highest-value assertion in this file. isAllThrough gates the KS2
// content back on for a school that also has KS4 data, so an all-through
// page must show BOTH key stages. This is the case most likely to break.
it('renders both KS2 and KS4 figures on the same page', () => {
renderSchoolDetail(allThroughFixture);
expect(screen.getAllByText('63%').length).toBeGreaterThan(0); // KS2 RWM
expect(screen.getAllByText('48.7').length).toBeGreaterThan(0); // KS4 Attainment 8
});
it('labels the KS2 block explicitly', () => {
renderSchoolDetail(allThroughFixture);
expect(screen.getAllByText(/Primary: KS2 SATs/).length).toBeGreaterThan(0);
});
});
describe('special schools', () => {
// Guards the PR #70 regression, where a special school displayed
// "0% -62 below England" against a mainstream benchmark that does not fit.
it('suppresses the England comparison', () => {
renderSchoolDetail(specialFixture);
expect(screen.queryByText(/England avg:/)).not.toBeInTheDocument();
});
it('explains why the comparison is missing', () => {
renderSchoolDetail(specialFixture);
expect(screen.getByRole('note')).toHaveTextContent('This is a special school.');
});
});
describe('admissions', () => {
it('toggles between the year view and the multi-year trend', async () => {
const user = userEvent.setup();
const { container } = renderSchoolDetail(primaryFixture);
const yearBtn = screen.getByRole('button', { name: 'This year' });
const trendBtn = screen.getByRole('button', { name: /3-year trend/ });
expect(yearBtn).toHaveAttribute('aria-pressed', 'true');
const yearView = container.querySelector('[class*="admissionsViewYear"]')!;
const trendView = container.querySelector('[class*="admissionsViewTrend"]')!;
expect(yearView).not.toHaveAttribute('hidden');
expect(trendView).toHaveAttribute('hidden');
await user.click(trendBtn);
expect(trendBtn).toHaveAttribute('aria-pressed', 'true');
expect(yearView).toHaveAttribute('hidden');
expect(trendView).not.toHaveAttribute('hidden');
});
it('renders the headline admissions figures', () => {
renderSchoolDetail(primaryFixture);
expect(screen.getByText('Places offered')).toBeInTheDocument();
expect(screen.getByText('Got their first choice')).toBeInTheDocument();
});
it('omits the toggle when there is only one year of offer data', () => {
renderSecondarySchoolDetail(secondaryFixture);
expect(screen.queryByRole('button', { name: 'This year' })).not.toBeInTheDocument();
});
});
describe('section navigation', () => {
it('lists the sections that have data', () => {
const { container } = renderSchoolDetail(primaryFixture);
for (const id of ['ofsted', 'results', 'admissions', 'history', 'finances']) {
expect(container.querySelector(`#${id}`)).toBeInTheDocument();
}
});
it('omits sections with no data', () => {
const { container } = renderSchoolDetail(specialFixture);
expect(container.querySelector('#ofsted')).not.toBeInTheDocument();
expect(container.querySelector('#admissions')).not.toBeInTheDocument();
});
});