UNPKG

maplibre-gl

Version:

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

1,456 lines (1,230 loc) 74.6 kB
import Map, {MapOptions} from './map'; import {createMap, setMatchMedia, setPerformance, setWebGlContext} from '../util/test/util'; import LngLat from '../geo/lng_lat'; import Tile from '../source/tile'; import {OverscaledTileID} from '../source/tile_id'; import {Event, ErrorEvent} from '../util/evented'; import simulate from '../../test/unit/lib/simulate_interaction'; import {fixedLngLat, fixedNum} from '../../test/unit/lib/fixed'; import {LayerSpecification, SourceSpecification, StyleSpecification} from '../style-spec/types.g'; import {RequestTransformFunction} from '../util/request_manager'; import {extend} from '../util/util'; import {LngLatBoundsLike} from '../geo/lng_lat_bounds'; import {IControl} from './control/control'; import EvaluationParameters from '../style/evaluation_parameters'; import {fakeServer, FakeServer} from 'nise'; function createStyleSource() { return { type: 'geojson', data: { type: 'FeatureCollection', features: [] } } as SourceSpecification; } let server: FakeServer; beforeEach(() => { setPerformance(); setWebGlContext(); setMatchMedia(); global.fetch = null; server = fakeServer.create(); }); afterEach(() => { server.restore(); }); 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(); }); test('initial bounds in constructor options', () => { const container = window.document.createElement('div'); Object.defineProperty(container, 'offsetWidth', {value: 512}); Object.defineProperty(container, 'offsetHeight', {value: 512}); const bounds = [[-133, 16], [-68, 50]]; const map = createMap({container, bounds}); expect(fixedLngLat(map.getCenter(), 4)).toEqual({lng: -100.5, lat: 34.7171}); expect(fixedNum(map.getZoom(), 3)).toBe(2.113); }); test('initial bounds options in constructor options', () => { const bounds = [[-133, 16], [-68, 50]]; const map = (fitBoundsOptions) => { const container = window.document.createElement('div'); Object.defineProperty(container, 'offsetWidth', {value: 512}); Object.defineProperty(container, 'offsetHeight', {value: 512}); return createMap({container, bounds, fitBoundsOptions}); }; const unpadded = map(undefined); const padded = map({padding: 100}); expect(unpadded.getZoom() > padded.getZoom()).toBeTruthy(); }); describe('disables handlers', () => { test('disables all handlers', () => { const map = createMap({interactive: false}); expect(map.boxZoom.isEnabled()).toBeFalsy(); expect(map.doubleClickZoom.isEnabled()).toBeFalsy(); expect(map.dragPan.isEnabled()).toBeFalsy(); expect(map.dragRotate.isEnabled()).toBeFalsy(); expect(map.keyboard.isEnabled()).toBeFalsy(); expect(map.scrollZoom.isEnabled()).toBeFalsy(); expect(map.touchZoomRotate.isEnabled()).toBeFalsy(); }); const handlerNames = [ 'scrollZoom', 'boxZoom', 'dragRotate', 'dragPan', 'keyboard', 'doubleClickZoom', 'touchZoomRotate' ]; handlerNames.forEach((handlerName) => { test(`disables "${handlerName}" handler`, () => { const options = {}; options[handlerName] = false; const map = createMap(options); expect(map[handlerName].isEnabled()).toBeFalsy(); }); }); }); test('emits load event after a style is set', done => { const map = new Map({container: window.document.createElement('div')} as any as MapOptions); const fail = () => done('test failed'); const pass = () => done(); map.on('load', fail); setTimeout(() => { map.off('load', fail); map.on('load', pass); map.setStyle(createStyle()); }, 1); }); describe('#setStyle', () => { test('returns self', () => { const map = new Map({container: window.document.createElement('div')} as any as MapOptions); expect(map.setStyle({ version: 8, sources: {}, layers: [] })).toBe(map); }); test('sets up event forwarding', () => { createMap({}, (error, map) => { expect(error).toBeFalsy(); const events = []; function recordEvent(event) { events.push(event.type); } map.on('error', recordEvent); map.on('data', recordEvent); map.on('dataloading', recordEvent); map.style.fire(new Event('error')); map.style.fire(new Event('data')); map.style.fire(new Event('dataloading')); expect(events).toEqual([ 'error', 'data', 'dataloading', ]); }); }); test('fires *data and *dataloading events', () => { createMap({}, (error, map) => { expect(error).toBeFalsy(); const events = []; function recordEvent(event) { events.push(event.type); } map.on('styledata', recordEvent); map.on('styledataloading', recordEvent); map.on('sourcedata', recordEvent); map.on('sourcedataloading', recordEvent); map.on('tiledata', recordEvent); map.on('tiledataloading', recordEvent); map.style.fire(new Event('data', {dataType: 'style'})); map.style.fire(new Event('dataloading', {dataType: 'style'})); map.style.fire(new Event('data', {dataType: 'source'})); map.style.fire(new Event('dataloading', {dataType: 'source'})); map.style.fire(new Event('data', {dataType: 'tile'})); map.style.fire(new Event('dataloading', {dataType: 'tile'})); expect(events).toEqual([ 'styledata', 'styledataloading', 'sourcedata', 'sourcedataloading', 'tiledata', 'tiledataloading' ]); }); }); test('can be called more than once', () => { const map = createMap(); map.setStyle({version: 8, sources: {}, layers: []}, {diff: false}); map.setStyle({version: 8, sources: {}, layers: []}, {diff: false}); }); test('style transform overrides unmodified map transform', done => { const map = new Map({container: window.document.createElement('div')} as any as MapOptions); map.transform.lngRange = [-120, 140]; map.transform.latRange = [-60, 80]; map.transform.resize(600, 400); expect(map.transform.zoom).toBe(0.6983039737971014); expect(map.transform.unmodified).toBeTruthy(); map.setStyle(createStyle()); map.on('style.load', () => { expect(fixedLngLat(map.transform.center)).toEqual(fixedLngLat({lng: -73.9749, lat: 40.7736})); expect(fixedNum(map.transform.zoom)).toBe(12.5); expect(fixedNum(map.transform.bearing)).toBe(29); expect(fixedNum(map.transform.pitch)).toBe(50); done(); }); }); test('style transform does not override map transform modified via options', done => { const map = new Map({container: window.document.createElement('div'), zoom: 10, center: [-77.0186, 38.8888]} as any as MapOptions); expect(map.transform.unmodified).toBeFalsy(); map.setStyle(createStyle()); map.on('style.load', () => { expect(fixedLngLat(map.transform.center)).toEqual(fixedLngLat({lng: -77.0186, lat: 38.8888})); expect(fixedNum(map.transform.zoom)).toBe(10); expect(fixedNum(map.transform.bearing)).toBe(0); expect(fixedNum(map.transform.pitch)).toBe(0); done(); }); }); test('style transform does not override map transform modified via setters', done => { const map = new Map({container: window.document.createElement('div')} as any as MapOptions); expect(map.transform.unmodified).toBeTruthy(); map.setZoom(10); map.setCenter([-77.0186, 38.8888]); expect(map.transform.unmodified).toBeFalsy(); map.setStyle(createStyle()); map.on('style.load', () => { expect(fixedLngLat(map.transform.center)).toEqual(fixedLngLat({lng: -77.0186, lat: 38.8888})); expect(fixedNum(map.transform.zoom)).toBe(10); expect(fixedNum(map.transform.bearing)).toBe(0); expect(fixedNum(map.transform.pitch)).toBe(0); done(); }); }); test('passing null removes style', () => { const map = createMap(); const style = map.style; expect(style).toBeTruthy(); jest.spyOn(style, '_remove'); map.setStyle(null); expect(style._remove).toHaveBeenCalledTimes(1); }); }); 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); }); }); describe('#is_Loaded', () => { test('Map#isSourceLoaded', done => { const style = createStyle(); const map = createMap({style}); map.on('load', () => { map.on('data', (e) => { if (e.dataType === 'source' && e.sourceDataType === 'metadata') { expect(map.isSourceLoaded('geojson')).toBe(true); done(); } }); map.addSource('geojson', createStyleSource()); expect(map.isSourceLoaded('geojson')).toBe(false); }); }); test('Map#isStyleLoaded', done => { const style = createStyle(); const map = createMap({style}); expect(map.isStyleLoaded()).toBe(false); map.on('load', () => { expect(map.isStyleLoaded()).toBe(true); done(); }); }); test('Map#areTilesLoaded', done => { const style = createStyle(); const map = createMap({style}); expect(map.areTilesLoaded()).toBe(true); map.on('load', () => { const fakeTileId = new OverscaledTileID(0, 0, 0, 0, 0); map.addSource('geojson', createStyleSource()); map.style.sourceCaches.geojson._tiles[fakeTileId.key] = new Tile(fakeTileId, undefined); expect(map.areTilesLoaded()).toBe(false); map.style.sourceCaches.geojson._tiles[fakeTileId.key].state = 'loaded'; expect(map.areTilesLoaded()).toBe(true); done(); }); }); }); describe('#getStyle', () => { test('returns the style', done => { const style = createStyle(); const map = createMap({style}); map.on('load', () => { expect(map.getStyle()).toEqual(style); done(); }); }); test('returns the style with added sources', done => { const style = createStyle(); const map = createMap({style}); map.on('load', () => { map.addSource('geojson', createStyleSource()); expect(map.getStyle()).toEqual(extend(createStyle(), { sources: {geojson: createStyleSource()} })); done(); }); }); test('fires an error on checking if non-existant source is loaded', done => { const style = createStyle(); const map = createMap({style}); map.on('load', () => { map.on('error', ({error}) => { expect(error.message).toMatch(/There is no source with ID/); done(); }); map.isSourceLoaded('geojson'); }); }); test('returns the style with added layers', done => { const style = createStyle(); const map = createMap({style}); const layer = { id: 'background', type: 'background' } as LayerSpecification; map.on('load', () => { map.addLayer(layer); expect(map.getStyle()).toEqual(extend(createStyle(), { layers: [layer] })); done(); }); }); test('a layer can be added even if a map is created without a style', () => { const map = createMap({deleteStyle: true}); const layer = { id: 'background', type: 'background' } as LayerSpecification; map.addLayer(layer); }); test('a source can be added even if a map is created without a style', () => { const map = createMap({deleteStyle: true}); const source = createStyleSource(); map.addSource('fill', source); }); test('returns the style with added source and layer', done => { const style = createStyle(); const map = createMap({style}); const source = createStyleSource(); const layer = { id: 'fill', type: 'fill', source: 'fill' } as LayerSpecification; map.on('load', () => { map.addSource('fill', source); map.addLayer(layer); expect(map.getStyle()).toEqual(extend(createStyle(), { sources: {fill: source}, layers: [layer] })); done(); }); }); test('creates a new Style if diff fails', () => { const style = createStyle(); const map = createMap({style}); jest.spyOn(map.style, 'setState').mockImplementation(() => { throw new Error('Dummy error'); }); jest.spyOn(console, 'warn').mockImplementation(() => {}); const previousStyle = map.style; map.setStyle(style); expect(map.style && map.style !== previousStyle).toBeTruthy(); }); test('creates a new Style if diff option is false', () => { const style = createStyle(); const map = createMap({style}); const spy = jest.spyOn(map.style, 'setState'); const previousStyle = map.style; map.setStyle(style, {diff: false}); expect(map.style && map.style !== previousStyle).toBeTruthy(); expect(spy).not.toHaveBeenCalled(); }); }); test('#moveLayer', done => { const map = createMap({ style: extend(createStyle(), { sources: { mapbox: { type: 'vector', minzoom: 1, maxzoom: 10, tiles: ['http://example.com/{z}/{x}/{y}.png'] } }, layers: [{ id: 'layerId1', type: 'circle', source: 'mapbox', 'source-layer': 'sourceLayer' }, { id: 'layerId2', type: 'circle', source: 'mapbox', 'source-layer': 'sourceLayer' }] }) }); map.once('render', () => { map.moveLayer('layerId1', 'layerId2'); expect(map.getLayer('layerId1').id).toBe('layerId1'); expect(map.getLayer('layerId2').id).toBe('layerId2'); done(); }); }); test('#getLayer', done => { const layer = { id: 'layerId', type: 'circle', source: 'mapbox', 'source-layer': 'sourceLayer' }; const map = createMap({ style: extend(createStyle(), { sources: { mapbox: { type: 'vector', minzoom: 1, maxzoom: 10, tiles: ['http://example.com/{z}/{x}/{y}.png'] } }, layers: [layer] }) }); map.once('render', () => { const mapLayer = map.getLayer('layerId'); expect(mapLayer.id).toBe(layer.id); expect(mapLayer.type).toBe(layer.type); expect(mapLayer.source).toBe(layer.source); done(); }); }); describe('#resize', () => { test('sets width and height from container clients', () => { const map = createMap(), container = map.getContainer(); Object.defineProperty(container, 'clientWidth', {value: 250}); Object.defineProperty(container, 'clientHeight', {value: 250}); map.resize(); expect(map.transform.width).toBe(250); expect(map.transform.height).toBe(250); }); test('fires movestart, move, resize, and moveend events', () => { const map = createMap(), events = []; (['movestart', 'move', 'resize', 'moveend'] as any).forEach((event) => { map.on(event, (e) => { events.push(e.type); }); }); map.resize(); expect(events).toEqual(['movestart', 'move', 'resize', 'moveend']); }); test('listen to window resize event', done => { const original = global.addEventListener; global.addEventListener = function(type) { if (type === 'resize') { //restore original function not to mess with other tests global.addEventListener = original; done(); } }; createMap(); }); test('do not resize if trackResize is false', () => { const map = createMap({trackResize: false}); const spyA = jest.spyOn(map, 'stop'); const spyB = jest.spyOn(map, '_update'); const spyC = jest.spyOn(map, 'resize'); map._onWindowResize(undefined); expect(spyA).not.toHaveBeenCalled(); expect(spyB).not.toHaveBeenCalled(); expect(spyC).not.toHaveBeenCalled(); }); test('do resize if trackResize is true (default)', () => { const map = createMap(); const spyA = jest.spyOn(map, '_update'); const spyB = jest.spyOn(map, 'resize'); map._onWindowResize(undefined); expect(spyA).toHaveBeenCalled(); expect(spyB).toHaveBeenCalled(); }); }); describe('#getBounds', () => { test('getBounds', () => { const map = createMap({zoom: 0}); expect(parseFloat(map.getBounds().getCenter().lng.toFixed(10))).toBe(-0); expect(parseFloat(map.getBounds().getCenter().lat.toFixed(10))).toBe(0); expect(toFixed(map.getBounds().toArray())).toEqual(toFixed([ [-70.31249999999976, -57.326521225216965], [70.31249999999977, 57.32652122521695]])); }); test('rotated bounds', () => { const map = createMap({zoom: 1, bearing: 45}); expect( toFixed([[-49.718445552178764, -44.44541580601936], [49.7184455522, 44.445415806019355]]) ).toEqual(toFixed(map.getBounds().toArray())); map.setBearing(135); expect( toFixed([[-49.718445552178764, -44.44541580601936], [49.7184455522, 44.445415806019355]]) ).toEqual(toFixed(map.getBounds().toArray())); }); function toFixed(bounds) { const n = 10; return [ [normalizeFixed(bounds[0][0], n), normalizeFixed(bounds[0][1], n)], [normalizeFixed(bounds[1][0], n), normalizeFixed(bounds[1][1], n)] ]; } function normalizeFixed(num, n) { // workaround for "-0.0000000000" ≠ "0.0000000000" return parseFloat(num.toFixed(n)).toFixed(n); } }); describe('#setMaxBounds', () => { test('constrains map bounds', () => { const map = createMap({zoom: 0}); map.setMaxBounds([[-130.4297, 50.0642], [-61.52344, 24.20688]]); expect( toFixed([[-130.4297000000, 7.0136641176], [-61.5234400000, 60.2398142283]]) ).toEqual(toFixed(map.getBounds().toArray())); }); test('when no argument is passed, map bounds constraints are removed', () => { const map = createMap({zoom: 0}); map.setMaxBounds([[-130.4297, 50.0642], [-61.52344, 24.20688]]); expect( toFixed([[-166.28906999999964, -27.6835270554], [-25.664070000000066, 73.8248206697]]) ).toEqual(toFixed(map.setMaxBounds(null).setZoom(0).getBounds().toArray())); }); test('should not zoom out farther than bounds', () => { const map = createMap(); map.setMaxBounds([[-130.4297, 50.0642], [-61.52344, 24.20688]]); expect(map.setZoom(0).getZoom()).not.toBe(0); }); function toFixed(bounds) { const n = 9; return [ [bounds[0][0].toFixed(n), bounds[0][1].toFixed(n)], [bounds[1][0].toFixed(n), bounds[1][1].toFixed(n)] ]; } }); describe('#getMaxBounds', () => { test('returns null when no bounds set', () => { const map = createMap({zoom: 0}); expect(map.getMaxBounds()).toBeNull(); }); test('returns bounds', () => { const map = createMap({zoom: 0}); const bounds = [[-130.4297, 50.0642], [-61.52344, 24.20688]] as LngLatBoundsLike; map.setMaxBounds(bounds); expect(map.getMaxBounds().toArray()).toEqual(bounds); }); }); describe('#getRenderWorldCopies', () => { test('initially false', () => { const map = createMap({renderWorldCopies: false}); expect(map.getRenderWorldCopies()).toBe(false); }); test('initially true', () => { const map = createMap({renderWorldCopies: true}); expect(map.getRenderWorldCopies()).toBe(true); }); }); describe('#setRenderWorldCopies', () => { test('initially false', () => { const map = createMap({renderWorldCopies: false}); map.setRenderWorldCopies(true); expect(map.getRenderWorldCopies()).toBe(true); }); test('initially true', () => { const map = createMap({renderWorldCopies: true}); map.setRenderWorldCopies(false); expect(map.getRenderWorldCopies()).toBe(false); }); test('undefined', () => { const map = createMap({renderWorldCopies: false}); map.setRenderWorldCopies(undefined); expect(map.getRenderWorldCopies()).toBe(true); }); test('null', () => { const map = createMap({renderWorldCopies: true}); map.setRenderWorldCopies(null); expect(map.getRenderWorldCopies()).toBe(false); }); }); test('#setMinZoom', () => { const map = createMap({zoom: 5}); map.setMinZoom(3.5); map.setZoom(1); expect(map.getZoom()).toBe(3.5); }); test('unset minZoom', () => { const map = createMap({minZoom: 5}); map.setMinZoom(null); map.setZoom(1); expect(map.getZoom()).toBe(1); }); test('#getMinZoom', () => { const map = createMap({zoom: 0}); expect(map.getMinZoom()).toBe(-2); map.setMinZoom(10); expect(map.getMinZoom()).toBe(10); }); test('ignore minZooms over maxZoom', () => { const map = createMap({zoom: 2, maxZoom: 5}); expect(() => { map.setMinZoom(6); }).toThrow(); map.setZoom(0); expect(map.getZoom()).toBe(0); }); test('#setMaxZoom', () => { const map = createMap({zoom: 0}); map.setMaxZoom(3.5); map.setZoom(4); expect(map.getZoom()).toBe(3.5); }); test('unset maxZoom', () => { const map = createMap({maxZoom: 5}); map.setMaxZoom(null); map.setZoom(6); expect(map.getZoom()).toBe(6); }); test('#getMaxZoom', () => { const map = createMap({zoom: 0}); expect(map.getMaxZoom()).toBe(22); map.setMaxZoom(10); expect(map.getMaxZoom()).toBe(10); }); test('ignore maxZooms over minZoom', () => { const map = createMap({minZoom: 5}); expect(() => { map.setMaxZoom(4); }).toThrow(); map.setZoom(5); expect(map.getZoom()).toBe(5); }); test('throw on maxZoom smaller than minZoom at init', () => { expect(() => { createMap({minZoom: 10, maxZoom: 5}); }).toThrow(new Error('maxZoom must be greater than or equal to minZoom')); }); test('throw on maxZoom smaller than minZoom at init with falsey maxZoom', () => { expect(() => { createMap({minZoom: 1, maxZoom: 0}); }).toThrow(new Error('maxZoom must be greater than or equal to minZoom')); }); test('#setMinPitch', () => { const map = createMap({pitch: 20}); map.setMinPitch(10); map.setPitch(0); expect(map.getPitch()).toBe(10); }); test('unset minPitch', () => { const map = createMap({minPitch: 20}); map.setMinPitch(null); map.setPitch(0); expect(map.getPitch()).toBe(0); }); test('#getMinPitch', () => { const map = createMap({pitch: 0}); expect(map.getMinPitch()).toBe(0); map.setMinPitch(10); expect(map.getMinPitch()).toBe(10); }); test('ignore minPitchs over maxPitch', () => { const map = createMap({pitch: 0, maxPitch: 10}); expect(() => { map.setMinPitch(20); }).toThrow(); map.setPitch(0); expect(map.getPitch()).toBe(0); }); test('#setMaxPitch', () => { const map = createMap({pitch: 0}); map.setMaxPitch(10); map.setPitch(20); expect(map.getPitch()).toBe(10); }); test('unset maxPitch', () => { const map = createMap({maxPitch: 10}); map.setMaxPitch(null); map.setPitch(20); expect(map.getPitch()).toBe(20); }); test('#getMaxPitch', () => { const map = createMap({pitch: 0}); expect(map.getMaxPitch()).toBe(60); map.setMaxPitch(10); expect(map.getMaxPitch()).toBe(10); }); test('ignore maxPitchs over minPitch', () => { const map = createMap({minPitch: 10}); expect(() => { map.setMaxPitch(0); }).toThrow(); map.setPitch(10); expect(map.getPitch()).toBe(10); }); test('throw on maxPitch smaller than minPitch at init', () => { expect(() => { createMap({minPitch: 10, maxPitch: 5}); }).toThrow(new Error('maxPitch must be greater than or equal to minPitch')); }); test('throw on maxPitch smaller than minPitch at init with falsey maxPitch', () => { expect(() => { createMap({minPitch: 1, maxPitch: 0}); }).toThrow(new Error('maxPitch must be greater than or equal to minPitch')); }); test('throw on maxPitch greater than valid maxPitch at init', () => { expect(() => { createMap({maxPitch: 90}); }).toThrow(new Error('maxPitch must be less than or equal to 85')); }); test('throw on minPitch less than valid minPitch at init', () => { expect(() => { createMap({minPitch: -10}); }).toThrow(new Error('minPitch must be greater than or equal to 0')); }); test('#remove', () => { const map = createMap(); expect(map.getContainer().childNodes).toHaveLength(2); map.remove(); expect(map.getContainer().childNodes).toHaveLength(0); }); test('#remove calls onRemove on added controls', () => { const map = createMap(); const control = { onRemove: jest.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', done => { const map = createMap(); let onRemoveCalled = 0; let style; const control = { onRemove(map) { onRemoveCalled++; expect(map.getStyle()).toEqual(style); }, onAdd (_) { return window.document.createElement('div'); } }; map.addControl(control); map.on('style.load', () => { style = map.getStyle(); map.remove(); expect(onRemoveCalled).toBe(1); done(); }); }); test('does not fire "webglcontextlost" after #remove has been called', done => { const map = createMap(); const canvas = map.getCanvas(); map.once('webglcontextlost', () => done('"webglcontextlost" fired after #remove has been called')); map.remove(); // Dispatch the event manually because at the time of this writing, gl does not support // the WEBGL_lose_context extension. canvas.dispatchEvent(new window.Event('webglcontextlost')); done(); }); test('does not fire "webglcontextrestored" after #remove has been called', done => { const map = createMap(); const canvas = map.getCanvas(); map.once('webglcontextlost', () => { map.once('webglcontextrestored', () => done('"webglcontextrestored" fired after #remove has been called')); map.remove(); canvas.dispatchEvent(new window.Event('webglcontextrestored')); done(); }); // Dispatch the event manually because at the time of this writing, gl does not support // the WEBGL_lose_context extension. canvas.dispatchEvent(new window.Event('webglcontextlost')); }); test('#redraw', done => { const map = createMap(); map.once('idle', () => { map.once('render', () => done()); map.redraw(); }); }); test('#addControl', () => { const map = createMap(); const control = { onAdd(_) { expect(map).toBe(_); return window.document.createElement('div'); } } as any as IControl; map.addControl(control); expect(map._controls[0]).toBe(control); }); test('#removeControl errors on invalid arguments', () => { const map = createMap(); const control = {} as any as IControl; const stub = jest.spyOn(console, 'error').mockImplementation(() => {}); map.addControl(control); map.removeControl(control); expect(stub).toHaveBeenCalledTimes(2); }); test('#removeControl', () => { const map = createMap(); const control = { onAdd() { return window.document.createElement('div'); }, onRemove(_) { expect(map).toBe(_); } }; map.addControl(control); map.removeControl(control); expect(map._controls).toHaveLength(0); }); test('#hasControl', () => { const map = createMap(); function Ctrl() {} Ctrl.prototype = { onAdd(_) { return window.document.createElement('div'); } }; const control = new Ctrl(); expect(map.hasControl(control)).toBe(false); map.addControl(control); expect(map.hasControl(control)).toBe(true); }); 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}); }); test('#listImages', done => { const map = createMap(); map.on('load', () => { expect(map.listImages()).toHaveLength(0); map.addImage('img', {width: 1, height: 1, data: new Uint8Array(4)}); const images = map.listImages(); expect(images).toHaveLength(1); expect(images[0]).toBe('img'); done(); }); }); test('#listImages throws an error if called before "load"', () => { const map = createMap(); expect(() => { map.listImages(); }).toThrow(Error); }); describe('#queryRenderedFeatures', () => { test('if no arguments provided', done => { createMap({}, (err, map) => { expect(err).toBeFalsy(); const spy = jest.spyOn(map.style, 'queryRenderedFeatures'); const output = map.queryRenderedFeatures(); const args = spy.mock.calls[0]; expect(args[0]).toBeTruthy(); expect(args[1]).toEqual({availableImages: []}); expect(output).toEqual([]); done(); }); }); test('if only "geometry" provided', done => { createMap({}, (err, map) => { expect(err).toBeFalsy(); const spy = jest.spyOn(map.style, 'queryRenderedFeatures'); const output = map.queryRenderedFeatures(map.project(new LngLat(0, 0))); const args = spy.mock.calls[0]; expect(args[0]).toEqual([{x: 100, y: 100}]); // query geometry expect(args[1]).toEqual({availableImages: []}); // params expect(args[2]).toEqual(map.transform); // transform expect(output).toEqual([]); done(); }); }); test('if only "params" provided', done => { createMap({}, (err, map) => { expect(err).toBeFalsy(); const spy = jest.spyOn(map.style, 'queryRenderedFeatures'); const output = map.queryRenderedFeatures({filter: ['all']}); const args = spy.mock.calls[0]; expect(args[0]).toBeTruthy(); expect(args[1]).toEqual({availableImages: [], filter: ['all']}); expect(output).toEqual([]); done(); }); }); test('if both "geometry" and "params" provided', done => { createMap({}, (err, map) => { expect(err).toBeFalsy(); const spy = jest.spyOn(map.style, 'queryRenderedFeatures'); const output = map.queryRenderedFeatures({filter: ['all']}); const args = spy.mock.calls[0]; expect(args[0]).toBeTruthy(); expect(args[1]).toEqual({availableImages: [], filter: ['all']}); expect(output).toEqual([]); done(); }); }); test('if "geometry" with unwrapped coords provided', done => { createMap({}, (err, map) => { expect(err).toBeFalsy(); const spy = jest.spyOn(map.style, 'queryRenderedFeatures'); map.queryRenderedFeatures(map.project(new LngLat(360, 0))); expect(spy.mock.calls[0][0]).toEqual([{x: 612, y: 100}]); done(); }); }); test('returns an empty array when no style is loaded', () => { const map = createMap({style: undefined}); expect(map.queryRenderedFeatures()).toEqual([]); }); }); describe('#setLayoutProperty', () => { test('sets property', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'geojson': { 'type': 'geojson', 'data': { 'type': 'FeatureCollection', 'features': [] } } }, 'layers': [{ 'id': 'symbol', 'type': 'symbol', 'source': 'geojson', 'layout': { 'text-transform': 'uppercase' } }] } }); map.on('style.load', () => { map.style.dispatcher.broadcast = function(key, value: any) { expect(key).toBe('updateLayers'); expect(value.layers.map((layer) => { return layer.id; })).toEqual(['symbol']); }; map.setLayoutProperty('symbol', 'text-transform', 'lowercase'); map.style.update({} as EvaluationParameters); expect(map.getLayoutProperty('symbol', 'text-transform')).toBe('lowercase'); done(); }); }); test('throw before loaded', () => { const map = createMap({ style: { version: 8, sources: {}, layers: [] } }); expect(() => { map.setLayoutProperty('symbol', 'text-transform', 'lowercase'); }).toThrow(Error); }); test('fires an error if layer not found', done => { const map = createMap({ style: { version: 8, sources: {}, layers: [] } }); map.on('style.load', () => { map.on('error', ({error}) => { expect(error.message).toMatch(/Cannot style non-existing layer "non-existant"./); done(); }); map.setLayoutProperty('non-existant', 'text-transform', 'lowercase'); }); }); test('fires a data event', done => { // background layers do not have a source const map = createMap({ style: { 'version': 8, 'sources': {}, 'layers': [{ 'id': 'background', 'type': 'background', 'layout': { 'visibility': 'none' } }] } }); map.once('style.load', () => { map.once('data', (e) => { if (e.dataType === 'style') { done(); } }); map.setLayoutProperty('background', 'visibility', 'visible'); }); }); test('sets visibility on background layer', done => { // background layers do not have a source const map = createMap({ style: { 'version': 8, 'sources': {}, 'layers': [{ 'id': 'background', 'type': 'background', 'layout': { 'visibility': 'none' } }] } }); map.on('style.load', () => { map.setLayoutProperty('background', 'visibility', 'visible'); expect(map.getLayoutProperty('background', 'visibility')).toBe('visible'); done(); }); }); test('sets visibility on raster layer', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'maplibre-satellite': { 'type': 'raster', 'tiles': ['http://example.com/{z}/{x}/{y}.png'] } }, 'layers': [{ 'id': 'satellite', 'type': 'raster', 'source': 'maplibre-satellite', 'layout': { 'visibility': 'none' } }] } }); // Suppress errors because we're not loading tiles from a real URL. map.on('error', () => {}); map.on('style.load', () => { map.setLayoutProperty('satellite', 'visibility', 'visible'); expect(map.getLayoutProperty('satellite', 'visibility')).toBe('visible'); done(); }); }); test('sets visibility on video layer', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'drone': { 'type': 'video', 'urls': [], 'coordinates': [ [-122.51596391201019, 37.56238816766053], [-122.51467645168304, 37.56410183312965], [-122.51309394836426, 37.563391708549425], [-122.51423120498657, 37.56161849366671] ] } }, 'layers': [{ 'id': 'shore', 'type': 'raster', 'source': 'drone', 'layout': { 'visibility': 'none' } }] } }); map.on('style.load', () => { map.setLayoutProperty('shore', 'visibility', 'visible'); expect(map.getLayoutProperty('shore', 'visibility')).toBe('visible'); done(); }); }); test('sets visibility on image layer', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'image': { 'type': 'image', 'url': '', 'coordinates': [ [-122.51596391201019, 37.56238816766053], [-122.51467645168304, 37.56410183312965], [-122.51309394836426, 37.563391708549425], [-122.51423120498657, 37.56161849366671] ] } }, 'layers': [{ 'id': 'image', 'type': 'raster', 'source': 'image', 'layout': { 'visibility': 'none' } }] } }); map.on('style.load', () => { map.setLayoutProperty('image', 'visibility', 'visible'); expect(map.getLayoutProperty('image', 'visibility')).toBe('visible'); done(); }); }); }); describe('#getLayoutProperty', () => { test('fires an error if layer not found', done => { const map = createMap({ style: { version: 8, sources: {}, layers: [] } }); map.on('style.load', () => { map.on('error', ({error}) => { expect(error.message).toMatch(/Cannot get style of non-existing layer "non-existant"./); done(); }); (map as any).getLayoutProperty('non-existant', 'text-transform', 'lowercase'); }); }); }); describe('#setPaintProperty', () => { test('sets property', done => { const map = createMap({ style: { 'version': 8, 'sources': {}, 'layers': [{ 'id': 'background', 'type': 'background' }] } }); map.on('style.load', () => { map.setPaintProperty('background', 'background-color', 'red'); expect(map.getPaintProperty('background', 'background-color')).toBe('red'); done(); }); }); test('throw before loaded', () => { const map = createMap({ style: { version: 8, sources: {}, layers: [] } }); expect(() => { map.setPaintProperty('background', 'background-color', 'red'); }).toThrow(Error); }); test('fires an error if layer not found', done => { const map = createMap({ style: { version: 8, sources: {}, layers: [] } }); map.on('style.load', () => { map.on('error', ({error}) => { expect(error.message).toMatch(/Cannot style non-existing layer "non-existant"./); done(); }); map.setPaintProperty('non-existant', 'background-color', 'red'); }); }); }); describe('#setFeatureState', () => { test('sets state', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'geojson': createStyleSource() }, 'layers': [] } }); map.on('load', () => { map.setFeatureState({source: 'geojson', id: 12345}, {'hover': true}); const fState = map.getFeatureState({source: 'geojson', id: 12345}); expect(fState.hover).toBe(true); done(); }); }); test('works with string