PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m12s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m19s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 21s
Trial the map as the default for every postcode search, not only on desktop (DEFAULT_LOCATION_VIEW in HomeView; results_view_changed shows how often readers switch away). The view is now derived: the reader's choice once made, else the default for the kind of search. That follows a hero search through client-side navigation without syncing state. With nothing left varying by device, the user-agent default goes (lib/device.ts, the headers() read in page.tsx). That settles two review points: the HTML no longer needs Vary: User-Agent, and there is no narrow-window fallback left to flash the map before the list. Review fix in LeafletMapInner: the card reopened only when the selection or the school list changed, so a rebuild caused by the centre, reference point or radius closed it for good while the page still had the school selected. The selection effect now follows a pins-rebuilt counter. SchoolMap memoises its fallback centre, which was a fresh array on every render and would have rebuilt every pin each time. Phones hide the map view's list pane, so it no longer builds up to 500 hidden cards there (decided after mount, so hydration still matches). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
4.4 KiB
TypeScript
84 lines
4.4 KiB
TypeScript
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
import { HomeView } from '@/components/HomeView';
|
|
import { fetchSchools } from '@/lib/api';
|
|
import { primaryFixture } from '../support/schoolFixtures';
|
|
import type { SchoolsResponse, School } from '@/lib/types';
|
|
|
|
let params = new URLSearchParams('postcode=SW1A+1AA');
|
|
jest.mock('next/navigation', () => ({
|
|
useSearchParams: () => params,
|
|
usePathname: () => '/',
|
|
useRouter: () => ({ push: jest.fn(), replace: 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('@/components/FilterBar', () => ({ FilterBar: () => null }));
|
|
jest.mock('@/components/SchoolRow', () => ({ SchoolRow: ({ school }: {school: School}) => <div>{school.school_name}</div> }));
|
|
jest.mock('@/components/SchoolMap', () => ({ SchoolMap: ({ schools }: {schools: School[]}) => <div data-testid="map">{schools.map(s => s.school_name).join(',')}</div> }));
|
|
|
|
const filters = { local_authorities: [], school_types: [], years: [], phases: [], genders: [], admissions_policies: [] };
|
|
function response(name: string): SchoolsResponse {
|
|
return { schools: [{ ...primaryFixture.schoolInfo, school_name: name }],
|
|
total: 2, page: 1, page_size: 1, total_pages: 2 };
|
|
}
|
|
function deferred() {
|
|
let resolve!: (value: SchoolsResponse) => void;
|
|
let reject!: (error: Error) => void;
|
|
const promise = new Promise<SchoolsResponse>((yes, no) => { resolve = yes; reject = no; });
|
|
return { promise, resolve, reject };
|
|
}
|
|
beforeEach(() => {
|
|
params = new URLSearchParams('postcode=SW1A+1AA');
|
|
jest.mocked(fetchSchools).mockReset();
|
|
});
|
|
|
|
test('load-more results from an old search are discarded, even after returning to it', async () => {
|
|
// Name searches: a postcode search opens on the map, which has no Load more.
|
|
params = new URLSearchParams('search=abbey');
|
|
const pending = deferred();
|
|
jest.mocked(fetchSchools).mockReturnValueOnce(pending.promise);
|
|
const view = render(<HomeView initialSchools={response('Initial A')} filters={filters} />);
|
|
fireEvent.click(screen.getByRole('button', { name: 'Load more schools' }));
|
|
const signal = jest.mocked(fetchSchools).mock.calls[0][1]?.signal;
|
|
params = new URLSearchParams('search=brecknock');
|
|
view.rerender(<HomeView initialSchools={response('Initial B')} filters={filters} />);
|
|
expect(signal?.aborted).toBe(true);
|
|
params = new URLSearchParams('search=abbey');
|
|
view.rerender(<HomeView initialSchools={response('Fresh A')} filters={filters} />);
|
|
await act(async () => pending.resolve(response('Stale append')));
|
|
expect(screen.queryByText('Stale append')).not.toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: 'Load more schools' })).toBeEnabled();
|
|
});
|
|
|
|
test('an older map response cannot overwrite the current search', async () => {
|
|
const first = deferred(), second = deferred();
|
|
jest.mocked(fetchSchools).mockReturnValueOnce(first.promise).mockReturnValueOnce(second.promise);
|
|
const initial = response('Initial A');
|
|
const view = render(<HomeView initialSchools={initial} filters={filters} />);
|
|
fireEvent.click(screen.getByRole('button', { name: 'Map' }));
|
|
params = new URLSearchParams('postcode=SW2+1AA');
|
|
view.rerender(<HomeView initialSchools={response('Initial B')} filters={filters} />);
|
|
await act(async () => second.resolve(response('Current map')));
|
|
await act(async () => first.resolve(response('Stale map')));
|
|
expect(screen.getByTestId('map')).toHaveTextContent('Current map');
|
|
expect(screen.getByTestId('map')).not.toHaveTextContent('Stale map');
|
|
});
|
|
|
|
test('failed map requests can be retried by reopening the map', async () => {
|
|
const pending = deferred();
|
|
jest.mocked(fetchSchools).mockReturnValueOnce(pending.promise).mockResolvedValue(response('Retry result'));
|
|
render(<HomeView initialSchools={response('Initial')} filters={filters} />);
|
|
fireEvent.click(screen.getByRole('button', { name: 'Map' }));
|
|
await act(async () => pending.reject(new Error('offline')));
|
|
fireEvent.click(screen.getByRole('button', { name: 'List' }));
|
|
await act(async () => fireEvent.click(screen.getByRole('button', { name: 'Map' })));
|
|
expect(fetchSchools).toHaveBeenCalledTimes(2);
|
|
expect(screen.getByTestId('map')).toHaveTextContent('Retry result');
|
|
});
|