From 68452681f8d5a2c4dfbf105794bb7890f9cb3fbd Mon Sep 17 00:00:00 2001 From: Tudor Date: Fri, 2 Oct 2026 10:28:34 +0100 Subject: [PATCH] fix(search): match primary phases by their words, not exact labels The secondary-only filters were hidden for an exact set of lowercased labels, so a change of case, hyphen or spacing in a GIAS phase label would have left them showing. Phases are now matched on their words, as compareGroups already reads "primary". The E2E journey no longer counts school types: it asserts that choosing one leaves the same list on offer, whatever the data holds. A test now pins that a filter with no options is left out, not shown with only its "any" option. Co-Authored-By: Claude Opus 5.5 --- e2e/tests/journeys.spec.ts | 8 +++++--- .../components/FilterBarOptions.test.tsx | 10 ++++++++-- nextjs-app/components/FilterBar.tsx | 17 +++++++++++++---- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/e2e/tests/journeys.spec.ts b/e2e/tests/journeys.spec.ts index a5cc567..c168add 100644 --- a/e2e/tests/journeys.spec.ts +++ b/e2e/tests/journeys.spec.ts @@ -296,11 +296,13 @@ test('school type and gender switch straight to another value', async ({ page }) await gender.selectOption('boys'); await expect(page).toHaveURL(/[?&]gender=boys(&|$)/); + // Whatever types the data holds, choosing one leaves the same list on offer. const type = page.getByRole('combobox', { name: 'School type', exact: true }); - const [first] = (await type.locator('option').allTextContents()).slice(1); - await type.selectOption(first); + const offered = await type.locator('option').allTextContents(); + expect(offered.length, 'no school type to choose').toBeGreaterThan(1); + await type.selectOption(offered[1]); await expect(page).toHaveURL(/[?&]school_type=/); - await expect.poll(() => type.locator('option').count()).toBeGreaterThan(2); + await expect.poll(() => type.locator('option').allTextContents()).toEqual(offered); }); test('a phase outside primary/secondary filters to that phase, not to everything', async ({ page }) => { diff --git a/nextjs-app/__tests__/components/FilterBarOptions.test.tsx b/nextjs-app/__tests__/components/FilterBarOptions.test.tsx index 509b26f..35355b4 100644 --- a/nextjs-app/__tests__/components/FilterBarOptions.test.tsx +++ b/nextjs-app/__tests__/components/FilterBarOptions.test.tsx @@ -87,13 +87,19 @@ describe('the secondary-only filters', () => { expect(shown(openSheet())).toEqual(secondaryOnly); }); - it.each(['primary', 'nursery', 'middle deemed primary'])('hide for the %s phase', (phase) => { + it.each(['primary', 'nursery', 'middle deemed primary', 'Middle-deemed Primary'])( + 'hide for the %s phase', (phase) => { params = new URLSearchParams(`postcode=SW196AR&radius=1&phase=${encodeURIComponent(phase)}`); render(); expect(shown(openSheet())).toEqual([]); }); - it.each(['secondary', 'all-through'])('show for the %s phase', (phase) => { + it('leave out a filter with no options, rather than show only its "any"', () => { + render(); + expect(shown(openSheet())).toEqual(['Sixth form']); + }); + + it.each(['secondary', 'all-through', 'middle deemed secondary', '16 plus'])('show for the %s phase', (phase) => { params = new URLSearchParams(`postcode=SW196AR&radius=1&phase=${phase}`); render(); expect(shown(openSheet())).toEqual(secondaryOnly); diff --git a/nextjs-app/components/FilterBar.tsx b/nextjs-app/components/FilterBar.tsx index d2cc2ab..4f406c3 100644 --- a/nextjs-app/components/FilterBar.tsx +++ b/nextjs-app/components/FilterBar.tsx @@ -69,10 +69,19 @@ type FilterKey = (typeof FILTER_KEYS)[number]; /** Filters that only mean something for schools teaching beyond primary. */ const SECONDARY_ONLY_KEYS = ["gender", "has_sixth_form", "admissions_policy"] as const; -/** Phases with no secondary-age pupils, for which those filters are hidden. */ -const PRIMARY_PHASES = new Set(["primary", "nursery", "middle deemed primary"]); - -const hasSecondaryFilters = (phase: string) => !PRIMARY_PHASES.has(phase.toLowerCase()); +/** + * False for a phase with no secondary-age pupils, for which those filters are + * hidden: Primary, Nursery and Middle deemed primary. Matched on the words, + * not exact labels, so a change of case, hyphen or spacing in the GIAS label + * cannot leave them showing. The same reading of "primary" as + * compareGroups in lib/compareLogic.ts. The API's PHASE_GROUPS answers a + * different question (which phases a phase filter returns: primary includes + * all-through), so it is not this list. + */ +function hasSecondaryFilters(phase: string): boolean { + const p = phase.toLowerCase().replace(/[^a-z]+/g, " ").trim(); + return !(p === "nursery" || (p.includes("primary") && !p.includes("secondary"))); +} function SlidersIcon() { return (