mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 21:46:49 +00:00
576 lines
20 KiB
TypeScript
576 lines
20 KiB
TypeScript
import { act } from '@testing-library/react';
|
|
import maplibregl from 'maplibre-gl';
|
|
import React from 'react';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { render } from '../../../tests/helpers/render';
|
|
import { resetAllStores } from '../../../tests/helpers/store';
|
|
import type { GeoPosition, TrackingMode } from '../../hooks/useGeolocation';
|
|
import { useSettingsStore } from '../../store/settingsStore';
|
|
|
|
// The planner-waypoint and location-search camera cases of MapViewGL, and the tour
|
|
// map's via ferrata layer (RC-10), with the same map doubles as MapViewGL.test.tsx.
|
|
|
|
// Stable fake map so fitBounds call counts survive re-renders. The canvas
|
|
// container is a single element so listeners registered by the component are
|
|
// reachable from tests via dispatchEvent.
|
|
const glCanvasContainer = vi.hoisted(() => document.createElement('div'));
|
|
const glMap = vi.hoisted(() => ({
|
|
on: vi.fn(),
|
|
off: vi.fn(),
|
|
once: vi.fn(),
|
|
loaded: vi.fn().mockReturnValue(true),
|
|
fitBounds: vi.fn(),
|
|
flyTo: vi.fn(),
|
|
jumpTo: vi.fn(),
|
|
getZoom: vi.fn().mockReturnValue(10),
|
|
getCenter: vi.fn(() => ({ lng: 0, lat: 0 })),
|
|
addControl: vi.fn(),
|
|
removeControl: vi.fn(),
|
|
remove: vi.fn(),
|
|
addSource: vi.fn(),
|
|
getSource: vi.fn().mockReturnValue(null),
|
|
addLayer: vi.fn(),
|
|
// Every overlay that adds a layer takes it away again on cleanup; a real
|
|
// MapLibre/Mapbox map has both halves, and a double with only the adding half
|
|
// fails the teardown rather than the behaviour under test.
|
|
removeLayer: vi.fn(),
|
|
removeSource: vi.fn(),
|
|
setLayoutProperty: vi.fn(),
|
|
setFilter: vi.fn(),
|
|
hasImage: vi.fn().mockReturnValue(false),
|
|
addImage: vi.fn(),
|
|
removeImage: vi.fn(),
|
|
getStyle: vi.fn().mockReturnValue({ layers: [] }),
|
|
isStyleLoaded: vi.fn().mockReturnValue(true),
|
|
getCanvasContainer: vi.fn(() => glCanvasContainer),
|
|
getContainer: vi.fn(() => glCanvasContainer),
|
|
getLayer: vi.fn().mockReturnValue(null),
|
|
queryRenderedFeatures: vi.fn().mockReturnValue([]),
|
|
querySourceFeatures: vi.fn().mockReturnValue([]),
|
|
unproject: vi.fn(() => ({ lng: 2.3522, lat: 48.8566 })),
|
|
// The MapLibre place pins position themselves with this, so a mock without it
|
|
// never exercises the path the real map takes.
|
|
project: vi.fn((lngLat: [number, number]) => ({ x: lngLat[0] * 10, y: lngLat[1] * 10 })),
|
|
getBounds: vi.fn(() => ({ getSouth: () => 0, getWest: () => 0, getNorth: () => 1, getEast: () => 1 })),
|
|
easeTo: vi.fn(),
|
|
getCanvas: vi.fn(() => document.createElement('canvas')),
|
|
getBearing: vi.fn(() => 0),
|
|
setTerrain: vi.fn(),
|
|
setConfigProperty: vi.fn(),
|
|
queryTerrainElevation: vi.fn((): number | null => null),
|
|
}));
|
|
|
|
const glBounds = vi.hoisted(() => {
|
|
const state = {
|
|
instances: [] as Array<{ extend: ReturnType<typeof vi.fn> }>,
|
|
};
|
|
return {
|
|
get instances() {
|
|
return state.instances;
|
|
},
|
|
clear: () => {
|
|
state.instances = [];
|
|
},
|
|
create: () => {
|
|
const bounds = {
|
|
extend: vi.fn(() => bounds),
|
|
};
|
|
state.instances.push(bounds);
|
|
return bounds;
|
|
},
|
|
};
|
|
});
|
|
|
|
// Markers and popups are created through the mocked GL constructors, so the
|
|
// tests can reach the very instances the component built (their DOM element,
|
|
// the coordinates they were placed at, the popup content they were given).
|
|
interface FakeMarker {
|
|
element: HTMLElement;
|
|
lngLat: number[] | null;
|
|
setLngLat: (ll: number[]) => FakeMarker;
|
|
getLngLat: () => { lng: number; lat: number };
|
|
addTo: (map: unknown) => FakeMarker;
|
|
remove: () => void;
|
|
getElement: () => HTMLElement;
|
|
}
|
|
|
|
interface FakePopup {
|
|
setLngLat: (ll: unknown) => FakePopup;
|
|
setHTML: (html: string) => FakePopup;
|
|
setText: (text: string) => FakePopup;
|
|
setDOMContent: (node: HTMLElement) => FakePopup;
|
|
addTo: (map: unknown) => FakePopup;
|
|
remove: () => void;
|
|
}
|
|
|
|
const glMarkers = vi.hoisted(() => {
|
|
const created: FakeMarker[] = [];
|
|
return {
|
|
get created() {
|
|
return created;
|
|
},
|
|
clear: () => {
|
|
created.length = 0;
|
|
},
|
|
make: (opts?: { element?: HTMLElement }): FakeMarker => {
|
|
const element = opts?.element ?? document.createElement('div');
|
|
const marker: FakeMarker = {
|
|
element,
|
|
lngLat: null,
|
|
setLngLat: vi.fn((ll: number[]) => {
|
|
marker.lngLat = ll;
|
|
return marker;
|
|
}),
|
|
getLngLat: vi.fn(() => ({ lng: Number(marker.lngLat?.[0] ?? 0), lat: Number(marker.lngLat?.[1] ?? 0) })),
|
|
addTo: vi.fn((_map: unknown) => marker),
|
|
remove: vi.fn(),
|
|
getElement: vi.fn(() => element),
|
|
};
|
|
created.push(marker);
|
|
return marker;
|
|
},
|
|
};
|
|
});
|
|
|
|
const glPopup = vi.hoisted((): FakePopup => {
|
|
const popup: FakePopup = {
|
|
setLngLat: vi.fn((_ll: unknown) => popup),
|
|
setHTML: vi.fn((_html: string) => popup),
|
|
setText: vi.fn((_text: string) => popup),
|
|
setDOMContent: vi.fn((_node: HTMLElement) => popup),
|
|
addTo: vi.fn((_map: unknown) => popup),
|
|
remove: vi.fn(),
|
|
};
|
|
return popup;
|
|
});
|
|
|
|
vi.mock('mapbox-gl', () => ({
|
|
default: {
|
|
accessToken: '',
|
|
Map: vi.fn(function () {
|
|
return glMap;
|
|
}),
|
|
Marker: vi.fn(function (opts?: { element?: HTMLElement }) {
|
|
return glMarkers.make(opts);
|
|
}),
|
|
LngLatBounds: vi.fn(function () {
|
|
return glBounds.create();
|
|
}),
|
|
NavigationControl: vi.fn(),
|
|
Popup: vi.fn(function () {
|
|
return glPopup;
|
|
}),
|
|
},
|
|
}));
|
|
vi.mock('mapbox-gl/dist/mapbox-gl.css', () => ({}));
|
|
|
|
vi.mock('maplibre-gl', () => ({
|
|
default: {
|
|
Map: vi.fn(function () {
|
|
return glMap;
|
|
}),
|
|
Marker: vi.fn(function (opts?: { element?: HTMLElement }) {
|
|
return glMarkers.make(opts);
|
|
}),
|
|
LngLatBounds: vi.fn(function () {
|
|
return glBounds.create();
|
|
}),
|
|
NavigationControl: vi.fn(),
|
|
Popup: vi.fn(function () {
|
|
return glPopup;
|
|
}),
|
|
},
|
|
}));
|
|
vi.mock('maplibre-gl/dist/maplibre-gl.css', () => ({}));
|
|
|
|
vi.mock('./mapboxSetup', () => ({
|
|
isStandardFamily: vi.fn(() => false),
|
|
supportsCustom3d: vi.fn(() => false),
|
|
wantsTerrain: vi.fn(() => false),
|
|
addCustom3dBuildings: vi.fn(),
|
|
addTerrainAndSky: vi.fn(),
|
|
}));
|
|
|
|
const locationMarker = vi.hoisted(() => ({ update: vi.fn(), destroy: vi.fn() }));
|
|
|
|
vi.mock('./locationMarkerMapbox', () => ({
|
|
attachLocationMarker: vi.fn(() => locationMarker),
|
|
}));
|
|
|
|
const reservationOverlay = vi.hoisted(() => ({
|
|
update: vi.fn((_reservations: unknown, _opts: unknown, _routes: unknown) => {}),
|
|
destroy: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('./reservationsMapbox', () => ({
|
|
ReservationMapboxOverlay: vi.fn(function () {
|
|
return reservationOverlay;
|
|
}),
|
|
}));
|
|
|
|
const geoStub = vi.hoisted(() => ({
|
|
position: null as GeoPosition | null,
|
|
mode: 'off' as TrackingMode,
|
|
error: null as string | null,
|
|
errorCode: null as string | null,
|
|
cycleMode: vi.fn(async () => {}),
|
|
setMode: vi.fn((_m: TrackingMode | ((prev: TrackingMode) => TrackingMode)) => {}),
|
|
}));
|
|
|
|
vi.mock('../../hooks/useGeolocation', () => ({
|
|
useGeolocation: vi.fn(() => geoStub),
|
|
}));
|
|
|
|
vi.mock('../../services/photoService', () => ({
|
|
getCached: vi.fn(() => null),
|
|
isLoading: vi.fn(() => false),
|
|
fetchPhoto: vi.fn(),
|
|
onThumbReady: vi.fn(() => () => {}),
|
|
getAllThumbs: vi.fn(() => ({})),
|
|
}));
|
|
|
|
import mapboxgl from 'mapbox-gl';
|
|
import * as photoService from '../../services/photoService';
|
|
import { groupFerratas } from '../Ferratas/ferrataGroups';
|
|
import { MapViewGL as MapViewGLWithEngine } from './MapViewGL';
|
|
import * as mapboxSetup from './mapboxSetup';
|
|
|
|
// The engine is a prop, not a module import (see MapViewGL.test.tsx). This shim makes
|
|
// the same choice glLazy.tsx makes in production.
|
|
function MapViewGL(props: Omit<React.ComponentProps<typeof MapViewGLWithEngine>, 'gl'>) {
|
|
return <MapViewGLWithEngine {...props} gl={props.glProvider === 'maplibre-gl' ? maplibregl : mapboxgl} />;
|
|
}
|
|
|
|
function pointer(type: string, x: number, y: number): Event {
|
|
const event = new Event(type, { bubbles: true, cancelable: true });
|
|
Object.defineProperties(event, {
|
|
button: { value: 0 },
|
|
clientX: { value: x },
|
|
clientY: { value: y },
|
|
pointerId: { value: 1 },
|
|
});
|
|
return event;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
glMap.on.mockImplementation(() => glMap);
|
|
glMap.off.mockImplementation(() => glMap);
|
|
glMap.once.mockImplementation(() => glMap);
|
|
glMap.loaded.mockReturnValue(true);
|
|
glMap.getSource.mockReturnValue(null);
|
|
glMap.getLayer.mockReturnValue(null);
|
|
glMap.queryRenderedFeatures.mockReturnValue([]);
|
|
glMap.querySourceFeatures.mockReturnValue([]);
|
|
glMap.getCanvas.mockImplementation(() => document.createElement('canvas'));
|
|
glMap.getBearing.mockReturnValue(0);
|
|
glMap.queryTerrainElevation.mockReturnValue(null);
|
|
// A busy style, a refused source and a custom layer list are each set by one test
|
|
// for the case it covers. Put back here, or every test after it would silently run
|
|
// against a map that never settles.
|
|
glMap.isStyleLoaded.mockReturnValue(true);
|
|
glMap.getStyle.mockReturnValue({ layers: [] });
|
|
glMap.addSource.mockImplementation(() => undefined);
|
|
glMarkers.clear();
|
|
// clearAllMocks() wipes call history but keeps implementations, so anything a
|
|
// test overrides with mockReturnValue has to be put back here.
|
|
vi.mocked(mapboxSetup.isStandardFamily).mockReturnValue(false);
|
|
vi.mocked(mapboxSetup.supportsCustom3d).mockReturnValue(false);
|
|
vi.mocked(mapboxSetup.wantsTerrain).mockReturnValue(false);
|
|
vi.mocked(photoService.getCached).mockReturnValue(undefined);
|
|
vi.mocked(photoService.isLoading).mockReturnValue(false);
|
|
vi.mocked(photoService.onThumbReady).mockReturnValue(() => {});
|
|
vi.mocked(photoService.getAllThumbs).mockReturnValue({});
|
|
geoStub.position = null;
|
|
geoStub.mode = 'off';
|
|
geoStub.error = null;
|
|
useSettingsStore.setState({
|
|
settings: {
|
|
...useSettingsStore.getState().settings,
|
|
map_provider: 'mapbox-gl',
|
|
mapbox_access_token: 'pk.test_token',
|
|
mapbox_style: 'mapbox://styles/mapbox/streets-v12',
|
|
mapbox_3d_enabled: false,
|
|
},
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
glBounds.clear();
|
|
resetAllStores();
|
|
});
|
|
|
|
describe('MapViewGL', () => {
|
|
function loadOnAttach() {
|
|
glMap.on.mockImplementation((event: string, handlerOrLayer: unknown) => {
|
|
if (event === 'load' && typeof handlerOrLayer === 'function') (handlerOrLayer as () => void)();
|
|
return glMap;
|
|
});
|
|
}
|
|
|
|
it('honors a location-search camera intent and preserves it through later route updates', async () => {
|
|
loadOnAttach();
|
|
const searchPoint: [number, number] = [35.0116, 135.7681];
|
|
const { rerender } = render(
|
|
<MapViewGL
|
|
places={[]}
|
|
route={null}
|
|
followSelection={false}
|
|
focusKey="tour:0:search:1"
|
|
focusPoints={[searchPoint]}
|
|
glProvider="maplibre-gl"
|
|
/>
|
|
);
|
|
await act(async () => {});
|
|
expect(glMap.fitBounds).toHaveBeenCalledOnce();
|
|
expect(glMap.fitBounds).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ maxZoom: 15 }));
|
|
const mapConstructions = vi.mocked(maplibregl.Map).mock.calls.length;
|
|
glMap.fitBounds.mockClear();
|
|
|
|
rerender(
|
|
<MapViewGL
|
|
places={[]}
|
|
route={[[searchPoint, [35.02, 135.77]]]}
|
|
followSelection={false}
|
|
focusKey="tour:0:search:1"
|
|
focusPoints={[searchPoint, [35.02, 135.77]]}
|
|
glProvider="maplibre-gl"
|
|
/>
|
|
);
|
|
await act(async () => {});
|
|
expect(glMap.fitBounds).not.toHaveBeenCalled();
|
|
expect(vi.mocked(maplibregl.Map).mock.calls).toHaveLength(mapConstructions);
|
|
|
|
rerender(
|
|
<MapViewGL
|
|
places={[]}
|
|
followSelection={false}
|
|
focusKey="tour:1"
|
|
focusPoints={[
|
|
[46.8, 9.5],
|
|
[47.1, 10.1],
|
|
]}
|
|
glProvider="maplibre-gl"
|
|
/>
|
|
);
|
|
await act(async () => {});
|
|
expect(glMap.fitBounds).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it.each(['maplibre-gl', 'mapbox-gl'] as const)(
|
|
'RC-08: %s previews waypoint drags locally and commits only on pointer up without changing the camera',
|
|
async (provider) => {
|
|
loadOnAttach();
|
|
glMap.project.mockImplementation((lngLat: [number, number]) => ({ x: lngLat[0] * 10, y: lngLat[1] * 10 }));
|
|
glMap.unproject.mockReturnValue({ lng: 2.3522, lat: 48.8566 });
|
|
glMap.getContainer.mockReturnValue(glCanvasContainer);
|
|
const waypoint = { id: 'via-1', lat: 48, lng: 11, role: 'via' as const };
|
|
const waypoints = [waypoint];
|
|
const onMove = vi.fn(() => true);
|
|
const onSelect = vi.fn();
|
|
const onMapClick = vi.fn();
|
|
const view = render(
|
|
<MapViewGL
|
|
places={[]}
|
|
plannerWaypoints={waypoints}
|
|
onPlannerWaypointClick={onSelect}
|
|
onPlannerWaypointMove={onMove}
|
|
onMapClick={onMapClick}
|
|
focusKey="tour:1"
|
|
focusPoints={[[waypoint.lat, waypoint.lng]]}
|
|
routeProfileFocus={null}
|
|
glProvider={provider}
|
|
/>
|
|
);
|
|
await act(async () => {});
|
|
glMap.fitBounds.mockClear();
|
|
const pin =
|
|
provider === 'maplibre-gl'
|
|
? (glCanvasContainer.querySelector('[aria-label="Waypoint 1"]') as HTMLButtonElement)
|
|
: (glMarkers.created.find((marker) => marker.element.getAttribute('aria-label') === 'Waypoint 1')
|
|
?.element as HTMLButtonElement);
|
|
const startTransform = pin.style.transform;
|
|
const libraryMarker =
|
|
provider === 'mapbox-gl' ? glMarkers.created.find((marker) => marker.element === pin) : undefined;
|
|
const startLngLat = libraryMarker?.lngLat;
|
|
|
|
act(() => {
|
|
pin.dispatchEvent(pointer('pointerdown', 10, 10));
|
|
pin.dispatchEvent(pointer('pointermove', 30, 40));
|
|
});
|
|
if (provider === 'maplibre-gl') expect(pin.style.transform).not.toBe(startTransform);
|
|
else expect(libraryMarker?.lngLat).not.toEqual(startLngLat);
|
|
expect(onMove).not.toHaveBeenCalled();
|
|
expect(waypoints[0]).toEqual(waypoint);
|
|
|
|
view.rerender(
|
|
<MapViewGL
|
|
places={[]}
|
|
plannerWaypoints={waypoints}
|
|
onPlannerWaypointClick={onSelect}
|
|
onPlannerWaypointMove={onMove}
|
|
onMapClick={onMapClick}
|
|
focusKey="tour:1"
|
|
focusPoints={[[waypoint.lat, waypoint.lng]]}
|
|
routeProfileFocus={{ distanceMeters: 100, elevationMeters: 0, lat: 48.1, lng: 11.1, sampleIndex: 0 }}
|
|
glProvider={provider}
|
|
/>
|
|
);
|
|
if (provider === 'maplibre-gl') expect(pin.style.transform).not.toBe(startTransform);
|
|
else expect(libraryMarker?.lngLat).not.toEqual(startLngLat);
|
|
|
|
act(() => {
|
|
pin.dispatchEvent(pointer('pointerup', 30, 40));
|
|
pin.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
});
|
|
expect(onMove).toHaveBeenCalledOnce();
|
|
expect(onMove).toHaveBeenCalledWith(waypoint.id, 48.8566, 2.3522);
|
|
expect(onSelect).not.toHaveBeenCalled();
|
|
expect(onMapClick).not.toHaveBeenCalled();
|
|
view.rerender(
|
|
<MapViewGL
|
|
places={[]}
|
|
plannerWaypoints={[{ ...waypoint, lat: 48.4, lng: 11.3 }]}
|
|
onPlannerWaypointClick={onSelect}
|
|
onPlannerWaypointMove={onMove}
|
|
onMapClick={onMapClick}
|
|
focusKey="tour:1"
|
|
focusPoints={[[48.4, 11.3]]}
|
|
routeProfileFocus={{ distanceMeters: 100, elevationMeters: 0, lat: 48.1, lng: 11.1, sampleIndex: 0 }}
|
|
glProvider={provider}
|
|
/>
|
|
);
|
|
expect(glMap.fitBounds).not.toHaveBeenCalled();
|
|
expect(glMap.flyTo).not.toHaveBeenCalled();
|
|
expect(glMap.jumpTo).not.toHaveBeenCalled();
|
|
}
|
|
);
|
|
|
|
it('RC-08: pointer cancellation restores the MapLibre pin and plain clicks still select and add separately', async () => {
|
|
loadOnAttach();
|
|
const waypoint = { id: 'start', lat: 48, lng: 11, role: 'start' as const };
|
|
const onMove = vi.fn(() => true);
|
|
const onSelect = vi.fn();
|
|
const onMapClick = vi.fn();
|
|
render(
|
|
<MapViewGL
|
|
places={[]}
|
|
plannerWaypoints={[waypoint]}
|
|
onPlannerWaypointClick={onSelect}
|
|
onPlannerWaypointMove={onMove}
|
|
onMapClick={onMapClick}
|
|
glProvider="maplibre-gl"
|
|
/>
|
|
);
|
|
await act(async () => {});
|
|
const pin = glCanvasContainer.querySelector('[aria-label="Waypoint 1"]') as HTMLButtonElement;
|
|
const initialTransform = pin.style.transform;
|
|
act(() => {
|
|
pin.dispatchEvent(pointer('pointerdown', 10, 10));
|
|
pin.dispatchEvent(pointer('pointermove', 30, 40));
|
|
pin.dispatchEvent(pointer('pointercancel', 30, 40));
|
|
});
|
|
expect(pin.style.transform).toBe(initialTransform);
|
|
expect(onMove).not.toHaveBeenCalled();
|
|
|
|
act(() => {
|
|
pin.dispatchEvent(pointer('pointerdown', 10, 10));
|
|
pin.dispatchEvent(pointer('pointerup', 10, 10));
|
|
pin.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
});
|
|
expect(onSelect).toHaveBeenCalledOnce();
|
|
expect(onSelect).toHaveBeenCalledWith(waypoint.id);
|
|
expect(onMapClick).not.toHaveBeenCalled();
|
|
});
|
|
|
|
const FERRATA = {
|
|
id: 'way/42',
|
|
name: 'Via delle Trincee',
|
|
grade: { raw: '4', level: 4, plus: null },
|
|
routeName: null,
|
|
geometry: [
|
|
[46.52, 11.8],
|
|
[46.53, 11.81],
|
|
] as [number, number][],
|
|
osmUrl: 'https://www.openstreetmap.org/way/42',
|
|
};
|
|
const FERRATA_GROUPS = groupFerratas([FERRATA]);
|
|
const ferrataLayer = (active: boolean) => ({
|
|
active,
|
|
onToggle: vi.fn(),
|
|
status: active ? ('ready' as const) : ('idle' as const),
|
|
count: active ? 1 : 0,
|
|
groups: FERRATA_GROUPS,
|
|
selectedId: null,
|
|
onSelect: vi.fn(),
|
|
});
|
|
const addedFerratas = () => glMap.addSource.mock.calls.filter(([id]) => id === 'trek-ferratas');
|
|
|
|
it('RC-10: the via ferrata layer is drawn on the ready map only while it is on, its switch in the tour pill', async () => {
|
|
loadOnAttach();
|
|
const off = ferrataLayer(false);
|
|
const view = render(
|
|
<MapViewGL
|
|
places={[]}
|
|
viewBaseLayer="topo"
|
|
onViewBaseLayerChange={vi.fn()}
|
|
ferrataLayer={off}
|
|
glProvider="maplibre-gl"
|
|
/>
|
|
);
|
|
await act(async () => {});
|
|
expect(addedFerratas()).toHaveLength(0);
|
|
const toggle = view.getByRole('button', { name: 'Via ferratas' });
|
|
expect(toggle).toHaveAttribute('aria-pressed', 'false');
|
|
act(() => toggle.click());
|
|
expect(off.onToggle).toHaveBeenCalledOnce();
|
|
|
|
view.rerender(
|
|
<MapViewGL
|
|
places={[]}
|
|
viewBaseLayer="topo"
|
|
onViewBaseLayerChange={vi.fn()}
|
|
ferrataLayer={ferrataLayer(true)}
|
|
glProvider="maplibre-gl"
|
|
/>
|
|
);
|
|
await act(async () => {});
|
|
expect(addedFerratas()).toHaveLength(1);
|
|
expect(view.getByRole('button', { name: 'Via ferratas' })).toHaveAttribute('aria-pressed', 'true');
|
|
expect(view.getByTestId('ferrata-count-badge')).toHaveTextContent('1');
|
|
});
|
|
|
|
it('RC-10: a click on a drawn ferrata opens it and is not a map click, so no control point lands under it', async () => {
|
|
loadOnAttach();
|
|
glMap.getLayer.mockImplementation((id: string) => (id === 'trek-ferratas-hit' ? { id } : null));
|
|
glMap.queryRenderedFeatures.mockImplementation((_point: unknown, options?: { layers?: string[] }) =>
|
|
options?.layers?.includes('trek-ferratas-hit') ? [{ properties: { id: FERRATA.id } }] : []
|
|
);
|
|
const layer = ferrataLayer(true);
|
|
const onMapClick = vi.fn();
|
|
render(
|
|
<MapViewGL
|
|
places={[]}
|
|
viewBaseLayer="topo"
|
|
onViewBaseLayerChange={vi.fn()}
|
|
ferrataLayer={layer}
|
|
onMapClick={onMapClick}
|
|
glProvider="maplibre-gl"
|
|
/>
|
|
);
|
|
await act(async () => {});
|
|
const mapClicks = glMap.on.mock.calls
|
|
.filter(([event, handler]) => event === 'click' && typeof handler === 'function')
|
|
.map(([, handler]) => handler as (event: unknown) => void);
|
|
expect(mapClicks.length).toBeGreaterThan(0);
|
|
const click = {
|
|
point: { x: 5, y: 5 },
|
|
lngLat: { lat: 46.52, lng: 11.8 },
|
|
originalEvent: { target: document.createElement('div') },
|
|
};
|
|
act(() => mapClicks.forEach((handler) => handler(click)));
|
|
expect(onMapClick).not.toHaveBeenCalled();
|
|
expect(layer.onSelect).toHaveBeenCalledWith(FERRATA_GROUPS[0], FERRATA, { lat: 46.52, lng: 11.8 });
|
|
});
|
|
});
|