import { act, renderHook, waitFor } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { useHexagonSelection } from './useHexagonSelection'; import type { FeatureMeta, HexagonStatsResponse, PostcodeGeometry } from '../types'; vi.mock('../lib/pocketbase', () => ({ default: { authStore: { isValid: false, token: '' } }, })); vi.mock('../lib/analytics', () => ({ trackEvent: vi.fn(), })); const postcodeGeometry: PostcodeGeometry = { type: 'Polygon', coordinates: [ [ [-0.12, 51.5], [-0.11, 51.5], [-0.11, 51.51], [-0.12, 51.51], [-0.12, 51.5], ], ], }; function stats(count: number): HexagonStatsResponse { return { count, numeric_features: [], enum_features: [], central_postcode: 'SW1A 1AA', }; } function jsonResponse(body: unknown): Response { return new Response(JSON.stringify(body), { status: 200, headers: { 'Content-Type': 'application/json' }, }); } describe('useHexagonSelection', () => { const requests: string[] = []; const features: FeatureMeta[] = [{ name: 'Price', type: 'numeric', min: 0, max: 100 }]; beforeEach(() => { requests.length = 0; vi.stubGlobal( 'fetch', vi.fn((input: string | URL | Request) => { const url = new URL(String(input), 'http://localhost'); requests.push(`${url.pathname}${url.search}`); if (url.pathname === '/api/postcode-stats') { const emptyPostcode = url.searchParams.get('postcode') === 'EMPTY 1AA'; return Promise.resolve( jsonResponse(stats(url.searchParams.has('filters') || emptyPostcode ? 0 : 4)) ); } if (url.pathname === '/api/hexagon-stats') { return Promise.resolve(jsonResponse(stats(12))); } return Promise.resolve(new Response(null, { status: 404 })); }) ); }); afterEach(() => { vi.unstubAllGlobals(); }); it('keeps a postcode search selected when filters exclude its properties', async () => { const { result } = renderHook(() => useHexagonSelection({ filters: { Price: [0, 50] }, features, hexagonData: [], resolution: 9, usePostcodeView: true, travelTimeEntries: [], }) ); act(() => { result.current.handleLocationSearch('SW1A 1AA', postcodeGeometry, 51.505, -0.115); }); await waitFor(() => { expect(result.current.selectedHexagon).toEqual({ id: 'SW1A 1AA', type: 'postcode', resolution: 9, lockedResolution: true, }); }); expect(result.current.selectedPostcodeGeometry).toBe(postcodeGeometry); await waitFor(() => { expect(result.current.areaStats?.count).toBe(0); expect(result.current.unfilteredAreaCount).toBe(4); }); expect(requests.some((url) => url.startsWith('/api/hexagon-stats'))).toBe(false); }); it('keeps a postcode search selected when stats are based on all properties', async () => { const { result } = renderHook(() => useHexagonSelection({ filters: { Price: [0, 50] }, features, hexagonData: [], resolution: 9, usePostcodeView: true, travelTimeEntries: [], }) ); act(() => { result.current.setAreaStatsUseFilters(false); }); act(() => { result.current.handleLocationSearch('SW1A 1AA', postcodeGeometry, 51.505, -0.115); }); await waitFor(() => { expect(result.current.selectedHexagon).toEqual({ id: 'SW1A 1AA', type: 'postcode', resolution: 9, lockedResolution: true, }); }); expect(result.current.areaStats?.count).toBe(4); expect(result.current.unfilteredAreaCount).toBeNull(); expect(requests.some((url) => url.startsWith('/api/hexagon-stats'))).toBe(false); }); it('keeps an empty postcode search selected instead of widening to hexagons', async () => { const { result } = renderHook(() => useHexagonSelection({ filters: {}, features, hexagonData: [], resolution: 9, usePostcodeView: true, travelTimeEntries: [], }) ); act(() => { result.current.handleLocationSearch('EMPTY 1AA', postcodeGeometry, 51.505, -0.115); }); await waitFor(() => { expect(result.current.areaStats?.count).toBe(0); }); expect(result.current.selectedHexagon).toEqual({ id: 'EMPTY 1AA', type: 'postcode', resolution: 9, lockedResolution: true, }); expect(result.current.selectedPostcodeGeometry).toBe(postcodeGeometry); expect(requests.some((url) => url.startsWith('/api/hexagon-stats'))).toBe(false); }); it('does not convert a searched postcode back to a hexagon while the map reaches postcode zoom', async () => { const { result } = renderHook(() => useHexagonSelection({ filters: {}, features, hexagonData: [], resolution: 9, usePostcodeView: false, travelTimeEntries: [], }) ); act(() => { result.current.handleLocationSearch('SW1A 1AA', postcodeGeometry, 51.505, -0.115); }); await waitFor(() => { expect(result.current.areaStats?.count).toBe(4); }); expect(result.current.selectedHexagon).toEqual({ id: 'SW1A 1AA', type: 'postcode', resolution: 9, lockedResolution: true, }); expect(result.current.selectedPostcodeGeometry).toBe(postcodeGeometry); expect(requests.some((url) => url.startsWith('/api/postcode/'))).toBe(false); expect(requests.some((url) => url.startsWith('/api/hexagon-stats'))).toBe(false); }); });