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>
108 lines
5.0 KiB
TypeScript
108 lines
5.0 KiB
TypeScript
import { act, fireEvent, render } from '@testing-library/react';
|
|
import LeafletMapInner from '@/components/LeafletMapInner';
|
|
import { primaryFixture } from '../support/schoolFixtures';
|
|
import type { School } from '@/lib/types';
|
|
|
|
/*
|
|
* The results map's own logic, against real Leaflet in jsdom: which pin is
|
|
* selected, whether the card opens, and what the card offers. jsdom lays out
|
|
* nothing, so this pins behaviour, never positions.
|
|
*/
|
|
|
|
const base = primaryFixture.schoolInfo;
|
|
const a: School = { ...base, urn: 1, school_name: 'Southmead Primary School', latitude: 51.43, longitude: -0.21, distance: 0.2, rwm_expected_pct: 52 };
|
|
const b: School = { ...base, urn: 2, school_name: 'Greenmead School', latitude: 51.431, longitude: -0.205, distance: 0.2,
|
|
school_type: 'Community special school', rwm_expected_pct: 0, reading_expected_pct: 0, writing_expected_pct: 0, maths_expected_pct: 0 };
|
|
const schools = [a, b];
|
|
const centre: [number, number] = [51.43, -0.21];
|
|
|
|
function setWide(wide: boolean) {
|
|
window.matchMedia = ((q: string) => ({
|
|
matches: wide, media: q, addEventListener() {}, removeEventListener() {},
|
|
})) as unknown as typeof window.matchMedia;
|
|
}
|
|
|
|
function renderMap(props: Partial<React.ComponentProps<typeof LeafletMapInner>> = {}) {
|
|
const all = {
|
|
schools, center: centre, zoom: 13, referencePoint: centre, radiusMiles: 1,
|
|
nationalAvgRwm: 62, ...props,
|
|
};
|
|
const view = render(<LeafletMapInner {...all} />);
|
|
return { ...view, rerender: (next: Partial<typeof all>) => view.rerender(<LeafletMapInner {...all} {...next} />) };
|
|
}
|
|
|
|
beforeEach(() => setWide(true));
|
|
|
|
it('draws a pin per school, the search location and the radius', () => {
|
|
const { container } = renderMap();
|
|
expect(container.querySelectorAll('.sc-pin')).toHaveLength(2);
|
|
expect(container.querySelector('.sc-home')).not.toBeNull();
|
|
expect(container.querySelector('.sc-radius-label')).toHaveTextContent('1 mile');
|
|
});
|
|
|
|
it('reports a pin click, and marks and opens the selected school', () => {
|
|
const onMarkerClick = jest.fn();
|
|
const { container, rerender } = renderMap({ onMarkerClick });
|
|
fireEvent.click(container.querySelectorAll('.sc-pin')[0]);
|
|
expect(onMarkerClick).toHaveBeenCalledWith(a);
|
|
|
|
rerender({ onMarkerClick, selectedUrn: 1 });
|
|
expect(container.querySelectorAll('.sc-pin--selected')).toHaveLength(1);
|
|
expect(container.querySelector('.sc-popup')).toHaveTextContent('Southmead Primary School');
|
|
expect(container.querySelector('.sc-popup')).toHaveTextContent('52% RWM -10 pts');
|
|
});
|
|
|
|
it('keeps the special-school card free of a benchmark and a placeholder 0%', () => {
|
|
const { container } = renderMap({ selectedUrn: 2 });
|
|
const card = container.querySelector('.sc-popup')!;
|
|
expect(card).toHaveTextContent('Greenmead School');
|
|
expect(card).not.toHaveTextContent(/%|pts/);
|
|
});
|
|
|
|
it('adds to compare from the card, and the card follows the basket', () => {
|
|
const onAddToCompare = jest.fn();
|
|
const { container, rerender } = renderMap({ onAddToCompare, selectedUrn: 1, compareUrns: [] });
|
|
fireEvent.click(container.querySelector('[data-compare]')!);
|
|
expect(onAddToCompare).toHaveBeenCalledWith(a);
|
|
|
|
rerender({ onAddToCompare, selectedUrn: 1, compareUrns: [1] });
|
|
expect(container.querySelector('[data-compare]')).toHaveTextContent('✓ Comparing');
|
|
fireEvent.click(container.querySelector('[data-compare]')!);
|
|
expect(onAddToCompare).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('tells the page when the card is closed from the map, but not when replaced', () => {
|
|
const onDeselect = jest.fn();
|
|
const { container, rerender } = renderMap({ onDeselect, selectedUrn: 1 });
|
|
rerender({ onDeselect, selectedUrn: 2 });
|
|
expect(onDeselect).not.toHaveBeenCalled();
|
|
|
|
fireEvent.click(container.querySelector('.leaflet-popup-close-button')!);
|
|
expect(onDeselect).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('keeps the selection when the results reload under it', () => {
|
|
const onDeselect = jest.fn();
|
|
const { container, rerender } = renderMap({ onDeselect, selectedUrn: 1 });
|
|
act(() => rerender({ onDeselect, selectedUrn: 1, schools: [...schools] }));
|
|
expect(onDeselect).not.toHaveBeenCalled();
|
|
expect(container.querySelector('.sc-popup')).toHaveTextContent('Southmead Primary School');
|
|
});
|
|
|
|
it('opens no card on a narrow screen, where the page shows a bottom sheet', () => {
|
|
setWide(false);
|
|
const { container } = renderMap({ selectedUrn: 1 });
|
|
expect(container.querySelectorAll('.sc-pin--selected')).toHaveLength(1);
|
|
expect(container.querySelector('.sc-popup')).toBeNull();
|
|
});
|
|
|
|
it('puts the card back when the pins are rebuilt for a reason other than the schools', () => {
|
|
const onDeselect = jest.fn();
|
|
const { container, rerender } = renderMap({ onDeselect, selectedUrn: 1 });
|
|
act(() => rerender({ onDeselect, selectedUrn: 1, radiusMiles: 3, referencePoint: [51.43, -0.21] }));
|
|
expect(onDeselect).not.toHaveBeenCalled();
|
|
expect(container.querySelector('.sc-radius-label')).toHaveTextContent('3 miles');
|
|
expect(container.querySelector('.sc-popup')).toHaveTextContent('Southmead Primary School');
|
|
expect(container.querySelectorAll('.sc-pin--selected')).toHaveLength(1);
|
|
});
|