fix(search): offer every filter option, not only those in the results
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m17s
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 1m22s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 15s

School type, gender and admissions took their options from the result
set, which the filter had already narrowed: choose "Girls" and only
"Girls" was offered, so switching to "Boys" meant clearing first. They
now offer the full lists from /api/filters, as phase already did. Local
authority stays scoped to the results, so a postcode search offers the
councils nearby rather than all 153.

Whether gender, sixth form and admissions show was also decided by the
results (any secondary school in them). It is now decided by the phase
alone: hidden for Primary, Nursery and Middle deemed primary, shown
otherwise. Choosing one of those phases clears the three filters, which
would otherwise stay applied with no control showing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
TudorandClaude Opus 5.5 committed 2026-10-02 10:22:39 +01:00
1 parent eb13ab0b5e
commit 0cc4f52816
4 files changed
+168 -16

No files matched your search

@@ -0,0 +1,119 @@
import { fireEvent, render, screen, within } from '@testing-library/react';
import { FilterBar } from '@/components/FilterBar';
import type { ResultFilters } from '@/lib/types';
/*
* A filter's options must not come from the results it is filtering, or
* choosing one leaves only that one on offer: pick "Girls" and "Boys" is gone
* until the filter is cleared. School type, gender and admissions offer the
* full lists, as phase already did. Local authority stays scoped to the
* results, so a postcode search offers the councils nearby rather than 153.
*
* Gender, sixth form and admissions show unless the phase chosen is a primary
* one, so what the results happen to contain never decides which filters
* there are.
*/
let params = new URLSearchParams('postcode=SW196AR&radius=1');
const push = jest.fn();
jest.mock('next/navigation', () => ({
useSearchParams: () => params,
usePathname: () => '/',
useRouter: () => ({ push, replace: jest.fn(), prefetch: jest.fn() }),
}));
jest.mock('@/lib/analytics', () => ({ track: jest.fn() }));
const filters = {
local_authorities: ['Merton', 'Wandsworth'],
school_types: ['Academy converter', 'Community school'], years: [],
phases: ['Middle deemed primary', 'Nursery', 'Primary', 'Secondary', 'All-through'],
genders: ['Boys', 'Girls', 'Mixed'],
admissions_policies: ['Non-selective', 'Selective'],
};
// What the results came back with once narrowed by the chosen filters.
const narrowed: ResultFilters = {
local_authorities: ['Wandsworth'], school_types: ['Community school'],
phases: ['Secondary'], genders: ['Girls'], admissions_policies: ['Non-selective'],
};
const pushedParams = () => new URLSearchParams(push.mock.calls.at(-1)![0].split('?')[1]);
const openSheet = () => {
fireEvent.click(screen.getByRole('button', { name: /^Filters/ }));
return screen.getByRole('dialog', { name: 'Filters' });
};
const optionsOf = (sheet: HTMLElement, name: string) =>
within(within(sheet).getByRole('combobox', { name }))
.getAllByRole('option').map((o) => o.textContent);
beforeEach(() => {
params = new URLSearchParams('postcode=SW196AR&radius=1');
push.mockClear();
});
describe('filter options', () => {
it('offer every school type, gender and admissions policy, whatever the results hold', () => {
params = new URLSearchParams('postcode=SW196AR&radius=1&school_type=Community+school&gender=girls');
render(<FilterBar filters={filters} resultFilters={narrowed} />);
const sheet = openSheet();
expect(optionsOf(sheet, 'School type')).toEqual(['Any school type', 'Academy converter', 'Community school']);
expect(optionsOf(sheet, 'Gender')).toEqual(['Boys, Girls & Mixed', 'Boys', 'Girls', 'Mixed']);
expect(optionsOf(sheet, 'Admissions')).toEqual(['All admissions types', 'Non-selective', 'Selective']);
});
it('keep local authority to the councils in the results', () => {
render(<FilterBar filters={filters} resultFilters={narrowed} />);
expect(optionsOf(openSheet(), 'Local authority')).toEqual(['All Local Authorities', 'Wandsworth']);
});
it('offer the full school types in the desktop row too', () => {
render(<FilterBar filters={filters} resultFilters={narrowed} />);
const row = screen.getByRole('group', { name: 'Filters' });
expect(within(within(row).getByRole('combobox', { name: 'School type' }))
.getAllByRole('option')).toHaveLength(3);
});
});
describe('the secondary-only filters', () => {
const secondaryOnly = ['Gender', 'Sixth form', 'Admissions'];
const shown = (sheet: HTMLElement) =>
secondaryOnly.filter((name) => within(sheet).queryByRole('combobox', { name }));
it('show with any phase, even when no secondary school is in the results', () => {
const primariesOnly = { ...narrowed, phases: ['Primary'], genders: [], admissions_policies: [] };
render(<FilterBar filters={filters} resultFilters={primariesOnly} />);
expect(shown(openSheet())).toEqual(secondaryOnly);
});
it.each(['primary', 'nursery', 'middle deemed primary'])('hide for the %s phase', (phase) => {
params = new URLSearchParams(`postcode=SW196AR&radius=1&phase=${encodeURIComponent(phase)}`);
render(<FilterBar filters={filters} />);
expect(shown(openSheet())).toEqual([]);
});
it.each(['secondary', 'all-through'])('show for the %s phase', (phase) => {
params = new URLSearchParams(`postcode=SW196AR&radius=1&phase=${phase}`);
render(<FilterBar filters={filters} />);
expect(shown(openSheet())).toEqual(secondaryOnly);
});
it('are cleared by choosing a primary phase, rather than left applied and hidden', () => {
params = new URLSearchParams(
'postcode=SW196AR&radius=1&phase=secondary&gender=girls&has_sixth_form=yes&admissions_policy=selective&school_type=Community+school');
render(<FilterBar filters={filters} />);
fireEvent.change(within(openSheet()).getByRole('combobox', { name: 'Phase' }), { target: { value: 'primary' } });
const next = pushedParams();
expect(next.get('phase')).toBe('primary');
for (const key of ['gender', 'has_sixth_form', 'admissions_policy']) expect(next.get(key)).toBeNull();
expect(next.get('school_type')).toBe('Community school');
});
it('are kept when the new phase still has them', () => {
params = new URLSearchParams('postcode=SW196AR&radius=1&phase=secondary&gender=girls');
render(<FilterBar filters={filters} />);
fireEvent.change(within(openSheet()).getByRole('combobox', { name: 'Phase' }), { target: { value: 'all-through' } });
expect(pushedParams().get('gender')).toBe('girls');
});
});