UNPKG

maplibre-gl

Version:

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

345 lines (306 loc) 14.3 kB
import {describe, beforeEach, test, expect, vi} from 'vitest'; import {Map, type MapOptions} from '../map'; import {createMap, beforeMapTest, createStyle, createStyleSource, sleep} from '../../util/test/util'; import {Tile} from '../../tile/tile'; import {OverscaledTileID} from '../../tile/tile_id'; import {fixedLngLat} from '../../../test/unit/lib/fixed'; import {type RequestTransformFunction, ResourceType} from '../../util/request_manager'; import {type MapSourceDataEvent} from '../events'; import {MessageType} from '../../util/actor_messages'; import {Style} from '../../style/style'; beforeEach(() => { beforeMapTest(); global.fetch = null; }); describe('Map', () => { test('version', () => { const map = createMap({interactive: true, style: null}); expect(typeof map.version === 'string').toBeTruthy(); // Semver regex: https://gist.github.com/jhorsman/62eeea161a13b80e39f5249281e17c39 // Backslashes are doubled to escape them const regexp = new RegExp('^([0-9]+)\\.([0-9]+)\\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?(?:\\+[0-9A-Za-z-]+)?$'); expect(regexp.test(map.version)).toBeTruthy(); }); test('constructor', () => { const map = createMap({interactive: true, style: null}); expect(map.getContainer()).toBeTruthy(); expect(map.getStyle()).toBeUndefined(); expect(map.boxZoom.isEnabled()).toBeTruthy(); expect(map.doubleClickZoom.isEnabled()).toBeTruthy(); expect(map.dragPan.isEnabled()).toBeTruthy(); expect(map.dragRotate.isEnabled()).toBeTruthy(); expect(map.keyboard.isEnabled()).toBeTruthy(); expect(map.scrollZoom.isEnabled()).toBeTruthy(); expect(map.touchZoomRotate.isEnabled()).toBeTruthy(); expect(() => { new Map({ container: 'anElementIdWhichDoesNotExistInTheDocument' } as any as MapOptions); }).toThrow( new Error('Container \'anElementIdWhichDoesNotExistInTheDocument\' not found.') ); }); test('bad map-specific token breaks map', () => { const container = window.document.createElement('div'); Object.defineProperty(container, 'offsetWidth', {value: 512}); Object.defineProperty(container, 'offsetHeight', {value: 512}); createMap(); //t.error(); }); describe('setTransformRequest', () => { test('returns self', () => { const transformRequest = (() => {}) as any as RequestTransformFunction; const map = new Map({container: window.document.createElement('div')} as any as MapOptions); expect(map.setTransformRequest(transformRequest)).toBe(map); expect(map._requestManager._transformRequestFn).toBe(transformRequest); }); test('can be called more than once', () => { const map = createMap(); const transformRequest = (() => {}) as any as RequestTransformFunction; map.setTransformRequest(transformRequest); map.setTransformRequest(transformRequest); }); test('removes function when called with null', () => { const map = createMap(); const transformRequest = vi.fn(); map.setTransformRequest(transformRequest); map.setTransformRequest(null); map._requestManager.transformRequest('', ResourceType.Unknown); expect(transformRequest).not.toHaveBeenCalled(); }); }); describe('is_Loaded', () => { test('Map.isSourceLoaded', async () => { const style = createStyle(); const map = createMap({style}); await map.once('load'); const promise = new Promise<void>((resolve) => { map.on('data', (e) => { if (e.dataType === 'source' && e.sourceDataType === 'idle') { expect(map.isSourceLoaded('geojson')).toBe(true); resolve(); } }); }); map.addSource('geojson', createStyleSource()); expect(map.isSourceLoaded('geojson')).toBe(false); await promise; }); test('Map.isSourceLoaded (equivalent to event.isSourceLoaded)', async () => { const style = createStyle(); const map = createMap({style}); await map.once('load'); const promise = new Promise<void>((resolve) => { map.on('data', (e: MapSourceDataEvent) => { if (e.dataType === 'source' && 'source' in e) { expect(map.isSourceLoaded('geojson')).toBe(e.isSourceLoaded); if (e.sourceDataType === 'idle') { resolve(); } } }); }); map.addSource('geojson', createStyleSource()); expect(map.isSourceLoaded('geojson')).toBe(false); await promise; }); test('Map.isStyleLoaded', async () => { const style = createStyle(); const map = createMap({style}); expect(map.isStyleLoaded()).toBe(false); await map.once('load'); expect(map.isStyleLoaded()).toBe(true); }); test('Map.areTilesLoaded', async () => { const style = createStyle(); const map = createMap({style}); expect(map.areTilesLoaded()).toBe(true); await map.once('load'); const fakeTileId = new OverscaledTileID(0, 0, 0, 0, 0); map.addSource('geojson', createStyleSource()); map.style.tileManagers.geojson._inViewTiles.setTile(fakeTileId.key, new Tile(fakeTileId, undefined)); expect(map.areTilesLoaded()).toBe(false); map.style.tileManagers.geojson._inViewTiles.getTileById(fakeTileId.key).state = 'loaded'; expect(map.areTilesLoaded()).toBe(true); }); }); test('remove', () => { const map = createMap(); const spyWorkerPoolRelease = vi.spyOn(map.style.dispatcher.workerPool, 'release'); expect(map.getContainer().childNodes).toHaveLength(2); map.remove(); expect(spyWorkerPoolRelease).toHaveBeenCalledTimes(1); expect(map.getContainer().childNodes).toHaveLength(0); // Cleanup spyWorkerPoolRelease.mockClear(); }); test('remove while style is loading via URL does not crash', async () => { global.fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify(createStyle()))); let loadURLPromise: Promise<void>; const originalLoadURL = Style.prototype.loadURL; const loadURLSpy = vi.spyOn(Style.prototype, 'loadURL').mockImplementation(function (...args) { loadURLPromise = originalLoadURL.apply(this, args); return loadURLPromise; }); const map = createMap({style: 'https://example.com/style.json'}); const onErrorFired = vi.fn(); map.on('error', onErrorFired); map.remove(); await loadURLPromise; expect(onErrorFired).not.toHaveBeenCalled(); loadURLSpy.mockRestore(); }); test('remove while setStyle is fetching a new style via URL does not crash', async () => { const style = createStyle(); let resolveFetch: (value: Response) => void; const fetchPromise = new Promise<Response>(resolve => { resolveFetch = resolve; }); global.fetch = vi.fn() .mockResolvedValueOnce(new Response(JSON.stringify(style))) .mockReturnValueOnce(fetchPromise); const map = createMap({style}); await map.once('style.load'); const onError = vi.fn(); map.on('error', onError); map.setStyle('https://example.com/style.json'); map.remove(); resolveFetch(new Response(JSON.stringify(style))); await fetchPromise; expect(onError).not.toHaveBeenCalled(); }); test('second setStyle with URL aborts the first', async () => { const style = createStyle(); const map = createMap({style}); await map.once('style.load'); const abortControllers: AbortController[] = []; const getJSONSpy = vi.spyOn(await import('../../util/ajax'), 'getJSON') .mockImplementation((_req, abortController) => { abortControllers.push(abortController); return Promise.resolve({data: style, cacheControl: null, expires: null}); }); map.setStyle('https://example.com/style1.json'); const firstDiffRequest = map._diffStyleRequest; map.setStyle('https://example.com/style2.json'); expect(firstDiffRequest.signal.aborted).toBe(true); await sleep(0); expect(abortControllers).toHaveLength(1); expect(abortControllers[0].signal.aborted).toBe(false); getJSONSpy.mockRestore(); }); test('setStyle with object aborts a pending diff URL fetch', async () => { const style = createStyle(); let resolveFetch: (value: Response) => void; const fetchPromise = new Promise<Response>(resolve => { resolveFetch = resolve; }); global.fetch = vi.fn() .mockResolvedValueOnce(new Response(JSON.stringify(style))) .mockReturnValueOnce(fetchPromise); const map = createMap({style}); await map.once('style.load'); const onError = vi.fn(); map.on('error', onError); map.setStyle('https://example.com/style.json'); const diffRequest = map._diffStyleRequest; map.setStyle(createStyle()); expect(diffRequest.signal.aborted).toBe(true); resolveFetch(new Response(JSON.stringify(style))); await fetchPromise; expect(onError).not.toHaveBeenCalled(); }); test('setStyle with diff:false aborts a pending diff fetch', async () => { const style = createStyle(); let resolveFetch: (value: Response) => void; const fetchPromise = new Promise<Response>(resolve => { resolveFetch = resolve; }); global.fetch = vi.fn() .mockResolvedValueOnce(new Response(JSON.stringify(style))) .mockReturnValueOnce(fetchPromise); const map = createMap({style}); await map.once('style.load'); const onError = vi.fn(); map.on('error', onError); map.setStyle('https://example.com/style.json'); const diffRequest = map._diffStyleRequest; map.setStyle(createStyle(), {diff: false}); expect(diffRequest.signal.aborted).toBe(true); resolveFetch(new Response(JSON.stringify(style))); await fetchPromise; expect(onError).not.toHaveBeenCalled(); }); test('setStyle with null aborts a pending diff fetch', async () => { const style = createStyle(); let resolveFetch: (value: Response) => void; const fetchPromise = new Promise<Response>(resolve => { resolveFetch = resolve; }); global.fetch = vi.fn() .mockResolvedValueOnce(new Response(JSON.stringify(style))) .mockReturnValueOnce(fetchPromise); const map = createMap({style}); await map.once('style.load'); const onError = vi.fn(); map.on('error', onError); map.setStyle('https://example.com/style.json'); const diffRequest = map._diffStyleRequest; map.setStyle(null); expect(diffRequest.signal.aborted).toBe(true); resolveFetch(new Response(JSON.stringify(style))); await fetchPromise; expect(onError).not.toHaveBeenCalled(); }); test('remove calls onRemove on added controls', () => { const map = createMap(); const control = { onRemove: vi.fn(), onAdd(_) { return window.document.createElement('div'); } }; map.addControl(control); map.remove(); expect(control.onRemove).toHaveBeenCalledTimes(1); }); test('remove calls onRemove on added controls before style is destroyed', async () => { const map = createMap(); let onRemoveCalled = 0; let style = null; const control = { onRemove(map) { onRemoveCalled++; expect(map.getStyle()).toEqual(style); }, onAdd(_) { return window.document.createElement('div'); } }; map.addControl(control); map.once('style.load'); style = map.getStyle(); map.remove(); expect(onRemoveCalled).toBe(1); }); test('remove broadcasts removeMap to worker', () => { const map = createMap(); const _broadcastSpyOn = vi.spyOn(map.style.dispatcher, 'broadcast'); map.remove(); expect(_broadcastSpyOn).toHaveBeenCalledWith(MessageType.removeMap, undefined); }); test('project', () => { const map = createMap(); expect(map.project([0, 0])).toEqual({x: 100, y: 100}); }); test('unproject', () => { const map = createMap(); expect(fixedLngLat(map.unproject([100, 100]))).toEqual({lng: 0, lat: 0}); }); describe('cooperativeGestures option', () => { test('cooperativeGesture container element is hidden from a11y tree', () => { const map = createMap({cooperativeGestures: true}); expect(map.getContainer().querySelector('.maplibregl-cooperative-gesture-screen').getAttribute('aria-hidden')).toBeTruthy(); }); test('cooperativeGesture container element is not available when cooperativeGestures not initialized', () => { const map = createMap({cooperativeGestures: false}); expect(map.getContainer().querySelector('.maplibregl-cooperative-gesture-screen')).toBeFalsy(); }); test('cooperativeGesture container element is not available when cooperativeGestures disabled', () => { const map = createMap({cooperativeGestures: true}); map.cooperativeGestures.disable(); expect(map.getContainer().querySelector('.maplibregl-cooperative-gesture-screen')).toBeFalsy(); }); }); });