import { act, renderHook } from '@testing-library/react'; import { describe, expect, it } from 'vitest'; import type { POI } from '../types'; import { usePoiLayers } from './usePoiLayers'; const supermarket: POI = { id: 'poi-1', name: 'Market Hall', category: 'Supermarket', group: 'Groceries', lat: 51.5, lng: -0.12, emoji: '🛒', }; const waitrose: POI = { id: 'poi-3', name: 'Waitrose Marylebone', category: 'Waitrose', group: 'Groceries', lat: 51.52, lng: -0.15, emoji: '🛒', }; const busStop: POI = { id: 'poi-2', name: 'High Street Stop', category: 'Bus stop', group: 'Public Transport', lat: 51.501, lng: -0.121, emoji: '🚌', }; const foodWarehouse: POI = { id: 'poi-4', name: 'Iceland Avonmead Food Warehouse', category: 'Iceland', icon_category: 'The Food Warehouse', group: 'Groceries', lat: 51.49, lng: -0.18, emoji: '🛒', }; function layerById(layers: readonly unknown[], id: string) { const layer = layers.find((item) => (item as { id?: string }).id === id); if (!layer) throw new Error(`Layer ${id} not found`); return layer as { props: Record }; } type PoiIconDef = { url: string; width: number; height: number }; type PoiColor = [number, number, number, number]; describe('usePoiLayers', () => { it('returns the expected layer stack', () => { const { result } = renderHook(() => usePoiLayers({ pois: [supermarket], zoom: 15, isDark: false }) ); expect(result.current.poiLayers.map((layer) => layer.id)).toEqual([ 'poi-shadow', 'poi-background', 'poi-icons', 'poi-clusters', 'poi-cluster-text', ]); }); it('uses POI category logos for map marker icons', () => { const { result } = renderHook(() => usePoiLayers({ pois: [waitrose], zoom: 15, isDark: false }) ); const iconLayer = layerById(result.current.poiLayers, 'poi-icons'); const getIcon = iconLayer.props.getIcon as (poi: POI) => PoiIconDef; const getSize = iconLayer.props.getSize as (poi: POI) => number; expect(getIcon(waitrose)).toMatchObject({ url: '/assets/poi-icons/logos/waitrose.svg', width: 96, height: 48, }); expect(getSize(waitrose)).toBe(14); }); it('prefers POI fascia icon categories for map marker icons', () => { const { result } = renderHook(() => usePoiLayers({ pois: [foodWarehouse], zoom: 15, isDark: false }) ); const iconLayer = layerById(result.current.poiLayers, 'poi-icons'); const getIcon = iconLayer.props.getIcon as (poi: POI) => PoiIconDef; const getSize = iconLayer.props.getSize as (poi: POI) => number; expect(getIcon(foodWarehouse)).toMatchObject({ url: '/assets/poi-icons/logos/the_food_warehouse.png', width: 96, height: 48, }); expect(getSize(foodWarehouse)).toBe(14); }); it('keeps generic emoji POIs at the compact marker size', () => { const { result } = renderHook(() => usePoiLayers({ pois: [supermarket], zoom: 15, isDark: false }) ); const iconLayer = layerById(result.current.poiLayers, 'poi-icons'); const getIcon = iconLayer.props.getIcon as (poi: POI) => PoiIconDef; const getSize = iconLayer.props.getSize as (poi: POI) => number; expect(getIcon(supermarket)).toMatchObject({ url: '/assets/twemoji/1f6d2.png', width: 72, height: 72, }); expect(getSize(supermarket)).toBe(11); }); it('hides the circular marker badge behind bundled logo icons', () => { const { result } = renderHook(() => usePoiLayers({ pois: [waitrose, supermarket], zoom: 15, isDark: false }) ); const shadowLayer = layerById(result.current.poiLayers, 'poi-shadow'); const backgroundLayer = layerById(result.current.poiLayers, 'poi-background'); const getShadowRadius = shadowLayer.props.getRadius as (poi: POI) => number; const getBackgroundRadius = backgroundLayer.props.getRadius as (poi: POI) => number; const getFillColor = backgroundLayer.props.getFillColor as (poi: POI) => PoiColor; const getLineColor = backgroundLayer.props.getLineColor as (poi: POI) => PoiColor; expect(getShadowRadius(waitrose)).toBe(0); expect(getBackgroundRadius(waitrose)).toBe(14); expect(getFillColor(waitrose)).toEqual([0, 0, 0, 0]); expect(getLineColor(waitrose)).toEqual([0, 0, 0, 0]); expect(getShadowRadius(supermarket)).toBe(10); expect(getBackgroundRadius(supermarket)).toBe(8); expect(getFillColor(supermarket)).toEqual([255, 255, 255, 255]); expect(getLineColor(supermarket)).toEqual([34, 197, 94, 255]); }); it('hides minor POI categories until the configured zoom threshold', () => { const { result, rerender } = renderHook( ({ zoom }) => usePoiLayers({ pois: [busStop], zoom, isDark: false }), { initialProps: { zoom: 13 } } ); expect(layerById(result.current.poiLayers, 'poi-background').props.data).toEqual([]); expect(result.current.visiblePois).toEqual([]); rerender({ zoom: 14 }); expect(layerById(result.current.poiLayers, 'poi-background').props.data).toEqual([busStop]); expect(result.current.visiblePois).toEqual([busStop]); }); it('keeps POI hover popup state in sync with layer hover events', () => { const { result } = renderHook(() => usePoiLayers({ pois: [supermarket], zoom: 15, isDark: false }) ); const backgroundLayer = layerById(result.current.poiLayers, 'poi-background'); act(() => { (backgroundLayer.props.onHover as (info: unknown) => void)({ object: supermarket, x: 42, y: 88, }); }); expect(result.current.popupInfo).toEqual({ x: 42, y: 88, name: supermarket.name, category: supermarket.category, icon_category: undefined, group: supermarket.group, emoji: supermarket.emoji, id: supermarket.id, }); act(() => { result.current.clearPopupInfo(); }); expect(result.current.popupInfo).toBeNull(); }); it('creates cluster hover popup state from clustered POIs', () => { const clusteredPois = Array.from( { length: 4 }, (_, index): POI => ({ ...supermarket, id: `clustered-${index}`, name: `Clustered ${index}`, lat: 51.5 + index * 0.0001, lng: -0.12 - index * 0.0001, }) ); const { result } = renderHook(() => usePoiLayers({ pois: clusteredPois, zoom: 5, isDark: true }) ); const clusterLayer = layerById(result.current.poiLayers, 'poi-clusters'); const clusters = clusterLayer.props.data as Array<{ count: number; lng: number; lat: number }>; expect(clusters).toHaveLength(1); expect(clusters[0].count).toBe(4); act(() => { (clusterLayer.props.onHover as (info: unknown) => void)({ object: clusters[0], x: 12, y: 24, }); }); expect(result.current.popupInfo).toMatchObject({ x: 12, y: 24, name: '4 places', isCluster: true, clusterCount: 4, }); }); });