From ccd5074c904f715dfa4457bf2f28c270e45881f6 Mon Sep 17 00:00:00 2001 From: Tudor Date: Fri, 28 Aug 2026 16:16:25 +0100 Subject: [PATCH] test(e2e): destination journeys, including the no-bar rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The helper throws rather than skipping when no school returns a destinations block: a silent skip would let a real regression in the sections ride along unnoticed, which is why the distance journeys were changed the same way in 4f01fbd. The disadvantaged journey computes the residual itself and asserts it appears nowhere on the page — the one number the section must never state. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01BvdDKvFFSZuMVDH5fEyTob --- e2e/tests/journeys.spec.ts | 122 +++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) 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); +});