Files
TREK/client/src/components/Map/placeClusterLayers.test.ts
T
Maurice 3bfef73a46 refactor(maps): move the place clusters out of MapViewGL
The cluster source, its layers, the expansion zoom and the DOM bubbles
now live in placeClusterLayers.ts with their own tests, which brings
MapViewGL back under its size baseline instead of growing it. Lowers the
size and eslint baselines that this change made smaller.
2026-10-28 10:11:56 +01:00

87 lines
3.7 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import {
type ClusterMap,
PLACE_CLUSTER_SOURCE_ID,
addPlaceClusterLayers,
clusterBubbleSize,
createClusterBubble,
drawClusterBubbles,
} from './placeClusterLayers';
function fakeMap(features: unknown[], expansionZoom: unknown = 11) {
const source = { getClusterExpansionZoom: vi.fn(() => expansionZoom) };
const map = {
addSource: vi.fn(),
addLayer: vi.fn(),
getSource: vi.fn((id: string) => (id === PLACE_CLUSTER_SOURCE_ID ? source : undefined)),
easeTo: vi.fn(),
querySourceFeatures: vi.fn((_id: string, query: { filter: unknown[] }) =>
query.filter[0] === 'has' ? features : []
),
};
return { map: map as unknown as ClusterMap & typeof map, source };
}
describe('cluster bubbles', () => {
it('FE-COMP-PLACECLUSTER-001: a bubble grows in three steps with its count', () => {
expect([1, 9, 10, 49, 50, 500].map(clusterBubbleSize)).toEqual([36, 36, 42, 42, 48, 48]);
});
it('FE-COMP-PLACECLUSTER-002: the GL bubble is the Leaflet markup inside a bare wrapper the pin can move', () => {
const wrapper = createClusterBubble(38);
expect(wrapper.className).toBe('');
const bubble = wrapper.firstElementChild as HTMLElement;
expect(bubble.className).toBe('marker-cluster-custom');
expect(bubble.style.width).toBe('42px');
expect(bubble.style.height).toBe('42px');
expect(bubble.textContent).toBe('38');
});
it('FE-COMP-PLACECLUSTER-003: the canvas circle and count stay as an invisible hit area', () => {
const { map } = fakeMap([]);
addPlaceClusterLayers(map);
expect(map.addSource).toHaveBeenCalledWith(PLACE_CLUSTER_SOURCE_ID, expect.objectContaining({ cluster: true }));
const layers = map.addLayer.mock.calls.map(([layer]) => layer as { id: string; paint: Record<string, unknown> });
expect(layers.find((l) => l.id === 'trip-place-clusters-circle')?.paint['circle-opacity']).toBe(0);
expect(layers.find((l) => l.id === 'trip-place-clusters-count')?.paint['text-opacity']).toBe(0);
});
it('FE-COMP-PLACECLUSTER-004: one bubble per cluster, placed at its centre, that zooms in on a click', () => {
const cluster = { properties: { cluster_id: 5, point_count: 12 }, geometry: { coordinates: [139.7, 35.6] } };
const { map, source } = fakeMap([
cluster,
cluster, // the same cluster crossing a tile edge
{ properties: { cluster_id: 6 }, geometry: { coordinates: [1, 1] } }, // no count
{ properties: { cluster_id: 7, point_count: 3 } }, // no geometry
]);
const placed: { el: HTMLElement; lng: number; lat: number }[] = [];
const pins = drawClusterBubbles(map, (el, lng, lat) => {
placed.push({ el, lng, lat });
return lng;
});
expect(pins).toEqual([139.7]);
expect(placed[0].el.textContent).toBe('12');
expect(placed[0].lat).toBe(35.6);
const onMapClick = vi.fn();
document.body.appendChild(placed[0].el);
document.body.addEventListener('click', onMapClick);
placed[0].el.dispatchEvent(new MouseEvent('click', { bubbles: true }));
document.body.removeEventListener('click', onMapClick);
placed[0].el.remove();
// The click is the bubble's: the map underneath never sees it.
expect(onMapClick).not.toHaveBeenCalled();
expect(source.getClusterExpansionZoom).toHaveBeenCalledWith(5, expect.any(Function));
expect(map.easeTo).toHaveBeenCalledWith({ center: [139.7, 35.6], zoom: 11, duration: 350 });
});
it('FE-COMP-PLACECLUSTER-005: a source that reports nothing draws nothing', () => {
const { map } = fakeMap([]);
map.querySourceFeatures.mockReturnValue(null as never);
expect(drawClusterBubbles(map, () => 1)).toEqual([]);
});
});