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(); }); });