diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3044089b..4a7ccfef 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -96,6 +96,7 @@ jobs:
with:
node-version-file: '.nvmrc'
- run: npm ci
+ - run: npx playwright install --with-deps chromium
- run: npm run test-unit-ci
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
@@ -117,17 +118,21 @@ jobs:
with:
node-version-file: '.nvmrc'
- run: npm ci
- - name: Cypress run
- uses: cypress-io/github-action@fa4a118725a8f001170d49631ea89e5d66fee626 # v7.4.1
- with:
- build: npm run build
- start: npm run start
- browser: chrome
+ - run: npx playwright install --with-deps chromium
+ - name: Playwright run
+ run: npm run test-e2e
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
- files: ${{ github.workspace }}/.nyc_output/out.json
+ files: ${{ github.workspace }}/coverage/coverage-final.json
verbose: true
+ - name: Upload Playwright report
+ if: ${{ !cancelled() }}
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: playwright-report
+ path: playwright-report/
+ retention-days: 7
e2e-tests-docker:
name: "E2E tests using chrome and docker"
@@ -141,14 +146,21 @@ jobs:
with:
node-version-file: '.nvmrc'
- run: npm ci
- - name: Cypress run
- uses: cypress-io/github-action@fa4a118725a8f001170d49631ea89e5d66fee626 # v7.4.1
- with:
- build: docker build -t maputnik .
- start: docker run --rm --network host maputnik --port=8888
- browser: chrome
- - name: Upload coverage reports to Codecov
- uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
- with:
- files: ${{ github.workspace }}/.nyc_output/out.json
- verbose: true
+ - run: npx playwright install --with-deps chromium
+ - name: Build docker image
+ run: docker build -t maputnik .
+ - name: Start maputnik container
+ run: docker run -d --network host --name maputnik maputnik --port=8888
+ - name: Wait for maputnik to be ready
+ run: |
+ for i in $(seq 1 60); do
+ if curl -sSf http://localhost:8888/ > /dev/null; then
+ echo "maputnik is up"; exit 0
+ fi
+ sleep 1
+ done
+ echo "maputnik did not start in time"; docker logs maputnik; exit 1
+ - name: Playwright run
+ run: npm run test-e2e
+ env:
+ E2E_NO_WEBSERVER: "1"
diff --git a/.gitignore b/.gitignore
index dba2935c..92d15c11 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,10 +33,14 @@ node_modules
public
/errorShots
/old
-/cypress/screenshots
/dist/
/desktop/version.go
+# Playwright
+/test-results/
+/playwright-report/
+/playwright/.cache/
+
# IDE
.vscode/
.idea/
diff --git a/.nycrc.json b/.nycrc.json
index dfb4729b..b068c670 100644
--- a/.nycrc.json
+++ b/.nycrc.json
@@ -4,12 +4,13 @@
"check-coverage": false,
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": [
- "cypress/**/*.*",
+ "e2e/**/*.*",
"**/*.d.ts",
- "**/*.cy.tsx",
- "**/*.cy.ts",
+ "**/*.test.ts",
+ "**/*.test.tsx",
+ "**/*.browser.test.tsx",
"./coverage/**",
- "./cypress/**",
+ "./e2e/**",
"./dist/**",
"node_modules"
],
diff --git a/AGENTS.md b/AGENTS.md
index 2f1f556c..501d2edb 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -24,22 +24,22 @@ The project type checked and built with:
npm run build
```
-To run the tests make sure that xvfb is installed:
+Install the Playwright browser (first time only):
```
-apt install xvfb
+npx playwright install --with-deps chromium
```
-Run the development server in the background with Vite:
+Then run the end-to-end tests (Playwright starts the dev server automatically):
```
-nohup npm run start &
+npm run test
```
-Then start the Cypress tests with:
+Run the unit and component tests with Vitest:
```
-xvfb-run -a npm run test
+npm run test-unit
```
## Pull Requests
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7a4813ae..5d7a6bff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
## main
### ✨ Features and improvements
+- Replace Cypress with Playwright for end-to-end tests and Vitest browser mode (Playwright provider) for component tests; drop the `@shellygo/cypress-test-utils` helper in favour of a `MaputnikDriver` page object
- _...Add new stuff here..._
### 🐞 Bug fixes
diff --git a/README.md b/README.md
index a62383b7..ea69e7e9 100644
--- a/README.md
+++ b/README.md
@@ -79,26 +79,45 @@ npm run sort-styles
```
## Tests
-For E2E testing we use [Cypress](https://www.cypress.io/)
- [Cypress](https://www.cypress.io/) doesn't start a server so you'll need to start one manually by running `npm run start`.
+### End-to-end tests
-Now open a terminal and run the following using *chrome*:
+For E2E testing we use [Playwright](https://playwright.dev/). The tests live in the [`e2e`](/e2e) directory and drive the app through the `MaputnikDriver` page object.
+
+The first time you run the tests, install the browser:
+
+```
+npx playwright install chromium
+```
+
+Playwright automatically starts the dev server (`npm run start`) for you, so you can just run:
```
npm run test
```
-or *firefox*:
-```
-npm run test -- --browser firefox
-```
-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:
+Some useful options:
```
-npm run cy:open
+# see the tests run in a headed browser
+npm run test -- --headed
+
+# run a single spec / filter by title
+npm run test -- e2e/map.spec.ts
+npm run test -- -g "zoom level"
+
+# open the interactive UI mode
+npx playwright test --ui
+```
+
+Running the E2E tests also produces a code-coverage report in `coverage/` (collected via istanbul instrumentation of the dev server).
+
+### Unit & component tests
+
+Unit tests and component tests run with [Vitest](https://vitest.dev/); component tests (`*.browser.test.tsx`) use Vitest's browser mode with the Playwright provider.
+
+```
+npm run test-unit
```
## Release process
diff --git a/cypress.config.ts b/cypress.config.ts
deleted file mode 100644
index 6c47f29b..00000000
--- a/cypress.config.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { defineConfig } from "cypress";
-import { createRequire } from "module";
-const require = createRequire(import.meta.url);
-
-export default defineConfig({
- env: {
- codeCoverage: {
- exclude: "cypress/**/*.*",
- },
- },
-
- e2e: {
- specPattern: "e2e/**/*.spec.ts",
- setupNodeEvents(on, config) {
- // implement node event listeners here
- require("@cypress/code-coverage/task")(on, config);
- on("before:browser:launch", (browser, launchOptions) => {
- if (browser.family !== "chromium") {
- return;
- }
- launchOptions.args.push("--disable-gpu");
- launchOptions.args.push("--enable-features=AllowSwiftShaderFallback,AllowSoftwareGLFallbackDueToCrashes");
- launchOptions.args.push("--enable-unsafe-swiftshader");
- return launchOptions;
- });
- return config;
- },
- baseUrl: "http://localhost:8888",
- scrollBehavior: "center",
- retries: {
- runMode: 2,
- openMode: 0,
- },
- },
-
- component: {
- devServer: {
- framework: "react",
- bundler: "vite",
- },
- },
-});
diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts
deleted file mode 100644
index 95857aea..00000000
--- a/cypress/support/commands.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-///
-// ***********************************************
-// 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
-// }
-// }
-// }
diff --git a/cypress/support/component-index.html b/cypress/support/component-index.html
deleted file mode 100644
index 5f9622ae..00000000
--- a/cypress/support/component-index.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
- Components App
-
-
-
-
-
diff --git a/cypress/support/component.ts b/cypress/support/component.ts
deleted file mode 100644
index 1b940dc5..00000000
--- a/cypress/support/component.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-// ***********************************************************
-// This example support/component.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 "./commands";
-
-import { mount } from "cypress/react";
-
-// Augment the Cypress namespace to include type definitions for
-// your custom command.
-// Alternatively, can be defined in cypress/support/component.d.ts
-// with a at the top of your spec.
-declare global {
- /* eslint-disable @typescript-eslint/no-namespace */
- namespace Cypress {
- interface Chainable {
- mount: typeof mount
- }
- }
-}
-
-Cypress.Commands.add("mount", mount);
-
-// Example use:
-// cy.mount()
diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts
deleted file mode 100644
index 1aa9aa2b..00000000
--- a/cypress/support/e2e.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-// ***********************************************************
-// 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/e2e/accessibility.spec.ts b/e2e/accessibility.spec.ts
index edbd3a10..0f1f9995 100644
--- a/e2e/accessibility.spec.ts
+++ b/e2e/accessibility.spec.ts
@@ -1,42 +1,48 @@
+import { test, describe, beforeEach } from "./utils/fixtures";
import { MaputnikDriver } from "./maputnik-driver";
-const test = it;
-
describe("accessibility", () => {
- const { beforeAndAfter, get, when, then } = new MaputnikDriver();
- beforeAndAfter();
+ const { given, get, when, then } = new MaputnikDriver();
+
+ beforeEach(async () => {
+ await given.setupMockBackedResponses();
+ await when.setStyle("both");
+ });
describe("skip links", () => {
- beforeEach(() => {
- when.setStyle("layer");
+ beforeEach(async () => {
+ await when.setStyle("layer");
});
- test("skip link to layer list", () => {
+ test("skip link to layer list", async () => {
const selector = "root:skip:layer-list";
- then(get.elementByTestId(selector)).shouldExist();
- when.tab();
- then(get.elementByTestId(selector)).shouldBeFocused();
- when.click(selector);
- then(get.skipTargetLayerList()).shouldBeFocused();
+ await then(get.elementByTestId(selector)).shouldExist();
+ await when.tab();
+ await then(get.elementByTestId(selector)).shouldBeFocused();
+ await when.click(selector);
+ await then(get.skipTargetLayerList()).shouldBeFocused();
});
- test("skip link to layer editor", () => {
+ test("skip link to layer editor", async () => {
const selector = "root:skip:layer-editor";
- then(get.elementByTestId(selector)).shouldExist();
- then(get.elementByTestId("skip-target-layer-editor")).shouldExist();
- when.tab().tab();
- then(get.elementByTestId(selector)).shouldBeFocused();
- when.click(selector);
- then(get.skipTargetLayerEditor()).shouldBeFocused();
+ await then(get.elementByTestId(selector)).shouldExist();
+ await then(get.elementByTestId("skip-target-layer-editor")).shouldExist();
+ await when.tab();
+ await when.tab();
+ await then(get.elementByTestId(selector)).shouldBeFocused();
+ await when.click(selector);
+ await then(get.skipTargetLayerEditor()).shouldBeFocused();
});
- test("skip link to map view", () => {
+ test("skip link to map view", async () => {
const selector = "root:skip:map-view";
- then(get.elementByTestId(selector)).shouldExist();
- when.tab().tab().tab();
- then(get.elementByTestId(selector)).shouldBeFocused();
- when.click(selector);
- then(get.canvas()).shouldBeFocused();
+ await then(get.elementByTestId(selector)).shouldExist();
+ await when.tab();
+ await when.tab();
+ await when.tab();
+ await then(get.elementByTestId(selector)).shouldBeFocused();
+ await when.click(selector);
+ await then(get.canvas()).shouldBeFocused();
});
});
});
diff --git a/e2e/code-editor.spec.ts b/e2e/code-editor.spec.ts
index 70a6b685..131e969f 100644
--- a/e2e/code-editor.spec.ts
+++ b/e2e/code-editor.spec.ts
@@ -1,20 +1,23 @@
+import { beforeEach, describe, test } from "./utils/fixtures";
import { MaputnikDriver } from "./maputnik-driver";
-const test = it;
-
describe("code editor", () => {
- const { beforeAndAfter, when, get, then } = new MaputnikDriver();
- beforeAndAfter();
+ const { given, get, when, then } = new MaputnikDriver();
- test("open code editor", () => {
- when.click("nav:code-editor");
- then(get.element(".maputnik-code-editor")).shouldExist();
+ beforeEach(async () => {
+ await given.setupMockBackedResponses();
+ await when.setStyle("both");
});
- test("closes code editor", () => {
- when.click("nav:code-editor");
- then(get.element(".maputnik-code-editor")).shouldExist();
- when.click("nav:code-editor");
- then(get.element(".maputnik-code-editor")).shouldNotExist();
+ test("open code editor", async () => {
+ await when.click("nav:code-editor");
+ await then(get.element(".maputnik-code-editor")).shouldExist();
+ });
+
+ test("closes code editor", async () => {
+ await when.click("nav:code-editor");
+ await then(get.element(".maputnik-code-editor")).shouldExist();
+ await when.click("nav:code-editor");
+ await then(get.element(".maputnik-code-editor")).shouldNotExist();
});
});
diff --git a/cypress/fixtures/example-layer-style.json b/e2e/fixtures/example-layer-style.json
similarity index 100%
rename from cypress/fixtures/example-layer-style.json
rename to e2e/fixtures/example-layer-style.json
diff --git a/cypress/fixtures/example-style-with-fonts.json b/e2e/fixtures/example-style-with-fonts.json
similarity index 100%
rename from cypress/fixtures/example-style-with-fonts.json
rename to e2e/fixtures/example-style-with-fonts.json
diff --git a/cypress/fixtures/example-style-with-zoom-5-and-center-50-50.json b/e2e/fixtures/example-style-with-zoom-5-and-center-50-50.json
similarity index 100%
rename from cypress/fixtures/example-style-with-zoom-5-and-center-50-50.json
rename to e2e/fixtures/example-style-with-zoom-5-and-center-50-50.json
diff --git a/cypress/fixtures/example-style-with-zoom-7-and-center-0-51.json b/e2e/fixtures/example-style-with-zoom-7-and-center-0-51.json
similarity index 100%
rename from cypress/fixtures/example-style-with-zoom-7-and-center-0-51.json
rename to e2e/fixtures/example-style-with-zoom-7-and-center-0-51.json
diff --git a/cypress/fixtures/example-style.json b/e2e/fixtures/example-style.json
similarity index 100%
rename from cypress/fixtures/example-style.json
rename to e2e/fixtures/example-style.json
diff --git a/cypress/fixtures/geojson-raster-style.json b/e2e/fixtures/geojson-raster-style.json
similarity index 100%
rename from cypress/fixtures/geojson-raster-style.json
rename to e2e/fixtures/geojson-raster-style.json
diff --git a/cypress/fixtures/geojson-style.json b/e2e/fixtures/geojson-style.json
similarity index 100%
rename from cypress/fixtures/geojson-style.json
rename to e2e/fixtures/geojson-style.json
diff --git a/cypress/fixtures/raster-style.json b/e2e/fixtures/raster-style.json
similarity index 100%
rename from cypress/fixtures/raster-style.json
rename to e2e/fixtures/raster-style.json
diff --git a/cypress/fixtures/rectangles-style.json b/e2e/fixtures/rectangles-style.json
similarity index 100%
rename from cypress/fixtures/rectangles-style.json
rename to e2e/fixtures/rectangles-style.json
diff --git a/e2e/history.spec.ts b/e2e/history.spec.ts
index 811fc3f9..aa36e194 100644
--- a/e2e/history.spec.ts
+++ b/e2e/history.spec.ts
@@ -1,126 +1,90 @@
+import { beforeEach, describe, test } from "./utils/fixtures";
import { MaputnikDriver } from "./maputnik-driver";
-const test = it;
-
describe("history", () => {
- const { beforeAndAfter, when, get, then } = new MaputnikDriver();
- beforeAndAfter();
+ const { given, get, when, then } = new MaputnikDriver();
- let undoKeyCombo: string;
- let redoKeyCombo: string;
+ const undoKeyCombo = process.platform === "darwin" ? "{meta}z" : "{ctrl}z";
+ const redoKeyCombo = process.platform === "darwin" ? "{meta}{shift}z" : "{ctrl}y";
- before(() => {
- const isMac = get.isMac();
- undoKeyCombo = isMac ? "{meta}z" : "{ctrl}z";
- redoKeyCombo = isMac ? "{meta}{shift}z" : "{ctrl}y";
+ beforeEach(async () => {
+ await given.setupMockBackedResponses();
+ await when.setStyle("both");
});
- test("undo/redo", () => {
- when.setStyle("geojson");
- when.modal.open();
+ test("undo/redo", async () => {
+ await when.setStyle("geojson");
+ await when.modal.open();
- when.modal.fillLayers({
+ await when.modal.fillLayers({
id: "step 1",
type: "background",
});
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: "step 1",
- type: "background",
- },
- ],
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id: "step 1", type: "background" }],
});
- when.modal.open();
- when.modal.fillLayers({
+ await when.modal.open();
+ await when.modal.fillLayers({
id: "step 2",
type: "background",
});
-
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
- {
- id: "step 1",
- type: "background",
- },
- {
- id: "step 2",
- type: "background",
- },
+ { id: "step 1", type: "background" },
+ { id: "step 2", type: "background" },
],
});
- when.typeKeys(undoKeyCombo);
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: "step 1",
- type: "background",
- },
- ],
+ await when.typeKeys(undoKeyCombo);
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id: "step 1", type: "background" }],
});
- when.typeKeys(undoKeyCombo);
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
+ await when.typeKeys(undoKeyCombo);
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
- when.typeKeys(redoKeyCombo);
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: "step 1",
- type: "background",
- },
- ],
+ await when.typeKeys(redoKeyCombo);
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id: "step 1", type: "background" }],
});
- when.typeKeys(redoKeyCombo);
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ await when.typeKeys(redoKeyCombo);
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
- {
- id: "step 1",
- type: "background",
- },
- {
- id: "step 2",
- type: "background",
- },
+ { id: "step 1", type: "background" },
+ { id: "step 2", type: "background" },
],
});
});
- test("should not redo after undo and value change", () => {
- when.setStyle("geojson");
- when.modal.open();
- when.modal.fillLayers({
+ test("should not redo after undo and value change", async () => {
+ await when.setStyle("geojson");
+ await when.modal.open();
+ await when.modal.fillLayers({
id: "step 1",
type: "background",
});
- when.modal.open();
- when.modal.fillLayers({
+ await when.modal.open();
+ await when.modal.fillLayers({
id: "step 2",
type: "background",
});
- when.typeKeys(undoKeyCombo);
- when.typeKeys(undoKeyCombo);
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
+ await when.typeKeys(undoKeyCombo);
+ await when.typeKeys(undoKeyCombo);
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
- when.modal.open();
- when.modal.fillLayers({
+ await when.modal.open();
+ await when.modal.fillLayers({
id: "step 3",
type: "background",
});
- when.typeKeys(redoKeyCombo);
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: "step 3",
- type: "background",
- },
- ],
+ await when.typeKeys(redoKeyCombo);
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id: "step 3", type: "background" }],
});
});
});
diff --git a/e2e/i18n.spec.ts b/e2e/i18n.spec.ts
index 33d8f3c6..adf02878 100644
--- a/e2e/i18n.spec.ts
+++ b/e2e/i18n.spec.ts
@@ -1,37 +1,38 @@
+import { beforeEach, describe, test } from "./utils/fixtures";
import { MaputnikDriver } from "./maputnik-driver";
-const test = it;
-
describe("i18n", () => {
- const { beforeAndAfter, get, when, then } = new MaputnikDriver();
- beforeAndAfter();
+ const { given, get, when, then } = new MaputnikDriver();
+
+ beforeEach(async () => {
+ await given.setupMockBackedResponses();
+ await when.setStyle("both");
+ });
describe("language detector", () => {
- test("English", () => {
- const url = "?lng=en";
- when.visit(url);
- then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("en");
+ test("English", async () => {
+ await when.visit("?lng=en");
+ await then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("en");
});
- test("Japanese", () => {
- const url = "?lng=ja";
- when.visit(url);
- then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("ja");
+ test("Japanese", async () => {
+ await when.visit("?lng=ja");
+ await then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("ja");
});
});
describe("language switcher", () => {
- beforeEach(() => {
- when.setStyle("layer");
+ beforeEach(async () => {
+ await when.setStyle("layer");
});
- test("the language switcher switches to Japanese", () => {
+ test("the language switcher switches to Japanese", async () => {
const selector = "maputnik-lang-select";
- then(get.elementByTestId(selector)).shouldExist();
- when.select(selector, "ja");
- then(get.elementByTestId(selector)).shouldHaveValue("ja");
+ await then(get.elementByTestId(selector)).shouldExist();
+ await when.select(selector, "ja");
+ await then(get.elementByTestId(selector)).shouldHaveValue("ja");
- then(get.elementByTestId("nav:settings")).shouldHaveText("スタイル設定");
+ await then(get.elementByTestId("nav:settings")).shouldHaveText("スタイル設定");
});
});
});
diff --git a/e2e/keyboard.spec.ts b/e2e/keyboard.spec.ts
index baf9595d..8b67f12f 100644
--- a/e2e/keyboard.spec.ts
+++ b/e2e/keyboard.spec.ts
@@ -1,61 +1,66 @@
+import { beforeEach, describe, test } from "./utils/fixtures";
import { MaputnikDriver } from "./maputnik-driver";
-const test = it;
describe("keyboard", () => {
- const { beforeAndAfter, given, when, get, then } = new MaputnikDriver();
- beforeAndAfter();
+ const { given, get, when, then } = new MaputnikDriver();
+
+ beforeEach(async () => {
+ await given.setupMockBackedResponses();
+ await when.setStyle("both");
+ });
+
describe("shortcuts", () => {
- beforeEach(() => {
- given.setupMockBackedResponses();
- when.setStyle("");
+ beforeEach(async () => {
+ await given.setupMockBackedResponses();
+ await when.setStyle("");
});
- test("ESC should unfocus", () => {
+ test("ESC should unfocus", async () => {
const targetSelector = "maputnik-select";
- when.focus(targetSelector);
- then(get.elementByTestId(targetSelector)).shouldBeFocused();
- when.typeKeys("{esc}");
- then(get.elementByTestId(targetSelector)).shouldNotBeFocused();
+ await when.focus(targetSelector);
+ await then(get.elementByTestId(targetSelector)).shouldBeFocused();
+ await when.typeKeys("{esc}");
+ await then(get.elementByTestId(targetSelector)).shouldNotBeFocused();
});
- test("'?' should show shortcuts modal", () => {
- when.typeKeys("?");
- then(get.elementByTestId("modal:shortcuts")).shouldBeVisible();
+ test("'?' should show shortcuts modal", async () => {
+ await when.typeKeys("?");
+ await then(get.elementByTestId("modal:shortcuts")).shouldBeVisible();
});
- test("'o' should show open modal", () => {
- when.typeKeys("o");
- then(get.elementByTestId("modal:open")).shouldBeVisible();
+ test("'o' should show open modal", async () => {
+ await when.typeKeys("o");
+ await then(get.elementByTestId("modal:open")).shouldBeVisible();
});
- test("'e' should show export modal", () => {
- when.typeKeys("e");
- then(get.elementByTestId("modal:export")).shouldBeVisible();
+ test("'e' should show export modal", async () => {
+ await when.typeKeys("e");
+ await then(get.elementByTestId("modal:export")).shouldBeVisible();
});
- test("'d' should show sources modal", () => {
- when.typeKeys("d");
- then(get.elementByTestId("modal:sources")).shouldBeVisible();
+ test("'d' should show sources modal", async () => {
+ await when.typeKeys("d");
+ await then(get.elementByTestId("modal:sources")).shouldBeVisible();
});
- test("'s' should show settings modal", () => {
- when.typeKeys("s");
- then(get.elementByTestId("modal:settings")).shouldBeVisible();
+ test("'s' should show settings modal", async () => {
+ await when.typeKeys("s");
+ await then(get.elementByTestId("modal:settings")).shouldBeVisible();
});
- test("'i' should change map to inspect mode", () => {
- when.typeKeys("i");
- then(get.inputValue("maputnik-select")).shouldEqual("inspect");
+ test("'i' should change map to inspect mode", async () => {
+ await when.typeKeys("i");
+ await then(get.inputValue("maputnik-select")).shouldEqual("inspect");
});
- test("'m' should focus map", () => {
- when.typeKeys("m");
- then(get.canvas()).shouldBeFocused();
+ test("'m' should focus map", async () => {
+ await when.typeKeys("m");
+ await then(get.canvas()).shouldBeFocused();
});
- test("'!' should show debug modal", () => {
- when.typeKeys("!");
- then(get.elementByTestId("modal:debug")).shouldBeVisible();
+ test("'!' should show debug modal", async () => {
+ await when.typeKeys("!");
+ await then(get.elementByTestId("modal:debug")).shouldBeVisible();
});
});
});
diff --git a/e2e/layer-editor.spec.ts b/e2e/layer-editor.spec.ts
index 1b3b074d..c510315b 100644
--- a/e2e/layer-editor.spec.ts
+++ b/e2e/layer-editor.spec.ts
@@ -1,98 +1,79 @@
-import { MaputnikDriver } from "./maputnik-driver";
import { v1 as uuid } from "uuid";
-
-const test = it;
+import { beforeEach, describe, test } from "./utils/fixtures";
+import { MaputnikDriver } from "./maputnik-driver";
describe("layer editor", () => {
- const { beforeAndAfter, get, when, then } = new MaputnikDriver();
- beforeAndAfter();
- beforeEach(() => {
- when.setStyle("both");
- when.modal.open();
+ const { given, get, when, then } = new MaputnikDriver();
+
+ beforeEach(async () => {
+ await given.setupMockBackedResponses();
+ await when.setStyle("both");
+ await when.modal.open();
});
- function createBackground() {
+ async function createBackground() {
const id = uuid();
- when.selectWithin("add-layer.layer-type", "background");
- when.setValue("add-layer.layer-id.input", "background:" + id);
+ await when.selectWithin("add-layer.layer-type", "background");
+ await when.setValue("add-layer.layer-id.input", "background:" + id);
- when.click("add-layer");
+ await when.click("add-layer");
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: "background:" + id,
- type: "background",
- },
- ],
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id: "background:" + id, type: "background" }],
});
return id;
}
- test("expand/collapse");
- test("id", () => {
- const bgId = createBackground();
+ test.skip("expand/collapse", () => {});
- when.click("layer-list-item:background:" + bgId);
+ test("id", async () => {
+ const bgId = await createBackground();
+
+ await when.click("layer-list-item:background:" + bgId);
const id = uuid();
- when.setValue("layer-editor.layer-id.input", "foobar:" + id);
- when.click("min-zoom");
+ await when.setValue("layer-editor.layer-id.input", "foobar:" + id);
+ await when.click("min-zoom");
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: "foobar:" + id,
- type: "background",
- },
- ],
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id: "foobar:" + id, type: "background" }],
});
});
describe("source", () => {
- test("should show error when the source is invalid", () => {
- when.modal.fillLayers({
+ test("should show error when the source is invalid", async () => {
+ await when.modal.fillLayers({
type: "circle",
layer: "invalid",
});
- then(get.element(".maputnik-input-block--error .maputnik-input-block-label")).shouldHaveCss("color", "rgb(207, 74, 74)");
+ await then(
+ get.element(".maputnik-input-block--error .maputnik-input-block-label")
+ ).shouldHaveCss("color", "rgb(207, 74, 74)");
});
});
describe("min-zoom", () => {
let bgId: string;
- beforeEach(() => {
- bgId = createBackground();
- when.click("layer-list-item:background:" + bgId);
- when.setValue("min-zoom.input-text", "1");
- when.click("layer-editor.layer-id");
+ beforeEach(async () => {
+ bgId = await createBackground();
+ await when.click("layer-list-item:background:" + bgId);
+ await when.setValue("min-zoom.input-text", "1");
+ await when.click("layer-editor.layer-id");
});
- test("should update min-zoom in local storage", () => {
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: "background:" + bgId,
- type: "background",
- minzoom: 1,
- },
- ],
+ test("should update min-zoom in local storage", async () => {
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id: "background:" + bgId, type: "background", minzoom: 1 }],
});
});
- it("when clicking next layer should update style on local storage", () => {
- when.type("min-zoom.input-text", "{backspace}");
- when.click("max-zoom.input-text");
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: "background:" + bgId,
- type: "background",
- minzoom: 1,
- },
- ],
+ test("when clicking next layer should update style on local storage", async () => {
+ await when.type("min-zoom.input-text", "{backspace}");
+ await when.click("max-zoom.input-text");
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id: "background:" + bgId, type: "background", minzoom: 1 }],
});
});
});
@@ -100,22 +81,16 @@ describe("layer editor", () => {
describe("max-zoom", () => {
let bgId: string;
- beforeEach(() => {
- bgId = createBackground();
- when.click("layer-list-item:background:" + bgId);
- when.setValue("max-zoom.input-text", "1");
- when.click("layer-editor.layer-id");
+ beforeEach(async () => {
+ bgId = await createBackground();
+ await when.click("layer-list-item:background:" + bgId);
+ await when.setValue("max-zoom.input-text", "1");
+ await when.click("layer-editor.layer-id");
});
- test("should update style in local storage", () => {
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: "background:" + bgId,
- type: "background",
- maxzoom: 1,
- },
- ],
+ test("should update style in local storage", async () => {
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id: "background:" + bgId, type: "background", maxzoom: 1 }],
});
});
});
@@ -124,41 +99,34 @@ describe("layer editor", () => {
let bgId: string;
const comment = "42";
- beforeEach(() => {
- bgId = createBackground();
- when.click("layer-list-item:background:" + bgId);
- when.setValue("layer-comment.input", comment);
- when.click("layer-editor.layer-id");
+ beforeEach(async () => {
+ bgId = await createBackground();
+ await when.click("layer-list-item:background:" + bgId);
+ await when.setValue("layer-comment.input", comment);
+ await when.click("layer-editor.layer-id");
});
- test("should update style in local storage", () => {
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ test("should update style in local storage", async () => {
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + bgId,
type: "background",
- metadata: {
- "maputnik:comment": comment,
- },
+ metadata: { "maputnik:comment": comment },
},
],
});
});
describe("when unsetting", () => {
- beforeEach(() => {
- when.clear("layer-comment.input");
- when.click("min-zoom.input-text");
+ beforeEach(async () => {
+ await when.clear("layer-comment.input");
+ await when.click("min-zoom.input-text");
});
- test("should update style in local storage", () => {
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: "background:" + bgId,
- type: "background",
- },
- ],
+ test("should update style in local storage", async () => {
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id: "background:" + bgId, type: "background" }],
});
});
});
@@ -166,131 +134,113 @@ describe("layer editor", () => {
describe("color", () => {
let bgId: string;
- beforeEach(() => {
- bgId = createBackground();
- when.click("layer-list-item:background:" + bgId);
- when.click("spec-field:background-color");
+ beforeEach(async () => {
+ bgId = await createBackground();
+ await when.click("layer-list-item:background:" + bgId);
+ await when.click("spec-field:background-color");
});
- test("should update style in local storage", () => {
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: "background:" + bgId,
- type: "background",
- },
- ],
+ test("should update style in local storage", async () => {
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id: "background:" + bgId, type: "background" }],
});
});
});
describe("opacity", () => {
let bgId: string;
- beforeEach(() => {
- bgId = createBackground();
- when.click("layer-list-item:background:" + bgId);
- when.type("spec-field-input:background-opacity", "0.");
+ beforeEach(async () => {
+ bgId = await createBackground();
+ await when.click("layer-list-item:background:" + bgId);
+ await when.type("spec-field-input:background-opacity", "0.");
});
- test("should keep '.' in the input field", () => {
- then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0.");
+ test("should keep '.' in the input field", async () => {
+ await then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0.");
});
- test("should revert to a valid value when focus out", () => {
- when.click("layer-list-item:background:" + bgId);
- then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0");
+ test("should revert to a valid value when focus out", async () => {
+ await when.click("layer-list-item:background:" + bgId);
+ await then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0");
});
});
-
-
describe("filter", () => {
- test("expand/collapse");
- test("compound filter");
+ test.skip("expand/collapse", () => {});
+ test.skip("compound filter", () => {});
});
describe("layout", () => {
- test("text-font", () => {
- when.setStyle("font");
- when.collapseGroupInLayerEditor();
- when.collapseGroupInLayerEditor(1);
- when.collapseGroupInLayerEditor(2);
- when.doWithin("spec-field:text-font", () => {
- get.element(".maputnik-autocomplete input").first().click();
- });
- then(get.element(".maputnik-autocomplete-menu-item")).shouldBeVisible();
- then(get.element(".maputnik-autocomplete-menu-item")).shouldHaveLength(3);
+ test("text-font", async () => {
+ await when.setStyle("font");
+ await when.collapseGroupInLayerEditor();
+ await when.collapseGroupInLayerEditor(1);
+ await when.collapseGroupInLayerEditor(2);
+ await when.clickWithin("spec-field:text-font", ".maputnik-autocomplete input");
+ await then(get.element(".maputnik-autocomplete-menu-item")).shouldBeVisible();
+ await then(get.element(".maputnik-autocomplete-menu-item")).shouldHaveLength(3);
});
});
describe("paint", () => {
- test("expand/collapse");
- test("color");
- test("pattern");
- test("opacity");
+ test.skip("expand/collapse", () => {});
+ test.skip("color", () => {});
+ test.skip("pattern", () => {});
+ test.skip("opacity", () => {});
});
describe("json-editor", () => {
- test("add", () => {
- const id = when.modal.fillLayers({
+ test("add", async () => {
+ const id = await when.modal.fillLayers({
type: "circle",
layer: "example",
});
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: id,
- type: "circle",
- source: "example",
- },
- ],
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id, type: "circle", source: "example" }],
});
- const sourceText = get.elementByText('"source"');
+ await when.clickByText('"source"');
+ await when.typeKeys('"');
- sourceText.click();
- sourceText.type("\"");
-
- then(get.element(".cm-lint-marker-error")).shouldExist();
+ await then(get.element(".cm-lint-marker-error")).shouldExist();
});
+ test.skip("expand/collapse", () => {});
+ test.skip("modify", () => {});
- test("expand/collapse");
- test("modify");
+ test("parse error", async () => {
+ const bgId = await createBackground();
- test("parse error", () => {
- const bgId = createBackground();
+ await when.click("layer-list-item:background:" + bgId);
+ await when.collapseGroupInLayerEditor();
+ await when.collapseGroupInLayerEditor(1);
+ await then(get.element(".cm-lint-marker-error")).shouldNotExist();
- when.click("layer-list-item:background:" + bgId);
- when.collapseGroupInLayerEditor();
- when.collapseGroupInLayerEditor(1);
- then(get.element(".cm-lint-marker-error")).shouldNotExist();
-
- when.appendTextInJsonEditor(
- "\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {"
- );
- then(get.element(".cm-lint-marker-error")).shouldExist();
+ // Inject an invalid token (CodeMirror auto-closes brackets/quotes, so a
+ // bare word reliably breaks the JSON) and expect a lint error.
+ await when.appendTextInJsonEditor("zzz");
+ await then(get.element(".cm-lint-marker-error")).shouldExist();
});
});
describe("sticky header", () => {
- test("should keep layer header visible when scrolling properties", () => {
- // Setup: Create a layer with many properties (e.g., symbol layer)
- when.modal.fillLayers({
+ test("should keep layer header visible when scrolling properties", async () => {
+ // Setup: Create a layer with many properties (e.g. symbol layer)
+ await when.modal.fillLayers({
type: "symbol",
layer: "example",
});
- when.wait(500);
+ await when.wait(500);
const header = get.elementByTestId("layer-editor.header");
- then(header).shouldBeVisible();
+ await then(header).shouldBeVisible();
- get.element(".maputnik-scroll-container").scrollTo("bottom", { ensureScrollable: false });
- when.wait(200);
+ await when.scrollToBottom(get.element(".maputnik-scroll-container"));
+ await when.wait(200);
- then(header).shouldBeVisible();
- then(get.elementByTestId("skip-target-layer-editor")).shouldBeVisible();
+ await then(header).shouldBeVisible();
+ await then(get.elementByTestId("skip-target-layer-editor")).shouldBeVisible();
});
});
});
diff --git a/e2e/layers-list.spec.ts b/e2e/layers-list.spec.ts
index 41e6705e..786a1b3d 100644
--- a/e2e/layers-list.spec.ts
+++ b/e2e/layers-list.spec.ts
@@ -1,412 +1,265 @@
+import { beforeEach, describe, test } from "./utils/fixtures";
import { MaputnikDriver } from "./maputnik-driver";
-const test = it;
describe("layers list", () => {
- const { beforeAndAfter, get, when, then } = new MaputnikDriver();
- beforeAndAfter();
- beforeEach(() => {
- when.setStyle("both");
- when.modal.open();
+ const { given, get, when, then } = new MaputnikDriver();
+
+ beforeEach(async () => {
+ await given.setupMockBackedResponses();
+ await when.setStyle("both");
+ await when.modal.open();
});
describe("ops", () => {
let id: string;
- beforeEach(() => {
- id = when.modal.fillLayers({
- type: "background",
- });
+ beforeEach(async () => {
+ id = await when.modal.fillLayers({ type: "background" });
});
- test("should update layers in local storage", () => {
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: id,
- type: "background",
- },
- ],
+ test("should update layers in local storage", async () => {
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id, type: "background" }],
});
});
describe("when clicking delete", () => {
- beforeEach(() => {
- when.click("layer-list-item:" + id + ":delete");
+ beforeEach(async () => {
+ await when.click("layer-list-item:" + id + ":delete");
});
- test("should empty layers in local storage", () => {
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ test("should empty layers in local storage", async () => {
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [],
});
});
});
describe("when clicking duplicate", () => {
- beforeEach(() => {
- when.click("layer-list-item:" + id + ":copy");
+ beforeEach(async () => {
+ await when.click("layer-list-item:" + id + ":copy");
});
- test("should add copy layer in local storage", () => {
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ test("should add copy layer in local storage", async () => {
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
- {
- id: id + "-copy",
- type: "background",
- },
- {
- id: id,
- type: "background",
- },
+ { id: id + "-copy", type: "background" },
+ { id, type: "background" },
],
});
});
});
describe("when clicking hide", () => {
- beforeEach(() => {
- when.click("layer-list-item:" + id + ":toggle-visibility");
+ beforeEach(async () => {
+ await when.click("layer-list-item:" + id + ":toggle-visibility");
});
- test("should update visibility to none in local storage", () => {
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: id,
- type: "background",
- layout: {
- visibility: "none",
- },
- },
- ],
+ test("should update visibility to none in local storage", async () => {
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id, type: "background", layout: { visibility: "none" } }],
});
});
describe("when clicking show", () => {
- beforeEach(() => {
- when.click("layer-list-item:" + id + ":toggle-visibility");
+ beforeEach(async () => {
+ await when.click("layer-list-item:" + id + ":toggle-visibility");
});
- test("should update visibility to visible in local storage", () => {
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: id,
- type: "background",
- layout: {
- visibility: "visible",
- },
- },
- ],
+ test("should update visibility to visible in local storage", async () => {
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id, type: "background", layout: { visibility: "visible" } }],
});
});
});
describe("when selecting a layer", () => {
let secondId: string;
- beforeEach(() => {
- when.modal.open();
- secondId = when.modal.fillLayers({
+ beforeEach(async () => {
+ await when.modal.open();
+ secondId = await when.modal.fillLayers({
id: "second-layer",
type: "background",
});
});
- test("should show the selected layer in the editor", () => {
- when.realClick("layer-list-item:" + secondId);
- then(get.elementByTestId("layer-editor.layer-id.input")).shouldHaveValue(secondId);
- when.realClick("layer-list-item:" + id);
- then(get.elementByTestId("layer-editor.layer-id.input")).shouldHaveValue(id);
+ test("should show the selected layer in the editor", async () => {
+ await when.realClick("layer-list-item:" + secondId);
+ await then(get.elementByTestId("layer-editor.layer-id.input")).shouldHaveValue(secondId);
+ await when.realClick("layer-list-item:" + id);
+ await then(get.elementByTestId("layer-editor.layer-id.input")).shouldHaveValue(id);
});
});
});
});
describe("background", () => {
- test("add", () => {
- const id = when.modal.fillLayers({
- type: "background",
- });
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: id,
- type: "background",
- },
- ],
+ test("add", async () => {
+ const id = await when.modal.fillLayers({ type: "background" });
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id, type: "background" }],
});
});
- describe("modify", () => {});
+ test.skip("modify", () => {});
});
describe("fill", () => {
- test("add", () => {
- const id = when.modal.fillLayers({
- type: "fill",
- layer: "example",
- });
-
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: id,
- type: "fill",
- source: "example",
- },
- ],
+ test("add", async () => {
+ const id = await when.modal.fillLayers({ type: "fill", layer: "example" });
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id, type: "fill", source: "example" }],
});
});
// TODO: Change source
- test("change source");
+ test.skip("change source", () => {});
});
describe("line", () => {
- test("add", () => {
- const id = when.modal.fillLayers({
- type: "line",
- layer: "example",
- });
-
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: id,
- type: "line",
- source: "example",
- },
- ],
+ test("add", async () => {
+ const id = await when.modal.fillLayers({ type: "line", layer: "example" });
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id, type: "line", source: "example" }],
});
});
- test("groups", () => {
- when.modal.open();
- const id1 = when.modal.fillLayers({
- id: "aa",
- type: "line",
- layer: "example",
- });
+ test("groups", async () => {
+ const id1 = await when.modal.fillLayers({ id: "aa", type: "line", layer: "example" });
- when.modal.open();
- const id2 = when.modal.fillLayers({
- id: "aa-2",
- type: "line",
- layer: "example",
- });
+ await when.modal.open();
+ const id2 = await when.modal.fillLayers({ id: "aa-2", type: "line", layer: "example" });
- when.modal.open();
- const id3 = when.modal.fillLayers({
- id: "b",
- type: "line",
- layer: "example",
- });
+ await when.modal.open();
+ const id3 = await when.modal.fillLayers({ id: "b", type: "line", layer: "example" });
- then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible();
- then(get.elementByTestId("layer-list-item:" + id2)).shouldNotBeVisible();
- then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible();
- when.click("layer-list-group:aa-0");
- then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible();
- then(get.elementByTestId("layer-list-item:" + id2)).shouldBeVisible();
- then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible();
- when.click("layer-list-item:" + id2);
- when.click("skip-target-layer-editor");
- when.click("menu-move-layer-down");
- then(get.elementByTestId("layer-list-group:aa-0")).shouldNotExist();
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ await then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible();
+ await then(get.elementByTestId("layer-list-item:" + id2)).shouldNotBeVisible();
+ await then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible();
+ await when.click("layer-list-group:aa-0");
+ await then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible();
+ await then(get.elementByTestId("layer-list-item:" + id2)).shouldBeVisible();
+ await then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible();
+ await when.click("layer-list-item:" + id2);
+ await when.click("skip-target-layer-editor");
+ await when.click("menu-move-layer-down");
+ await then(get.elementByTestId("layer-list-group:aa-0")).shouldNotExist();
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
- {
- id: "aa",
- type: "line",
- source: "example",
- },
- {
- id: "b",
- type: "line",
- source: "example",
- },
- {
- id: "aa-2",
- type: "line",
- source: "example",
- },
+ { id: "aa", type: "line", source: "example" },
+ { id: "b", type: "line", source: "example" },
+ { id: "aa-2", type: "line", source: "example" },
],
});
});
});
describe("symbol", () => {
- test("add", () => {
- const id = when.modal.fillLayers({
- type: "symbol",
- layer: "example",
- });
-
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: id,
- type: "symbol",
- source: "example",
- },
- ],
+ test("add", async () => {
+ const id = await when.modal.fillLayers({ type: "symbol", layer: "example" });
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id, type: "symbol", source: "example" }],
});
});
- test("should show spec info when hovering and clicking single line property", () => {
- when.modal.fillLayers({
- type: "symbol",
- layer: "example",
- });
+ test("should show spec info when hovering and clicking single line property", async () => {
+ await when.modal.fillLayers({ type: "symbol", layer: "example" });
- when.hover("spec-field-container:text-rotate");
- then(get.elementByTestId("field-doc-button-Rotate")).shouldBeVisible();
- when.click("field-doc-button-Rotate", 0);
- then(get.elementByTestId("spec-field-doc")).shouldContainText("Rotates the ");
+ await when.hover("spec-field-container:text-rotate");
+ await then(get.elementByTestId("field-doc-button-Rotate")).shouldBeVisible();
+ await when.click("field-doc-button-Rotate", 0);
+ await then(get.elementByTestId("spec-field-doc")).shouldContainText("Rotates the ");
});
- test("should show spec info when hovering and clicking multi line property", () => {
- when.modal.fillLayers({
- type: "symbol",
- layer: "example",
- });
+ test("should show spec info when hovering and clicking multi line property", async () => {
+ await when.modal.fillLayers({ type: "symbol", layer: "example" });
- when.hover("spec-field-container:text-offset");
- then(get.elementByTestId("field-doc-button-Offset")).shouldBeVisible();
- when.click("field-doc-button-Offset", 0);
- then(get.elementByTestId("spec-field-doc")).shouldContainText("Offset distance");
+ await when.hover("spec-field-container:text-offset");
+ await then(get.elementByTestId("field-doc-button-Offset")).shouldBeVisible();
+ await when.click("field-doc-button-Offset", 0);
+ await then(get.elementByTestId("spec-field-doc")).shouldContainText("Offset distance");
});
- test("should hide spec info when clicking a second time", () => {
- when.modal.fillLayers({
- type: "symbol",
- layer: "example",
- });
+ test("should hide spec info when clicking a second time", async () => {
+ await when.modal.fillLayers({ type: "symbol", layer: "example" });
- when.hover("spec-field-container:text-rotate");
- then(get.elementByTestId("field-doc-button-Rotate")).shouldBeVisible();
- when.click("field-doc-button-Rotate", 0);
- when.wait(200);
- when.click("field-doc-button-Rotate", 0);
- then(get.elementByTestId("spec-field-doc")).shouldNotBeVisible();
+ await when.hover("spec-field-container:text-rotate");
+ await then(get.elementByTestId("field-doc-button-Rotate")).shouldBeVisible();
+ await when.click("field-doc-button-Rotate", 0);
+ await when.wait(200);
+ await when.click("field-doc-button-Rotate", 0);
+ await then(get.elementByTestId("spec-field-doc")).shouldNotBeVisible();
});
});
describe("raster", () => {
- test("add", () => {
- const id = when.modal.fillLayers({
- type: "raster",
- layer: "raster",
- });
-
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: id,
- type: "raster",
- source: "raster",
- },
- ],
+ test("add", async () => {
+ const id = await when.modal.fillLayers({ type: "raster", layer: "raster" });
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id, type: "raster", source: "raster" }],
});
});
});
describe("circle", () => {
- test("add", () => {
- const id = when.modal.fillLayers({
- type: "circle",
- layer: "example",
- });
-
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: id,
- type: "circle",
- source: "example",
- },
- ],
+ test("add", async () => {
+ const id = await when.modal.fillLayers({ type: "circle", layer: "example" });
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id, type: "circle", source: "example" }],
});
});
});
describe("fill extrusion", () => {
- test("add", () => {
- const id = when.modal.fillLayers({
- type: "fill-extrusion",
- layer: "example",
- });
-
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: id,
- type: "fill-extrusion",
- source: "example",
- },
- ],
+ test("add", async () => {
+ const id = await when.modal.fillLayers({ type: "fill-extrusion", layer: "example" });
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id, type: "fill-extrusion", source: "example" }],
});
});
});
describe("hillshade", () => {
- test("add", () => {
- const id = when.modal.fillLayers({
- type: "hillshade",
- layer: "example",
+ test("add", async () => {
+ const id = await when.modal.fillLayers({ type: "hillshade", layer: "example" });
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id, type: "hillshade", source: "example" }],
});
+ });
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ test("set hillshade illumination direction array", async () => {
+ const id = await when.modal.fillLayers({ type: "hillshade", layer: "example" });
+ await when.collapseGroupInLayerEditor();
+ await when.collapseGroupInLayerEditor(1);
+ await when.setValueToPropertyArray("spec-field:hillshade-illumination-direction", "1");
+ await when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "2");
+ await when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "3");
+ await when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "4");
+
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
- id: id,
+ id,
type: "hillshade",
source: "example",
+ paint: { "hillshade-illumination-direction": [1, 2, 3, 4] },
},
],
});
});
- test("set hillshade illumination direction array", () => {
- const id = when.modal.fillLayers({
- type: "hillshade",
- layer: "example",
- });
- when.collapseGroupInLayerEditor();
- when.collapseGroupInLayerEditor(1);
- when.setValueToPropertyArray("spec-field:hillshade-illumination-direction", "1");
- when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "2");
- when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "3");
- when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "4");
+ test("set hillshade highlight color array", async () => {
+ const id = await when.modal.fillLayers({ type: "hillshade", layer: "example" });
+ await when.collapseGroupInLayerEditor();
+ await when.setValueToPropertyArray("spec-field:hillshade-highlight-color", "blue");
+ await when.addValueToPropertyArray("spec-field:hillshade-highlight-color", "#00ff00");
+ await when.addValueToPropertyArray("spec-field:hillshade-highlight-color", "rgba(255, 255, 0, 1)");
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
- id: id,
+ id,
type: "hillshade",
source: "example",
paint: {
- "hillshade-illumination-direction": [ 1, 2, 3, 4 ]
- }
- },
- ],
- });
- });
-
- test("set hillshade highlight color array", () => {
- const id = when.modal.fillLayers({
- type: "hillshade",
- layer: "example",
- });
- when.collapseGroupInLayerEditor();
- when.setValueToPropertyArray("spec-field:hillshade-highlight-color", "blue");
- when.addValueToPropertyArray("spec-field:hillshade-highlight-color", "#00ff00");
- when.addValueToPropertyArray("spec-field:hillshade-highlight-color", "rgba(255, 255, 0, 1)");
-
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: id,
- type: "hillshade",
- source: "example",
- paint: {
- "hillshade-highlight-color": [ "blue", "#00ff00", "rgba(255, 255, 0, 1)" ]
- }
+ "hillshade-highlight-color": ["blue", "#00ff00", "rgba(255, 255, 0, 1)"],
+ },
},
],
});
@@ -414,129 +267,85 @@ describe("layers list", () => {
});
describe("color-relief", () => {
- test("add", () => {
- const id = when.modal.fillLayers({
- type: "color-relief",
- layer: "example",
- });
-
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
- layers: [
- {
- id: id,
- type: "color-relief",
- source: "example",
- },
- ],
+ test("add", async () => {
+ const id = await when.modal.fillLayers({ type: "color-relief", layer: "example" });
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ layers: [{ id, type: "color-relief", source: "example" }],
});
});
- test("adds elevation expression when clicking the elevation button", () => {
- when.modal.fillLayers({
- type: "color-relief",
- layer: "example",
- });
- when.collapseGroupInLayerEditor();
- when.click("make-elevation-function");
- then(get.element("[data-wd-key='spec-field-container:color-relief-color'] .cm-line")).shouldBeVisible();
- });
- });
-
- describe("groups", () => {
- test("simple", () => {
- when.setStyle("geojson");
-
- when.modal.open();
- when.modal.fillLayers({
- id: "foo",
- type: "background",
- });
-
- when.modal.open();
- when.modal.fillLayers({
- id: "foo_bar",
- type: "background",
- });
-
- when.modal.open();
- when.modal.fillLayers({
- id: "foo_bar_baz",
- type: "background",
- });
-
- then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
- then(get.elementByTestId("layer-list-item:foo_bar")).shouldNotBeVisible();
- then(
- get.elementByTestId("layer-list-item:foo_bar_baz")
- ).shouldNotBeVisible();
- when.click("layer-list-group:foo-0");
- then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
- then(get.elementByTestId("layer-list-item:foo_bar")).shouldBeVisible();
- then(
- get.elementByTestId("layer-list-item:foo_bar_baz")
+ test("adds elevation expression when clicking the elevation button", async () => {
+ await when.modal.fillLayers({ type: "color-relief", layer: "example" });
+ await when.collapseGroupInLayerEditor();
+ await when.click("make-elevation-function");
+ await then(
+ get.element("[data-wd-key='spec-field-container:color-relief-color'] .cm-line")
).shouldBeVisible();
});
});
+ describe("groups", () => {
+ test("simple", async () => {
+ await when.setStyle("geojson");
+
+ await when.modal.open();
+ await when.modal.fillLayers({ id: "foo", type: "background" });
+
+ await when.modal.open();
+ await when.modal.fillLayers({ id: "foo_bar", type: "background" });
+
+ await when.modal.open();
+ await when.modal.fillLayers({ id: "foo_bar_baz", type: "background" });
+
+ await then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
+ await then(get.elementByTestId("layer-list-item:foo_bar")).shouldNotBeVisible();
+ await then(get.elementByTestId("layer-list-item:foo_bar_baz")).shouldNotBeVisible();
+ await when.click("layer-list-group:foo-0");
+ await then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
+ await then(get.elementByTestId("layer-list-item:foo_bar")).shouldBeVisible();
+ await then(get.elementByTestId("layer-list-item:foo_bar_baz")).shouldBeVisible();
+ });
+ });
+
describe("drag and drop", () => {
- test("move layer should update local storage", () => {
- when.modal.open();
- const firstId = when.modal.fillLayers({
- id: "a",
- type: "background",
- });
- when.modal.open();
- const secondId = when.modal.fillLayers({
- id: "b",
- type: "background",
- });
- when.modal.open();
- const thirdId = when.modal.fillLayers({
- id: "c",
- type: "background",
- });
+ test("move layer should update local storage", async () => {
+ const firstId = await when.modal.fillLayers({ id: "a", type: "background" });
+ await when.modal.open();
+ const secondId = await when.modal.fillLayers({ id: "b", type: "background" });
+ await when.modal.open();
+ const thirdId = await when.modal.fillLayers({ id: "c", type: "background" });
- when.dragAndDropWithWait("layer-list-item:" + firstId, "layer-list-item:" + thirdId);
+ await when.dragAndDropWithWait("layer-list-item:" + firstId, "layer-list-item:" + thirdId);
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
- {
- id: secondId,
- type: "background",
- },
- {
- id: thirdId,
- type: "background",
- },
- {
- id: firstId,
- type: "background",
- },
+ { id: secondId, type: "background" },
+ { id: thirdId, type: "background" },
+ { id: firstId, type: "background" },
],
});
});
});
describe("sticky header", () => {
- test("should keep header visible when scrolling layer list", () => {
+ test("should keep header visible when scrolling layer list", async () => {
// Setup: Create multiple layers to enable scrolling
- for (let i = 0; i < 20; i++) {
- when.modal.open();
- when.modal.fillLayers({
- id: `layer-${i}`,
- type: "background",
- });
+ // The modal is already open (beforeEach) for the first layer.
+ await when.modal.fillLayers({ id: "layer-0", type: "background" });
+ for (let i = 1; i < 20; i++) {
+ await when.modal.open();
+ await when.modal.fillLayers({ id: `layer-${i}`, type: "background" });
}
- when.wait(500);
+ await when.wait(500);
const header = get.elementByTestId("layer-list.header");
- then(header).shouldBeVisible();
+ await then(header).shouldBeVisible();
- // Scroll the layer list container (use ensureScrollable: false to avoid flakiness)
- get.elementByTestId("layer-list").scrollTo("bottom", { ensureScrollable: false });
- when.wait(200);
- then(header).shouldBeVisible();
- then(get.elementByTestId("layer-list:add-layer")).shouldBeVisible();
+ // Scroll the layer list container
+ await when.scrollToBottom(get.elementByTestId("layer-list"));
+ await when.wait(200);
+ await then(header).shouldBeVisible();
+ await then(get.elementByTestId("layer-list:add-layer")).shouldBeVisible();
});
});
});
diff --git a/e2e/map.spec.ts b/e2e/map.spec.ts
index 046620f7..b33fdc66 100644
--- a/e2e/map.spec.ts
+++ b/e2e/map.spec.ts
@@ -1,69 +1,67 @@
+import { beforeEach, describe, test } from "./utils/fixtures";
import { MaputnikDriver } from "./maputnik-driver";
-const test = it;
-
describe("map", () => {
- const { beforeAndAfter, get, when, then } = new MaputnikDriver();
- beforeAndAfter();
+ const { given, get, when, then } = new MaputnikDriver();
+
+ beforeEach(async () => {
+ await given.setupMockBackedResponses();
+ await when.setStyle("both");
+ });
+
describe("zoom level", () => {
- test("via url", () => {
+ test("via url", async () => {
const zoomLevel = 12.37;
- when.setStyle("geojson", zoomLevel);
- then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
- then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
- "Zoom: " + zoomLevel
- );
+ await when.setStyle("geojson", zoomLevel);
+ await then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
+ await then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText("Zoom: " + zoomLevel);
});
- test("via map controls", () => {
+ test("via map controls", async () => {
const zoomLevel = 12.37;
- when.setStyle("geojson", zoomLevel);
- then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
- when.clickZoomIn();
- then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
- "Zoom: " + (zoomLevel + 1)
- );
+ await when.setStyle("geojson", zoomLevel);
+ await then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
+ await when.clickZoomIn();
+ await then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText("Zoom: " + (zoomLevel + 1));
});
- test("via style file definition", () => {
- when.setStyle("zoom_7_center_0_51");
- then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
- then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
- "Zoom: " + (7)
- );
- then(get.locationHash().should("contain", "#7/51/0"));
+ test("via style file definition", async () => {
+ await when.setStyle("zoom_7_center_0_51");
+ await then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
+ await then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText("Zoom: " + 7);
+ await then(get.locationHash()).shouldInclude("#7/51/0");
// opening another stylefile does not update the map view again
// as discussed in https://github.com/maplibre/maputnik/issues/1546
- when.openASecondStyleWithDifferentZoomAndCenter();
- then(get.locationHash().should("contain", "#7/51/0"));
-
+ await when.openASecondStyleWithDifferentZoomAndCenter();
+ await then(get.locationHash()).shouldInclude("#7/51/0");
});
});
describe("search", () => {
- test("should exist", () => {
- then(get.searchControl()).shouldBeVisible();
+ test("should exist", async () => {
+ await then(get.searchControl()).shouldBeVisible();
});
});
describe("popup", () => {
- beforeEach(() => {
- when.setStyle("rectangles");
- then(get.locationHash().should("exist"));
- });
- test("should open on feature click", () => {
- when.clickCenter("maplibre:map");
- then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
+ beforeEach(async () => {
+ await when.setStyle("rectangles");
+ await then(get.locationHash()).shouldExist();
});
- test("should open a second feature after closing popup", () => {
- when.clickCenter("maplibre:map");
- then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
- when.closePopup();
- then(get.elementByTestId("feature-layer-popup")).shouldNotExist();
- when.clickCenter("maplibre:map");
- then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
+ test("should open on feature click", async () => {
+ await when.clickCenter("maplibre:map");
+ await then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
+ });
+
+ test("should open a second feature after closing popup", async () => {
+ await when.clickCenter("maplibre:map");
+ await then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
+ await when.closePopup();
+ await then(get.elementByTestId("feature-layer-popup")).shouldNotExist();
+ await when.clickCenter("maplibre:map");
+ await then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
});
});
});
diff --git a/e2e/maputnik-cypress-helper.ts b/e2e/maputnik-cypress-helper.ts
deleted file mode 100644
index 90467828..00000000
--- a/e2e/maputnik-cypress-helper.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-///
-import { CypressHelper } from "@shellygo/cypress-test-utils";
-import "cypress-real-events/support";
-
-export default class MaputnikCypressHelper {
- private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" });
-
- public given = {
- ...this.helper.given,
- };
-
- public get = {
- locationHash: (): Cypress.Chainable => cy.location("hash"),
- ...this.helper.get,
- };
-
- public when = {
- dragAndDropWithWait: (element: string, targetElement: string) => {
- this.helper.get.elementByTestId(element).realMouseDown({ button: "left", position: "center" });
- this.helper.get.elementByTestId(element).realMouseMove(0, 10, { position: "center" });
- this.helper.get.elementByTestId(targetElement).realMouseMove(0, 0, { position: "center" });
- this.helper.when.wait(1);
- this.helper.get.elementByTestId(targetElement).realMouseUp();
- },
- clickCenter: (element: string) => {
- this.helper.get.elementByTestId(element).realMouseDown({ button: "left", position: "center" });
- this.helper.when.wait(200);
- this.helper.get.elementByTestId(element).realMouseUp();
- },
- openFileByFixture: (fixture: string, buttonTestId: string, inputTestId: string) => {
- cy.window().then((win) => {
- const file = {
- text: cy.stub().resolves(cy.fixture(fixture).then(JSON.stringify)),
- };
- const fileHandle = {
- getFile: cy.stub().resolves(file),
- };
- if (!win.showOpenFilePicker) {
- this.helper.get.elementByTestId(inputTestId).selectFile("cypress/fixtures/" + fixture, { force: true });
- } else {
- cy.stub(win, "showOpenFilePicker").resolves([fileHandle]);
- this.helper.get.elementByTestId(buttonTestId).click();
- }
- });
- },
- dropFileByFixture: (fixture: string, dropzoneTestId: string) => {
- this.helper.get.elementByTestId(dropzoneTestId).selectFile("cypress/fixtures/" + fixture, {
- action: "drag-drop",
- force: true,
- });
- },
- ...this.helper.when,
- };
-
- public beforeAndAfter = this.helper.beforeAndAfter;
-}
diff --git a/e2e/maputnik-driver.ts b/e2e/maputnik-driver.ts
index 692b038d..ad594a9e 100644
--- a/e2e/maputnik-driver.ts
+++ b/e2e/maputnik-driver.ts
@@ -1,122 +1,55 @@
-///
+import { PlaywrightHelper } from "./playwright-helper";
+import { ModalDriver } from "./modal-driver";
-import { CypressHelper } from "@shellygo/cypress-test-utils";
-import { Assertable, then } from "@shellygo/cypress-test-utils/assertable";
-import MaputnikCypressHelper from "./maputnik-cypress-helper";
-import ModalDriver from "./modal-driver";
const baseUrl = "http://localhost:8888/";
+const isMac = process.platform === "darwin";
-const styleFromWindow = (win: Window) => {
- const styleId = win.localStorage.getItem("maputnik:latest_style");
- const styleItemKey = `maputnik:style:${styleId}`;
- const styleItem = win.localStorage.getItem(styleItemKey);
- if (!styleItem) throw new Error("Could not get styleItem from localStorage");
- const obj = JSON.parse(styleItem);
- return obj;
-};
-
-export class MaputnikAssertable extends Assertable {
- shouldEqualToStoredStyle = () =>
- then(
- new CypressHelper().get.window().then((win: Window) => {
- const style = styleFromWindow(win);
- then(this.chainable).shouldDeepNestedInclude(style);
- })
- );
-}
-
+/**
+ * The maputnik-specific driver. It builds on the generic {@link PlaywrightHelper}
+ * — spreading its `given`/`when`/`get` primitives and adding domain concepts
+ * (loading a style, the add-layer modal, the JSON editor, …). All Playwright
+ * access goes through the helper; the driver never touches `page` directly.
+ */
export class MaputnikDriver {
- private helper = new MaputnikCypressHelper();
- private modalDriver = new ModalDriver();
+ private readonly helper = new PlaywrightHelper();
+ private readonly modalDriver = new ModalDriver();
- public beforeAndAfter = () => {
- beforeEach(() => {
- this.given.setupMockBackedResponses();
- this.when.setStyle("both");
- });
- };
+ then = this.helper.then;
- public then = (chainable: Cypress.Chainable) =>
- new MaputnikAssertable(chainable);
+ /** Reads the maputnik style currently persisted in localStorage. */
+ private async readStoredStyle(): Promise {
+ const styleId = await this.helper.get.localStorageItem("maputnik:latest_style");
+ const styleItem = await this.helper.get.localStorageItem(`maputnik:style:${styleId}`);
+ if (!styleItem) throw new Error("Could not get styleItem from localStorage");
+ return JSON.parse(styleItem);
+ }
public given = {
...this.helper.given,
- setupMockBackedResponses: () => {
- this.helper.given.interceptAndMockResponse({
- method: "GET",
- url: baseUrl + "example-style.json",
- response: {
- fixture: "example-style.json",
- },
- alias: "example-style.json",
- });
- this.helper.given.interceptAndMockResponse({
- method: "GET",
- url: baseUrl + "example-layer-style.json",
- response: {
- fixture: "example-layer-style.json",
- },
- });
- this.helper.given.interceptAndMockResponse({
- method: "GET",
- url: baseUrl + "geojson-style.json",
- response: {
- fixture: "geojson-style.json",
- },
- });
- this.helper.given.interceptAndMockResponse({
- method: "GET",
- url: baseUrl + "raster-style.json",
- response: {
- fixture: "raster-style.json",
- },
- });
- this.helper.given.interceptAndMockResponse({
- method: "GET",
- url: baseUrl + "geojson-raster-style.json",
- response: {
- fixture: "geojson-raster-style.json",
- },
- });
- this.helper.given.interceptAndMockResponse({
- method: "GET",
- url: baseUrl + "rectangles-style.json",
- response: {
- fixture: "rectangles-style.json",
- },
- });
- this.helper.given.interceptAndMockResponse({
- method: "GET",
- url: baseUrl + "example-style-with-fonts.json",
- response: {
- fixture: "example-style-with-fonts.json",
- },
- });
- this.helper.given.interceptAndMockResponse({
- method: "GET",
- url: baseUrl + "example-style-with-zoom-7-and-center-0-51.json",
- response: {
- fixture: "example-style-with-zoom-7-and-center-0-51.json",
- },
- });
- this.helper.given.interceptAndMockResponse({
- method: "GET",
- url: baseUrl + "example-style-with-zoom-5-and-center-50-50.json",
- response: {
- fixture: "example-style-with-zoom-5-and-center-50-50.json",
- },
- });
- this.helper.given.interceptAndMockResponse({
- method: "GET",
- url: "*example.local/*",
- response: [],
- });
- this.helper.given.interceptAndMockResponse({
- method: "GET",
- url: "*example.com/*",
- response: [],
- });
- this.helper.given.interceptAndMockResponse({
+
+ setupMockBackedResponses: async () => {
+ const styleFixtures = [
+ "example-style.json",
+ "example-layer-style.json",
+ "geojson-style.json",
+ "raster-style.json",
+ "geojson-raster-style.json",
+ "rectangles-style.json",
+ "example-style-with-fonts.json",
+ "example-style-with-zoom-7-and-center-0-51.json",
+ "example-style-with-zoom-5-and-center-50-50.json",
+ ];
+ for (const fixture of styleFixtures) {
+ await this.helper.given.interceptAndMockResponse({
+ method: "GET",
+ url: baseUrl + fixture,
+ response: { fixture },
+ alias: fixture === "example-style.json" ? "example-style.json" : undefined,
+ });
+ }
+ await this.helper.given.interceptAndMockResponse({ method: "GET", url: /example\.local\//, response: [] });
+ await this.helper.given.interceptAndMockResponse({ method: "GET", url: /example\.com\//, response: [] });
+ await this.helper.given.interceptAndMockResponse({
method: "GET",
url: "https://www.glyph-server.com/*",
response: ["Font 1", "Font 2", "Font 3"],
@@ -126,147 +59,134 @@ export class MaputnikDriver {
public when = {
...this.helper.when,
+
modal: this.modalDriver.when,
- doWithin: (selector: string, fn: () => void) => {
- this.helper.when.doWithin(fn, selector);
- },
- tab: () => this.helper.get.element("body").tab(),
- waitForExampleFileResponse: () => {
- this.helper.when.waitForResponse("example-style.json");
- },
- openASecondStyleWithDifferentZoomAndCenter: () => {
- cy.contains("button", "Open").click();
- cy.get('[data-wd-key="modal:open.url.input"]')
- .should("be.enabled")
- .clear()
- .type("http://localhost:8888/example-style-with-zoom-5-and-center-50-50.json{enter}");
- },
- chooseExampleFile: () => {
- this.helper.given.fixture("example-style.json", "example-style.json");
- this.helper.when.openFileByFixture("example-style.json", "modal:open.dropzone", "modal:open.file.input");
- this.helper.when.wait(200);
- },
- dropExampleFile: () => {
- this.helper.given.fixture("example-style.json", "example-style.json");
- this.helper.when.dropFileByFixture("example-style.json", "modal:open.dropzone");
- this.helper.when.wait(200);
- },
- setStyle: (
- styleProperties: "geojson" | "raster" | "both" | "layer" | "rectangles" | "font" | "zoom_7_center_0_51" | "",
+
+ setStyle: async (
+ styleProperties:
+ | "geojson"
+ | "raster"
+ | "both"
+ | "layer"
+ | "rectangles"
+ | "font"
+ | "zoom_7_center_0_51"
+ | "",
zoom?: number
) => {
- const url = new URL(baseUrl);
- switch (styleProperties) {
- case "geojson":
- url.searchParams.set("style", baseUrl + "geojson-style.json");
- break;
- case "raster":
- url.searchParams.set("style", baseUrl + "raster-style.json");
- break;
- case "both":
- url.searchParams.set("style", baseUrl + "geojson-raster-style.json");
- break;
- case "layer":
- url.searchParams.set("style", baseUrl + "example-layer-style.json");
- break;
- case "rectangles":
- url.searchParams.set("style", baseUrl + "rectangles-style.json");
- break;
- case "font":
- url.searchParams.set("style", baseUrl + "example-style-with-fonts.json");
- break;
- case "zoom_7_center_0_51":
- url.searchParams.set("style", baseUrl + "example-style-with-zoom-7-and-center-0-51.json");
- break;
- }
+ const styleFileByKey: Record = {
+ geojson: "geojson-style.json",
+ raster: "raster-style.json",
+ both: "geojson-raster-style.json",
+ layer: "example-layer-style.json",
+ rectangles: "rectangles-style.json",
+ font: "example-style-with-fonts.json",
+ zoom_7_center_0_51: "example-style-with-zoom-7-and-center-0-51.json",
+ };
+ const url = new URL(baseUrl);
+ if (styleProperties && styleFileByKey[styleProperties]) {
+ url.searchParams.set("style", baseUrl + styleFileByKey[styleProperties]);
+ }
if (zoom) {
url.hash = `${zoom}/41.3805/2.1635`;
}
- this.helper.when.visit(url.toString());
- if (styleProperties) {
- this.helper.when.acceptConfirm();
- }
- // when methods should not include assertions
+
+ await this.helper.when.visit(url.toString());
+
const toolbarLink = this.helper.get.elementByTestId("toolbar:link");
- toolbarLink.scrollIntoView();
- toolbarLink.should("be.visible");
+ await toolbarLink.scrollIntoViewIfNeeded();
+ await this.then(toolbarLink).shouldBeVisible();
},
- typeKeys: (keys: string) => this.helper.get.element("body").type(keys),
-
- clickZoomIn: () => {
- this.helper.get.element(".maplibregl-ctrl-zoom-in").click();
+ openASecondStyleWithDifferentZoomAndCenter: async () => {
+ await this.helper.when.clickButtonByName("Open");
+ const input = this.helper.get.elementByTestId("modal:open.url.input");
+ await input.fill("http://localhost:8888/example-style-with-zoom-5-and-center-50-50.json");
+ await input.press("Enter");
},
- selectWithin: (selector: string, value: string) => {
- this.when.doWithin(selector, () => {
- this.helper.get.element("select").select(value);
- });
+ chooseExampleFile: async () => {
+ await this.helper.when.openFileByFixture("example-style.json", "modal:open.dropzone", "modal:open.file.input");
+ await this.helper.when.wait(200);
},
- select: (selector: string, value: string) => {
- this.helper.get.elementByTestId(selector).select(value);
+ dropExampleFile: async () => {
+ await this.helper.when.dropFileByFixture("example-style.json", "modal:open.dropzone");
+ await this.helper.when.wait(200);
},
- focus: (selector: string) => {
- this.helper.when.focus(selector);
+ clickZoomIn: async () => {
+ await this.helper.get.element(".maplibregl-ctrl-zoom-in").click();
},
- setValue: (selector: string, text: string) => {
- this.helper.get
- .elementByTestId(selector)
- .clear()
- .type(text, { parseSpecialCharSequences: false });
+ closePopup: async () => {
+ await this.helper.get.element(".maplibregl-popup-close-button").click();
},
- setValueToPropertyArray: (selector: string, value: string) => {
- this.when.doWithin(selector, () => {
- this.helper.get.element(".maputnik-array-block-content input").last().type("{selectall}"+value, {force: true });
- });
+ collapseGroupInLayerEditor: async (index = 0) => {
+ await this.helper.get.element(".maputnik-layer-editor-group__button").nth(index).click();
},
- addValueToPropertyArray: (selector: string, value: string) => {
- this.when.doWithin(selector, () => {
- this.helper.get.element(".maputnik-array-add-value").click({ force: true });
- this.helper.get.element(".maputnik-array-block-content input").last().type("{selectall}"+value, {force: true });
- });
+ appendTextInJsonEditor: async (text: string) => {
+ await this.helper.get.element(".cm-line").first().click();
+ // Move to the very start of the document so the inserted text breaks the
+ // root JSON structure (CodeMirror auto-closes brackets otherwise).
+ await this.helper.when.typeKeys("{home}");
+ await this.helper.when.typeText(text);
},
- closePopup: () => {
- this.helper.get.element(".maplibregl-popup-close-button").click();
+ setTextInJsonEditor: async (text: string) => {
+ await this.helper.get.element(".cm-line").first().click();
+ await this.helper.when.typeKeys("{selectall}");
+ await this.helper.when.typeText(text);
},
- collapseGroupInLayerEditor: (index = 0) => {
- this.helper.get.element(".maputnik-layer-editor-group__button").eq(index).realClick();
+ setValueToPropertyArray: async (selector: string, value: string) => {
+ const input = this.helper.get.elementByTestId(selector).locator(".maputnik-array-block-content input").last();
+ await input.focus();
+ await this.helper.when.typeKeys("{selectall}");
+ await this.helper.when.typeText(value);
},
- appendTextInJsonEditor: (text: string) => {
- this.helper.get.element(".cm-line").first().click().type(text, { parseSpecialCharSequences: false });
+ addValueToPropertyArray: async (selector: string, value: string) => {
+ const block = this.helper.get.elementByTestId(selector);
+ await block.locator(".maputnik-array-add-value").click();
+ const input = block.locator(".maputnik-array-block-content input").last();
+ await input.focus();
+ await this.helper.when.typeKeys("{selectall}");
+ await this.helper.when.typeText(value);
},
- setTextInJsonEditor: (text: string) => {
- this.helper.get.element(".cm-line").first().click().clear().type(text, { parseSpecialCharSequences: false });
- }
+ waitForExampleFileResponse: () => this.helper.when.waitForResponse("example-style.json"),
+
+ /** Fill localStorage until we get a QuotaExceededError. */
+ fillLocalStorage: () => this.helper.when.fillLocalStorageUntilQuota("maputnik:fill-"),
};
public get = {
...this.helper.get,
- isMac: () => {
- return Cypress.platform === "darwin";
- },
- styleFromLocalStorage: () =>
- this.helper.get.window().then((win) => styleFromWindow(win)),
+ isMac: () => isMac,
- exampleFileUrl: () => {
- return baseUrl + "example-style.json";
- },
- skipTargetLayerList: () =>
- this.helper.get.elementByTestId("skip-target-layer-list"),
- skipTargetLayerEditor: () =>
- this.helper.get.elementByTestId("skip-target-layer-editor"),
canvas: () => this.helper.get.element("canvas"),
- searchControl: () => this.helper.get.element(".maplibregl-ctrl-geocoder")
+
+ searchControl: () => this.helper.get.element(".maplibregl-ctrl-geocoder"),
+
+ skipTargetLayerList: () => this.helper.get.elementByTestId("skip-target-layer-list"),
+
+ skipTargetLayerEditor: () => this.helper.get.elementByTestId("skip-target-layer-editor"),
+
+ styleFromLocalStorage: () => this.helper.query(() => this.readStoredStyle()),
+
+ fixture: (name: string) => this.helper.readFixture(name),
+
+ responseBody: (alias: string) => {
+ // Our mocked style responses always return the matching fixture.
+ const name = alias.endsWith(".json") ? alias : `${alias}.json`;
+ return this.helper.readFixture(name);
+ },
+
+ exampleFileUrl: () => baseUrl + "example-style.json",
};
}
diff --git a/e2e/modal-driver.ts b/e2e/modal-driver.ts
index 76878b83..3f7e4915 100644
--- a/e2e/modal-driver.ts
+++ b/e2e/modal-driver.ts
@@ -1,40 +1,38 @@
import { v1 as uuid } from "uuid";
-import MaputnikCypressHelper from "./maputnik-cypress-helper";
+import { PlaywrightHelper } from "./playwright-helper";
-export default class ModalDriver {
- private helper = new MaputnikCypressHelper();
+export class ModalDriver {
+ private readonly helper = new PlaywrightHelper();
public when = {
- fillLayers: (opts: { type: string; layer?: string; id?: string }) => {
- // Having logic in test code is an anti pattern.
- // This should be split to multiple single responsibility functions
- const type = opts.type;
- const layer = opts.layer;
- let id;
- if (opts.id) {
- id = opts.id;
- } else {
- id = `${type}:${uuid()}`;
- }
- this.helper.when.selectOption("add-layer.layer-type.select", type);
- this.helper.when.type("add-layer.layer-id.input", id);
+ fillLayers: async (opts: { type: string; layer?: string; id?: string }) => {
+ const { when, get, then } = this.helper;
+ const id = opts.id ?? `${opts.type}:${uuid()}`;
- if (layer) {
- this.helper.when.doWithin(() => {
- this.helper.get.element("input").clear().type(layer!);
- }, "add-layer.layer-source-block");
+ await when.select("add-layer.layer-type.select", opts.type);
+ await when.type("add-layer.layer-id.input", id);
+
+ if (opts.layer) {
+ const input = get.elementByTestId("add-layer.layer-source-block").locator("input");
+ await input.click();
+ await input.fill(opts.layer);
+ // The source input is a controlled downshift combobox; wait for React to
+ // settle on the typed value before submitting.
+ await then(input).shouldHaveValue(opts.layer);
+ // Close the autocomplete menu so it does not intercept the add button.
+ await get.elementByTestId("add-layer.layer-id.input").click();
}
- this.helper.when.click("add-layer");
+ await when.click("add-layer");
return id;
},
- open: () => {
- this.helper.when.click("layer-list:add-layer");
+ open: async () => {
+ await this.helper.when.click("layer-list:add-layer");
},
- close: (key: string) => {
- this.helper.when.click(key + ".close-modal");
+ close: async (key: string) => {
+ await this.helper.when.click(key + ".close-modal");
},
};
}
diff --git a/e2e/modals.spec.ts b/e2e/modals.spec.ts
index f2c13fe5..370fb556 100644
--- a/e2e/modals.spec.ts
+++ b/e2e/modals.spec.ts
@@ -1,103 +1,106 @@
+import { test, expect, describe, beforeEach } from "./utils/fixtures";
import { MaputnikDriver } from "./maputnik-driver";
-import tokens from "../src/config/tokens.json" with {type: "json"};
-const test = it;
+import tokens from "../src/config/tokens.json" with { type: "json" };
describe("modals", () => {
- const { beforeAndAfter, when, get, given, then } = new MaputnikDriver();
- beforeAndAfter();
+ const { given, get, when, then } = new MaputnikDriver();
- beforeEach(() => {
- when.setStyle("");
+ beforeEach(async () => {
+ await given.setupMockBackedResponses();
+ await when.setStyle("both");
+ await when.setStyle("");
});
+
describe("open", () => {
- beforeEach(() => {
- when.click("nav:open");
+ beforeEach(async () => {
+ await when.click("nav:open");
});
- test("close", () => {
- when.modal.close("modal:open");
- then(get.elementByTestId("modal:open")).shouldNotExist();
+ test("close", async () => {
+ await when.modal.close("modal:open");
+ await then(get.elementByTestId("modal:open")).shouldNotExist();
});
- test("upload", () => {
- when.chooseExampleFile();
- then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
+ test("upload", async () => {
+ await when.chooseExampleFile();
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude(get.fixture("example-style.json"));
});
- test("upload via drag and drop", () => {
- when.dropExampleFile();
- then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
+ test("upload via drag and drop", async () => {
+ await when.dropExampleFile();
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude(get.fixture("example-style.json"));
});
describe("when click open url", () => {
- beforeEach(() => {
+ beforeEach(async () => {
const styleFileUrl = get.exampleFileUrl();
- when.setValue("modal:open.url.input", styleFileUrl);
- when.click("modal:open.url.button");
- when.wait(200);
+ await when.setValue("modal:open.url.input", styleFileUrl);
+ await when.click("modal:open.url.button");
+ await when.wait(200);
});
- test("load from url", () => {
- then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
+ test("load from url", async () => {
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude(get.responseBody("example-style.json"));
});
});
});
describe("shortcuts", () => {
- test("open/close", () => {
- when.setStyle("");
- when.typeKeys("?");
- when.modal.close("modal:shortcuts");
- then(get.elementByTestId("modal:shortcuts")).shouldNotExist();
+ test("open/close", async () => {
+ await when.setStyle("");
+ await when.typeKeys("?");
+ await when.modal.close("modal:shortcuts");
+ await then(get.elementByTestId("modal:shortcuts")).shouldNotExist();
});
});
describe("export", () => {
- beforeEach(() => {
- when.click("nav:export");
+ beforeEach(async () => {
+ await when.click("nav:export");
});
- test("close", () => {
- when.modal.close("modal:export");
- then(get.elementByTestId("modal:export")).shouldNotExist();
+ test("close", async () => {
+ await when.modal.close("modal:export");
+ await then(get.elementByTestId("modal:export")).shouldNotExist();
});
// TODO: Work out how to download a file and check the contents
- test("download");
+ test.skip("download", () => {});
});
describe("sources", () => {
- beforeEach(() => {
- when.setStyle("layer");
- when.click("nav:sources");
+ beforeEach(async () => {
+ await when.setStyle("layer");
+ await when.click("nav:sources");
});
- test("active sources");
- test("public source");
+ test.skip("active sources", () => {});
+ test.skip("public source", () => {});
- test("add new source", () => {
+ test("add new source", async () => {
const sourceId = "n1z2v3r";
- when.setValue("modal:sources.add.source_id", sourceId);
- when.select("modal:sources.add.source_type", "tile_vector");
- when.select("modal:sources.add.scheme_type", "tms");
- when.click("modal:sources.add.add_source");
- when.wait(200);
- then(
- get.styleFromLocalStorage().then((style) => style.sources[sourceId])
- ).shouldInclude({
+ await when.setValue("modal:sources.add.source_id", sourceId);
+ await when.select("modal:sources.add.source_type", "tile_vector");
+ await when.select("modal:sources.add.scheme_type", "tms");
+ await when.click("modal:sources.add.add_source");
+ await when.wait(200);
+ await then(get.styleFromLocalStorage().then((style) => style.sources[sourceId])).shouldInclude({
scheme: "tms",
});
});
- test("add new pmtiles source", () => {
+ test("add new pmtiles source", async () => {
const sourceId = "pmtilestest";
- when.setValue("modal:sources.add.source_id", sourceId);
- when.select("modal:sources.add.source_type", "pmtiles_vector");
- when.setValue("modal:sources.add.source_url", "https://data.source.coop/protomaps/openstreetmap/v4.pmtiles");
- when.click("modal:sources.add.add_source");
- when.click("modal:sources.add.add_source");
- when.wait(200);
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ await when.setValue("modal:sources.add.source_id", sourceId);
+ await when.select("modal:sources.add.source_type", "pmtiles_vector");
+ await when.setValue(
+ "modal:sources.add.source_url",
+ "https://data.source.coop/protomaps/openstreetmap/v4.pmtiles"
+ );
+ await when.click("modal:sources.add.add_source");
+ await when.click("modal:sources.add.add_source");
+ await when.wait(200);
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
sources: {
pmtilestest: {
type: "vector",
@@ -107,351 +110,296 @@ describe("modals", () => {
});
});
- test("add new raster source", () => {
+ test("add new raster source", async () => {
const sourceId = "rastertest";
- when.setValue("modal:sources.add.source_id", sourceId);
- when.select("modal:sources.add.source_type", "tile_raster");
- when.select("modal:sources.add.scheme_type", "xyz");
- when.setValue("modal:sources.add.tile_size", "128");
- when.click("modal:sources.add.add_source");
- when.wait(200);
- then(
- get.styleFromLocalStorage().then((style) => style.sources[sourceId])
- ).shouldInclude({
+ await when.setValue("modal:sources.add.source_id", sourceId);
+ await when.select("modal:sources.add.source_type", "tile_raster");
+ await when.select("modal:sources.add.scheme_type", "xyz");
+ await when.setValue("modal:sources.add.tile_size", "128");
+ await when.click("modal:sources.add.add_source");
+ await when.wait(200);
+ await then(get.styleFromLocalStorage().then((style) => style.sources[sourceId])).shouldInclude({
tileSize: 128,
});
});
});
describe("inspect", () => {
- test("toggle", () => {
+ test("toggle", async () => {
// There is no assertion in this test
- when.setStyle("geojson");
- when.select("maputnik-select", "inspect");
+ await when.setStyle("geojson");
+ await when.select("maputnik-select", "inspect");
});
});
describe("style settings", () => {
- beforeEach(() => {
- when.click("nav:settings");
+ beforeEach(async () => {
+ await when.click("nav:settings");
});
describe("when click name filed spec information", () => {
- beforeEach(() => {
- when.click("field-doc-button-Name");
+ beforeEach(async () => {
+ await when.click("field-doc-button-Name");
});
- test("should show the spec information", () => {
- then(get.elementsText("spec-field-doc")).shouldInclude(
- "name for the style"
- );
+ test("should show the spec information", async () => {
+ await then(get.elementsText("spec-field-doc")).shouldInclude("name for the style");
});
});
describe("when set name and click owner", () => {
- beforeEach(() => {
- when.setValue("modal:settings.name", "foobar");
- when.click("modal:settings.owner");
- when.wait(200);
+ beforeEach(async () => {
+ await when.setValue("modal:settings.name", "foobar");
+ await when.click("modal:settings.owner");
+ await when.wait(200);
});
- test("show name specifications", () => {
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ test("show name specifications", async () => {
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
name: "foobar",
});
});
});
describe("when set owner and click name", () => {
- beforeEach(() => {
- when.setValue("modal:settings.owner", "foobar");
- when.click("modal:settings.name");
- when.wait(200);
+ beforeEach(async () => {
+ await when.setValue("modal:settings.owner", "foobar");
+ await when.click("modal:settings.name");
+ await when.wait(200);
});
- test("should update owner in local storage", () => {
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ test("should update owner in local storage", async () => {
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
owner: "foobar",
});
});
});
- test("sprite url", () => {
- when.setTextInJsonEditor("\"http://example.com\"");
- when.click("modal:settings.name");
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ test("sprite url", async () => {
+ await when.setTextInJsonEditor('"http://example.com"');
+ await when.click("modal:settings.name");
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
sprite: "http://example.com",
});
});
- test("sprite object", () => {
- when.setTextInJsonEditor(JSON.stringify([{ id: "1", url: "2" }]));
+ test("sprite object", async () => {
+ await when.setTextInJsonEditor(JSON.stringify([{ id: "1", url: "2" }]));
- when.click("modal:settings.name");
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ await when.click("modal:settings.name");
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
sprite: [{ id: "1", url: "2" }],
});
});
- test("glyphs url", () => {
+ test("glyphs url", async () => {
const glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
- when.setValue("modal:settings.glyphs", glyphsUrl);
- when.click("modal:settings.name");
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ await when.setValue("modal:settings.glyphs", glyphsUrl);
+ await when.click("modal:settings.name");
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
glyphs: glyphsUrl,
});
});
- test("maptiler access token", () => {
+ test("maptiler access token", async () => {
const apiKey = "testing123";
- when.setValue(
- "modal:settings.maputnik:openmaptiles_access_token",
- apiKey
- );
- when.click("modal:settings.name");
- then(
- get.styleFromLocalStorage().then((style) => style.metadata)
- ).shouldInclude({
+ await when.setValue("modal:settings.maputnik:openmaptiles_access_token", apiKey);
+ await when.click("modal:settings.name");
+ await then(get.styleFromLocalStorage().then((style) => style.metadata)).shouldInclude({
"maputnik:openmaptiles_access_token": apiKey,
});
});
- test("thunderforest access token", () => {
+ test("thunderforest access token", async () => {
const apiKey = "testing123";
- when.setValue(
- "modal:settings.maputnik:thunderforest_access_token",
- apiKey
- );
- when.click("modal:settings.name");
- then(
- get.styleFromLocalStorage().then((style) => style.metadata)
- ).shouldInclude({ "maputnik:thunderforest_access_token": apiKey });
+ await when.setValue("modal:settings.maputnik:thunderforest_access_token", apiKey);
+ await when.click("modal:settings.name");
+ await then(get.styleFromLocalStorage().then((style) => style.metadata)).shouldInclude({
+ "maputnik:thunderforest_access_token": apiKey,
+ });
});
- test("stadia access token", () => {
+ test("stadia access token", async () => {
const apiKey = "testing123";
- when.setValue(
- "modal:settings.maputnik:stadia_access_token",
- apiKey
- );
- when.click("modal:settings.name");
- then(
- get.styleFromLocalStorage().then((style) => style.metadata)
- ).shouldInclude({ "maputnik:stadia_access_token": apiKey });
+ await when.setValue("modal:settings.maputnik:stadia_access_token", apiKey);
+ await when.click("modal:settings.name");
+ await then(get.styleFromLocalStorage().then((style) => style.metadata)).shouldInclude({
+ "maputnik:stadia_access_token": apiKey,
+ });
});
- test("locationiq access token", () => {
+ test("locationiq access token", async () => {
const apiKey = "testing123";
- when.setValue(
- "modal:settings.maputnik:locationiq_access_token",
- apiKey
- );
- when.click("modal:settings.name");
- then(
- get.styleFromLocalStorage().then((style) => style.metadata)
- ).shouldInclude({ "maputnik:locationiq_access_token": apiKey });
+ await when.setValue("modal:settings.maputnik:locationiq_access_token", apiKey);
+ await when.click("modal:settings.name");
+ await then(get.styleFromLocalStorage().then((style) => style.metadata)).shouldInclude({
+ "maputnik:locationiq_access_token": apiKey,
+ });
});
- test("style projection mercator", () => {
- when.select("modal:settings.projection", "mercator");
- then(
- get.styleFromLocalStorage().then((style) => style.projection)
- ).shouldInclude({ type: "mercator" });
+ test("style projection mercator", async () => {
+ await when.select("modal:settings.projection", "mercator");
+ await then(get.styleFromLocalStorage().then((style) => style.projection)).shouldInclude({
+ type: "mercator",
+ });
});
- test("style projection globe", () => {
- when.select("modal:settings.projection", "globe");
- then(
- get.styleFromLocalStorage().then((style) => style.projection)
- ).shouldInclude({ type: "globe" });
+ test("style projection globe", async () => {
+ await when.select("modal:settings.projection", "globe");
+ await then(get.styleFromLocalStorage().then((style) => style.projection)).shouldInclude({
+ type: "globe",
+ });
});
-
- test("style projection vertical-perspective", () => {
- when.select("modal:settings.projection", "vertical-perspective");
- then(
- get.styleFromLocalStorage().then((style) => style.projection)
- ).shouldInclude({ type: "vertical-perspective" });
-
+ test("style projection vertical-perspective", async () => {
+ await when.select("modal:settings.projection", "vertical-perspective");
+ await then(get.styleFromLocalStorage().then((style) => style.projection)).shouldInclude({
+ type: "vertical-perspective",
+ });
});
- test("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");
- then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
- "ol"
- );
+ test("style renderer", async () => {
+ await when.select("modal:settings.maputnik:renderer", "ol");
+ await then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual("ol");
- when.click("modal:settings.name");
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ await when.click("modal:settings.name");
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
metadata: { "maputnik:renderer": "ol" },
});
});
+ test("include API key when change renderer", async () => {
+ await when.click("modal:settings.close-modal");
+ await when.click("nav:open");
+ await when.clickByAttribute("aria-label", "MapTiler Basic");
+ await when.wait(1000);
+ await when.click("nav:settings");
- test("include API key when change renderer", () => {
+ await when.select("modal:settings.maputnik:renderer", "mlgljs");
+ await then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual("mlgljs");
- when.click("modal:settings.close-modal");
- when.click("nav:open");
+ await when.select("modal:settings.maputnik:renderer", "ol");
+ await then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual("ol");
- get.elementByAttribute("aria-label", "MapTiler Basic").should("exist").click();
- when.wait(1000);
- when.click("nav:settings");
-
- when.select("modal:settings.maputnik:renderer", "mlgljs");
- then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
- "mlgljs"
+ await given.intercept(
+ /https:\/\/api\.maptiler\.com\/tiles\/v3-openmaptiles\/tiles\.json\?key=.*/,
+ "tileRequest",
+ "GET"
);
- when.select("modal:settings.maputnik:renderer", "ol");
- then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
- "ol"
+ await when.select("modal:settings.maputnik:renderer", "mlgljs");
+ await then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual("mlgljs");
+
+ const request = await when.waitForResponse("tileRequest");
+ expect(request.url()).toContain(
+ `https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=${tokens.openmaptiles}`
);
-
- given.intercept("https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=*", "tileRequest", "GET");
-
- when.select("modal:settings.maputnik:renderer", "mlgljs");
- then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
- "mlgljs"
- );
-
- when.waitForResponse("tileRequest").its("request").its("url").should("include", `https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=${tokens.openmaptiles}`);
- when.waitForResponse("tileRequest").its("request").its("url").should("include", `https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=${tokens.openmaptiles}`);
- when.waitForResponse("tileRequest").its("request").its("url").should("include", `https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=${tokens.openmaptiles}`);
});
-
});
describe("add layer", () => {
- beforeEach(() => {
- when.setStyle("layer");
- when.modal.open();
+ beforeEach(async () => {
+ await when.setStyle("layer");
+ await when.modal.open();
});
- test("shows duplicate id error", () => {
- when.setValue("add-layer.layer-id.input", "background");
- when.click("add-layer");
- then(get.elementByTestId("modal:add-layer")).shouldExist();
- then(get.element(".maputnik-modal-error")).shouldContainText(
- "Layer ID already exists"
- );
+ test("shows duplicate id error", async () => {
+ await when.setValue("add-layer.layer-id.input", "background");
+ await when.click("add-layer");
+ await then(get.elementByTestId("modal:add-layer")).shouldExist();
+ await then(get.element(".maputnik-modal-error")).shouldContainText("Layer ID already exists");
});
});
- describe("sources", () => {
- test("toggle");
+ describe("sources placeholder", () => {
+ test.skip("toggle", () => {});
});
describe("global state", () => {
- beforeEach(() => {
- when.click("nav:global-state");
+ beforeEach(async () => {
+ await when.click("nav:global-state");
});
- test("add variable", () => {
- when.wait(100);
- when.click("global-state-add-variable");
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ test("add variable", async () => {
+ await when.wait(100);
+ await when.click("global-state-add-variable");
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
state: { key1: { default: "value" } },
});
});
-
- test("add multiple variables", () => {
- when.click("global-state-add-variable");
- when.click("global-state-add-variable");
- when.click("global-state-add-variable");
- when.wait(100);
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ test("add multiple variables", async () => {
+ await when.click("global-state-add-variable");
+ await when.click("global-state-add-variable");
+ await when.click("global-state-add-variable");
+ await when.wait(100);
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
state: { key1: { default: "value" }, key2: { default: "value" }, key3: { default: "value" } },
});
});
- test("remove variable", () => {
- when.click("global-state-add-variable");
- when.click("global-state-add-variable");
- when.click("global-state-add-variable");
- when.click("global-state-remove-variable", 0);
- when.wait(100);
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ test("remove variable", async () => {
+ await when.click("global-state-add-variable");
+ await when.click("global-state-add-variable");
+ await when.click("global-state-add-variable");
+ await when.click("global-state-remove-variable", 0);
+ await when.wait(100);
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
state: { key2: { default: "value" }, key3: { default: "value" } },
});
});
- test("edit variable key", () => {
- when.click("global-state-add-variable");
- when.wait(100);
- when.setValue("global-state-variable-key:0", "mykey");
- when.typeKeys("{enter}");
- when.wait(100);
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ test("edit variable key", async () => {
+ await when.click("global-state-add-variable");
+ await when.wait(100);
+ await when.setValue("global-state-variable-key:0", "mykey");
+ await when.typeKeys("{enter}");
+ await when.wait(100);
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
state: { mykey: { default: "value" } },
});
});
- test("edit variable value", () => {
- when.click("global-state-add-variable");
- when.wait(100);
- when.setValue("global-state-variable-value:0", "myvalue");
- when.typeKeys("{enter}");
- when.wait(100);
- then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
+ test("edit variable value", async () => {
+ await when.click("global-state-add-variable");
+ await when.wait(100);
+ await when.setValue("global-state-variable-value:0", "myvalue");
+ await when.typeKeys("{enter}");
+ await when.wait(100);
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
state: { key1: { default: "myvalue" } },
});
});
});
describe("error panel", () => {
- test("not visible when no errors", () => {
- then(get.element("maputnik-message-panel-error")).shouldNotExist();
+ test("not visible when no errors", async () => {
+ await then(get.element("maputnik-message-panel-error")).shouldNotExist();
});
- test("visible on style error", () => {
- when.modal.open();
- when.modal.fillLayers({
+ test("visible on style error", async () => {
+ await when.modal.open();
+ await when.modal.fillLayers({
type: "circle",
layer: "invalid",
});
- then(get.element(".maputnik-message-panel-error")).shouldBeVisible();
+ await then(get.element(".maputnik-message-panel-error")).shouldBeVisible();
});
});
describe("Handle localStorage QuotaExceededError", () => {
- test("handles quota exceeded error when opening style from URL", () => {
+ test("handles quota exceeded error when opening style from URL", async () => {
// Clear localStorage to start fresh
- cy.clearLocalStorage();
-
- // fill localStorage until we get a QuotaExceededError
- cy.window().then(win => {
- let chunkSize = 1000;
- const chunk = new Array(chunkSize).join("x");
- let index = 0;
-
- // Keep adding until we hit the quota
- while (true) {
- try {
- const key = `maputnik:fill-${index++}`;
- win.localStorage.setItem(key, chunk);
- } catch (e: any) {
- // Verify it's a quota error
- if (e.name === "QuotaExceededError") {
- if (chunkSize <= 1) return;
- else {
- chunkSize /= 2;
- continue;
- }
- }
- throw e; // Unexpected error
- }
- }
- });
+ await when.clearLocalStorage();
+ await when.fillLocalStorage();
// Open the style via URL input
- when.click("nav:open");
- when.setValue("modal:open.url.input", get.exampleFileUrl());
- when.click("modal:open.url.button");
+ await when.click("nav:open");
+ await when.setValue("modal:open.url.input", get.exampleFileUrl());
+ await when.click("modal:open.url.button");
- then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
- then(get.styleFromLocalStorage()).shouldExist();
+ await then(get.styleFromLocalStorage()).shouldDeepNestedInclude(get.responseBody("example-style.json"));
+ await then(get.styleFromLocalStorage()).shouldExist();
});
});
});
diff --git a/e2e/playwright-helper.ts b/e2e/playwright-helper.ts
new file mode 100644
index 00000000..b99a2f02
--- /dev/null
+++ b/e2e/playwright-helper.ts
@@ -0,0 +1,414 @@
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import { expect, type Locator, type Page, type Request } from "@playwright/test";
+import { currentPage, recordCoverageChunk } from "./utils/fixtures";
+
+const DATA_ATTRIBUTE = "data-wd-key";
+const FIXTURES_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "fixtures");
+const isMac = process.platform === "darwin";
+
+function testIdSelector(testId: string): string {
+ return `[${DATA_ATTRIBUTE}="${testId}"]`;
+}
+
+/** Retries `assertion` until it stops throwing */
+async function retry(
+ assertion: () => Promise | void,
+ timeout = 10000,
+ interval = 100
+): Promise {
+ const start = Date.now();
+ let lastError: unknown;
+ while (true) {
+ try {
+ await assertion();
+ return;
+ } catch (error) {
+ lastError = error;
+ if (Date.now() - start > timeout) throw lastError;
+ await new Promise((resolve) => setTimeout(resolve, interval));
+ }
+ }
+}
+
+/**
+ * A lazily-evaluated value (e.g. the style in localStorage). Assertions on a
+ * Query re-read the value until they pass.
+ */
+class Query {
+ readonly __query = true as const;
+ constructor(private readonly getter: () => Promise) {}
+
+ get(): Promise {
+ return this.getter();
+ }
+
+ then(mapper: (value: T) => U | Promise): Query {
+ return new Query(async () => mapper(await this.getter()));
+ }
+}
+
+function isQuery(target: unknown): target is Query {
+ return typeof target === "object" && target !== null && (target as Query).__query === true;
+}
+
+function isLocator(target: unknown): target is Locator {
+ return (
+ typeof target === "object" &&
+ target !== null &&
+ typeof (target as Locator).count === "function" &&
+ typeof (target as Locator).boundingBox === "function"
+ );
+}
+
+/** Asserts that every top-level key in `expected` deep-equals its counterpart in `actual`. */
+function assertDeepNestedInclude(actual: any, expected: Record): void {
+ for (const key of Object.keys(expected)) {
+ expect(actual?.[key], `property "${key}"`).toEqual(expected[key]);
+ }
+}
+
+/**
+ * Fluent, auto-retrying assertions over a Playwright Locator or a lazily
+ * evaluated value/Query. This is the generic base that the maputnik-specific
+ * assertable extends.
+ */
+export class Assertable {
+ constructor(private readonly target: T) {}
+
+ private locator(): Locator {
+ if (!isLocator(this.target)) throw new Error("Expected a Locator target for this assertion");
+ return this.target;
+ }
+
+ protected async assertValue(assertion: (value: any) => void): Promise {
+ const target = this.target;
+ if (isQuery(target)) {
+ await retry(async () => assertion(await target.get()));
+ } else {
+ assertion(await (target as any));
+ }
+ }
+
+ // Element assertions (auto-retrying via Playwright web-first assertions).
+ shouldBeVisible = () => expect(this.locator().first()).toBeVisible();
+ // Some testids resolve to many elements that are always rendered but hidden
+ // (e.g. per-field documentation panels); "not visible" means none is visible.
+ shouldNotBeVisible = () => expect(this.locator().filter({ visible: true })).toHaveCount(0);
+ shouldExist = async () => {
+ if (isLocator(this.target)) {
+ await expect(this.locator().first()).toBeAttached();
+ } else {
+ await this.assertValue((value) => expect(value).toBeTruthy());
+ }
+ };
+ shouldNotExist = () => expect(this.locator()).toHaveCount(0);
+ shouldBeFocused = () => expect(this.locator().first()).toBeFocused();
+ shouldNotBeFocused = () => expect(this.locator().first()).not.toBeFocused();
+ shouldHaveValue = (value: string) => expect(this.locator().first()).toHaveValue(value);
+ shouldContainText = async (text: string) => {
+ const locator = this.locator();
+ // Prefer the visible element when a testid resolves to several (only the
+ // open documentation panel is visible; the rest are hidden in the DOM).
+ const target = (await locator.count()) > 1 ? locator.filter({ visible: true }).first() : locator.first();
+ await expect(target).toContainText(text);
+ };
+ shouldHaveText = (text: string) => expect(this.locator().first()).toHaveText(text);
+ shouldHaveLength = (length: number) => expect(this.locator()).toHaveCount(length);
+ shouldHaveCss = (property: string, value: string) => expect(this.locator().first()).toHaveCSS(property, value);
+
+ // Value assertions (auto-retrying for Query targets).
+ shouldEqual = (value: any) => this.assertValue((actual) => expect(actual).toBe(value));
+
+ shouldInclude = (value: any) =>
+ this.assertValue((actual) => {
+ if (typeof value === "object" && value !== null) {
+ expect(actual).toMatchObject(value);
+ } else {
+ expect(String(actual)).toContain(String(value));
+ }
+ });
+
+ shouldDeepNestedInclude = (value: Record) =>
+ this.assertValue((actual) => assertDeepNestedInclude(actual, value));
+}
+
+async function typeSequence(page: Page, text: string): Promise {
+ const tokens = text.match(/\{[^}]+\}|[^{]+/g) ?? [];
+ const modifierMap: Record = { meta: "Meta", ctrl: "Control", shift: "Shift", alt: "Alt" };
+ const namedKeys: Record = {
+ esc: "Escape",
+ enter: "Enter",
+ backspace: "Backspace",
+ del: "Delete",
+ tab: "Tab",
+ home: "Home",
+ };
+
+ for (let i = 0; i < tokens.length; i++) {
+ const token = tokens[i];
+ if (!token.startsWith("{") || !token.endsWith("}")) {
+ await page.keyboard.type(token);
+ continue;
+ }
+ const name = token.slice(1, -1).toLowerCase();
+ if (name === "selectall") {
+ await page.keyboard.press(isMac ? "Meta+a" : "Control+a");
+ } else if (namedKeys[name]) {
+ await page.keyboard.press(namedKeys[name]);
+ } else if (modifierMap[name]) {
+ const modifiers = [modifierMap[name]];
+ let j = i + 1;
+ while (j < tokens.length && /^\{(meta|ctrl|shift|alt)\}$/i.test(tokens[j])) {
+ modifiers.push(modifierMap[tokens[j].slice(1, -1).toLowerCase()]);
+ j++;
+ }
+ const key = tokens[j] ?? "";
+ await page.keyboard.press([...modifiers, key].join("+"));
+ i = j;
+ }
+ }
+}
+
+async function centerOf(locator: Locator): Promise<{ x: number; y: number }> {
+ const box = await locator.boundingBox();
+ if (!box) throw new Error("Element has no bounding box");
+ return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
+}
+
+/**
+ * This is where all plywright-specific test helpers live.
+ * It is used by the MaputnikDriver to implement the Maputnik-specific test helpers.
+ */
+export class PlaywrightHelper {
+ private readonly recordedRequests = new Map();
+
+ private get page(): Page {
+ return currentPage();
+ }
+
+ private testId(testId: string): Locator {
+ return this.page.locator(testIdSelector(testId));
+ }
+
+ /** Reads and parses a JSON fixture from the fixtures directory. */
+ public readFixture(name: string): any {
+ return JSON.parse(fs.readFileSync(path.join(FIXTURES_DIR, name), "utf-8"));
+ }
+
+ /** Wraps a lazily-evaluated value so assertions on it auto-retry. */
+ public query(getter: () => Promise): Query {
+ return new Query(getter);
+ }
+
+ /** Entry point for fluent assertions over a Locator or a value/Query. */
+ public then = (target: T): Assertable => new Assertable(target);
+
+ public given = {
+ intercept: async (pattern: RegExp, alias: string, _method = "GET") => {
+ this.recordedRequests.set(alias, []);
+ await this.page.route(pattern, (route) => {
+ this.recordedRequests.get(alias)!.push(route.request());
+ route.continue();
+ });
+ },
+
+ interceptAndMockResponse: async (options: {
+ method?: string;
+ url: string | RegExp;
+ response: unknown | { fixture: string };
+ alias?: string;
+ }) => {
+ const { url, response, alias } = options;
+ if (alias) this.recordedRequests.set(alias, []);
+ await this.page.route(url, (route) => {
+ if (alias) this.recordedRequests.get(alias)!.push(route.request());
+ const body =
+ response && typeof response === "object" && "fixture" in (response as any)
+ ? this.readFixture((response as { fixture: string }).fixture)
+ : response;
+ route.fulfill({ json: body });
+ });
+ },
+ };
+
+ public when = {
+ visit: async (url: string) => {
+ // Snapshot coverage before navigating, since a full page load resets it.
+ await recordCoverageChunk(this.page);
+ await this.page.goto(url);
+ },
+
+ wait: (ms: number) => this.page.waitForTimeout(ms),
+
+ tab: () => this.page.keyboard.press("Tab"),
+
+ typeKeys: (keys: string) => typeSequence(this.page, keys),
+
+ /** Types raw text into the focused element (no "{key}" sequence parsing). */
+ typeText: (text: string) => this.page.keyboard.type(text),
+
+ clickButtonByName: async (name: string) => {
+ await this.page.getByRole("button", { name }).click();
+ },
+
+ click: async (testId: string, index = 0) => {
+ // Documentation buttons are wrapped in a