test: instantiate MaputnikDriver once per describe block

Make the driver page-lazy (it resolves the running test's page on demand via
an auto fixture) so it can be created a single time at describe scope and
reused across the block's tests, matching the pre-migration ergonomics:

  const { given, get, when, then } = new MaputnikDriver();

instead of pulling `driver` out of a fixture and destructuring it in every
test. Coverage collection and dialog handling move into the auto fixture.

Also inject a bare invalid token ("zzz") in the json-editor parse-error test:
CodeMirror auto-closes brackets/quotes, so " {" no longer reliably breaks the
JSON.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
HarelM
2026-07-08 17:48:44 +03:00
parent 7c5e5358cb
commit ac9186e7f8
11 changed files with 302 additions and 343 deletions
+13 -10
View File
@@ -1,15 +1,20 @@
import { test, setupMaputnik } from "./fixtures";
import { test } from "./fixtures";
import { MaputnikDriver } from "./maputnik-driver";
test.describe("accessibility", () => {
setupMaputnik();
const { given, get, when, then } = new MaputnikDriver();
test.beforeEach(async () => {
await given.setupMockBackedResponses();
await when.setStyle("both");
});
test.describe("skip links", () => {
test.beforeEach(async ({ driver }) => {
await driver.when.setStyle("layer");
test.beforeEach(async () => {
await when.setStyle("layer");
});
test("skip link to layer list", async ({ driver }) => {
const { get, when, then } = driver;
test("skip link to layer list", async () => {
const selector = "root:skip:layer-list";
await then(get.elementByTestId(selector)).shouldExist();
await when.tab();
@@ -18,8 +23,7 @@ test.describe("accessibility", () => {
await then(get.skipTargetLayerList()).shouldBeFocused();
});
test("skip link to layer editor", async ({ driver }) => {
const { get, when, then } = driver;
test("skip link to layer editor", async () => {
const selector = "root:skip:layer-editor";
await then(get.elementByTestId(selector)).shouldExist();
await then(get.elementByTestId("skip-target-layer-editor")).shouldExist();
@@ -30,8 +34,7 @@ test.describe("accessibility", () => {
await then(get.skipTargetLayerEditor()).shouldBeFocused();
});
test("skip link to map view", async ({ driver }) => {
const { get, when, then } = driver;
test("skip link to map view", async () => {
const selector = "root:skip:map-view";
await then(get.elementByTestId(selector)).shouldExist();
await when.tab();
+10 -6
View File
@@ -1,16 +1,20 @@
import { test, setupMaputnik } from "./fixtures";
import { test } from "./fixtures";
import { MaputnikDriver } from "./maputnik-driver";
test.describe("code editor", () => {
setupMaputnik();
const { given, get, when, then } = new MaputnikDriver();
test("open code editor", async ({ driver }) => {
const { when, get, then } = driver;
test.beforeEach(async () => {
await given.setupMockBackedResponses();
await when.setStyle("both");
});
test("open code editor", async () => {
await when.click("nav:code-editor");
await then(get.element(".maputnik-code-editor")).shouldExist();
});
test("closes code editor", async ({ driver }) => {
const { when, get, then } = driver;
test("closes code editor", async () => {
await when.click("nav:code-editor");
await then(get.element(".maputnik-code-editor")).shouldExist();
await when.click("nav:code-editor");
+40 -21
View File
@@ -1,27 +1,46 @@
import { test as base, expect } from "@playwright/test";
import { MaputnikDriver } from "./maputnik-driver";
import { test as base, expect, type Page } from "@playwright/test";
import { readCoverage, writeCoverage } from "./coverage";
let activePage: Page | undefined;
const coverageChunks: unknown[] = [];
/** The page for the currently running test. Throws if used outside a test. */
export function currentPage(): Page {
if (!activePage) {
throw new Error("No active page: a MaputnikDriver method was called outside of a running test.");
}
return activePage;
}
/** Records a coverage snapshot (called before navigations, which reset __coverage__). */
export function recordCoverageChunk(chunk: unknown): void {
if (chunk) coverageChunks.push(chunk);
}
/**
* Playwright test with a per-test `driver` (the maputnik page object) that also
* collects istanbul coverage once the test finishes.
* Auto fixture that binds the current test's page for the (page-lazy)
* MaputnikDriver, auto-accepts confirm dialogs, and writes the istanbul
* coverage collected during the test to `.nyc_output`.
*/
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 const test = base.extend<{ maputnikPage: void }>({
maputnikPage: [
async ({ page }, use, testInfo) => {
activePage = page;
coverageChunks.length = 0;
// Accept confirm dialogs (e.g. the "replace current style" prompt). These
// are dismissed by default, which would cancel loading a style via URL.
page.on("dialog", (dialog) => dialog.accept().catch(() => undefined));
await use();
const finalCoverage = await readCoverage(page);
if (finalCoverage) coverageChunks.push(finalCoverage);
coverageChunks.forEach((chunk, index) => writeCoverage(chunk, `${testInfo.testId}-${index}`));
coverageChunks.length = 0;
activePage = undefined;
},
{ auto: true },
],
});
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");
});
}
+10 -6
View File
@@ -1,13 +1,18 @@
import { test, setupMaputnik } from "./fixtures";
import { test } from "./fixtures";
import { MaputnikDriver } from "./maputnik-driver";
test.describe("history", () => {
setupMaputnik();
const { given, get, when, then } = new MaputnikDriver();
const undoKeyCombo = process.platform === "darwin" ? "{meta}z" : "{ctrl}z";
const redoKeyCombo = process.platform === "darwin" ? "{meta}{shift}z" : "{ctrl}y";
test("undo/redo", async ({ driver }) => {
const { get, when, then } = driver;
test.beforeEach(async () => {
await given.setupMockBackedResponses();
await when.setStyle("both");
});
test("undo/redo", async () => {
await when.setStyle("geojson");
await when.modal.open();
@@ -53,8 +58,7 @@ test.describe("history", () => {
});
});
test("should not redo after undo and value change", async ({ driver }) => {
const { get, when, then } = driver;
test("should not redo after undo and value change", async () => {
await when.setStyle("geojson");
await when.modal.open();
await when.modal.fillLayers({
+13 -10
View File
@@ -1,29 +1,32 @@
import { test, setupMaputnik } from "./fixtures";
import { test } from "./fixtures";
import { MaputnikDriver } from "./maputnik-driver";
test.describe("i18n", () => {
setupMaputnik();
const { given, get, when, then } = new MaputnikDriver();
test.beforeEach(async () => {
await given.setupMockBackedResponses();
await when.setStyle("both");
});
test.describe("language detector", () => {
test("English", async ({ driver }) => {
const { get, when, then } = driver;
test("English", async () => {
await when.visit("?lng=en");
await then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("en");
});
test("Japanese", async ({ driver }) => {
const { get, when, then } = driver;
test("Japanese", async () => {
await when.visit("?lng=ja");
await then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("ja");
});
});
test.describe("language switcher", () => {
test.beforeEach(async ({ driver }) => {
await driver.when.setStyle("layer");
test.beforeEach(async () => {
await when.setStyle("layer");
});
test("the language switcher switches to Japanese", async ({ driver }) => {
const { get, when, then } = driver;
test("the language switcher switches to Japanese", async () => {
const selector = "maputnik-lang-select";
await then(get.elementByTestId(selector)).shouldExist();
await when.select(selector, "ja");
+20 -23
View File
@@ -1,16 +1,21 @@
import { test, setupMaputnik } from "./fixtures";
import { test } from "./fixtures";
import { MaputnikDriver } from "./maputnik-driver";
test.describe("keyboard", () => {
setupMaputnik();
const { given, get, when, then } = new MaputnikDriver();
test.beforeEach(async () => {
await given.setupMockBackedResponses();
await when.setStyle("both");
});
test.describe("shortcuts", () => {
test.beforeEach(async ({ driver }) => {
await driver.given.setupMockBackedResponses();
await driver.when.setStyle("");
test.beforeEach(async () => {
await given.setupMockBackedResponses();
await when.setStyle("");
});
test("ESC should unfocus", async ({ driver }) => {
const { get, when, then } = driver;
test("ESC should unfocus", async () => {
const targetSelector = "maputnik-select";
await when.focus(targetSelector);
await then(get.elementByTestId(targetSelector)).shouldBeFocused();
@@ -18,50 +23,42 @@ test.describe("keyboard", () => {
await then(get.elementByTestId(targetSelector)).shouldNotBeFocused();
});
test("'?' should show shortcuts modal", async ({ driver }) => {
const { get, when, then } = driver;
test("'?' should show shortcuts modal", async () => {
await when.typeKeys("?");
await then(get.elementByTestId("modal:shortcuts")).shouldBeVisible();
});
test("'o' should show open modal", async ({ driver }) => {
const { get, when, then } = driver;
test("'o' should show open modal", async () => {
await when.typeKeys("o");
await then(get.elementByTestId("modal:open")).shouldBeVisible();
});
test("'e' should show export modal", async ({ driver }) => {
const { get, when, then } = driver;
test("'e' should show export modal", async () => {
await when.typeKeys("e");
await then(get.elementByTestId("modal:export")).shouldBeVisible();
});
test("'d' should show sources modal", async ({ driver }) => {
const { get, when, then } = driver;
test("'d' should show sources modal", async () => {
await when.typeKeys("d");
await then(get.elementByTestId("modal:sources")).shouldBeVisible();
});
test("'s' should show settings modal", async ({ driver }) => {
const { get, when, then } = driver;
test("'s' should show settings modal", async () => {
await when.typeKeys("s");
await then(get.elementByTestId("modal:settings")).shouldBeVisible();
});
test("'i' should change map to inspect mode", async ({ driver }) => {
const { get, when, then } = driver;
test("'i' should change map to inspect mode", async () => {
await when.typeKeys("i");
await then(get.inputValue("maputnik-select")).shouldEqual("inspect");
});
test("'m' should focus map", async ({ driver }) => {
const { get, when, then } = driver;
test("'m' should focus map", async () => {
await when.typeKeys("m");
await then(get.canvas()).shouldBeFocused();
});
test("'!' should show debug modal", async ({ driver }) => {
const { get, when, then } = driver;
test("'!' should show debug modal", async () => {
await when.typeKeys("!");
await then(get.elementByTestId("modal:debug")).shouldBeVisible();
});
+45 -60
View File
@@ -1,16 +1,17 @@
import { v1 as uuid } from "uuid";
import { test, setupMaputnik } from "./fixtures";
import type { MaputnikDriver } from "./maputnik-driver";
import { test } from "./fixtures";
import { MaputnikDriver } from "./maputnik-driver";
test.describe("layer editor", () => {
setupMaputnik();
test.beforeEach(async ({ driver }) => {
await driver.when.setStyle("both");
await driver.when.modal.open();
const { given, get, when, then } = new MaputnikDriver();
test.beforeEach(async () => {
await given.setupMockBackedResponses();
await when.setStyle("both");
await when.modal.open();
});
async function createBackground(driver: MaputnikDriver) {
const { get, when, then } = driver;
async function createBackground() {
const id = uuid();
await when.selectWithin("add-layer.layer-type", "background");
@@ -26,9 +27,8 @@ test.describe("layer editor", () => {
test.skip("expand/collapse", () => {});
test("id", async ({ driver }) => {
const { get, when, then } = driver;
const bgId = await createBackground(driver);
test("id", async () => {
const bgId = await createBackground();
await when.click("layer-list-item:background:" + bgId);
@@ -42,8 +42,7 @@ test.describe("layer editor", () => {
});
test.describe("source", () => {
test("should show error when the source is invalid", async ({ driver }) => {
const { get, when, then } = driver;
test("should show error when the source is invalid", async () => {
await when.modal.fillLayers({
type: "circle",
layer: "invalid",
@@ -57,22 +56,20 @@ test.describe("layer editor", () => {
test.describe("min-zoom", () => {
let bgId: string;
test.beforeEach(async ({ driver }) => {
const { when } = driver;
bgId = await createBackground(driver);
test.beforeEach(async () => {
bgId = await createBackground();
await when.click("layer-list-item:background:" + bgId);
await when.setValue("min-zoom.input-text", "1");
await when.click("layer-editor.layer-id");
});
test("should update min-zoom in local storage", async ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
test("should update min-zoom in local storage", async () => {
await 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;
test("when clicking next layer should update style on local storage", async () => {
await when.type("min-zoom.input-text", "{backspace}");
await when.click("max-zoom.input-text");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
@@ -84,16 +81,15 @@ test.describe("layer editor", () => {
test.describe("max-zoom", () => {
let bgId: string;
test.beforeEach(async ({ driver }) => {
const { when } = driver;
bgId = await createBackground(driver);
test.beforeEach(async () => {
bgId = await createBackground();
await when.click("layer-list-item:background:" + bgId);
await when.setValue("max-zoom.input-text", "1");
await when.click("layer-editor.layer-id");
});
test("should update style in local storage", async ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
test("should update style in local storage", async () => {
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id: "background:" + bgId, type: "background", maxzoom: 1 }],
});
});
@@ -103,16 +99,15 @@ test.describe("layer editor", () => {
let bgId: string;
const comment = "42";
test.beforeEach(async ({ driver }) => {
const { when } = driver;
bgId = await createBackground(driver);
test.beforeEach(async () => {
bgId = await createBackground();
await when.click("layer-list-item:background:" + bgId);
await when.setValue("layer-comment.input", comment);
await when.click("layer-editor.layer-id");
});
test("should update style in local storage", async ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
test("should update style in local storage", async () => {
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + bgId,
@@ -124,14 +119,13 @@ test.describe("layer editor", () => {
});
test.describe("when unsetting", () => {
test.beforeEach(async ({ driver }) => {
const { when } = driver;
test.beforeEach(async () => {
await when.clear("layer-comment.input");
await when.click("min-zoom.input-text");
});
test("should update style in local storage", async ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
test("should update style in local storage", async () => {
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id: "background:" + bgId, type: "background" }],
});
});
@@ -140,15 +134,14 @@ test.describe("layer editor", () => {
test.describe("color", () => {
let bgId: string;
test.beforeEach(async ({ driver }) => {
const { when } = driver;
bgId = await createBackground(driver);
test.beforeEach(async () => {
bgId = await createBackground();
await when.click("layer-list-item:background:" + bgId);
await when.click("spec-field:background-color");
});
test("should update style in local storage", async ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
test("should update style in local storage", async () => {
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id: "background:" + bgId, type: "background" }],
});
});
@@ -156,21 +149,17 @@ test.describe("layer editor", () => {
test.describe("opacity", () => {
let bgId: string;
test.beforeEach(async ({ driver }) => {
const { when } = driver;
bgId = await createBackground(driver);
test.beforeEach(async () => {
bgId = await createBackground();
await when.click("layer-list-item:background:" + bgId);
await when.type("spec-field-input:background-opacity", "0.");
});
test("should keep '.' in the input field", async ({ driver }) => {
await driver
.then(driver.get.elementByTestId("spec-field-input:background-opacity"))
.shouldHaveValue("0.");
test("should keep '.' in the input field", async () => {
await then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0.");
});
test("should revert to a valid value when focus out", async ({ driver }) => {
const { get, when, then } = driver;
test("should revert to a valid value when focus out", async () => {
await when.click("layer-list-item:background:" + bgId);
await then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0");
});
@@ -182,8 +171,7 @@ test.describe("layer editor", () => {
});
test.describe("layout", () => {
test("text-font", async ({ driver }) => {
const { get, when, then } = driver;
test("text-font", async () => {
await when.setStyle("font");
await when.collapseGroupInLayerEditor();
await when.collapseGroupInLayerEditor(1);
@@ -204,8 +192,7 @@ test.describe("layer editor", () => {
});
test.describe("json-editor", () => {
test("add", async ({ driver }) => {
const { get, when, then } = driver;
test("add", async () => {
const id = await when.modal.fillLayers({
type: "circle",
layer: "example",
@@ -225,25 +212,23 @@ test.describe("layer editor", () => {
test.skip("expand/collapse", () => {});
test.skip("modify", () => {});
test("parse error", async ({ driver }) => {
const { get, when, then } = driver;
const bgId = await createBackground(driver);
test("parse error", async () => {
const bgId = await 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();
await when.appendTextInJsonEditor(
" {"
);
// Inject an invalid token (CodeMirror auto-closes brackets/quotes, so a
// bare word reliably breaks the JSON) and expect a lint error.
await when.appendTextInJsonEditor("zzz");
await then(get.element(".cm-lint-marker-error")).shouldExist();
});
});
test.describe("sticky header", () => {
test("should keep layer header visible when scrolling properties", async ({ driver }) => {
const { get, when, then } = driver;
test("should keep layer header visible when scrolling properties", async () => {
// Setup: Create a layer with many properties (e.g. symbol layer)
await when.modal.fillLayers({
type: "symbol",
+51 -68
View File
@@ -1,41 +1,44 @@
import { test, setupMaputnik } from "./fixtures";
import { test } from "./fixtures";
import { MaputnikDriver } from "./maputnik-driver";
test.describe("layers list", () => {
setupMaputnik();
test.beforeEach(async ({ driver }) => {
await driver.when.setStyle("both");
await driver.when.modal.open();
const { given, get, when, then } = new MaputnikDriver();
test.beforeEach(async () => {
await given.setupMockBackedResponses();
await when.setStyle("both");
await when.modal.open();
});
test.describe("ops", () => {
let id: string;
test.beforeEach(async ({ driver }) => {
id = await driver.when.modal.fillLayers({ type: "background" });
test.beforeEach(async () => {
id = await when.modal.fillLayers({ type: "background" });
});
test("should update layers in local storage", async ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
test("should update layers in local storage", async () => {
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "background" }],
});
});
test.describe("when clicking delete", () => {
test.beforeEach(async ({ driver }) => {
await driver.when.click("layer-list-item:" + id + ":delete");
test.beforeEach(async () => {
await when.click("layer-list-item:" + id + ":delete");
});
test("should empty layers in local storage", async ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
test("should empty layers in local storage", async () => {
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [],
});
});
});
test.describe("when clicking duplicate", () => {
test.beforeEach(async ({ driver }) => {
await driver.when.click("layer-list-item:" + id + ":copy");
test.beforeEach(async () => {
await when.click("layer-list-item:" + id + ":copy");
});
test("should add copy layer in local storage", async ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
test("should add copy layer in local storage", async () => {
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{ id: id + "-copy", type: "background" },
{ id, type: "background" },
@@ -45,23 +48,23 @@ test.describe("layers list", () => {
});
test.describe("when clicking hide", () => {
test.beforeEach(async ({ driver }) => {
await driver.when.click("layer-list-item:" + id + ":toggle-visibility");
test.beforeEach(async () => {
await when.click("layer-list-item:" + id + ":toggle-visibility");
});
test("should update visibility to none in local storage", async ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
test("should update visibility to none in local storage", async () => {
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "background", layout: { visibility: "none" } }],
});
});
test.describe("when clicking show", () => {
test.beforeEach(async ({ driver }) => {
await driver.when.click("layer-list-item:" + id + ":toggle-visibility");
test.beforeEach(async () => {
await when.click("layer-list-item:" + id + ":toggle-visibility");
});
test("should update visibility to visible in local storage", async ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
test("should update visibility to visible in local storage", async () => {
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "background", layout: { visibility: "visible" } }],
});
});
@@ -69,15 +72,14 @@ test.describe("layers list", () => {
test.describe("when selecting a layer", () => {
let secondId: string;
test.beforeEach(async ({ driver }) => {
await driver.when.modal.open();
secondId = await driver.when.modal.fillLayers({
test.beforeEach(async () => {
await when.modal.open();
secondId = await when.modal.fillLayers({
id: "second-layer",
type: "background",
});
});
test("should show the selected layer in the editor", async ({ driver }) => {
const { get, when, then } = driver;
test("should show the selected layer in the editor", async () => {
await when.realClick("layer-list-item:" + secondId);
await then(get.elementByTestId("layer-editor.layer-id.input")).shouldHaveValue(secondId);
await when.realClick("layer-list-item:" + id);
@@ -88,8 +90,7 @@ test.describe("layers list", () => {
});
test.describe("background", () => {
test("add", async ({ driver }) => {
const { get, when, then } = driver;
test("add", async () => {
const id = await when.modal.fillLayers({ type: "background" });
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "background" }],
@@ -100,8 +101,7 @@ test.describe("layers list", () => {
});
test.describe("fill", () => {
test("add", async ({ driver }) => {
const { get, when, then } = driver;
test("add", async () => {
const id = await when.modal.fillLayers({ type: "fill", layer: "example" });
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "fill", source: "example" }],
@@ -113,16 +113,14 @@ test.describe("layers list", () => {
});
test.describe("line", () => {
test("add", async ({ driver }) => {
const { get, when, then } = driver;
test("add", async () => {
const id = await when.modal.fillLayers({ type: "line", layer: "example" });
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "line", source: "example" }],
});
});
test("groups", async ({ driver }) => {
const { get, when, then } = driver;
test("groups", async () => {
await when.modal.open();
const id1 = await when.modal.fillLayers({ id: "aa", type: "line", layer: "example" });
@@ -154,16 +152,14 @@ test.describe("layers list", () => {
});
test.describe("symbol", () => {
test("add", async ({ driver }) => {
const { get, when, then } = driver;
test("add", async () => {
const id = await when.modal.fillLayers({ type: "symbol", layer: "example" });
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "symbol", source: "example" }],
});
});
test("should show spec info when hovering and clicking single line property", async ({ driver }) => {
const { get, when, then } = driver;
test("should show spec info when hovering and clicking single line property", async () => {
await when.modal.fillLayers({ type: "symbol", layer: "example" });
await when.hover("spec-field-container:text-rotate");
@@ -172,8 +168,7 @@ test.describe("layers list", () => {
await then(get.elementByTestId("spec-field-doc")).shouldContainText("Rotates the ");
});
test("should show spec info when hovering and clicking multi line property", async ({ driver }) => {
const { get, when, then } = driver;
test("should show spec info when hovering and clicking multi line property", async () => {
await when.modal.fillLayers({ type: "symbol", layer: "example" });
await when.hover("spec-field-container:text-offset");
@@ -182,8 +177,7 @@ test.describe("layers list", () => {
await then(get.elementByTestId("spec-field-doc")).shouldContainText("Offset distance");
});
test("should hide spec info when clicking a second time", async ({ driver }) => {
const { get, when, then } = driver;
test("should hide spec info when clicking a second time", async () => {
await when.modal.fillLayers({ type: "symbol", layer: "example" });
await when.hover("spec-field-container:text-rotate");
@@ -196,8 +190,7 @@ test.describe("layers list", () => {
});
test.describe("raster", () => {
test("add", async ({ driver }) => {
const { get, when, then } = driver;
test("add", async () => {
const id = await when.modal.fillLayers({ type: "raster", layer: "raster" });
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "raster", source: "raster" }],
@@ -206,8 +199,7 @@ test.describe("layers list", () => {
});
test.describe("circle", () => {
test("add", async ({ driver }) => {
const { get, when, then } = driver;
test("add", async () => {
const id = await when.modal.fillLayers({ type: "circle", layer: "example" });
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "circle", source: "example" }],
@@ -216,8 +208,7 @@ test.describe("layers list", () => {
});
test.describe("fill extrusion", () => {
test("add", async ({ driver }) => {
const { get, when, then } = driver;
test("add", async () => {
const id = await when.modal.fillLayers({ type: "fill-extrusion", layer: "example" });
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "fill-extrusion", source: "example" }],
@@ -226,16 +217,14 @@ test.describe("layers list", () => {
});
test.describe("hillshade", () => {
test("add", async ({ driver }) => {
const { get, when, then } = driver;
test("add", async () => {
const id = await when.modal.fillLayers({ type: "hillshade", layer: "example" });
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "hillshade", source: "example" }],
});
});
test("set hillshade illumination direction array", async ({ driver }) => {
const { get, when, then } = driver;
test("set hillshade illumination direction array", async () => {
const id = await when.modal.fillLayers({ type: "hillshade", layer: "example" });
await when.collapseGroupInLayerEditor();
await when.collapseGroupInLayerEditor(1);
@@ -256,8 +245,7 @@ test.describe("layers list", () => {
});
});
test("set hillshade highlight color array", async ({ driver }) => {
const { get, when, then } = driver;
test("set hillshade highlight color array", async () => {
const id = await when.modal.fillLayers({ type: "hillshade", layer: "example" });
await when.collapseGroupInLayerEditor();
await when.setValueToPropertyArray("spec-field:hillshade-highlight-color", "blue");
@@ -280,16 +268,14 @@ test.describe("layers list", () => {
});
test.describe("color-relief", () => {
test("add", async ({ driver }) => {
const { get, when, then } = driver;
test("add", async () => {
const id = await when.modal.fillLayers({ type: "color-relief", layer: "example" });
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "color-relief", source: "example" }],
});
});
test("adds elevation expression when clicking the elevation button", async ({ driver }) => {
const { get, when, then } = driver;
test("adds elevation expression when clicking the elevation button", async () => {
await when.modal.fillLayers({ type: "color-relief", layer: "example" });
await when.collapseGroupInLayerEditor();
await when.click("make-elevation-function");
@@ -300,8 +286,7 @@ test.describe("layers list", () => {
});
test.describe("groups", () => {
test("simple", async ({ driver }) => {
const { get, when, then } = driver;
test("simple", async () => {
await when.setStyle("geojson");
await when.modal.open();
@@ -324,8 +309,7 @@ test.describe("layers list", () => {
});
test.describe("drag and drop", () => {
test("move layer should update local storage", async ({ driver }) => {
const { get, when, then } = driver;
test("move layer should update local storage", async () => {
await when.modal.open();
const firstId = await when.modal.fillLayers({ id: "a", type: "background" });
await when.modal.open();
@@ -346,8 +330,7 @@ test.describe("layers list", () => {
});
test.describe("sticky header", () => {
test("should keep header visible when scrolling layer list", async ({ driver }) => {
const { get, when, then } = driver;
test("should keep header visible when scrolling layer list", async () => {
// Setup: Create multiple layers to enable scrolling
for (let i = 0; i < 20; i++) {
await when.modal.open();
+17 -17
View File
@@ -1,19 +1,23 @@
import { test, setupMaputnik } from "./fixtures";
import { test } from "./fixtures";
import { MaputnikDriver } from "./maputnik-driver";
test.describe("map", () => {
setupMaputnik();
const { given, get, when, then } = new MaputnikDriver();
test.beforeEach(async () => {
await given.setupMockBackedResponses();
await when.setStyle("both");
});
test.describe("zoom level", () => {
test("via url", async ({ driver }) => {
const { get, when, then } = driver;
test("via url", async () => {
const zoomLevel = 12.37;
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", async ({ driver }) => {
const { get, when, then } = driver;
test("via map controls", async () => {
const zoomLevel = 12.37;
await when.setStyle("geojson", zoomLevel);
await then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
@@ -21,8 +25,7 @@ test.describe("map", () => {
await then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText("Zoom: " + (zoomLevel + 1));
});
test("via style file definition", async ({ driver }) => {
const { get, when, then } = driver;
test("via style file definition", async () => {
await when.setStyle("zoom_7_center_0_51");
await then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
await then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText("Zoom: " + 7);
@@ -36,26 +39,23 @@ test.describe("map", () => {
});
test.describe("search", () => {
test("should exist", async ({ driver }) => {
const { get, then } = driver;
test("should exist", async () => {
await then(get.searchControl()).shouldBeVisible();
});
});
test.describe("popup", () => {
test.beforeEach(async ({ driver }) => {
await driver.when.setStyle("rectangles");
await driver.then(driver.get.locationHash()).shouldExist();
test.beforeEach(async () => {
await when.setStyle("rectangles");
await then(get.locationHash()).shouldExist();
});
test("should open on feature click", async ({ driver }) => {
const { get, when, then } = driver;
test("should open on feature click", async () => {
await when.clickCenter("maplibre:map");
await then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
});
test("should open a second feature after closing popup", async ({ driver }) => {
const { get, when, then } = driver;
test("should open a second feature after closing popup", async () => {
await when.clickCenter("maplibre:map");
await then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
await when.closePopup();
+11 -23
View File
@@ -1,8 +1,9 @@
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 { expect, type Locator, type Page, type Request } from "@playwright/test";
import { readCoverage } from "./coverage";
import { currentPage, recordCoverageChunk } from "./fixtures";
import { ModalDriver } from "./modal-driver";
const baseUrl = "http://localhost:8888/";
@@ -205,28 +206,14 @@ async function centerOf(locator: Locator): Promise<{ x: number; y: number }> {
export class MaputnikDriver {
private scope: Locator | null = null;
private readonly recordedRequests = new Map<string, Request[]>();
private readonly coverageChunks: unknown[] = [];
private readonly modalDriver = new ModalDriver(this);
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}`);
});
/**
* The page for the currently running test. Resolved lazily so a single driver
* instance can be created once per `describe` and reused across its tests.
*/
private get page(): Page {
return currentPage();
}
// ---- Element access ------------------------------------------------------
@@ -315,7 +302,8 @@ export class MaputnikDriver {
modal: this.modalDriver.when,
visit: async (url: string) => {
await this.captureCoverage();
// Snapshot coverage before navigating, since a full page load resets it.
recordCoverageChunk(await readCoverage(this.page));
const target = url.startsWith("http") ? url : new URL(url, baseUrl).toString();
await this.page.goto(target);
},
+72 -99
View File
@@ -1,54 +1,54 @@
import { test, expect, setupMaputnik } from "./fixtures";
import { test, expect } from "./fixtures";
import { MaputnikDriver } from "./maputnik-driver";
import tokens from "../src/config/tokens.json" with { type: "json" };
test.describe("modals", () => {
setupMaputnik();
const { given, get, when, then } = new MaputnikDriver();
test.beforeEach(async ({ driver }) => {
await driver.when.setStyle("");
test.beforeEach(async () => {
await given.setupMockBackedResponses();
// Load a style first so it is persisted to localStorage, then reset the URL
// to the root (no style param) — several tests read the stored style.
await when.setStyle("both");
await when.setStyle("");
});
test.describe("open", () => {
test.beforeEach(async ({ driver }) => {
await driver.when.click("nav:open");
test.beforeEach(async () => {
await when.click("nav:open");
});
test("close", async ({ driver }) => {
const { get, when, then } = driver;
test("close", async () => {
await when.modal.close("modal:open");
await then(get.elementByTestId("modal:open")).shouldNotExist();
});
test("upload", async ({ driver }) => {
const { get, when, then } = driver;
test("upload", async () => {
await when.chooseExampleFile();
await then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
});
test("upload via drag and drop", async ({ driver }) => {
const { get, when, then } = driver;
test("upload via drag and drop", async () => {
await when.dropExampleFile();
await then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
});
test.describe("when click open url", () => {
test.beforeEach(async ({ driver }) => {
const { get, when } = driver;
test.beforeEach(async () => {
const styleFileUrl = get.exampleFileUrl();
await when.setValue("modal:open.url.input", styleFileUrl);
await when.click("modal:open.url.button");
await when.wait(200);
});
test("load from url", async ({ driver }) => {
await driver.then(driver.get.responseBody("example-style.json")).shouldEqualToStoredStyle();
test("load from url", async () => {
await then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
});
});
});
test.describe("shortcuts", () => {
test("open/close", async ({ driver }) => {
const { get, when, then } = driver;
test("open/close", async () => {
await when.setStyle("");
await when.typeKeys("?");
await when.modal.close("modal:shortcuts");
@@ -57,12 +57,11 @@ test.describe("modals", () => {
});
test.describe("export", () => {
test.beforeEach(async ({ driver }) => {
await driver.when.click("nav:export");
test.beforeEach(async () => {
await when.click("nav:export");
});
test("close", async ({ driver }) => {
const { get, when, then } = driver;
test("close", async () => {
await when.modal.close("modal:export");
await then(get.elementByTestId("modal:export")).shouldNotExist();
});
@@ -72,16 +71,15 @@ test.describe("modals", () => {
});
test.describe("sources", () => {
test.beforeEach(async ({ driver }) => {
await driver.when.setStyle("layer");
await driver.when.click("nav:sources");
test.beforeEach(async () => {
await when.setStyle("layer");
await when.click("nav:sources");
});
test.skip("active sources", () => {});
test.skip("public source", () => {});
test("add new source", async ({ driver }) => {
const { get, when, then } = driver;
test("add new source", async () => {
const sourceId = "n1z2v3r";
await when.setValue("modal:sources.add.source_id", sourceId);
await when.select("modal:sources.add.source_type", "tile_vector");
@@ -93,8 +91,7 @@ test.describe("modals", () => {
});
});
test("add new pmtiles source", async ({ driver }) => {
const { get, when, then } = driver;
test("add new pmtiles source", async () => {
const sourceId = "pmtilestest";
await when.setValue("modal:sources.add.source_id", sourceId);
await when.select("modal:sources.add.source_type", "pmtiles_vector");
@@ -115,8 +112,7 @@ test.describe("modals", () => {
});
});
test("add new raster source", async ({ driver }) => {
const { get, when, then } = driver;
test("add new raster source", async () => {
const sourceId = "rastertest";
await when.setValue("modal:sources.add.source_id", sourceId);
await when.select("modal:sources.add.source_type", "tile_raster");
@@ -131,59 +127,56 @@ test.describe("modals", () => {
});
test.describe("inspect", () => {
test("toggle", async ({ driver }) => {
test("toggle", async () => {
// There is no assertion in this test
await driver.when.setStyle("geojson");
await driver.when.select("maputnik-select", "inspect");
await when.setStyle("geojson");
await when.select("maputnik-select", "inspect");
});
});
test.describe("style settings", () => {
test.beforeEach(async ({ driver }) => {
await driver.when.click("nav:settings");
test.beforeEach(async () => {
await when.click("nav:settings");
});
test.describe("when click name filed spec information", () => {
test.beforeEach(async ({ driver }) => {
await driver.when.click("field-doc-button-Name");
test.beforeEach(async () => {
await when.click("field-doc-button-Name");
});
test("should show the spec information", async ({ driver }) => {
await driver.then(driver.get.elementsText("spec-field-doc")).shouldInclude("name for the style");
test("should show the spec information", async () => {
await then(get.elementsText("spec-field-doc")).shouldInclude("name for the style");
});
});
test.describe("when set name and click owner", () => {
test.beforeEach(async ({ driver }) => {
const { when } = driver;
test.beforeEach(async () => {
await when.setValue("modal:settings.name", "foobar");
await when.click("modal:settings.owner");
await when.wait(200);
});
test("show name specifications", async ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
test("show name specifications", async () => {
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
name: "foobar",
});
});
});
test.describe("when set owner and click name", () => {
test.beforeEach(async ({ driver }) => {
const { when } = driver;
test.beforeEach(async () => {
await when.setValue("modal:settings.owner", "foobar");
await when.click("modal:settings.name");
await when.wait(200);
});
test("should update owner in local storage", async ({ driver }) => {
await driver.then(driver.get.styleFromLocalStorage()).shouldDeepNestedInclude({
test("should update owner in local storage", async () => {
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
owner: "foobar",
});
});
});
test("sprite url", async ({ driver }) => {
const { get, when, then } = driver;
test("sprite url", async () => {
await when.setTextInJsonEditor('"http://example.com"');
await when.click("modal:settings.name");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
@@ -191,8 +184,7 @@ test.describe("modals", () => {
});
});
test("sprite object", async ({ driver }) => {
const { get, when, then } = driver;
test("sprite object", async () => {
await when.setTextInJsonEditor(JSON.stringify([{ id: "1", url: "2" }]));
await when.click("modal:settings.name");
@@ -201,8 +193,7 @@ test.describe("modals", () => {
});
});
test("glyphs url", async ({ driver }) => {
const { get, when, then } = driver;
test("glyphs url", async () => {
const glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
await when.setValue("modal:settings.glyphs", glyphsUrl);
await when.click("modal:settings.name");
@@ -211,8 +202,7 @@ test.describe("modals", () => {
});
});
test("maptiler access token", async ({ driver }) => {
const { get, when, then } = driver;
test("maptiler access token", async () => {
const apiKey = "testing123";
await when.setValue("modal:settings.maputnik:openmaptiles_access_token", apiKey);
await when.click("modal:settings.name");
@@ -221,8 +211,7 @@ test.describe("modals", () => {
});
});
test("thunderforest access token", async ({ driver }) => {
const { get, when, then } = driver;
test("thunderforest access token", async () => {
const apiKey = "testing123";
await when.setValue("modal:settings.maputnik:thunderforest_access_token", apiKey);
await when.click("modal:settings.name");
@@ -231,8 +220,7 @@ test.describe("modals", () => {
});
});
test("stadia access token", async ({ driver }) => {
const { get, when, then } = driver;
test("stadia access token", async () => {
const apiKey = "testing123";
await when.setValue("modal:settings.maputnik:stadia_access_token", apiKey);
await when.click("modal:settings.name");
@@ -241,8 +229,7 @@ test.describe("modals", () => {
});
});
test("locationiq access token", async ({ driver }) => {
const { get, when, then } = driver;
test("locationiq access token", async () => {
const apiKey = "testing123";
await when.setValue("modal:settings.maputnik:locationiq_access_token", apiKey);
await when.click("modal:settings.name");
@@ -251,32 +238,28 @@ test.describe("modals", () => {
});
});
test("style projection mercator", async ({ driver }) => {
const { get, when, then } = driver;
test("style projection mercator", async () => {
await when.select("modal:settings.projection", "mercator");
await then(get.styleFromLocalStorage().then((style) => style.projection)).shouldInclude({
type: "mercator",
});
});
test("style projection globe", async ({ driver }) => {
const { get, when, then } = driver;
test("style projection globe", async () => {
await when.select("modal:settings.projection", "globe");
await then(get.styleFromLocalStorage().then((style) => style.projection)).shouldInclude({
type: "globe",
});
});
test("style projection vertical-perspective", async ({ driver }) => {
const { get, when, then } = driver;
test("style projection vertical-perspective", async () => {
await when.select("modal:settings.projection", "vertical-perspective");
await then(get.styleFromLocalStorage().then((style) => style.projection)).shouldInclude({
type: "vertical-perspective",
});
});
test("style renderer", async ({ driver }) => {
const { get, when, then } = driver;
test("style renderer", async () => {
await when.select("modal:settings.maputnik:renderer", "ol");
await then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual("ol");
@@ -286,9 +269,7 @@ test.describe("modals", () => {
});
});
test("include API key when change renderer", async ({ driver }) => {
const { get, when, given } = driver;
test("include API key when change renderer", async () => {
await when.click("modal:settings.close-modal");
await when.click("nav:open");
@@ -297,10 +278,10 @@ test.describe("modals", () => {
await when.click("nav:settings");
await when.select("modal:settings.maputnik:renderer", "mlgljs");
await driver.then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual("mlgljs");
await then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual("mlgljs");
await when.select("modal:settings.maputnik:renderer", "ol");
await driver.then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual("ol");
await then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual("ol");
await given.intercept(
"https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=*",
@@ -309,7 +290,7 @@ test.describe("modals", () => {
);
await when.select("modal:settings.maputnik:renderer", "mlgljs");
await driver.then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual("mlgljs");
await then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual("mlgljs");
const request = await when.waitForResponse("tileRequest");
expect(request.url()).toContain(
@@ -319,13 +300,12 @@ test.describe("modals", () => {
});
test.describe("add layer", () => {
test.beforeEach(async ({ driver }) => {
await driver.when.setStyle("layer");
await driver.when.modal.open();
test.beforeEach(async () => {
await when.setStyle("layer");
await when.modal.open();
});
test("shows duplicate id error", async ({ driver }) => {
const { get, when, then } = driver;
test("shows duplicate id error", async () => {
await when.setValue("add-layer.layer-id.input", "background");
await when.click("add-layer");
await then(get.elementByTestId("modal:add-layer")).shouldExist();
@@ -338,12 +318,11 @@ test.describe("modals", () => {
});
test.describe("global state", () => {
test.beforeEach(async ({ driver }) => {
await driver.when.click("nav:global-state");
test.beforeEach(async () => {
await when.click("nav:global-state");
});
test("add variable", async ({ driver }) => {
const { get, when, then } = driver;
test("add variable", async () => {
await when.wait(100);
await when.click("global-state-add-variable");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
@@ -351,8 +330,7 @@ test.describe("modals", () => {
});
});
test("add multiple variables", async ({ driver }) => {
const { get, when, then } = driver;
test("add multiple variables", async () => {
await when.click("global-state-add-variable");
await when.click("global-state-add-variable");
await when.click("global-state-add-variable");
@@ -362,8 +340,7 @@ test.describe("modals", () => {
});
});
test("remove variable", async ({ driver }) => {
const { get, when, then } = driver;
test("remove variable", async () => {
await when.click("global-state-add-variable");
await when.click("global-state-add-variable");
await when.click("global-state-add-variable");
@@ -374,8 +351,7 @@ test.describe("modals", () => {
});
});
test("edit variable key", async ({ driver }) => {
const { get, when, then } = driver;
test("edit variable key", async () => {
await when.click("global-state-add-variable");
await when.wait(100);
await when.setValue("global-state-variable-key:0", "mykey");
@@ -386,8 +362,7 @@ test.describe("modals", () => {
});
});
test("edit variable value", async ({ driver }) => {
const { get, when, then } = driver;
test("edit variable value", async () => {
await when.click("global-state-add-variable");
await when.wait(100);
await when.setValue("global-state-variable-value:0", "myvalue");
@@ -400,12 +375,11 @@ test.describe("modals", () => {
});
test.describe("error panel", () => {
test("not visible when no errors", async ({ driver }) => {
await driver.then(driver.get.element("maputnik-message-panel-error")).shouldNotExist();
test("not visible when no errors", async () => {
await then(get.element("maputnik-message-panel-error")).shouldNotExist();
});
test("visible on style error", async ({ driver }) => {
const { get, when, then } = driver;
test("visible on style error", async () => {
await when.modal.open();
await when.modal.fillLayers({
type: "circle",
@@ -416,8 +390,7 @@ test.describe("modals", () => {
});
test.describe("Handle localStorage QuotaExceededError", () => {
test("handles quota exceeded error when opening style from URL", async ({ driver, page }) => {
const { get, when, then } = driver;
test("handles quota exceeded error when opening style from URL", async ({ page }) => {
// Clear localStorage to start fresh
await when.clearLocalStorage();