225 lines
6.9 KiB
TypeScript
225 lines
6.9 KiB
TypeScript
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<string, unknown> };
|
|
}
|
|
|
|
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,
|
|
});
|
|
});
|
|
});
|