2026-08-28 16:14:13 +01:00
|
|
|
import { render, screen } from '@testing-library/react';
|
|
|
|
|
import { DestinationsSection } from '@/components/school/DestinationsSection';
|
|
|
|
|
import type { DestinationPhase } from '@/lib/types';
|
|
|
|
|
import type { DestinationCategory, DestinationStatus } from '@/lib/destinations';
|
|
|
|
|
|
|
|
|
|
const cell = (
|
|
|
|
|
category: DestinationCategory,
|
|
|
|
|
pupils: number | null,
|
|
|
|
|
status: DestinationStatus = 'published',
|
|
|
|
|
) => ({
|
|
|
|
|
category, pupils,
|
|
|
|
|
percentage: pupils === null ? null : (pupils / 180) * 100,
|
|
|
|
|
status,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const ALL_PUBLISHED = [
|
|
|
|
|
cell('school_sixth_form', 75), cell('sixth_form_college', 21),
|
|
|
|
|
cell('further_education', 55), cell('other_education', 6),
|
|
|
|
|
cell('apprenticeship', 8), cell('employment', 6),
|
|
|
|
|
cell('not_sustained', 5), cell('not_captured', 4),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const fullPhase: DestinationPhase = {
|
|
|
|
|
cohort_year: '2022/23',
|
2026-08-28 19:58:02 +01:00
|
|
|
groups: { all: { cohort: 180, categories: ALL_PUBLISHED } },
|
2026-08-28 16:14:13 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const suppressedPhase: DestinationPhase = {
|
|
|
|
|
cohort_year: '2022/23',
|
|
|
|
|
groups: {
|
|
|
|
|
all: {
|
|
|
|
|
cohort: 180,
|
|
|
|
|
categories: [
|
|
|
|
|
cell('school_sixth_form', 75), cell('sixth_form_college', null, 'suppressed'),
|
|
|
|
|
cell('further_education', 55), cell('other_education', 6),
|
|
|
|
|
cell('apprenticeship', 8), cell('employment', 6),
|
|
|
|
|
cell('not_sustained', 5), cell('not_captured', 4),
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
describe('DestinationsSection', () => {
|
|
|
|
|
it('dates its own cohort so it is not read as stale next to the GCSE section', () => {
|
|
|
|
|
render(<DestinationsSection destinations={fullPhase} />);
|
|
|
|
|
expect(screen.getByText(/2022\/23/)).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders one bar segment per published category', () => {
|
|
|
|
|
const { container } = render(<DestinationsSection destinations={fullPhase} />);
|
|
|
|
|
expect(container.querySelectorAll('[data-destination-segment]')).toHaveLength(8);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders NO bar at all when a category is withheld', () => {
|
|
|
|
|
const { container } = render(<DestinationsSection destinations={suppressedPhase} />);
|
|
|
|
|
// R1: a bar with a gap in it publishes the withheld figure by its width.
|
|
|
|
|
expect(container.querySelectorAll('[data-destination-segment]')).toHaveLength(0);
|
|
|
|
|
expect(screen.getAllByText(/withheld/i).length).toBeGreaterThan(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('never states the remainder for a partially suppressed group', () => {
|
|
|
|
|
const { container } = render(<DestinationsSection destinations={suppressedPhase} />);
|
|
|
|
|
// 180 cohort - 159 published = 21, the withheld figure. It must appear nowhere.
|
|
|
|
|
expect(container.textContent).not.toMatch(/\b21\b/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows a card value for a group whose components are all published', () => {
|
|
|
|
|
render(<DestinationsSection destinations={fullPhase} />);
|
|
|
|
|
// academic route = 75 + 21 = 96 of 180 = 53%
|
|
|
|
|
expect(screen.getByText('53%')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('refuses a card value when one of its components is withheld', () => {
|
|
|
|
|
render(<DestinationsSection destinations={suppressedPhase} />);
|
|
|
|
|
// academic route needs sixth_form_college, which is suppressed.
|
|
|
|
|
expect(screen.getByText(/not published/i)).toBeInTheDocument();
|
|
|
|
|
expect(screen.queryByText('53%')).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('never claims a pupil stayed at this school', () => {
|
|
|
|
|
const { container } = render(<DestinationsSection destinations={fullPhase} />);
|
|
|
|
|
// The published file reports destination TYPE, never destination institution.
|
|
|
|
|
expect(container.textContent).not.toMatch(/stayed on (here|at this school)/i);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders nothing when no group carries categories', () => {
|
|
|
|
|
const empty: DestinationPhase = { cohort_year: '2022/23', groups: {} };
|
|
|
|
|
const { container } = render(<DestinationsSection destinations={empty} />);
|
|
|
|
|
expect(container.firstChild).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-08-30 21:48:39 +01:00
|
|
|
|
|
|
|
|
describe('the detail table keeps the three statuses apart', () => {
|
|
|
|
|
// 'suppressed' and 'not_applicable' are different claims, and the mart, the
|
|
|
|
|
// SQLAlchemy model and the serialiser all preserve the difference. The table
|
|
|
|
|
// used to key its Share column off `percentage === null`, which is true for
|
|
|
|
|
// both, so a category that simply does not apply was labelled "withheld" —
|
|
|
|
|
// while the Pupils column beside it rendered blank.
|
|
|
|
|
const mixedPhase: DestinationPhase = {
|
|
|
|
|
cohort_year: '2022/23',
|
|
|
|
|
groups: {
|
|
|
|
|
all: {
|
|
|
|
|
cohort: 180,
|
|
|
|
|
categories: [
|
|
|
|
|
cell('school_sixth_form', 75),
|
|
|
|
|
cell('sixth_form_college', null, 'suppressed'),
|
|
|
|
|
cell('further_education', null, 'suppressed'),
|
|
|
|
|
cell('apprenticeship', null, 'not_applicable'),
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const rowFor = (container: HTMLElement, category: string) =>
|
|
|
|
|
Array.from(container.querySelectorAll('tbody tr'))
|
|
|
|
|
.find(tr => tr.textContent?.includes(category));
|
|
|
|
|
|
|
|
|
|
it('never labels a not-applicable category as withheld', () => {
|
|
|
|
|
const { container } = render(<DestinationsSection destinations={mixedPhase} />);
|
|
|
|
|
const row = rowFor(container, 'Apprenticeship');
|
|
|
|
|
expect(row).toBeTruthy();
|
|
|
|
|
expect(row!.textContent).not.toMatch(/withheld/i);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('labels a genuinely suppressed category as withheld in both columns', () => {
|
|
|
|
|
const { container } = render(<DestinationsSection destinations={mixedPhase} />);
|
|
|
|
|
const row = rowFor(container, 'Sixth-form college');
|
|
|
|
|
expect(row).toBeTruthy();
|
|
|
|
|
expect(row!.querySelectorAll('td')).toHaveLength(2);
|
|
|
|
|
Array.from(row!.querySelectorAll('td')).forEach(td =>
|
|
|
|
|
expect(td.textContent).toMatch(/withheld/i));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('the two columns of a row never disagree about what the row is', () => {
|
|
|
|
|
const { container } = render(<DestinationsSection destinations={mixedPhase} />);
|
|
|
|
|
Array.from(container.querySelectorAll('tbody tr')).forEach(tr => {
|
|
|
|
|
const cells = Array.from(tr.querySelectorAll('td'))
|
|
|
|
|
.map(td => /withheld/i.test(td.textContent ?? ''));
|
|
|
|
|
expect(new Set(cells).size).toBe(1);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows a published category its real figures', () => {
|
|
|
|
|
const { container } = render(<DestinationsSection destinations={mixedPhase} />);
|
|
|
|
|
const row = rowFor(container, 'State-funded school sixth form');
|
|
|
|
|
expect(row!.textContent).toMatch(/75/);
|
|
|
|
|
expect(row!.textContent).toMatch(/42%/);
|
|
|
|
|
});
|
|
|
|
|
});
|