feat(search): filter on phones through one sheet
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m14s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 35s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m18s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m15s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 22s
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m14s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 35s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m18s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m15s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 22s
On phones the results toolbar's filters were a sideways-scrolling row led by "More filters", so phase showed only in part and school type not at all, and the panel "More filters" opened held neither of them. Phones now get a single Filters button beside the folded search summary, counting every applied filter. It opens a bottom sheet with every filter: distance as five segments, then phase, school type, local authority and the secondary-only filters. Changes apply at once, as on desktop, so the footer's "Show N schools" only closes the sheet. Applied filters show as removable chips on a second line, which appears only when something is applied. Desktop and tablet are unchanged. Modal gains dialog semantics, a pinned footer and focus handling, and moves above the pinned toolbar, the floating List/Map button and the comparison toast, which its old z-index sat beneath. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4b54c25943
commit
cf3c773f86
10 files changed
+933
-168
No files matched your search
@@ -0,0 +1,156 @@
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import { FilterBar } from '@/components/FilterBar';
|
||||
|
||||
/*
|
||||
* Phones filter through one "Filters" button and a bottom sheet holding every
|
||||
* filter, rather than a sideways-scrolling row whose later chips (phase, type)
|
||||
* sat off-screen beside a "More filters" panel that did not contain them.
|
||||
* Which markup shows at which width is CSS and invisible to jsdom; these pin
|
||||
* the behaviour and the accessible names.
|
||||
*/
|
||||
|
||||
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', 'Merton'], school_types: ['Community school'], years: [],
|
||||
phases: ['Primary', 'Secondary'], genders: ['Girls', 'Mixed'], admissions_policies: [],
|
||||
};
|
||||
|
||||
const pushedParams = () => new URLSearchParams(push.mock.calls.at(-1)![0].split('?')[1]);
|
||||
|
||||
beforeEach(() => {
|
||||
params = new URLSearchParams('postcode=SW196AR&radius=1');
|
||||
push.mockClear();
|
||||
});
|
||||
|
||||
describe('the phone Filters button', () => {
|
||||
it('sits beside the folded search summary', () => {
|
||||
render(<FilterBar filters={filters} />);
|
||||
const summary = screen.getByRole('button', { name: /Edit search/ });
|
||||
expect(summary.parentElement).toContainElement(screen.getByRole('button', { name: 'Filters' }));
|
||||
});
|
||||
|
||||
it('counts every applied filter, phase and type included', () => {
|
||||
params = new URLSearchParams('postcode=SW196AR&radius=1&phase=primary&school_type=Community+school');
|
||||
render(<FilterBar filters={filters} />);
|
||||
expect(screen.getByRole('button', { name: 'Filters, 2 applied' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('is still offered before anything has been searched', () => {
|
||||
params = new URLSearchParams('local_authority=Wandsworth');
|
||||
render(<FilterBar filters={filters} />);
|
||||
expect(screen.getByRole('button', { name: 'Filters, 1 applied' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('stays out of the hero', () => {
|
||||
render(<FilterBar filters={filters} isHero />);
|
||||
expect(screen.queryByRole('button', { name: /^Filters/ })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('the filter sheet', () => {
|
||||
const openSheet = () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /^Filters/ }));
|
||||
return screen.getByRole('dialog', { name: 'Filters' });
|
||||
};
|
||||
|
||||
it('holds every filter in one place', () => {
|
||||
render(<FilterBar filters={filters} />);
|
||||
const sheet = openSheet();
|
||||
expect(within(sheet).getByRole('radiogroup', { name: 'Distance' })).toBeInTheDocument();
|
||||
for (const name of ['Phase', 'School type', 'Local authority']) {
|
||||
expect(within(sheet).getByRole('combobox', { name })).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
it('shows the secondary-only filters once they apply', () => {
|
||||
params = new URLSearchParams('postcode=SW196AR&radius=1&phase=secondary');
|
||||
render(<FilterBar filters={filters} />);
|
||||
const sheet = openSheet();
|
||||
for (const name of ['Gender', 'Sixth form']) {
|
||||
expect(within(sheet).getByRole('combobox', { name })).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
it('offers distance only for a postcode search', () => {
|
||||
params = new URLSearchParams('search=southmead');
|
||||
render(<FilterBar filters={filters} />);
|
||||
expect(within(openSheet()).queryByRole('radiogroup', { name: 'Distance' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('changes the distance', () => {
|
||||
render(<FilterBar filters={filters} />);
|
||||
const distance = within(openSheet()).getByRole('radiogroup', { name: 'Distance' });
|
||||
expect(within(distance).getByRole('radio', { name: 'Within 1 mile' })).toBeChecked();
|
||||
fireEvent.click(within(distance).getByRole('radio', { name: 'Within 3 miles' }));
|
||||
expect(pushedParams().get('radius')).toBe('3');
|
||||
});
|
||||
|
||||
it('applies a change straight away and stays open for the next one', () => {
|
||||
render(<FilterBar filters={filters} />);
|
||||
const sheet = openSheet();
|
||||
fireEvent.change(within(sheet).getByRole('combobox', { name: 'Phase' }), { target: { value: 'primary' } });
|
||||
expect(pushedParams().get('phase')).toBe('primary');
|
||||
expect(screen.getByRole('dialog', { name: 'Filters' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('closes on the results button, which gives the count', () => {
|
||||
render(<FilterBar filters={filters} resultCount={12} />);
|
||||
fireEvent.click(within(openSheet()).getByRole('button', { name: 'Show 12 schools' }));
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('says so when nothing matches', () => {
|
||||
render(<FilterBar filters={filters} resultCount={0} />);
|
||||
expect(within(openSheet()).getByRole('button', { name: 'No schools match' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clears the filters but keeps the search', () => {
|
||||
params = new URLSearchParams('postcode=SW196AR&radius=1&phase=primary&local_authority=Wandsworth');
|
||||
render(<FilterBar filters={filters} />);
|
||||
fireEvent.click(within(openSheet()).getByRole('button', { name: 'Clear all' }));
|
||||
const next = pushedParams();
|
||||
expect(next.get('phase')).toBeNull();
|
||||
expect(next.get('local_authority')).toBeNull();
|
||||
expect(next.get('postcode')).toBe('SW196AR');
|
||||
expect(next.get('radius')).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('the applied-filter chips', () => {
|
||||
it('appear only when something is applied', () => {
|
||||
render(<FilterBar filters={filters} />);
|
||||
expect(screen.queryByRole('group', { name: 'Applied filters' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('name each filter by its label and remove it on tap', () => {
|
||||
params = new URLSearchParams('postcode=SW196AR&radius=1&phase=primary&gender=girls&has_sixth_form=no');
|
||||
render(<FilterBar filters={filters} />);
|
||||
const chips = screen.getByRole('group', { name: 'Applied filters' });
|
||||
for (const label of ['Primary', 'Girls', 'Without sixth form']) {
|
||||
expect(within(chips).getByRole('button', { name: `Remove filter: ${label}` })).toBeInTheDocument();
|
||||
}
|
||||
fireEvent.click(within(chips).getByRole('button', { name: 'Remove filter: Primary' }));
|
||||
const next = pushedParams();
|
||||
expect(next.get('phase')).toBeNull();
|
||||
expect(next.get('gender')).toBe('girls');
|
||||
expect(next.get('postcode')).toBe('SW196AR');
|
||||
});
|
||||
|
||||
it('carry a Clear all that keeps the search', () => {
|
||||
params = new URLSearchParams('search=southmead&school_type=Community+school');
|
||||
render(<FilterBar filters={filters} />);
|
||||
fireEvent.click(within(screen.getByRole('group', { name: 'Applied filters' }))
|
||||
.getByRole('button', { name: 'Clear all' }));
|
||||
const next = pushedParams();
|
||||
expect(next.get('school_type')).toBeNull();
|
||||
expect(next.get('search')).toBe('southmead');
|
||||
});
|
||||
});
|
||||
@@ -105,11 +105,21 @@ describe('the toolbar filters', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('the phone filter row', () => {
|
||||
describe('the phone chips line', () => {
|
||||
it('drops its "more this way" fade when nothing is left to scroll', () => {
|
||||
params = new URLSearchParams('postcode=SW196AR&radius=1&phase=primary');
|
||||
render(<FilterBar filters={filters} />);
|
||||
// jsdom lays nothing out, so the row reads as not overflowing at all.
|
||||
expect(screen.getByRole('group', { name: 'Filters' }).className).toMatch(/controlsAtEnd/);
|
||||
// jsdom lays nothing out, so the line reads as not overflowing at all.
|
||||
const line = screen.getByRole('group', { name: 'Applied filters' }).parentElement!;
|
||||
expect(line.className).toMatch(/controlsAtEnd/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the phone filter sheet', () => {
|
||||
it('offers the results total', () => {
|
||||
render(<HomeView initialSchools={results()} filters={filters} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Filters' }));
|
||||
expect(screen.getByRole('button', { name: 'Show 1 school' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user