perfect-postcode/frontend/src/hooks/useHexagonSelection.test.ts
2026-05-13 08:00:12 +01:00

202 lines
5.6 KiB
TypeScript

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);
});
});