maplibre-gl
Version:
BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
1,393 lines (1,186 loc) • 106 kB
text/typescript
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.mockRestore();
});
});
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('#getLayersOrder', () => {
test('returns ids of layers in the correct order', done => {
const map = createMap({
style: extend(createStyle(), {
'sources': {
'raster': {
type: 'raster',
tiles: ['http://tiles.server']
}
},
'layers': [{
'id': 'raster',
'type': 'raster',
'source': 'raster'
}]
})
});
map.on('style.load', () => {
map.addLayer({
id: 'custom',
type: 'custom',
render() {}
}, 'raster');
expect(map.getLayersOrder()).toEqual(['custom', 'raster']);
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', () => {
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.lis