diff --git a/.eslintignore b/.eslintignore
index 1eae0cf6..ec3d0791 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,2 +1,3 @@
dist/
node_modules/
+test/src/utils/mapbox-gl-mock/*.js
diff --git a/aliases.js b/aliases.js
deleted file mode 100644
index 17c6c175..00000000
--- a/aliases.js
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (c) 2015 - 2017 Uber Technologies, Inc.
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-// THE SOFTWARE.
-
-// Registers an alias for this module
-const {resolve} = require('path');
-
-const ALIASES = {
- 'react-map-gl/test': resolve('./test'),
- 'react-map-gl': resolve('./src'),
- 'mapbox-gl$': resolve('./node_modules/mapbox-gl/dist/mapbox-gl.js'),
- webworkify: 'webworkify-webpack-dropin'
-};
-
-if (module.require) {
- const moduleAlias = module.require('module-alias');
- moduleAlias.addAliases(ALIASES);
-}
-
-module.exports = ALIASES;
diff --git a/test/browser.js b/test/browser.js
index 797c439a..c68f5652 100644
--- a/test/browser.js
+++ b/test/browser.js
@@ -1,6 +1,4 @@
/* global window */
-require('@babel/register');
-
const test = require('tape');
const {_enableDOMLogging: enableDOMLogging} = require('@probe.gl/test-utils');
@@ -21,5 +19,4 @@ enableDOMLogging({
})
});
-require('./src');
-// require('./render');
+require('./render');
diff --git a/test/data/style.json b/test/data/style.json
index 34bf331e..b4762b63 100644
--- a/test/data/style.json
+++ b/test/data/style.json
@@ -40,7 +40,7 @@
"prefetchable": true,
"priority": 0,
"tiles": [
- "http://localhost:5000/test/data/tile/v1/{z}/{x}/{y}/COMPOSITE"
+ "https://raw.githubusercontent.com/visgl/react-map-gl/master/test/data/tile/v1/{z}/{x}/{y}/COMPOSITE"
],
"type": "vector"
},
@@ -70,13 +70,13 @@
"minzoom": 10,
"priority": 1,
"tiles": [
- "http://localhost:5000/test/data/tile/v1/{z}/{x}/{y}/POI"
+ "https://raw.githubusercontent.com/visgl/react-map-gl/master/test/data/tile/v1/{z}/{x}/{y}/POI"
],
"type": "vector"
}
},
- "sprite": "http://localhost:5000/test/data/sprite/tools/14/sprites",
- "glyphs": "http://localhost:5000/test/data/glyph/{fontstack}/{range}",
+ "sprite": "https://raw.githubusercontent.com/visgl/react-map-gl/master/test/data/sprite/tools/14/sprites",
+ "glyphs": "https://raw.githubusercontent.com/visgl/react-map-gl/master/test/data/glyph/{fontstack}/{range}",
"layers": [
{
"id": "background",
diff --git a/test/node.js b/test/node.js
index 11d1ecc3..8c5e9522 100644
--- a/test/node.js
+++ b/test/node.js
@@ -1,5 +1,16 @@
const register = require('@babel/register').default;
+const path = require('path');
+const {JSDOM} = require('jsdom');
+
+const moduleAlias = require('module-alias');
+moduleAlias.addAliases({
+ 'mapbox-gl': path.resolve(__dirname, './src/utils/mapbox-gl-mock')
+});
register({extensions: ['.ts', '.tsx', '.js']});
+const dom = new JSDOM(`
`);
+/* global global */
+global.document = dom.window.document;
+
require('./src');
diff --git a/test/render/golden-images/marker.png b/test/render/golden-images/marker.png
new file mode 100644
index 00000000..8c68cf7c
Binary files /dev/null and b/test/render/golden-images/marker.png differ
diff --git a/test/render/index.js b/test/render/index.js
index 1bb090c7..e9e4af90 100644
--- a/test/render/index.js
+++ b/test/render/index.js
@@ -1,7 +1,7 @@
/* global window, document, FontFace */
import test from 'tape-promise/tape';
import * as React from 'react';
-import MapGL from 'react-map-gl';
+import Map from 'react-map-gl';
import {render, unmountComponentAtNode} from 'react-dom';
import TEST_CASES from './test-cases';
@@ -19,7 +19,7 @@ function getBoundingBoxInPage(domElement) {
};
}
-async function runTestCase({Component = MapGL, props}) {
+async function runTestCase({Component = Map, props}) {
const container = document.createElement('div');
container.style.width = `${WIDTH}px`;
container.style.height = `${HEIGHT}px`;
@@ -33,14 +33,19 @@ async function runTestCase({Component = MapGL, props}) {
container.remove();
};
const onLoad = ({target}) => {
- // Wait for mapbox's animation to finish
- target.once('idle', () =>
- resolve({
- map: target,
- boundingBox: getBoundingBoxInPage(container),
- unmount
- })
- );
+ // Wait for animations to finish
+ target.once('idle', () => {
+ /* global setTimeout */
+ setTimeout(
+ () =>
+ resolve({
+ map: target,
+ boundingBox: getBoundingBoxInPage(container),
+ unmount
+ }),
+ 500
+ );
+ });
};
const onError = evt => {
@@ -49,15 +54,17 @@ async function runTestCase({Component = MapGL, props}) {
reject(evt.error);
};
- render(
- ,
- container
- );
+ render( , container);
});
}
// CI does not have the same default fonts as local boxes.
-async function loadFont() {
+async function loadStyles() {
+ const link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = './node_modules/mapbox-gl/dist/mapbox-gl.css';
+ document.head.append(link);
+
const font = new FontFace(
'Roboto',
'url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.woff2)',
@@ -74,7 +81,7 @@ test('Render test', async t => {
// Default tape test timeout is 500ms - allow enough time for render and screenshot
t.timeoutAfter(TEST_CASES.length * 4000);
- await loadFont();
+ await loadStyles();
for (const testCase of TEST_CASES) {
t.comment(testCase.title);
@@ -91,9 +98,9 @@ test('Render test', async t => {
goldenImage,
region: boundingBox,
tolerance: 0.05,
- includeEmpty: false
+ includeEmpty: false,
// Uncomment to save screenshot
- // , saveOnFail: true
+ saveOnFail: true
});
error = result.error;
diff --git a/test/render/test-cases.js b/test/render/test-cases.js
index 6c71f7ef..0f1aeae1 100644
--- a/test/render/test-cases.js
+++ b/test/render/test-cases.js
@@ -1,12 +1,6 @@
/* global __MAPBOX_TOKEN__ */
import * as React from 'react';
-import {StaticMap, NavigationControl, GeolocateControl, Popup, Source, Layer} from 'react-map-gl';
-
-const EMPTY_MAP_STYLE = {
- version: 8,
- sources: {},
- layers: []
-};
+import {NavigationControl, GeolocateControl, Marker, Popup, Source, Layer} from 'react-map-gl';
const ALT_EMPTY_MAP_STYLE = {
version: 8,
@@ -27,7 +21,7 @@ export default [
{
title: 'Basic map',
props: {
- mapboxApiAccessToken: __MAPBOX_TOKEN__,
+ mapboxAccessToken: __MAPBOX_TOKEN__,
mapStyle: 'mapbox://styles/mapbox/dark-v9',
longitude: -122.4,
latitude: 37.78,
@@ -39,7 +33,7 @@ export default [
{
title: 'Invalid map token',
props: {
- mapboxApiAccessToken: 'invalid_token',
+ mapboxAccessToken: 'invalid_token',
mapStyle: 'mapbox://styles/mapbox/dark-v9',
longitude: -122.4,
latitude: 37.78,
@@ -50,8 +44,8 @@ export default [
{
title: 'Custom tile server',
props: {
- mapboxApiAccessToken: __MAPBOX_TOKEN__,
- mapStyle: 'http://localhost:5000/test/data/style.json',
+ mapboxAccessToken: __MAPBOX_TOKEN__,
+ mapStyle: '/test/data/style.json',
longitude: -122.4,
latitude: 37.78,
zoom: 12.5
@@ -61,29 +55,62 @@ export default [
},
{
title: 'NavigationControl',
- Component: StaticMap,
props: {
- mapboxApiAccessToken: __MAPBOX_TOKEN__,
- mapStyle: EMPTY_MAP_STYLE,
+ mapboxAccessToken: __MAPBOX_TOKEN__,
longitude: -122.4,
latitude: 37.78,
zoom: 12.5,
- reuseMaps: true,
bearing: 30,
- children: (
-
-
-
- )
+ children:
},
goldenImage: 'test/render/golden-images/navigation-control.png'
},
{
- title: 'Popup',
- Component: StaticMap,
+ title: 'GeolocateControl',
props: {
- mapboxApiAccessToken: __MAPBOX_TOKEN__,
- mapStyle: EMPTY_MAP_STYLE,
+ mapboxAccessToken: __MAPBOX_TOKEN__,
+ longitude: -122.4,
+ latitude: 37.78,
+ zoom: 12.5,
+ bearing: 30,
+ children: (
+
+ )
+ },
+ goldenImage: 'test/render/golden-images/geolocate-control.png'
+ },
+ {
+ title: 'Marker',
+ props: {
+ mapboxAccessToken: __MAPBOX_TOKEN__,
+ longitude: -122.4,
+ latitude: 37.78,
+ reuseMaps: true,
+ zoom: 12.5,
+ children: [
+ ,
+
+
+
+
+
+ ]
+ },
+ threshold: 0.95,
+ goldenImage: 'test/render/golden-images/marker.png'
+ },
+ {
+ title: 'Popup',
+ props: {
+ mapboxAccessToken: __MAPBOX_TOKEN__,
longitude: -122.4,
latitude: 37.78,
reuseMaps: true,
@@ -109,13 +136,10 @@ export default [
},
{
title: 'JSX Source/Layer',
- Component: StaticMap,
props: {
- mapboxApiAccessToken: __MAPBOX_TOKEN__,
- mapStyle: EMPTY_MAP_STYLE,
+ mapboxAccessToken: __MAPBOX_TOKEN__,
longitude: -122.4,
latitude: 37.78,
- reuseMaps: true,
zoom: 12.5,
children: [
- )
- },
- goldenImage: 'test/render/golden-images/geolocate-control.png'
}
-].filter(testCase => testCase.props.mapboxApiAccessToken);
+].filter(testCase => testCase.props.mapboxAccessToken);
diff --git a/test/src/components/controls.spec.js b/test/src/components/controls.spec.js
new file mode 100644
index 00000000..2ce73461
--- /dev/null
+++ b/test/src/components/controls.spec.js
@@ -0,0 +1,49 @@
+import {
+ Map,
+ AttributionControl,
+ FullscreenControl,
+ GeolocateControl,
+ NavigationControl,
+ ScaleControl
+} from 'react-map-gl';
+import * as React from 'react';
+import ReactTestRenderer from 'react-test-renderer';
+import test from 'tape-promise/tape';
+
+test('Controls', t => {
+ const renderer = ReactTestRenderer.create( );
+ renderer.update(
+
+
+
+ );
+ t.ok(renderer.root, 'Rendered ');
+ renderer.update(
+
+
+
+ );
+ t.ok(renderer.root, 'Rendered ');
+ renderer.update(
+
+
+
+ );
+ t.ok(renderer.root, 'Rendered ');
+ renderer.update(
+
+
+
+ );
+ t.ok(renderer.root, 'Rendered ');
+ renderer.update(
+
+
+
+ );
+ t.ok(renderer.root, 'Rendered ');
+
+ renderer.unmount();
+
+ t.end();
+});
diff --git a/test/src/components/layer.spec.js b/test/src/components/layer.spec.js
new file mode 100644
index 00000000..46082f25
--- /dev/null
+++ b/test/src/components/layer.spec.js
@@ -0,0 +1,79 @@
+import {Map, Source, Layer} from 'react-map-gl';
+import * as React from 'react';
+import {create, act} from 'react-test-renderer';
+import test from 'tape-promise/tape';
+
+import {sleep} from '../utils/test-utils';
+
+test('Source/Layer', async t => {
+ const mapRef = {current: null};
+
+ const mapStyle = {};
+ const geoJSON = {
+ type: 'Point',
+ coordinates: [0, 0]
+ };
+ const pointLayer = {
+ type: 'circle',
+ paint: {
+ 'circle-radius': 10,
+ 'circle-color': '#007cbf'
+ }
+ };
+ const pointLayer2 = {
+ type: 'circle',
+ paint: {
+ 'circle-radius': 10,
+ 'circle-color': '#000000'
+ },
+ layout: {
+ visibility: 'none'
+ }
+ };
+
+ let map;
+ act(() => {
+ map = create(
+
+
+
+
+
+ );
+ });
+ await sleep(5);
+ t.ok(mapRef.current.getLayer('my-layer'), 'Layer is added');
+
+ act(() =>
+ map.update(
+
+
+
+
+
+ )
+ );
+ t.is(mapRef.current.getLayer('my-layer').paint['circle-color'], '#000000', 'Layer is updated');
+ t.is(mapRef.current.getLayer('my-layer').layout.visibility, 'none', 'Layer is updated');
+
+ act(() =>
+ map.update(
+
+
+
+
+
+ )
+ );
+ await sleep(5);
+ t.ok(mapRef.current.getLayer('my-layer'), 'Layer is added after style change');
+
+ act(() => map.update( ));
+ await sleep(5);
+ t.notOk(mapRef.current.getSource('my-data'), 'Source is removed');
+ t.notOk(mapRef.current.getLayer('my-layer'), 'Layer is removed');
+
+ map.unmount();
+
+ t.end();
+});
diff --git a/test/src/components/map.spec.js b/test/src/components/map.spec.js
index 4e4c719b..246afabb 100644
--- a/test/src/components/map.spec.js
+++ b/test/src/components/map.spec.js
@@ -1,15 +1,161 @@
+/* global setTimeout */
import {Map} from 'react-map-gl';
import * as React from 'react';
-import ReactTestUtils from 'react-test-renderer/shallow';
-// import ReactTestRenderer from 'react-test-renderer';
+import {create, act} from 'react-test-renderer';
import test from 'tape-promise/tape';
-test('InteractiveMap#default export', t => {
+import {sleep} from '../utils/test-utils';
+
+test('Map', async t => {
t.ok(Map, 'Map is defined');
+ const mapRef = {current: null};
- const map = ;
- const result = ReactTestUtils.createRenderer().render(map);
+ let onloadCalled = 0;
+ const onLoad = () => onloadCalled++;
+
+ let map;
+ act(() => {
+ map = create(
+
+ );
+ });
+
+ t.ok(map.root, 'Rendered ');
+ t.is(mapRef.current.getCenter().lng, -100, 'longitude is set');
+ t.is(mapRef.current.getCenter().lat, 40, 'latitude is set');
+ t.is(mapRef.current.getZoom(), 4, 'zoom is set');
+
+ act(() => {
+ map.update( );
+ });
+
+ t.is(mapRef.current.getCenter().lng, -122, 'longitude is updated');
+ t.is(mapRef.current.getCenter().lat, 38, 'latitude is updated');
+ t.is(mapRef.current.getZoom(), 14, 'zoom is updated');
+
+ await sleep(1);
+ t.is(onloadCalled, 1, 'onLoad is called');
+
+ map.unmount();
- t.ok(result, 'Map rendered');
t.end();
});
+
+test('Map#uncontrolled', async t => {
+ t.plan(5);
+
+ const lastLat = 40;
+ function onRender(e) {
+ const {lat} = e.target.getCenter();
+ t.ok(lastLat > lat, 'animating');
+ }
+
+ act(() => {
+ create(
+ {
+ e.target.easeTo({center: [-122, 38], zoom: 14});
+ }}
+ onRender={onRender}
+ />
+ );
+ });
+});
+
+test('Map#controlled#no-update', async t => {
+ t.plan(5);
+
+ function onRender(e) {
+ const {lat} = e.target.getCenter();
+ t.is(lat, 40, `latitude should match props: ${lat}`);
+ }
+
+ act(() => {
+ create(
+ {
+ e.target.easeTo({center: [-122, 38], zoom: 14});
+ }}
+ onRender={onRender}
+ />
+ );
+ });
+});
+
+test('Map#controlled#mirrow-back', async t => {
+ t.plan(5);
+
+ let lastLat;
+ function onRender(e) {
+ const {lat} = e.target.getCenter();
+ t.is(lat, lastLat, `latitude should match state: ${lat}`);
+ }
+
+ function App() {
+ const [viewState, setViewState] = React.useState({
+ longitude: -100,
+ latitude: 40,
+ zoom: 4
+ });
+
+ lastLat = viewState.latitude;
+
+ return (
+ {
+ e.target.easeTo({center: [-122, 38], zoom: 14});
+ }}
+ onMove={e => setViewState(e.viewState)}
+ onRender={onRender}
+ />
+ );
+ }
+
+ act(() => {
+ create( );
+ });
+});
+
+test('Map#controlled#delayed-update', async t => {
+ t.plan(6);
+
+ let lastLat;
+ function onRender(e) {
+ const {lat} = e.target.getCenter();
+ t.is(lat, lastLat, `latitude should match state: ${lat}`);
+ }
+
+ function App() {
+ const [viewState, setViewState] = React.useState({
+ longitude: -100,
+ latitude: 40,
+ zoom: 4
+ });
+
+ lastLat = viewState.latitude;
+
+ return (
+ {
+ e.target.easeTo({center: [-122, 38], zoom: 14});
+ }}
+ onMove={e => setTimeout(() => setViewState(e.viewState))}
+ onRender={onRender}
+ />
+ );
+ }
+
+ act(() => {
+ create( );
+ });
+});
diff --git a/test/src/components/marker.spec.js b/test/src/components/marker.spec.js
new file mode 100644
index 00000000..af3ab048
--- /dev/null
+++ b/test/src/components/marker.spec.js
@@ -0,0 +1,96 @@
+import {Map, Marker} from 'react-map-gl';
+import * as React from 'react';
+import {create, act} from 'react-test-renderer';
+import test from 'tape-promise/tape';
+
+import {createPortalMock} from '../utils/test-utils';
+
+test('Marker', t => {
+ const restoreMock = createPortalMock();
+ const mapRef = {current: null};
+
+ let map;
+ act(() => {
+ map = create(
+
+
+
+ );
+ });
+
+ const marker = mapRef.current.getMap()._markers[0];
+ t.ok(marker, 'Marker is created');
+
+ const offset = marker.getOffset();
+ const draggable = marker.isDraggable();
+ const rotation = marker.getRotation();
+ const pitchAlignment = marker.getPitchAlignment();
+ const rotationAlignment = marker.getRotationAlignment();
+
+ act(() => {
+ map.update(
+
+
+
+ );
+ });
+
+ t.is(offset, marker.getOffset(), 'offset did not change deeply');
+
+ let callbackType = '';
+ act(() => {
+ map.update(
+
+ (callbackType = 'dragstart')}
+ onDrag={() => (callbackType = 'drag')}
+ onDragEnd={() => (callbackType = 'dragend')}
+ />
+
+ );
+ });
+
+ t.not(offset, marker.getOffset(), 'offset is updated');
+ t.not(draggable, marker.isDraggable(), 'draggable is updated');
+ t.not(rotation, marker.getRotation(), 'rotation is updated');
+ t.not(pitchAlignment, marker.getPitchAlignment(), 'pitchAlignment is updated');
+ t.not(rotationAlignment, marker.getRotationAlignment(), 'rotationAlignment is updated');
+
+ marker.fire('dragstart');
+ t.is(callbackType, 'dragstart', 'onDragStart called');
+ marker.fire('drag');
+ t.is(callbackType, 'drag', 'onDrag called');
+ marker.fire('dragend');
+ t.is(callbackType, 'dragend', 'onDragEnd called');
+
+ act(() => {
+ map.update( );
+ });
+
+ t.is(mapRef.current.getMap()._markers.length, 0, 'marker is removed');
+
+ act(() => {
+ map.update(
+
+
+
+
+
+ );
+ });
+
+ t.ok(map.root.findByProps({id: 'marker-content'}), 'content is rendered');
+
+ map.unmount();
+
+ restoreMock();
+
+ t.end();
+});
diff --git a/test/src/components/popup.spec.js b/test/src/components/popup.spec.js
new file mode 100644
index 00000000..50c59b38
--- /dev/null
+++ b/test/src/components/popup.spec.js
@@ -0,0 +1,63 @@
+import {Map, Popup} from 'react-map-gl';
+import * as React from 'react';
+import {create, act} from 'react-test-renderer';
+import test from 'tape-promise/tape';
+
+import {createPortalMock} from '../utils/test-utils';
+
+test('Popup', t => {
+ const restoreMock = createPortalMock();
+ const mapRef = {current: null};
+
+ let map;
+ act(() => {
+ map = create(
+
+
+ You are here
+
+
+ );
+ });
+
+ const popup = mapRef.current.getMap()._markers[0];
+ t.ok(popup, 'Popup is created');
+
+ const {anchor, offset, maxWidth} = popup.options;
+
+ act(() => {
+ map.update(
+
+
+ You are here
+
+
+ );
+ });
+
+ t.is(offset, popup.options.offset, 'offset did not change deeply');
+
+ act(() => {
+ map.update(
+
+
+ You are here
+
+
+ );
+ });
+
+ t.not(offset, popup.options.offset, 'offset is updated');
+ t.not(anchor, popup.options.anchor, 'anchor is updated');
+ t.not(maxWidth, popup.options.maxWidth, 'maxWidth is updated');
+
+ restoreMock();
+
+ t.end();
+});
diff --git a/test/src/components/source.spec.js b/test/src/components/source.spec.js
new file mode 100644
index 00000000..8242472c
--- /dev/null
+++ b/test/src/components/source.spec.js
@@ -0,0 +1,58 @@
+import {Map, Source} from 'react-map-gl';
+import * as React from 'react';
+import {create, act} from 'react-test-renderer';
+import test from 'tape-promise/tape';
+
+import {sleep} from '../utils/test-utils';
+
+test('Source/Layer', async t => {
+ const mapRef = {current: null};
+
+ const mapStyle = {};
+ const geoJSON = {
+ type: 'Point',
+ coordinates: [0, 0]
+ };
+ const geoJSON2 = {
+ type: 'Point',
+ coordinates: [1, 1]
+ };
+
+ let map;
+ act(() => {
+ map = create(
+
+
+
+ );
+ });
+ await sleep(5);
+ t.ok(mapRef.current.getSource('my-data'), 'Source is added');
+
+ act(() =>
+ map.update(
+
+
+
+ )
+ );
+ await sleep(5);
+ t.ok(mapRef.current.getSource('my-data'), 'Source is added after style change');
+
+ act(() =>
+ map.update(
+
+
+
+ )
+ );
+ t.is(mapRef.current.getSource('my-data').getData(), geoJSON2, 'Source is updated');
+
+ act(() => map.update( ));
+ await sleep(5);
+ t.notOk(mapRef.current.getSource('my-data'), 'Source is removed');
+
+ map.unmount();
+
+ t.end();
+});
diff --git a/test/src/components/use-map.spec.js b/test/src/components/use-map.spec.js
new file mode 100644
index 00000000..92684988
--- /dev/null
+++ b/test/src/components/use-map.spec.js
@@ -0,0 +1,53 @@
+import {Map, MapProvider, useMap} from 'react-map-gl';
+import * as React from 'react';
+import {create, act} from 'react-test-renderer';
+import test from 'tape-promise/tape';
+
+test('useMap', t => {
+ let app;
+ let maps;
+
+ function TestControl() {
+ maps = useMap();
+ return null;
+ }
+
+ act(() => {
+ app = create(
+
+
+
+
+
+ );
+ });
+
+ t.ok(maps.mapA, 'Context has mapA');
+ t.ok(maps.mapB, 'Context has mapB');
+
+ act(() => {
+ app = create(
+
+
+
+
+ );
+ });
+
+ t.ok(maps.mapA, 'Context has mapA');
+ t.notOk(maps.mapB, 'mapB is removed');
+
+ act(() => {
+ app = create(
+
+
+
+ );
+ });
+
+ t.notOk(maps.mapA, 'mapA is removed');
+
+ app.unmount();
+
+ t.end();
+});
diff --git a/test/src/index.js b/test/src/index.js
index f6bf0b34..f340721b 100644
--- a/test/src/index.js
+++ b/test/src/index.js
@@ -1,4 +1,11 @@
import './components/map.spec';
+import './components/controls.spec';
+import './components/source.spec';
+import './components/layer.spec';
+import './components/marker.spec';
+import './components/popup.spec';
+import './components/use-map.spec';
import './utils/deep-equal.spec';
import './utils/transform.spec';
+import './utils/style-utils.spec';
diff --git a/test/src/utils/mapbox-gl-mock/edge_insets.js b/test/src/utils/mapbox-gl-mock/edge_insets.js
new file mode 100644
index 00000000..fb8381ec
--- /dev/null
+++ b/test/src/utils/mapbox-gl-mock/edge_insets.js
@@ -0,0 +1,72 @@
+// Generated with
+// flow-remove-types ./node_modules/mapbox-gl/src/geo/edge_insets.js
+
+import Point from '@mapbox/point-geometry';
+import {clamp, number} from './util.js';
+
+class EdgeInsets {
+ constructor(top = 0, bottom = 0, left = 0, right = 0) {
+ if (
+ isNaN(top) ||
+ top < 0 ||
+ isNaN(bottom) ||
+ bottom < 0 ||
+ isNaN(left) ||
+ left < 0 ||
+ isNaN(right) ||
+ right < 0
+ ) {
+ throw new Error(
+ 'Invalid value for edge-insets, top, bottom, left and right must all be numbers'
+ );
+ }
+
+ this.top = top;
+ this.bottom = bottom;
+ this.left = left;
+ this.right = right;
+ }
+
+ interpolate(start, target, t) {
+ if (target.top != null && start.top != null) this.top = number(start.top, target.top, t);
+ if (target.bottom != null && start.bottom != null)
+ this.bottom = number(start.bottom, target.bottom, t);
+ if (target.left != null && start.left != null) this.left = number(start.left, target.left, t);
+ if (target.right != null && start.right != null)
+ this.right = number(start.right, target.right, t);
+
+ return this;
+ }
+
+ getCenter(width, height) {
+ // Clamp insets so they never overflow width/height and always calculate a valid center
+ const x = clamp((this.left + width - this.right) / 2, 0, width);
+ const y = clamp((this.top + height - this.bottom) / 2, 0, height);
+
+ return new Point(x, y);
+ }
+
+ equals(other) {
+ return (
+ this.top === other.top &&
+ this.bottom === other.bottom &&
+ this.left === other.left &&
+ this.right === other.right
+ );
+ }
+
+ clone() {
+ return new EdgeInsets(this.top, this.bottom, this.left, this.right);
+ }
+
+ toJSON() {
+ return {
+ top: this.top,
+ bottom: this.bottom,
+ left: this.left,
+ right: this.right
+ };
+ }
+}
+
+export default EdgeInsets;
diff --git a/test/src/utils/mapbox-gl-mock/evented.js b/test/src/utils/mapbox-gl-mock/evented.js
new file mode 100644
index 00000000..abc2403c
--- /dev/null
+++ b/test/src/utils/mapbox-gl-mock/evented.js
@@ -0,0 +1,128 @@
+// Generated with
+// flow-remove-types ./node_modules/mapbox-gl/src/util/evented.js
+
+import {extend} from './util.js';
+
+function _addEventListener(type, listener, listenerList) {
+ const listenerExists = listenerList[type] && listenerList[type].indexOf(listener) !== -1;
+ if (!listenerExists) {
+ listenerList[type] = listenerList[type] || [];
+ listenerList[type].push(listener);
+ }
+}
+
+function _removeEventListener(type, listener, listenerList) {
+ if (listenerList && listenerList[type]) {
+ const index = listenerList[type].indexOf(listener);
+ if (index !== -1) {
+ listenerList[type].splice(index, 1);
+ }
+ }
+}
+
+export class Event {
+ constructor(type, data = {}) {
+ extend(this, data);
+ this.type = type;
+ }
+}
+
+export class ErrorEvent extends Event {
+ constructor(error, data = {}) {
+ super('error', extend({error}, data));
+ }
+}
+
+export class Evented {
+ on(type, listener) {
+ this._listeners = this._listeners || {};
+ _addEventListener(type, listener, this._listeners);
+
+ return this;
+ }
+
+ off(type, listener) {
+ _removeEventListener(type, listener, this._listeners);
+ _removeEventListener(type, listener, this._oneTimeListeners);
+
+ return this;
+ }
+
+ once(type, listener) {
+ if (!listener) {
+ return new Promise(resolve => this.once(type, resolve));
+ }
+
+ this._oneTimeListeners = this._oneTimeListeners || {};
+ _addEventListener(type, listener, this._oneTimeListeners);
+
+ return this;
+ }
+
+ fire(event, properties) {
+ // Compatibility with (type: string, properties: Object) signature from previous versions.
+ // See https://github.com/mapbox/mapbox-gl-js/issues/6522,
+ // https://github.com/mapbox/mapbox-gl-draw/issues/766
+ if (typeof event === 'string') {
+ event = new Event(event, properties || {});
+ }
+
+ const type = event.type;
+
+ if (this.listens(type)) {
+ event.target = this;
+
+ // make sure adding or removing listeners inside other listeners won't cause an infinite loop
+ const listeners =
+ this._listeners && this._listeners[type] ? this._listeners[type].slice() : [];
+
+ for (const listener of listeners) {
+ listener.call(this, event);
+ }
+
+ const oneTimeListeners =
+ this._oneTimeListeners && this._oneTimeListeners[type]
+ ? this._oneTimeListeners[type].slice()
+ : [];
+ for (const listener of oneTimeListeners) {
+ _removeEventListener(type, listener, this._oneTimeListeners);
+ listener.call(this, event);
+ }
+
+ const parent = this._eventedParent;
+ if (parent) {
+ extend(
+ event,
+ typeof this._eventedParentData === 'function'
+ ? this._eventedParentData()
+ : this._eventedParentData
+ );
+ parent.fire(event);
+ }
+
+ // To ensure that no error events are dropped, print them to the
+ // console if they have no listeners.
+ } else if (event instanceof ErrorEvent) {
+ console.error(event.error);
+ }
+
+ return this;
+ }
+
+ listens(type) {
+ return Boolean(
+ (this._listeners && this._listeners[type] && this._listeners[type].length > 0) ||
+ (this._oneTimeListeners &&
+ this._oneTimeListeners[type] &&
+ this._oneTimeListeners[type].length > 0) ||
+ (this._eventedParent && this._eventedParent.listens(type))
+ );
+ }
+
+ setEventedParent(parent, data) {
+ this._eventedParent = parent;
+ this._eventedParentData = data;
+
+ return this;
+ }
+}
diff --git a/test/src/utils/mapbox-gl-mock/globals.js b/test/src/utils/mapbox-gl-mock/globals.js
new file mode 100644
index 00000000..265f00d0
--- /dev/null
+++ b/test/src/utils/mapbox-gl-mock/globals.js
@@ -0,0 +1,26 @@
+export function supported() {
+ return true;
+}
+
+let rtlTextPlugin = '';
+let baseApiUrl = 'https://api.mapbox.com';
+
+export function getRTLTextPluginStatus() {
+ return rtlTextPlugin ? 'deferred' : 'unavailable';
+}
+
+export function setRTLTextPlugin(url) {
+ rtlTextPlugin = url;
+}
+
+export default {
+ supported,
+ getRTLTextPluginStatus,
+ setRTLTextPlugin,
+ get baseApiUrl() {
+ return baseApiUrl;
+ },
+ set baseApiUrl(value) {
+ baseApiUrl = value;
+ }
+};
diff --git a/test/src/utils/mapbox-gl-mock/index.js b/test/src/utils/mapbox-gl-mock/index.js
new file mode 100644
index 00000000..a36432ad
--- /dev/null
+++ b/test/src/utils/mapbox-gl-mock/index.js
@@ -0,0 +1,34 @@
+// Adapted from https://github.com/mapbox/mapbox-gl-js-mock/
+// BSD 3-Clause License
+// Copyright (c) 2017, Mapbox
+
+class FakeControl {
+ constructor(opts) {
+ this.options = opts;
+ }
+ addTo() {}
+ onAdd() {}
+ onRemove() {}
+ on() {}
+}
+
+import Map from './map';
+import LngLat from './lng_lat';
+import LngLatBounds from './lng_lat_bounds';
+import globals from './globals';
+import Marker from './marker';
+import Popup from './popup';
+
+export default {
+ ...globals,
+ Map,
+ LngLat,
+ LngLatBounds,
+ Marker,
+ Popup,
+ NavigationControl: FakeControl,
+ ScaleControl: FakeControl,
+ AttributionControl: FakeControl,
+ GeolocateControl: FakeControl,
+ FullscreenControl: FakeControl
+};
diff --git a/test/src/utils/mapbox-gl-mock/lng_lat.js b/test/src/utils/mapbox-gl-mock/lng_lat.js
new file mode 100644
index 00000000..7c37b2e6
--- /dev/null
+++ b/test/src/utils/mapbox-gl-mock/lng_lat.js
@@ -0,0 +1,79 @@
+// Generated with
+// flow-remove-types ./node_modules/mapbox-gl/src/geo/lng_lat.js
+
+import {wrap} from './util.js';
+import LngLatBounds from './lng_lat_bounds.js';
+
+export const earthRadius = 6371008.8;
+
+class LngLat {
+ lng;
+ lat;
+
+ constructor(lng, lat) {
+ if (isNaN(lng) || isNaN(lat)) {
+ throw new Error(`Invalid LngLat object: (${lng}, ${lat})`);
+ }
+ this.lng = Number(lng);
+ this.lat = Number(lat);
+ if (this.lat > 90 || this.lat < -90) {
+ throw new Error('Invalid LngLat latitude value: must be between -90 and 90');
+ }
+ }
+
+ wrap() {
+ return new LngLat(wrap(this.lng, -180, 180), this.lat);
+ }
+
+ toArray() {
+ return [this.lng, this.lat];
+ }
+
+ toString() {
+ return `LngLat(${this.lng}, ${this.lat})`;
+ }
+
+ distanceTo(lngLat) {
+ const rad = Math.PI / 180;
+ const lat1 = this.lat * rad;
+ const lat2 = lngLat.lat * rad;
+ const a =
+ Math.sin(lat1) * Math.sin(lat2) +
+ Math.cos(lat1) * Math.cos(lat2) * Math.cos((lngLat.lng - this.lng) * rad);
+
+ const maxMeters = earthRadius * Math.acos(Math.min(a, 1));
+ return maxMeters;
+ }
+
+ toBounds(radius = 0) {
+ const earthCircumferenceInMetersAtEquator = 40075017;
+ const latAccuracy = (360 * radius) / earthCircumferenceInMetersAtEquator;
+ const lngAccuracy = latAccuracy / Math.cos((Math.PI / 180) * this.lat);
+
+ return new LngLatBounds(
+ new LngLat(this.lng - lngAccuracy, this.lat - latAccuracy),
+ new LngLat(this.lng + lngAccuracy, this.lat + latAccuracy)
+ );
+ }
+
+ static convert(input) {
+ if (input instanceof LngLat) {
+ return input;
+ }
+ if (Array.isArray(input) && (input.length === 2 || input.length === 3)) {
+ return new LngLat(Number(input[0]), Number(input[1]));
+ }
+ if (!Array.isArray(input) && typeof input === 'object' && input !== null) {
+ return new LngLat(
+ // flow can't refine this to have one of lng or lat, so we have to cast to any
+ Number('lng' in input ? input.lng : input.lon),
+ Number(input.lat)
+ );
+ }
+ throw new Error(
+ '`LngLatLike` argument must be specified as a LngLat instance, an object {lng: , lat: }, an object {lon: , lat: }, or an array of [, ]'
+ );
+ }
+}
+
+export default LngLat;
diff --git a/test/src/utils/mapbox-gl-mock/lng_lat_bounds.js b/test/src/utils/mapbox-gl-mock/lng_lat_bounds.js
new file mode 100644
index 00000000..a99b8ff3
--- /dev/null
+++ b/test/src/utils/mapbox-gl-mock/lng_lat_bounds.js
@@ -0,0 +1,139 @@
+// Generated with
+// flow-remove-types ./node_modules/mapbox-gl/src/geo/lng_lat_bounds.js
+
+import LngLat from './lng_lat.js';
+
+class LngLatBounds {
+ _ne;
+ _sw;
+
+ // This constructor is too flexible to type. It should not be so flexible.
+ constructor(sw, ne) {
+ if (!sw) {
+ // noop
+ } else if (ne) {
+ this.setSouthWest(sw).setNorthEast(ne);
+ } else if (sw.length === 4) {
+ this.setSouthWest([sw[0], sw[1]]).setNorthEast([sw[2], sw[3]]);
+ } else {
+ this.setSouthWest(sw[0]).setNorthEast(sw[1]);
+ }
+ }
+
+ setNorthEast(ne) {
+ this._ne = ne instanceof LngLat ? new LngLat(ne.lng, ne.lat) : LngLat.convert(ne);
+ return this;
+ }
+
+ setSouthWest(sw) {
+ this._sw = sw instanceof LngLat ? new LngLat(sw.lng, sw.lat) : LngLat.convert(sw);
+ return this;
+ }
+
+ extend(obj) {
+ const sw = this._sw;
+ const ne = this._ne;
+ let ne2;
+ let sw2;
+
+ if (obj instanceof LngLat) {
+ sw2 = obj;
+ ne2 = obj;
+ } else if (obj instanceof LngLatBounds) {
+ sw2 = obj._sw;
+ ne2 = obj._ne;
+
+ if (!sw2 || !ne2) return this;
+ } else {
+ if (Array.isArray(obj)) {
+ if (obj.length === 4 || obj.every(Array.isArray)) {
+ const lngLatBoundsObj = obj;
+ return this.extend(LngLatBounds.convert(lngLatBoundsObj));
+ }
+ const lngLatObj = obj;
+ return this.extend(LngLat.convert(lngLatObj));
+ }
+ return this;
+ }
+
+ if (!sw && !ne) {
+ this._sw = new LngLat(sw2.lng, sw2.lat);
+ this._ne = new LngLat(ne2.lng, ne2.lat);
+ } else {
+ sw.lng = Math.min(sw2.lng, sw.lng);
+ sw.lat = Math.min(sw2.lat, sw.lat);
+ ne.lng = Math.max(ne2.lng, ne.lng);
+ ne.lat = Math.max(ne2.lat, ne.lat);
+ }
+
+ return this;
+ }
+
+ getCenter() {
+ return new LngLat((this._sw.lng + this._ne.lng) / 2, (this._sw.lat + this._ne.lat) / 2);
+ }
+
+ getSouthWest() {
+ return this._sw;
+ }
+
+ getNorthEast() {
+ return this._ne;
+ }
+
+ getNorthWest() {
+ return new LngLat(this.getWest(), this.getNorth());
+ }
+
+ getSouthEast() {
+ return new LngLat(this.getEast(), this.getSouth());
+ }
+
+ getWest() {
+ return this._sw.lng;
+ }
+
+ getSouth() {
+ return this._sw.lat;
+ }
+
+ getEast() {
+ return this._ne.lng;
+ }
+
+ getNorth() {
+ return this._ne.lat;
+ }
+
+ toArray() {
+ return [this._sw.toArray(), this._ne.toArray()];
+ }
+
+ toString() {
+ return `LngLatBounds(${this._sw.toString()}, ${this._ne.toString()})`;
+ }
+
+ isEmpty() {
+ return !(this._sw && this._ne);
+ }
+
+ contains(lnglat) {
+ const {lng, lat} = LngLat.convert(lnglat);
+
+ const containsLatitude = this._sw.lat <= lat && lat <= this._ne.lat;
+ let containsLongitude = this._sw.lng <= lng && lng <= this._ne.lng;
+ if (this._sw.lng > this._ne.lng) {
+ // wrapped coordinates
+ containsLongitude = this._sw.lng >= lng && lng >= this._ne.lng;
+ }
+
+ return containsLatitude && containsLongitude;
+ }
+
+ static convert(input) {
+ if (!input || input instanceof LngLatBounds) return input;
+ return new LngLatBounds(input);
+ }
+}
+
+export default LngLatBounds;
diff --git a/test/src/utils/mapbox-gl-mock/map.js b/test/src/utils/mapbox-gl-mock/map.js
new file mode 100644
index 00000000..544be3d3
--- /dev/null
+++ b/test/src/utils/mapbox-gl-mock/map.js
@@ -0,0 +1,249 @@
+// Adapted from https://github.com/mapbox/mapbox-gl-js-mock/
+// BSD 3-Clause License
+// Copyright (c) 2017, Mapbox
+import LngLat from './lng_lat';
+import {Evented, Event} from './evented';
+import TaskQueue from './task_queue';
+import Transform from './transform';
+import {extend, number} from './util';
+import Style from './style';
+
+const defaultOptions = {
+ doubleClickZoom: true
+};
+
+function functor(x) {
+ return function () {
+ return x;
+ };
+}
+
+export default class Map extends Evented {
+ constructor(options) {
+ super();
+
+ this.options = extend(options || {}, defaultOptions);
+ this._events = {};
+ this.style = new Style();
+ this._isMoving = false;
+ this._controls = [];
+ this._markers = [];
+ this._renderTaskQueue = new TaskQueue();
+
+ this.transform = new Transform();
+ this.transform.center = LngLat.convert(this.options.center || [0, 0]);
+ this.transform.zoom = this.options.zoom || 0;
+ this.transform.pitch = this.options.pitch || 0;
+ this.transform.bearing = this.options.bearing || 0;
+
+ setTimeout(() => {
+ this.style._loaded = true;
+ this.fire(new Event('styledata'));
+ this.fire(new Event('load'));
+ }, 0);
+ }
+
+ addControl(control) {
+ this._controls.push(control);
+ control.onAdd(this);
+ }
+ removeControl(control) {
+ const i = this._controls.indexOf(control);
+ if (i >= 0) {
+ this._controls.splice(i, 1);
+ control.onRemove(this);
+ }
+ }
+
+ _addMarker(marker) {
+ this._markers.push(marker);
+ }
+ _removeMarker(marker) {
+ const i = this._markers.indexOf(marker);
+ if (i >= 0) {
+ this._markers.splice(i, 1);
+ }
+ }
+
+ getContainer() {
+ const container = {
+ parentNode: container,
+ appendChild() {},
+ removeChild() {},
+ getElementsByClassName() {
+ return [container];
+ },
+ addEventListener(name, handle) {},
+ removeEventListener() {},
+ classList: {
+ add() {},
+ remove() {}
+ }
+ };
+
+ return container;
+ }
+
+ loaded() {
+ return true;
+ }
+
+ addSource(name, source) {
+ this.style.addSource(name, source);
+ if (source.type === 'geojson') {
+ const e = {
+ type: 'data',
+ sourceDataType: 'metadata',
+ sourceId: name,
+ isSourceLoaded: true,
+ dataType: 'source',
+ source
+ };
+ this.fire(new Event('data', e));
+ }
+ }
+
+ getSource(name) {
+ const source = this.style.getSource(name);
+ if (source) {
+ return {
+ getData: () => source.data,
+ setData: data => {
+ source.data = data;
+ if (source.type === 'geojson') {
+ const e = {
+ type: 'data',
+ sourceDataType: 'content',
+ sourceId: name,
+ isSourceLoaded: true,
+ dataType: 'source',
+ source
+ };
+ this.fire(new Event('data', e));
+ }
+ },
+ loadTile() {}
+ };
+ }
+ return null;
+ }
+
+ removeSource(name) {
+ this.style.removeSource(name);
+ }
+
+ setStyle(style) {
+ this.style = new Style();
+ setTimeout(() => {
+ this.style._loaded = true;
+ this.fire(new Event('styledata'));
+ }, 0);
+ }
+
+ addLayer(layer, before) {
+ this.style.addLayer(layer, before);
+ }
+ moveLayer(layerId, beforeId) {}
+ removeLayer(layerId) {
+ this.style.removeLayer(layerId);
+ }
+ getLayer(layerId) {
+ return this.style.getLayer(layerId);
+ }
+ setLayoutProperty(layerId, name, value) {
+ this.style.setLayoutProperty(layerId, name, value);
+ }
+ setPaintProperty(layerId, name, value) {
+ this.style.setPaintProperty(layerId, name, value);
+ }
+
+ doubleClickZoom = {
+ disable() {},
+ enable() {}
+ };
+
+ boxZoom = {
+ disable() {},
+ enable() {}
+ };
+
+ dragPan = {
+ disable() {},
+ enable() {}
+ };
+
+ project() {}
+
+ queryRenderedFeatures(pointOrBox, queryParams) {
+ return [];
+ }
+
+ getCenter() {
+ return this.transform.center;
+ }
+ getZoom() {
+ return this.transform.zoom;
+ }
+ getBearing() {
+ return this.transform.bearing;
+ }
+ getPitch() {
+ return this.transform.pitch;
+ }
+ getPadding() {
+ return this.transform.padding;
+ }
+
+ easeTo({center: [lng, lat], zoom}) {
+ const FRAMES = 5;
+ let f = 0;
+
+ this._isMoving = true;
+ this.fire(new Event('movestart'));
+
+ const startLng = this.getCenter().lng;
+ const startLat = this.getCenter().lat;
+ const startZoom = this.getZoom();
+
+ const onFrame = () => {
+ f++;
+
+ this.transform.center = LngLat.convert([
+ number(startLng, lng, f / FRAMES),
+ number(startLat, lat, f / FRAMES)
+ ]);
+ this.transform.zoom = number(startZoom, zoom, f / FRAMES);
+
+ this.fire(new Event('move'));
+
+ if (f < FRAMES) {
+ this._renderTaskQueue.add(onFrame);
+ this.triggerRepaint();
+ } else {
+ this.fire(new Event('moveend'));
+ this._isMoving = false;
+ }
+ };
+
+ this._renderTaskQueue.add(onFrame);
+ this.triggerRepaint();
+ }
+
+ isMoving() {
+ return this._isMoving;
+ }
+
+ triggerRepaint() {
+ setTimeout(() => this._render(), 0);
+ }
+
+ _render() {
+ this._renderTaskQueue.run();
+ this.fire(new Event('render'));
+ }
+
+ remove() {
+ this._events = [];
+ this.sources = [];
+ }
+}
diff --git a/test/src/utils/mapbox-gl-mock/marker.js b/test/src/utils/mapbox-gl-mock/marker.js
new file mode 100644
index 00000000..349eaae1
--- /dev/null
+++ b/test/src/utils/mapbox-gl-mock/marker.js
@@ -0,0 +1,78 @@
+import {Evented} from './evented';
+import LngLat from './lng_lat';
+
+export default class Marker extends Evented {
+ constructor(opts) {
+ super();
+ this.options = opts;
+
+ this._map = null;
+ this._lngLat = null;
+ this._popup = null;
+ this._element = opts.element || {};
+ }
+
+ addTo(map) {
+ this._map = map;
+ map._addMarker(this);
+ return this;
+ }
+ remove() {
+ this._map._removeMarker(this);
+ this._map = null;
+ }
+
+ getElement() {
+ return this._element;
+ }
+
+ getLngLat() {
+ return this._lngLat;
+ }
+ setLngLat(value) {
+ this._lngLat = LngLat.convert(value);
+ return this;
+ }
+ getOffset() {
+ return this.options.offset;
+ }
+ setOffset(value) {
+ this.options.offset = value;
+ return this;
+ }
+ isDraggable() {
+ return this.options.draggable;
+ }
+ setDraggable(value) {
+ this.options.draggable = value;
+ return this;
+ }
+ getRotation() {
+ return this.options.rotation;
+ }
+ setRotation(value) {
+ this.options.rotation = value;
+ return this;
+ }
+ getRotationAlignment() {
+ return this.options.rotationAlignment;
+ }
+ setRotationAlignment(value) {
+ this.options.rotationAlignment = value;
+ return this;
+ }
+ getPitchAlignment() {
+ return this.options.pitchAlignment;
+ }
+ setPitchAlignment(value) {
+ this.options.pitchAlignment = value;
+ return this;
+ }
+ getPopup() {
+ return this._popup;
+ }
+ setPopup(value) {
+ this._popup = value;
+ return this;
+ }
+}
diff --git a/test/src/utils/mapbox-gl-mock/popup.js b/test/src/utils/mapbox-gl-mock/popup.js
new file mode 100644
index 00000000..4efd2377
--- /dev/null
+++ b/test/src/utils/mapbox-gl-mock/popup.js
@@ -0,0 +1,55 @@
+import {Evented} from './evented';
+import LngLat from './lng_lat';
+
+export default class Popup extends Evented {
+ constructor(opts) {
+ super();
+ this.options = opts;
+
+ this._lngLat = null;
+ this._content = null;
+ this._classList = new Set(opts.className ? opts.className.trim().split(/\s+/) : []);
+ }
+
+ addTo(map) {
+ this._map = map;
+ map._addMarker(this);
+ return this;
+ }
+ remove() {
+ this._map._removeMarker(this);
+ this._map = null;
+ }
+
+ getLngLat() {
+ return this._lngLat;
+ }
+ setLngLat(value) {
+ this._lngLat = LngLat.convert(value);
+ return this;
+ }
+
+ setText(text) {
+ this._content = text;
+ return this;
+ }
+ setHTML(html) {
+ this._content = html;
+ return this;
+ }
+ setDOMContent(htmlNode) {
+ this._content = htmlNode;
+ return this;
+ }
+ getMaxWidth() {
+ return this.options.maxWidth;
+ }
+ setMaxWidth(value) {
+ this.options.maxWidth = value;
+ return this;
+ }
+ setOffset(value) {
+ this.options.offset = value;
+ return this;
+ }
+}
diff --git a/test/src/utils/mapbox-gl-mock/style.js b/test/src/utils/mapbox-gl-mock/style.js
new file mode 100644
index 00000000..dcb844f6
--- /dev/null
+++ b/test/src/utils/mapbox-gl-mock/style.js
@@ -0,0 +1,111 @@
+// Adapted from https://github.com/mapbox/mapbox-gl-js-mock/
+// BSD 3-Clause License
+// Copyright (c) 2017, Mapbox
+
+export default class Style {
+ constructor() {
+ this.stylesheet = {
+ owner: 'mapbox',
+ id: 'testmap'
+ };
+
+ this._loaded = false;
+ this._light = null;
+ this._fog = null;
+ this._terrain = null;
+
+ this._sources = {};
+ this._layers = {};
+ }
+
+ loaded() {
+ return this._loaded;
+ }
+
+ _checkLoaded() {
+ if (!this._loaded) {
+ throw new Error('style is not done loading');
+ }
+ }
+
+ setLight(value) {
+ this._checkLoaded();
+ this._light = value;
+ }
+ setFog(value) {
+ this._checkLoaded();
+ this._fog = value;
+ }
+ setTerrain(value) {
+ this._checkLoaded();
+ this._terrain = value;
+ }
+
+ addSource(name, source) {
+ this._checkLoaded();
+ if (name in this._sources) {
+ throw new Error('Source with the same id already exists');
+ }
+ this._sources[name] = source;
+ }
+
+ getSource(name) {
+ return this._sources[name];
+ }
+
+ removeSource(name) {
+ this._checkLoaded();
+ if (!(name in this._sources)) {
+ throw new Error('No source with this id');
+ }
+ for (const layerId in this._layers) {
+ if (this._layers[layerId].source === name) {
+ throw new Error('Source cannot be removed while layer is using it.');
+ }
+ }
+ delete this._sources[name];
+ }
+
+ addLayer(layer, before) {
+ this._checkLoaded();
+ if (layer.id in this._layers) {
+ throw new Error('Layer with the same id already exists');
+ }
+ if (!(layer.source in this._sources)) {
+ throw new Error('Layer source does not exist');
+ }
+ this._layers[layer.id] = layer;
+ }
+
+ removeLayer(layerId) {
+ this._checkLoaded();
+ if (!(layerId in this._layers)) {
+ throw new Error('No layer with this id');
+ }
+ delete this._layers[layerId];
+ }
+
+ getLayer(layerId) {
+ return this._layers[layerId];
+ }
+
+ setLayoutProperty(layerId, name, value) {
+ this._checkLoaded();
+ const layer = this.getLayer(layerId);
+ if (!layer) {
+ throw new Error('No layer with this id');
+ }
+ layer.layout = layer.layout || {};
+ layer.layout[name] = value;
+ }
+
+ setPaintProperty(layerId, name, value) {
+ this._checkLoaded();
+ const layer = this.getLayer(layerId);
+ if (!layer) {
+ throw new Error('No layer with this id');
+ }
+ layer.paint = layer.paint || {};
+ layer.paint[name] = value;
+ }
+}
diff --git a/test/src/utils/mapbox-gl-mock/task_queue.js b/test/src/utils/mapbox-gl-mock/task_queue.js
new file mode 100644
index 00000000..a0621032
--- /dev/null
+++ b/test/src/utils/mapbox-gl-mock/task_queue.js
@@ -0,0 +1,58 @@
+// Generated with
+// flow-remove-types ./node_modules/mapbox-gl/src/util/task_queue.js
+
+import assert from 'assert';
+
+class TaskQueue {
+ constructor() {
+ this._queue = [];
+ this._id = 0;
+ this._cleared = false;
+ this._currentlyRunning = false;
+ }
+
+ add(callback) {
+ const id = ++this._id;
+ const queue = this._queue;
+ queue.push({callback, id, cancelled: false});
+ return id;
+ }
+
+ remove(id) {
+ const running = this._currentlyRunning;
+ const queue = running ? this._queue.concat(running) : this._queue;
+ for (const task of queue) {
+ if (task.id === id) {
+ task.cancelled = true;
+ return;
+ }
+ }
+ }
+
+ run(timeStamp = 0) {
+ assert(!this._currentlyRunning);
+ const queue = (this._currentlyRunning = this._queue);
+
+ // Tasks queued by callbacks in the current queue should be executed
+ // on the next run, not the current run.
+ this._queue = [];
+
+ for (const task of queue) {
+ if (task.cancelled) continue;
+ task.callback(timeStamp);
+ if (this._cleared) break;
+ }
+
+ this._cleared = false;
+ this._currentlyRunning = false;
+ }
+
+ clear() {
+ if (this._currentlyRunning) {
+ this._cleared = true;
+ }
+ this._queue = [];
+ }
+}
+
+export default TaskQueue;
diff --git a/test/src/utils/transform.js b/test/src/utils/mapbox-gl-mock/transform.js
similarity index 59%
rename from test/src/utils/transform.js
rename to test/src/utils/mapbox-gl-mock/transform.js
index c02b23d5..368819f8 100644
--- a/test/src/utils/transform.js
+++ b/test/src/utils/mapbox-gl-mock/transform.js
@@ -1,55 +1,7 @@
-function wrap(n, min, max) {
- const d = max - min;
- const w = ((((n - min) % d) + d) % d) + min;
- return w === min ? max : w;
-}
+import {wrap, clamp} from './util';
-function clamp(n, min, max) {
- return Math.min(max, Math.max(min, n));
-}
+import EdgeInsets from './edge_insets';
-/**
- * A dummy class that simulates mapbox's EdgeInsets
- */
-class EdgeInsets {
- constructor(top = 0, bottom = 0, left = 0, right = 0) {
- this.top = top;
- this.bottom = bottom;
- this.left = left;
- this.right = right;
- }
-
- set(target) {
- if (target.top !== null) this.top = target.top;
- if (target.bottom !== null) this.bottom = target.bottom;
- if (target.left !== null) this.left = target.left;
- if (target.right !== null) this.right = target.right;
-
- return this;
- }
-
- equals(other) {
- return (
- this.top === other.top &&
- this.bottom === other.bottom &&
- this.left === other.left &&
- this.right === other.right
- );
- }
-
- toJSON() {
- return {
- top: this.top,
- bottom: this.bottom,
- left: this.left,
- right: this.right
- };
- }
-}
-
-/**
- * A dummy class that simulates mapbox's Transform
- */
export default class Transform {
constructor() {
this.minZoom = 0;
@@ -119,7 +71,17 @@ export default class Transform {
set padding(padding) {
if (this._edgeInsets.equals(padding)) return;
// Update edge-insets inplace
- this._edgeInsets.set(padding);
+ this._edgeInsets.interpolate(this._edgeInsets, padding, 1);
+ }
+
+ clone() {
+ const that = new Transform();
+ that.center = this.center;
+ that.zoom = this.zoom;
+ that.bearing = this.bearing;
+ that.pitch = this.pitch;
+ that.padding = this.padding;
+ return that;
}
isPaddingEqual(padding) {
diff --git a/test/src/utils/mapbox-gl-mock/util.js b/test/src/utils/mapbox-gl-mock/util.js
new file mode 100644
index 00000000..54ac8e65
--- /dev/null
+++ b/test/src/utils/mapbox-gl-mock/util.js
@@ -0,0 +1,25 @@
+// Generated with
+// flow-remove-types ./node_modules/mapbox-gl/src/util/util.js
+
+export function clamp(n, min, max) {
+ return Math.min(max, Math.max(min, n));
+}
+
+export function wrap(n, min, max) {
+ const d = max - min;
+ const w = ((((n - min) % d) + d) % d) + min;
+ return w === min ? max : w;
+}
+
+export function extend(dest, ...sources) {
+ for (const src of sources) {
+ for (const k in src) {
+ dest[k] = src[k];
+ }
+ }
+ return dest;
+}
+
+export function number(a, b, t) {
+ return a * (1 - t) + b * t;
+}
diff --git a/test/src/utils/style-utils.spec.js b/test/src/utils/style-utils.spec.js
new file mode 100644
index 00000000..2706536c
--- /dev/null
+++ b/test/src/utils/style-utils.spec.js
@@ -0,0 +1,213 @@
+import test from 'tape-promise/tape';
+
+import {normalizeStyle} from 'react-map-gl/utils/style-utils';
+
+const testStyle = {
+ version: 8,
+ name: 'Test',
+ sources: {
+ mapbox: {
+ url: 'mapbox://mapbox.mapbox-streets-v7',
+ type: 'vector'
+ }
+ },
+ sprite: 'mapbox://sprites/mapbox/basic-v8',
+ glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
+ layers: [
+ {
+ id: 'background',
+ type: 'background',
+ paint: {
+ 'background-color': '#dedede'
+ }
+ },
+ {
+ id: 'park',
+ type: 'fill',
+ source: 'mapbox',
+ 'source-layer': 'landuse_overlay',
+ filter: ['==', 'class', 'park'],
+ paint: {
+ 'fill-color': '#d2edae',
+ 'fill-opacity': 0.75
+ },
+ interactive: true
+ },
+ {
+ id: 'road',
+ source: 'mapbox',
+ 'source-layer': 'road',
+ layout: {
+ 'line-cap': 'butt',
+ 'line-join': 'miter'
+ },
+ filter: ['all', ['==', '$type', 'LineString']],
+ type: 'line',
+ paint: {
+ 'line-color': '#efefef',
+ 'line-width': {
+ base: 1.55,
+ stops: [
+ [4, 0.25],
+ [20, 30]
+ ]
+ }
+ },
+ minzoom: 5,
+ maxzoom: 20,
+ interactive: true
+ },
+ {
+ id: 'park-2',
+ ref: 'park',
+ paint: {
+ 'fill-color': '#00f080',
+ 'fill-opacity': 0.5
+ }
+ },
+ {
+ id: 'road-outline',
+ ref: 'road',
+ minzoom: 10,
+ maxzoom: 12,
+ paint: {
+ 'line-color': '#efefef',
+ 'line-width': {
+ base: 2,
+ stops: [
+ [4, 0.5],
+ [20, 40]
+ ]
+ }
+ }
+ }
+ ]
+};
+
+const expectedStyle = {
+ version: 8,
+ name: 'Test',
+ sources: {
+ mapbox: {
+ url: 'mapbox://mapbox.mapbox-streets-v7',
+ type: 'vector'
+ }
+ },
+ sprite: 'mapbox://sprites/mapbox/basic-v8',
+ glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
+ layers: [
+ {
+ id: 'background',
+ type: 'background',
+ paint: {
+ 'background-color': '#dedede'
+ }
+ },
+ {
+ id: 'park',
+ type: 'fill',
+ source: 'mapbox',
+ 'source-layer': 'landuse_overlay',
+ filter: ['==', 'class', 'park'],
+ paint: {
+ 'fill-color': '#d2edae',
+ 'fill-opacity': 0.75
+ }
+ },
+ {
+ id: 'road',
+ source: 'mapbox',
+ 'source-layer': 'road',
+ layout: {
+ 'line-cap': 'butt',
+ 'line-join': 'miter'
+ },
+ filter: ['all', ['==', '$type', 'LineString']],
+ type: 'line',
+ paint: {
+ 'line-color': '#efefef',
+ 'line-width': {
+ base: 1.55,
+ stops: [
+ [4, 0.25],
+ [20, 30]
+ ]
+ }
+ },
+ minzoom: 5,
+ maxzoom: 20
+ },
+ {
+ id: 'park-2',
+ type: 'fill',
+ source: 'mapbox',
+ 'source-layer': 'landuse_overlay',
+ filter: ['==', 'class', 'park'],
+ paint: {
+ 'fill-color': '#00f080',
+ 'fill-opacity': 0.5
+ }
+ },
+ {
+ id: 'road-outline',
+ source: 'mapbox',
+ 'source-layer': 'road',
+ layout: {
+ 'line-cap': 'butt',
+ 'line-join': 'miter'
+ },
+ filter: ['all', ['==', '$type', 'LineString']],
+ type: 'line',
+ minzoom: 5,
+ maxzoom: 20,
+ paint: {
+ 'line-color': '#efefef',
+ 'line-width': {
+ base: 2,
+ stops: [
+ [4, 0.5],
+ [20, 40]
+ ]
+ }
+ }
+ }
+ ]
+};
+
+test('normalizeStyle', t => {
+ // Make sure the style is not mutated
+ freezeRecursive(testStyle);
+
+ t.is(normalizeStyle(null), null, 'Handles null');
+ t.is(
+ normalizeStyle('mapbox://styles/mapbox/light-v9'),
+ 'mapbox://styles/mapbox/light-v9',
+ 'Handles url string'
+ );
+
+ let result = normalizeStyle(testStyle);
+ t.notEqual(result, testStyle, 'style is not mutated');
+ t.deepEqual(result, expectedStyle, 'plain object style is normalized');
+
+ // Immutable-like object
+ result = normalizeStyle({toJS: () => testStyle});
+ t.deepEqual(result, expectedStyle, 'immutable style is normalized');
+
+ t.end();
+});
+
+function freezeRecursive(obj) {
+ if (!obj) return;
+ if (typeof obj === 'object') {
+ if (Array.isArray(obj)) {
+ for (const el of obj) {
+ freezeRecursive(el);
+ }
+ } else {
+ for (const key in obj) {
+ freezeRecursive(obj[key]);
+ }
+ }
+ Object.freeze(obj);
+ }
+}
diff --git a/test/src/utils/test-utils.js b/test/src/utils/test-utils.js
new file mode 100644
index 00000000..fc8e0416
--- /dev/null
+++ b/test/src/utils/test-utils.js
@@ -0,0 +1,17 @@
+import * as React from 'react';
+
+/* global setTimeout */
+export function sleep(milliseconds) {
+ return new Promise(resolve => setTimeout(resolve, milliseconds));
+}
+
+export function createPortalMock() {
+ const reactDom = require('react-dom');
+ const createPortal = reactDom.createPortal;
+ reactDom.createPortal = function mockCreatePortal(content, container) {
+ return {content}
;
+ };
+ return () => {
+ reactDom.createPortal = createPortal;
+ };
+}
diff --git a/test/src/utils/transform.spec.js b/test/src/utils/transform.spec.js
index c47e7a61..28876a77 100644
--- a/test/src/utils/transform.spec.js
+++ b/test/src/utils/transform.spec.js
@@ -1,7 +1,7 @@
import test from 'tape-promise/tape';
import {transformToViewState, applyViewStateToTransform} from 'react-map-gl/utils/transform';
-import Transform from './transform';
+import Transform from './mapbox-gl-mock/transform';
test('applyViewStateToTransform', t => {
const tr = new Transform();
diff --git a/webpack.config.js b/webpack.config.js
index fb5e513b..c5e04396 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -14,7 +14,11 @@ module.exports = env => {
loader: 'babel-loader',
exclude: [/node_modules/],
options: {
- presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'],
+ presets: [
+ ['@babel/preset-env', {targets: 'last 2 chrome versions'}],
+ '@babel/preset-react',
+ '@babel/preset-typescript'
+ ],
plugins: ['@babel/plugin-proposal-class-properties']
}
}
diff --git a/yarn.lock b/yarn.lock
index 78ff80d5..64693bdf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3389,9 +3389,9 @@ camelcase@^5.0.0, camelcase@^5.3.1:
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
caniuse-lite@^1.0.30001286:
- version "1.0.30001295"
- resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001295.tgz"
- integrity sha512-lSP16vcyC0FEy0R4ECc9duSPoKoZy+YkpGkue9G4D81OfPnliopaZrU10+qtPdT8PbGXad/PNx43TIQrOmJZSQ==
+ version "1.0.30001296"
+ resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001296.tgz"
+ integrity sha512-WfrtPEoNSoeATDlf4y3QvkwiELl9GyPLISV5GejTbbQRtQx4LhsXmc9IQ6XCL2d7UxCyEzToEZNMeqR79OUw8Q==
caseless@~0.12.0:
version "0.12.0"