import { act, fireEvent, render, screen, within } from '@testing-library/react';
import { HomeView } from '@/components/HomeView';
import { FilterBar } from '@/components/FilterBar';
import { fetchSchools } from '@/lib/api';
import { track } from '@/lib/analytics';
import { primaryFixture } from '../support/schoolFixtures';
import type { SchoolsResponse } from '@/lib/types';
/*
* The results toolbar (option B of the 2026-09-30 results-controls mockups):
* search, filters and the List/Map switch pinned under the header, with a
* floating List/Map button standing in for the switch on phones. Layout is CSS
* and not visible to jsdom; these pin the behaviour and the accessible names
* the E2E journeys rely on.
*/
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('@/context/ComparisonContext', () => ({
useComparisonContext: () => ({ addSchool: jest.fn(), removeSchool: jest.fn(), selectedSchools: [] }),
}));
jest.mock('@/lib/api', () => ({
fetchSchools: jest.fn(),
fetchNationalAverages: jest.fn(async () => ({})),
fetchLAaverages: jest.fn(async () => ({ secondary: { attainment_8_by_la: {} } })),
}));
jest.mock('@/lib/analytics', () => ({ track: jest.fn() }));
jest.mock('@/components/SchoolMap', () => ({ SchoolMap: () =>
}));
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 {
return { schools: [{ ...primaryFixture.schoolInfo, school_name: 'Southmead Primary School' }],
total: 1, page: 1, page_size: 25, total_pages: 1 };
}
beforeEach(() => {
params = new URLSearchParams('postcode=SW196AR&radius=1');
push.mockClear();
jest.mocked(track).mockClear();
jest.mocked(fetchSchools).mockReset().mockResolvedValue(results());
});
describe('the List/Map switch', () => {
it('lives in the toolbar with the filters and says which view is on', () => {
render();
const view = screen.getByRole('group', { name: 'Results view' });
expect(view.closest('div[class*="resultsToolbar"]')).not.toBeNull();
// A postcode search opens on the map.
expect(screen.getByRole('button', { name: 'Map' })).toHaveAttribute('aria-pressed', 'true');
expect(screen.getByRole('button', { name: 'List' })).toHaveAttribute('aria-pressed', 'false');
});
it('has a floating twin that flips between map and list', async () => {
render();
await act(async () => {});
fireEvent.click(screen.getByRole('button', { name: 'Show list' }));
expect(screen.queryByTestId('map')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'List' })).toHaveAttribute('aria-pressed', 'true');
expect(track).toHaveBeenCalledWith('results_view_changed', { view: 'list', via: 'floating' });
await act(async () => fireEvent.click(screen.getByRole('button', { name: 'Show map' })));
expect(screen.getByTestId('map')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Show list' })).toBeInTheDocument();
});
it('does not track a click on the view already showing', async () => {
render();
await act(async () => {});
fireEvent.click(screen.getByRole('button', { name: 'Map' }));
expect(track).not.toHaveBeenCalledWith('results_view_changed', expect.anything());
});
it('is absent from a name search, which has no map', () => {
params = new URLSearchParams('search=southmead');
render();
expect(screen.queryByRole('group', { name: 'Results view' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Show map' })).not.toBeInTheDocument();
});
});
describe('the toolbar filters', () => {
it('keeps distance, phase and school type in the row, not behind More filters', () => {
render();
const row = screen.getByRole('group', { name: 'Filters' });
for (const name of ['Distance', 'Phase', 'School type']) {
expect(row).toContainElement(screen.getByRole('combobox', { name }));
}
expect(screen.getByRole('combobox', { name: 'Distance' })).toHaveDisplayValue('Within 1 mile');
expect(screen.queryByRole('combobox', { name: 'Local authority' })).not.toBeInTheDocument();
});
it('counts only what More filters hides', () => {
params = new URLSearchParams('postcode=SW196AR&radius=1&school_type=council&local_authority=Wandsworth');
render();
expect(screen.getByRole('button', { name: /More filters \(1\)/ })).toBeInTheDocument();
});
});
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();
// 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();
fireEvent.click(screen.getByRole('button', { name: 'Filters' }));
expect(screen.getByRole('button', { name: 'Show 1 school' })).toBeInTheDocument();
});
});
describe('the folded search', () => {
it('summarises the search and unfolds on tap', () => {
render();
const summary = screen.getByRole('button', { name: 'Edit search: SW196AR, within 1 mile' });
fireEvent.click(summary);
expect(screen.queryByRole('button', { name: /Edit search/ })).not.toBeInTheDocument();
});
it('folds again once the edited search is submitted', () => {
render();
fireEvent.click(screen.getByRole('button', { name: /Edit search/ }));
const input = screen.getByRole('searchbox', { name: 'School name or postcode' });
fireEvent.change(input, { target: { value: 'SW19 1AA' } });
fireEvent.submit(input.closest('form')!);
expect(screen.getByRole('button', { name: /Edit search/ })).toBeInTheDocument();
});
it('refolds and shows the new text when the search changes some other way', () => {
const view = render();
fireEvent.click(screen.getByRole('button', { name: /Edit search/ }));
fireEvent.change(screen.getByRole('searchbox', { name: 'School name or postcode' }),
{ target: { value: 'half-typed' } });
// Back button: the URL changes under the component, nothing is submitted.
params = new URLSearchParams('postcode=SW170AA&radius=3');
view.rerender();
expect(screen.getByRole('button', { name: 'Edit search: SW170AA, within 3 miles' }))
.toBeInTheDocument();
expect(screen.getByRole('searchbox', { name: 'School name or postcode' }))
.toHaveValue('SW170AA');
});
it('never appears in the hero, or before anything has been searched', () => {
const { unmount } = render();
expect(screen.queryByRole('button', { name: /Edit search/ })).not.toBeInTheDocument();
unmount();
params = new URLSearchParams('local_authority=Wandsworth');
render();
expect(screen.queryByRole('button', { name: /Edit search/ })).not.toBeInTheDocument();
});
});
describe('the results list', () => {
it('repeats no applied filters above the results; the filter bar shows them', () => {
params = new URLSearchParams('search=southmead&school_type=council&local_authority=Wandsworth');
const { container } = render();
expect(container.querySelector('[class*="activeFilters"]')).toBeNull();
expect(screen.queryByText('Search: southmead')).not.toBeInTheDocument();
});
});
describe('the desktop Clear all', () => {
const row = () => screen.getByRole('group', { name: 'Filters' });
it('removes the filters and keeps the search, rather than going home', () => {
params = new URLSearchParams('postcode=SW196AR&radius=3&phase=primary&school_type=council&local_authority=Wandsworth');
render();
fireEvent.click(within(row()).getByRole('button', { name: 'Clear all' }));
const pushed = push.mock.calls.at(-1)![0] as string;
const next = new URLSearchParams(pushed.split('?')[1]);
expect(next.get('postcode')).toBe('SW196AR');
expect(next.get('radius')).toBe('3');
for (const key of ['phase', 'school_type', 'local_authority']) expect(next.get(key)).toBeNull();
});
it('is not offered when only a search is applied', () => {
params = new URLSearchParams('search=southmead');
render();
expect(within(row()).queryByRole('button', { name: /^Clear/ })).not.toBeInTheDocument();
});
});