mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 14:36:31 +00:00
a54503f26d
MapLibre/Mapbox showed every place as its own rich HTML marker with no grouping when zoomed out, unlike the Leaflet map. Feed the place points through a clustered GeoJSON source: clustered points render as a dark count bubble (click to zoom in and expand) while the rich HTML photo markers are only drawn for the points the source reports as unclustered. Always on, matching the Leaflet MarkerClusterGroup.
269 lines
8.5 KiB
TypeScript
269 lines
8.5 KiB
TypeScript
import React from 'react'
|
|
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'
|
|
import { render } from '../../../tests/helpers/render'
|
|
import { act } from '@testing-library/react'
|
|
import { resetAllStores } from '../../../tests/helpers/store'
|
|
import { buildPlace } from '../../../tests/helpers/factories'
|
|
import { useSettingsStore } from '../../store/settingsStore'
|
|
|
|
// Stable fake map so fitBounds call counts survive re-renders.
|
|
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),
|
|
addControl: vi.fn(),
|
|
removeControl: vi.fn(),
|
|
remove: vi.fn(),
|
|
addSource: vi.fn(),
|
|
getSource: vi.fn().mockReturnValue(null),
|
|
addLayer: vi.fn(),
|
|
setLayoutProperty: vi.fn(),
|
|
getStyle: vi.fn().mockReturnValue({ layers: [] }),
|
|
isStyleLoaded: vi.fn().mockReturnValue(true),
|
|
getCanvasContainer: vi.fn(() => document.createElement('div')),
|
|
getLayer: vi.fn().mockReturnValue(null),
|
|
queryRenderedFeatures: vi.fn().mockReturnValue([]),
|
|
querySourceFeatures: vi.fn().mockReturnValue([]),
|
|
easeTo: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('mapbox-gl', () => ({
|
|
default: {
|
|
accessToken: '',
|
|
Map: vi.fn(function () {
|
|
return glMap
|
|
}),
|
|
Marker: vi.fn(function () {
|
|
return {
|
|
setLngLat: vi.fn().mockReturnThis(),
|
|
addTo: vi.fn().mockReturnThis(),
|
|
remove: vi.fn(),
|
|
getElement: vi.fn(() => document.createElement('div')),
|
|
}
|
|
}),
|
|
LngLatBounds: vi.fn(function () {
|
|
return { extend: vi.fn().mockReturnThis() }
|
|
}),
|
|
NavigationControl: vi.fn(),
|
|
Popup: vi.fn(function () {
|
|
return {
|
|
setLngLat: vi.fn().mockReturnThis(),
|
|
setHTML: vi.fn().mockReturnThis(),
|
|
addTo: vi.fn().mockReturnThis(),
|
|
remove: vi.fn(),
|
|
}
|
|
}),
|
|
},
|
|
}))
|
|
vi.mock('mapbox-gl/dist/mapbox-gl.css', () => ({}))
|
|
|
|
vi.mock('maplibre-gl', () => ({
|
|
default: {
|
|
Map: vi.fn(function () {
|
|
return glMap
|
|
}),
|
|
Marker: vi.fn(function () {
|
|
return {
|
|
setLngLat: vi.fn().mockReturnThis(),
|
|
addTo: vi.fn().mockReturnThis(),
|
|
remove: vi.fn(),
|
|
getElement: vi.fn(() => document.createElement('div')),
|
|
}
|
|
}),
|
|
LngLatBounds: vi.fn(function () {
|
|
return { extend: vi.fn().mockReturnThis() }
|
|
}),
|
|
NavigationControl: vi.fn(),
|
|
Popup: vi.fn(function () {
|
|
return {
|
|
setLngLat: vi.fn().mockReturnThis(),
|
|
setHTML: vi.fn().mockReturnThis(),
|
|
addTo: vi.fn().mockReturnThis(),
|
|
remove: vi.fn(),
|
|
}
|
|
}),
|
|
},
|
|
}))
|
|
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(),
|
|
}))
|
|
|
|
vi.mock('./locationMarkerMapbox', () => ({
|
|
attachLocationMarker: vi.fn(() => ({ update: vi.fn() })),
|
|
}))
|
|
|
|
vi.mock('./reservationsMapbox', () => ({
|
|
ReservationMapboxOverlay: vi.fn(function () {
|
|
return { update: vi.fn(), destroy: vi.fn() }
|
|
}),
|
|
}))
|
|
|
|
vi.mock('../../hooks/useGeolocation', () => ({
|
|
useGeolocation: vi.fn(() => ({
|
|
position: null,
|
|
mode: 'off',
|
|
error: null,
|
|
cycleMode: vi.fn(),
|
|
setMode: vi.fn(),
|
|
})),
|
|
}))
|
|
|
|
vi.mock('../../services/photoService', () => ({
|
|
getCached: vi.fn(() => null),
|
|
isLoading: vi.fn(() => false),
|
|
fetchPhoto: vi.fn(),
|
|
onThumbReady: vi.fn(() => () => {}),
|
|
getAllThumbs: vi.fn(() => ({})),
|
|
}))
|
|
|
|
import { MapViewGL } from './MapViewGL'
|
|
|
|
function buildMapPlace(overrides: Record<string, any> = {}) {
|
|
return {
|
|
...buildPlace(),
|
|
category_name: null,
|
|
category_color: null,
|
|
category_icon: null,
|
|
...overrides,
|
|
} as any
|
|
}
|
|
|
|
beforeEach(() => {
|
|
glMap.on.mockImplementation(() => glMap)
|
|
glMap.off.mockImplementation(() => glMap)
|
|
glMap.once.mockImplementation(() => glMap)
|
|
glMap.getSource.mockReturnValue(null)
|
|
glMap.getLayer.mockReturnValue(null)
|
|
glMap.queryRenderedFeatures.mockReturnValue([])
|
|
glMap.querySourceFeatures.mockReturnValue([])
|
|
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,
|
|
},
|
|
} as any)
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks()
|
|
resetAllStores()
|
|
})
|
|
|
|
describe('MapViewGL', () => {
|
|
it('FE-COMP-MAPVIEWGL-001: opening place inspector does not refit bounds (issue #921)', async () => {
|
|
const places = [
|
|
buildMapPlace({ id: 1, lat: 48.8584, lng: 2.2945 }),
|
|
buildMapPlace({ id: 2, lat: 48.86, lng: 2.337 }),
|
|
]
|
|
|
|
const { rerender } = render(
|
|
<MapViewGL places={places} fitKey={1} selectedPlaceId={null} hasInspector={false} />,
|
|
)
|
|
await act(async () => {})
|
|
const after_initial = glMap.fitBounds.mock.calls.length
|
|
|
|
// Selecting a place flips hasInspector → paddingOpts memo changes.
|
|
// fitBounds must NOT fire again (this was the bug).
|
|
rerender(
|
|
<MapViewGL places={places} fitKey={1} selectedPlaceId={1} hasInspector={true} />,
|
|
)
|
|
await act(async () => {})
|
|
expect(glMap.fitBounds).toHaveBeenCalledTimes(after_initial)
|
|
})
|
|
|
|
it('FE-COMP-MAPVIEWGL-002: closing inspector does not refit bounds (issue #921)', async () => {
|
|
const places = [
|
|
buildMapPlace({ id: 1, lat: 48.8584, lng: 2.2945 }),
|
|
]
|
|
|
|
const { rerender } = render(
|
|
<MapViewGL places={places} fitKey={1} selectedPlaceId={1} hasInspector={true} />,
|
|
)
|
|
await act(async () => {})
|
|
const after_initial = glMap.fitBounds.mock.calls.length
|
|
|
|
// Closing inspector (X button) clears selectedPlaceId → hasInspector=false → new paddingOpts.
|
|
rerender(
|
|
<MapViewGL places={places} fitKey={1} selectedPlaceId={null} hasInspector={false} />,
|
|
)
|
|
await act(async () => {})
|
|
expect(glMap.fitBounds).toHaveBeenCalledTimes(after_initial)
|
|
})
|
|
|
|
it('FE-COMP-MAPVIEWGL-003: bumping fitKey triggers a new fitBounds call', async () => {
|
|
const places = [
|
|
buildMapPlace({ id: 1, lat: 48.8584, lng: 2.2945 }),
|
|
]
|
|
|
|
const { rerender } = render(<MapViewGL places={places} fitKey={1} />)
|
|
await act(async () => {})
|
|
const after_first = glMap.fitBounds.mock.calls.length
|
|
|
|
rerender(<MapViewGL places={places} fitKey={2} />)
|
|
await act(async () => {})
|
|
expect(glMap.fitBounds.mock.calls.length).toBeGreaterThan(after_first)
|
|
})
|
|
|
|
it('FE-COMP-MAPVIEWGL-004: renders with the MapLibre provider and no token', async () => {
|
|
const mapboxgl = (await import('mapbox-gl')).default
|
|
const maplibregl = (await import('maplibre-gl')).default
|
|
useSettingsStore.setState({
|
|
settings: {
|
|
...useSettingsStore.getState().settings,
|
|
map_provider: 'maplibre-gl',
|
|
mapbox_access_token: '', // MapLibre/OpenFreeMap is tokenless — must not short-circuit
|
|
maplibre_style: 'https://tiles.openfreemap.org/styles/liberty',
|
|
},
|
|
} as any)
|
|
const places = [buildMapPlace({ id: 1, lat: 48.8584, lng: 2.2945 })]
|
|
|
|
render(<MapViewGL places={places} fitKey={1} glProvider="maplibre-gl" />)
|
|
await act(async () => {})
|
|
|
|
// The MapLibre engine builds the map even without a token; Mapbox is not used.
|
|
expect(maplibregl.Map).toHaveBeenCalled()
|
|
expect(mapboxgl.Map).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('FE-COMP-MAPVIEWGL-005: adds the clustered place source + layers so markers group on zoom-out (#1385)', async () => {
|
|
glMap.on.mockImplementation((event: string, handlerOrLayer: unknown) => {
|
|
if (event === 'load' && typeof handlerOrLayer === 'function') (handlerOrLayer as () => void)()
|
|
return glMap
|
|
})
|
|
useSettingsStore.setState({
|
|
settings: {
|
|
...useSettingsStore.getState().settings,
|
|
map_provider: 'maplibre-gl',
|
|
mapbox_access_token: '',
|
|
maplibre_style: 'https://tiles.openfreemap.org/styles/liberty',
|
|
},
|
|
} as any)
|
|
|
|
render(<MapViewGL places={[buildMapPlace({ id: 1, lat: 48.8584, lng: 2.2945 })]} fitKey={1} glProvider="maplibre-gl" />)
|
|
await act(async () => {})
|
|
|
|
expect(glMap.addSource).toHaveBeenCalledWith('trip-place-clusters', expect.objectContaining({
|
|
type: 'geojson',
|
|
cluster: true,
|
|
clusterRadius: 30,
|
|
clusterMaxZoom: 10,
|
|
}))
|
|
expect(glMap.addLayer).toHaveBeenCalledWith(expect.objectContaining({ id: 'trip-place-clusters-circle' }))
|
|
expect(glMap.addLayer).toHaveBeenCalledWith(expect.objectContaining({ id: 'trip-place-clusters-count' }))
|
|
})
|
|
})
|