2026-09-30 18:35:37 +01:00
|
|
|
import { act, fireEvent, render, screen } 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: () => <div data-testid="map" /> }));
|
|
|
|
|
|
|
|
|
|
const filters = {
|
|
|
|
|
local_authorities: ['Wandsworth'], school_types: ['Community school'], years: [],
|
|
|
|
|
phases: ['Primary', 'Secondary'], genders: [], admissions_policies: [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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(<HomeView initialSchools={results()} filters={filters} />);
|
|
|
|
|
const view = screen.getByRole('group', { name: 'Results view' });
|
|
|
|
|
expect(view.closest('div[class*="resultsToolbar"]')).not.toBeNull();
|
|
|
|
|
expect(screen.getByRole('button', { name: 'List' })).toHaveAttribute('aria-pressed', 'true');
|
|
|
|
|
expect(screen.getByRole('button', { name: 'Map' })).toHaveAttribute('aria-pressed', 'false');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('has a floating twin that flips between map and list', async () => {
|
|
|
|
|
render(<HomeView initialSchools={results()} filters={filters} />);
|
|
|
|
|
await act(async () => fireEvent.click(screen.getByRole('button', { name: 'Show map' })));
|
|
|
|
|
expect(screen.getByTestId('map')).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByRole('button', { name: 'Map' })).toHaveAttribute('aria-pressed', 'true');
|
|
|
|
|
expect(track).toHaveBeenCalledWith('results_view_changed', { view: 'map', via: 'floating' });
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Show list' }));
|
|
|
|
|
expect(screen.queryByTestId('map')).not.toBeInTheDocument();
|
|
|
|
|
expect(screen.getByRole('button', { name: 'Show map' })).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('does not track a click on the view already showing', () => {
|
|
|
|
|
render(<HomeView initialSchools={results()} filters={filters} />);
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'List' }));
|
|
|
|
|
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(<HomeView initialSchools={results()} filters={filters} />);
|
|
|
|
|
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(<FilterBar filters={filters} />);
|
|
|
|
|
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=Community+school&local_authority=Wandsworth');
|
|
|
|
|
render(<FilterBar filters={filters} />);
|
|
|
|
|
expect(screen.getByRole('button', { name: /More filters \(1\)/ })).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-30 20:47:57 +01:00
|
|
|
describe('the phone filter row', () => {
|
|
|
|
|
it('drops its "more this way" fade when nothing is left to scroll', () => {
|
|
|
|
|
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/);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-30 18:35:37 +01:00
|
|
|
describe('the folded search', () => {
|
|
|
|
|
it('summarises the search and unfolds on tap', () => {
|
|
|
|
|
render(<FilterBar filters={filters} />);
|
|
|
|
|
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(<FilterBar filters={filters} />);
|
|
|
|
|
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();
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-30 20:47:57 +01:00
|
|
|
it('refolds and shows the new text when the search changes some other way', () => {
|
|
|
|
|
const view = render(<FilterBar filters={filters} />);
|
|
|
|
|
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(<FilterBar filters={filters} />);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByRole('button', { name: 'Edit search: SW170AA, within 3 miles' }))
|
|
|
|
|
.toBeInTheDocument();
|
|
|
|
|
expect(screen.getByRole('searchbox', { name: 'School name or postcode' }))
|
|
|
|
|
.toHaveValue('SW170AA');
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-30 18:35:37 +01:00
|
|
|
it('never appears in the hero, or before anything has been searched', () => {
|
|
|
|
|
const { unmount } = render(<FilterBar filters={filters} isHero />);
|
|
|
|
|
expect(screen.queryByRole('button', { name: /Edit search/ })).not.toBeInTheDocument();
|
|
|
|
|
unmount();
|
|
|
|
|
params = new URLSearchParams('local_authority=Wandsworth');
|
|
|
|
|
render(<FilterBar filters={filters} />);
|
|
|
|
|
expect(screen.queryByRole('button', { name: /Edit search/ })).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|