140 lines
6.3 KiB
TypeScript
140 lines
6.3 KiB
TypeScript
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), opening on the map for
|
||
|
|
* desktop. 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[];
|
||
|
|
}) => (
|
||
|
|
<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);
|
||
|
|
window.innerWidth = 1440;
|
||
|
|
});
|
||
|
|
|
||
|
|
async function renderMap() {
|
||
|
|
const view = render(<HomeView initialSchools={results()} filters={filters} initialView="map" />);
|
||
|
|
await act(async () => {});
|
||
|
|
return view;
|
||
|
|
}
|
||
|
|
|
||
|
|
it('opens on the map when the server says desktop', async () => {
|
||
|
|
await renderMap();
|
||
|
|
expect(screen.getByTestId('map')).toHaveAttribute('data-radius', '1');
|
||
|
|
expect(screen.getByRole('button', { name: 'Map' })).toHaveAttribute('aria-pressed', 'true');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('falls back to the list in a window too narrow for the split', async () => {
|
||
|
|
window.innerWidth = 800;
|
||
|
|
await renderMap();
|
||
|
|
expect(screen.queryByTestId('map')).not.toBeInTheDocument();
|
||
|
|
expect(screen.getByRole('button', { name: 'List' })).toHaveAttribute('aria-pressed', 'true');
|
||
|
|
});
|
||
|
|
|
||
|
|
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(/compactItemSelected/);
|
||
|
|
|
||
|
|
fireEvent.click(screen.getByRole('button', { name: 'pin' }));
|
||
|
|
expect(card(2).className).toMatch(/compactItemSelected/);
|
||
|
|
expect(card(1).className).not.toMatch(/compactItemSelected/);
|
||
|
|
});
|
||
|
|
|
||
|
|
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% RWM -10 pts · 269 pupils');
|
||
|
|
expect(card(3)).toHaveTextContent('62 pupils');
|
||
|
|
expect(card(3)).not.toHaveTextContent(/%|pts/);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('follows a new default after a client-side search, until the reader picks', async () => {
|
||
|
|
params = new URLSearchParams('');
|
||
|
|
const empty = { schools: [], total: 0, page: 1, page_size: 25, total_pages: 0 } as SchoolsResponse;
|
||
|
|
const view = render(<HomeView initialSchools={empty} filters={filters} initialView="list" />);
|
||
|
|
|
||
|
|
// Hero search: same component, new props.
|
||
|
|
params = new URLSearchParams('postcode=SW196AR&radius=1');
|
||
|
|
view.rerender(<HomeView initialSchools={results()} filters={filters} initialView="map" />);
|
||
|
|
await act(async () => {});
|
||
|
|
expect(screen.getByTestId('map')).toBeInTheDocument();
|
||
|
|
|
||
|
|
// Chosen: a later search keeps the reader's view.
|
||
|
|
fireEvent.click(screen.getByRole('button', { name: 'List' }));
|
||
|
|
params = new URLSearchParams('postcode=SW170AA&radius=1');
|
||
|
|
view.rerender(<HomeView initialSchools={results()} filters={filters} initialView="list" />);
|
||
|
|
view.rerender(<HomeView initialSchools={results()} filters={filters} initialView="map" />);
|
||
|
|
await act(async () => {});
|
||
|
|
expect(screen.queryByTestId('map')).not.toBeInTheDocument();
|
||
|
|
});
|