diff --git a/e2e/tests/journeys.spec.ts b/e2e/tests/journeys.spec.ts index 966f2ab..e006c5d 100644 --- a/e2e/tests/journeys.spec.ts +++ b/e2e/tests/journeys.spec.ts @@ -1304,6 +1304,52 @@ test('with the distance feature off, the section is absent rather than empty', a .toHaveCount(0); }); +/** + * A secondary school carrying an EES admissions row, which is what makes its + * Admissions section render while the distance feature is dark. + */ +async function secondarySchoolWithAdmissions(page: Page) { + const list = await page.request.get('/api/schools?phase=secondary&page_size=40'); + if (!list.ok()) return null; + const body = await list.json(); + for (const s of (body?.schools ?? []).slice(0, 25)) { + const res = await page.request.get(`/api/schools/${s.urn}`); + if (!res.ok()) continue; + const detail = await res.json(); + if (detail?.admissions == null) continue; + return { urn: s.urn as number }; + } + return null; +} + +test('with the distance feature off, a secondary page makes no claim about publication', async ({ page }) => { + /* + * Shipping dark must not put words in the council's mouth. The secondary + * template is the only one that words the absence, and "X has not published + * a cut-off distance for this school" is false wherever X does publish and + * we are simply withholding it. + * + * This is why the API omits the key rather than sending null: absent means + * "cut-offs are not published at all", null means "this school has none". + * Only the second is a fact about the school, and only the second is sayable. + */ + test.skip(await distanceFeatureIsOn(page), + 'the admission_distance flag is on in this environment'); + + const found = await secondarySchoolWithAdmissions(page); + test.skip(found === null, 'no secondary school in the sample has an admissions row'); + + await page.goto(`/school/${found!.urn}`); + await expect(page.locator('h1').first()).toBeVisible({ timeout: 15_000 }); + + // The Admissions section is still there — this is not a test that the whole + // section vanished, which would pass for the wrong reason. + await expect(page.locator('#admissions')).toHaveCount(1); + + await expect(page.getByText(/has not published a cut-off distance/)).toHaveCount(0); + await expect(page.getByText(/Contact the admissions authority/)).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. diff --git a/nextjs-app/__tests__/components/lastDistanceOffered.test.tsx b/nextjs-app/__tests__/components/lastDistanceOffered.test.tsx index 9f16df5..3f9efa1 100644 --- a/nextjs-app/__tests__/components/lastDistanceOffered.test.tsx +++ b/nextjs-app/__tests__/components/lastDistanceOffered.test.tsx @@ -98,6 +98,17 @@ describe('secondary detail page', () => { expect(screen.getByText(/has not published a cut-off distance/)).toBeInTheDocument(); }); + + it('makes no claim about publication when the feature is switched off', () => { + // Absent, not null. The API omits the key entirely while the + // admission_distance flag is off, and "Islington has not published a + // cut-off distance" is then a statement about us, not about Islington — + // false wherever the authority does publish one. + renderSecondarySchoolDetail({ ...secondaryFixture, admissionDistance: undefined }); + + expect(screen.queryByText(/has not published a cut-off distance/)).not.toBeInTheDocument(); + expect(screen.queryByText(/Contact the admissions authority/)).not.toBeInTheDocument(); + }); }); // ── The Distance section ─────────────────────────────────────────────── diff --git a/nextjs-app/app/school/[slug]/page.tsx b/nextjs-app/app/school/[slug]/page.tsx index 4e9f6aa..bef695a 100644 --- a/nextjs-app/app/school/[slug]/page.tsx +++ b/nextjs-app/app/school/[slug]/page.tsx @@ -232,7 +232,7 @@ export default async function SchoolPage({ params }: SchoolPageProps) { census={census ?? null} admissions={admissions ?? null} admissionsHistory={admissions_history ?? []} - admissionDistance={admission_distance ?? null} + admissionDistance={admission_distance} deprivation={deprivation ?? null} finance={finance ?? null} nationalAvg={nationalAvg} diff --git a/nextjs-app/components/school/DistanceSection.tsx b/nextjs-app/components/school/DistanceSection.tsx index 8990960..45651b5 100644 --- a/nextjs-app/components/school/DistanceSection.tsx +++ b/nextjs-app/components/school/DistanceSection.tsx @@ -24,7 +24,7 @@ export function DistanceSection({ admissionDistance, schoolInfo, }: { - admissionDistance: SchoolAdmissionDistance | null; + admissionDistance: SchoolAdmissionDistance | null | undefined; schoolInfo: School; }) { // Without a figure there is nothing to compare against, and without diff --git a/nextjs-app/components/school/SecondaryAdmissionsSection.tsx b/nextjs-app/components/school/SecondaryAdmissionsSection.tsx index 49c8970..63471dc 100644 --- a/nextjs-app/components/school/SecondaryAdmissionsSection.tsx +++ b/nextjs-app/components/school/SecondaryAdmissionsSection.tsx @@ -21,11 +21,17 @@ export function SecondaryAdmissionsSection({ published cut-off and no EES admissions row. */ admissions: SchoolAdmissions | null; admissionsHistory: SchoolAdmissions[]; - admissionDistance: SchoolAdmissionDistance | null; + admissionDistance: SchoolAdmissionDistance | null | undefined; schoolInfo: School; hasSixthForm: boolean; }) { const cutoff = describeCutoff(admissionDistance); + /* Absent means cut-offs are not being published at all; null means this + school has no published cut-off. Only the second is a fact about the + school, and only the second can be stated. Saying "X has not published a + cut-off" while the feature is dark describes us, and is false wherever the + authority does publish one. */ + const featureOn = admissionDistance !== undefined; // Moved with this section from SecondarySchoolDetailView, its only consumer. const admissionsTag = (() => { const policy = schoolInfo.admissions_policy?.toLowerCase() ?? ''; @@ -102,7 +108,7 @@ export function SecondaryAdmissionsSection({ {CUTOFF_NOTE} {CUTOFF_MEASUREMENT_NOTE} {cutoff.routeNote && <> {cutoff.routeNote}>}
- ) : ( + ) : featureOn ? ({describeCutoffAbsence({ localAuthority: schoolInfo.local_authority, @@ -110,7 +116,7 @@ export function SecondaryAdmissionsSection({ admissionsHistory, })}
- )} + ) : null} {hasSixthForm && (