PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m4s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 17s
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 2m44s
The location tables carried one column: a percentage. A parent shortlisting from a town page is asking a different question first — does it take my child's age, is it a faith school, does it have a nursery — and the page could not answer any of it. Primary tables gain Ages, Religious character, Nursery and Constituency; secondary tables the same minus Nursery, which is a question about a different intake. An all-through school renders in both groups, so its nursery shows under primary alone. The measure moves to the second column rather than the last. Six columns overflow a phone and .tableWrap turns that into a horizontal swipe; with the measure last, the one number the page exists for is the one scrolled off the screen. Cell rules are the ones the school page already uses, so the two surfaces cannot disagree about the same school: "Does not apply", "None" and "Not applicable" all read as no religious character, and the en-dash age normalisation moves into formatAgeSpan, which formatAgeRange now delegates to. Backend: nursery_provision and parliamentary_constituency were not in the place response. Both are optional GIAS mart columns that data_loader degrades to NULL, and the `in rows.columns` guard keeps a mart the pipeline has not rebuilt working. Also fixes a live bug on the same line: SCHOOL_COLUMNS already ends with latitude and longitude, and the endpoint concatenated them again, so pandas dropped one of every duplicated pair and warned "columns are not unique" on each request. Ordered de-duplication removes the warning and the silent drop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FuPUioHpxtaiDNagQvjxyM
480 lines
21 KiB
TypeScript
480 lines
21 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('');
|
||
});
|
||
});
|
||
|
||
describe('PlaceView authorities', () => {
|
||
const straddling: PlaceDetail = {
|
||
place: { kind: 'outcode', slug: 'sw19', name: 'SW19', count: 33,
|
||
parent_authority: 'Merton', phases: ['primary'],
|
||
authorities: [
|
||
{ name: 'Merton', slug: 'merton', count: 26 },
|
||
{ name: 'Wandsworth', slug: 'wandsworth', count: 7 },
|
||
] },
|
||
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('names every authority the place straddles, not just the largest', () => {
|
||
// SW19 is mostly Merton but partly Wandsworth. Naming one asserts
|
||
// something false about a quarter of outcodes.
|
||
render(<PlaceView detail={straddling} englandAverage={61} neighbours={[]} />);
|
||
expect(screen.getByRole('link', { name: 'Merton' }))
|
||
.toHaveAttribute('href', '/schools/authority/merton');
|
||
expect(screen.getByRole('link', { name: 'Wandsworth' }))
|
||
.toHaveAttribute('href', '/schools/authority/wandsworth');
|
||
});
|
||
|
||
it('joins them readably rather than as a bare list', () => {
|
||
// Asserted on the summary line's whole text: a loose /and/ matcher also
|
||
// hits "Wandsworth".
|
||
const { container } = render(<PlaceView detail={straddling}
|
||
englandAverage={61} neighbours={[]} />);
|
||
const summary = container.querySelector('header p');
|
||
expect(summary?.textContent).toContain('Merton and Wandsworth');
|
||
});
|
||
|
||
it('falls back to the single parent when the field is absent', () => {
|
||
// A cached API response predating the authorities field must not blank
|
||
// the line entirely.
|
||
const legacy = { ...straddling,
|
||
place: { ...straddling.place, authorities: undefined } };
|
||
render(<PlaceView detail={legacy} englandAverage={61} neighbours={[]} />);
|
||
expect(screen.getByRole('link', { name: 'Merton' })).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
describe('PlaceView list ordering', () => {
|
||
const detail3: 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: 40, attainment_8_score: null } as never,
|
||
{ urn: 2, school_name: 'Beta Primary', phase: 'Primary',
|
||
rwm_expected_pct: 90, attainment_8_score: null } as never,
|
||
],
|
||
averages: { rwm_expected_pct: 65, attainment_8_score: null },
|
||
};
|
||
|
||
it('renders schools in the order the API sent them, not by score', () => {
|
||
// The API sorts alphabetically now; the component must not re-sort.
|
||
render(<PlaceView detail={detail3} englandAverage={61} neighbours={[]} />);
|
||
const links = screen.getAllByRole('link', { name: /Primary$/ });
|
||
expect(links.map((l) => l.textContent))
|
||
.toEqual(['Alpha Primary', 'Beta Primary']);
|
||
});
|
||
|
||
it('declares the list as ascending rather than implying a ranking', () => {
|
||
// An ItemList carrying `position` reads as a ranking unless it says
|
||
// otherwise, and the table is A-Z.
|
||
const { container } = render(<PlaceView detail={detail3} englandAverage={61}
|
||
neighbours={[]} />);
|
||
const ld = JSON.parse(
|
||
container.querySelector('script[type="application/ld+json"]')!.textContent!);
|
||
const list = ld['@graph'].find((n: { '@type': string }) => n['@type'] === 'ItemList');
|
||
expect(list.itemListOrder).toBe('https://schema.org/ItemListOrderAscending');
|
||
});
|
||
});
|
||
|
||
describe('PlaceView phase links', () => {
|
||
const authority: PlaceDetail = {
|
||
place: { kind: 'authority', slug: 'barnet', name: 'Barnet', count: 156,
|
||
parent_authority: null, phases: ['primary', 'secondary'] },
|
||
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('keeps an authority phase link in the authority namespace', () => {
|
||
// The link was built as `/schools/${slug}/${phase}` for every kind, so an
|
||
// authority page pointed into the town namespace. For 87 of 151
|
||
// authorities that 404'd; for the other 64 it silently landed on the town
|
||
// page of the same name — a different set of schools, and exactly the
|
||
// duplicate the two namespaces exist to prevent. Barnet is one of the 64.
|
||
render(<PlaceView detail={authority} englandAverage={61} neighbours={[]} />);
|
||
expect(screen.getByRole('link', { name: /^Primary schools in Barnet$/ }))
|
||
.toHaveAttribute('href', '/schools/authority/barnet/primary');
|
||
expect(screen.getByRole('link', { name: /^Secondary schools in Barnet$/ }))
|
||
.toHaveAttribute('href', '/schools/authority/barnet/secondary');
|
||
});
|
||
|
||
it('still uses the bare namespace for a town', () => {
|
||
render(<PlaceView detail={detail} englandAverage={61} neighbours={[]} />);
|
||
expect(screen.getByRole('link', { name: /^Primary schools in Brentwood$/ }))
|
||
.toHaveAttribute('href', '/schools/brentwood/primary');
|
||
});
|
||
|
||
it('offers no phase link when the place publishes none', () => {
|
||
// Outcodes are the case: no phase route exists for them, so the registry
|
||
// reports no phases and the nav does not render.
|
||
const outcode = { ...detail,
|
||
place: { ...detail.place, kind: 'outcode', slug: 'cm13', name: 'CM13',
|
||
phases: [] } };
|
||
render(<PlaceView detail={outcode} englandAverage={61} neighbours={[]} />);
|
||
expect(screen.queryByRole('navigation', { name: 'By phase' }))
|
||
.not.toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
describe('PlaceView unlinkable authorities', () => {
|
||
const withUnpublished: PlaceDetail = {
|
||
place: { kind: 'outcode', slug: 'tr21', name: 'TR21', count: 8,
|
||
parent_authority: 'Cornwall', phases: [],
|
||
authorities: [
|
||
{ name: 'Cornwall', slug: 'cornwall', count: 6 },
|
||
{ name: 'Isles Of Scilly', slug: null, count: 2 },
|
||
] },
|
||
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('names an authority with no page without linking it', () => {
|
||
// City of London and the Isles of Scilly hold fewer schools than a page
|
||
// needs. Saying where the place is stays right; linking there would 404.
|
||
const { container } = render(<PlaceView detail={withUnpublished}
|
||
englandAverage={61} neighbours={[]} />);
|
||
expect(screen.getByRole('link', { name: 'Cornwall' })).toBeInTheDocument();
|
||
expect(screen.queryByRole('link', { name: 'Isles Of Scilly' }))
|
||
.not.toBeInTheDocument();
|
||
expect(container.querySelector('header p')?.textContent)
|
||
.toContain('Isles Of Scilly');
|
||
});
|
||
});
|
||
|
||
describe('PlaceView school attributes', () => {
|
||
/*
|
||
* The table shipped with one column of scores, which answers "how did they
|
||
* do" and nothing about whether the school is one a family could use. Age
|
||
* range, faith, nursery and constituency are the four facts a parent
|
||
* filters on before they look at a number at all.
|
||
*/
|
||
const withAttributes: PlaceDetail = {
|
||
place: { kind: 'town', slug: 'chelmsford', name: 'Chelmsford', count: 3,
|
||
parent_authority: 'Essex', phases: ['primary', 'secondary'] },
|
||
schools: [
|
||
{ urn: 1, school_name: 'Alpha Primary', phase: 'Primary',
|
||
rwm_expected_pct: 82, attainment_8_score: null,
|
||
age_range: '4-11', religious_denomination: 'Church of England',
|
||
nursery_provision: true,
|
||
parliamentary_constituency: 'Chelmsford' } as never,
|
||
{ urn: 2, school_name: 'Beta High', phase: 'Secondary',
|
||
rwm_expected_pct: null, attainment_8_score: 47,
|
||
age_range: '11-16', religious_denomination: 'Does not apply',
|
||
nursery_provision: false,
|
||
parliamentary_constituency: 'Witham' } as never,
|
||
],
|
||
averages: { rwm_expected_pct: 63, attainment_8_score: 45 },
|
||
};
|
||
|
||
function headings(container: HTMLElement, table = 0): string[] {
|
||
return Array.from(container.querySelectorAll('table')[table]
|
||
.querySelectorAll('thead th')).map((th) => th.textContent ?? '');
|
||
}
|
||
|
||
it('heads a primary table with all four attributes', () => {
|
||
const { container } = render(<PlaceView detail={withAttributes}
|
||
englandAverage={61} neighbours={[]} />);
|
||
expect(headings(container)).toEqual([
|
||
'School', 'Reading, writing & maths',
|
||
'Ages', 'Religious character', 'Nursery', 'Constituency',
|
||
]);
|
||
});
|
||
|
||
it('omits nursery from a secondary table, where it does not apply', () => {
|
||
const { container } = render(<PlaceView detail={withAttributes}
|
||
englandAverage={61} neighbours={[]} />);
|
||
expect(headings(container, 1)).toEqual([
|
||
'School', 'Attainment 8', 'Ages', 'Religious character', 'Constituency',
|
||
]);
|
||
});
|
||
|
||
it('keeps the measure beside the school name, where a phone can see it', () => {
|
||
// Six columns overflow a phone and .tableWrap turns that into a swipe.
|
||
// With the measure last, the one number the page exists for is the one
|
||
// scrolled off the screen.
|
||
const { container } = render(<PlaceView detail={withAttributes}
|
||
phase="primary" englandAverage={61} neighbours={[]} />);
|
||
expect(headings(container)[1]).toBe('Reading, writing & maths');
|
||
});
|
||
|
||
it('shows the age range without repeating the column heading', () => {
|
||
render(<PlaceView detail={withAttributes} englandAverage={61}
|
||
neighbours={[]} />);
|
||
expect(screen.getByText('4–11')).toBeInTheDocument();
|
||
expect(screen.queryByText('Ages 4–11')).not.toBeInTheDocument();
|
||
});
|
||
|
||
it('names the faith of a faith school', () => {
|
||
render(<PlaceView detail={withAttributes} englandAverage={61}
|
||
neighbours={[]} />);
|
||
expect(screen.getByText('Church of England')).toBeInTheDocument();
|
||
});
|
||
|
||
it('reads "Does not apply" as no religious character, not as a value', () => {
|
||
// GIAS spells the absence of a faith as "Does not apply", which is a
|
||
// database answer rather than an English one. The school page already
|
||
// suppresses it; the two must not disagree about the same school.
|
||
const { container } = render(<PlaceView detail={withAttributes}
|
||
englandAverage={61} neighbours={[]} />);
|
||
const secondary = container.querySelectorAll('table')[1]
|
||
.querySelectorAll('tbody td');
|
||
expect(secondary[3].textContent).toBe('—');
|
||
expect(screen.queryByText(/Does not apply/)).not.toBeInTheDocument();
|
||
});
|
||
|
||
it('marks a nursery as such and a school without one as not', () => {
|
||
const { container } = render(<PlaceView detail={withAttributes}
|
||
englandAverage={61} neighbours={[]} />);
|
||
const cells = container.querySelectorAll('table')[0]
|
||
.querySelectorAll('tbody td');
|
||
expect(cells[4].textContent).toBe('Yes');
|
||
});
|
||
|
||
it('names the constituency of each school', () => {
|
||
render(<PlaceView detail={withAttributes} englandAverage={61}
|
||
neighbours={[]} />);
|
||
expect(screen.getByText('Chelmsford', { selector: 'td' })).toBeInTheDocument();
|
||
expect(screen.getByText('Witham', { selector: 'td' })).toBeInTheDocument();
|
||
});
|
||
|
||
it('dashes an attribute the data does not carry', () => {
|
||
// nursery_provision and parliamentary_constituency are absent from marts
|
||
// the pipeline has not rebuilt, and the API degrades them to null rather
|
||
// than failing. A row must survive that.
|
||
const bare: PlaceDetail = {
|
||
...withAttributes,
|
||
schools: [{ urn: 3, school_name: 'Gamma Primary', phase: 'Primary',
|
||
rwm_expected_pct: 70 } as never],
|
||
};
|
||
const { container } = render(<PlaceView detail={bare} phase="primary"
|
||
englandAverage={61} neighbours={[]} />);
|
||
const cells = Array.from(container.querySelectorAll('tbody td'))
|
||
.map((td) => td.textContent);
|
||
expect(cells.slice(2)).toEqual(['—', '—', '—', '—']);
|
||
});
|
||
|
||
it('gives an all-through school its nursery under primary only', () => {
|
||
// All-through schools render in both groups. Nursery belongs to the
|
||
// primary reading of the same school, not the secondary one.
|
||
const allThrough: PlaceDetail = {
|
||
...withAttributes,
|
||
schools: [{ urn: 4, school_name: 'Delta Academy', phase: 'All-through',
|
||
rwm_expected_pct: 66, attainment_8_score: 51,
|
||
age_range: '4-18', religious_denomination: 'None',
|
||
nursery_provision: true,
|
||
parliamentary_constituency: 'Chelmsford' } as never],
|
||
};
|
||
const { container } = render(<PlaceView detail={allThrough}
|
||
englandAverage={61} neighbours={[]} />);
|
||
const tables = container.querySelectorAll('table');
|
||
expect(tables[0].textContent).toContain('Yes');
|
||
expect(tables[1].textContent).not.toContain('Yes');
|
||
});
|
||
});
|