From 4f01fbdedbd41c3d3bd7d6a6fac5cef76a8a70b8 Mon Sep 17 00:00:00 2001 From: Tudor Date: Sun, 23 Aug 2026 10:57:34 +0100 Subject: [PATCH] test(e2e): make the distance journeys fail loudly, not skip quietly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing distance journeys all skip when no school has a published figure, which is right when the feature is off — and wrong when it is supposed to be on and is silently broken, because that shows up as a green run full of skips. The new gate fails in exactly that case. Feature state is read from the data, not from /api/flags: the public proxy denies that path on purpose, since it names unreleased features. Presence of the admission_distance key is the observable effect. Verified against staging, where the feature is currently on: the on-gate passes, the off-gate skips, the existing eight distance journeys are unaffected. One honest caveat — the /api/flags check passes on staging today because that image predates the endpoint, not because the denylist works. The denylist itself is covered by the jest unit test; this is defence in depth and becomes a real assertion once deployed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj --- e2e/tests/journeys.spec.ts | 74 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/e2e/tests/journeys.spec.ts b/e2e/tests/journeys.spec.ts index 7a81e60..e99174d 100644 --- a/e2e/tests/journeys.spec.ts +++ b/e2e/tests/journeys.spec.ts @@ -1226,6 +1226,27 @@ const CUTOFF_CANDIDATE_URNS = [ 101099, 100553, 102574, 100769, // mixed ]; +/** + * Whether the last-distance-offered feature is switched on here. + * + * Read from the data rather than from /api/flags, which the public proxy + * denies on purpose — the endpoint names unreleased features. The observable + * effect is the field's presence: the flag is off iff no candidate school + * carries an `admission_distance` key at all. + * + * The distinction that matters: `admission_distance: null` means this school + * has no published cut-off, and the key being ABSENT means cut-offs are not + * being published at all. + */ +async function distanceFeatureIsOn(page: Page): Promise { + for (const urn of CUTOFF_CANDIDATE_URNS) { + const res = await page.request.get(`/api/schools/${urn}`); + if (!res.ok()) continue; + if ('admission_distance' in (await res.json())) return true; + } + return false; +} + async function schoolWithCutoff(page: Page) { for (const urn of CUTOFF_CANDIDATE_URNS) { const res = await page.request.get(`/api/schools/${urn}`); @@ -1237,6 +1258,59 @@ async function schoolWithCutoff(page: Page) { return null; } +test('when the distance feature is on, a school with a cut-off is findable', async ({ page }) => { + /* + * The gate that stops the other distance journeys passing vacuously. + * + * They all skip when schoolWithCutoff() finds nothing, which is right when + * the feature is off — but it means a feature that is *supposed* to be on + * and is silently broken shows up as a green run full of skips. This test + * fails in that case. + */ + test.skip(!(await distanceFeatureIsOn(page)), + 'the admission_distance flag is off in this environment'); + + expect(await schoolWithCutoff(page), + 'the distance feature is on, but no candidate school has a cut-off — ' + + 'the flag is on and the data or the query behind it is broken') + .not.toBeNull(); +}); + +test('with the distance feature off, the section is absent rather than empty', async ({ page }) => { + // Shipping dark means the page renders as it did before the feature existed, + // not as a feature with its content removed. + test.skip(await distanceFeatureIsOn(page), + 'the admission_distance flag is on in this environment'); + + // A school that exists, found rather than hardcoded — a 404 page would + // satisfy the absent-heading assertion without proving anything. + // + // A plain loop, not Array.find: find's predicate is synchronous, so an async + // one returns a Promise, every Promise is truthy, and it would always hand + // back the first URN whether or not that school exists. + let urn: number | null = null; + for (const candidate of CUTOFF_CANDIDATE_URNS) { + if ((await page.request.get(`/api/schools/${candidate}`)).ok()) { + urn = candidate; + break; + } + } + expect(urn, 'no candidate school resolves in this environment').not.toBeNull(); + + await page.goto(`/school/${urn}`); + await expect(page.locator('h1')).toBeVisible(); + + await expect(page.getByRole('heading', { name: /How far away are you\?/ })) + .toHaveCount(0); +}); + +test('/api/flags is not reachable from the public internet', async ({ page }) => { + // It names every unreleased feature and whether it is on. Next reads it + // server-side over the Docker network; the public proxy must deny it. + const res = await page.request.get('/api/flags'); + expect(res.status()).toBe(404); +}); + test('a published cut-off distance is shown with the year it belongs to', async ({ page }) => { const found = await schoolWithCutoff(page); test.skip(found === null, 'no school in the sample has a published cut-off distance yet');