2026-09-30 21:30:33 +01:00
|
|
|
import { act, fireEvent, render, screen, within } from '@testing-library/react';
|
|
|
|
|
import { HomeView } from '@/components/HomeView';
|
|
|
|
|
import { fetchSchools, fetchNationalAverages } from '@/lib/api';
|
|
|
|
|
import { primaryFixture } from '../support/schoolFixtures';
|
|
|
|
|
import type { School, SchoolsResponse } from '@/lib/types';
|
|
|
|
|
|
|
|
|
|
/*
|
2026-09-30 21:50:34 +01:00
|
|
|
* The map view: the list beside the map (mockup B), where every postcode
|
|
|
|
|
* search opens. The map itself is Leaflet and mocked here; what is pinned is what
|
2026-09-30 21:30:33 +01:00
|
|
|
* HomeView hands it and the list it draws beside it.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
let params = new URLSearchParams('postcode=SW196AR&radius=1');
|
|
|
|
|
jest.mock('next/navigation', () => ({
|
|
|
|
|
useSearchParams: () => params,
|
|
|
|
|
usePathname: () => '/',
|
|
|
|
|
useRouter: () => ({ push: jest.fn(), 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(),
|
|
|
|
|
fetchLAaverages: jest.fn(async () => ({ secondary: { attainment_8_by_la: {} } })),
|
|
|
|
|
}));
|
2026-10-01 15:27:14 +01:00
|
|
|
// Renders only the List/Map switch HomeView hands it, which lives in its row.
|
|
|
|
|
jest.mock('@/components/FilterBar', () => ({
|
|
|
|
|
FilterBar: ({ viewSwitch }: { viewSwitch?: unknown }) => viewSwitch || null,
|
|
|
|
|
}));
|
2026-09-30 21:30:33 +01:00
|
|
|
jest.mock('@/components/SchoolMap', () => ({
|
|
|
|
|
SchoolMap: ({ selectedUrn, radiusMiles, onMarkerClick, schools }: {
|
|
|
|
|
selectedUrn: number | null; radiusMiles?: number;
|
|
|
|
|
onMarkerClick: (s: School) => void; schools: School[];
|
|
|
|
|
}) => (
|
|
|
|
|
<div data-testid="map" data-selected={selectedUrn ?? ''} data-radius={radiusMiles}>
|
|
|
|
|
<button type="button" onClick={() => onMarkerClick(schools[1])}>pin</button>
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const base = primaryFixture.schoolInfo;
|
|
|
|
|
const southmead: School = {
|
|
|
|
|
...base, urn: 2, school_name: 'Southmead Primary School', distance: 0.2,
|
|
|
|
|
school_type: 'Community school', rwm_expected_pct: 52, total_pupils: 269,
|
|
|
|
|
};
|
|
|
|
|
const greenmead: School = {
|
|
|
|
|
...base, urn: 3, school_name: 'Greenmead School', distance: 0.2,
|
|
|
|
|
school_type: 'Community special school', rwm_expected_pct: 0,
|
|
|
|
|
reading_expected_pct: 0, writing_expected_pct: 0, maths_expected_pct: 0, total_pupils: 62,
|
|
|
|
|
};
|
|
|
|
|
const ourLady: School = {
|
|
|
|
|
...base, urn: 1, school_name: 'Our Lady Queen of Heaven RC School', distance: 0,
|
|
|
|
|
school_type: 'Voluntary aided school', rwm_expected_pct: 70, total_pupils: 224,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function results(): SchoolsResponse {
|
|
|
|
|
return {
|
|
|
|
|
schools: [ourLady, southmead, greenmead], total: 3, page: 1, page_size: 25, total_pages: 1,
|
|
|
|
|
location_info: { postcode: 'SW196AR', radius: 1.60934, coordinates: [51.42, -0.21] },
|
|
|
|
|
} as SchoolsResponse;
|
|
|
|
|
}
|
|
|
|
|
const filters = { local_authorities: [], school_types: [], years: [], phases: [], genders: [], admissions_policies: [] };
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
params = new URLSearchParams('postcode=SW196AR&radius=1');
|
|
|
|
|
jest.mocked(fetchSchools).mockReset().mockResolvedValue(results());
|
|
|
|
|
jest.mocked(fetchNationalAverages).mockResolvedValue({ primary: { rwm_expected_pct: 62 } } as never);
|
2026-09-30 21:50:34 +01:00
|
|
|
setWide(true);
|
2026-09-30 21:30:33 +01:00
|
|
|
});
|
|
|
|
|
|
2026-09-30 21:50:34 +01:00
|
|
|
/** Desktop unless a test says otherwise: the list pane is shown from 769px. */
|
|
|
|
|
function setWide(wide: boolean) {
|
|
|
|
|
window.matchMedia = ((q: string) => ({
|
|
|
|
|
matches: wide, media: q, addEventListener() {}, removeEventListener() {},
|
|
|
|
|
})) as unknown as typeof window.matchMedia;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-30 21:30:33 +01:00
|
|
|
async function renderMap() {
|
2026-09-30 21:50:34 +01:00
|
|
|
const view = render(<HomeView initialSchools={results()} filters={filters} />);
|
2026-09-30 21:30:33 +01:00
|
|
|
await act(async () => {});
|
|
|
|
|
return view;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-30 21:50:34 +01:00
|
|
|
it('opens a postcode search on the map', async () => {
|
2026-09-30 21:30:33 +01:00
|
|
|
await renderMap();
|
|
|
|
|
expect(screen.getByTestId('map')).toHaveAttribute('data-radius', '1');
|
|
|
|
|
expect(screen.getByRole('button', { name: 'Map' })).toHaveAttribute('aria-pressed', 'true');
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-30 21:50:34 +01:00
|
|
|
it('lists a name search, which has no map', async () => {
|
|
|
|
|
params = new URLSearchParams('search=southmead');
|
|
|
|
|
render(<HomeView initialSchools={results()} filters={filters} />);
|
|
|
|
|
await act(async () => {});
|
2026-09-30 21:30:33 +01:00
|
|
|
expect(screen.queryByTestId('map')).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('puts the count and the sort in the list beside the map, once', async () => {
|
|
|
|
|
await renderMap();
|
2026-10-01 10:18:26 +01:00
|
|
|
// Short beside the map, so it shares one line with the sort.
|
|
|
|
|
expect(screen.getAllByRole('heading', { name: '3 schools within 1 mile' })).toHaveLength(1);
|
2026-09-30 21:30:33 +01:00
|
|
|
expect(screen.getAllByRole('combobox')).toHaveLength(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('selects the pin from the card, and the card from the pin', async () => {
|
|
|
|
|
const { container } = await renderMap();
|
|
|
|
|
const card = (urn: number) => container.querySelector(`[data-urn="${urn}"]`) as HTMLElement;
|
|
|
|
|
|
|
|
|
|
fireEvent.click(within(card(1)).getByText(/pupils/));
|
|
|
|
|
expect(screen.getByTestId('map')).toHaveAttribute('data-selected', '1');
|
2026-10-01 09:03:50 +01:00
|
|
|
expect(card(1).className).toMatch(/mapRowSelected/);
|
2026-09-30 21:30:33 +01:00
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'pin' }));
|
2026-10-01 09:03:50 +01:00
|
|
|
expect(card(2).className).toMatch(/mapRowSelected/);
|
|
|
|
|
expect(card(1).className).not.toMatch(/mapRowSelected/);
|
2026-09-30 21:30:33 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('clicking a card\'s link or button does not also select it', async () => {
|
|
|
|
|
const { container } = await renderMap();
|
|
|
|
|
const card = container.querySelector('[data-urn="1"]') as HTMLElement;
|
|
|
|
|
fireEvent.click(within(card).getByRole('button', { name: '+ Compare' }));
|
|
|
|
|
expect(screen.getByTestId('map')).toHaveAttribute('data-selected', '');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows the England comparison for mainstream schools only, and never a placeholder 0%', async () => {
|
|
|
|
|
const { container } = await renderMap();
|
|
|
|
|
const card = (urn: number) => container.querySelector(`[data-urn="${urn}"]`) as HTMLElement;
|
2026-10-01 09:03:50 +01:00
|
|
|
expect(card(2)).toHaveTextContent('52%Reading, Writing & Maths-10 pts vs national');
|
|
|
|
|
expect(card(2)).toHaveTextContent('269pupils');
|
|
|
|
|
expect(card(3)).toHaveTextContent('62pupils');
|
2026-09-30 21:30:33 +01:00
|
|
|
expect(card(3)).not.toHaveTextContent(/%|pts/);
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-30 21:50:34 +01:00
|
|
|
it('opens the map after a hero search, and keeps the reader\'s choice after that', async () => {
|
|
|
|
|
// Landing page, then a hero search: the same instance gets new props.
|
2026-09-30 21:30:33 +01:00
|
|
|
params = new URLSearchParams('');
|
|
|
|
|
const empty = { schools: [], total: 0, page: 1, page_size: 25, total_pages: 0 } as SchoolsResponse;
|
2026-09-30 21:50:34 +01:00
|
|
|
const view = render(<HomeView initialSchools={empty} filters={filters} />);
|
2026-09-30 21:30:33 +01:00
|
|
|
params = new URLSearchParams('postcode=SW196AR&radius=1');
|
2026-09-30 21:50:34 +01:00
|
|
|
view.rerender(<HomeView initialSchools={results()} filters={filters} />);
|
2026-09-30 21:30:33 +01:00
|
|
|
await act(async () => {});
|
|
|
|
|
expect(screen.getByTestId('map')).toBeInTheDocument();
|
|
|
|
|
|
2026-09-30 21:50:34 +01:00
|
|
|
// Chosen: a later search keeps the list.
|
2026-09-30 21:30:33 +01:00
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'List' }));
|
|
|
|
|
params = new URLSearchParams('postcode=SW170AA&radius=1');
|
2026-09-30 21:50:34 +01:00
|
|
|
view.rerender(<HomeView initialSchools={results()} filters={filters} />);
|
2026-09-30 21:30:33 +01:00
|
|
|
await act(async () => {});
|
|
|
|
|
expect(screen.queryByTestId('map')).not.toBeInTheDocument();
|
|
|
|
|
});
|
2026-09-30 21:50:34 +01:00
|
|
|
|
|
|
|
|
it('builds no list cards on a phone, where the pane is hidden', async () => {
|
|
|
|
|
setWide(false);
|
|
|
|
|
const { container } = await renderMap();
|
|
|
|
|
expect(screen.getByTestId('map')).toBeInTheDocument();
|
|
|
|
|
expect(container.querySelectorAll('[data-urn]')).toHaveLength(0);
|
|
|
|
|
// The count stays: it is the pane's heading, shown above the map.
|
|
|
|
|
expect(screen.getByRole('heading', { name: /3 schools within/ })).toBeInTheDocument();
|
|
|
|
|
});
|
2026-10-01 09:03:50 +01:00
|
|
|
|
|
|
|
|
it('draws the list view\'s own row beside the map, with the same content', async () => {
|
|
|
|
|
const { container } = await renderMap();
|
2026-10-01 09:09:47 +01:00
|
|
|
const beside = container.querySelector('[data-urn="2"] > [class~="row"]')!.textContent;
|
2026-10-01 09:03:50 +01:00
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'List' }));
|
|
|
|
|
const row = screen.getByRole('link', { name: 'Southmead Primary School' }).closest('[class~="row"]')!;
|
|
|
|
|
expect(row.parentElement?.className).toMatch(/schoolList/);
|
|
|
|
|
expect(row.textContent).toBe(beside);
|
|
|
|
|
});
|
2026-10-01 09:09:47 +01:00
|
|
|
|
|
|
|
|
it('lets a keyboard pick a pin from the list, with a real button', async () => {
|
|
|
|
|
await renderMap();
|
|
|
|
|
const show = screen.getByRole('button', { name: 'Show Southmead Primary School on the map' });
|
|
|
|
|
expect(show).toHaveAttribute('aria-pressed', 'false');
|
|
|
|
|
fireEvent.click(show);
|
|
|
|
|
expect(screen.getByTestId('map')).toHaveAttribute('data-selected', '2');
|
|
|
|
|
expect(show).toHaveAttribute('aria-pressed', 'true');
|
|
|
|
|
});
|
2026-10-01 10:18:26 +01:00
|
|
|
|
|
|
|
|
it('keeps the postcode in the heading in list view, where there is room', async () => {
|
|
|
|
|
await renderMap();
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'List' }));
|
|
|
|
|
expect(screen.getByRole('heading', { name: '3 schools within 1.0 miles of SW196AR' })).toBeInTheDocument();
|
|
|
|
|
});
|