[v7] Tests (#1677)

This commit is contained in:
Xiaoji Chen 2022-01-06 23:56:03 -08:00 committed by Xiaoji Chen
parent 86f364a41e
commit 100b6ead5d
34 changed files with 1960 additions and 180 deletions

View File

@ -1,2 +1,3 @@
dist/
node_modules/
test/src/utils/mapbox-gl-mock/*.js

View File

@ -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;

View File

@ -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');

View File

@ -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",

View File

@ -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(`<!DOCTYPE html><div id="map"></div>`);
/* global global */
global.document = dom.window.document;
require('./src');

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -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(
<Component width="100%" height="100%" {...props} onLoad={onLoad} onError={onError} />,
container
);
render(<Component {...props} onLoad={onLoad} onError={onError} />, 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;

View File

@ -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: (
<div style={{position: 'absolute', left: 10, top: 10}}>
<NavigationControl />
</div>
)
children: <NavigationControl position="top-left" />
},
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: (
<GeolocateControl
position="top-left"
positionOptions={{enableHighAccuracy: true}}
trackUserLocation={true}
/>
)
},
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: [
<Marker key="0" longitude={-122.4} latitude={37.78} />,
<Marker key="1" longitude={-122.41} latitude={37.779} anchor="bottom">
<svg height={24} viewBox="0 0 24 24" style={{fill: '#d00', stroke: 'none'}}>
<path
d={`M20.2,15.7L20.2,15.7c1.1-1.6,1.8-3.6,1.8-5.7c0-5.6-4.5-10-10-10S2,4.5,2,10c0,2,0.6,3.9,1.6,5.4c0,0.1,0.1,0.2,0.2,0.3
c0,0,0.1,0.1,0.1,0.2c0.2,0.3,0.4,0.6,0.7,0.9c2.6,3.1,7.4,7.6,7.4,7.6s4.8-4.5,7.4-7.5c0.2-0.3,0.5-0.6,0.7-0.9
C20.1,15.8,20.2,15.8,20.2,15.7z`}
/>
</svg>
</Marker>
]
},
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: [
<Source
@ -131,13 +155,11 @@ export default [
},
{
title: 'JSX Source/Layer toggle style',
Component: StaticMap,
props: {
mapboxApiAccessToken: __MAPBOX_TOKEN__,
mapboxAccessToken: __MAPBOX_TOKEN__,
mapStyle: ALT_EMPTY_MAP_STYLE,
longitude: -122.4,
latitude: 37.78,
reuseMaps: true,
zoom: 12.5,
children: [
<Source
@ -153,37 +175,14 @@ export default [
},
{
title: 'JSX Source/Layer removal',
Component: StaticMap,
props: {
mapboxApiAccessToken: __MAPBOX_TOKEN__,
mapboxAccessToken: __MAPBOX_TOKEN__,
mapStyle: ALT_EMPTY_MAP_STYLE,
longitude: -122.4,
latitude: 37.78,
reuseMaps: true,
zoom: 12.5,
children: []
},
goldenImage: 'test/render/golden-images/alt-empty-map.png'
},
{
title: 'GeolocateControl',
Component: StaticMap,
props: {
mapboxApiAccessToken: __MAPBOX_TOKEN__,
mapStyle: EMPTY_MAP_STYLE,
longitude: -122.4,
latitude: 37.78,
zoom: 12.5,
reuseMaps: true,
bearing: 30,
children: (
<GeolocateControl
style={{position: 'absolute', left: 10, top: 10}}
positionOptions={{enableHighAccuracy: true}}
trackUserLocation={true}
/>
)
},
goldenImage: 'test/render/golden-images/geolocate-control.png'
}
].filter(testCase => testCase.props.mapboxApiAccessToken);
].filter(testCase => testCase.props.mapboxAccessToken);

View File

@ -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(<Map />);
renderer.update(
<Map>
<AttributionControl />
</Map>
);
t.ok(renderer.root, 'Rendered <AttributionControl />');
renderer.update(
<Map>
<FullscreenControl />
</Map>
);
t.ok(renderer.root, 'Rendered <FullscreenControl />');
renderer.update(
<Map>
<GeolocateControl />
</Map>
);
t.ok(renderer.root, 'Rendered <GeolocateControl />');
renderer.update(
<Map>
<NavigationControl />
</Map>
);
t.ok(renderer.root, 'Rendered <NavigationControl />');
renderer.update(
<Map>
<ScaleControl />
</Map>
);
t.ok(renderer.root, 'Rendered <ScaleControl />');
renderer.unmount();
t.end();
});

View File

@ -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(
<Map ref={mapRef}>
<Source id="my-data" type="geojson" data={geoJSON}>
<Layer id="my-layer" {...pointLayer} />
</Source>
</Map>
);
});
await sleep(5);
t.ok(mapRef.current.getLayer('my-layer'), 'Layer is added');
act(() =>
map.update(
<Map ref={mapRef}>
<Source id="my-data" type="geojson" data={geoJSON}>
<Layer id="my-layer" {...pointLayer2} />
</Source>
</Map>
)
);
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(
<Map ref={mapRef} mapStyle={mapStyle}>
<Source id="my-data" type="geojson" data={geoJSON}>
<Layer id="my-layer" {...pointLayer2} />
</Source>
</Map>
)
);
await sleep(5);
t.ok(mapRef.current.getLayer('my-layer'), 'Layer is added after style change');
act(() => map.update(<Map ref={mapRef} mapStyle={mapStyle} />));
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();
});

View File

