Added more functional tests.

This commit is contained in:
orangemug 2020-05-31 15:33:09 +01:00
parent 3d4cc34a08
commit 8d3ad6b1a1
19 changed files with 1085 additions and 91 deletions

View file

@ -0,0 +1,832 @@
import autoBind from 'react-autobind';
import React from 'react'
import cloneDeep from 'lodash.clonedeep'
import clamp from 'lodash.clamp'
import get from 'lodash.get'
import {unset} from 'lodash'
import {arrayMove} from 'react-sortable-hoc'
import url from 'url'
import MapboxGlMap from './map/MapboxGlMap'
import OpenLayersMap from './map/OpenLayersMap'
import LayerList from './layers/LayerList'
import LayerEditor from './layers/LayerEditor'
import Toolbar from './Toolbar'
import AppLayout from './AppLayout'
import MessagePanel from './MessagePanel'
import SettingsModal from './modals/SettingsModal'
import ExportModal from './modals/ExportModal'
import SourcesModal from './modals/SourcesModal'
import OpenModal from './modals/OpenModal'
import ShortcutsModal from './modals/ShortcutsModal'
import SurveyModal from './modals/SurveyModal'
import DebugModal from './modals/DebugModal'
import JSONEditorModal from './modals/JSONEditorModal'
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
import {latest, validate} from '@mapbox/mapbox-gl-style-spec'
import style from '../libs/style'
import { initialStyleUrl, loadStyleUrl, removeStyleQuerystring } from '../libs/urlopen'
import { undoMessages, redoMessages } from '../libs/diffmessage'
import { StyleStore } from '../libs/stylestore'
import { ApiStyleStore } from '../libs/apistore'
import { RevisionStore } from '../libs/revisions'
import LayerWatcher from '../libs/layerwatcher'
import tokens from '../config/tokens.json'
import isEqual from 'lodash.isequal'
import Debug from '../libs/debug'
import queryUtil from '../libs/query-util'
import MapboxGl from 'mapbox-gl'
// Similar functionality as <https://github.com/mapbox/mapbox-gl-js/blob/7e30aadf5177486c2cfa14fe1790c60e217b5e56/src/util/mapbox.js>
function normalizeSourceURL (url, apiToken="") {
const matches = url.match(/^mapbox:\/\/(.*)/);
if (matches) {
// mapbox://mapbox.mapbox-streets-v7
return `https://api.mapbox.com/v4/${matches[1]}.json?secure&access_token=${apiToken}`
}
else {
return url;
}
}
function setFetchAccessToken(url, mapStyle) {
const matchesTilehosting = url.match(/\.tilehosting\.com/);
const matchesMaptiler = url.match(/\.maptiler\.com/);
const matchesThunderforest = url.match(/\.thunderforest\.com/);
if (matchesTilehosting || matchesMaptiler) {
const accessToken = style.getAccessToken("openmaptiles", mapStyle, {allowFallback: true})
if (accessToken) {
return url.replace('{key}', accessToken)
}
}
else if (matchesThunderforest) {
const accessToken = style.getAccessToken("thunderforest", mapStyle, {allowFallback: true})
if (accessToken) {
return url.replace('{key}', accessToken)
}
}
else {
return url;
}
}
function updateRootSpec(spec, fieldName, newValues) {
return {
...spec,
$root: {
...spec.$root,
[fieldName]: {
...spec.$root[fieldName],
values: newValues
}
}
}
}
export default class App extends React.Component {
constructor(props) {
super(props)
autoBind(this);
this.revisionStore = new RevisionStore()
const params = new URLSearchParams(window.location.search.substring(1))
let port = params.get("localport")
if (port == null && (window.location.port != 80 && window.location.port != 443)) {
port = window.location.port
}
this.styleStore = new ApiStyleStore({
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, {save: false}),
port: port,
host: params.get("localhost")
})
const shortcuts = [
{
key: "j",
handler: () => {
this.toggleModal("json_editor");
}
},
{
key: "?",
handler: () => {
this.toggleModal("shortcuts");
}
},
{
key: "o",
handler: () => {
this.toggleModal("open");
}
},
{
key: "e",
handler: () => {
this.toggleModal("export");
}
},
{
key: "d",
handler: () => {
this.toggleModal("sources");
}
},
{
key: "s",
handler: () => {
this.toggleModal("settings");
}
},
{
key: "i",
handler: () => {
this.setMapState(
this.state.mapState === "map" ? "inspect" : "map"
);
}
},
{
key: "m",
handler: () => {
document.querySelector(".mapboxgl-canvas").focus();
}
},
{
key: "!",
handler: () => {
this.toggleModal("debug");
}
},
]
document.body.addEventListener("keyup", (e) => {
if(e.key === "Escape") {
e.target.blur();
document.body.focus();
}
else if(this.state.isOpen.shortcuts || document.activeElement === document.body) {
const shortcut = shortcuts.find((shortcut) => {
return (shortcut.key === e.key)
})
if(shortcut) {
this.setModal("shortcuts", false);
shortcut.handler(e);
}
}
})
const styleUrl = initialStyleUrl()
if(styleUrl && window.confirm("Load style from URL: " + styleUrl + " and discard current changes?")) {
this.styleStore = new StyleStore()
loadStyleUrl(styleUrl, mapStyle => this.onStyleChanged(mapStyle))
removeStyleQuerystring()
} else {
if(styleUrl) {
removeStyleQuerystring()
}
this.styleStore.init(err => {
if(err) {
console.log('Falling back to local storage for storing styles')
this.styleStore = new StyleStore()
}
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle))
if(Debug.enabled()) {
Debug.set("maputnik", "styleStore", this.styleStore);
Debug.set("maputnik", "revisionStore", this.revisionStore);
}
})
}
if(Debug.enabled()) {
Debug.set("maputnik", "revisionStore", this.revisionStore);
Debug.set("maputnik", "styleStore", this.styleStore);
}
const queryObj = url.parse(window.location.href, true).query;
this.state = {
errors: [],
infos: [],
mapStyle: style.emptyStyle,
selectedLayerIndex: 0,
sources: {},
vectorLayers: {},
mapState: "map",
spec: latest,
mapView: {
zoom: 0,
center: {
lng: 0,
lat: 0,
},
},
isOpen: {
settings: false,
sources: false,
open: false,
shortcuts: false,
export: false,
// TODO: Disabled for now, this should be opened on the Nth visit to the editor
survey: false,
debug: false,
},
mapboxGlDebugOptions: {
showTileBoundaries: false,
showCollisionBoxes: false,
showOverdrawInspector: false,
},
openlayersDebugOptions: {
debugToolbox: false,
},
}
this.layerWatcher = new LayerWatcher({
onVectorLayersChange: v => this.setState({ vectorLayers: v })
})
}
handleKeyPress = (e) => {
if(navigator.platform.toUpperCase().indexOf('MAC') >= 0) {
if(e.metaKey && e.shiftKey && e.keyCode === 90) {
e.preventDefault();
this.onRedo(e);
}
else if(e.metaKey && e.keyCode === 90) {
e.preventDefault();
this.onUndo(e);
}
}
else {
if(e.ctrlKey && e.keyCode === 90) {
e.preventDefault();
this.onUndo(e);
}
else if(e.ctrlKey && e.keyCode === 89) {
e.preventDefault();
this.onRedo(e);
}
}
}
componentDidMount() {
window.addEventListener("keydown", this.handleKeyPress);
}
componentWillUnmount() {
window.removeEventListener("keydown", this.handleKeyPress);
}
saveStyle(snapshotStyle) {
this.styleStore.save(snapshotStyle)
}
updateFonts(urlTemplate) {
const metadata = this.state.mapStyle.metadata || {}
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
downloadGlyphsMetadata(glyphUrl, fonts => {
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
})
}
updateIcons(baseUrl) {
downloadSpriteMetadata(baseUrl, icons => {
this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)})
})
}
onChangeMetadataProperty = (property, value) => {
// If we're changing renderer reset the map state.
if (
property === 'maputnik:renderer' &&
value !== get(this.state.mapStyle, ['metadata', 'maputnik:renderer'], 'mbgljs')
) {
this.setState({
mapState: 'map'
});
}
const changedStyle = {
...this.state.mapStyle,
metadata: {
...this.state.mapStyle.metadata,
[property]: value
}
}
this.onStyleChanged(changedStyle)
}
onStyleChanged = (newStyle, opts={}) => {
opts = {
save: true,
addRevision: true,
...opts,
};
const errors = validate(newStyle, latest) || [];
// The validate function doesn't give us errors for duplicate error with
// empty string for layer.id, manually deal with that here.
const layerErrors = [];
if (newStyle && newStyle.layers) {
const foundLayers = new Map();
newStyle.layers.forEach((layer, index) => {
if (layer.id === "" && foundLayers.has(layer.id)) {
const message = `Duplicate layer: [empty string]`;
layerErrors.push({
message,
parsed: {
type: "layer",
data: {
index,
message,
}
}
});
}
foundLayers.set(layer.id, true);
});
}
const mappedErrors = errors.map(error => {
const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
if (layerMatch) {
const [matchStr, index, group, property, message] = layerMatch;
const key = (group && property) ? [group, property].join(".") : property;
return {
message: error.message,
parsed: {
type: "layer",
data: {
index,
key,
message
}
}
}
}
else {
return {
message: error.message,
};
}
}).concat(layerErrors);
let dirtyMapStyle = undefined;
if (errors.length > 0) {
dirtyMapStyle = cloneDeep(newStyle);
errors.forEach(error => {
const {message} = error;
if (message) {
try {
const objPath = message.split(":")[0];
// Errors can be deply nested for example 'layers[0].filter[1][1][0]' we only care upto the property 'layers[0].filter'
const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^\[]+/)[0];
unset(dirtyMapStyle, unsetPath);
}
catch (err) {
console.warn(err);
}
}
});
}
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
this.updateFonts(newStyle.glyphs)
}
if(newStyle.sprite !== this.state.mapStyle.sprite) {
this.updateIcons(newStyle.sprite)
}
if (opts.addRevision) {
this.revisionStore.addRevision(newStyle);
}
if (opts.save) {
this.saveStyle(newStyle);
}
this.setState({
mapStyle: newStyle,
dirtyMapStyle: dirtyMapStyle,
errors: mappedErrors,
})
this.fetchSources();
}
onUndo = () => {
const activeStyle = this.revisionStore.undo()
const messages = undoMessages(this.state.mapStyle, activeStyle)
this.onStyleChanged(activeStyle, {addRevision: false});
this.setState({
infos: messages,
})
}
onRedo = () => {
const activeStyle = this.revisionStore.redo()
const messages = redoMessages(this.state.mapStyle, activeStyle)
this.onStyleChanged(activeStyle, {addRevision: false});
this.setState({
infos: messages,
})
}
onMoveLayer = (move) => {
let { oldIndex, newIndex } = move;
let layers = this.state.mapStyle.layers;
oldIndex = clamp(oldIndex, 0, layers.length-1);
newIndex = clamp(newIndex, 0, layers.length-1);
if(oldIndex === newIndex) return;
if (oldIndex === this.state.selectedLayerIndex) {
this.setState({
selectedLayerIndex: newIndex
});
}
layers = layers.slice(0);
layers = arrayMove(layers, oldIndex, newIndex);
this.onLayersChange(layers);
}
onLayersChange = (changedLayers) => {
const changedStyle = {
...this.state.mapStyle,
layers: changedLayers
}
this.onStyleChanged(changedStyle)
}
onLayerDestroy = (index) => {
let layers = this.state.mapStyle.layers;
const remainingLayers = layers.slice(0);
remainingLayers.splice(index, 1);
this.onLayersChange(remainingLayers);
}
onLayerCopy = (index) => {
let layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0)
const clonedLayer = cloneDeep(changedLayers[index])
clonedLayer.id = clonedLayer.id + "-copy"
changedLayers.splice(index, 0, clonedLayer)
this.onLayersChange(changedLayers)
}
onLayerVisibilityToggle = (index) => {
let layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0)
const layer = { ...changedLayers[index] }
const changedLayout = 'layout' in layer ? {...layer.layout} : {}
changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none'
layer.layout = changedLayout
changedLayers[index] = layer
this.onLayersChange(changedLayers)
}
onLayerIdChange = (index, oldId, newId) => {
const changedLayers = this.state.mapStyle.layers.slice(0)
changedLayers[index] = {
...changedLayers[index],
id: newId
}
this.onLayersChange(changedLayers)
}
onLayerChanged = (index, layer) => {
const changedLayers = this.state.mapStyle.layers.slice(0)
changedLayers[index] = layer
this.onLayersChange(changedLayers)
}
setMapState = (newState) => {
this.setState({
mapState: newState
})
}
setDefaultValues = (styleObj) => {
const metadata = styleObj.metadata || {}
if(metadata['maputnik:renderer'] === undefined) {
const changedStyle = {
...styleObj,
metadata: {
...styleObj.metadata,
'maputnik:renderer': 'mbgljs'
}
}
return changedStyle
} else {
return styleObj
}
}
openStyle = (styleObj) => {
styleObj = this.setDefaultValues(styleObj)
this.onStyleChanged(styleObj)
}
fetchSources() {
const sourceList = {...this.state.sources};
for(let [key, val] of Object.entries(this.state.mapStyle.sources)) {
if(sourceList.hasOwnProperty(key)) {
continue;
}
sourceList[key] = {
type: val.type,
layers: []
};
if(!this.state.sources.hasOwnProperty(key) && val.type === "vector" && val.hasOwnProperty("url")) {
let url = val.url;
try {
url = normalizeSourceURL(url, MapboxGl.accessToken);
} catch(err) {
console.warn("Failed to normalizeSourceURL: ", err);
}
try {
url = setFetchAccessToken(url, this.state.mapStyle)
} catch(err) {
console.warn("Failed to setFetchAccessToken: ", err);
}
fetch(url, {
mode: 'cors',
})
.then((response) => {
return response.json();
})
.then((json) => {
if(!json.hasOwnProperty("vector_layers")) {
return;
}
// Create new objects before setState
const sources = Object.assign({}, this.state.sources);
for(let layer of json.vector_layers) {
sources[key].layers.push(layer.id)
}
console.debug("Updating source: "+key);
this.setState({
sources: sources
});
})
.catch((err) => {
console.error("Failed to process sources for '%s'", url, err);
})
}
}
if(!isEqual(this.state.sources, sourceList)) {
console.debug("Setting sources");
this.setState({
sources: sourceList
})
}
}
_getRenderer () {
const metadata = this.state.mapStyle.metadata || {};
return metadata['maputnik:renderer'] || 'mbgljs';
}
onMapChange = (mapView) => {
this.setState({
mapView,
});
}
mapRenderer() {
const {mapStyle, dirtyMapStyle} = this.state;
const metadata = this.state.mapStyle.metadata || {};
const mapProps = {
mapStyle: (dirtyMapStyle || mapStyle),
replaceAccessTokens: (mapStyle) => {
return style.replaceAccessTokens(mapStyle, {
allowFallback: true
});
},
onDataChange: (e) => {
this.layerWatcher.analyzeMap(e.map)
this.fetchSources();
},
}
const renderer = this._getRenderer();
let mapElement;
// Check if OL code has been loaded?
if(renderer === 'ol') {
mapElement = <OpenLayersMap
{...mapProps}
onChange={this.onMapChange}
debugToolbox={this.state.openlayersDebugOptions.debugToolbox}
onLayerSelect={this.onLayerSelect}
/>
} else {
mapElement = <MapboxGlMap {...mapProps}
onChange={this.onMapChange}
options={this.state.mapboxGlDebugOptions}
inspectModeEnabled={this.state.mapState === "inspect"}
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
onLayerSelect={this.onLayerSelect} />
}
let filterName;
if(this.state.mapState.match(/^filter-/)) {
filterName = this.state.mapState.replace(/^filter-/, "");
}
const elementStyle = {};
if (filterName) {
elementStyle.filter = `url('#${filterName}')`;
};
return <div style={elementStyle} className="maputnik-map__container">
{mapElement}
</div>
}
onLayerSelect = (index) => {
this.setState({ selectedLayerIndex: index })
}
setModal(modalName, value) {
if(modalName === 'survey' && value === false) {
localStorage.setItem('survey', '');
}
this.setState({
isOpen: {
...this.state.isOpen,
[modalName]: value
}
})
}
toggleModal(modalName) {
this.setModal(modalName, !this.state.isOpen[modalName]);
}
onChangeOpenlayersDebug = (key, value) => {
this.setState({
openlayersDebugOptions: {
...this.state.openlayersDebugOptions,
[key]: value,
}
});
}
onChangeMaboxGlDebug = (key, value) => {
this.setState({
mapboxGlDebugOptions: {
...this.state.mapboxGlDebugOptions,
[key]: value,
}
});
}
render() {
const layers = this.state.mapStyle.layers || []
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null
const metadata = this.state.mapStyle.metadata || {}
const toolbar = <Toolbar
renderer={this._getRenderer()}
mapState={this.state.mapState}
mapStyle={this.state.mapStyle}
inspectModeEnabled={this.state.mapState === "inspect"}
sources={this.state.sources}
onStyleChanged={this.onStyleChanged}
onStyleOpen={this.onStyleChanged}
onSetMapState={this.setMapState}
onToggleModal={this.toggleModal.bind(this)}
/>
const layerList = <LayerList
onMoveLayer={this.onMoveLayer}
onLayerDestroy={this.onLayerDestroy}
onLayerCopy={this.onLayerCopy}
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
onLayersChange={this.onLayersChange}
onLayerSelect={this.onLayerSelect}
selectedLayerIndex={this.state.selectedLayerIndex}
layers={layers}
sources={this.state.sources}
errors={this.state.errors}
/>
const layerEditor = selectedLayer ? <LayerEditor
key={selectedLayer.id}
layer={selectedLayer}
layerIndex={this.state.selectedLayerIndex}
isFirstLayer={this.state.selectedLayerIndex < 1}
isLastLayer={this.state.selectedLayerIndex === this.state.mapStyle.layers.length-1}
sources={this.state.sources}
vectorLayers={this.state.vectorLayers}
spec={this.state.spec}
onMoveLayer={this.onMoveLayer}
onLayerChanged={this.onLayerChanged}
onLayerDestroy={this.onLayerDestroy}
onLayerCopy={this.onLayerCopy}
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
onLayerIdChange={this.onLayerIdChange}
errors={this.state.errors}
/> : null
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
currentLayer={selectedLayer}
onLayerSelect={this.onLayerSelect}
mapStyle={this.state.mapStyle}
errors={this.state.errors}
infos={this.state.infos}
/> : null
const modals = <div>
<DebugModal
renderer={this._getRenderer()}
mapboxGlDebugOptions={this.state.mapboxGlDebugOptions}
openlayersDebugOptions={this.state.openlayersDebugOptions}
onChangeMaboxGlDebug={this.onChangeMaboxGlDebug}
onChangeOpenlayersDebug={this.onChangeOpenlayersDebug}
isOpen={this.state.isOpen.debug}
onOpenToggle={this.toggleModal.bind(this, 'debug')}
mapView={this.state.mapView}
/>
<ShortcutsModal
ref={(el) => this.shortcutEl = el}
isOpen={this.state.isOpen.shortcuts}
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
/>
<SettingsModal
mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged}
onChangeMetadataProperty={this.onChangeMetadataProperty}
isOpen={this.state.isOpen.settings}
onOpenToggle={this.toggleModal.bind(this, 'settings')}
openlayersDebugOptions={this.state.openlayersDebugOptions}
/>
<JSONEditorModal
title="JSON Editor"
mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged}
isOpen={this.state.isOpen.json_editor}
onOpenToggle={this.toggleModal.bind(this, 'json_editor')}
/>
<ExportModal
mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged}
isOpen={this.state.isOpen.export}
onOpenToggle={this.toggleModal.bind(this, 'export')}
/>
<OpenModal
isOpen={this.state.isOpen.open}
onStyleOpen={this.openStyle}
onOpenToggle={this.toggleModal.bind(this, 'open')}
/>
<SourcesModal
mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged}
isOpen={this.state.isOpen.sources}
onOpenToggle={this.toggleModal.bind(this, 'sources')}
/>
<SurveyModal
isOpen={this.state.isOpen.survey}
onOpenToggle={this.toggleModal.bind(this, 'survey')}
/>
</div>
return <AppLayout
toolbar={toolbar}
layerList={layerList}
layerEditor={layerEditor}
map={this.mapRenderer()}
bottom={bottomPanel}
modals={modals}
/>
}
}

