PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m3s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 11s
PR Checks / Build Frontend (no push) (pull_request) Successful in 44s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 14s
The heading sat on the right edge of the column and every value on the left. A specificity collision, not a layout problem: the two were aligned by different selectors and only one of them won. .table td (0,1,1) text-align: left <- won for the value .num (0,1,0) text-align: right <- lost .table th:last-child (0,2,1) text-align: right <- won for the heading The heading and the value cell now share one class and one rule, so they cannot drift apart again whatever else changes around them. The column also stretched to half the table. It now hugs its content with width:1% and nowrap, so the school name takes the remaining width — which is what made the gap read as misalignment on a wide screen, and what crowded the name column on a narrow one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
202 lines
8.7 KiB
TypeScript
202 lines
8.7 KiB
TypeScript
import { render, screen } from '@testing-library/react';
|
|
import { PlaceView } from '@/components/places/PlaceView';
|
|
import type { PlaceDetail } from '@/lib/places';
|
|
|
|
const detail: PlaceDetail = {
|
|
place: { kind: 'town', slug: 'brentwood', name: 'Brentwood', count: 29,
|
|
parent_authority: 'Essex', phases: ['primary'] },
|
|
schools: [
|
|
{ urn: 1, school_name: 'Alpha Primary', rwm_expected_pct: 82,
|
|
ofsted_grade: 1, phase: 'Primary' } as never,
|
|
{ urn: 2, school_name: 'Beta Primary', rwm_expected_pct: 44,
|
|
ofsted_grade: 3, phase: 'Primary' } as never,
|
|
],
|
|
averages: { rwm_expected_pct: 63, attainment_8_score: null },
|
|
};
|
|
|
|
describe('PlaceView', () => {
|
|
it('leads with an H1 that matches how the place is searched', () => {
|
|
render(<PlaceView detail={detail} phase="primary" englandAverage={61}
|
|
neighbours={[]} />);
|
|
expect(screen.getByRole('heading', { level: 1 }))
|
|
.toHaveTextContent(/primary schools in brentwood/i);
|
|
});
|
|
|
|
it('states the count so the page says something before the table', () => {
|
|
render(<PlaceView detail={detail} phase="primary" englandAverage={61}
|
|
neighbours={[]} />);
|
|
expect(screen.getByText(/29 schools/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it('compares the local average against England, which a list cannot', () => {
|
|
render(<PlaceView detail={detail} phase="primary" englandAverage={61}
|
|
neighbours={[]} />);
|
|
expect(screen.getByTestId('local-vs-england')).toHaveTextContent('63');
|
|
expect(screen.getByTestId('local-vs-england')).toHaveTextContent('61');
|
|
});
|
|
|
|
it('links every school in scope, which is what de-orphans them', () => {
|
|
render(<PlaceView detail={detail} phase="primary" englandAverage={61}
|
|
neighbours={[]} />);
|
|
expect(screen.getAllByRole('link', { name: /Primary$/ })).toHaveLength(2);
|
|
});
|
|
|
|
it('links to the parent authority so the place sits in a hierarchy', () => {
|
|
render(<PlaceView detail={detail} phase="primary" englandAverage={61}
|
|
neighbours={[]} />);
|
|
expect(screen.getByRole('link', { name: /Essex/i }))
|
|
.toHaveAttribute('href', '/schools/authority/essex');
|
|
});
|
|
|
|
it('shows the Ofsted distribution, not just a count of Outstanding', () => {
|
|
render(<PlaceView detail={detail} phase="primary" englandAverage={61}
|
|
neighbours={[]} />);
|
|
expect(screen.getByTestId('ofsted-distribution')).toBeInTheDocument();
|
|
});
|
|
|
|
it('links to neighbouring places so the page is not a dead end', () => {
|
|
render(<PlaceView detail={detail} phase="primary" englandAverage={61}
|
|
neighbours={[{ kind: 'town', slug: 'romford', name: 'Romford', count: 40 }]} />);
|
|
expect(screen.getByRole('link', { name: /Romford/ }))
|
|
.toHaveAttribute('href', '/schools/romford');
|
|
});
|
|
|
|
it('says nothing about an average it does not have', () => {
|
|
render(<PlaceView detail={{ ...detail, averages:
|
|
{ rwm_expected_pct: null, attainment_8_score: null } }}
|
|
phase="primary" englandAverage={61} neighbours={[]} />);
|
|
expect(screen.queryByTestId('local-vs-england')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('PlaceView structured data', () => {
|
|
function jsonLd() {
|
|
const { container } = render(<PlaceView detail={detail} phase="primary"
|
|
englandAverage={61} neighbours={[]} />);
|
|
const el = container.querySelector('script[type="application/ld+json"]');
|
|
return JSON.parse(el!.textContent!);
|
|
}
|
|
|
|
it('declares the page as a ranked list, not prose', () => {
|
|
const types = jsonLd()['@graph'].map((n: { '@type': string }) => n['@type']);
|
|
expect(types).toContain('ItemList');
|
|
expect(types).toContain('BreadcrumbList');
|
|
});
|
|
|
|
it('gives every listed school an absolute URL on the canonical host', () => {
|
|
const list = jsonLd()['@graph'].find((n: { '@type': string }) => n['@type'] === 'ItemList');
|
|
expect(list.itemListElement).toHaveLength(2);
|
|
for (const item of list.itemListElement) {
|
|
expect(item.url).toMatch(/^https:\/\/www\.schoolcompare\.co\.uk\/school\//);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('PlaceView phase variants', () => {
|
|
it('links the phase variants that exist', () => {
|
|
render(<PlaceView detail={detail} englandAverage={61} neighbours={[]} />);
|
|
expect(screen.getByRole('link', { name: /Primary schools in Brentwood/i }))
|
|
.toHaveAttribute('href', '/schools/brentwood/primary');
|
|
});
|
|
|
|
it('links no variant for a phase below its own threshold', () => {
|
|
render(<PlaceView detail={detail} englandAverage={61} neighbours={[]} />);
|
|
expect(screen.queryByRole('link', { name: /Secondary schools in Brentwood/i }))
|
|
.not.toBeInTheDocument();
|
|
});
|
|
|
|
it('does not link sideways from a variant page to itself', () => {
|
|
render(<PlaceView detail={detail} phase="primary" englandAverage={61}
|
|
neighbours={[]} />);
|
|
expect(screen.queryByRole('link', { name: /Primary schools in Brentwood/i }))
|
|
.not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('PlaceView presentation', () => {
|
|
// /schools/brentwood shipped with 8 of 27 rows blank: an unphased page shows
|
|
// one primary-only measure for a list that also holds secondaries.
|
|
const mixed: PlaceDetail = {
|
|
place: { kind: 'town', slug: 'brentwood', name: 'Brentwood', count: 4,
|
|
parent_authority: 'Essex', phases: ['primary', 'secondary'] },
|
|
schools: [
|
|
{ urn: 1, school_name: 'Alpha Primary', phase: 'Primary',
|
|
rwm_expected_pct: 82, attainment_8_score: null } as never,
|
|
{ urn: 2, school_name: 'Beta High', phase: 'Secondary',
|
|
rwm_expected_pct: null, attainment_8_score: 47 } as never,
|
|
],
|
|
averages: { rwm_expected_pct: 63, attainment_8_score: 45 },
|
|
};
|
|
|
|
it('gives each phase its own table rather than one column of blanks', () => {
|
|
render(<PlaceView detail={mixed} englandAverage={61} neighbours={[]} />);
|
|
expect(screen.getByRole('heading', { name: /^Primary schools/ })).toBeInTheDocument();
|
|
expect(screen.getByRole('heading', { name: /^Secondary schools/ })).toBeInTheDocument();
|
|
expect(screen.getByText('82%')).toBeInTheDocument();
|
|
expect(screen.getByText('47')).toBeInTheDocument();
|
|
});
|
|
|
|
it('names the measure in plain words, not jargon', () => {
|
|
// The first cut said "RWM expected", which appears nowhere else on the site.
|
|
render(<PlaceView detail={mixed} englandAverage={61} neighbours={[]} />);
|
|
expect(screen.getByText('Reading, writing & maths')).toBeInTheDocument();
|
|
expect(screen.getByText('Attainment 8')).toBeInTheDocument();
|
|
expect(screen.queryByText(/RWM expected/i)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('says a missing result is unpublished rather than showing a bare dash', () => {
|
|
const noResult: PlaceDetail = {
|
|
...mixed,
|
|
schools: [{ urn: 3, school_name: 'New Primary', phase: 'Primary',
|
|
rwm_expected_pct: null, attainment_8_score: null } as never],
|
|
};
|
|
render(<PlaceView detail={noResult} englandAverage={61} neighbours={[]} />);
|
|
expect(screen.getByText('Not published')).toBeInTheDocument();
|
|
});
|
|
|
|
it('styles school links to the site convention rather than browser default', () => {
|
|
const { container } = render(<PlaceView detail={mixed} englandAverage={61}
|
|
neighbours={[]} />);
|
|
const link = container.querySelector('a[href^="/school/"]');
|
|
expect(link?.className).toBeTruthy();
|
|
});
|
|
|
|
it('a phased page shows one table and no phase headings', () => {
|
|
render(<PlaceView detail={mixed} phase="primary" englandAverage={61}
|
|
neighbours={[]} />);
|
|
expect(screen.queryByRole('heading', { name: /^Secondary schools/ }))
|
|
.not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('PlaceView table alignment', () => {
|
|
const aligned: PlaceDetail = {
|
|
place: { kind: 'town', slug: 'brentwood', name: 'Brentwood', count: 2,
|
|
parent_authority: 'Essex', phases: ['primary'] },
|
|
schools: [
|
|
{ urn: 1, school_name: 'Alpha Primary', phase: 'Primary',
|
|
rwm_expected_pct: 82, attainment_8_score: null } as never,
|
|
],
|
|
averages: { rwm_expected_pct: 63, attainment_8_score: null },
|
|
};
|
|
|
|
it('aligns the measure heading and its values with the same class', () => {
|
|
// They were aligned by two different selectors whose specificity did not
|
|
// match: `.table th:last-child` (0,2,1) won and went right, while `.num`
|
|
// (0,1,0) lost to `.table td` (0,1,1) and stayed left. Sharing one class
|
|
// is what makes them impossible to drift apart.
|
|
const { container } = render(<PlaceView detail={aligned} englandAverage={61}
|
|
neighbours={[]} />);
|
|
const th = container.querySelectorAll('th')[1];
|
|
const td = container.querySelectorAll('tbody td')[1];
|
|
expect(th.className).toBeTruthy();
|
|
expect(td.className).toBe(th.className);
|
|
});
|
|
|
|
it('leaves the school-name column unclassed so it takes the spare width', () => {
|
|
const { container } = render(<PlaceView detail={aligned} englandAverage={61}
|
|
neighbours={[]} />);
|
|
expect(container.querySelectorAll('th')[0].className).toBe('');
|
|
});
|
|
});
|