test: migrate e2e to Playwright and component test to Vitest browser mode

Replace Cypress with Playwright for the end-to-end suite and drop
@shellygo/cypress-test-utils in favour of a hand-written MaputnikDriver
page object that keeps the fluent then(...).shouldX() assertion style
(now async). The InputAutocomplete component test moves to Vitest browser
mode using the Playwright provider.

- e2e/maputnik-driver.ts: driver + MaputnikAssertable over Playwright
- e2e/{fixtures,coverage,global-setup,global-teardown}.ts: test fixture,
  istanbul coverage collection, and nyc report generation
- playwright.config.ts / vitest.config.ts
- Code coverage preserved: dev server is istanbul-instrumented, per-test
  window.__coverage__ is merged via nyc into coverage/coverage-final.json
- CI: Cypress jobs replaced with Playwright; docker e2e runs against the
  container via E2E_NO_WEBSERVER
- Remove Cypress deps, config and support files; update docs and .nycrc

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
HarelM
2026-07-08 12:42:32 +03:00
parent f7b48139a4
commit c3dd253737
33 changed files with 1908 additions and 3879 deletions
+29 -24
View File
@@ -1,40 +1,45 @@
import { MaputnikDriver } from "./maputnik-driver";
import { test, setupMaputnik } from "./fixtures";
test.describe("accessibility", () => {
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
setupMaputnik();
test.describe("skip links", () => {
beforeEach(() => {
when.setStyle("layer");
test.beforeEach(async ({ driver }) => {
await driver.when.setStyle("layer");
});
test("skip link to layer list", () => {
test("skip link to layer list", async ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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();
});
});
});
+12 -11
View File
@@ -1,18 +1,19 @@
import { MaputnikDriver } from "./maputnik-driver";
import { test, setupMaputnik } from "./fixtures";
test.describe("code editor", () => {
const { beforeAndAfter, when, get, then } = new MaputnikDriver();
beforeAndAfter();
setupMaputnik();
test("open code editor", () => {
when.click("nav:code-editor");
then(get.element(".maputnik-code-editor")).shouldExist();
test("open code editor", async ({ driver }) => {
const { when, get, then } = driver;
await when.click("nav:code-editor");
await then(get.element(".maputnik-code-editor")).shouldExist();
});
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("closes code editor", async ({ driver }) => {
const { when, get, then } = driver;
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();
});
});
+30
View File
@@ -0,0 +1,30 @@
import fs from "node:fs";
import path from "node:path";
import type { Page } from "@playwright/test";
const OUTPUT_DIR = path.resolve(process.cwd(), ".nyc_output");
/**
* Reads the istanbul coverage object (injected by vite-plugin-istanbul) from the
* given page. Returns `null` when the page has not been instrumented.
*/
export async function readCoverage(page: Page): Promise<unknown | null> {
try {
return await page.evaluate(() => (window as unknown as { __coverage__?: unknown }).__coverage__ ?? null);
} catch {
// Page might be navigating/closed.
return null;
}
}
/**
* Persists a coverage chunk to `.nyc_output` so that `nyc report` can merge it.
* istanbul-lib-coverage (used by nyc) sums the hit counts across every file it
* finds, so writing one file per chunk is enough to accumulate coverage across
* navigations and tests.
*/
export function writeCoverage(coverage: unknown, id: string): void {
if (!coverage) return;
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
fs.writeFileSync(path.join(OUTPUT_DIR, `playwright-${id}.json`), JSON.stringify(coverage));
}
+27
View File
@@ -0,0 +1,27 @@
import { test as base, expect } from "@playwright/test";
import { MaputnikDriver } from "./maputnik-driver";
/**
* Playwright test with a per-test `driver` (the maputnik page object) that also
* collects istanbul coverage once the test finishes.
*/
export const test = base.extend<{ driver: MaputnikDriver }>({
driver: async ({ page }, use, testInfo) => {
const driver = new MaputnikDriver(page);
await use(driver);
await driver.flushCoverage(testInfo);
},
});
export { expect };
/**
* Registers the shared `beforeEach` used by most specs: mock the style responses
* and load the default (geojson + raster) style.
*/
export function setupMaputnik(): void {
test.beforeEach(async ({ driver }) => {
await driver.given.setupMockBackedResponses();
await driver.when.setStyle("both");
});
}
+12
View File
@@ -0,0 +1,12 @@
import fs from "node:fs";
import path from "node:path";
/**
* Clears the istanbul coverage output directory before the e2e run so stale
* coverage from previous runs is not merged into the report.
*/
export default function globalSetup(): void {
const dir = path.resolve(process.cwd(), ".nyc_output");
fs.rmSync(dir, { recursive: true, force: true });
fs.mkdirSync(dir, { recursive: true });
}
+23
View File
@@ -0,0 +1,23 @@
import fs from "node:fs";
import path from "node:path";
import { execFileSync } from "node:child_process";
/**
* Merges the per-test istanbul coverage chunks collected in `.nyc_output` into a
* report (configured by `.nycrc.json`) once the whole e2e run has finished.
*/
export default function globalTeardown(): void {
const dir = path.resolve(process.cwd(), ".nyc_output");
const hasCoverage = fs.existsSync(dir) && fs.readdirSync(dir).some((f) => f.endsWith(".json"));
if (!hasCoverage) {
console.warn("No coverage data collected; skipping coverage report.");
return;
}
try {
execFileSync("npx", ["nyc", "report"], { stdio: "inherit" });
} catch (error) {
// Don't fail the whole run if the report can't be generated (e.g. when
// running against a container whose source paths differ from the host).
console.warn("Failed to generate coverage report:", error);
}
}
+43 -81
View File
@@ -1,124 +1,86 @@
import { MaputnikDriver } from "./maputnik-driver";
import { test, setupMaputnik } from "./fixtures";
test.describe("history", () => {
const { beforeAndAfter, when, get, then } = new MaputnikDriver();
beforeAndAfter();
setupMaputnik();
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";
});
test("undo/redo", async ({ driver }) => {
const { get, when, then } = driver;
await when.setStyle("geojson");
await when.modal.open();
test("undo/redo", () => {
when.setStyle("geojson");
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 ({ driver }) => {
const { get, when, then } = driver;
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" }],
});
});
});
+18 -18
View File
@@ -1,35 +1,35 @@
import { MaputnikDriver } from "./maputnik-driver";
import { test, setupMaputnik } from "./fixtures";
test.describe("i18n", () => {
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
setupMaputnik();
test.describe("language detector", () => {
test("English", () => {
const url = "?lng=en";
when.visit(url);
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("en");
test("English", async ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
await when.visit("?lng=ja");
await then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("ja");
});
});
test.describe("language switcher", () => {
beforeEach(() => {
when.setStyle("layer");
test.beforeEach(async ({ driver }) => {
await driver.when.setStyle("layer");
});
test("the language switcher switches to Japanese", () => {
test("the language switcher switches to Japanese", async ({ driver }) => {
const { get, when, then } = driver;
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("スタイル設定");
});
});
});
+44 -35
View File
@@ -1,60 +1,69 @@
import { MaputnikDriver } from "./maputnik-driver";
import { test, setupMaputnik } from "./fixtures";
test.describe("keyboard", () => {
const { beforeAndAfter, given, when, get, then } = new MaputnikDriver();
beforeAndAfter();
setupMaputnik();
test.describe("shortcuts", () => {
beforeEach(() => {
given.setupMockBackedResponses();
when.setStyle("");
test.beforeEach(async ({ driver }) => {
await driver.given.setupMockBackedResponses();
await driver.when.setStyle("");
});
test("ESC should unfocus", () => {
test("ESC should unfocus", async ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
await when.typeKeys("!");
await then(get.elementByTestId("modal:debug")).shouldBeVisible();
});
});
});
+138 -166
View File
@@ -1,96 +1,82 @@
import { MaputnikDriver } from "./maputnik-driver";
import { v1 as uuid } from "uuid";
import { test, setupMaputnik } from "./fixtures";
import type { MaputnikDriver } from "./maputnik-driver";
test.describe("layer editor", () => {
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
beforeEach(() => {
when.setStyle("both");
when.modal.open();
setupMaputnik();
test.beforeEach(async ({ driver }) => {
await driver.when.setStyle("both");
await driver.when.modal.open();
});
function createBackground() {
async function createBackground(driver: MaputnikDriver) {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
const bgId = await createBackground(driver);
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" }],
});
});
test.describe("source", () => {
test("should show error when the source is invalid", () => {
when.modal.fillLayers({
test("should show error when the source is invalid", async ({ driver }) => {
const { get, when, then } = driver;
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)");
});
});
test.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");
test.beforeEach(async ({ driver }) => {
const { when } = driver;
bgId = await createBackground(driver);
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 ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id: "background:" + bgId, type: "background", minzoom: 1 }],
});
});
test("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 ({ driver }) => {
const { get, when, then } = driver;
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 }],
});
});
});
@@ -98,22 +84,17 @@ test.describe("layer editor", () => {
test.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");
test.beforeEach(async ({ driver }) => {
const { when } = driver;
bgId = await createBackground(driver);
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 ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id: "background:" + bgId, type: "background", maxzoom: 1 }],
});
});
});
@@ -122,41 +103,36 @@ test.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");
test.beforeEach(async ({ driver }) => {
const { when } = driver;
bgId = await createBackground(driver);
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 ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + bgId,
type: "background",
metadata: {
"maputnik:comment": comment,
},
metadata: { "maputnik:comment": comment },
},
],
});
});
test.describe("when unsetting", () => {
beforeEach(() => {
when.clear("layer-comment.input");
when.click("min-zoom.input-text");
test.beforeEach(async ({ driver }) => {
const { when } = driver;
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 ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id: "background:" + bgId, type: "background" }],
});
});
});
@@ -164,131 +140,127 @@ test.describe("layer editor", () => {
test.describe("color", () => {
let bgId: string;
beforeEach(() => {
bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
when.click("spec-field:background-color");
test.beforeEach(async ({ driver }) => {
const { when } = driver;
bgId = await createBackground(driver);
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 ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id: "background:" + bgId, type: "background" }],
});
});
});
test.describe("opacity", () => {
let bgId: string;
beforeEach(() => {
bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
when.type("spec-field-input:background-opacity", "0.");
test.beforeEach(async ({ driver }) => {
const { when } = driver;
bgId = await createBackground(driver);
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 ({ driver }) => {
await driver
.then(driver.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 ({ driver }) => {
const { get, when, then } = driver;
await when.click("layer-list-item:background:" + bgId);
await then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0");
});
});
test.describe("filter", () => {
test("expand/collapse");
test("compound filter");
test.skip("expand/collapse", () => {});
test.skip("compound filter", () => {});
});
test.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();
test("text-font", async ({ driver }) => {
const { get, when, then } = driver;
await when.setStyle("font");
await when.collapseGroupInLayerEditor();
await when.collapseGroupInLayerEditor(1);
await when.collapseGroupInLayerEditor(2);
await when.doWithin("spec-field:text-font", async () => {
await get.element(".maputnik-autocomplete input").first().click();
});
then(get.element(".maputnik-autocomplete-menu-item")).shouldBeVisible();
then(get.element(".maputnik-autocomplete-menu-item")).shouldHaveLength(3);
await then(get.element(".maputnik-autocomplete-menu-item")).shouldBeVisible();
await then(get.element(".maputnik-autocomplete-menu-item")).shouldHaveLength(3);
});
});
test.describe("paint", () => {
test("expand/collapse");
test("color");
test("pattern");
test("opacity");
test.skip("expand/collapse", () => {});
test.skip("color", () => {});
test.skip("pattern", () => {});
test.skip("opacity", () => {});
});
test.describe("json-editor", () => {
test("add", () => {
const id = when.modal.fillLayers({
test("add", async ({ driver }) => {
const { get, when, then } = driver;
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 sourceText.click();
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 ({ driver }) => {
const { get, when, then } = driver;
const bgId = await createBackground(driver);
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 {"
await when.appendTextInJsonEditor(
" {"
);
then(get.element(".cm-lint-marker-error")).shouldExist();
await then(get.element(".cm-lint-marker-error")).shouldExist();
});
});
test.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 ({ driver }) => {
const { get, when, then } = driver;
// 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 get
.element(".maputnik-scroll-container")
.evaluate((el) => el.scrollTo(0, el.scrollHeight));
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();
});
});
});
+212 -385
View File
@@ -1,411 +1,278 @@
import { MaputnikDriver } from "./maputnik-driver";
import { test, setupMaputnik } from "./fixtures";
test.describe("layers list", () => {
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
beforeEach(() => {
when.setStyle("both");
when.modal.open();
setupMaputnik();
test.beforeEach(async ({ driver }) => {
await driver.when.setStyle("both");
await driver.when.modal.open();
});
test.describe("ops", () => {
let id: string;
beforeEach(() => {
id = when.modal.fillLayers({
type: "background",
});
test.beforeEach(async ({ driver }) => {
id = await driver.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 ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "background" }],
});
});
test.describe("when clicking delete", () => {
beforeEach(() => {
when.click("layer-list-item:" + id + ":delete");
test.beforeEach(async ({ driver }) => {
await driver.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 ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [],
});
});
});
test.describe("when clicking duplicate", () => {
beforeEach(() => {
when.click("layer-list-item:" + id + ":copy");
test.beforeEach(async ({ driver }) => {
await driver.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 ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id + "-copy",
type: "background",
},
{
id: id,
type: "background",
},
{ id: id + "-copy", type: "background" },
{ id, type: "background" },
],
});
});
});
test.describe("when clicking hide", () => {
beforeEach(() => {
when.click("layer-list-item:" + id + ":toggle-visibility");
test.beforeEach(async ({ driver }) => {
await driver.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 ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "background", layout: { visibility: "none" } }],
});
});
test.describe("when clicking show", () => {
beforeEach(() => {
when.click("layer-list-item:" + id + ":toggle-visibility");
test.beforeEach(async ({ driver }) => {
await driver.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 ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "background", layout: { visibility: "visible" } }],
});
});
});
test.describe("when selecting a layer", () => {
let secondId: string;
beforeEach(() => {
when.modal.open();
secondId = when.modal.fillLayers({
test.beforeEach(async ({ driver }) => {
await driver.when.modal.open();
secondId = await driver.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 ({ driver }) => {
const { get, when, then } = driver;
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);
});
});
});
});
test.describe("background", () => {
test("add", () => {
const id = when.modal.fillLayers({
type: "background",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "background",
},
],
test("add", async ({ driver }) => {
const { get, when, then } = driver;
const id = await when.modal.fillLayers({ type: "background" });
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "background" }],
});
});
test.describe("modify", () => {});
test.skip("modify", () => {});
});
test.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 ({ driver }) => {
const { get, when, then } = driver;
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", () => {});
});
test.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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
await when.modal.open();
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" },
],
});
});
});
test.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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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();
});
});
test.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 ({ driver }) => {
const { get, when, then } = driver;
const id = await when.modal.fillLayers({ type: "raster", layer: "raster" });
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "raster", source: "raster" }],
});
});
});
test.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 ({ driver }) => {
const { get, when, then } = driver;
const id = await when.modal.fillLayers({ type: "circle", layer: "example" });
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "circle", source: "example" }],
});
});
});
test.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 ({ driver }) => {
const { get, when, then } = driver;
const id = await when.modal.fillLayers({ type: "fill-extrusion", layer: "example" });
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "fill-extrusion", source: "example" }],
});
});
});
test.describe("hillshade", () => {
test("add", () => {
const id = when.modal.fillLayers({
type: "hillshade",
layer: "example",
test("add", async ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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)"],
},
},
],
});
@@ -413,129 +280,89 @@ test.describe("layers list", () => {
});
test.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 ({ driver }) => {
const { get, when, then } = driver;
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();
});
});
test.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 ({ driver }) => {
const { get, when, then } = driver;
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();
});
});
test.describe("groups", () => {
test("simple", async ({ driver }) => {
const { get, when, then } = driver;
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();
});
});
test.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 ({ driver }) => {
const { get, when, then } = driver;
await when.modal.open();
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" },
],
});
});
});
test.describe("sticky header", () => {
test("should keep header visible when scrolling layer list", () => {
test("should keep header visible when scrolling layer list", async ({ driver }) => {
const { get, when, then } = driver;
// 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",
});
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 get.elementByTestId("layer-list").evaluate((el) => el.scrollTo(0, el.scrollHeight));
await when.wait(200);
await then(header).shouldBeVisible();
await then(get.elementByTestId("layer-list:add-layer")).shouldBeVisible();
});
});
});
+42 -42
View File
@@ -1,67 +1,67 @@
import { MaputnikDriver } from "./maputnik-driver";
import { test, setupMaputnik } from "./fixtures";
test.describe("map", () => {
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
setupMaputnik();
test.describe("zoom level", () => {
test("via url", () => {
test("via url", async ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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");
});
});
test.describe("search", () => {
test("should exist", () => {
then(get.searchControl()).shouldBeVisible();
test("should exist", async ({ driver }) => {
const { get, then } = driver;
await then(get.searchControl()).shouldBeVisible();
});
});
test.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();
test.beforeEach(async ({ driver }) => {
await driver.when.setStyle("rectangles");
await driver.then(driver.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 ({ driver }) => {
const { get, when, then } = driver;
await when.clickCenter("maplibre:map");
await then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
});
test("should open a second feature after closing popup", async ({ driver }) => {
const { get, when, then } = driver;
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();
});
});
});
+542 -223
View File
@@ -1,122 +1,307 @@
/// <reference types="cypress-plugin-tab" />
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { expect, type Locator, type Page, type Request, type TestInfo } from "@playwright/test";
import { readCoverage, writeCoverage } from "./coverage";
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 FIXTURES_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "fixtures");
const DATA_ATTRIBUTE = "data-wd-key";
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;
};
const isMac = process.platform === "darwin";
export class MaputnikAssertable<T> extends Assertable<T> {
shouldEqualToStoredStyle = () =>
then(
new CypressHelper().get.window().then((win: Window) => {
const style = styleFromWindow(win);
then(this.chainable).shouldDeepNestedInclude(style);
})
);
function testIdSelector(testId: string): string {
return `[${DATA_ATTRIBUTE}="${testId}"]`;
}
export function readFixture(name: string): any {
const contents = fs.readFileSync(path.join(FIXTURES_DIR, name), "utf-8");
return JSON.parse(contents);
}
/** Reads the maputnik style currently persisted in localStorage. */
function styleFromLocalStorage(page: Page): Promise<any> {
return page.evaluate(() => {
const styleId = window.localStorage.getItem("maputnik:latest_style");
const styleItemKey = `maputnik:style:${styleId}`;
const styleItem = window.localStorage.getItem(styleItemKey);
if (!styleItem) throw new Error("Could not get styleItem from localStorage");
return JSON.parse(styleItem);
});
}
async function retry(assertion: () => Promise<void> | void, timeout = 10000, interval = 100): Promise<void> {
const start = Date.now();
let lastError: unknown;
for (;;) {
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, mirroring Cypress' retry-ability.
*/
export class Query<T> {
readonly __maputnikQuery = true as const;
constructor(private readonly getter: () => Promise<T>) {}
get(): Promise<T> {
return this.getter();
}
then<U>(mapper: (value: T) => U | Promise<U>): Query<U> {
return new Query<U>(async () => mapper(await this.getter()));
}
}
function isQuery(target: unknown): target is Query<unknown> {
return typeof target === "object" && target !== null && (target as Query<unknown>).__maputnikQuery === 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<string, unknown>): void {
for (const key of Object.keys(expected)) {
expect(actual?.[key], `property "${key}"`).toEqual(expected[key]);
}
}
export class MaputnikAssertable<T> {
constructor(private readonly target: T, private readonly page?: Page) {}
private locator(): Locator {
if (!isLocator(this.target)) throw new Error("Expected a Locator target for this assertion");
return this.target;
}
private async assertValue(assertion: (value: any) => void): Promise<void> {
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<string, unknown>) =>
this.assertValue((actual) => assertDeepNestedInclude(actual, value));
/**
* Asserts that the object under test (a fixture / response body) contains every
* top-level property of the style currently stored in localStorage.
*/
shouldEqualToStoredStyle = async () => {
if (!this.page) throw new Error("shouldEqualToStoredStyle requires a page-bound assertable");
const expected = await (this.target as any);
await retry(async () => {
const stored = await styleFromLocalStorage(this.page!);
assertDeepNestedInclude(expected, stored);
});
};
}
/**
* Translates a Cypress-style key sequence (e.g. "{meta}z", "{esc}", "0.") into
* Playwright keyboard actions on the currently focused element.
*/
async function typeSequence(page: Page, text: string): Promise<void> {
const tokens = text.match(/\{[^}]+\}|[^{]+/g) ?? [];
const modifierMap: Record<string, string> = { meta: "Meta", ctrl: "Control", shift: "Shift", alt: "Alt" };
const namedKeys: Record<string, string> = {
esc: "Escape",
enter: "Enter",
backspace: "Backspace",
del: "Delete",
tab: "Tab",
};
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 };
}
export class MaputnikDriver {
private helper = new MaputnikCypressHelper();
private modalDriver = new ModalDriver();
private scope: Locator | null = null;
private readonly recordedRequests = new Map<string, Request[]>();
private readonly coverageChunks: unknown[] = [];
private readonly modalDriver = new ModalDriver(this);
public beforeAndAfter = () => {
beforeEach(() => {
this.given.setupMockBackedResponses();
this.when.setStyle("both");
constructor(private readonly page: Page) {
// Accept confirm dialogs (e.g. the "replace current style" prompt shown when
// a style is loaded via the URL). Playwright dismisses dialogs by default.
page.on("dialog", (dialog) => dialog.accept().catch(() => undefined));
}
// ---- Coverage collection -------------------------------------------------
/** Snapshots the current coverage before the page is unloaded. */
private async captureCoverage(): Promise<void> {
const coverage = await readCoverage(this.page);
if (coverage) this.coverageChunks.push(coverage);
}
async flushCoverage(testInfo: TestInfo): Promise<void> {
await this.captureCoverage();
this.coverageChunks.forEach((chunk, index) => {
writeCoverage(chunk, `${testInfo.testId}-${index}`);
});
};
}
public then = (chainable: Cypress.Chainable<any>) =>
new MaputnikAssertable(chainable);
// ---- Element access ------------------------------------------------------
private root(): Page | Locator {
return this.scope ?? this.page;
}
private testId(testId: string): Locator {
return this.root().locator(testIdSelector(testId));
}
then = <T>(target: T) => new MaputnikAssertable(target, this.page);
// ---- given ---------------------------------------------------------------
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",
fixture: (_name: string, _alias?: string) => {
// Fixtures are read directly from disk in Playwright, no registration needed.
},
intercept: async (url: string, alias: string, _method = "GET") => {
this.recordedRequests.set(alias, []);
// Convert the Cypress-style glob (which may contain "?" in a query string)
// into a regex so query parameters match reliably.
const pattern = new RegExp(
"^" + url.replace(/[.+^${}()|[\]\\?]/g, "\\$&").replace(/\*/g, ".*") + "$"
);
await this.page.route(pattern, (route) => {
this.recordedRequests.get(alias)!.push(route.request());
route.continue();
});
this.helper.given.interceptAndMockResponse({
method: "GET",
url: baseUrl + "example-layer-style.json",
response: {
fixture: "example-layer-style.json",
},
},
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)
? readFixture((response as { fixture: string }).fixture)
: response;
route.fulfill({ json: body });
});
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.given.interceptAndMockResponse({
method: "GET",
url: baseUrl + fixture,
response: { fixture },
alias: fixture === "example-style.json" ? "example-style.json" : undefined,
});
}
await this.given.interceptAndMockResponse({ method: "GET", url: /example\.local\//, response: [] });
await this.given.interceptAndMockResponse({ method: "GET", url: /example\.com\//, response: [] });
await this.given.interceptAndMockResponse({
method: "GET",
url: "https://www.glyph-server.com/*",
response: ["Font 1", "Font 2", "Font 3"],
@@ -124,149 +309,283 @@ export class MaputnikDriver {
},
};
// ---- when ----------------------------------------------------------------
public when = {
...this.helper.when,
modal: this.modalDriver.when,
doWithin: (selector: string, fn: () => void) => {
this.helper.when.doWithin(fn, selector);
visit: async (url: string) => {
await this.captureCoverage();
const target = url.startsWith("http") ? url : new URL(url, baseUrl).toString();
await this.page.goto(target);
},
tab: () => this.helper.get.element("body").tab(),
waitForExampleFileResponse: () => {
this.helper.when.waitForResponse("example-style.json");
wait: (ms: number) => this.page.waitForTimeout(ms),
tab: () => this.page.keyboard.press("Tab"),
typeKeys: (keys: string) => typeSequence(this.page, keys),
doWithin: async (selector: string, fn: () => Promise<void> | void) => {
const previous = this.scope;
this.scope = (previous ?? this.page).locator(testIdSelector(selector));
try {
await fn();
} finally {
this.scope = previous;
}
},
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}");
click: async (testId: string, index = 0) => {
// Documentation buttons are wrapped in a <label>/.maputnik-doc-target that
// Playwright treats as intercepting the click; bypass the check for them.
const force = testId.startsWith("field-doc-button-");
await this.testId(testId).nth(index).click({ force });
},
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);
realClick: async (testId: string) => {
await this.testId(testId).click();
},
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);
hover: async (testId: string) => {
await this.testId(testId).hover();
},
setStyle: (
styleProperties: "geojson" | "raster" | "both" | "layer" | "rectangles" | "font" | "zoom_7_center_0_51" | "",
focus: async (testId: string) => {
await this.testId(testId).focus();
},
clear: async (testId: string) => {
await this.testId(testId).clear();
},
select: async (testId: string, value: string) => {
await this.testId(testId).selectOption(value);
},
selectWithin: async (selector: string, value: string) => {
await this.root().locator(testIdSelector(selector)).locator("select").selectOption(value);
},
setValue: async (testId: string, text: string) => {
const input = this.testId(testId);
await input.fill("");
await input.fill(text);
},
type: async (testId: string, text: string) => {
await this.testId(testId).focus();
// Place the caret at the start of the field (matching how the original
// Cypress suite typed), so a leading "{backspace}" is a no-op rather than
// clearing an already-committed value.
await this.page.keyboard.press("Home");
await typeSequence(this.page, text);
},
setValueToPropertyArray: async (selector: string, value: string) => {
await this.when.doWithin(selector, async () => {
const input = this.root().locator(".maputnik-array-block-content input").last();
await input.focus();
await typeSequence(this.page, "{selectall}" + value);
});
},
addValueToPropertyArray: async (selector: string, value: string) => {
await this.when.doWithin(selector, async () => {
await this.root().locator(".maputnik-array-add-value").click();
const input = this.root().locator(".maputnik-array-block-content input").last();
await input.focus();
await typeSequence(this.page, "{selectall}" + value);
});
},
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<string, string> = {
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
const toolbarLink = this.helper.get.elementByTestId("toolbar:link");
toolbarLink.scrollIntoView();
toolbarLink.should("be.visible");
await this.when.visit(url.toString());
const toolbarLink = this.testId("toolbar:link");
await toolbarLink.scrollIntoViewIfNeeded();
await expect(toolbarLink).toBeVisible();
},
typeKeys: (keys: string) => this.helper.get.element("body").type(keys),
clickZoomIn: () => {
this.helper.get.element(".maplibregl-ctrl-zoom-in").click();
openASecondStyleWithDifferentZoomAndCenter: async () => {
await this.page.getByRole("button", { name: "Open" }).click();
const input = this.testId("modal:open.url.input");
await expect(input).toBeEnabled();
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.openFileByFixture("example-style.json", "modal:open.dropzone", "modal:open.file.input");
await this.when.wait(200);
},
dropExampleFile: async () => {
await this.dropFileByFixture("example-style.json", "modal:open.dropzone");
await this.when.wait(200);
},
clickZoomIn: async () => {
await this.page.locator(".maplibregl-ctrl-zoom-in").click();
},
closePopup: async () => {
await this.page.locator(".maplibregl-popup-close-button").click();
},
clickCenter: async (testId: string) => {
const { x, y } = await centerOf(this.testId(testId));
await this.page.mouse.move(x, y);
await this.page.mouse.down();
await this.when.wait(200);
await this.page.mouse.up();
},
collapseGroupInLayerEditor: async (index = 0) => {
await this.page.locator(".maputnik-layer-editor-group__button").nth(index).click();
},
appendTextInJsonEditor: async (text: string) => {
await this.page.locator(".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.page.keyboard.press("Home");
await typeSequence(this.page, text);
},
setTextInJsonEditor: async (text: string) => {
const firstLine = this.page.locator(".cm-line").first();
await firstLine.click();
await this.page.keyboard.press(isMac ? "Meta+a" : "Control+a");
await this.page.keyboard.type(text);
},
dragAndDropWithWait: async (source: string, target: string) => {
const from = await centerOf(this.testId(source));
const to = await centerOf(this.testId(target));
await this.page.mouse.move(from.x, from.y);
await this.page.mouse.down();
await this.page.mouse.move(from.x, from.y + 10);
await this.page.mouse.move(to.x, to.y, { steps: 10 });
await this.when.wait(100);
await this.page.mouse.up();
},
waitForResponse: async (alias: string) => {
const requests = this.recordedRequests.get(alias);
if (!requests) throw new Error(`No intercept registered for alias "${alias}"`);
await retry(async () => {
if (requests.length === 0) throw new Error(`No request recorded for alias "${alias}"`);
});
return requests[requests.length - 1];
},
select: (selector: string, value: string) => {
this.helper.get.elementByTestId(selector).select(value);
},
waitForExampleFileResponse: () => this.when.waitForResponse("example-style.json"),
focus: (selector: string) => {
this.helper.when.focus(selector);
},
setValue: (selector: string, text: string) => {
this.helper.get
.elementByTestId(selector)
.clear()
.type(text, { parseSpecialCharSequences: false });
},
setValueToPropertyArray: (selector: string, value: string) => {
this.when.doWithin(selector, () => {
this.helper.get.element(".maputnik-array-block-content input").last().type("{selectall}"+value, {force: true });
});
},
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 });
});
},
closePopup: () => {
this.helper.get.element(".maplibregl-popup-close-button").click();
},
collapseGroupInLayerEditor: (index = 0) => {
this.helper.get.element(".maputnik-layer-editor-group__button").eq(index).realClick();
},
appendTextInJsonEditor: (text: string) => {
this.helper.get.element(".cm-line").first().click().type(text, { parseSpecialCharSequences: false });
},
setTextInJsonEditor: (text: string) => {
this.helper.get.element(".cm-line").first().click().clear().type(text, { parseSpecialCharSequences: false });
}
clearLocalStorage: () => this.page.evaluate(() => window.localStorage.clear()),
};
// ---- get -----------------------------------------------------------------
public get = {
...this.helper.get,
isMac: () => {
return Cypress.platform === "darwin";
isMac: () => isMac,
element: (selector: string) => this.root().locator(selector),
elementByTestId: (testId: string) => this.testId(testId),
elementByText: (text: string) => this.root().getByText(text),
elementByAttribute: (attribute: string, value: string) =>
this.root().locator(`[${attribute}="${value}"]`),
canvas: () => this.page.locator("canvas"),
searchControl: () => this.page.locator(".maplibregl-ctrl-geocoder"),
skipTargetLayerList: () => this.testId("skip-target-layer-list"),
skipTargetLayerEditor: () => this.testId("skip-target-layer-editor"),
inputValue: (testId: string) => new Query<string>(() => this.testId(testId).first().inputValue()),
elementsText: (testId: string) => new Query<string>(() => this.testId(testId).first().innerText()),
locationHash: () => new Query<string>(async () => new URL(this.page.url()).hash),
styleFromLocalStorage: () => new Query<any>(() => styleFromLocalStorage(this.page)),
fixture: (name: string) => Promise.resolve(readFixture(name)),
responseBody: (alias: string) => {
// Our mocked style responses always return the matching fixture.
const name = alias.endsWith(".json") ? alias : `${alias}.json`;
return Promise.resolve(readFixture(name));
},
styleFromLocalStorage: () =>
this.helper.get.window().then((win) => styleFromWindow(win)),
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")
exampleFileUrl: () => baseUrl + "example-style.json",
};
// ---- file open helpers ---------------------------------------------------
private async openFileByFixture(fixture: string, buttonTestId: string, inputTestId: string): Promise<void> {
const content = JSON.stringify(readFixture(fixture));
const hasPicker = await this.page.evaluate(() => "showOpenFilePicker" in window);
if (hasPicker) {
await this.page.evaluate((fileContent) => {
(window as any).showOpenFilePicker = async () => [
{ getFile: async () => ({ text: async () => fileContent }) },
];
}, content);
await this.testId(buttonTestId).click();
} else {
await this.testId(inputTestId).setInputFiles({
name: fixture,
mimeType: "application/json",
buffer: Buffer.from(content),
});
}
}
private async dropFileByFixture(fixture: string, dropzoneTestId: string): Promise<void> {
const content = JSON.stringify(readFixture(fixture));
const dataTransfer = await this.page.evaluateHandle((fileContent) => {
const dt = new DataTransfer();
dt.items.add(new File([fileContent], "example-style.json", { type: "application/json" }));
return dt;
}, content);
const dropzone = this.testId(dropzoneTestId);
await dropzone.dispatchEvent("dragenter", { dataTransfer });
await dropzone.dispatchEvent("dragover", { dataTransfer });
await dropzone.dispatchEvent("drop", { dataTransfer });
}
}
+30 -25
View File
@@ -1,40 +1,45 @@
import { v1 as uuid } from "uuid";
import MaputnikCypressHelper from "./maputnik-cypress-helper";
import { expect } from "@playwright/test";
import type { MaputnikDriver } from "./maputnik-driver";
export default class ModalDriver {
private helper = new MaputnikCypressHelper();
export class ModalDriver {
constructor(private readonly driver: MaputnikDriver) {}
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 } = this.driver;
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) {
await when.doWithin("add-layer.layer-source-block", async () => {
const input = get.element("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 expect(input).toHaveValue(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 () => {
// No-op when the add-layer modal is already open (some specs call open()
// both in a beforeEach and at the start of the test body).
const modal = this.driver.get.elementByTestId("modal:add-layer").first();
if (await modal.isVisible()) return;
await this.driver.when.click("layer-list:add-layer");
},
close: (key: string) => {
this.helper.when.click(key + ".close-modal");
close: async (key: string) => {
await this.driver.when.click(key + ".close-modal");
},
};
}
+252 -252
View File
@@ -1,102 +1,111 @@
import { MaputnikDriver } from "./maputnik-driver";
import tokens from "../../src/config/tokens.json" with {type: "json"};
import { test, expect, setupMaputnik } from "./fixtures";
import tokens from "../src/config/tokens.json" with { type: "json" };
test.describe("modals", () => {
const { beforeAndAfter, when, get, given, then } = new MaputnikDriver();
beforeAndAfter();
setupMaputnik();
beforeEach(() => {
when.setStyle("");
test.beforeEach(async ({ driver }) => {
await driver.when.setStyle("");
});
test.describe("open", () => {
beforeEach(() => {
when.click("nav:open");
test.beforeEach(async ({ driver }) => {
await driver.when.click("nav:open");
});
test("close", () => {
when.modal.close("modal:open");
then(get.elementByTestId("modal:open")).shouldNotExist();
test("close", async ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
await when.chooseExampleFile();
await then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
});
test("upload via drag and drop", () => {
when.dropExampleFile();
then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
test("upload via drag and drop", async ({ driver }) => {
const { get, when, then } = driver;
await when.dropExampleFile();
await then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
});
test.describe("when click open url", () => {
beforeEach(() => {
test.beforeEach(async ({ driver }) => {
const { get, when } = driver;
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 ({ driver }) => {
await driver.then(driver.get.responseBody("example-style.json")).shouldEqualToStoredStyle();
});
});
});
test.describe("shortcuts", () => {
test("open/close", () => {
when.setStyle("");
when.typeKeys("?");
when.modal.close("modal:shortcuts");
then(get.elementByTestId("modal:shortcuts")).shouldNotExist();
test("open/close", async ({ driver }) => {
const { get, when, then } = driver;
await when.setStyle("");
await when.typeKeys("?");
await when.modal.close("modal:shortcuts");
await then(get.elementByTestId("modal:shortcuts")).shouldNotExist();
});
});
test.describe("export", () => {
beforeEach(() => {
when.click("nav:export");
test.beforeEach(async ({ driver }) => {
await driver.when.click("nav:export");
});
test("close", () => {
when.modal.close("modal:export");
then(get.elementByTestId("modal:export")).shouldNotExist();
test("close", async ({ driver }) => {
const { get, when, then } = driver;
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", () => {});
});
test.describe("sources", () => {
beforeEach(() => {
when.setStyle("layer");
when.click("nav:sources");
test.beforeEach(async ({ driver }) => {
await driver.when.setStyle("layer");
await driver.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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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",
@@ -106,338 +115,329 @@ test.describe("modals", () => {
});
});
test("add new raster source", () => {
test("add new raster source", async ({ driver }) => {
const { get, when, then } = driver;
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,
});
});
});
test.describe("inspect", () => {
test("toggle", () => {
test("toggle", async ({ driver }) => {
// There is no assertion in this test
when.setStyle("geojson");
when.select("maputnik-select", "inspect");
await driver.when.setStyle("geojson");
await driver.when.select("maputnik-select", "inspect");
});
});
test.describe("style settings", () => {
beforeEach(() => {
when.click("nav:settings");
test.beforeEach(async ({ driver }) => {
await driver.when.click("nav:settings");
});
test.describe("when click name filed spec information", () => {
beforeEach(() => {
when.click("field-doc-button-Name");
test.beforeEach(async ({ driver }) => {
await driver.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 ({ driver }) => {
await driver.then(driver.get.elementsText("spec-field-doc")).shouldInclude("name for the style");
});
});
test.describe("when set name and click owner", () => {
beforeEach(() => {
when.setValue("modal:settings.name", "foobar");
when.click("modal:settings.owner");
when.wait(200);
test.beforeEach(async ({ driver }) => {
const { when } = driver;
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 ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
name: "foobar",
});
});
});
test.describe("when set owner and click name", () => {
beforeEach(() => {
when.setValue("modal:settings.owner", "foobar");
when.click("modal:settings.name");
when.wait(200);
test.beforeEach(async ({ driver }) => {
const { when } = driver;
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 ({ driver }) => {
await driver.then(driver.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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, given } = driver;
await when.click("modal:settings.close-modal");
await when.click("nav:open");
test("include API key when change renderer", () => {
await get.elementByAttribute("aria-label", "MapTiler Basic").click();
await when.wait(1000);
await when.click("nav:settings");
when.click("modal:settings.close-modal");
when.click("nav:open");
await when.select("modal:settings.maputnik:renderer", "mlgljs");
await driver.then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual("mlgljs");
get.elementByAttribute("aria-label", "MapTiler Basic").should("exist").click();
when.wait(1000);
when.click("nav:settings");
await when.select("modal:settings.maputnik:renderer", "ol");
await driver.then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual("ol");
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 driver.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}`);
});
});
test.describe("add layer", () => {
beforeEach(() => {
when.setStyle("layer");
when.modal.open();
test.beforeEach(async ({ driver }) => {
await driver.when.setStyle("layer");
await driver.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 ({ driver }) => {
const { get, when, then } = driver;
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");
});
});
test.describe("sources", () => {
test("toggle");
test.describe("sources placeholder", () => {
test.skip("toggle", () => {});
});
test.describe("global state", () => {
beforeEach(() => {
when.click("nav:global-state");
test.beforeEach(async ({ driver }) => {
await driver.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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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 ({ driver }) => {
const { get, when, then } = driver;
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" } },
});
});
});
test.describe("error panel", () => {
test("not visible when no errors", () => {
then(get.element("maputnik-message-panel-error")).shouldNotExist();
test("not visible when no errors", async ({ driver }) => {
await driver.then(driver.get.element("maputnik-message-panel-error")).shouldNotExist();
});
test("visible on style error", () => {
when.modal.open();
when.modal.fillLayers({
test("visible on style error", async ({ driver }) => {
const { get, when, then } = driver;
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();
});
});
test.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 ({ driver, page }) => {
const { get, when, then } = driver;
// Clear localStorage to start fresh
cy.clearLocalStorage();
await when.clearLocalStorage();
// fill localStorage until we get a QuotaExceededError
cy.window().then(win => {
await page.evaluate(() => {
let chunkSize = 1000;
const chunk = new Array(chunkSize).join("x");
let index = 0;
// Keep adding until we hit the quota
while (true) {
for (;;) {
try {
const key = `maputnik:fill-${index++}`;
win.localStorage.setItem(key, chunk);
window.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;
}
chunkSize /= 2;
continue;
}
throw e; // Unexpected error
}
@@ -445,12 +445,12 @@ test.describe("modals", () => {
});
// 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.responseBody("example-style.json")).shouldEqualToStoredStyle();
await then(get.styleFromLocalStorage()).shouldExist();
});
});
});