@ -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 = <Map />;
const result = ReactTestUtils.createRenderer().render(map);
let onloadCalled = 0;
const onLoad = () => onloadCalled++;
let map;
act(() => {
map = create(
<Map
ref={mapRef}
initialViewState={{longitude: -100, latitude: 40, zoom: 4}}
onLoad={onLoad}
/>
);
});
t.ok(map.root, 'Rendered <Map />');
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(<Map ref={mapRef} longitude={-122} latitude={38} zoom={14} onLoad={onLoad} />);
});
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(
<Map
initialViewState={{longitude: -100, latitude: 40, zoom: 4}}
onLoad={e => {
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(
<Map
longitude={-100}
latitude={40}
zoom={4}
onLoad={e => {
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 (
<Map
{...viewState}
onLoad={e => {
e.target.easeTo({center: [-122, 38], zoom: 14});
}}
onMove={e => setViewState(e.viewState)}
onRender={onRender}
/>
);
}
act(() => {
create(<App />);
});
});
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 (
<Map
{...viewState}
onLoad={e => {
e.target.easeTo({center: [-122, 38], zoom: 14});
}}
onMove={e => setTimeout(() => setViewState(e.viewState))}
onRender={onRender}
/>
);
}
act(() => {
create(<App />);
});
});

View File

@ -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(
<Map ref={mapRef}>
<Marker longitude={-122} latitude={38} />
</Map>
);
});
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(
<Map ref={mapRef}>
<Marker longitude={-122} latitude={38} offset={[0, 0]} />
</Map>
);
});
t.is(offset, marker.getOffset(), 'offset did not change deeply');
let callbackType = '';
act(() => {
map.update(
<Map ref={mapRef}>
<Marker
longitude={-122}
latitude={38}
offset={[0, 1]}
rotation={30}
draggable
pitchAlignment="viewport"
rotationAlignment="viewport"
onDragStart={() => (callbackType = 'dragstart')}
onDrag={() => (callbackType = 'drag')}
onDragEnd={() => (callbackType = 'dragend')}
/>
</Map>
);
});
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(<Map ref={mapRef} />);
});
t.is(mapRef.current.getMap()._markers.length, 0, 'marker is removed');
act(() => {
map.update(
<Map ref={mapRef}>
<Marker longitude={-100} latitude={40}>
<div id="marker-content" />
</Marker>
</Map>
);
});
t.ok(map.root.findByProps({id: 'marker-content'}), 'content is rendered');
map.unmount();
restoreMock();
t.end();
});

View File

@ -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(
<Map ref={mapRef}>
<Popup longitude={-122} latitude={38} offset={[0, 10]}>
You are here
</Popup>
</Map>
);
});
const popup = mapRef.current.getMap()._markers[0];
t.ok(popup, 'Popup is created');
const {anchor, offset, maxWidth} = popup.options;
act(() => {
map.update(
<Map ref={mapRef}>
<Popup longitude={-122} latitude={38} offset={[0, 10]}>
You are here
</Popup>
</Map>
);
});
t.is(offset, popup.options.offset, 'offset did not change deeply');
act(() => {
map.update(
<Map ref={mapRef}>
<Popup
longitude={-122}
latitude={38}
offset={{top: [0, 0], left: [10, 0]}}
anchor="top"
maxWidth="100px"
>
You are here
</Popup>
</Map>
);
});
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();
});

View File

@ -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(
<Map ref={mapRef}>
<Source id="my-data" type="geojson" data={geoJSON} />
</Map>
);
});
await sleep(5);
t.ok(mapRef.current.getSource('my-data'), 'Source is added');
act(() =>
map.update(
<Map ref={mapRef} mapStyle={mapStyle}>
<Source id="my-data" type="geojson" data={geoJSON2} />
</Map>
)
);
await sleep(5);
t.ok(mapRef.current.getSource('my-data'), 'Source is added after style change');
act(() =>
map.update(
<Map ref={mapRef} mapStyle={mapStyle}>
<Source id="my-data" type="geojson" data={geoJSON2} />
</Map>
)
);
t.is(mapRef.current.getSource('my-data').getData(), geoJSON2, 'Source is updated');
act(() => map.update(<Map ref={mapRef} mapStyle={mapStyle} />));
await sleep(5);
t.notOk(mapRef.current.getSource('my-data'), 'Source is removed');
map.unmount();
t.end();
});

View File

@ -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(
<MapProvider>
<Map id="mapA" />
<Map id="mapB" />
<TestControl />
</MapProvider>
);
});
t.ok(maps.mapA, 'Context has mapA');
t.ok(maps.mapB, 'Context has mapB');
act(() => {
app = create(
<MapProvider>
<Map id="mapA" />
<TestControl />
</MapProvider>
);
});
t.ok(maps.mapA, 'Context has mapA');
t.notOk(maps.mapB, 'mapB is removed');
act(() => {
app = create(
<MapProvider>
<TestControl />
</MapProvider>
);
});
t.notOk(maps.mapA, 'mapA is removed');
app.unmount();
t.end();
});

View File

@ -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';

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
}
};

View File

@ -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
};

View File

@ -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: <lng>, lat: <lat>}, an object {lon: <lng>, lat: <lat>}, or an array of [<lng>, <lat>]'
);
}
}
export default LngLat;

View File

@ -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;

View File

@ -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 = [];
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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) {

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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 <div>{content}</div>;
};
return () => {
reactDom.createPortal = createPortal;
};
}

View File

@ -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();

View File

@ -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']
}
}

View File

@ -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"