2026-09-15 10:17:18 +01:00
|
|
|
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 () => {
|
2026-09-30 21:50:34 +01:00
|
|
|
// Name searches: a postcode search opens on the map, which has no Load more.
|
|
|
|
|
params = new URLSearchParams('search=abbey');
|
2026-09-15 10:17:18 +01:00
|
|
|
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;
|
2026-09-30 21:50:34 +01:00
|
|
|
params = new URLSearchParams('search=brecknock');
|
2026-09-15 10:17:18 +01:00
|
|
|
view.rerender(<HomeView initialSchools={response('Initial B')} filters={filters} />);
|
|
|
|
|
expect(signal?.aborted).toBe(true);
|
2026-09-30 21:50:34 +01:00
|
|
|
params = new URLSearchParams('search=abbey');
|
2026-09-15 10:17:18 +01:00
|
|
|
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');
|
|
|
|
|
});
|