UNPKG

maplibre-gl

Version:

BSD licensed community fork of mapbox-gl, a WebGL interactive maps library

1,144 lines (856 loc) 39 kB
import {describe, beforeEach, test, expect, vi} from 'vitest'; import simulate from '../../../test/unit/lib/simulate_interaction'; import {type StyleLayer} from '../../style/style_layer'; import {createMap, beforeMapTest, createStyle, sleep, createTerrain} from '../../util/test/util'; import {type MapGeoJSONFeature} from '../../util/vectortile_to_geojson'; import {type MapLayerEventType, type MapLibreEvent} from '../events'; import {Map, type MapOptions} from '../map'; import {Event as EventedEvent, ErrorEvent} from '../../util/evented'; import {GlobeProjection} from '../../geo/projection/globe_projection'; import {type StyleSpecification} from '@maplibre/maplibre-gl-style-spec'; type IsAny<T> = 0 extends T & 1 ? T : never; type NotAny<T> = T extends IsAny<T> ? never : T; function assertNotAny<T>(_x: NotAny<T>) { } beforeEach(() => { beforeMapTest(); }); describe('map events', () => { test('Map.on adds a non-delegated event listener', () => { const map = createMap(); const spy = vi.fn(function (e) { expect(this).toBe(map); expect(e.type).toBe('click'); }); map.on('click', spy); simulate.click(map.getCanvas()); expect(spy).toHaveBeenCalledTimes(1); }); test('Map.off removes a non-delegated event listener', () => { const map = createMap(); const spy = vi.fn(); map.on('click', spy); map.off('click', spy); simulate.click(map.getCanvas()); expect(spy).not.toHaveBeenCalled(); }); test('Map.on adds a listener for an event on a given layer', () => { const map = createMap(); const features = [{} as MapGeoJSONFeature]; vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockImplementation((_point, options) => { expect(options).toEqual({layers: ['layer']}); return features; }); const spy = vi.fn(function (e) { expect(this).toBe(map); expect(e.type).toBe('click'); expect(e.features).toBe(features); }); map.on('click', 'layer', spy); simulate.click(map.getCanvas()); expect(spy).toHaveBeenCalledTimes(1); }); test('Map.on adds a listener for an event on multiple layers', () => { const map = createMap(); const features = [{} as MapGeoJSONFeature]; vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures') .mockImplementationOnce((_point, options) => { expect(options).toEqual({layers: ['layer1', 'layer2']}); return features; }); const spy = vi.fn((e) => { expect(e.type).toBe('click'); expect(e.features).toBe(features); }); map.on('click', ['layer1', 'layer2'], spy); simulate.click(map.getCanvas()); expect(spy).toHaveBeenCalledTimes(1); }); test('Map.on adds a listener for an event on multiple layers and allows to unsubscribe', () => { const map = createMap(); const features = [{} as MapGeoJSONFeature]; vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures') .mockImplementationOnce((_point, options) => { expect(options).toEqual({layers: ['layer1', 'layer2']}); return features; }); const spy = vi.fn(); const subscription = map.on('click', ['layer1', 'layer2'], spy); subscription.unsubscribe(); simulate.click(map.getCanvas()); expect(spy).not.toHaveBeenCalled(); }); test('Map.on adds listener which calls queryRenderedFeatures only for existing layers', () => { const map = createMap(); const features = [{} as MapGeoJSONFeature]; vi.spyOn(map, 'getLayer').mockImplementation((id: string) => { if (id === 'nonExistingLayer') { return undefined; } return {} as StyleLayer; }); vi.spyOn(map, 'queryRenderedFeatures') .mockImplementationOnce((_point, options) => { expect(options).toEqual({layers: ['layer1', 'layer2']}); return features; }); const spy = vi.fn(function (e) { expect(this).toBe(map); expect(e.type).toBe('click'); expect(e.features).toBe(features); }); map.on('click', ['layer1', 'layer2', 'nonExistingLayer'], spy); simulate.click(map.getCanvas()); expect(spy).toHaveBeenCalledTimes(1); }); test('Map.on adds a listener not triggered for events not matching any features', () => { const map = createMap(); const features = []; vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockImplementation((point, options) => { expect(options).toEqual({layers: ['layer']}); return features; }); const spy = vi.fn(); map.on('click', 'layer', spy); simulate.click(map.getCanvas()); expect(spy).not.toHaveBeenCalled(); }); test('Map.on adds a listener not triggered when the specified layer does not exist', () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockReturnValue(null as unknown as StyleLayer); const spy = vi.fn(); map.on('click', 'layer', spy); simulate.click(map.getCanvas()); expect(spy).not.toHaveBeenCalled(); }); test('Map.on distinguishes distinct event types', () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]); const spyDown = vi.fn((e) => { expect(e.type).toBe('mousedown'); }); const spyUp = vi.fn((e) => { expect(e.type).toBe('mouseup'); }); map.on('mousedown', 'layer', spyDown); map.on('mouseup', 'layer', spyUp); simulate.click(map.getCanvas()); expect(spyDown).toHaveBeenCalledTimes(1); expect(spyUp).toHaveBeenCalledTimes(1); }); test('Map.on distinguishes distinct layers', () => { const map = createMap(); const featuresA = [{} as MapGeoJSONFeature]; const featuresB = [{} as MapGeoJSONFeature]; vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockImplementation((_point, options) => { return (options as any).layers[0] === 'A' ? featuresA : featuresB; }); const spyA = vi.fn((e) => { expect(e.features).toBe(featuresA); }); const spyB = vi.fn((e) => { expect(e.features).toBe(featuresB); }); map.on('click', 'A', spyA); map.on('click', 'B', spyB); simulate.click(map.getCanvas()); expect(spyA).toHaveBeenCalledTimes(1); expect(spyB).toHaveBeenCalledTimes(1); }); test('Map.on distinguishes distinct listeners', () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]); const spyA = vi.fn(); const spyB = vi.fn(); map.on('click', 'layer', spyA); map.on('click', 'layer', spyB); simulate.click(map.getCanvas()); expect(spyA).toHaveBeenCalledTimes(1); expect(spyB).toHaveBeenCalledTimes(1); }); test('Map.on calls an event listener with no type arguments, defaulting to \'unknown\' originalEvent type', () => { const map = createMap(); const handler = { onMove: function onMove(_event: MapLibreEvent) {} }; vi.spyOn(handler, 'onMove'); map.on('move', (event) => handler.onMove(event)); map.jumpTo({center: {lng: 10, lat: 10}}); expect(handler.onMove).toHaveBeenCalledTimes(1); }); test('Map.on allows a listener to infer the event type ', () => { const map = createMap(); const spy = vi.fn(); map.on('mousemove', (event) => { assertNotAny(event); const {lng, lat} = event.lngLat; spy({lng, lat}); }); simulate.mousemove(map.getCanvas()); expect(spy).toHaveBeenCalledTimes(1); }); test('Map.off removes a delegated event listener', () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]); const spy = vi.fn(); map.on('click', 'layer', spy); map.off('click', 'layer', spy); simulate.click(map.getCanvas()); expect(spy).not.toHaveBeenCalled(); }); test('Map.off removes a delegated event listener for multiple layers', () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]); const spy = vi.fn(); map.on('click', ['layer1', 'layer2'], spy); map.off('click', ['layer1', 'layer2'], spy); simulate.click(map.getCanvas()); expect(spy).not.toHaveBeenCalled(); }); test('Map.off distinguishes distinct event types', () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]); const spy = vi.fn((e) => { expect(e.type).toBe('mousedown'); }); map.on('mousedown', 'layer', spy); map.on('mouseup', 'layer', spy); map.off('mouseup', 'layer', spy); simulate.click(map.getCanvas()); expect(spy).toHaveBeenCalledTimes(1); }); test('Map.off distinguishes distinct layers', () => { const map = createMap(); const featuresA = [{} as MapGeoJSONFeature]; vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockImplementation((point, options) => { expect(options).toEqual({layers: ['A']}); return featuresA; }); const spy = vi.fn((e) => { expect(e.features).toBe(featuresA); }); map.on('click', 'A', spy); map.on('click', 'B', spy); map.off('click', 'B', spy); simulate.click(map.getCanvas()); expect(spy).toHaveBeenCalledTimes(1); }); test('Map.off distinguishes distinct layer arrays', () => { const map = createMap(); const featuresAB = [{} as MapGeoJSONFeature]; vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockImplementation((point, options) => { expect(options).toEqual({layers: ['A', 'B']}); return featuresAB; }); const spy = vi.fn((e) => { expect(e.features).toBe(featuresAB); }); map.on('click', ['A', 'B'], spy); map.on('click', ['A', 'C'], spy); map.off('click', ['A', 'C'], spy); simulate.click(map.getCanvas()); expect(spy).toHaveBeenCalledTimes(1); }); test('Map.off compares full layer array list, including layers missing in style', () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockImplementation((id: string) => { if (id === 'nonExistingLayer') { return undefined; } return {} as StyleLayer; }); vi.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]); const spy = vi.fn(); map.on('click', ['A', 'C', 'nonExistingLayer'], spy); map.off('click', ['A', 'C'], spy); simulate.click(map.getCanvas()); map.off('click', ['A', 'C', 'nonExistingLayer'], spy); simulate.click(map.getCanvas()); expect(spy).toHaveBeenCalledTimes(1); }); test('Map.off distinguishes distinct listeners', () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]); const spyA = vi.fn(); const spyB = vi.fn(); map.on('click', 'layer', spyA); map.on('click', 'layer', spyB); map.off('click', 'layer', spyB); simulate.click(map.getCanvas()); expect(spyA).toHaveBeenCalledTimes(1); expect(spyB).not.toHaveBeenCalled(); }); test('Map.off calls an event listener with no type arguments, defaulting to \'unknown\' originalEvent type', () => { const map = createMap(); const handler = { onMove: function onMove(_event: MapLibreEvent) {} }; vi.spyOn(handler, 'onMove'); map.off('move', (event) => handler.onMove(event)); map.jumpTo({center: {lng: 10, lat: 10}}); expect(handler.onMove).toHaveBeenCalledTimes(0); }); test('Map.off allows a listener to infer the event type ', () => { const map = createMap(); const spy = vi.fn(); map.off('mousemove', (event) => { assertNotAny(event); const {lng, lat} = event.lngLat; spy({lng, lat}); }); simulate.mousemove(map.getCanvas()); expect(spy).toHaveBeenCalledTimes(0); }); test('Map.once calls an event listener with no type arguments, defaulting to \'unknown\' originalEvent type', () => { const map = createMap(); const handler = { onMoveOnce: function onMoveOnce(_event: MapLibreEvent) {} }; vi.spyOn(handler, 'onMoveOnce'); map.once('move', (event) => handler.onMoveOnce(event)); map.jumpTo({center: {lng: 10, lat: 10}}); expect(handler.onMoveOnce).toHaveBeenCalledTimes(1); }); test('Map.once allows a listener to infer the event type ', () => { const map = createMap(); const spy = vi.fn(); map.once('mousemove', (event) => { assertNotAny(event); const {lng, lat} = event.lngLat; spy({lng, lat}); }); simulate.mousemove(map.getCanvas()); expect(spy).toHaveBeenCalledTimes(1); }); test('Map.off removes listener registered with Map.once', () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]); const spy = vi.fn(); map.once('click', 'layer', spy); map.off('click', 'layer', spy); simulate.click(map.getCanvas()); expect(spy).not.toHaveBeenCalled(); }); (['mouseenter', 'mouseover'] as (keyof MapLayerEventType)[]).forEach((event) => { test(`Map.on ${event} does not fire if the specified layer does not exist`, () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockReturnValue(null as unknown as StyleLayer); const spy = vi.fn(); map.on(event, 'layer', spy); simulate.mousemove(map.getCanvas()); simulate.mousemove(map.getCanvas()); expect(spy).not.toHaveBeenCalled(); }); test(`Map.on ${event} fires when entering the specified layer`, () => { const map = createMap(); const features = [{} as MapGeoJSONFeature]; vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockImplementation((_point, options) => { expect(options).toEqual({layers: ['layer']}); return features; }); const spy = vi.fn(function (e) { expect(this).toBe(map); expect(e.type).toBe(event); expect(e.target).toBe(map); expect(e.features).toBe(features); }); map.on(event, 'layer', spy); simulate.mousemove(map.getCanvas()); expect(spy).toHaveBeenCalledTimes(1); }); test(`Map.on ${event} does not fire on mousemove within the specified layer`, () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]); const spy = vi.fn(); map.on(event, 'layer', spy); simulate.mousemove(map.getCanvas()); simulate.mousemove(map.getCanvas()); expect(spy).toHaveBeenCalledTimes(1); }); test(`Map.on ${event} fires when reentering the specified layer`, () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures') .mockReturnValueOnce([{} as MapGeoJSONFeature]) .mockReturnValueOnce([]) .mockReturnValueOnce([{} as MapGeoJSONFeature]); const spy = vi.fn(); map.on(event, 'layer', spy); simulate.mousemove(map.getCanvas()); simulate.mousemove(map.getCanvas()); simulate.mousemove(map.getCanvas()); expect(spy).toHaveBeenCalledTimes(2); }); test(`Map.on ${event} fires when reentering the specified layer after leaving the canvas`, () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]); const spy = vi.fn(); map.on(event, 'layer', spy); simulate.mousemove(map.getCanvas()); simulate.mouseout(map.getCanvas()); simulate.mousemove(map.getCanvas()); expect(spy).toHaveBeenCalledTimes(2); }); test(`Map.on ${event} distinguishes distinct layers`, () => { const map = createMap(); const featuresA = [{} as MapGeoJSONFeature]; const featuresB = [{} as MapGeoJSONFeature]; vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockImplementation((_point, options) => { return (options as any).layers[0] === 'A' ? featuresA : featuresB; }); const spyA = vi.fn((e) => { expect(e.features).toBe(featuresA); }); const spyB = vi.fn((e) => { expect(e.features).toBe(featuresB); }); map.on(event, 'A', spyA); map.on(event, 'B', spyB); simulate.mousemove(map.getCanvas()); simulate.mousemove(map.getCanvas()); expect(spyA).toHaveBeenCalledTimes(1); expect(spyB).toHaveBeenCalledTimes(1); }); test(`Map.on ${event} distinguishes distinct layers when multiple layers provided`, () => { const map = createMap(); const nonEmptyFeatures = [{} as MapGeoJSONFeature]; const emptyFeatures = []; vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockImplementation((_point, options) => { const layers = (options as any).layers as string[]; if (layers.includes('A')) { return nonEmptyFeatures; } return emptyFeatures; }); const spyA = vi.fn(); const spyAB = vi.fn(); const spyC = vi.fn(); map.on(event, 'A', spyA); map.on(event, ['A', 'B'], spyAB); map.on(event, 'C', spyC); simulate.mousemove(map.getCanvas()); simulate.mousemove(map.getCanvas()); expect(spyA).toHaveBeenCalledTimes(1); expect(spyAB).toHaveBeenCalledTimes(1); expect(spyC).not.toHaveBeenCalled(); }); test(`Map.on ${event} filters non-existing layers`, () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockImplementation((id: string) => id === 'B' ? undefined : {} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockImplementation((_point, options) => { expect((options as any).layers).toStrictEqual(['A', 'C']); return [{} as MapGeoJSONFeature]; }); const spyAC = vi.fn(); map.on(event, ['A', 'B', 'C'], spyAC); simulate.mousemove(map.getCanvas()); expect(map.queryRenderedFeatures).toHaveBeenCalled(); expect(spyAC).toHaveBeenCalledTimes(1); }); test(`Map.on ${event} distinguishes distinct listeners`, () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]); const spyA = vi.fn(); const spyB = vi.fn(); map.on(event, 'layer', spyA); map.on(event, 'layer', spyB); simulate.mousemove(map.getCanvas()); expect(spyA).toHaveBeenCalledTimes(1); expect(spyB).toHaveBeenCalledTimes(1); }); test(`Map.off ${event} removes a delegated event listener`, () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]); const spy = vi.fn(); map.on(event, 'layer', spy); map.off(event, 'layer', spy); simulate.mousemove(map.getCanvas()); expect(spy).not.toHaveBeenCalled(); }); test(`Map.off ${event} distinguishes distinct layers`, () => { const map = createMap(); const featuresA = [{} as MapGeoJSONFeature]; vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockImplementation((_point, options) => { expect(options).toEqual({layers: ['A']}); return featuresA; }); const spy = vi.fn((e) => { expect(e.features).toBe(featuresA); }); map.on(event, 'A', spy); map.on(event, 'B', spy); map.off(event, 'B', spy); simulate.mousemove(map.getCanvas()); expect(spy).toHaveBeenCalledTimes(1); }); test(`Map.off ${event} distinguishes distinct layers when multiple layers provided`, () => { const map = createMap(); const featuresAB = [{} as MapGeoJSONFeature]; vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockImplementation((_point, options) => { expect(options).toEqual({layers: ['A', 'B']}); return featuresAB; }); const spy = vi.fn((e) => { expect(e.features).toBe(featuresAB); }); map.on(event, ['A', 'B'], spy); map.on(event, ['B', 'C'], spy); map.off(event, ['B', 'C'], spy); simulate.mousemove(map.getCanvas()); expect(spy).toHaveBeenCalledTimes(1); expect(map.queryRenderedFeatures).toHaveBeenCalledTimes(1); }); test(`Map.off ${event} distinguishes distinct listeners`, () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]); const spyA = vi.fn(); const spyB = vi.fn(); map.on(event, 'layer', spyA); map.on(event, 'layer', spyB); map.off(event, 'layer', spyB); simulate.mousemove(map.getCanvas()); expect(spyA).toHaveBeenCalledTimes(1); expect(spyB).not.toHaveBeenCalled(); }); }); (['mouseleave', 'mouseout'] as (keyof MapLayerEventType)[]).forEach((event) => { test(`Map.on ${event} does not fire if the specified layer does not exist`, () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockReturnValue(undefined); vi.spyOn(map, 'queryRenderedFeatures'); const spy = vi.fn(); map.on(event, 'layer', spy); simulate.mousemove(map.getCanvas()); simulate.mousemove(map.getCanvas()); expect(spy).not.toHaveBeenCalled(); expect(map.queryRenderedFeatures).not.toHaveBeenCalled(); }); test(`Map.on ${event} fires if one of specified layers exists`, () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockImplementation((id: string) => id === 'A' ? {} as StyleLayer : undefined); vi.spyOn(map, 'queryRenderedFeatures') .mockReturnValueOnce([{} as MapGeoJSONFeature]) .mockReturnValueOnce([]); const spy = vi.fn(); map.on(event, ['A', 'B'], spy); simulate.mousemove(map.getCanvas()); simulate.mousemove(map.getCanvas()); expect(spy).toHaveBeenCalledTimes(1); }); test(`Map.on ${event} does not fire on mousemove when entering or within the specified layer`, () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]); const spy = vi.fn(); map.on(event, 'layer', spy); simulate.mousemove(map.getCanvas()); simulate.mousemove(map.getCanvas()); expect(spy).not.toHaveBeenCalled(); }); test(`Map.on ${event} fires when exiting the specified layer`, () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures') .mockReturnValueOnce([{} as MapGeoJSONFeature]) .mockReturnValueOnce([]); const spy = vi.fn(function (e) { expect(this).toBe(map); expect(e.type).toBe(event); expect(e.features).toBeUndefined(); }); map.on(event, 'layer', spy); simulate.mousemove(map.getCanvas()); simulate.mousemove(map.getCanvas()); expect(spy).toHaveBeenCalledTimes(1); }); test(`Map.on ${event} fires when exiting the canvas`, () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]); const spy = vi.fn(function (e) { expect(this).toBe(map); expect(e.type).toBe(event); expect(e.features).toBeUndefined(); }); map.on(event, 'layer', spy); simulate.mousemove(map.getCanvas()); simulate.mouseout(map.getCanvas()); expect(spy).toHaveBeenCalledTimes(1); }); test(`Map.off ${event} removes a delegated event listener`, () => { const map = createMap(); vi.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer); vi.spyOn(map, 'queryRenderedFeatures') .mockReturnValueOnce([{} as MapGeoJSONFeature]) .mockReturnValueOnce([]); const spy = vi.fn(); map.on(event, 'layer', spy); map.off(event, 'layer', spy); simulate.mousemove(map.getCanvas()); simulate.mousemove(map.getCanvas()); simulate.mouseout(map.getCanvas()); expect(spy).not.toHaveBeenCalled(); }); }); test('Map.on mousedown can have default behavior prevented and still fire subsequent click event', () => { const map = createMap(); map.on('mousedown', e => e.preventDefault()); const click = vi.fn(); map.on('click', click); simulate.click(map.getCanvas()); expect(click).toHaveBeenCalled(); map.remove(); }); test('Map.on mousedown doesn\'t fire subsequent click event if mousepos changes', () => { const map = createMap(); map.on('mousedown', e => e.preventDefault()); const click = vi.fn(); map.on('click', click); const canvas = map.getCanvas(); simulate.drag(canvas, {}, {clientX: 100, clientY: 100}); expect(click).not.toHaveBeenCalled(); map.remove(); }); test('Map.on mousedown fires subsequent click event if mouse position changes less than click tolerance', () => { const map = createMap({clickTolerance: 4}); map.on('mousedown', e => e.preventDefault()); const click = vi.fn(); map.on('click', click); const canvas = map.getCanvas(); simulate.drag(canvas, {clientX: 100, clientY: 100}, {clientX: 100, clientY: 103}); expect(click).toHaveBeenCalled(); map.remove(); }); test('Map.on mousedown does not fire subsequent click event if mouse position changes more than click tolerance', () => { const map = createMap({clickTolerance: 4}); map.on('mousedown', e => e.preventDefault()); const click = vi.fn(); map.on('click', click); const canvas = map.getCanvas(); simulate.drag(canvas, {clientX: 100, clientY: 100}, {clientX: 100, clientY: 104}); expect(click).not.toHaveBeenCalled(); map.remove(); }); test('Map.on click fires subsequent click event if there is no corresponding mousedown/mouseup event', () => { const map = createMap({clickTolerance: 4}); const click = vi.fn(); map.on('click', click); const canvas = map.getCanvas(); const event = new MouseEvent('click', {bubbles: true, clientX: 100, clientY: 100}); canvas.dispatchEvent(event); expect(click).toHaveBeenCalled(); map.remove(); }); test('Map.isMoving() returns false in mousedown/mouseup/click with no movement', () => { const map = createMap({interactive: true, clickTolerance: 4}); let mousedown, mouseup, click; map.on('mousedown', () => { mousedown = map.isMoving(); }); map.on('mouseup', () => { mouseup = map.isMoving(); }); map.on('click', () => { click = map.isMoving(); }); const canvas = map.getCanvas(); canvas.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, clientX: 100, clientY: 100})); expect(mousedown).toBe(false); map._renderTaskQueue.run(); expect(mousedown).toBe(false); canvas.dispatchEvent(new MouseEvent('mouseup', {bubbles: true, clientX: 100, clientY: 100})); expect(mouseup).toBe(false); map._renderTaskQueue.run(); expect(mouseup).toBe(false); canvas.dispatchEvent(new MouseEvent('click', {bubbles: true, clientX: 100, clientY: 100})); expect(click).toBe(false); map._renderTaskQueue.run(); expect(click).toBe(false); map.remove(); }); test('emits load event after a style is set', async () => { const map = new Map({container: window.document.createElement('div')} as any as MapOptions); const failSpy = vi.fn(); map.on('load', failSpy); await sleep(1); map.off('load', failSpy); const promise = map.once('load'); map.setStyle(createStyle()); await promise; expect(failSpy).not.toHaveBeenCalled(); }); test('errors inside load event are not suppressed', async () => { const map = new Map({container: window.document.createElement('div')} as any as MapOptions); const loadHandler = vi.fn(() => { throw new Error('Error in load handler'); }); map.on('load', loadHandler); await sleep(1); expect(loadHandler).toThrowError(); }); test('no idle event during move', async () => { const style = createStyle(); const map = createMap({style, fadeDuration: 0}); await map.once('idle'); map.zoomTo(0.5, {duration: 100}); expect(map.isMoving()).toBeTruthy(); await map.once('idle'); expect(map.isMoving()).toBeFalsy(); }); test('fires sourcedataabort event on dataabort event', async () => { const map = createMap(); const sourcePromise = map.once('sourcedataabort'); map.fire(new EventedEvent('dataabort')); await sourcePromise; }); test('getZoom on moveend is the same as after the map end moving, with terrain on', async () => { const map = createMap({interactive: true, clickTolerance: 4}); await map.once('style.load'); map.terrain = createTerrain(); let actualZoom: number; map.on('moveend', () => { // this can't use a promise due to race condition actualZoom = map.getZoom(); }); const canvas = map.getCanvas(); simulate.dragWithMove(canvas, {x: 100, y: 100}, {x: 100, y: 150}); map._renderTaskQueue.run(); expect(actualZoom).toBe(map.getZoom()); }); test('drag from center', () => { const map = createMap({interactive: true, clickTolerance: 4}); map.on('moveend', () => { expect(map.getCenter().lng).toBeCloseTo(0, 10); expect(map.getCenter().lat).toBeCloseTo(33.13755119234696, 10); expect(map.getCenterElevation()).toBeCloseTo(0, 10); }); const canvas = map.getCanvas(); simulate.dragWithMove(canvas, {x: 100, y: 100}, {x: 100, y: 150}); map._renderTaskQueue.run(); }); test('drag from off center', () => { const map = createMap({interactive: true, clickTolerance: 4}); map.on('moveend', () => { expect(map.getCenter().lng).toBeCloseTo(0, 10); expect(map.getCenter().lat).toBeCloseTo(33.13755119234696, 10); expect(map.getCenterElevation()).toBeCloseTo(0, 10); }); const canvas = map.getCanvas(); simulate.dragWithMove(canvas, {x: 50, y: 50}, {x: 50, y: 100}); map._renderTaskQueue.run(); }); describe('error event', () => { test('logs errors to console when it has NO listeners', () => { // to avoid seeing error in the console in Jest let stub = vi.spyOn(console, 'error').mockImplementation(() => {}); const map = createMap(); stub.mockReset(); stub = vi.spyOn(console, 'error').mockImplementation(() => {}); const error = new Error('test'); map.fire(new ErrorEvent(error)); expect(stub).toHaveBeenCalledTimes(1); expect(stub.mock.calls[0][0]).toBe(error); }); test('calls listeners', async () => { const map = createMap(); const error = new Error('test'); const promise = map.once('error'); map.fire(new ErrorEvent(error)); const event = await promise; expect(event.error).toBe(error); }); test('does not call listeners after unsubscribe', async () => { const map = createMap(); const error = new Error('test'); const spy = vi.fn(); const subscription = map.on('error', spy); subscription.unsubscribe(); map.fire(new ErrorEvent(error)); expect(spy).not.toHaveBeenCalled(); }); test('throws error when request fails', async () => { const style: StyleSpecification = { ...createStyle(), sources: { 'source': { type: 'vector', url: 'maplibre://nonexistent' } }, layers: [ { id: 'layer', source: 'source', type: 'fill', 'source-layer': 'test' } ] }; const map = createMap(); map.setStyle(style); const errorHandler = vi.fn(); map.on('error', errorHandler); map.triggerRepaint(); await sleep(100); expect(errorHandler).toHaveBeenCalledTimes(1); }); }); describe('projectiontransition event', () => { test('projectiontransition events is fired when setProjection is called', async () => { const map = createMap(); await map.once('load'); const spy = vi.fn(); map.on('projectiontransition', (e) => spy(e.newProjection)); map.setProjection({ type: 'globe', }); map.setProjection({ type: 'mercator', }); expect(spy).toHaveBeenCalledTimes(2); expect(spy).toHaveBeenNthCalledWith(1, 'globe'); expect(spy).toHaveBeenNthCalledWith(2, 'mercator'); }); test('projectiontransition is fired when globe transitions to mercator', async () => { const map = createMap(); vi.spyOn(GlobeProjection.prototype, 'updateGPUdependent').mockImplementation(() => {}); await map.once('load'); const spy = vi.fn(); map.on('projectiontransition', (e) => spy(e.newProjection)); map.setProjection({ type: 'globe', }); map.setZoom(18); map.redraw(); await sleep(550); map.redraw(); map.setZoom(0); map.redraw(); await sleep(550); map.redraw(); map.setProjection({ type: 'mercator', }); expect(spy).toHaveBeenCalledTimes(2); expect(spy).toHaveBeenNthCalledWith(1, 'globe'); expect(spy).toHaveBeenNthCalledWith(2, 'mercator'); }); }); });