test(e2e): destination journeys, including the no-bar rule
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m5s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 19s
PR Checks / Build Frontend (no push) (pull_request) Successful in 47s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m16s
PR Checks / AI Code Review (Claude) (pull_request) Canceled after 1m10s
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m5s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 19s
PR Checks / Build Frontend (no push) (pull_request) Successful in 47s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m16s
PR Checks / AI Code Review (Claude) (pull_request) Canceled after 1m10s
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvdDKvFFSZuMVDH5fEyTob
This commit is contained in:
1 parent
2b4cf20d75
commit
ccd5074c90
1 file changed
+122
@@ -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);
|
||||
});
|
||||
Reference in new issue
Block a user