diff --git a/e2e/tests/journeys.spec.ts b/e2e/tests/journeys.spec.ts index 966f2ab..e953520 100644 --- a/e2e/tests/journeys.spec.ts +++ b/e2e/tests/journeys.spec.ts @@ -2366,3 +2366,125 @@ test('with autosuggest off, the search box is a plain input', async ({ page }) = await page.getByRole('button', { name: /Search/i }).first().click(); await expect(page).toHaveURL(/search=abbey/); }); + +// ── Destination measures ─────────────────────────────────────────────────── +// +// These journeys need marts.fact_ks4_destinations to be populated, which only +// happens after the annual EES DAG runs. Until then the helper below fails the +// suite loudly rather than skipping: a silent skip here would let a genuine +// regression in the sections ride along unnoticed, which is exactly what the +// distance journeys were changed to avoid. + +async function secondaryWithDestinations(page: Page): Promise<{ + urn: string; destinations: any; +}> { + const res = await page.request.get('/api/schools?search=school&per_page=100'); + expect(res.ok()).toBeTruthy(); + const body = await res.json(); + const urns: string[] = (body.schools ?? []) + .filter((s: { phase?: string; attainment_8_score?: number | null }) => + s.phase === 'Secondary' && s.attainment_8_score != null) + .map((s: { urn: number }) => String(s.urn)); + + for (const urn of urns.slice(0, 25)) { + const detail = await page.request.get(`/api/schools/${urn}`); + if (!detail.ok()) continue; + const data = await detail.json(); + if (data.destinations?.ks4) return { urn, destinations: data.destinations }; + } + throw new Error( + 'No secondary school returned a destinations block. Either the annual EES ' + + 'DAG has not run on this environment, or the destinations marts are empty.', + ); +} + +test('a secondary school page says where its Year 11 leavers went', async ({ page }) => { + const { urn } = await secondaryWithDestinations(page); + await page.goto(`/school/${urn}`); + + const section = page.locator('#destinations'); + await expect(section).toBeVisible({ timeout: 15_000 }); + await expect(section.getByRole('heading', { name: 'After Year 11' })).toBeVisible(); + // The section must date its own cohort: destinations run about two GCSE + // years behind the results above them, and an undated figure reads as stale. + await expect(section).toContainText(/20\d{2}\/\d{2}/); +}); + +test('the destinations bar is absent entirely whenever a figure is withheld', async ({ page }) => { + const { urn, destinations } = await secondaryWithDestinations(page); + await page.goto(`/school/${urn}`); + const section = page.locator('#destinations'); + await expect(section).toBeVisible({ timeout: 15_000 }); + + const allGroup = destinations.ks4.groups.all; + const suppressed = (allGroup?.categories ?? []) + .filter((c: { status: string }) => c.status === 'suppressed'); + + if (suppressed.length > 0) { + // R1: a bar drawn from the published segments leaves a gap whose width is + // the withheld figure, readable straight off the axis. + await expect(section.locator('[data-destination-segment]')).toHaveCount(0); + await expect(section.getByText(/withheld/i).first()).toBeVisible(); + } else { + const published = (allGroup?.categories ?? []) + .filter((c: { status: string }) => c.status === 'published'); + await expect(section.locator('[data-destination-segment]')) + .toHaveCount(published.length); + } +}); + +test('switching to disadvantaged pupils never reveals a withheld figure', async ({ page }) => { + const { urn, destinations } = await secondaryWithDestinations(page); + const disadvantaged = destinations.ks4.groups.disadvantaged; + test.skip(!disadvantaged, 'this school publishes no disadvantaged breakdown'); + + await page.goto(`/school/${urn}`); + const section = page.locator('#destinations'); + await expect(section).toBeVisible({ timeout: 15_000 }); + + const radio = section.getByRole('radio', { name: /disadvantaged/i }); + await expect(radio).toBeVisible(); + await radio.click(); + + const suppressed = (disadvantaged.categories ?? []) + .filter((c: { status: string }) => c.status === 'suppressed'); + if (suppressed.length > 0) { + await expect(section.locator('[data-destination-segment]')).toHaveCount(0); + + // The residual must appear nowhere on the page — it is the withheld figure. + const cohort: number = disadvantaged.cohort; + const publishedTotal = (disadvantaged.categories ?? []) + .filter((c: { status: string }) => c.status === 'published') + .reduce((sum: number, c: { pupils: number }) => sum + c.pupils, 0); + const residual = cohort - publishedTotal; + const text = (await section.textContent()) ?? ''; + expect(text).not.toMatch(new RegExp(`\\b${residual}\\b`)); + } +}); + +test('a school with no sixth form has no post-16 destinations section', async ({ page }) => { + const res = await page.request.get('/api/schools?search=school&per_page=100'); + const body = await res.json(); + const noSixthForm = (body.schools ?? []) + .filter((s: { phase?: string; has_sixth_form?: boolean }) => + s.phase === 'Secondary' && s.has_sixth_form === false) + .map((s: { urn: number }) => String(s.urn)); + test.skip(noSixthForm.length === 0, 'no sixth-form-less secondary in this dataset'); + + await page.goto(`/school/${noSixthForm[0]}`); + await expect(page.locator('h1').first()).toBeVisible({ timeout: 15_000 }); + // Absence is the correct statement, so there must be no placeholder either. + await expect(page.locator('#post16-destinations')).toHaveCount(0); + await expect(page.getByText(/destination data coming soon/i)).toHaveCount(0); +}); + +test('the destinations section never claims a pupil stayed at this school', async ({ page }) => { + const { urn } = await secondaryWithDestinations(page); + await page.goto(`/school/${urn}`); + const section = page.locator('#destinations'); + await expect(section).toBeVisible({ timeout: 15_000 }); + // The published file records the TYPE of place a leaver went to, never which + // one, so the page can never say a pupil stayed on here. + const text = (await section.textContent()) ?? ''; + expect(text).not.toMatch(/stayed on (here|at this school)/i); +});