feat(search): school type groups and a faith filter
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
15b8923e60
commit
17bfb4a3f7
8 files changed
+190
-18
No files matched your search
@@ -5,7 +5,7 @@ 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
|
||||
* until the filter is cleared. School type groups, 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.
|
||||
*
|
||||
@@ -29,6 +29,10 @@ const filters = {
|
||||
phases: ['Middle deemed primary', 'Nursery', 'Primary', 'Secondary', 'All-through'],
|
||||
genders: ['Boys', 'Girls', 'Mixed'],
|
||||
admissions_policies: ['Non-selective', 'Selective'],
|
||||
school_type_groups: [
|
||||
{ value: 'academy', label: 'State school: academy or free school' },
|
||||
{ value: 'council', label: 'State school: council-run' },
|
||||
],
|
||||
};
|
||||
|
||||
// What the results came back with once narrowed by the chosen filters.
|
||||
@@ -54,11 +58,11 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
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');
|
||||
it('offer every school type group, gender and admissions policy, whatever the results hold', () => {
|
||||
params = new URLSearchParams('postcode=SW196AR&radius=1&school_type=council&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, 'School type')).toEqual(['Any school type', 'State school: academy or free school', 'State school: council-run']);
|
||||
expect(optionsOf(sheet, 'Gender')).toEqual(['Boys, Girls & Mixed', 'Boys', 'Girls', 'Mixed']);
|
||||
expect(optionsOf(sheet, 'Admissions')).toEqual(['All admissions types', 'Non-selective', 'Selective']);
|
||||
});
|
||||
@@ -107,13 +111,13 @@ describe('the secondary-only filters', () => {
|
||||
|
||||
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');
|
||||
'postcode=SW196AR&radius=1&phase=secondary&gender=girls&has_sixth_form=yes&admissions_policy=selective&school_type=council');
|
||||
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');
|
||||
expect(next.get('school_type')).toBe('council');
|
||||
});
|
||||
|
||||
it('are kept when the new phase still has them', () => {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import { FilterBar } from '@/components/FilterBar';
|
||||
import { track } from '@/lib/analytics';
|
||||
|
||||
/*
|
||||
* School type offers six groups a parent recognises, not GIAS's 34 types, and
|
||||
* a Faith filter sits beside it (spec 2026-10-02-school-type-groups-and-faith-
|
||||
* filter-design.md). Both lists come from /api/filters.
|
||||
*/
|
||||
|
||||
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: ['Wandsworth'], school_types: ['Community school'], years: [],
|
||||
phases: ['Primary', 'Secondary'], genders: [], admissions_policies: [],
|
||||
school_type_groups: [
|
||||
{ value: 'academy', label: 'State school: academy or free school' },
|
||||
{ value: 'council', label: 'State school: council-run' },
|
||||
{ value: 'special', label: 'Special school (SEND)' },
|
||||
],
|
||||
faiths: [
|
||||
{ value: 'none', label: 'No religious character' },
|
||||
{ value: 'roman_catholic', label: 'Roman Catholic' },
|
||||
],
|
||||
};
|
||||
|
||||
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 = (scope: HTMLElement, name: string) =>
|
||||
within(within(scope).getByRole('combobox', { name })).getAllByRole('option').map((o) => o.textContent);
|
||||
|
||||
beforeEach(() => {
|
||||
params = new URLSearchParams('postcode=SW196AR&radius=1');
|
||||
push.mockClear();
|
||||
jest.mocked(track).mockClear();
|
||||
});
|
||||
|
||||
describe('School type', () => {
|
||||
it('offers the groups, not the GIAS types, on desktop and in the sheet', () => {
|
||||
render(<FilterBar filters={filters} />);
|
||||
const groups = ['Any school type', 'State school: academy or free school',
|
||||
'State school: council-run', 'Special school (SEND)'];
|
||||
expect(optionsOf(screen.getByRole('group', { name: 'Filters' }), 'School type')).toEqual(groups);
|
||||
expect(optionsOf(openSheet(), 'School type')).toEqual(groups);
|
||||
});
|
||||
|
||||
it('puts the group key in the URL and names the chip by its label', () => {
|
||||
const view = render(<FilterBar filters={filters} />);
|
||||
fireEvent.change(within(openSheet()).getByRole('combobox', { name: 'School type' }),
|
||||
{ target: { value: 'special' } });
|
||||
expect(pushedParams().get('school_type')).toBe('special');
|
||||
params = new URLSearchParams('postcode=SW196AR&radius=1&school_type=special');
|
||||
view.rerender(<FilterBar filters={filters} />);
|
||||
expect(screen.getByRole('button', { name: 'Remove filter: Special school (SEND)' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('names an old raw-label link\'s chip by that label', () => {
|
||||
params = new URLSearchParams('postcode=SW196AR&radius=1&school_type=Community+school');
|
||||
render(<FilterBar filters={filters} />);
|
||||
expect(screen.getByRole('button', { name: 'Remove filter: Community school' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('is left out when the API sends no groups', () => {
|
||||
render(<FilterBar filters={{ ...filters, school_type_groups: undefined }} />);
|
||||
expect(screen.queryByRole('combobox', { name: 'School type' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Faith', () => {
|
||||
it('offers its options in the More filters panel and in the sheet', () => {
|
||||
render(<FilterBar filters={filters} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /More filters/ }));
|
||||
const faiths = ['Any faith or none', 'No religious character', 'Roman Catholic'];
|
||||
expect(optionsOf(document.body, 'Faith')).toEqual(faiths);
|
||||
expect(optionsOf(openSheet(), 'Faith')).toEqual(faiths);
|
||||
});
|
||||
|
||||
it('shows as a chip, counts on both buttons, and clears with Clear all', () => {
|
||||
params = new URLSearchParams('postcode=SW196AR&radius=1&faith=roman_catholic');
|
||||
render(<FilterBar filters={filters} />);
|
||||
expect(screen.getByRole('button', { name: 'Remove filter: Roman Catholic' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Filters, 1 applied' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /More filters \(1\)/ })).toBeInTheDocument();
|
||||
fireEvent.click(within(screen.getByRole('group', { name: 'Applied filters' }))
|
||||
.getByRole('button', { name: 'Clear all' }));
|
||||
expect(pushedParams().get('faith')).toBeNull();
|
||||
expect(pushedParams().get('postcode')).toBe('SW196AR');
|
||||
});
|
||||
|
||||
it('goes into the search analytics event', () => {
|
||||
params = new URLSearchParams('faith=roman_catholic');
|
||||
render(<FilterBar filters={filters} />);
|
||||
const input = screen.getByRole('searchbox', { name: 'School name or postcode' });
|
||||
fireEvent.change(input, { target: { value: 'st marys' } });
|
||||
fireEvent.submit(input.closest('form')!);
|
||||
expect(track).toHaveBeenCalledWith('search_submitted',
|
||||
expect.objectContaining({ filters_active: 'faith=roman_catholic', filters_count: 1 }));
|
||||
});
|
||||
|
||||
it('is left out when the API sends no faiths', () => {
|
||||
render(<FilterBar filters={{ ...filters, faiths: [] }} />);
|
||||
expect(within(openSheet()).queryByRole('combobox', { name: 'Faith' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,7 @@ jest.mock('@/lib/analytics', () => ({ track: jest.fn() }));
|
||||
const filters = {
|
||||
local_authorities: ['Wandsworth', 'Merton'], school_types: ['Community school'], years: [],
|
||||
phases: ['Primary', 'Secondary'], genders: ['Girls', 'Mixed'], admissions_policies: [],
|
||||
school_type_groups: [{ value: 'council', label: 'State school: council-run' }],
|
||||
};
|
||||
|
||||
const pushedParams = () => new URLSearchParams(push.mock.calls.at(-1)![0].split('?')[1]);
|
||||
@@ -38,7 +39,7 @@ describe('the phone Filters button', () => {
|
||||
});
|
||||
|
||||
it('counts every applied filter, phase and type included', () => {
|
||||
params = new URLSearchParams('postcode=SW196AR&radius=1&phase=primary&school_type=Community+school');
|
||||
params = new URLSearchParams('postcode=SW196AR&radius=1&phase=primary&school_type=council');
|
||||
render(<FilterBar filters={filters} />);
|
||||
expect(screen.getByRole('button', { name: 'Filters, 2 applied' })).toBeInTheDocument();
|
||||
});
|
||||
@@ -145,7 +146,7 @@ describe('the applied-filter chips', () => {
|
||||
});
|
||||
|
||||
it('carry a Clear all that keeps the search', () => {
|
||||
params = new URLSearchParams('search=southmead&school_type=Community+school');
|
||||
params = new URLSearchParams('search=southmead&school_type=council');
|
||||
render(<FilterBar filters={filters} />);
|
||||
fireEvent.click(within(screen.getByRole('group', { name: 'Applied filters' }))
|
||||
.getByRole('button', { name: 'Clear all' }));
|
||||
|
||||
@@ -27,6 +27,7 @@ jest.mock('@/lib/analytics', () => ({ track: jest.fn() }));
|
||||
const filters = {
|
||||
local_authorities: ['Wandsworth'], school_types: ['Community school'], years: [],
|
||||
phases: ['Primary', 'Secondary'], genders: [], admissions_policies: [],
|
||||
school_type_groups: [{ value: 'council', label: 'State school: council-run' }],
|
||||
};
|
||||
|
||||
const pushedParams = () => new URLSearchParams(push.mock.calls.at(-1)![0].split('?')[1]);
|
||||
@@ -53,10 +54,10 @@ it('builds a second change on the first, not on the URL it has not reached', ()
|
||||
const sheet = openSheet();
|
||||
fireEvent.change(within(sheet).getByRole('combobox', { name: 'Phase' }), { target: { value: 'primary' } });
|
||||
fireEvent.change(within(sheet).getByRole('combobox', { name: 'School type' }),
|
||||
{ target: { value: 'Community school' } });
|
||||
{ target: { value: 'council' } });
|
||||
const next = pushedParams();
|
||||
expect(next.get('phase')).toBe('primary');
|
||||
expect(next.get('school_type')).toBe('Community school');
|
||||
expect(next.get('school_type')).toBe('council');
|
||||
expect(next.get('postcode')).toBe('SW196AR');
|
||||
});
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ jest.mock('@/components/SchoolMap', () => ({ SchoolMap: () => <div data-testid="
|
||||
const filters = {
|
||||
local_authorities: ['Wandsworth'], school_types: ['Community school'], years: [],
|
||||
phases: ['Primary', 'Secondary'], genders: [], admissions_policies: [],
|
||||
school_type_groups: [{ value: 'council', label: 'State school: council-run' }],
|
||||
};
|
||||
|
||||
function results(): SchoolsResponse {
|
||||
@@ -99,7 +100,7 @@ describe('the toolbar filters', () => {
|
||||
});
|
||||
|
||||
it('counts only what More filters hides', () => {
|
||||
params = new URLSearchParams('postcode=SW196AR&radius=1&school_type=Community+school&local_authority=Wandsworth');
|
||||
params = new URLSearchParams('postcode=SW196AR&radius=1&school_type=council&local_authority=Wandsworth');
|
||||
render(<FilterBar filters={filters} />);
|
||||
expect(screen.getByRole('button', { name: /More filters \(1\)/ })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user