import {Map, MapOptions} from './map'; import {createMap, setErrorWebGlContext, beforeMapTest} 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 {GeoJSONSourceSpecification, LayerSpecification, SourceSpecification, StyleSpecification} from '@maplibre/maplibre-gl-style-spec'; 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'; import {CameraOptions} from './camera'; import {Terrain} from '../render/terrain'; import {mercatorZfromAltitude} from '../geo/mercator_coordinate'; import {Transform} from '../geo/transform'; import {StyleImageInterface} from '../style/style_image'; import {Style} from '../style/style'; import {MapSourceDataEvent} from './events'; import {config} from '../util/config'; function createStyleSource() { return { type: 'geojson', data: { type: 'FeatureCollection', features: [] } } as SourceSpecification; } let server: FakeServer; beforeEach(() => { beforeMapTest(); 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('#mapOptions', () => { test('maxTileCacheZoomLevels: Default value is set', () => { const map = createMap(); expect(map._maxTileCacheZoomLevels).toBe(config.MAX_TILE_CACHE_ZOOM_LEVELS); }); test('maxTileCacheZoomLevels: Value can be set via map options', () => { const map = createMap({maxTileCacheZoomLevels: 1}); expect(map._maxTileCacheZoomLevels).toBe(1); }); test('Style validation is enabled by default', () => { let validationOption = false; jest.spyOn(Style.prototype, 'loadJSON').mockImplementationOnce((styleJson, options) => { validationOption = options.validate; }); createMap(); expect(validationOption).toBeTruthy(); }); test('Style validation disabled using mapOptions', () => { let validationOption = true; jest.spyOn(Style.prototype, 'loadJSON').mockImplementationOnce((styleJson, options) => { validationOption = options.validate; }); createMap({validateStyle: false}); expect(validationOption).toBeFalsy(); }); test('fadeDuration is set after first idle event', async () => { let idleTriggered = false; const fadeDuration = 100; const spy = jest.spyOn(Style.prototype, 'update').mockImplementation((parameters: EvaluationParameters) => { if (!idleTriggered) { expect(parameters.fadeDuration).toBe(0); } else { expect(parameters.fadeDuration).toBe(fadeDuration); } }); const style = createStyle(); const map = createMap({style, fadeDuration}); await map.once('idle'); idleTriggered = true; map.zoomTo(0.5, {duration: 100}); spy.mockReset(); }); }); 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('setStyle back to the first style should work', done => { const redStyle = {version: 8 as const, sources: {}, layers: [ {id: 'background', type: 'background' as const, paint: {'background-color': 'red'}}, ]}; const blueStyle = {version: 8 as const, sources: {}, layers: [ {id: 'background', type: 'background' as const, paint: {'background-color': 'blue'}}, ]}; const map = createMap({style: redStyle}); map.setStyle(blueStyle); map.once('style.load', () => { map.setStyle(redStyle); const serializedStyle = map.style.serialize(); expect(serializedStyle.layers[0].paint['background-color']).toBe('red'); done(); }); }); 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); }); test('passing null releases the worker', () => { const map = createMap(); const spyWorkerPoolAcquire = jest.spyOn(map.style.dispatcher.workerPool, 'acquire'); const spyWorkerPoolRelease = jest.spyOn(map.style.dispatcher.workerPool, 'release'); map.setStyle({version: 8, sources: {}, layers: []}, {diff: false}); expect(spyWorkerPoolAcquire).toHaveBeenCalledTimes(1); expect(spyWorkerPoolRelease).toHaveBeenCalledTimes(0); spyWorkerPoolAcquire.mockClear(); map.setStyle(null); expect(spyWorkerPoolAcquire).toHaveBeenCalledTimes(0); expect(spyWorkerPoolRelease).toHaveBeenCalledTimes(1); // Cleanup spyWorkerPoolAcquire.mockClear(); spyWorkerPoolRelease.mockClear(); }); test('transformStyle should copy the source and the layer into next style', done => { const style = extend(createStyle(), { sources: { maplibre: { type: 'vector', minzoom: 1, maxzoom: 10, tiles: ['http://example.com/{z}/{x}/{y}.png'] } }, layers: [{ id: 'layerId0', type: 'circle', source: 'maplibre', 'source-layer': 'sourceLayer' }, { id: 'layerId1', type: 'circle', source: 'maplibre', 'source-layer': 'sourceLayer' }] }); const map = createMap({style}); map.setStyle(createStyle(), { diff: false, transformStyle: (prevStyle, nextStyle) => ({ ...nextStyle, sources: { ...nextStyle.sources, maplibre: prevStyle.sources.maplibre }, layers: [ ...nextStyle.layers, prevStyle.layers[0] ] }) }); map.on('style.load', () => { const loadedStyle = map.style.serialize(); expect('maplibre' in loadedStyle.sources).toBeTruthy(); expect(loadedStyle.layers[0].id).toBe(style.layers[0].id); expect(loadedStyle.layers).toHaveLength(1); done(); }); }); test('delayed setStyle with transformStyle should copy the source and the layer into next style with diffing', done => { const style = extend(createStyle(), { sources: { maplibre: { type: 'vector', minzoom: 1, maxzoom: 10, tiles: ['http://example.com/{z}/{x}/{y}.png'] } }, layers: [{ id: 'layerId0', type: 'circle', source: 'maplibre', 'source-layer': 'sourceLayer' }, { id: 'layerId1', type: 'circle', source: 'maplibre', 'source-layer': 'sourceLayer' }] }); const map = createMap({style}); window.setTimeout(() => { map.setStyle(createStyle(), { diff: true, transformStyle: (prevStyle, nextStyle) => ({ ...nextStyle, sources: { ...nextStyle.sources, maplibre: prevStyle.sources.maplibre }, layers: [ ...nextStyle.layers, prevStyle.layers[0] ] }) }); const loadedStyle = map.style.serialize(); expect('maplibre' in loadedStyle.sources).toBeTruthy(); expect(loadedStyle.layers[0].id).toBe(style.layers[0].id); expect(loadedStyle.layers).toHaveLength(1); done(); }, 100); }); test('transformStyle should get called when passed to setStyle after the map is initialised without a style', done => { const map = createMap({deleteStyle: true}); map.setStyle(createStyle(), { diff: true, transformStyle: (prevStyle, nextStyle) => { expect(prevStyle).toBeUndefined(); return { ...nextStyle, sources: { maplibre: { type: 'vector', minzoom: 1, maxzoom: 10, tiles: ['http://example.com/{z}/{x}/{y}.png'] } }, layers: [{ id: 'layerId0', type: 'circle', source: 'maplibre', 'source-layer': 'sourceLayer' }] }; } }); map.on('style.load', () => { const loadedStyle = map.style.serialize(); expect('maplibre' in loadedStyle.sources).toBeTruthy(); expect(loadedStyle.layers[0].id).toBe('layerId0'); done(); }); }); test('map load should be fired when transformStyle is used on setStyle after the map is initialised without a style', done => { const map = createMap({deleteStyle: true}); map.setStyle({version: 8, sources: {}, layers: []}, { diff: true, transformStyle: (prevStyle, nextStyle) => { expect(prevStyle).toBeUndefined(); expect(nextStyle).toBeDefined(); return createStyle(); } }); map.on('load', () => done()); }); test('Override default style validation', () => { let validationOption = true; jest.spyOn(Style.prototype, 'loadJSON').mockImplementationOnce((styleJson, options) => { validationOption = options.validate; }); const map = createMap({style: null}); map.setStyle({version: 8, sources: {}, layers: []}, {validate: false}); expect(validationOption).toBeFalsy(); }); }); 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 === 'idle') { expect(map.isSourceLoaded('geojson')).toBe(true); done(); } }); map.addSource('geojson', createStyleSource()); expect(map.isSourceLoaded('geojson')).toBe(false); }); }); test('Map#isSourceLoaded (equivalent to event.isSourceLoaded)', done => { const style = createStyle(); const map = createMap({style}); map.on('load', () => { map.on('data', (e) => { if (e.dataType === 'source' && 'source' in e) { const sourceDataEvent = e as MapSourceDataEvent; expect(map.isSourceLoaded('geojson')).toBe(sourceDataEvent.isSourceLoaded); if (sourceDataEvent.sourceDataType === 'idle') { 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 undefined if the style has not loaded yet', done => { const style = createStyle(); const map = createMap({style}); expect(map.getStyle()).toBeUndefined(); done(); }); 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('a layer can be added with an embedded source specification', () => { const map = createMap({deleteStyle: true}); const source: GeoJSONSourceSpecification = { type: 'geojson', data: {type: 'Point', coordinates: [0, 0]} }; map.addLayer({ id: 'foo', type: 'symbol', 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', async () => { 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' }] }) }); await map.once('render'); map.moveLayer('layerId1', 'layerId2'); expect(map.getLayer('layerId1').id).toBe('layerId1'); expect(map.getLayer('layerId2').id).toBe('layerId2'); }); test('#getLayer', async () => { 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] }) }); await 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); }); 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', () => { const spy = jest.fn(); global.ResizeObserver = jest.fn().mockImplementation(() => ({ observe: spy })); createMap(); expect(spy).toHaveBeenCalled(); }); test('do not resize if trackResize is false', () => { let observerCallback: Function = null; global.ResizeObserver = jest.fn().mockImplementation((c) => ({ observe: () => { observerCallback = c; } })); const map = createMap({trackResize: false}); const spyA = jest.spyOn(map, 'stop'); const spyB = jest.spyOn(map, '_update'); const spyC = jest.spyOn(map, 'resize'); observerCallback(); expect(spyA).not.toHaveBeenCalled(); expect(spyB).not.toHaveBeenCalled(); expect(spyC).not.toHaveBeenCalled(); }); test('do resize if trackResize is true (default)', async () => { let observerCallback: Function = null; global.ResizeObserver = jest.fn().mockImplementation((c) => ({ observe: () => { observerCallback = c; } })); const map = createMap(); const updateSpy = jest.spyOn(map, '_update'); const resizeSpy = jest.spyOn(map, 'resize'); // The initial "observe" event fired by ResizeObserver should be captured/muted // in the map constructor observerCallback(); expect(updateSpy).not.toHaveBeenCalled(); expect(resizeSpy).not.toHaveBeenCalled(); // The next "observe" event should fire a resize / _update observerCallback(); expect(updateSpy).toHaveBeenCalled(); expect(resizeSpy).toHaveBeenCalledTimes(1); // Additional "observe" events should be throttled observerCallback(); observerCallback(); observerCallback(); observerCallback(); expect(resizeSpy).toHaveBeenCalledTimes(1); await new Promise((resolve) => { setTimeout(resolve, 100); }); expect(resizeSpy).toHaveBeenCalledTimes(2); }); test('width and height correctly rounded', () => { const map = createMap(); const container = map.getContainer(); Object.defineProperty(container, 'clientWidth', {value: 250.6}); Object.defineProperty(container, 'clientHeight', {value: 250.6}); map.resize(); expect(map.getCanvas().width).toBe(250); expect(map.getCanvas().height).toBe(250); expect(map.painter.width).toBe(250); expect(map.painter.height).toBe(250); }); }); 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(); const spyWorkerPoolRelease = jest.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 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', async () => { const map = createMap(); await map.once('idle'); const renderPromise = map.once('render'); map.redraw(); await renderPromise; }); 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', async () => { // background layers do not have a source const map = createMap({ style: { 'version': 8, 'sources': {}, 'layers': [{ 'id': 'background', 'type': 'background', 'layout': { 'visibility': 'none' } }] } }); await map.once('style.load'); const dataPromise = map.once('data'); map.setLayoutProperty('background', 'visibility', 'visible'); const e = await dataPromise; expect(e.dataType).toBe('style'); }); 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 ids', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'geojson': createStyleSource() }, 'layers': [] } }); map.on('load', () => { map.setFeatureState({source: 'geojson', id: 'foo'}, {'hover': true}); const fState = map.getFeatureState({source: 'geojson', id: 'foo'}); expect(fState.hover).toBe(true); done(); }); }); test('parses feature id as an int', 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('throw before loaded', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'geojson': createStyleSource() }, 'layers': [] } }); expect(() => { map.setFeatureState({source: 'geojson', id: 12345}, {'hover': true}); }).toThrow(Error); done(); }); test('fires an error if source not found', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'geojson': createStyleSource() }, 'layers': [] } }); map.on('load', () => { map.on('error', ({error}) => { expect(error.message).toMatch(/source/); done(); }); map.setFeatureState({source: 'vector', id: 12345}, {'hover': true}); }); }); test('fires an error if sourceLayer not provided for a vector source', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'vector': { 'type': 'vector', 'tiles': ['http://example.com/{z}/{x}/{y}.png'] } }, 'layers': [] } }); map.on('load', () => { map.on('error', ({error}) => { expect(error.message).toMatch(/sourceLayer/); done(); }); (map as any).setFeatureState({source: 'vector', sourceLayer: 0, id: 12345}, {'hover': true}); }); }); test('fires an error if id not provided', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'vector': { 'type': 'vector', 'tiles': ['http://example.com/{z}/{x}/{y}.png'] } }, 'layers': [] } }); map.on('load', () => { map.on('error', ({error}) => { expect(error.message).toMatch(/id/); done(); }); (map as any).setFeatureState({source: 'vector', sourceLayer: '1'}, {'hover': true}); }); }); }); describe('#removeFeatureState', () => { test('accepts "0" id', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'geojson': createStyleSource() }, 'layers': [] } }); map.on('load', () => { map.setFeatureState({source: 'geojson', id: 0}, {'hover': true, 'click': true}); map.removeFeatureState({source: 'geojson', id: 0}, 'hover'); const fState = map.getFeatureState({source: 'geojson', id: 0}); expect(fState.hover).toBeUndefined(); expect(fState.click).toBe(true); done(); }); }); test('accepts string id', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'geojson': createStyleSource() }, 'layers': [] } }); map.on('load', () => { map.setFeatureState({source: 'geojson', id: 'foo'}, {'hover': true, 'click': true}); map.removeFeatureState({source: 'geojson', id: 'foo'}, 'hover'); const fState = map.getFeatureState({source: 'geojson', id: 'foo'}); expect(fState.hover).toBeUndefined(); expect(fState.click).toBe(true); done(); }); }); test('remove specific state property', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'geojson': createStyleSource() }, 'layers': [] } }); map.on('load', () => { map.setFeatureState({source: 'geojson', id: 12345}, {'hover': true}); map.removeFeatureState({source: 'geojson', id: 12345}, 'hover'); const fState = map.getFeatureState({source: 'geojson', id: 12345}); expect(fState.hover).toBeUndefined(); done(); }); }); test('remove all state properties of one feature', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'geojson': createStyleSource() }, 'layers': [] } }); map.on('load', () => { map.setFeatureState({source: 'geojson', id: 1}, {'hover': true, 'foo': true}); map.removeFeatureState({source: 'geojson', id: 1}); const fState = map.getFeatureState({source: 'geojson', id: 1}); expect(fState.hover).toBeUndefined(); expect(fState.foo).toBeUndefined(); done(); }); }); test('remove properties for zero-based feature IDs.', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'geojson': createStyleSource() }, 'layers': [] } }); map.on('load', () => { map.setFeatureState({source: 'geojson', id: 0}, {'hover': true, 'foo': true}); map.removeFeatureState({source: 'geojson', id: 0}); const fState = map.getFeatureState({source: 'geojson', id: 0}); expect(fState.hover).toBeUndefined(); expect(fState.foo).toBeUndefined(); done(); }); }); test('other properties persist when removing specific property', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'geojson': createStyleSource() }, 'layers': [] } }); map.on('load', () => { map.setFeatureState({source: 'geojson', id: 1}, {'hover': true, 'foo': true}); map.removeFeatureState({source: 'geojson', id: 1}, 'hover'); const fState = map.getFeatureState({source: 'geojson', id: 1}); expect(fState.foo).toBe(true); done(); }); }); test('remove all state properties of all features in source', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'geojson': createStyleSource() }, 'layers': [] } }); map.on('load', () => { map.setFeatureState({source: 'geojson', id: 1}, {'hover': true, 'foo': true}); map.setFeatureState({source: 'geojson', id: 2}, {'hover': true, 'foo': true}); map.removeFeatureState({source: 'geojson'}); const fState1 = map.getFeatureState({source: 'geojson', id: 1}); expect(fState1.hover).toBeUndefined(); expect(fState1.foo).toBeUndefined(); const fState2 = map.getFeatureState({source: 'geojson', id: 2}); expect(fState2.hover).toBeUndefined(); expect(fState2.foo).toBeUndefined(); done(); }); }); test('specific state deletion should not interfere with broader state deletion', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'geojson': createStyleSource() }, 'layers': [] } }); map.on('load', () => { map.setFeatureState({source: 'geojson', id: 1}, {'hover': true, 'foo': true}); map.setFeatureState({source: 'geojson', id: 2}, {'hover': true, 'foo': true}); map.removeFeatureState({source: 'geojson', id: 1}); map.removeFeatureState({source: 'geojson', id: 1}, 'foo'); const fState1 = map.getFeatureState({source: 'geojson', id: 1}); expect(fState1.hover).toBeUndefined(); map.setFeatureState({source: 'geojson', id: 1}, {'hover': true, 'foo': true}); map.removeFeatureState({source: 'geojson'}); map.removeFeatureState({source: 'geojson', id: 1}, 'foo'); const fState2 = map.getFeatureState({source: 'geojson', id: 2}); expect(fState2.hover).toBeUndefined(); map.setFeatureState({source: 'geojson', id: 2}, {'hover': true, 'foo': true}); map.removeFeatureState({source: 'geojson'}); map.removeFeatureState({source: 'geojson', id: 2}, 'foo'); const fState3 = map.getFeatureState({source: 'geojson', id: 2}); expect(fState3.hover).toBeUndefined(); done(); }); }); test('add/remove and remove/add state', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'geojson': createStyleSource() }, 'layers': [] } }); map.on('load', () => { map.setFeatureState({source: 'geojson', id: 12345}, {'hover': true}); map.removeFeatureState({source: 'geojson', id: 12345}); map.setFeatureState({source: 'geojson', id: 12345}, {'hover': true}); const fState1 = map.getFeatureState({source: 'geojson', id: 12345}); expect(fState1.hover).toBe(true); map.removeFeatureState({source: 'geojson', id: 12345}); const fState2 = map.getFeatureState({source: 'geojson', id: 12345}); expect(fState2.hover).toBeUndefined(); done(); }); }); test('throw before loaded', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'geojson': createStyleSource() }, 'layers': [] } }); expect(() => { (map as any).removeFeatureState({source: 'geojson', id: 12345}, {'hover': true}); }).toThrow(Error); done(); }); test('fires an error if source not found', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'geojson': createStyleSource() }, 'layers': [] } }); map.on('load', () => { map.on('error', ({error}) => { expect(error.message).toMatch(/source/); done(); }); (map as any).removeFeatureState({source: 'vector', id: 12345}, {'hover': true}); }); }); test('fires an error if sourceLayer not provided for a vector source', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'vector': { 'type': 'vector', 'tiles': ['http://example.com/{z}/{x}/{y}.png'] } }, 'layers': [] } }); map.on('load', () => { map.on('error', ({error}) => { expect(error.message).toMatch(/sourceLayer/); done(); }); (map as any).removeFeatureState({source: 'vector', sourceLayer: 0, id: 12345}, {'hover': true}); }); }); test('fires an error if state property is provided without a feature id', done => { const map = createMap({ style: { 'version': 8, 'sources': { 'vector': { 'type': 'vector', 'tiles': ['http://example.com/{z}/{x}/{y}.png'] } }, 'layers': [] } }); map.on('load', () => { map.on('error', ({error}) => { expect(error.message).toMatch(/id/); done(); }); (map as any).removeFeatureState({source: 'vector', sourceLayer: '1'}, {'hover': true}); }); }); }); describe('error event', () => { test('logs errors to console when it has NO listeners', () => { const map = createMap(); const stub = jest.spyOn(console, 'error').mockImplementation(() => {}); stub.mockReset(); 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', done => { const map = createMap(); const error = new Error('test'); map.on('error', (event) => { expect(event.error).toBe(error); done(); }); map.fire(new ErrorEvent(error)); }); }); test('render stabilizes', done => { const style = createStyle(); style.sources.mapbox = { type: 'vector', minzoom: 1, maxzoom: 10, tiles: ['http://example.com/{z}/{x}/{y}.png'] }; style.layers.push({ id: 'layerId', type: 'circle', source: 'mapbox', 'source-layer': 'sourceLayer' }); let timer; const map = createMap({style}); map.on('render', () => { if (timer) clearTimeout(timer); timer = setTimeout(() => { map.off('render', undefined); map.on('render', () => { done('test failed'); }); expect((map as any)._frameId).toBeFalsy(); done(); }, 100); }); }); test('no render after idle event', done => { const style = createStyle(); const map = createMap({style}); map.on('idle', () => { map.on('render', () => { done('test failed'); }); setTimeout(() => { done(); }, 100); }); }); test('no render before style loaded', done => { server.respondWith('/styleUrl', JSON.stringify(createStyle())); const map = createMap({style: '/styleUrl'}); jest.spyOn(map, 'triggerRepaint').mockImplementationOnce(() => { if (!map.style._loaded) { done('test failed'); } }); map.on('render', () => { if (map.style._loaded) { done(); } else { done('test failed'); } }); // Force a update should not call triggerRepaint till style is loaded. // Once style is loaded, it will trigger the update. map._update(); server.respond(); }); 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('#removeLayer restores Map#loaded() to true', 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: 'layerId', type: 'circle', source: 'mapbox', 'source-layer': 'sourceLayer' }] }) }); map.once('render', () => { map.removeLayer('layerId'); map.on('render', () => { if (map.loaded()) { map.remove(); done(); } }); }); }); test('stops camera animation on mousedown when interactive', () => { const map = createMap({interactive: true}); map.flyTo({center: [200, 0], duration: 100}); simulate.mousedown(map.getCanvasContainer()); expect(map.isEasing()).toBe(false); map.remove(); }); test('continues camera animation on mousedown when non-interactive', () => { const map = createMap({interactive: false}); map.flyTo({center: [200, 0], duration: 100}); simulate.mousedown(map.getCanvasContainer()); expect(map.isEasing()).toBe(true); map.remove(); }); test('stops camera animation on touchstart when interactive', () => { const map = createMap({interactive: true}); map.flyTo({center: [200, 0], duration: 100}); simulate.touchstart(map.getCanvasContainer(), {touches: [{target: map.getCanvas(), clientX: 0, clientY: 0}]}); expect(map.isEasing()).toBe(false); map.remove(); }); test('continues camera animation on touchstart when non-interactive', () => { const map = createMap({interactive: false}); map.flyTo({center: [200, 0], duration: 100}); simulate.touchstart(map.getCanvasContainer()); expect(map.isEasing()).toBe(true); map.remove(); }); test('continues camera animation on resize', () => { const map = createMap(), container = map.getContainer(); map.flyTo({center: [200, 0], duration: 100}); Object.defineProperty(container, 'clientWidth', {value: 250}); Object.defineProperty(container, 'clientHeight', {value: 250}); map.resize(); expect(map.isMoving()).toBeTruthy(); }); test('map fires `styleimagemissing` for missing icons', done => { const map = createMap(); const id = 'missing-image'; const sampleImage = {width: 2, height: 1, data: new Uint8Array(8)}; let called: string; map.on('styleimagemissing', e => { map.addImage(e.id, sampleImage); called = e.id; }); expect(map.hasImage(id)).toBeFalsy(); map.style.imageManager.getImages([id], (alwaysNull, generatedImage) => { expect(generatedImage[id].data.width).toEqual(sampleImage.width); expect(generatedImage[id].data.height).toEqual(sampleImage.height); expect(generatedImage[id].data.data).toEqual(sampleImage.data); expect(called).toBe(id); expect(map.hasImage(id)).toBeTruthy(); done(); }); }); test('map getImage matches addImage, uintArray', () => { const map = createMap(); const id = 'add-get-uint'; const inputImage = {width: 2, height: 1, data: new Uint8Array(8)}; map.addImage(id, inputImage); expect(map.hasImage(id)).toBeTruthy(); const gotImage = map.getImage(id); expect(gotImage.data.width).toEqual(inputImage.width); expect(gotImage.data.height).toEqual(inputImage.height); expect(gotImage.sdf).toBe(false); }); test('map getImage matches addImage, uintClampedArray', () => { const map = createMap(); const id = 'add-get-uint-clamped'; const inputImage = {width: 1, height: 2, data: new Uint8ClampedArray(8)}; map.addImage(id, inputImage); expect(map.hasImage(id)).toBeTruthy(); const gotImage = map.getImage(id); expect(gotImage.data.width).toEqual(inputImage.width); expect(gotImage.data.height).toEqual(inputImage.height); expect(gotImage.sdf).toBe(false); }); test('map getImage matches addImage, ImageData', () => { const map = createMap(); const id = 'add-get-image-data'; const inputImage = new ImageData(1, 3); map.addImage(id, inputImage); expect(map.hasImage(id)).toBeTruthy(); const gotImage = map.getImage(id); expect(gotImage.data.width).toEqual(inputImage.width); expect(gotImage.data.height).toEqual(inputImage.height); expect(gotImage.sdf).toBe(false); }); test('map getImage matches addImage, StyleImageInterface uint', () => { const map = createMap(); const id = 'add-get-style-image-iface-uint'; const inputImage: StyleImageInterface = { width: 3, height: 1, data: new Uint8Array(12) }; map.addImage(id, inputImage); expect(map.hasImage(id)).toBeTruthy(); const gotImage = map.getImage(id); expect(gotImage.data.width).toEqual(inputImage.width); expect(gotImage.data.height).toEqual(inputImage.height); expect(gotImage.sdf).toBe(false); }); test('map getImage matches addImage, StyleImageInterface clamped', () => { const map = createMap(); const id = 'add-get-style-image-iface-clamped'; const inputImage: StyleImageInterface = { width: 4, height: 1, data: new Uint8ClampedArray(16) }; map.addImage(id, inputImage); expect(map.hasImage(id)).toBeTruthy(); const gotImage = map.getImage(id); expect(gotImage.data.width).toEqual(inputImage.width); expect(gotImage.data.height).toEqual(inputImage.height); expect(gotImage.sdf).toBe(false); }); test('map getImage matches addImage, StyleImageInterface SDF', () => { const map = createMap(); const id = 'add-get-style-image-iface-sdf'; const inputImage: StyleImageInterface = { width: 5, height: 1, data: new Uint8Array(20) }; map.addImage(id, inputImage, {sdf: true}); expect(map.hasImage(id)).toBeTruthy(); const gotImage = map.getImage(id); expect(gotImage.data.width).toEqual(inputImage.width); expect(gotImage.data.height).toEqual(inputImage.height); expect(gotImage.sdf).toBe(true); }); test('map does not fire `styleimagemissing` for empty icon values', done => { const map = createMap(); map.on('load', () => { map.on('idle', () => { done(); }); map.addSource('foo', { type: 'geojson', data: {type: 'Point', coordinates: [0, 0]} }); map.addLayer({ id: 'foo', type: 'symbol', source: 'foo', layout: { 'icon-image': ['case', true, '', ''] } }); map.on('styleimagemissing', ({id}) => { done(`styleimagemissing fired for value ${id}`); }); }); }); describe('setPixelRatio', () => { test('resizes canvas', () => { const container = window.document.createElement('div'); Object.defineProperty(container, 'clientWidth', {value: 512}); Object.defineProperty(container, 'clientHeight', {value: 512}); const map = createMap({container, pixelRatio: 1}); expect(map.getCanvas().width).toBe(512); expect(map.getCanvas().height).toBe(512); map.setPixelRatio(2); expect(map.getCanvas().width).toBe(1024); expect(map.getCanvas().height).toBe(1024); }); test('resizes painter', () => { const container = window.document.createElement('div'); Object.defineProperty(container, 'clientWidth', {value: 512}); Object.defineProperty(container, 'clientHeight', {value: 512}); const map = createMap({container, pixelRatio: 1}); expect(map.painter.pixelRatio).toBe(1); expect(map.painter.width).toBe(512); expect(map.painter.height).toBe(512); map.setPixelRatio(2); expect(map.painter.pixelRatio).toBe(2); expect(map.painter.width).toBe(1024); expect(map.painter.height).toBe(1024); }); }); describe('getPixelRatio', () => { test('returns the pixel ratio', () => { const map = createMap({pixelRatio: 1}); expect(map.getPixelRatio()).toBe(1); map.setPixelRatio(2); expect(map.getPixelRatio()).toBe(2); }); }); test('pixel ratio defaults to devicePixelRatio', () => { const map = createMap(); expect(map.getPixelRatio()).toBe(devicePixelRatio); }); test('pixel ratio by default reflects devicePixelRatio changes', () => { global.devicePixelRatio = 0.25; const map = createMap(); expect(map.getPixelRatio()).toBe(0.25); global.devicePixelRatio = 1; expect(map.getPixelRatio()).toBe(1); }); test('canvas has the expected size', () => { const container = window.document.createElement('div'); Object.defineProperty(container, 'clientWidth', {value: 512}); Object.defineProperty(container, 'clientHeight', {value: 512}); const map = createMap({container, pixelRatio: 2}); expect(map.getCanvas().width).toBe(1024); expect(map.getCanvas().height).toBe(1024); }); test('painter has the expected size and pixel ratio', () => { const container = window.document.createElement('div'); Object.defineProperty(container, 'clientWidth', {value: 512}); Object.defineProperty(container, 'clientHeight', {value: 512}); const map = createMap({container, pixelRatio: 2}); expect(map.painter.pixelRatio).toBe(2); expect(map.painter.width).toBe(1024); expect(map.painter.height).toBe(1024); }); test('fires sourcedataabort event on dataabort event', async () => { const map = createMap(); const sourcePromise = map.once('sourcedataabort'); map.fire(new Event('dataabort')); await sourcePromise; }); describe('#setTerrain', () => { test('warn when terrain and hillshade source identical', done => { server.respondWith('/source.json', JSON.stringify({ minzoom: 5, maxzoom: 12, attribution: 'Terrain', tiles: ['http://example.com/{z}/{x}/{y}.pngraw'], bounds: [-47, -7, -45, -5] })); const map = createMap(); map.on('load', () => { map.addSource('terrainrgb', {type: 'raster-dem', url: '/source.json'}); server.respond(); map.addLayer({id: 'hillshade', type: 'hillshade', source: 'terrainrgb'}); const stub = jest.spyOn(console, 'warn').mockImplementation(() => { }); stub.mockReset(); map.setTerrain({ source: 'terrainrgb' }); expect(console.warn).toHaveBeenCalledTimes(1); done(); }); }); }); describe('#getTerrain', () => { test('returns null when not set', () => { const map = createMap(); expect(map.getTerrain()).toBeNull(); }); }); describe('#setCooperativeGestures', () => { test('returns self', () => { const map = createMap(); expect(map.setCooperativeGestures(true)).toBe(map); }); test('can be called more than once', () => { const map = createMap(); map.setCooperativeGestures(true); map.setCooperativeGestures(true); }); test('calling set with no arguments turns cooperative gestures off', done => { const map = createMap({cooperativeGestures: true}); map.on('load', () => { map.setCooperativeGestures(); expect(map.getCooperativeGestures()).toBeFalsy(); done(); }); }); }); describe('#getCooperativeGestures', () => { test('returns the cooperative gestures option', done => { const map = createMap({cooperativeGestures: true}); map.on('load', () => { expect(map.getCooperativeGestures()).toBe(true); done(); }); }); test('returns falsy if cooperative gestures option is not specified', done => { const map = createMap(); map.on('load', () => { expect(map.getCooperativeGestures()).toBeFalsy(); done(); }); }); test('returns the cooperative gestures option with custom messages', done => { const option = { 'windowsHelpText': 'Custom message', 'macHelpText': 'Custom message', 'mobileHelpText': 'Custom message', }; const map = createMap({cooperativeGestures: option}); map.on('load', () => { expect(map.getCooperativeGestures()).toEqual(option); done(); }); }); }); 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(); }); }); describe('getCameraTargetElevation', () => { test('Elevation is zero without terrain, and matches any given terrain', () => { const map = createMap(); expect(map.getCameraTargetElevation()).toBe(0); const terrainStub = {} as Terrain; map.terrain = terrainStub; const transform = new Transform(0, 22, 0, 60, true); transform.elevation = 200; transform.center = new LngLat(10.0, 50.0); transform.zoom = 14; transform.resize(512, 512); transform.elevation = 2000; map.transform = transform; expect(map.getCameraTargetElevation()).toBe(2000); }); }); describe('#calculateCameraOptionsFromTo', () => { // Choose initial zoom to avoid center being constrained by mercator latitude limits. test('pitch 90 with terrain', () => { const map = createMap(); const mockedGetElevation = jest.fn((_lngLat: LngLat, _zoom: number) => 111200); const terrainStub = {} as Terrain; terrainStub.getElevationForLngLatZoom = mockedGetElevation; map.terrain = terrainStub; // distance between lng x and lng x+1 is 111.2km at same lat // altitude same as center elevation => 90° pitch const cameraOptions: CameraOptions = map.calculateCameraOptionsFromTo(new LngLat(1, 0), 111200, new LngLat(0, 0)); expect(cameraOptions).toBeDefined(); expect(cameraOptions.pitch).toBeCloseTo(90); expect(mockedGetElevation.mock.calls).toHaveLength(1); }); test('pitch 153.435 with terrain', () => { const map = createMap(); const mockedGetElevation = jest.fn((_lngLat: LngLat, _zoom: number) => 111200 * 3); const terrainStub = {} as Terrain; terrainStub.getElevationForLngLatZoom = mockedGetElevation; map.terrain = terrainStub; // distance between lng x and lng x+1 is 111.2km at same lat // (elevation difference of cam and center) / 2 = grounddistance => // acos(111.2 / sqrt(111.2² + (111.2 * 2)²)) = acos(1/sqrt(5)) => 63.435 + 90 = 153.435 const cameraOptions: CameraOptions = map.calculateCameraOptionsFromTo(new LngLat(1, 0), 111200, new LngLat(0, 0)); expect(cameraOptions).toBeDefined(); expect(cameraOptions.pitch).toBeCloseTo(153.435); expect(mockedGetElevation.mock.calls).toHaveLength(1); }); test('pitch 63 with terrain', () => { const map = createMap(); const mockedGetElevation = jest.fn((_lngLat: LngLat, _zoom: number) => 111200 / 2); const terrainStub = {} as Terrain; terrainStub.getElevationForLngLatZoom = mockedGetElevation; map.terrain = terrainStub; // distance between lng x and lng x+1 is 111.2km at same lat // (elevation difference of cam and center) * 2 = grounddistance => // acos(111.2 / sqrt(111.2² + (111.2 * 0.5)²)) = acos(1/sqrt(1.25)) => 90 (looking down) - 26.565 = 63.435 const cameraOptions: CameraOptions = map.calculateCameraOptionsFromTo(new LngLat(0, 0), 111200, new LngLat(1, 0)); expect(cameraOptions).toBeDefined(); expect(cameraOptions.pitch).toBeCloseTo(63.435); expect(mockedGetElevation.mock.calls).toHaveLength(1); }); test('zoom distance 1000', () => { const map = createMap(); const mockedGetElevation = jest.fn((_lngLat: LngLat, _zoom: number) => 1000); const terrainStub = {} as Terrain; terrainStub.getElevationForLngLatZoom = mockedGetElevation; map.terrain = terrainStub; const expectedZoom = Math.log2(map.transform.cameraToCenterDistance / mercatorZfromAltitude(1000, 0) / map.transform.tileSize); const cameraOptions = map.calculateCameraOptionsFromTo(new LngLat(0, 0), 0, new LngLat(0, 0)); expect(cameraOptions).toBeDefined(); expect(cameraOptions.zoom).toBeCloseTo(expectedZoom); expect(mockedGetElevation.mock.calls).toHaveLength(1); }); test('don\'t call getElevation when altitude supplied', () => { const map = createMap(); const mockedGetElevation = jest.fn((_tileID: OverscaledTileID, _x: number, _y: number, _extent?: number) => 0); const terrainStub = {} as Terrain; terrainStub.getElevation = mockedGetElevation; map.terrain = terrainStub; const cameraOptions = map.calculateCameraOptionsFromTo(new LngLat(0, 0), 0, new LngLat(0, 0), 1000); expect(cameraOptions).toBeDefined(); expect(mockedGetElevation.mock.calls).toHaveLength(0); }); test('don\'t call getElevation when altitude 0 supplied', () => { const map = createMap(); const mockedGetElevation = jest.fn((_tileID: OverscaledTileID, _x: number, _y: number, _extent?: number) => 0); const terrainStub = {} as Terrain; terrainStub.getElevation = mockedGetElevation; map.terrain = terrainStub; const cameraOptions = map.calculateCameraOptionsFromTo(new LngLat(0, 0), 0, new LngLat(1, 0), 0); expect(cameraOptions).toBeDefined(); expect(mockedGetElevation.mock.calls).toHaveLength(0); }); }); describe('webgl errors', () => { test('WebGL error while creating map', () => { setErrorWebGlContext(); try { createMap(); } catch (e) { const errorMessageObject = JSON.parse(e.message); // this message is from map code expect(errorMessageObject.message).toBe('Failed to initialize WebGL'); // this is from test mock expect(errorMessageObject.statusMessage).toBe('mocked webglcontextcreationerror message'); } }); test('Hit WebGL max drawing buffer limit', () => { // Simulate a device with MAX_TEXTURE_SIZE=16834 and max rendering area of ~32Mpx const container = window.document.createElement('div'); Object.defineProperty(container, 'clientWidth', {value: 8000}); Object.defineProperty(container, 'clientHeight', {value: 4500}); const map = createMap({container, maxCanvasSize: [16834, 16834], pixelRatio: 1}); jest.spyOn(map.painter.context.gl, 'drawingBufferWidth', 'get').mockReturnValue(7536); jest.spyOn(map.painter.context.gl, 'drawingBufferHeight', 'get').mockReturnValue(4239); map.resize(); expect(map.getCanvas().width).toBe(7536); expect(map.getCanvas().height).toBe(4239); // Check if maxCanvasSize is updated expect(map._maxCanvasSize).toEqual([7536, 4239]); }); }); describe('Max Canvas Size option', () => { test('maxCanvasSize width = height', () => { const container = window.document.createElement('div'); Object.defineProperty(container, 'clientWidth', {value: 2048}); Object.defineProperty(container, 'clientHeight', {value: 2048}); const map = createMap({container, maxCanvasSize: [8192, 8192], pixelRatio: 5}); map.resize(); expect(map.getCanvas().width).toBe(8192); expect(map.getCanvas().height).toBe(8192); }); test('maxCanvasSize width != height', () => { const container = window.document.createElement('div'); Object.defineProperty(container, 'clientWidth', {value: 1024}); Object.defineProperty(container, 'clientHeight', {value: 2048}); const map = createMap({container, maxCanvasSize: [8192, 4096], pixelRatio: 3}); map.resize(); expect(map.getCanvas().width).toBe(2048); expect(map.getCanvas().height).toBe(4096); }); test('maxCanvasSize below clientWidth and clientHeigth', () => { const container = window.document.createElement('div'); Object.defineProperty(container, 'clientWidth', {value: 12834}); Object.defineProperty(container, 'clientHeight', {value: 9000}); const map = createMap({container, maxCanvasSize: [4096, 8192], pixelRatio: 1}); map.resize(); expect(map.getCanvas().width).toBe(4096); expect(map.getCanvas().height).toBe(2872); }); test('maxCanvasSize with setPixelRatio', () => { const container = window.document.createElement('div'); Object.defineProperty(container, 'clientWidth', {value: 2048}); Object.defineProperty(container, 'clientHeight', {value: 2048}); const map = createMap({container, maxCanvasSize: [3072, 3072], pixelRatio: 1.25}); map.resize(); expect(map.getCanvas().width).toBe(2560); expect(map.getCanvas().height).toBe(2560); map.setPixelRatio(2); expect(map.getCanvas().width).toBe(3072); expect(map.getCanvas().height).toBe(3072); }); }); }); function createStyle() { return { version: 8, center: [-73.9749, 40.7736], zoom: 12.5, bearing: 29, pitch: 50, sources: {}, layers: [] } as StyleSpecification; }