/** * The basket limit is five per phase group, not five overall, and the * provider is where it is enforced for every add path. * * Entries without a phase count against both groups. Baskets saved before * phases were recorded hold such entries, so the compare page backfills them * once it has fetched each school. */ import { act, render, screen, fireEvent } from '@testing-library/react'; import { AddToCompareButton } from '@/components/school/AddToCompareButton'; import { ComparisonProvider } from '@/context/ComparisonProvider'; import { useComparisonContext } from '@/context/ComparisonContext'; import type { NearbySchool, School } from '@/lib/types'; type Ctx = ReturnType; function renderBasket(stored: Partial[] = []) { window.localStorage.setItem('selectedSchools', JSON.stringify(stored)); const ref: { current: Ctx | null } = { current: null }; function Probe() { ref.current = useComparisonContext(); return null; } render( , ); return ref as { current: Ctx }; } const school = (urn: number, phase: string | null) => ({ urn, school_name: `School ${urn}`, phase }) as School; beforeEach(() => { window.localStorage.clear(); jest.spyOn(window, 'alert').mockImplementation(() => {}); }); afterEach(() => jest.restoreAllMocks()); it('holds five primary and five secondary schools, and no more of either', () => { const ctx = renderBasket(); act(() => { for (let i = 0; i < 6; i++) ctx.current.addSchool(school(100000 + i, 'Primary')); for (let i = 0; i < 6; i++) ctx.current.addSchool(school(200000 + i, 'Secondary')); }); expect(ctx.current.selectedSchools).toHaveLength(10); expect(window.alert).toHaveBeenCalledWith(expect.stringMatching(/5 primary schools/)); expect(window.alert).toHaveBeenCalledWith(expect.stringMatching(/5 secondary schools/)); }); it('frees the second group once a stored entry learns its phase', () => { // Three phase-less entries from an older basket plus two primaries: the // primary group reads as full although only two are really primary. const ctx = renderBasket([ school(100001, null), school(100002, null), school(100003, null), school(100004, 'Primary'), school(100005, 'Primary'), ]); expect(ctx.current.fullGroupFor({ phase: 'Primary' })).toBe('primary'); act(() => { ctx.current.backfillPhases({ 100001: 'Secondary', 100002: 'Secondary', 100003: 'Secondary' }); }); expect(ctx.current.fullGroupFor({ phase: 'Primary' })).toBeNull(); expect(ctx.current.selectedSchools.map((s) => s.phase)).toEqual([ 'Secondary', 'Secondary', 'Secondary', 'Primary', 'Primary', ]); }); it('never overwrites a phase the basket already has', () => { const ctx = renderBasket([school(100001, 'Primary')]); act(() => ctx.current.backfillPhases({ 100001: 'All-through' })); expect(ctx.current.selectedSchools[0].phase).toBe('Primary'); }); it('a nearby card adds its own phase, not an unknown one', () => { const nearby = { urn: 100009, school_name: 'Nearby', distance_miles: 0.4, school_type: 'Academy', age_range: '4-18', phase: 'All-through', shared: [], metric_value: null, metric_key: 'rwm_expected_pct', metric_year: null, } as NearbySchool; const ref: { current: Ctx | null } = { current: null }; function Probe() { ref.current = useComparisonContext(); return null; } render( , ); fireEvent.click(screen.getByRole('button', { name: /Add to compare/ })); expect(ref.current?.selectedSchools[0].phase).toBe('All-through'); });