View file

@ -185,18 +185,21 @@ export default class Toolbar extends React.Component {
>
{/* Keyboard accessible quick links */}
<button
data-wd-key="root:skip:layer-list"
className="maputnik-toolbar-skip"
onClick={e => this.onSkip("layer-list")}
>
Layers list
</button>
<button
data-wd-key="root:skip:layer-editor"
className="maputnik-toolbar-skip"
onClick={e => this.onSkip("layer-editor")}
>
Layer editor
</button>
<button
data-wd-key="root:skip:map-view"
className="maputnik-toolbar-skip"
onClick={e => this.onSkip("map")}
>

View file

@ -24,7 +24,7 @@ class DebugModal extends React.Component {
const osmLat = Number.parseFloat(mapView.center.lat).toFixed(5);
return <Modal
data-wd-key="debug-modal"
data-wd-key="modal:debug"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Debug'}

View file

@ -61,7 +61,7 @@ class ExportModal extends React.Component {
render() {
return <Modal
data-wd-key="export-modal"
data-wd-key="modal:export"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Export Style'}

View file

@ -20,7 +20,7 @@ class LoadingModal extends React.Component {
render() {
return <Modal
data-wd-key="loading-modal"
data-wd-key="modal:loading"
isOpen={this.props.isOpen}
underlayClickExits={false}
underlayProps={{

View file

@ -191,7 +191,7 @@ class OpenModal extends React.Component {
return (
<div>
<Modal
data-wd-key="open-modal"
data-wd-key="modal:open"
isOpen={this.props.isOpen}
onOpenToggle={() => this.onOpenToggle()}
title={'Open Style'}
@ -212,7 +212,7 @@ class OpenModal extends React.Component {
</p>
<form onSubmit={this.onSubmitUrl}>
<UrlInput
data-wd-key="open-modal.url.input"
data-wd-key="modal:open.url.input"
type="text"
className="maputnik-input"
default="Enter URL..."
@ -222,7 +222,7 @@ class OpenModal extends React.Component {
/>
<div>
<Button
data-wd-key="open-modal.url.button"
data-wd-key="modal:open.url.button"
type="submit"
className="maputnik-big-button"
disabled={this.state.styleUrl.length < 1}

View file

@ -81,29 +81,29 @@ class SettingsModal extends React.Component {
const transition = this.props.mapStyle.transition || {};
return <Modal
data-wd-key="modal-settings"
data-wd-key="modal:settings"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Style Settings'}
>
<div className="modal-settings">
<div className="modal:settings">
<InputBlock label={"Name"} fieldSpec={latest.$root.name}>
<StringInput {...inputProps}
data-wd-key="modal-settings.name"
data-wd-key="modal:settings.name"
value={this.props.mapStyle.name}
onChange={this.changeStyleProperty.bind(this, "name")}
/>
</InputBlock>
<InputBlock label={"Owner"} fieldSpec={{doc: "Owner ID of the style. Used by Mapbox or future style APIs."}}>
<StringInput {...inputProps}
data-wd-key="modal-settings.owner"
data-wd-key="modal:settings.owner"
value={this.props.mapStyle.owner}
onChange={this.changeStyleProperty.bind(this, "owner")}
/>
</InputBlock>
<InputBlock label={"Sprite URL"} fieldSpec={latest.$root.sprite}>
<UrlInput {...inputProps}
data-wd-key="modal-settings.sprite"
data-wd-key="modal:settings.sprite"
value={this.props.mapStyle.sprite}
onChange={this.changeStyleProperty.bind(this, "sprite")}
/>
@ -111,7 +111,7 @@ class SettingsModal extends React.Component {
<InputBlock label={"Glyphs URL"} fieldSpec={latest.$root.glyphs}>
<UrlInput {...inputProps}
data-wd-key="modal-settings.glyphs"
data-wd-key="modal:settings.glyphs"
value={this.props.mapStyle.glyphs}
onChange={this.changeStyleProperty.bind(this, "glyphs")}
/>
@ -122,7 +122,7 @@ class SettingsModal extends React.Component {
fieldSpec={fieldSpecAdditional.maputnik.mapbox_access_token}
>
<StringInput {...inputProps}
data-wd-key="modal-settings.maputnik:mapbox_access_token"
data-wd-key="modal:settings.maputnik:mapbox_access_token"
value={metadata['maputnik:mapbox_access_token']}
onChange={onChangeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}
/>
@ -133,7 +133,7 @@ class SettingsModal extends React.Component {
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
>
<StringInput {...inputProps}
data-wd-key="modal-settings.maputnik:openmaptiles_access_token"
data-wd-key="modal:settings.maputnik:openmaptiles_access_token"
value={metadata['maputnik:openmaptiles_access_token']}
onChange={onChangeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
/>
@ -144,7 +144,7 @@ class SettingsModal extends React.Component {
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
>
<StringInput {...inputProps}
data-wd-key="modal-settings.maputnik:thunderforest_access_token"
data-wd-key="modal:settings.maputnik:thunderforest_access_token"
value={metadata['maputnik:thunderforest_access_token']}
onChange={onChangeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
/>
@ -250,7 +250,7 @@ class SettingsModal extends React.Component {
fieldSpec={fieldSpecAdditional.maputnik.style_renderer}
>
<SelectInput {...inputProps}
data-wd-key="modal-settings.maputnik:renderer"
data-wd-key="modal:settings.maputnik:renderer"
options={[
['mbgljs', 'MapboxGL JS'],
['ol', 'Open Layers (experimental)'],

View file

@ -100,7 +100,7 @@ class ShortcutsModal extends React.Component {
return <Modal
data-wd-key="shortcuts-modal"
data-wd-key="modal:shortcuts"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Shortcuts'}

View file

@ -286,6 +286,7 @@ class SourcesModal extends React.Component {
const inputProps = { }
return <Modal
data-wd-key="modal:sources"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Sources'}

View file

@ -20,7 +20,7 @@ class SurveyModal extends React.Component {
render() {
return <Modal
data-wd-key="modal-survey"
data-wd-key="modal:survey"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title="Maputnik Survey"

View file

@ -0,0 +1,18 @@
{
"id": "test-style",
"version": 8,
"name": "Test Style",
"metadata": {
"maputnik:renderer": "mbgljs"
},
"sources": {},
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": [
{
"id": "background",
"type": "background"
}
]
}

View file

@ -0,0 +1,3 @@
describe("accessibility", function () {
require("./skip-links");
})

View file

@ -0,0 +1,51 @@
var assert = require("assert");
var config = require("../../config/specs");
var helper = require("../helper");
var wd = require("../../wd-helper");
describe("skip links", function() {
beforeEach(function () {
browser.url(config.baseUrl+"?debug&style="+helper.getGeoServerUrl("example-layer-style.json"));
browser.acceptAlert();
});
it("skip link to layer list", function() {
const selector = wd.$("root:skip:layer-list")
const elem = $(selector);
assert(elem.isExisting());
browser.keys(['Tab']);
assert(elem.isFocused());
elem.click();
const targetEl = $("#skip-target-layer-list");
assert(targetEl.isFocused());
});
it("skip link to layer editor", function() {
const selector = wd.$("root:skip:layer-editor")
const elem = $(selector);
assert(elem.isExisting());
browser.keys(['Tab']);
browser.keys(['Tab']);
assert(elem.isFocused());
elem.click();
const targetEl = $("#skip-target-layer-editor");
assert(targetEl.isFocused());
});
it("skip link to map view", function() {
const selector = wd.$("root:skip:map-view")
const elem = $(selector);
assert(elem.isExisting());
browser.keys(['Tab']);
browser.keys(['Tab']);
browser.keys(['Tab']);
assert(elem.isFocused());
elem.click();
const targetEl = $(".mapboxgl-canvas");
assert(targetEl.isFocused());
});
});

View file

@ -3,7 +3,23 @@ var config = require("../../config/specs");
var helper = require("../helper");
describe.skip("history", function() {
describe("history", function() {
let undoKeyCombo;
let undoKeyComboReset;
let redoKeyCombo;
let redoKeyComboReset;
before(function() {
const isMac = browser.execute(function() {
return navigator.platform.toUpperCase().indexOf('MAC') >= 0;
});
undoKeyCombo = ['Meta', 'z'];
undoKeyComboReset = ['Meta'];
redoKeyCombo = isMac ? ['Meta', 'Shift', 'z'] : ['Meta', 'y'];
redoKeyComboReset = isMac ? ['Meta', 'Shift'] : ['Meta'];
});
/**
* See <https://github.com/webdriverio/webdriverio/issues/1126>
*/
@ -13,7 +29,7 @@ describe.skip("history", function() {
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
"geojson:example"
]));
browser.alertAccept();
browser.acceptAlert();
helper.modal.addLayer.open();
@ -51,9 +67,8 @@ describe.skip("history", function() {
}
]);
browser
.keys(['Control', 'z'])
.keys(['Control']);
browser.keys(undoKeyCombo)
browser.keys(undoKeyComboReset);
styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
@ -62,16 +77,14 @@ describe.skip("history", function() {
}
]);
browser
.keys(['Control', 'z'])
.keys(['Control']);
browser.keys(undoKeyCombo)
browser.keys(undoKeyComboReset);
styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
]);
browser
.keys(['Control', 'y'])
.keys(['Control']);
browser.keys(redoKeyCombo)
browser.keys(redoKeyComboReset);
styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
@ -80,9 +93,8 @@ describe.skip("history", function() {
}
]);
browser
.keys(['Control', 'y'])
.keys(['Control']);
browser.keys(redoKeyCombo)
browser.keys(redoKeyComboReset);
styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{

View file

@ -37,6 +37,8 @@ describe('maputnik', function() {
require("./map");
require("./modals");
require("./screenshots");
require("./accessibility");
require("./keyboard");
// ------------------------
});

View file

@ -0,0 +1,58 @@
var assert = require("assert");
var config = require("../../config/specs");
var helper = require("../helper");
var wd = require("../../wd-helper");
describe("keyboard", function() {
describe("shortcuts", function() {
it("ESC should unfocus", function() {
const tmpTargetEl = $(wd.$("nav:inspect") + " select");
tmpTargetEl.click();
assert(tmpTargetEl.isFocused());
browser.keys(["Escape"]);
assert($("body").isFocused());
});
it("'?' should show shortcuts modal", function() {
browser.keys(["?"]);
assert($(wd.$("modal:shortcuts")).isDisplayed());
});
it("'o' should show open modal", function() {
browser.keys(["o"]);
assert($(wd.$("modal:open")).isDisplayed());
});
it("'e' should show export modal", function() {
browser.keys(["e"]);
assert($(wd.$("modal:export")).isDisplayed());
});
it("'d' should show sources modal", function() {
browser.keys(["d"]);
assert($(wd.$("modal:sources")).isDisplayed());
});
it("'s' should show settings modal", function() {
browser.keys(["s"]);
assert($(wd.$("modal:settings")).isDisplayed());
});
it.skip("'i' should change map to inspect mode", function() {
// browser.keys(["i"]);
});
it("'m' should focus map", function() {
browser.keys(["m"]);
$(".mapboxgl-canvas").isFocused();
});
it("'!' should show debug modal", function() {
browser.keys(["!"]);
assert($(wd.$("modal:debug")).isDisplayed());
});
});
});

View file

@ -115,12 +115,6 @@ describe("layers", function() {
})
})
describe("grouped", function() {
it("with underscore")
it("no without underscore")
it("double underscore only grouped once")
})
describe("tooltips", function() {
})
@ -130,20 +124,18 @@ describe("layers", function() {
describe('background', function () {
it.skip("add", function() {
it("add", function() {
var id = helper.modal.addLayer.fill({
type: "background"
})
browser.waitUntil(function() {
var styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": id,
"type": 'background'
}
]);
});
var styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": id,
"type": 'background'
}
]);
});
describe("modify", function() {
@ -192,9 +184,6 @@ describe("layers", function() {
]);
});
// NOTE: This needs to be removed from the code
it("type");
it("min-zoom", function() {
var bgId = createBackground();
@ -340,7 +329,7 @@ describe("layers", function() {
});
describe('fill', function () {
it.skip("add", function() {
it("add", function() {
// browser.debug();
var id = helper.modal.addLayer.fill({
@ -363,7 +352,7 @@ describe("layers", function() {
});
describe('line', function () {
it.skip("add", function() {
it("add", function() {
var id = helper.modal.addLayer.fill({
type: "line",
layer: "example"
@ -386,7 +375,7 @@ describe("layers", function() {
});
describe('symbol', function () {
it.skip("add", function() {
it("add", function() {
var id = helper.modal.addLayer.fill({
type: "symbol",
layer: "example"
@ -404,7 +393,7 @@ describe("layers", function() {
});
describe('raster', function () {
it.skip("add", function() {
it("add", function() {
var id = helper.modal.addLayer.fill({
type: "raster",
layer: "raster"
@ -422,7 +411,7 @@ describe("layers", function() {
});
describe('circle', function () {
it.skip("add", function() {
it("add", function() {
var id = helper.modal.addLayer.fill({
type: "circle",
layer: "example"
@ -441,7 +430,7 @@ describe("layers", function() {
});
describe('fill extrusion', function () {
it.skip("add", function() {
it("add", function() {
var id = helper.modal.addLayer.fill({
type: "fill-extrusion",
layer: "example"
@ -459,12 +448,12 @@ describe("layers", function() {
});
describe.skip("groups", function() {
describe("groups", function() {
it("simple", function() {
browser.url(config.baseUrl+"?debug&style="+getStyleUrl([
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
"geojson:example"
]));
browser.alertAccept();
browser.acceptAlert();
helper.modal.addLayer.open();
var aId = helper.modal.addLayer.fill({
@ -480,21 +469,22 @@ describe("layers", function() {
helper.modal.addLayer.open();
var bId = helper.modal.addLayer.fill({
id: "foo_baz",
id: "foo_bar_baz",
type: "background"
})
browser.waitForExist(wd.$("layer-list-group:foo-0"));
const groupEl = $(wd.$("layer-list-group:foo-0"));
groupEl.isDisplayed();
assert.equal(browser.isVisibleWithinViewport(wd.$("layer-list-item:foo")), false);
assert.equal(browser.isVisibleWithinViewport(wd.$("layer-list-item:foo_bar")), false);
assert.equal(browser.isVisibleWithinViewport(wd.$("layer-list-item:foo_baz")), false);
assert.equal($(wd.$("layer-list-item:foo")).isDisplayedInViewport(), true);
assert.equal($(wd.$("layer-list-item:foo_bar")).isDisplayedInViewport(), false);
assert.equal($(wd.$("layer-list-item:foo_bar_baz")).isDisplayedInViewport(), false);
browser.click(wd.$("layer-list-group:foo-0"));
groupEl.click();
assert.equal(browser.isVisibleWithinViewport(wd.$("layer-list-item:foo")), true);
assert.equal(browser.isVisibleWithinViewport(wd.$("layer-list-item:foo_bar")), true);
assert.equal(browser.isVisibleWithinViewport(wd.$("layer-list-item:foo_baz")), true);
assert.equal($(wd.$("layer-list-item:foo")).isDisplayedInViewport(), true);
assert.equal($(wd.$("layer-list-item:foo_bar")).isDisplayedInViewport(), true);
assert.equal($(wd.$("layer-list-item:foo_bar_baz")).isDisplayedInViewport(), true);
})
})
});

View file

@ -41,7 +41,7 @@ describe("modals", function() {
});
it("close", function() {
closeModal("open-modal");
closeModal("modal:open");
});
// "chooseFile" command currently not available for wdio v5 https://github.com/webdriverio/webdriverio/pull/3632
@ -57,9 +57,9 @@ describe("modals", function() {
it("load from url", function() {
var styleFileUrl = helper.getGeoServerUrl("example-style.json");
browser.setValueSafe(wd.$("open-modal.url.input"), styleFileUrl);
browser.setValueSafe(wd.$("modal:open.url.input"), styleFileUrl);
const selector = $(wd.$("open-modal.url.button"));
const selector = $(wd.$("modal:open.url.button"));
selector.click();
// Allow the network request to happen
@ -74,6 +74,24 @@ describe("modals", function() {
it("gallery")
})
describe("shortcuts", function() {
it("open/close", function() {
browser.url(config.baseUrl+"?debug");
const elem = $(".maputnik-toolbar-link");
elem.waitForExist();
browser.flushReactUpdates();
browser.keys(["?"]);
const modalEl = $(wd.$("modal:shortcuts"))
assert(modalEl.isDisplayed());
closeModal("modal:shortcuts");
});
});
describe("export", function() {
beforeEach(function() {
@ -89,14 +107,12 @@ describe("modals", function() {
});
it("close", function() {
closeModal("export-modal");
closeModal("modal:export");
});
// TODO: Work out how to download a file and check the contents
it("download")
// TODO: Work out how to mock the end git points
it("save to gist")
})
describe("sources", function() {
@ -131,8 +147,8 @@ describe("modals", function() {
});
it("name", function() {
browser.setValueSafe(wd.$("modal-settings.name"), "foobar")
const elem = $(wd.$("modal-settings.owner"));
browser.setValueSafe(wd.$("modal:settings.name"), "foobar")
const elem = $(wd.$("modal:settings.owner"));
elem.click();
browser.flushReactUpdates();
@ -140,8 +156,8 @@ describe("modals", function() {
assert.equal(styleObj.name, "foobar");
})
it("owner", function() {
browser.setValueSafe(wd.$("modal-settings.owner"), "foobar")
const elem = $(wd.$("modal-settings.name"));
browser.setValueSafe(wd.$("modal:settings.owner"), "foobar")
const elem = $(wd.$("modal:settings.name"));
elem.click();
browser.flushReactUpdates();
@ -149,8 +165,8 @@ describe("modals", function() {
assert.equal(styleObj.owner, "foobar");
})
it("sprite url", function() {
browser.setValueSafe(wd.$("modal-settings.sprite"), "http://example.com")
const elem = $(wd.$("modal-settings.name"));
browser.setValueSafe(wd.$("modal:settings.sprite"), "http://example.com")
const elem = $(wd.$("modal:settings.name"));
elem.click();
browser.flushReactUpdates();
@ -159,8 +175,8 @@ describe("modals", function() {
})
it("glyphs url", function() {
var glyphsUrl = "http://example.com/{fontstack}/{range}.pbf"
browser.setValueSafe(wd.$("modal-settings.glyphs"), glyphsUrl)
const elem = $(wd.$("modal-settings.name"));
browser.setValueSafe(wd.$("modal:settings.glyphs"), glyphsUrl)
const elem = $(wd.$("modal:settings.name"));
elem.click();
browser.flushReactUpdates();
@ -170,8 +186,8 @@ describe("modals", function() {
it("mapbox access token", function() {
var apiKey = "testing123";
browser.setValueSafe(wd.$("modal-settings.maputnik:mapbox_access_token"), apiKey);
const elem = $(wd.$("modal-settings.name"));
browser.setValueSafe(wd.$("modal:settings.maputnik:mapbox_access_token"), apiKey);
const elem = $(wd.$("modal:settings.name"));
elem.click();
browser.flushReactUpdates();
@ -183,8 +199,8 @@ describe("modals", function() {
it("maptiler access token", function() {
var apiKey = "testing123";
browser.setValueSafe(wd.$("modal-settings.maputnik:openmaptiles_access_token"), apiKey);
const elem = $(wd.$("modal-settings.name"));
browser.setValueSafe(wd.$("modal:settings.maputnik:openmaptiles_access_token"), apiKey);
const elem = $(wd.$("modal:settings.name"));
elem.click();
browser.flushReactUpdates();
@ -194,8 +210,8 @@ describe("modals", function() {
it("thunderforest access token", function() {
var apiKey = "testing123";
browser.setValueSafe(wd.$("modal-settings.maputnik:thunderforest_access_token"), apiKey);
const elem = $(wd.$("modal-settings.name"));
browser.setValueSafe(wd.$("modal:settings.maputnik:thunderforest_access_token"), apiKey);
const elem = $(wd.$("modal:settings.name"));
elem.click();
browser.flushReactUpdates();
@ -204,9 +220,9 @@ describe("modals", function() {
})
it("style renderer", function() {
const selector = $(wd.$("modal-settings.maputnik:renderer"));
const selector = $(wd.$("modal:settings.maputnik:renderer"));
selector.selectByAttribute('value', "ol");
const elem = $(wd.$("modal-settings.name"));
const elem = $(wd.$("modal:settings.name"));
elem.click();
browser.flushReactUpdates();

View file

@ -78,6 +78,14 @@ app.get("/styles/empty/:sources", function(req, res) {
res.send(json);
})
app.get("/example-layer-style.json", function(req, res) {
res.json(
JSON.parse(
fs.readFileSync(__dirname+"/example-layer-style.json").toString()
)
);
})
app.get("/example-style.json", function(req, res) {
res.json(
JSON.parse(