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'; /* * 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 * 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: {} } })), })); jest.mock('@/components/FilterBar', () => ({ FilterBar: () => null })); jest.mock('@/components/SchoolMap', () => ({ SchoolMap: ({ selectedUrn, radiusMiles, onMarkerClick, schools }: { selectedUrn: number | null; radiusMiles?: number; onMarkerClick: (s: School) => void; schools: School[]; }) => (
), })); 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); setWide(true); }); /** 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; } async function renderMap() { const view = render(); await act(async () => {}); return view; } it('opens a postcode search on the map', async () => { await renderMap(); expect(screen.getByTestId('map')).toHaveAttribute('data-radius', '1'); expect(screen.getByRole('button', { name: 'Map' })).toHaveAttribute('aria-pressed', 'true'); }); it('lists a name search, which has no map', async () => { params = new URLSearchParams('search=southmead'); render(); await act(async () => {}); expect(screen.queryByTestId('map')).not.toBeInTheDocument(); }); it('puts the count and the sort in the list beside the map, once', async () => { await renderMap(); expect(screen.getAllByRole('heading', { name: /3 schools within 1.0 miles of SW196AR/ })).toHaveLength(1); 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'); expect(card(1).className).toMatch(/mapRowSelected/); fireEvent.click(screen.getByRole('button', { name: 'pin' })); expect(card(2).className).toMatch(/mapRowSelected/); expect(card(1).className).not.toMatch(/mapRowSelected/); }); 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; expect(card(2)).toHaveTextContent('52%Reading, Writing & Maths-10 pts vs national'); expect(card(2)).toHaveTextContent('269pupils'); expect(card(3)).toHaveTextContent('62pupils'); expect(card(3)).not.toHaveTextContent(/%|pts/); }); 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. params = new URLSearchParams(''); const empty = { schools: [], total: 0, page: 1, page_size: 25, total_pages: 0 } as SchoolsResponse; const view = render(); params = new URLSearchParams('postcode=SW196AR&radius=1'); view.rerender(); await act(async () => {}); expect(screen.getByTestId('map')).toBeInTheDocument(); // Chosen: a later search keeps the list. fireEvent.click(screen.getByRole('button', { name: 'List' })); params = new URLSearchParams('postcode=SW170AA&radius=1'); view.rerender(); await act(async () => {}); expect(screen.queryByTestId('map')).not.toBeInTheDocument(); }); 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(); }); it('draws the list view\'s own row beside the map, with the same content', async () => { const { container } = await renderMap(); const beside = (container.querySelector('[data-urn="2"]') as HTMLElement).textContent; 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); });