mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2026-04-03 08:05:33 +00:00
1069 lines
35 KiB
JavaScript
1069 lines
35 KiB
JavaScript
import mapboxgl from 'mapbox-gl'
|
|
import { mount } from '@vue/test-utils'
|
|
import VueMeta from 'vue-meta'
|
|
import Vuex from 'vuex'
|
|
import Map from './map'
|
|
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder'
|
|
|
|
jest.mock('@mapbox/mapbox-gl-geocoder', () => {
|
|
return jest.fn().mockImplementation(jest.fn())
|
|
})
|
|
|
|
jest.mock('mapbox-gl', () => {
|
|
const popupInstance = {
|
|
isOpen: jest.fn(() => false),
|
|
remove: jest.fn(),
|
|
setLngLat: jest.fn(() => popupInstance),
|
|
setHTML: jest.fn(() => popupInstance),
|
|
setDOMContent: jest.fn(() => popupInstance),
|
|
addTo: jest.fn(() => popupInstance),
|
|
}
|
|
return {
|
|
accessToken: null,
|
|
GeolocateControl: jest.fn(),
|
|
Map: jest.fn(() => ({
|
|
addControl: jest.fn(),
|
|
on: jest.fn(),
|
|
remove: jest.fn(),
|
|
})),
|
|
NavigationControl: jest.fn(),
|
|
Popup: jest.fn(() => popupInstance),
|
|
__popupInstance: popupInstance,
|
|
}
|
|
})
|
|
|
|
const localVue = global.localVue
|
|
localVue.use(VueMeta, { keyName: 'head' })
|
|
|
|
const onEventMocks = {}
|
|
|
|
const mapOnMock = jest.fn((key, ...args) => {
|
|
onEventMocks[key] = args[args.length - 1]
|
|
})
|
|
const mapAddControlMock = jest.fn()
|
|
const mapAddSourceMock = jest.fn()
|
|
const mapAddLayerMock = jest.fn()
|
|
const mapAddImageMock = jest.fn()
|
|
const mapSetStyleMock = jest.fn()
|
|
const mapSetLayoutPropertyMock = jest.fn()
|
|
const mapFlyToMock = jest.fn()
|
|
const containerClickHandlers = []
|
|
const mapGetContainerMock = jest.fn(() => ({
|
|
addEventListener: jest.fn((event, handler) => {
|
|
if (event === 'click') containerClickHandlers.push(handler)
|
|
}),
|
|
}))
|
|
const mapQueryRenderedFeaturesMock = jest.fn(() => [])
|
|
|
|
const mapLoadImageMock = jest.fn((url, callback) => {
|
|
callback(null, 'image-data')
|
|
})
|
|
|
|
const mapGetStyleMock = jest.fn(() => ({
|
|
layers: [
|
|
{ id: 'some-label', layout: {} },
|
|
{ id: 'water-fill', layout: {} },
|
|
],
|
|
}))
|
|
|
|
const mapMock = {
|
|
on: mapOnMock,
|
|
addControl: mapAddControlMock,
|
|
addSource: mapAddSourceMock,
|
|
addLayer: mapAddLayerMock,
|
|
addImage: mapAddImageMock,
|
|
loadImage: mapLoadImageMock,
|
|
setStyle: mapSetStyleMock,
|
|
setLayoutProperty: mapSetLayoutPropertyMock,
|
|
flyTo: mapFlyToMock,
|
|
getContainer: mapGetContainerMock,
|
|
queryRenderedFeatures: mapQueryRenderedFeaturesMock,
|
|
getStyle: mapGetStyleMock,
|
|
getCanvas: jest.fn(() => ({
|
|
style: { cursor: '' },
|
|
})),
|
|
}
|
|
|
|
const stubs = {
|
|
'client-only': {
|
|
template: '<div><slot /></div>',
|
|
},
|
|
'mgl-map': {
|
|
template: '<div class="mgl-map-stub"><slot /></div>',
|
|
},
|
|
MglFullscreenControl: true,
|
|
MglNavigationControl: true,
|
|
MglGeolocateControl: true,
|
|
MglScaleControl: true,
|
|
empty: true,
|
|
}
|
|
|
|
const currentUser = {
|
|
id: 'u1',
|
|
slug: 'peter',
|
|
name: 'Peter Lustig',
|
|
about: 'Some about text',
|
|
}
|
|
|
|
const userLocation = {
|
|
id: 'loc1',
|
|
name: 'Berlin',
|
|
lng: 13.38333,
|
|
lat: 52.51667,
|
|
}
|
|
|
|
const otherUsers = [
|
|
{
|
|
id: 'u2',
|
|
slug: 'bob',
|
|
name: 'Bob',
|
|
about: 'Builder',
|
|
location: { id: 'loc2', name: 'Hamburg', lng: 10.0, lat: 53.55 },
|
|
},
|
|
{
|
|
id: 'u3',
|
|
slug: 'jenny',
|
|
name: 'Jenny',
|
|
about: null,
|
|
location: { id: 'loc3', name: 'Paris', lng: 2.35183, lat: 48.85658 },
|
|
},
|
|
]
|
|
|
|
const groups = [
|
|
{
|
|
id: 'g1',
|
|
slug: 'journalism',
|
|
name: 'Investigative Journalism',
|
|
about: 'Investigating things',
|
|
location: { id: 'loc2', name: 'Hamburg', lng: 10.0, lat: 53.55 },
|
|
},
|
|
]
|
|
|
|
const posts = [
|
|
{
|
|
id: 'e1',
|
|
slug: 'kindergeburtstag',
|
|
title: 'Kindergeburtstag',
|
|
content: '<p>Fun event</p>',
|
|
postType: 'Event',
|
|
eventLocation: { id: 'loc4', name: 'Stuttgart', lng: 9.17702, lat: 48.78232 },
|
|
},
|
|
]
|
|
|
|
describe('map', () => {
|
|
let wrapper
|
|
let mocks
|
|
let store
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
MapboxGeocoder.mockClear()
|
|
containerClickHandlers.length = 0
|
|
|
|
// Reset popup mock
|
|
mapboxgl.__popupInstance.isOpen.mockReturnValue(false)
|
|
|
|
mocks = {
|
|
$t: (t) => t,
|
|
$i18n: { locale: () => 'en' },
|
|
$env: {
|
|
MAPBOX_TOKEN: 'MY_MAPBOX_TOKEN',
|
|
},
|
|
$toast: {
|
|
error: jest.fn(),
|
|
},
|
|
$apollo: {
|
|
query: jest.fn().mockResolvedValue({
|
|
data: { User: [{ location: null }] },
|
|
}),
|
|
},
|
|
$filters: {
|
|
removeHtml: jest.fn((html) => html.replace(/<[^>]*>/g, '')),
|
|
},
|
|
}
|
|
|
|
store = new Vuex.Store({
|
|
getters: { 'auth/user': () => currentUser },
|
|
})
|
|
})
|
|
|
|
const createWrapper = () => {
|
|
return mount(Map, {
|
|
mocks,
|
|
localVue,
|
|
stubs,
|
|
store,
|
|
})
|
|
}
|
|
|
|
describe('without MAPBOX_TOKEN', () => {
|
|
beforeEach(() => {
|
|
mocks.$env.MAPBOX_TOKEN = ''
|
|
wrapper = createWrapper()
|
|
})
|
|
|
|
it('shows empty alert', () => {
|
|
expect(wrapper.find('empty-stub').exists()).toBe(true)
|
|
})
|
|
|
|
it('does not render map', () => {
|
|
expect(wrapper.find('.mgl-map-stub').exists()).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('with MAPBOX_TOKEN', () => {
|
|
beforeEach(() => {
|
|
wrapper = createWrapper()
|
|
})
|
|
|
|
it('renders map page', () => {
|
|
expect(wrapper.find('.map-page').exists()).toBe(true)
|
|
})
|
|
|
|
it('has correct <head> content', () => {
|
|
expect(wrapper.vm.$metaInfo.title).toBe('map.pageTitle')
|
|
})
|
|
|
|
it('renders legend with all marker types', () => {
|
|
const items = wrapper.findAll('.map-legend-item')
|
|
expect(items.length).toBe(4)
|
|
})
|
|
|
|
it('legend is closed by default on mobile', () => {
|
|
expect(wrapper.vm.legendOpen).toBe(false)
|
|
})
|
|
|
|
describe('legend toggle', () => {
|
|
it('toggles legendOpen on click', async () => {
|
|
const toggle = wrapper.find('.map-legend-toggle')
|
|
await toggle.trigger('click')
|
|
expect(wrapper.vm.legendOpen).toBe(true)
|
|
await toggle.trigger('click')
|
|
expect(wrapper.vm.legendOpen).toBe(false)
|
|
})
|
|
|
|
it('shows arrow up when closed', () => {
|
|
expect(wrapper.find('.map-legend-arrow').text()).toBe('▲')
|
|
})
|
|
|
|
it('shows arrow down when open', async () => {
|
|
await wrapper.find('.map-legend-toggle').trigger('click')
|
|
expect(wrapper.find('.map-legend-arrow').text()).toBe('▼')
|
|
})
|
|
})
|
|
|
|
describe('computed properties', () => {
|
|
it('mapCenter returns default center without user location', () => {
|
|
expect(wrapper.vm.mapCenter).toEqual([10.452764, 51.165707])
|
|
})
|
|
|
|
it('mapZoom returns 4 without user location', () => {
|
|
expect(wrapper.vm.mapZoom).toBe(4)
|
|
})
|
|
|
|
it('mapCenter returns user coordinates when available', async () => {
|
|
await wrapper.setData({ currentUserCoordinates: [13.38, 52.52] })
|
|
expect(wrapper.vm.mapCenter).toEqual([13.38, 52.52])
|
|
})
|
|
|
|
it('mapZoom returns 10 when user has location', async () => {
|
|
await wrapper.setData({ currentUserCoordinates: [13.38, 52.52] })
|
|
expect(wrapper.vm.mapZoom).toBe(10)
|
|
})
|
|
|
|
it('availableStyles has 4 styles with titles', () => {
|
|
const styles = wrapper.vm.availableStyles
|
|
expect(Object.keys(styles)).toEqual(['outdoors', 'streets', 'satellite', 'dark'])
|
|
expect(styles.outdoors.title).toBe('map.styles.outdoors')
|
|
})
|
|
|
|
it('mapOptions uses outdoors style by default', () => {
|
|
expect(wrapper.vm.mapOptions.style).toContain('outdoors')
|
|
})
|
|
|
|
it('mapOptions uses activeStyle when set', async () => {
|
|
await wrapper.setData({ activeStyle: 'mapbox://custom' })
|
|
expect(wrapper.vm.mapOptions.style).toBe('mapbox://custom')
|
|
})
|
|
|
|
it('isPreparedForMarkers is false initially', () => {
|
|
expect(wrapper.vm.isPreparedForMarkers).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('updateMapPosition', () => {
|
|
it('sets top from navbar height', () => {
|
|
const navbar = document.createElement('div')
|
|
navbar.id = 'navbar'
|
|
Object.defineProperty(navbar, 'offsetHeight', { value: 60 })
|
|
document.body.appendChild(navbar)
|
|
wrapper.vm.updateMapPosition()
|
|
expect(wrapper.vm.$el.style.top).toBe('60px')
|
|
document.body.removeChild(navbar)
|
|
})
|
|
|
|
it('sets bottom to 0 when footer is hidden', () => {
|
|
wrapper.vm.updateMapPosition()
|
|
expect(wrapper.vm.$el.style.bottom).toBe('0px')
|
|
})
|
|
|
|
it('sets bottom from footer height when visible', () => {
|
|
const footer = document.createElement('div')
|
|
footer.id = 'footer'
|
|
Object.defineProperty(footer, 'offsetHeight', { value: 40 })
|
|
document.body.appendChild(footer)
|
|
wrapper.vm.updateMapPosition()
|
|
expect(wrapper.vm.$el.style.bottom).toBe('40px')
|
|
document.body.removeChild(footer)
|
|
})
|
|
})
|
|
|
|
describe('onMapLoad', () => {
|
|
beforeEach(async () => {
|
|
wrapper.vm.onMapLoad({ map: mapMock })
|
|
})
|
|
|
|
it('registers style.load event', () => {
|
|
expect(mapOnMock).toHaveBeenCalledWith('style.load', expect.any(Function))
|
|
})
|
|
|
|
it('registers mouseenter event on markers layer', () => {
|
|
expect(mapOnMock).toHaveBeenCalledWith('mouseenter', 'markers', expect.any(Function))
|
|
})
|
|
|
|
it('registers mouseleave event on markers layer', () => {
|
|
expect(mapOnMock).toHaveBeenCalledWith('mouseleave', 'markers', expect.any(Function))
|
|
})
|
|
|
|
it('registers click event on markers layer', () => {
|
|
expect(mapOnMock).toHaveBeenCalledWith('click', 'markers', expect.any(Function))
|
|
})
|
|
|
|
it('adds geocoder control', () => {
|
|
expect(MapboxGeocoder).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
accessToken: 'MY_MAPBOX_TOKEN',
|
|
marker: false,
|
|
}),
|
|
)
|
|
expect(mapAddControlMock).toHaveBeenCalled()
|
|
})
|
|
|
|
it('adds style switcher control', () => {
|
|
// style switcher is the second addControl call (after geocoder)
|
|
const styleSwitcherCall = mapAddControlMock.mock.calls.find(
|
|
(call) => call[1] === 'top-right' && call[0].onAdd,
|
|
)
|
|
expect(styleSwitcherCall).toBeTruthy()
|
|
})
|
|
|
|
it('creates popup', () => {
|
|
expect(mapboxgl.Popup).toHaveBeenCalledWith({
|
|
closeButton: true,
|
|
closeOnClick: true,
|
|
maxWidth: '300px',
|
|
})
|
|
})
|
|
|
|
it('calls loadMarkersIconsAndAddMarkers', () => {
|
|
expect(mapLoadImageMock).toHaveBeenCalled()
|
|
})
|
|
|
|
describe('style.load event', () => {
|
|
it('reloads marker icons', () => {
|
|
mapLoadImageMock.mockClear()
|
|
mapAddImageMock.mockClear()
|
|
onEventMocks['style.load']()
|
|
expect(mapLoadImageMock).toHaveBeenCalledTimes(4)
|
|
})
|
|
|
|
it('re-adds source and layer after style change when markers exist', async () => {
|
|
// Prepare marker data so addMarkersOnCheckPrepared adds source/layer
|
|
await wrapper.setData({
|
|
users: otherUsers,
|
|
groups,
|
|
posts,
|
|
currentUserCoordinates: null,
|
|
currentUserLocation: null,
|
|
})
|
|
wrapper.vm.markers.isGeoJSON = true
|
|
wrapper.vm.markers.isSourceAndLayerAdded = true
|
|
|
|
mapAddSourceMock.mockClear()
|
|
mapAddLayerMock.mockClear()
|
|
|
|
onEventMocks['style.load']()
|
|
|
|
// loadMarkersIconsAndAddMarkers uses Promise.all().then() — flush microtasks
|
|
await wrapper.vm.$nextTick()
|
|
|
|
// After style.load, isSourceAndLayerAdded is reset and icons reload,
|
|
// then addMarkersOnCheckPrepared re-adds source and layer
|
|
expect(wrapper.vm.markers.isSourceAndLayerAdded).toBe(true)
|
|
expect(mapAddSourceMock).toHaveBeenCalledWith('markers', expect.any(Object))
|
|
expect(mapAddLayerMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({ id: 'markers', type: 'symbol' }),
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('style switcher control', () => {
|
|
let container
|
|
|
|
beforeEach(() => {
|
|
const styleSwitcherCall = mapAddControlMock.mock.calls.find(
|
|
(call) => call[1] === 'top-right' && call[0].onAdd,
|
|
)
|
|
container = styleSwitcherCall[0].onAdd()
|
|
})
|
|
|
|
it('creates container with correct class', () => {
|
|
expect(container.className).toBe('mapboxgl-ctrl map-style-switcher')
|
|
})
|
|
|
|
it('has a toggle button', () => {
|
|
const toggle = container.querySelector('.map-style-switcher-toggle')
|
|
expect(toggle).toBeTruthy()
|
|
expect(toggle.querySelector('svg')).toBeTruthy()
|
|
})
|
|
|
|
it('has a popover with 4 style buttons', () => {
|
|
const buttons = container.querySelectorAll('.map-style-popover-btn')
|
|
expect(buttons.length).toBe(4)
|
|
})
|
|
|
|
it('marks active style', () => {
|
|
const active = container.querySelector('.map-style-popover-btn--active')
|
|
expect(active).toBeTruthy()
|
|
})
|
|
|
|
it('toggle opens popover', () => {
|
|
const toggle = container.querySelector('.map-style-switcher-toggle')
|
|
const popover = container.querySelector('.map-style-popover')
|
|
toggle.click()
|
|
expect(popover.classList.contains('map-style-popover--open')).toBe(true)
|
|
})
|
|
|
|
it('toggle closes popover on second click', () => {
|
|
const toggle = container.querySelector('.map-style-switcher-toggle')
|
|
const popover = container.querySelector('.map-style-popover')
|
|
toggle.click()
|
|
toggle.click()
|
|
expect(popover.classList.contains('map-style-popover--open')).toBe(false)
|
|
})
|
|
|
|
it('clicking on map closes popover', () => {
|
|
const popover = container.querySelector('.map-style-popover')
|
|
popover.classList.add('map-style-popover--open')
|
|
containerClickHandlers.forEach((handler) => handler())
|
|
expect(popover.classList.contains('map-style-popover--open')).toBe(false)
|
|
})
|
|
|
|
it('clicking style button sets correct style and closes popover', () => {
|
|
const buttons = container.querySelectorAll('.map-style-popover-btn')
|
|
const popover = container.querySelector('.map-style-popover')
|
|
popover.classList.add('map-style-popover--open')
|
|
// Click "streets" button (index 1)
|
|
buttons[1].click()
|
|
expect(mapSetStyleMock).toHaveBeenCalledWith(wrapper.vm.availableStyles.streets.url)
|
|
expect(wrapper.vm.activeStyle).toBe(wrapper.vm.availableStyles.streets.url)
|
|
expect(popover.classList.contains('map-style-popover--open')).toBe(false)
|
|
expect(buttons[1].classList.contains('map-style-popover-btn--active')).toBe(true)
|
|
// Previous active button should no longer be active
|
|
expect(buttons[0].classList.contains('map-style-popover-btn--active')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('mouseenter event', () => {
|
|
const features = [
|
|
{
|
|
geometry: { coordinates: [10.0, 53.55] },
|
|
properties: {
|
|
type: 'user',
|
|
slug: 'bob',
|
|
id: 'u2',
|
|
name: 'Bob',
|
|
locationName: 'Hamburg',
|
|
description: 'Builder',
|
|
},
|
|
},
|
|
]
|
|
|
|
const getPopupDOM = () => mapboxgl.__popupInstance.setDOMContent.mock.calls[0][0]
|
|
|
|
it('shows popup when features found', () => {
|
|
mapQueryRenderedFeaturesMock.mockReturnValueOnce(features)
|
|
onEventMocks.mouseenter({
|
|
point: { x: 100, y: 200 },
|
|
lngLat: { lng: 10.0, lat: 53.55 },
|
|
})
|
|
expect(mapboxgl.__popupInstance.setLngLat).toHaveBeenCalled()
|
|
expect(mapboxgl.__popupInstance.setDOMContent).toHaveBeenCalled()
|
|
expect(mapboxgl.__popupInstance.addTo).toHaveBeenCalledWith(mapMock)
|
|
})
|
|
|
|
it('does not show popup when no features', () => {
|
|
mapQueryRenderedFeaturesMock.mockReturnValueOnce([])
|
|
onEventMocks.mouseenter({
|
|
point: { x: 100, y: 200 },
|
|
lngLat: { lng: 10.0, lat: 53.55 },
|
|
})
|
|
expect(mapboxgl.__popupInstance.setLngLat).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('popup includes location name header', () => {
|
|
mapQueryRenderedFeaturesMock.mockReturnValueOnce(features)
|
|
onEventMocks.mouseenter({
|
|
point: { x: 100, y: 200 },
|
|
lngLat: { lng: 10.0, lat: 53.55 },
|
|
})
|
|
const dom = getPopupDOM()
|
|
const header = dom.querySelector('.map-popup-header')
|
|
expect(header).toBeTruthy()
|
|
expect(header.textContent).toBe('Hamburg')
|
|
})
|
|
|
|
it('popup includes user name and profile link', () => {
|
|
mapQueryRenderedFeaturesMock.mockReturnValueOnce(features)
|
|
onEventMocks.mouseenter({
|
|
point: { x: 100, y: 200 },
|
|
lngLat: { lng: 10.0, lat: 53.55 },
|
|
})
|
|
const dom = getPopupDOM()
|
|
expect(dom.textContent).toContain('Bob')
|
|
const link = dom.querySelector('a')
|
|
expect(link.textContent).toBe('@bob')
|
|
expect(link.getAttribute('href')).toBe('/profile/u2/bob')
|
|
expect(link.getAttribute('rel')).toBe('noopener noreferrer')
|
|
})
|
|
|
|
it('popup includes description when present', () => {
|
|
mapQueryRenderedFeaturesMock.mockReturnValueOnce(features)
|
|
onEventMocks.mouseenter({
|
|
point: { x: 100, y: 200 },
|
|
lngLat: { lng: 10.0, lat: 53.55 },
|
|
})
|
|
const dom = getPopupDOM()
|
|
expect(dom.textContent).toContain('Builder')
|
|
})
|
|
|
|
it('popup shows multiple features separated by hr', () => {
|
|
const multiFeatures = [
|
|
...features,
|
|
{
|
|
geometry: { coordinates: [10.0, 53.55] },
|
|
properties: {
|
|
type: 'group',
|
|
slug: 'journalism',
|
|
id: 'g1',
|
|
name: 'Journalism',
|
|
locationName: 'Hamburg',
|
|
description: '',
|
|
},
|
|
},
|
|
]
|
|
mapQueryRenderedFeaturesMock.mockReturnValueOnce(multiFeatures)
|
|
onEventMocks.mouseenter({
|
|
point: { x: 100, y: 200 },
|
|
lngLat: { lng: 10.0, lat: 53.55 },
|
|
})
|
|
const dom = getPopupDOM()
|
|
expect(dom.querySelectorAll('hr').length).toBe(1)
|
|
const links = dom.querySelectorAll('a')
|
|
expect(links[1].textContent).toBe('&journalism')
|
|
expect(links[1].getAttribute('href')).toBe('/groups/g1/journalism')
|
|
})
|
|
|
|
it('clears pending leave timeout', () => {
|
|
jest.useFakeTimers()
|
|
wrapper.vm.popupOnLeaveTimeoutId = setTimeout(() => {}, 3000)
|
|
mapQueryRenderedFeaturesMock.mockReturnValueOnce(features)
|
|
onEventMocks.mouseenter({
|
|
point: { x: 100, y: 200 },
|
|
lngLat: { lng: 10.0, lat: 53.55 },
|
|
})
|
|
expect(wrapper.vm.popupOnLeaveTimeoutId).toBeNull()
|
|
jest.useRealTimers()
|
|
})
|
|
|
|
it('removes existing popup before showing new one', () => {
|
|
mapboxgl.__popupInstance.isOpen.mockReturnValueOnce(true)
|
|
mapQueryRenderedFeaturesMock.mockReturnValueOnce(features)
|
|
onEventMocks.mouseenter({
|
|
point: { x: 100, y: 200 },
|
|
lngLat: { lng: 10.0, lat: 53.55 },
|
|
})
|
|
expect(mapboxgl.__popupInstance.remove).toHaveBeenCalled()
|
|
})
|
|
|
|
it('adjusts coordinates for wrapped map', () => {
|
|
const wrappedFeatures = [
|
|
{
|
|
geometry: { coordinates: [370.0, 53.55] },
|
|
properties: {
|
|
type: 'user',
|
|
slug: 'bob',
|
|
id: 'u2',
|
|
name: 'Bob',
|
|
locationName: 'Hamburg',
|
|
description: '',
|
|
},
|
|
},
|
|
]
|
|
mapQueryRenderedFeaturesMock.mockReturnValueOnce(wrappedFeatures)
|
|
onEventMocks.mouseenter({
|
|
point: { x: 100, y: 200 },
|
|
lngLat: { lng: 10.0, lat: 53.55 },
|
|
})
|
|
const coords = mapboxgl.__popupInstance.setLngLat.mock.calls[0][0]
|
|
expect(coords[0]).toBe(10.0)
|
|
})
|
|
})
|
|
|
|
describe('mouseleave event', () => {
|
|
it('sets timeout to remove popup when open', () => {
|
|
jest.useFakeTimers()
|
|
mapboxgl.__popupInstance.isOpen.mockReturnValueOnce(true)
|
|
onEventMocks.mouseleave()
|
|
expect(wrapper.vm.popupOnLeaveTimeoutId).toBeTruthy()
|
|
jest.advanceTimersByTime(3000)
|
|
expect(mapboxgl.__popupInstance.remove).toHaveBeenCalled()
|
|
jest.useRealTimers()
|
|
})
|
|
|
|
it('does nothing when popup is not open', () => {
|
|
mapboxgl.__popupInstance.isOpen.mockReturnValueOnce(false)
|
|
onEventMocks.mouseleave()
|
|
expect(wrapper.vm.popupOnLeaveTimeoutId).toBeFalsy()
|
|
})
|
|
})
|
|
|
|
describe('click event on markers', () => {
|
|
const features = [
|
|
{
|
|
geometry: { coordinates: [10.0, 53.55] },
|
|
properties: {
|
|
type: 'user',
|
|
slug: 'bob',
|
|
id: 'u2',
|
|
name: 'Bob',
|
|
locationName: 'Hamburg',
|
|
description: '',
|
|
},
|
|
},
|
|
]
|
|
|
|
it('shows popup and stops propagation', () => {
|
|
const stopPropagation = jest.fn()
|
|
mapQueryRenderedFeaturesMock.mockReturnValueOnce(features)
|
|
onEventMocks.click({
|
|
point: { x: 100, y: 200 },
|
|
lngLat: { lng: 10.0, lat: 53.55 },
|
|
originalEvent: { stopPropagation },
|
|
})
|
|
expect(mapboxgl.__popupInstance.setLngLat).toHaveBeenCalled()
|
|
expect(stopPropagation).toHaveBeenCalled()
|
|
})
|
|
|
|
it('does nothing when no features found', () => {
|
|
mapQueryRenderedFeaturesMock.mockReturnValueOnce([])
|
|
onEventMocks.click({
|
|
point: { x: 100, y: 200 },
|
|
lngLat: { lng: 10.0, lat: 53.55 },
|
|
originalEvent: { stopPropagation: jest.fn() },
|
|
})
|
|
expect(mapboxgl.__popupInstance.setLngLat).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('popup content for different marker types', () => {
|
|
const getPopupDOMForType = () => mapboxgl.__popupInstance.setDOMContent.mock.calls[0][0]
|
|
|
|
it('generates correct link for event type', () => {
|
|
const eventFeatures = [
|
|
{
|
|
geometry: { coordinates: [9.17, 48.78] },
|
|
properties: {
|
|
type: 'event',
|
|
slug: 'party',
|
|
id: 'e1',
|
|
name: 'Party',
|
|
locationName: 'Stuttgart',
|
|
description: '',
|
|
},
|
|
},
|
|
]
|
|
mapQueryRenderedFeaturesMock.mockReturnValueOnce(eventFeatures)
|
|
onEventMocks.mouseenter({
|
|
point: { x: 100, y: 200 },
|
|
lngLat: { lng: 9.17, lat: 48.78 },
|
|
})
|
|
const dom = getPopupDOMForType()
|
|
const link = dom.querySelector('a')
|
|
expect(link.getAttribute('href')).toBe('/post/e1/party')
|
|
expect(link.textContent).toBe('party')
|
|
})
|
|
|
|
it('generates correct link for theUser type', () => {
|
|
const userFeatures = [
|
|
{
|
|
geometry: { coordinates: [13.38, 52.52] },
|
|
properties: {
|
|
type: 'theUser',
|
|
slug: 'peter',
|
|
id: 'u1',
|
|
name: 'Peter',
|
|
locationName: 'Berlin',
|
|
description: '',
|
|
},
|
|
},
|
|
]
|
|
mapQueryRenderedFeaturesMock.mockReturnValueOnce(userFeatures)
|
|
onEventMocks.mouseenter({
|
|
point: { x: 100, y: 200 },
|
|
lngLat: { lng: 13.38, lat: 52.52 },
|
|
})
|
|
const dom = getPopupDOMForType()
|
|
const link = dom.querySelector('a')
|
|
expect(link.getAttribute('href')).toBe('/profile/u1/peter')
|
|
expect(link.textContent).toBe('@peter')
|
|
})
|
|
|
|
it('omits location header when locationName is empty', () => {
|
|
const features = [
|
|
{
|
|
geometry: { coordinates: [10.0, 53.55] },
|
|
properties: {
|
|
type: 'user',
|
|
slug: 'bob',
|
|
id: 'u2',
|
|
name: 'Bob',
|
|
locationName: '',
|
|
description: '',
|
|
},
|
|
},
|
|
]
|
|
mapQueryRenderedFeaturesMock.mockReturnValueOnce(features)
|
|
onEventMocks.mouseenter({
|
|
point: { x: 100, y: 200 },
|
|
lngLat: { lng: 10.0, lat: 53.55 },
|
|
})
|
|
const dom = getPopupDOMForType()
|
|
expect(dom.querySelector('.map-popup-header')).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('loadMarkersIconsAndAddMarkers', () => {
|
|
it('loads all 4 marker icon images', () => {
|
|
expect(mapLoadImageMock).toHaveBeenCalledTimes(4)
|
|
expect(mapLoadImageMock).toHaveBeenCalledWith(
|
|
'img/mapbox/marker-icons/mapbox-marker-icon-20px-orange.png',
|
|
expect.any(Function),
|
|
)
|
|
})
|
|
|
|
it('adds images to map', () => {
|
|
expect(mapAddImageMock).toHaveBeenCalledTimes(4)
|
|
expect(mapAddImageMock).toHaveBeenCalledWith('marker-orange', 'image-data')
|
|
expect(mapAddImageMock).toHaveBeenCalledWith('marker-green', 'image-data')
|
|
})
|
|
|
|
it('sets isImagesLoaded to true', () => {
|
|
expect(wrapper.vm.markers.isImagesLoaded).toBe(true)
|
|
})
|
|
|
|
it('calls language to set label layers', () => {
|
|
expect(mapSetLayoutPropertyMock).toHaveBeenCalledWith('some-label', 'text-field', [
|
|
'get',
|
|
'name',
|
|
])
|
|
})
|
|
|
|
it('does not set layout on non-label layers', () => {
|
|
expect(mapSetLayoutPropertyMock).not.toHaveBeenCalledWith(
|
|
'water-fill',
|
|
expect.anything(),
|
|
expect.anything(),
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('setStyle', () => {
|
|
it('sets map style and activeStyle', () => {
|
|
wrapper.vm.setStyle('mapbox://styles/mapbox/dark-v10')
|
|
expect(mapSetStyleMock).toHaveBeenCalledWith('mapbox://styles/mapbox/dark-v10')
|
|
expect(wrapper.vm.activeStyle).toBe('mapbox://styles/mapbox/dark-v10')
|
|
})
|
|
})
|
|
|
|
describe('addMarkersOnCheckPrepared with data', () => {
|
|
beforeEach(async () => {
|
|
await wrapper.setData({
|
|
users: otherUsers,
|
|
groups,
|
|
posts,
|
|
currentUserCoordinates: [13.38333, 52.51667],
|
|
currentUserLocation: userLocation,
|
|
})
|
|
wrapper.vm.addMarkersOnCheckPrepared()
|
|
})
|
|
|
|
it('creates geoJSON features for users', () => {
|
|
const userFeatures = wrapper.vm.markers.geoJSON.filter(
|
|
(f) => f.properties.type === 'user',
|
|
)
|
|
expect(userFeatures.length).toBe(2)
|
|
})
|
|
|
|
it('excludes current user from user features', () => {
|
|
const currentUserFeature = wrapper.vm.markers.geoJSON.find(
|
|
(f) => f.properties.type === 'user' && f.properties.id === 'u1',
|
|
)
|
|
expect(currentUserFeature).toBeUndefined()
|
|
})
|
|
|
|
it('creates geoJSON feature for current user', () => {
|
|
const theUserFeature = wrapper.vm.markers.geoJSON.find(
|
|
(f) => f.properties.type === 'theUser',
|
|
)
|
|
expect(theUserFeature).toBeTruthy()
|
|
expect(theUserFeature.properties.iconRotate).toBe(45.0)
|
|
expect(theUserFeature.properties.locationName).toBe('Berlin')
|
|
})
|
|
|
|
it('creates geoJSON features for groups', () => {
|
|
const groupFeatures = wrapper.vm.markers.geoJSON.filter(
|
|
(f) => f.properties.type === 'group',
|
|
)
|
|
expect(groupFeatures.length).toBe(1)
|
|
expect(groupFeatures[0].properties.locationName).toBe('Hamburg')
|
|
})
|
|
|
|
it('creates geoJSON features for events', () => {
|
|
const eventFeatures = wrapper.vm.markers.geoJSON.filter(
|
|
(f) => f.properties.type === 'event',
|
|
)
|
|
expect(eventFeatures.length).toBe(1)
|
|
expect(eventFeatures[0].properties.locationName).toBe('Stuttgart')
|
|
})
|
|
|
|
it('adds source and layer to map', () => {
|
|
expect(mapAddSourceMock).toHaveBeenCalledWith(
|
|
'markers',
|
|
expect.objectContaining({ type: 'geojson' }),
|
|
)
|
|
expect(mapAddLayerMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({ id: 'markers', type: 'symbol' }),
|
|
)
|
|
})
|
|
|
|
it('calls flyTo', () => {
|
|
expect(mapFlyToMock).toHaveBeenCalledWith({
|
|
center: [13.38333, 52.51667],
|
|
zoom: 10,
|
|
})
|
|
})
|
|
|
|
it('sets isGeoJSON and isSourceAndLayerAdded to true', () => {
|
|
expect(wrapper.vm.markers.isGeoJSON).toBe(true)
|
|
expect(wrapper.vm.markers.isSourceAndLayerAdded).toBe(true)
|
|
})
|
|
|
|
describe('coordinate nudging for overlapping markers', () => {
|
|
it('nudges markers of different types at same coordinates', () => {
|
|
// User Bob and Group are both at Hamburg (10.0, 53.55)
|
|
const bobFeature = wrapper.vm.markers.geoJSON.find((f) => f.properties.id === 'u2')
|
|
const groupFeature = wrapper.vm.markers.geoJSON.find((f) => f.properties.id === 'g1')
|
|
// They should have different lng coordinates after nudging
|
|
expect(bobFeature.geometry.coordinates[0]).not.toBe(
|
|
groupFeature.geometry.coordinates[0],
|
|
)
|
|
// But same lat
|
|
expect(bobFeature.geometry.coordinates[1]).toBe(groupFeature.geometry.coordinates[1])
|
|
})
|
|
|
|
it('does not nudge markers of the same type at the same location', async () => {
|
|
// Reset geoJSON and flags to re-run with custom data
|
|
wrapper.vm.markers.geoJSON = []
|
|
wrapper.vm.markers.isGeoJSON = false
|
|
wrapper.vm.markers.isSourceAndLayerAdded = true
|
|
wrapper.vm.markers.isFlyToCenter = true
|
|
|
|
const sameLocationUsers = [
|
|
{
|
|
id: 'u10',
|
|
slug: 'alice',
|
|
name: 'Alice',
|
|
about: null,
|
|
location: { id: 'loc2', name: 'Hamburg', lng: 10.0, lat: 53.55 },
|
|
},
|
|
{
|
|
id: 'u11',
|
|
slug: 'charlie',
|
|
name: 'Charlie',
|
|
about: null,
|
|
location: { id: 'loc2', name: 'Hamburg', lng: 10.0, lat: 53.55 },
|
|
},
|
|
]
|
|
await wrapper.setData({
|
|
users: sameLocationUsers,
|
|
groups: [],
|
|
posts: [],
|
|
currentUserCoordinates: null,
|
|
currentUserLocation: null,
|
|
})
|
|
wrapper.vm.addMarkersOnCheckPrepared()
|
|
|
|
const userFeatures = wrapper.vm.markers.geoJSON.filter(
|
|
(f) => f.properties.type === 'user',
|
|
)
|
|
expect(userFeatures.length).toBe(2)
|
|
// Same type at same location — coordinates must remain identical
|
|
expect(userFeatures[0].geometry.coordinates[0]).toBe(
|
|
userFeatures[1].geometry.coordinates[0],
|
|
)
|
|
expect(userFeatures[0].geometry.coordinates[1]).toBe(
|
|
userFeatures[1].geometry.coordinates[1],
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('addMarkersOnCheckPrepared without current user coordinates', () => {
|
|
beforeEach(async () => {
|
|
await wrapper.setData({
|
|
users: otherUsers,
|
|
groups,
|
|
posts,
|
|
currentUserCoordinates: null,
|
|
currentUserLocation: null,
|
|
})
|
|
wrapper.vm.addMarkersOnCheckPrepared()
|
|
})
|
|
|
|
it('does not create theUser feature', () => {
|
|
const theUserFeature = wrapper.vm.markers.geoJSON.find(
|
|
(f) => f.properties.type === 'theUser',
|
|
)
|
|
expect(theUserFeature).toBeUndefined()
|
|
})
|
|
|
|
it('flies to default center', () => {
|
|
expect(mapFlyToMock).toHaveBeenCalledWith({
|
|
center: [10.452764, 51.165707],
|
|
zoom: 4,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('mapFlyToCenter', () => {
|
|
it('calls map.flyTo', () => {
|
|
mapFlyToMock.mockClear()
|
|
wrapper.vm.mapFlyToCenter()
|
|
expect(mapFlyToMock).toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('getUserLocation', () => {
|
|
it('returns location when user has one', async () => {
|
|
mocks.$apollo.query.mockResolvedValueOnce({
|
|
data: { User: [{ location: userLocation }] },
|
|
})
|
|
const result = await wrapper.vm.getUserLocation('u1')
|
|
expect(result).toEqual(userLocation)
|
|
})
|
|
|
|
it('returns null when user has no location', async () => {
|
|
mocks.$apollo.query.mockResolvedValueOnce({
|
|
data: { User: [{ location: null }] },
|
|
})
|
|
const result = await wrapper.vm.getUserLocation('u1')
|
|
expect(result).toBeNull()
|
|
})
|
|
|
|
it('returns null when no user found', async () => {
|
|
mocks.$apollo.query.mockResolvedValueOnce({
|
|
data: { User: [] },
|
|
})
|
|
const result = await wrapper.vm.getUserLocation('u1')
|
|
expect(result).toBeNull()
|
|
})
|
|
|
|
it('shows toast error on failure', async () => {
|
|
mocks.$apollo.query.mockRejectedValueOnce(new Error('Network error'))
|
|
const result = await wrapper.vm.getUserLocation('u1')
|
|
expect(result).toBeNull()
|
|
expect(mocks.$toast.error).toHaveBeenCalledWith('Network error')
|
|
})
|
|
})
|
|
|
|
describe('getCoordinates', () => {
|
|
it('returns [lng, lat] array', () => {
|
|
expect(wrapper.vm.getCoordinates({ lng: 10.0, lat: 53.55 })).toEqual([10.0, 53.55])
|
|
})
|
|
})
|
|
|
|
describe('isPreparedForMarkers watcher', () => {
|
|
it('calls addMarkersOnCheckPrepared when ready', async () => {
|
|
const spy = jest.spyOn(wrapper.vm, 'addMarkersOnCheckPrepared')
|
|
wrapper.vm.onMapLoad({ map: mapMock })
|
|
await wrapper.setData({
|
|
users: otherUsers,
|
|
groups,
|
|
posts,
|
|
})
|
|
await wrapper.vm.$nextTick()
|
|
expect(spy).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('mounted with user location', () => {
|
|
it('sets currentUserCoordinates from location', async () => {
|
|
mocks.$apollo.query.mockResolvedValue({
|
|
data: { User: [{ location: userLocation }] },
|
|
})
|
|
const w = createWrapper()
|
|
await w.vm.$nextTick()
|
|
await w.vm.$nextTick()
|
|
expect(w.vm.currentUserCoordinates).toEqual([13.38333, 52.51667])
|
|
})
|
|
})
|
|
|
|
describe('apollo mapData', () => {
|
|
it('query returns mapQuery', () => {
|
|
const queryFn = wrapper.vm.$options.apollo.mapData.query.bind(wrapper.vm)
|
|
expect(queryFn()).toBeTruthy()
|
|
})
|
|
|
|
it('variables returns correct filter', () => {
|
|
const variablesFn = wrapper.vm.$options.apollo.mapData.variables.bind(wrapper.vm)
|
|
const vars = variablesFn()
|
|
expect(vars.userFilter).toEqual({ hasLocation: true })
|
|
expect(vars.groupHasLocation).toBe(true)
|
|
expect(vars.postFilter.postType_in).toEqual(['Event'])
|
|
expect(vars.postFilter.hasLocation).toBe(true)
|
|
})
|
|
|
|
it('update sets users, groups, posts and calls addMarkersOnCheckPrepared', () => {
|
|
const spy = jest.spyOn(wrapper.vm, 'addMarkersOnCheckPrepared')
|
|
const updateFn = wrapper.vm.$options.apollo.mapData.update.bind(wrapper.vm)
|
|
updateFn({ User: otherUsers, Group: groups, Post: posts })
|
|
expect(wrapper.vm.users).toBe(otherUsers)
|
|
expect(wrapper.vm.groups).toBe(groups)
|
|
expect(wrapper.vm.posts).toBe(posts)
|
|
expect(spy).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('beforeDestroy', () => {
|
|
it('removes resize listener', () => {
|
|
const spy = jest.spyOn(window, 'removeEventListener')
|
|
wrapper.destroy()
|
|
expect(spy).toHaveBeenCalledWith('resize', wrapper.vm.updateMapPosition)
|
|
spy.mockRestore()
|
|
})
|
|
})
|
|
})
|
|
})
|