diff --git a/.babelrc b/.babelrc
deleted file mode 100644
index d008f68e..00000000
--- a/.babelrc
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "presets": [
- "@babel/preset-env",
- "@babel/preset-react"
- ],
- "plugins": [
- "static-fs",
- "react-hot-loader/babel",
- "@babel/plugin-proposal-class-properties",
- "@babel/transform-runtime"
- ],
- "env": {
- "test": {
- "plugins": [
- ["istanbul", {
- "exclude": ["node_modules/**", "test/**"]
- }]
- ]
- }
- }
-}
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 00000000..996d01b4
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,45 @@
+{
+ "root": true,
+ "env": {
+ "browser": true,
+ "es2020": true
+ },
+ "extends": [
+ "eslint:recommended",
+ "plugin:react/recommended",
+ "plugin:react/jsx-runtime",
+ "plugin:react-hooks/recommended",
+ ],
+ "ignorePatterns": [
+ "dist"
+ ],
+ "parser": "@typescript-eslint/parser",
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "sourceType": "module"
+ },
+ "settings": {
+ "react": { "version": "16.4" }
+ },
+ "plugins": [
+ "@typescript-eslint",
+ "react-refresh"],
+ "rules": {
+ "react-refresh/only-export-components": [
+ "warn",
+ { "allowConstantExport": true }
+ ],
+ "@typescript-eslint/no-unused-vars": [
+ "warn",
+ { "argsIgnorePattern": "^_" }
+ ],
+ "no-unused-vars": "off",
+ "react/prop-types": ["off"],
+ // Disable no-undef. It's covered by @typescript-eslint
+ "no-undef": "off",
+ "indent": ["error", 2]
+ },
+ "globals": {
+ "global": "readonly"
+ }
+}
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 97a8ed47..3da5e73d 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -9,7 +9,7 @@ assignees: ''
-**Maputnik version**:
+**Maputnik version**:
**Browser**:
**OS**:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..2e98e0e1
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,14 @@
+version: 2
+updates:
+ - package-ecosystem: "npm"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ open-pull-requests-limit: 2
+ versioning-strategy: increase
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ open-pull-requests-limit: 2
+ versioning-strategy: increase
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index df01595c..e9446c78 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,44 +2,24 @@ name: ci
on:
pull_request:
- branches: [ master ]
+ branches: [ main ]
push:
- branches: [ master ]
+ branches: [ main ]
jobs:
-
- # post a comment linking to codesandbox with the current branch
- # meta-demo-comment:
- # name: meta/demo-comment
- # runs-on: ubuntu-latest
-
- # if: ${{ github.event_name == 'pull_request' }}
-
- # steps:
- # - uses: unsplash/comment-on-pr@v1.2.0
- # env:
- # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- # with:
- # msg: "Demo: "
-
build-docker:
- name: build/docker
- runs-on: ${{ matrix.os }}
+ name: build docker
+ runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
- strategy:
- fail-fast: false
- matrix:
- os: [ubuntu-latest]
-
steps:
- - uses: actions/checkout@v2
- - run: docker build -t docker.pkg.github.com/maputnik/editor/editor:master .
+ - uses: actions/checkout@v4
+ - run: docker build -t docker.pkg.github.com/maputnik/editor/editor:main .
# build the editor
build-node:
- name: "build/node@${{ matrix.node-version }} (${{ matrix.os }})"
+ name: "build on ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
@@ -48,13 +28,12 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
- node-version: [12.x, 14.x, 16.x]
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v1
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
with:
- node-version: ${{ matrix.node-version }}
+ node-version-file: '.nvmrc'
- uses: actions/cache@v1
with:
path: ~/.npm
@@ -63,28 +42,21 @@ jobs:
${{ runner.os }}-node-
- run: npm ci
- run: npm run build
+ - run: npm run lint
+ - run: npm run lint-css
- build-artifacts:
- name: "build/artifacts (${{ matrix.os }})"
- runs-on: ${{ matrix.os }}
- env:
- GOPATH: ${{ github.workspace }}
- GO111MODULE: off
+ build-artifacts:
+ name: "build artifacts"
+ runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
- strategy:
- fail-fast: false
- matrix:
- os: [ubuntu-latest]
- node-version: [16.x]
-
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v1
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
with:
- node-version: ${{ matrix.node-version }}
+ node-version-file: '.nvmrc'
- uses: actions/cache@v1
with:
path: ~/.npm
@@ -98,13 +70,7 @@ jobs:
uses: actions/upload-artifact@v1
with:
name: editor
- path: build/build
- - run: npm run profiling-build
- - name: artifacts/editor-profiling
- uses: actions/upload-artifact@v1
- with:
- name: editor-profiling
- path: build/profiling
+ path: dist
- name: artifacts/storybook
uses: actions/upload-artifact@v1
with:
@@ -113,16 +79,16 @@ jobs:
# Build and upload desktop CLI artifacts
- name: Set up Go
- uses: actions/setup-go@v2
+ uses: actions/setup-go@v3
with:
- go-version: ^1.14.x
+ go-version: ^1.19.x
id: go
- name: Check out code into the Go module directory
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
repository: maputnik/desktop
- ref: v1.0.7
+ ref: master
path: ./src/github.com/maputnik/desktop/
- name: Make
@@ -145,53 +111,27 @@ jobs:
with:
name: maputnik-windows
path: ./src/github.com/maputnik/desktop/bin/windows/
-
- # build and test the editor
- test_selenium_standalone:
- name: "test/standalone-${{ matrix.browser }} (${{ matrix.os }})"
- runs-on: ${{ matrix.os }}
-
- if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
-
+
+ e2e-tests:
+ name: "E2E tests using ${{ matrix.browser }}"
strategy:
fail-fast: false
matrix:
- os: [ubuntu-latest]
- node-version: [16]
browser: [chrome, firefox]
- container:
- image: node:${{ matrix.node-version }}
- options: --network-alias testhost
-
- services:
- selenium:
- # geckodriver-0.31 seems to have problems as of 2022 May 1
- image: selenium/standalone-${{ matrix.browser == 'firefox' && 'firefox:99.0-geckodriver-0.30-20220427' || matrix.browser }}
- ports:
- - 4444:4444
- options: --shm-size=2gb
-
+ runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v2
- - uses: actions/cache@v1
- with:
- path: ~/.npm
- key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
- restore-keys: |
- ${{ runner.os }}-node-
- - run: npm ci
- - run: BROWSER=${{ matrix.browser }} TEST_NETWORK=testhost DOCKER_HOST=selenium npm run test
- - if: ${{ matrix.browser == 'chrome' }}
- run: ./node_modules/.bin/istanbul report --include build/coverage/coverage.json --dir build/coverage html lcov
- - if: ${{ matrix.browser == 'chrome' }}
- name: artifacts/coverage
- uses: actions/upload-artifact@v1
- with:
- name: coverage
- path: build/coverage
- - name: artifacts/screenshots
- uses: actions/upload-artifact@v1
- with:
- name: screenshots-${{ matrix.browser }}
- path: build/screenshots
+ - name: Checkout
+ uses: actions/checkout@v4
+ - run: npm ci
+ - name: Cypress run
+ uses: cypress-io/github-action@v6
+ with:
+ build: npm run build
+ start: npm run start
+ browser: ${{ matrix.browser }}
+ - name: Upload coverage reports to Codecov
+ uses: codecov/codecov-action@v3
+ with:
+ files: ${{ github.workspace }}/.nyc_output/out.json
+ verbose: true
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index ee8b519a..7a6c66fe 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -2,13 +2,12 @@ name: deploy
on:
push:
- branches: [ master ]
- push:
- tags:
- - 'v*'
+ branches: [ main ]
+ tags:
+ - 'v*'
jobs:
- # publish docker to github registry
+ # publish docker to GitHub registry
deploy-docker:
name: deploy/docker
runs-on: ${{ matrix.os }}
@@ -21,8 +20,7 @@ jobs:
os: [ubuntu-latest]
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u orangemug --password-stdin
- - run: docker build -t docker.pkg.github.com/maputnik/editor/editor:master .
- - run: docker push docker.pkg.github.com/maputnik/editor/editor:master
-
+ - run: docker build -t docker.pkg.github.com/maputnik/editor/editor:main .
+ - run: docker push docker.pkg.github.com/maputnik/editor/editor:main
diff --git a/.gitignore b/.gitignore
index af33419d..d465cc0c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@ lib-cov
# Coverage directory used by tools like istanbul
coverage
+.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
@@ -33,3 +34,5 @@ public
/errorShots
/old
/build
+/cypress/screenshots
+/dist/
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 00000000..ca0a999a
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+18.19
\ No newline at end of file
diff --git a/.nycrc.json b/.nycrc.json
new file mode 100644
index 00000000..dfb4729b
--- /dev/null
+++ b/.nycrc.json
@@ -0,0 +1,18 @@
+{
+ "all": true,
+ "extends": "@istanbuljs/nyc-config-typescript",
+ "check-coverage": false,
+ "include": ["src/**/*.ts", "src/**/*.tsx"],
+ "exclude": [
+ "cypress/**/*.*",
+ "**/*.d.ts",
+ "**/*.cy.tsx",
+ "**/*.cy.ts",
+ "./coverage/**",
+ "./cypress/**",
+ "./dist/**",
+ "node_modules"
+ ],
+ "report-dir": "coverage",
+ "reporter": ["json", "lcov", "json-summary"]
+}
diff --git a/.storybook/main.js b/.storybook/main.js
index 55c1c31f..3c2dfbbf 100644
--- a/.storybook/main.js
+++ b/.storybook/main.js
@@ -1,24 +1,9 @@
-const rules = require('../config/webpack.rules');
-
-module.exports = {
- stories: ['../stories/**/*.stories.js'],
- addons: [
- '@storybook/addon-actions',
- '@storybook/addon-links',
- '@storybook/addon-a11y/register',
- '@storybook/addon-storysource',
- ],
- webpackFinal: async config => {
- // do mutation to the config
- console.log("config.module", config.module);
-
- return {
- ...config,
- module: {
- rules: [
- ...rules,
- ]
- }
- };
- },
+const config = {
+ stories: ['../stories/**/*.stories.jsx'],
+ addons: ['@storybook/addon-actions', '@storybook/addon-links', '@storybook/addon-a11y/register', '@storybook/addon-storysource'],
+ framework: {
+ name: '@storybook/react-vite',
+ options: {}
+ }
};
+export default config;
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 723f57c2..a2f23b20 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:10 as builder
+FROM node:18 as builder
WORKDIR /maputnik
# Only copy package.json to prevent npm install from running on every build
@@ -6,14 +6,11 @@ COPY package.json package-lock.json ./
RUN npm install
# Build maputnik
-# TODO: we should also do a npm run test here (needs more dependencies)
COPY . .
RUN npm run build
#---------------------------------------------------------------------------
+# Create a clean nginx-alpine slim image with just the build results
+FROM nginx:alpine-slim
-# Create a clean nginx-alpine image with just the build results
-
-FROM nginx:alpine
-
-COPY --from=builder /maputnik/build/build /usr/share/nginx/html/
+COPY --from=builder /maputnik/dist /usr/share/nginx/html/
diff --git a/README.md b/README.md
index 9d3e68fa..a241a0a0 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
[github-action-ci]: https://github.com/maputnik/editor/actions?query=workflow%3Aci
[license]: https://tldrlegal.com/license/mit-license
-A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/mapbox-gl-style-spec/)
+A free and open visual editor for the [MapLibre GL styles](https://maplibre.org/maplibre-style-spec/)
targeted at developers and map designers.
@@ -36,7 +36,7 @@ The documentation can be found in the [Wiki](https://github.com/maputnik/editor/
## Develop
-Maputnik is written in ES6 and is using [React](https://github.com/facebook/react) and [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/api/).
+Maputnik is written in ES6 and is using [React](https://github.com/facebook/react) and [MapLibre GL JS](https://maplibre.org/projects/maplibre-gl-js/).
We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule).
@@ -49,18 +49,14 @@ npm install
npm run start
```
-If you want Maputnik to be accessible externally use the [`--host` option](https://webpack.js.org/configuration/dev-server/#devserverhost):
+If you want Maputnik to be accessible externally use the [`--host` option](https://vitejs.dev/config/server-options.html#server-host):
```bash
# start externally accessible dev server
npm run start -- --host 0.0.0.0
```
-The build process will watch for changes to the filesystem, rebuild and autoreload the editor. However note this from the [webpack-dev-server docs](https://webpack.js.org/configuration/dev-server/):
-
-> webpack uses the file system to get notified of file changes. In some cases this does not work. For example, when using Network File System (NFS). Vagrant also has a lot of problems with this. ([snippet source](https://webpack.js.org/configuration/dev-server/#devserverwatchoptions-))
-
-To enable polling add `export WEBPACK_DEV_SERVER_POLLING=1` to your environment.
+The build process will watch for changes to the filesystem, rebuild and autoreload the editor.
```
npm run build
@@ -76,9 +72,9 @@ npm run lint-styles
## Tests
-For testing we use [webdriverio](https://webdriver.io) and [selenium-standalone](https://github.com/webdriverio/selenium-standalone).
+For E2E testing we use [Cypress](https://www.cypress.io/)
-[selenium-standalone](https://github.com/webdriverio/selenium-standalone) starts a server that will launch browsers on your local machine. You need to have Java installed on your machine as well as *chrome* or *firefox*.
+ [Cypress](https://www.cypress.io/) doesn't starts a server so you'll need to start one manually by running `npm run start`.
Now open a terminal and run the following using *chrome*:
@@ -87,10 +83,16 @@ npm run test
```
or *firefox*:
```
-BROWSER=firefox npm run test
+npm run test -- --browser firefox
```
-After some time you should see a browser launch which will be automated by the test runner.
+See the following docs for more info: (Launching Browsers)[https://docs.cypress.io/guides/guides/launching-browsers]
+
+You can also see the tests as they run or select which suites to run by executing:
+
+```
+npm run cy:open
+```
## Related Projects
@@ -155,6 +157,5 @@ Sina Martinelli, Nicholas Doiron, Neil Cawse, Urs42, Benedikt Groß, Manuel Roth
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and contributors.
-**Disclaimer** This project is not affiliated with Mapbox or Mapbox Studio. It is an independent style editor for the
-open source technology in the Mapbox GL ecosystem.
+**Disclaimer** This is an independent style editor.
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by Mapbox Studio and make your own decisions for a good style editor.
diff --git a/config/wdio.conf.js b/config/wdio.conf.js
deleted file mode 100644
index 06b07a1b..00000000
--- a/config/wdio.conf.js
+++ /dev/null
@@ -1,47 +0,0 @@
-var webpack = require("webpack");
-var WebpackDevServer = require("webpack-dev-server");
-var webpackConfig = require("./webpack.config");
-var testConfig = require("../test/config/specs");
-var artifacts = require("../test/artifacts");
-
-
-var server;
-var SCREENSHOT_PATH = artifacts.pathSync("screenshots");
-
-exports.config = {
- runner: 'local',
- path: '/wd/hub',
- specs: [
- './test/functional/index.js'
- ],
- maxInstances: 10,
- capabilities: [
- {
- maxInstances: 5,
- browserName: (process.env.BROWSER || 'chrome'),
- }
- ],
- // geckodriver-0.31 seems to have problems as of 2022 May 1
- services: process.env.DOCKER_HOST ? [] : [ ['selenium-standalone', { drivers: { firefox: '0.30.0', chrome: 'latest' } } ] ],
- logLevel: 'info',
- bail: 0,
- screenshotPath: SCREENSHOT_PATH,
- hostname: process.env.DOCKER_HOST || "0.0.0.0",
- framework: 'mocha',
- reporters: ['spec'],
- mochaOpts: {
- ui: 'bdd',
- // Because we don't know how long the initial build will take...
- timeout: 4*60*1000,
- },
- onPrepare: async function (config, capabilities) {
- webpackConfig.devServer.host = testConfig.testNetwork;
- webpackConfig.devServer.port = testConfig.port;
- const compiler = webpack(webpackConfig);
- server = new WebpackDevServer(webpackConfig.devServer, compiler);
- await server.start();
- },
- onComplete: async function (exitCode, config, capabilities) {
- await server.stop();
- }
-}
diff --git a/config/webpack.config.js b/config/webpack.config.js
deleted file mode 100644
index fea68abb..00000000
--- a/config/webpack.config.js
+++ /dev/null
@@ -1,74 +0,0 @@
-"use strict";
-var path = require('path');
-var rules = require('./webpack.rules');
-var HtmlWebpackPlugin = require('html-webpack-plugin');
-var HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin');
-var CopyWebpackPlugin = require('copy-webpack-plugin');
-
-const HOST = process.env.HOST || "127.0.0.1";
-const PORT = process.env.PORT || "8888";
-
-module.exports = {
- target: 'web',
- mode: 'development',
- entry: [
- `webpack-dev-server/client?http://${HOST}:${PORT}`,
- `webpack/hot/only-dev-server`,
- `./src/index.jsx` // Your appʼs entry point
- ],
- devtool: process.env.WEBPACK_DEVTOOL || 'cheap-module-source-map',
- output: {
- path: path.join(__dirname, '..', 'public'),
- filename: 'bundle.js'
- },
- resolve: {
- extensions: ['.js', '.jsx']
- },
- module: {
- noParse: [
- /mapbox-gl\/dist\/mapbox-gl.js/
- ],
- rules: rules
- },
- node: {
- fs: "empty",
- net: 'empty',
- tls: 'empty'
- },
- devServer: {
- // enable HMR
- hot: true,
- // serve index.html in place of 404 responses to allow HTML5 history
- historyApiFallback: true,
- port: PORT,
- host: HOST,
- watchFiles: {
- options: {
- // Disabled polling by default as it causes lots of CPU usage and hence drains laptop batteries. To enable polling add WEBPACK_DEV_SERVER_POLLING to your environment
- // See for details
- usePolling: (!!process.env.WEBPACK_DEV_SERVER_POLLING ? true : false),
- watch: false
- }
- }
- },
- optimization: {
- noEmitOnErrors: true,
- },
- plugins: [
- new HtmlWebpackPlugin({
- title: 'Maputnik',
- template: './src/template.html'
- }),
- new HtmlWebpackInlineSVGPlugin({
- runPreEmit: true,
- }),
- new CopyWebpackPlugin({
- patterns: [
- {
- from: './src/manifest.json',
- to: 'manifest.json'
- }
- ]
- })
- ]
-};
diff --git a/config/webpack.production.config.js b/config/webpack.production.config.js
deleted file mode 100644
index 5afad473..00000000
--- a/config/webpack.production.config.js
+++ /dev/null
@@ -1,68 +0,0 @@
-var webpack = require('webpack');
-var path = require('path');
-var rules = require('./webpack.rules');
-var HtmlWebpackPlugin = require('html-webpack-plugin');
-var HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin');
-var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
-var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
-var CopyWebpackPlugin = require('copy-webpack-plugin');
-var artifacts = require("../test/artifacts");
-
-var OUTPATH = artifacts.pathSync("/build");
-
-module.exports = {
- entry: {
- app: './src/index.jsx',
- },
- output: {
- path: OUTPATH,
- filename: '[name].[contenthash].js',
- chunkFilename: '[contenthash].js'
- },
- resolve: {
- extensions: ['.js', '.jsx']
- },
- module: {
- noParse: [
- /mapbox-gl\/dist\/mapbox-gl.js/
- ],
- rules: rules
- },
- node: {
- fs: "empty",
- net: 'empty',
- tls: 'empty'
- },
- plugins: [
- new webpack.NoEmitOnErrorsPlugin(),
- new WebpackCleanupPlugin(),
- new webpack.DefinePlugin({
- 'process.env': {
- NODE_ENV: '"production"'
- }
- }),
- new HtmlWebpackPlugin({
- template: './src/template.html',
- title: 'Maputnik'
- }),
- new HtmlWebpackInlineSVGPlugin({
- runPreEmit: true,
- }),
- new CopyWebpackPlugin({
- patterns: [
- {
- from: './src/manifest.json',
- to: 'manifest.json'
- }
- ]
- }),
- new BundleAnalyzerPlugin({
- analyzerMode: 'static',
- defaultSizes: 'gzip',
- openAnalyzer: false,
- generateStatsFile: true,
- reportFilename: 'bundle-stats.html',
- statsFilename: 'bundle-stats.json',
- })
- ]
-};
diff --git a/config/webpack.profiling.config.js b/config/webpack.profiling.config.js
deleted file mode 100644
index 84c4da23..00000000
--- a/config/webpack.profiling.config.js
+++ /dev/null
@@ -1,20 +0,0 @@
-const webpackProdConfig = require('./webpack.production.config');
-const artifacts = require("../test/artifacts");
-
-const OUTPATH = artifacts.pathSync("/profiling");
-
-module.exports = {
- ...webpackProdConfig,
- output: {
- ...webpackProdConfig.output,
- path: OUTPATH,
- },
- resolve: {
- ...webpackProdConfig.resolve,
- alias: {
- ...webpackProdConfig.resolve.alias,
- 'react-dom$': 'react-dom/profiling',
- 'scheduler/tracing': 'scheduler/tracing-profiling',
- }
- }
-};
diff --git a/config/webpack.rules.js b/config/webpack.rules.js
deleted file mode 100644
index b93553de..00000000
--- a/config/webpack.rules.js
+++ /dev/null
@@ -1,44 +0,0 @@
-const path = require("path");
-
-module.exports = [
- {
- test: /\.jsx?$/,
- exclude: [
- path.resolve(__dirname, '../node_modules')
- ],
- use: 'babel-loader'
- },
- {
- test: /\.(eot|ttf|woff|woff2)$/,
- use: 'file-loader?name=fonts/[name].[ext]'
- },
- {
- test: /\.ico$/,
- use: 'file-loader?name=[name].[ext]'
- },
- {
- test: /\.(gif|jpg|png)$/,
- use: 'file-loader?name=img/[name].[ext]'
- },
- {
- test: /\.svg$/,
- use: [
- 'svg-inline-loader'
- ]
- },
- {
- test: /[\/\\](node_modules|global|src)[\/\\].*\.scss$/,
- use: [
- 'style-loader',
- "css-loader",
- "sass-loader"
- ]
- },
- {
- test: /[\/\\](node_modules|global|src)[\/\\].*\.css$/,
- use: [
- 'style-loader',
- 'css-loader'
- ]
- }
-];
diff --git a/cypress.config.ts b/cypress.config.ts
new file mode 100644
index 00000000..939d4606
--- /dev/null
+++ b/cypress.config.ts
@@ -0,0 +1,23 @@
+import { defineConfig } from "cypress";
+import { createRequire } from "module";
+const require = createRequire(import.meta.url);
+
+export default defineConfig({
+ env: {
+ codeCoverage: {
+ exclude: "cypress/**/*.*",
+ },
+ },
+ e2e: {
+ setupNodeEvents(on, config) {
+ // implement node event listeners here
+ require("@cypress/code-coverage/task")(on, config);
+ return config;
+ },
+ baseUrl: "http://localhost:8888",
+ retries: {
+ runMode: 2,
+ openMode: 0,
+ },
+ },
+});
diff --git a/cypress/e2e/accessibility.cy.ts b/cypress/e2e/accessibility.cy.ts
new file mode 100644
index 00000000..30231ac9
--- /dev/null
+++ b/cypress/e2e/accessibility.cy.ts
@@ -0,0 +1,40 @@
+import MaputnikDriver from "./driver";
+
+describe("accessibility", () => {
+ let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
+ beforeAndAfter();
+
+ describe("skip links", () => {
+ beforeEach(() => {
+ when.setStyle("layer");
+ });
+
+ it("skip link to layer list", () => {
+ const selector = "root:skip:layer-list";
+ should.exist(selector);
+ when.tab();
+ should.beFocused(selector);
+ when.click(selector);
+ should.beFocused("skip-target-layer-list");
+ });
+
+ // This fails for some reason only in Chrome, but passes in firefox. Adding a skip here to allow merge and later on we'll decide if we want to fix this or not.
+ it.skip("skip link to layer editor", () => {
+ const selector = "root:skip:layer-editor";
+ should.exist(selector);
+ when.tab().tab();
+ should.beFocused(selector);
+ when.click(selector);
+ should.beFocused("skip-target-layer-editor");
+ });
+
+ it("skip link to map view", () => {
+ const selector = "root:skip:map-view";
+ should.exist(selector);
+ when.tab().tab().tab();
+ should.beFocused(selector);
+ when.click(selector);
+ should.canvasBeFocused();
+ });
+ });
+});
diff --git a/cypress/e2e/driver.ts b/cypress/e2e/driver.ts
new file mode 100644
index 00000000..71762db2
--- /dev/null
+++ b/cypress/e2e/driver.ts
@@ -0,0 +1,210 @@
+import { CypressHelper } from "@shellygo/cypress-test-utils";
+import { v1 as uuid } from "uuid";
+export default class MaputnikDriver {
+ private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" });
+ public beforeAndAfter = () => {
+ beforeEach(() => {
+ this.given.setupInterception();
+ this.when.setStyle("both");
+ });
+ };
+
+ public given = {
+ setupInterception: () => {
+ cy.intercept("GET", "http://localhost:8888/example-style.json", {
+ fixture: "example-style.json",
+ }).as("example-style.json");
+ cy.intercept("GET", "http://localhost:8888/example-layer-style.json", {
+ fixture: "example-layer-style.json",
+ });
+ cy.intercept("GET", "http://localhost:8888/geojson-style.json", {
+ fixture: "geojson-style.json",
+ });
+ cy.intercept("GET", "http://localhost:8888/raster-style.json", {
+ fixture: "raster-style.json",
+ });
+ cy.intercept("GET", "http://localhost:8888/geojson-raster-style.json", {
+ fixture: "geojson-raster-style.json",
+ });
+ cy.intercept({ method: "GET", url: "*example.local/*" }, []);
+ cy.intercept({ method: "GET", url: "*example.com/*" }, []);
+ },
+ };
+
+ public when = {
+ within: (selector: string, fn: () => void) => {
+ this.helper.when.within(fn, selector);
+ },
+ tab: () => cy.get("body").tab(),
+ waitForExampleFileRequset: () => {
+ this.helper.when.waitForResponse("example-style.json");
+ },
+ chooseExampleFile: () => {
+ cy.get("input[type='file']").selectFile(
+ "cypress/fixtures/example-style.json",
+ { force: true }
+ );
+ },
+ setStyle: (
+ styleProperties: "geojson" | "raster" | "both" | "layer" | "",
+ zoom?: number
+ ) => {
+ let url = "?debug";
+ switch (styleProperties) {
+ case "geojson":
+ url += "&style=http://localhost:8888/geojson-style.json";
+ break;
+ case "raster":
+ url += "&style=http://localhost:8888/raster-style.json";
+ break;
+ case "both":
+ url += "&style=http://localhost:8888/geojson-raster-style.json";
+ break;
+ case "layer":
+ url += "&style=http://localhost:8888/example-layer-style.json";
+ break;
+ }
+ if (zoom) {
+ url += "#" + zoom + "/41.3805/2.1635";
+ }
+ cy.visit("http://localhost:8888/" + url);
+ if (styleProperties) {
+ cy.on("window:confirm", () => true);
+ }
+ cy.get(".maputnik-toolbar-link").should("be.visible");
+ },
+ fillLayersModal: (opts: {type: string, layer?: string, id?: string}) => {
+ var type = opts.type;
+ var layer = opts.layer;
+ var id;
+ if (opts.id) {
+ id = opts.id;
+ } else {
+ id = `${type}:${uuid()}`;
+ }
+
+ cy.get(
+ this.get.dataAttribute("add-layer.layer-type", "select")
+ ).select(type);
+ cy.get(this.get.dataAttribute("add-layer.layer-id", "input")).type(id);
+ if (layer) {
+ cy.get(
+ this.get.dataAttribute("add-layer.layer-source-block", "input")
+ ).type(layer);
+ }
+ this.when.click("add-layer");
+
+ return id;
+ },
+
+ typeKeys: (keys: string) => {
+ cy.get("body").type(keys);
+ },
+
+ click: (selector: string) => {
+ this.helper.when.click(selector);
+ },
+
+ clickZoomin: () => {
+ cy.get(".maplibregl-ctrl-zoom-in").click();
+ },
+
+ selectWithin: (selector: string, value: string) => {
+ this.when.within(selector, () => {
+ cy.get("select").select(value);
+ });
+ },
+
+ select: (selector: string, value: string) => {
+ this.helper.get.element(selector).select(value);
+ },
+
+ focus: (selector: string) => {
+ this.helper.when.focus(selector);
+ },
+
+ setValue: (selector: string, text: string) => {
+ cy.get(selector).clear().type(text, { parseSpecialCharSequences: false });
+ },
+
+ closeModal: (key: string) => {
+ this.helper.when.waitUntil(() => this.helper.get.element(key));
+ this.when.click(key + ".close-modal");
+ },
+
+ openLayersModal: () => {
+ this.helper.when.click("layer-list:add-layer");
+
+ cy.get(this.get.dataAttribute("modal:add-layer")).should("exist");
+ cy.get(this.get.dataAttribute("modal:add-layer")).should("be.visible");
+ },
+ };
+
+ public get = {
+ isMac: () => {
+ return Cypress.platform === "darwin";
+ },
+ styleFromWindow: (win: Window) => {
+ const styleId = win.localStorage.getItem("maputnik:latest_style");
+ const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`);
+ const obj = JSON.parse(styleItem || "");
+ return obj;
+ },
+ exampleFileUrl: () => {
+ return "http://localhost:8888/example-style.json";
+ },
+ dataAttribute: (key: string, selector?: string): string => {
+ return `*[data-wd-key='${key}'] ${selector || ""}`;
+ },
+ };
+
+ public should = {
+ canvasBeFocused: () => {
+ this.when.within("maplibre:map", () => {
+ cy.get("canvas").should("be.focused");
+ });
+ },
+ notExist: (selector: string) => {
+ cy.get(selector).should("not.exist");
+ },
+ beFocused: (selector: string) => {
+ this.helper.get.element(selector).should("have.focus");
+ },
+
+ notBeFocused: (selector: string) => {
+ this.helper.get.element(selector).should("not.have.focus");
+ },
+
+ beVisible: (selector: string) => {
+ this.helper.get.element(selector).should("be.visible");
+ },
+
+ notBeVisible: (selector: string) => {
+ this.helper.get.element(selector).should("not.be.visible");
+ },
+
+ equalStyleStore: (getter: (obj: any) => any, styleObj: any) => {
+ cy.window().then((win: any) => {
+ const obj = this.get.styleFromWindow(win);
+ assert.deepEqual(getter(obj), styleObj);
+ });
+ },
+
+ styleStoreEqualToExampleFileData: () => {
+ cy.window().then((win: any) => {
+ const obj = this.get.styleFromWindow(win);
+ cy.fixture("example-style.json").should("deep.equal", obj);
+ });
+ },
+
+ exist: (selector: string) => {
+ this.helper.get.element(selector).should("exist");
+ },
+ beSelected: (selector: string, value: string) => {
+ this.helper.get.element(selector).find(`option[value="${value}"]`).should("be.selected");
+ },
+ containText: (selector: string, text: string) => {
+ this.helper.get.element(selector).should("contain.text", text);
+ }
+ };
+}
diff --git a/cypress/e2e/history.cy.ts b/cypress/e2e/history.cy.ts
new file mode 100644
index 00000000..59b6001e
--- /dev/null
+++ b/cypress/e2e/history.cy.ts
@@ -0,0 +1,97 @@
+import MaputnikDriver from "./driver";
+
+describe("history", () => {
+ let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
+ beforeAndAfter();
+
+ let undoKeyCombo: string;
+ let redoKeyCombo: string;
+
+ before(() => {
+ const isMac = get.isMac();
+ undoKeyCombo = isMac ? "{meta}z" : "{ctrl}z";
+ redoKeyCombo = isMac ? "{meta}{shift}z" : "{ctrl}y";
+ });
+
+ it("undo/redo", () => {
+ when.setStyle("geojson");
+ when.openLayersModal();
+
+ should.equalStyleStore((a: any) => a.layers, []);
+
+ when.fillLayersModal({
+ id: "step 1",
+ type: "background",
+ });
+
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: "step 1",
+ type: "background",
+ },
+ ]
+ );
+
+ when.openLayersModal();
+ when.fillLayersModal({
+ id: "step 2",
+ type: "background",
+ });
+
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: "step 1",
+ type: "background",
+ },
+ {
+ id: "step 2",
+ type: "background",
+ },
+ ]
+ );
+
+ when.typeKeys(undoKeyCombo);
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: "step 1",
+ type: "background",
+ },
+ ]
+ );
+
+ when.typeKeys(undoKeyCombo);
+ should.equalStyleStore((a: any) => a.layers, []);
+
+ when.typeKeys(redoKeyCombo);
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: "step 1",
+ type: "background",
+ },
+ ]
+ );
+
+ when.typeKeys(redoKeyCombo);
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: "step 1",
+ type: "background",
+ },
+ {
+ id: "step 2",
+ type: "background",
+ },
+ ]
+ );
+ });
+});
diff --git a/cypress/e2e/keyboard.cy.ts b/cypress/e2e/keyboard.cy.ts
new file mode 100644
index 00000000..8cb5b1ce
--- /dev/null
+++ b/cypress/e2e/keyboard.cy.ts
@@ -0,0 +1,61 @@
+import MaputnikDriver from "./driver";
+
+describe("keyboard", () => {
+ let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
+ beforeAndAfter();
+ describe("shortcuts", () => {
+ beforeEach(() => {
+ given.setupInterception();
+ when.setStyle("");
+ });
+
+ it("ESC should unfocus", () => {
+ const targetSelector = "maputnik-select";
+ when.focus(targetSelector);
+ should.beFocused(targetSelector);
+
+ when.typeKeys("{esc}");
+ expect(should.notBeFocused(targetSelector));
+ });
+
+ it("'?' should show shortcuts modal", () => {
+ when.typeKeys("?");
+ should.beVisible("modal:shortcuts");
+ });
+
+ it("'o' should show open modal", () => {
+ when.typeKeys("o");
+ should.beVisible("modal:open");
+ });
+
+ it("'e' should show export modal", () => {
+ when.typeKeys("e");
+ should.beVisible("modal:export");
+ });
+
+ it("'d' should show sources modal", () => {
+ when.typeKeys("d");
+ should.beVisible("modal:sources");
+ });
+
+ it("'s' should show settings modal", () => {
+ when.typeKeys("s");
+ should.beVisible("modal:settings");
+ });
+
+ it("'i' should change map to inspect mode", () => {
+ when.typeKeys("i");
+ should.beSelected("nav:inspect", "inspect");
+ });
+
+ it("'m' should focus map", () => {
+ when.typeKeys("m");
+ should.canvasBeFocused();
+ });
+
+ it("'!' should show debug modal", () => {
+ when.typeKeys("!");
+ should.beVisible("modal:debug");
+ });
+ });
+});
diff --git a/cypress/e2e/layers.cy.ts b/cypress/e2e/layers.cy.ts
new file mode 100644
index 00000000..41efdb43
--- /dev/null
+++ b/cypress/e2e/layers.cy.ts
@@ -0,0 +1,491 @@
+import { v1 as uuid } from "uuid";
+import MaputnikDriver from "./driver";
+
+describe("layers", () => {
+ let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
+ beforeAndAfter();
+ beforeEach(() => {
+ when.setStyle("both");
+ when.openLayersModal();
+ });
+
+ describe("ops", () => {
+ it("delete", () => {
+ var id = when.fillLayersModal({
+ type: "background",
+ });
+
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: id,
+ type: "background",
+ },
+ ]
+ );
+
+ when.click("layer-list-item:" + id + ":delete");
+
+ should.equalStyleStore((a: any) => a.layers, []);
+ });
+
+ it("duplicate", () => {
+ var styleObj;
+ var id = when.fillLayersModal({
+ type: "background",
+ });
+
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: id,
+ type: "background",
+ },
+ ]
+ );
+
+ when.click("layer-list-item:" + id + ":copy");
+
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: id + "-copy",
+ type: "background",
+ },
+ {
+ id: id,
+ type: "background",
+ },
+ ]
+ );
+ });
+
+ it("hide", () => {
+ var styleObj;
+ var id = when.fillLayersModal({
+ type: "background",
+ });
+
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: id,
+ type: "background",
+ },
+ ]
+ );
+
+ when.click("layer-list-item:" + id + ":toggle-visibility");
+
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: id,
+ type: "background",
+ layout: {
+ visibility: "none",
+ },
+ },
+ ]
+ );
+
+ when.click("layer-list-item:" + id + ":toggle-visibility");
+
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: id,
+ type: "background",
+ layout: {
+ visibility: "visible",
+ },
+ },
+ ]
+ );
+ });
+ });
+
+ describe("background", () => {
+ it("add", () => {
+ var id = when.fillLayersModal({
+ type: "background",
+ });
+
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: id,
+ type: "background",
+ },
+ ]
+ );
+ });
+
+ describe("modify", () => {
+ function createBackground() {
+ // Setup
+ var id = uuid();
+
+ when.selectWithin("add-layer.layer-type", "background");
+ when.setValue(
+ get.dataAttribute("add-layer.layer-id", "input"),
+ "background:" + id
+ );
+
+ when.click("add-layer");
+
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: "background:" + id,
+ type: "background",
+ },
+ ]
+ );
+ return id;
+ }
+
+ // ====> THESE SHOULD BE FROM THE SPEC
+ describe("layer", () => {
+ it("expand/collapse");
+ it("id", () => {
+ var bgId = createBackground();
+
+ when.click("layer-list-item:background:" + bgId);
+
+ var id = uuid();
+ when.setValue(
+ get.dataAttribute("layer-editor.layer-id", "input"),
+ "foobar:" + id
+ );
+ when.click("min-zoom");
+
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: "foobar:" + id,
+ type: "background",
+ },
+ ]
+ );
+ });
+
+ it("min-zoom", () => {
+ var bgId = createBackground();
+
+ when.click("layer-list-item:background:" + bgId);
+ when.setValue(
+ get.dataAttribute("min-zoom", 'input[type="text"]'),
+ "1"
+ );
+
+ when.click("layer-editor.layer-id");
+
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: "background:" + bgId,
+ type: "background",
+ minzoom: 1,
+ },
+ ]
+ );
+
+ // AND RESET!
+ // driver.setValue(driver.get.dataAttribute("min-zoom", "input"), "")
+ // driver.click(driver.get.dataAttribute("max-zoom", "input"));
+
+ // driver.isStyleStoreEqual((a: any) => a.layers, [
+ // {
+ // "id": 'background:'+bgId,
+ // "type": 'background'
+ // }
+ // ]);
+ });
+
+ it("max-zoom", () => {
+ var bgId = createBackground();
+
+ when.click("layer-list-item:background:" + bgId);
+ when.setValue(
+ get.dataAttribute("max-zoom", 'input[type="text"]'),
+ "1"
+ );
+
+ when.click("layer-editor.layer-id");
+
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: "background:" + bgId,
+ type: "background",
+ maxzoom: 1,
+ },
+ ]
+ );
+ });
+
+ it("comments", () => {
+ var bgId = createBackground();
+ var id = uuid();
+
+ when.click("layer-list-item:background:" + bgId);
+ when.setValue(get.dataAttribute("layer-comment", "textarea"), id);
+
+ when.click("layer-editor.layer-id");
+
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: "background:" + bgId,
+ type: "background",
+ metadata: {
+ "maputnik:comment": id,
+ },
+ },
+ ]
+ );
+
+ // Unset it again.
+ // TODO: This fails
+ // driver.setValue(driver.getDataAttribute("layer-comment", "textarea"), "");
+ // driver.click(driver.getDataAttribute("min-zoom", "input"));
+
+ // driver.isStyleStoreEqual((a: any) => a.layers, [
+ // {
+ // "id": 'background:'+bgId,
+ // "type": 'background'
+ // }
+ // ]);
+ });
+
+ it("color", () => {
+ var bgId = createBackground();
+
+ when.click("layer-list-item:background:" + bgId);
+
+ when.click("spec-field:background-color");
+
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: "background:" + bgId,
+ type: "background",
+ },
+ ]
+ );
+ });
+ });
+
+ describe("filter", () => {
+ it("expand/collapse");
+ it("compound filter");
+ });
+
+ describe("paint", () => {
+ it("expand/collapse");
+ it("color");
+ it("pattern");
+ it("opacity");
+ });
+ // <=====
+
+ describe("json-editor", () => {
+ it("expand/collapse");
+ it("modify");
+
+ // TODO
+ it.skip("parse error", () => {
+ var bgId = createBackground();
+
+ when.click("layer-list-item:background:" + bgId);
+
+ var errorSelector = ".CodeMirror-lint-marker-error";
+ should.notExist(errorSelector);
+
+ when.click(".CodeMirror");
+ when.typeKeys(
+ "\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {"
+ );
+ should.exist(errorSelector);
+ });
+ });
+ });
+ });
+
+ describe("fill", () => {
+ it("add", () => {
+ var id = when.fillLayersModal({
+ type: "fill",
+ layer: "example",
+ });
+
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: id,
+ type: "fill",
+ source: "example",
+ },
+ ]
+ );
+ });
+
+ // TODO: Change source
+ it("change source");
+ });
+
+ describe("line", () => {
+ it("add", () => {
+ var id = when.fillLayersModal({
+ type: "line",
+ layer: "example",
+ });
+
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: id,
+ type: "line",
+ source: "example",
+ },
+ ]
+ );
+ });
+
+ it("groups", () => {
+ // TODO
+ // Click each of the layer groups.
+ });
+ });
+
+ describe("symbol", () => {
+ it("add", () => {
+ var id = when.fillLayersModal({
+ type: "symbol",
+ layer: "example",
+ });
+
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: id,
+ type: "symbol",
+ source: "example",
+ },
+ ]
+ );
+ });
+ });
+
+ describe("raster", () => {
+ it("add", () => {
+ var id = when.fillLayersModal({
+ type: "raster",
+ layer: "raster",
+ });
+
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: id,
+ type: "raster",
+ source: "raster",
+ },
+ ]
+ );
+ });
+ });
+
+ describe("circle", () => {
+ it("add", () => {
+ var id = when.fillLayersModal({
+ type: "circle",
+ layer: "example",
+ });
+
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: id,
+ type: "circle",
+ source: "example",
+ },
+ ]
+ );
+ });
+ });
+
+ describe("fill extrusion", () => {
+ it("add", () => {
+ var id = when.fillLayersModal({
+ type: "fill-extrusion",
+ layer: "example",
+ });
+
+ should.equalStyleStore(
+ (a: any) => a.layers,
+ [
+ {
+ id: id,
+ type: "fill-extrusion",
+ source: "example",
+ },
+ ]
+ );
+ });
+ });
+
+ describe("groups", () => {
+ it("simple", () => {
+ when.setStyle("geojson");
+
+ when.openLayersModal();
+ when.fillLayersModal({
+ id: "foo",
+ type: "background",
+ });
+
+ when.openLayersModal();
+ when.fillLayersModal({
+ id: "foo_bar",
+ type: "background",
+ });
+
+ when.openLayersModal();
+ when.fillLayersModal({
+ id: "foo_bar_baz",
+ type: "background",
+ });
+
+ should.beVisible("layer-list-item:foo");
+
+ should.notBeVisible("layer-list-item:foo_bar");
+ should.notBeVisible("layer-list-item:foo_bar_baz");
+
+ when.click("layer-list-group:foo-0");
+
+ should.beVisible("layer-list-item:foo");
+ should.beVisible("layer-list-item:foo_bar");
+ should.beVisible("layer-list-item:foo_bar_baz");
+ });
+ });
+});
diff --git a/cypress/e2e/map.cy.ts b/cypress/e2e/map.cy.ts
new file mode 100644
index 00000000..afccd755
--- /dev/null
+++ b/cypress/e2e/map.cy.ts
@@ -0,0 +1,23 @@
+import MaputnikDriver from "./driver";
+
+describe("map", () => {
+ let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
+ beforeAndAfter();
+ describe("zoom level", () => {
+ it("via url", () => {
+ var zoomLevel = 12.37;
+ when.setStyle("geojson", zoomLevel);
+ should.beVisible("maplibre:ctrl-zoom");
+ should.containText("maplibre:ctrl-zoom", "Zoom: " + zoomLevel);
+ });
+
+ it("via map controls", () => {
+ var zoomLevel = 12.37;
+ when.setStyle("geojson", zoomLevel);
+
+ should.beVisible("maplibre:ctrl-zoom");
+ when.clickZoomin();
+ should.containText("maplibre:ctrl-zoom", "Zoom: "+(zoomLevel + 1));
+ });
+ });
+});
diff --git a/cypress/e2e/modals.cy.ts b/cypress/e2e/modals.cy.ts
new file mode 100644
index 00000000..911deef6
--- /dev/null
+++ b/cypress/e2e/modals.cy.ts
@@ -0,0 +1,161 @@
+import MaputnikDriver from "./driver";
+
+describe("modals", () => {
+ let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
+ beforeAndAfter();
+ beforeEach(() => {
+ when.setStyle("");
+ });
+ describe("open", () => {
+ beforeEach(() => {
+ when.click("nav:open");
+ });
+
+ it("close", () => {
+ when.closeModal("modal:open");
+ should.notExist("modal:open");
+ });
+
+ it.skip("upload", () => {
+ // HM: I was not able to make the following choose file actually to select a file and close the modal...
+ when.chooseExampleFile();
+
+ should.styleStoreEqualToExampleFileData();
+ });
+
+ it("load from url", () => {
+ var styleFileUrl = get.exampleFileUrl();
+
+ when.setValue(get.dataAttribute("modal:open.url.input"), styleFileUrl);
+ when.click("modal:open.url.button");
+ when.waitForExampleFileRequset();
+
+ should.styleStoreEqualToExampleFileData();
+ });
+ });
+
+ describe("shortcuts", () => {
+ it("open/close", () => {
+ when.setStyle("");
+ when.typeKeys("?");
+ when.closeModal("modal:shortcuts");
+ should.notExist("modal:shortcuts");
+ });
+ });
+
+ describe("export", () => {
+ beforeEach(() => {
+ when.click("nav:export");
+ });
+
+ it("close", () => {
+ when.closeModal("modal:export");
+ should.notExist("modal:export");
+ });
+
+ // TODO: Work out how to download a file and check the contents
+ it("download");
+ });
+
+ describe("sources", () => {
+ it("active sources");
+ it("public source");
+ it("add new source");
+ });
+
+ describe("inspect", () => {
+ it("toggle", () => {
+ when.setStyle("geojson");
+
+ when.selectWithin("nav:inspect", "inspect");
+ });
+ });
+
+ describe("style settings", () => {
+ beforeEach(() => {
+ when.click("nav:settings");
+ });
+
+ it("name", () => {
+ when.click("field-doc-button-Name");
+
+ should.containText("spec-field-doc", "name for the style");
+ });
+
+ it("show name specifications", () => {
+ when.setValue(get.dataAttribute("modal:settings.name"), "foobar");
+ when.click("modal:settings.owner");
+
+ should.equalStyleStore((obj) => obj.name, "foobar");
+ });
+
+ it("owner", () => {
+ when.setValue(get.dataAttribute("modal:settings.owner"), "foobar");
+ when.click("modal:settings.name");
+
+ should.equalStyleStore((obj) => obj.owner, "foobar");
+ });
+ it("sprite url", () => {
+ when.setValue(
+ get.dataAttribute("modal:settings.sprite"),
+ "http://example.com"
+ );
+ when.click("modal:settings.name");
+
+ should.equalStyleStore((obj) => obj.sprite, "http://example.com");
+ });
+ it("glyphs url", () => {
+ var glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
+ when.setValue(get.dataAttribute("modal:settings.glyphs"), glyphsUrl);
+ when.click("modal:settings.name");
+
+ should.equalStyleStore((obj) => obj.glyphs, glyphsUrl);
+ });
+
+ it("maptiler access token", () => {
+ var apiKey = "testing123";
+ when.setValue(
+ get.dataAttribute(
+ "modal:settings.maputnik:openmaptiles_access_token"
+ ),
+ apiKey
+ );
+ when.click("modal:settings.name");
+
+ should.equalStyleStore(
+ (obj) => obj.metadata["maputnik:openmaptiles_access_token"],
+ apiKey
+ );
+ });
+
+ it("thunderforest access token", () => {
+ var apiKey = "testing123";
+ when.setValue(
+ get.dataAttribute(
+ "modal:settings.maputnik:thunderforest_access_token"
+ ),
+ apiKey
+ );
+ when.click("modal:settings.name");
+
+ should.equalStyleStore(
+ (obj) => obj.metadata["maputnik:thunderforest_access_token"],
+ apiKey
+ );
+ });
+
+ it("style renderer", () => {
+ cy.on("uncaught:exception", () => false); // this is due to the fact that this is an invalid style for openlayers
+ when.select("modal:settings.maputnik:renderer", "ol");
+ should.beSelected("modal:settings.maputnik:renderer", "ol");
+
+ when.click("modal:settings.name");
+
+ should.equalStyleStore((obj) => obj.metadata["maputnik:renderer"], "ol");
+ });
+ });
+
+ describe("sources", () => {
+ it("toggle");
+ });
+});
diff --git a/test/example-layer-style.json b/cypress/fixtures/example-layer-style.json
similarity index 90%
rename from test/example-layer-style.json
rename to cypress/fixtures/example-layer-style.json
index 7a065a18..5c267e30 100644
--- a/test/example-layer-style.json
+++ b/cypress/fixtures/example-layer-style.json
@@ -3,7 +3,7 @@
"version": 8,
"name": "Test Style",
"metadata": {
- "maputnik:renderer": "mbgljs"
+ "maputnik:renderer": "mlgljs"
},
"sources": {},
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
diff --git a/test/example-style.json b/cypress/fixtures/example-style.json
similarity index 88%
rename from test/example-style.json
rename to cypress/fixtures/example-style.json
index f2ec9da7..150ccf84 100644
--- a/test/example-style.json
+++ b/cypress/fixtures/example-style.json
@@ -3,7 +3,7 @@
"version": 8,
"name": "Test Style",
"metadata": {
- "maputnik:renderer": "mbgljs"
+ "maputnik:renderer": "mlgljs"
},
"sources": {},
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
diff --git a/cypress/fixtures/geojson-raster-style.json b/cypress/fixtures/geojson-raster-style.json
new file mode 100644
index 00000000..a069d367
--- /dev/null
+++ b/cypress/fixtures/geojson-raster-style.json
@@ -0,0 +1,34 @@
+{
+ "id": "test-style",
+ "version": 8,
+ "name": "Test Style",
+ "metadata": {
+ "maputnik:renderer": "mlgljs"
+ },
+ "sources": {
+ "example": {
+ "type": "vector",
+ "data": {
+ "type": "FeatureCollection",
+ "features":[{
+ "type": "Feature",
+ "properties": {
+ "name": "Dinagat Islands"
+ },
+ "geometry":{
+ "type": "Point",
+ "coordinates": [125.6, 10.1]
+ }
+ }]
+ }
+ },
+ "raster": {
+ "tileSize": 256,
+ "tiles": ["http://localhost/example/{x}/{y}/{z}"],
+ "type": "raster"
+ }
+ },
+ "glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
+ "sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
+ "layers": []
+ }
\ No newline at end of file
diff --git a/cypress/fixtures/geojson-style.json b/cypress/fixtures/geojson-style.json
new file mode 100644
index 00000000..0eb43a43
--- /dev/null
+++ b/cypress/fixtures/geojson-style.json
@@ -0,0 +1,29 @@
+{
+ "id": "test-style",
+ "version": 8,
+ "name": "Test Style",
+ "metadata": {
+ "maputnik:renderer": "mlgljs"
+ },
+ "sources": {
+ "example": {
+ "type": "vector",
+ "data": {
+ "type": "FeatureCollection",
+ "features":[{
+ "type": "Feature",
+ "properties": {
+ "name": "Dinagat Islands"
+ },
+ "geometry":{
+ "type": "Point",
+ "coordinates": [125.6, 10.1]
+ }
+ }]
+ }
+ }
+ },
+ "glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
+ "sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
+ "layers": []
+ }
\ No newline at end of file
diff --git a/cypress/fixtures/raster-style.json b/cypress/fixtures/raster-style.json
new file mode 100644
index 00000000..8b4d8734
--- /dev/null
+++ b/cypress/fixtures/raster-style.json
@@ -0,0 +1,18 @@
+{
+ "id": "test-style",
+ "version": 8,
+ "name": "Test Style",
+ "metadata": {
+ "maputnik:renderer": "mlgljs"
+ },
+ "sources": {
+ "raster": {
+ "tileSize": 256,
+ "tiles": ["http://localhost/example/{x}/{y}/{z}"],
+ "type": "raster"
+ }
+ },
+ "glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
+ "sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
+ "layers": []
+ }
\ No newline at end of file
diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts
new file mode 100644
index 00000000..698b01a4
--- /dev/null
+++ b/cypress/support/commands.ts
@@ -0,0 +1,37 @@
+///
+// ***********************************************
+// This example commands.ts shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add('login', (email, password) => { ... })
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
+//
+//
+// -- This will overwrite an existing command --
+// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
+//
+// declare global {
+// namespace Cypress {
+// interface Chainable {
+// login(email: string, password: string): Chainable
+// drag(subject: string, options?: Partial): Chainable
+// dismiss(subject: string, options?: Partial): Chainable
+// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable
+// }
+// }
+// }
\ No newline at end of file
diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts
new file mode 100644
index 00000000..1aa9aa2b
--- /dev/null
+++ b/cypress/support/e2e.ts
@@ -0,0 +1,22 @@
+// ***********************************************************
+// This example support/e2e.ts is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import "@cypress/code-coverage/support";
+import "cypress-plugin-tab";
+import "./commands";
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
diff --git a/src/template.html b/index.html
similarity index 94%
rename from src/template.html
rename to index.html
index cfa2a3e9..dc58a2fb 100644
--- a/src/template.html
+++ b/index.html
@@ -2,9 +2,10 @@
- <%= htmlWebpackPlugin.options.title %>
+ Maputnik
-
+
+
-
})
- const infos = this.props.infos.map((m, i) => {
+ const infos = this.props.infos?.map((m, i) => {
return {m}
})
diff --git a/src/components/AppToolbar.jsx b/src/components/AppToolbar.tsx
similarity index 73%
rename from src/components/AppToolbar.jsx
rename to src/components/AppToolbar.tsx
index f6b7dd16..db676a00 100644
--- a/src/components/AppToolbar.jsx
+++ b/src/components/AppToolbar.tsx
@@ -1,38 +1,35 @@
import React from 'react'
-import PropTypes from 'prop-types'
import classnames from 'classnames'
import {detect} from 'detect-browser';
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdAssignmentTurnedIn} from 'react-icons/md'
-
-
-import logoImage from 'maputnik-design/logos/logo-color.svg'
import pkgJson from '../../package.json'
// This is required because of , there isn't another way to detect support that I'm aware of.
const browser = detect();
-const colorAccessibilityFiltersEnabled = ['chrome', 'firefox'].indexOf(browser.name) > -1;
+const colorAccessibilityFiltersEnabled = ['chrome', 'firefox'].indexOf(browser!.name) > -1;
-class IconText extends React.Component {
- static propTypes = {
- children: PropTypes.node,
- }
+type IconTextProps = {
+ children?: React.ReactNode
+};
+
+class IconText extends React.Component {
render() {
return {this.props.children}
}
}
-class ToolbarLink extends React.Component {
- static propTypes = {
- className: PropTypes.string,
- children: PropTypes.node,
- href: PropTypes.string,
- onToggleModal: PropTypes.func,
- }
+type ToolbarLinkProps = {
+ className?: string
+ children?: React.ReactNode
+ href?: string
+ onToggleModal?(...args: unknown[]): unknown
+};
+class ToolbarLink extends React.Component {
render() {
return {
render() {
return {
render() {
return {
render() {
return