From c22f7404e7680100d3552d35a2f39cc764567e6e Mon Sep 17 00:00:00 2001 From: HarelM Date: Sun, 12 Jul 2026 09:34:46 +0300 Subject: [PATCH] Add more end-to-end tests --- e2e/layer-editor.spec.ts | 80 +++++++++++++++++++++++++++++++--- e2e/layers-list.spec.ts | 20 +++++++-- e2e/maputnik-driver.ts | 39 +++++++++++++++++ e2e/modals.spec.ts | 16 ++++--- e2e/playwright-helper.ts | 1 + src/components/LayerEditor.tsx | 1 + 6 files changed, 142 insertions(+), 15 deletions(-) diff --git a/e2e/layer-editor.spec.ts b/e2e/layer-editor.spec.ts index fc46d1f2..64908116 100644 --- a/e2e/layer-editor.spec.ts +++ b/e2e/layer-editor.spec.ts @@ -25,7 +25,19 @@ describe("layer editor", () => { return id; } - test.skip("expand/collapse", () => {}); + test("expand/collapse", async () => { + const bgId = await createBackground(); + await when.click("layer-list-item:background:" + bgId); + + // Groups start expanded, so the "Layer" group's fields are on screen. + await then(get.elementByTestId("layer-editor.layer-id.input")).shouldBeVisible(); + + await when.toggleGroupInLayerEditor("Layer"); + await then(get.elementByTestId("layer-editor.layer-id.input")).shouldNotBeVisible(); + + await when.toggleGroupInLayerEditor("Layer"); + await then(get.elementByTestId("layer-editor.layer-id.input")).shouldBeVisible(); + }); test("id", async () => { const bgId = await createBackground(); @@ -295,10 +307,43 @@ describe("layer editor", () => { }); describe("paint", () => { - test.skip("expand/collapse", () => {}); - test.skip("color", () => {}); - test.skip("pattern", () => {}); - test.skip("opacity", () => {}); + let id: string; + + beforeEach(async () => { + id = await when.modal.fillLayers({ type: "fill", layer: "example" }); + }); + + test("expand/collapse", async () => { + await then(get.elementByTestId("spec-field:fill-color")).shouldBeVisible(); + + await when.toggleGroupInLayerEditor("Paint properties"); + await then(get.elementByTestId("spec-field:fill-color")).shouldNotBeVisible(); + + await when.toggleGroupInLayerEditor("Paint properties"); + await then(get.elementByTestId("spec-field:fill-color")).shouldBeVisible(); + }); + + test("color", async () => { + await when.setColorValue("fill-color", "#ff0000"); + await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ + layers: [{ id, type: "fill", source: "example", paint: { "fill-color": "#ff0000" } }], + }); + }); + + test("pattern", async () => { + await when.setStringValue("fill-pattern", "some-pattern"); + await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ + layers: [{ id, type: "fill", source: "example", paint: { "fill-pattern": "some-pattern" } }], + }); + }); + + test("opacity", async () => { + await when.setValue("spec-field-input:fill-opacity", "0.4"); + await when.click("layer-editor.layer-id"); + await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ + layers: [{ id, type: "fill", source: "example", paint: { "fill-opacity": 0.4 } }], + }); + }); }); describe("json-editor", () => { @@ -318,8 +363,29 @@ describe("layer editor", () => { await then(get.element(".cm-lint-marker-error")).shouldExist(); }); - test.skip("expand/collapse", () => {}); - test.skip("modify", () => {}); + test("expand/collapse", async () => { + const bgId = await createBackground(); + await when.click("layer-list-item:background:" + bgId); + + await then(get.element(".cm-content")).shouldBeVisible(); + + await when.toggleGroupInLayerEditor("JSON Editor"); + await then(get.element(".cm-content")).shouldNotBeVisible(); + + await when.toggleGroupInLayerEditor("JSON Editor"); + await then(get.element(".cm-content")).shouldBeVisible(); + }); + + test("modify", async () => { + const bgId = await createBackground(); + await when.click("layer-list-item:background:" + bgId); + + // Append a property to the layer's JSON; the editor writes it back to the style. + await when.appendToJsonEditorLine('"background"', ',\n"minzoom": 5'); + await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ + layers: [{ id: "background:" + bgId, type: "background", minzoom: 5 }], + }); + }); test("parse error", async () => { const bgId = await createBackground(); diff --git a/e2e/layers-list.spec.ts b/e2e/layers-list.spec.ts index 786a1b3d..a635878c 100644 --- a/e2e/layers-list.spec.ts +++ b/e2e/layers-list.spec.ts @@ -97,7 +97,15 @@ describe("layers list", () => { }); }); - test.skip("modify", () => {}); + test("modify", async () => { + const id = await when.modal.fillLayers({ type: "background" }); + await when.click("layer-list-item:" + id); + await when.setValue("spec-field-input:background-opacity", "0.4"); + await when.click("layer-editor.layer-id"); + await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ + layers: [{ id, type: "background", paint: { "background-opacity": 0.4 } }], + }); + }); }); describe("fill", () => { @@ -108,8 +116,14 @@ describe("layers list", () => { }); }); - // TODO: Change source - test.skip("change source", () => {}); + test("change source", async () => { + const id = await when.modal.fillLayers({ type: "fill", layer: "example" }); + // The "both" style ships with an "example" and a "raster" source. + await when.changeLayerSource("raster"); + await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ + layers: [{ id, type: "fill", source: "raster" }], + }); + }); }); describe("line", () => { diff --git a/e2e/maputnik-driver.ts b/e2e/maputnik-driver.ts index 319a82f2..3e1ebe6a 100644 --- a/e2e/maputnik-driver.ts +++ b/e2e/maputnik-driver.ts @@ -128,6 +128,27 @@ export class MaputnikDriver { await this.helper.get.element(".maputnik-layer-editor-group__button").nth(index).click(); }, + /** Expands/collapses a layer-editor group by its title, e.g. "Paint properties". */ + toggleGroupInLayerEditor: async (title: string) => { + await this.helper.when.click("layer-editor-group:" + title); + }, + + /** Adds one of the predefined public sources listed in the sources modal. */ + addPublicSource: async (index = 0) => { + await this.helper.get.element(".maputnik-public-source-select").nth(index).click(); + }, + + /** + * Picks a source for the selected layer from the source autocomplete. + * The autocomplete is a controlled (downshift) input, so the value has to be + * filled rather than typed key by key, then chosen from the filtered menu. + */ + changeLayerSource: async (sourceId: string) => { + const input = this.helper.get.elementByTestId("layer-editor.layer-source").locator("input"); + await input.fill(sourceId); + await this.helper.get.element(".maputnik-autocomplete-menu-item").first().click(); + }, + appendTextInJsonEditor: async (text: string) => { await this.helper.get.element(".cm-line").first().click(); // Move to the very start of the document so the inserted text breaks the @@ -199,6 +220,24 @@ export class MaputnikDriver { await input.fill(value); }, + /** Sets a plain string spec field (e.g. a pattern), which has no dedicated input test id. */ + setStringValue: async (fieldName: string, value: string) => { + const input = this.helper.get.elementByTestId("spec-field:" + fieldName).locator("input.maputnik-string"); + await input.fill(value); + await input.blur(); + }, + + /** + * Appends text to the end of the JSON editor line holding `lineText`. + * CodeMirror types over its own auto-inserted closing quotes/brackets, so a + * well-formed fragment stays well-formed. + */ + appendToJsonEditorLine: async (lineText: string, text: string) => { + await this.helper.when.clickByText(lineText); + await this.helper.when.typeKeys("{end}"); + await this.helper.when.typeText(text); + }, + exportCreateHtml: async () => { await this.helper.get.element(".maputnik-modal-export-buttons button").last().click(); }, diff --git a/e2e/modals.spec.ts b/e2e/modals.spec.ts index e3a331d9..0bdbc77a 100644 --- a/e2e/modals.spec.ts +++ b/e2e/modals.spec.ts @@ -91,7 +91,17 @@ describe("modals", () => { ).shouldEqual(before - 1); }); - test.skip("public source", () => {}); + test("public source", async () => { + await when.addPublicSource(); + await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ + sources: { + openmaptiles: { + type: "vector", + url: `https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=${tokens.openmaptiles}`, + }, + }, + }); + }); test("add new source", async () => { const sourceId = "n1z2v3r"; @@ -327,10 +337,6 @@ describe("modals", () => { }); }); - describe("sources placeholder", () => { - test.skip("toggle", () => {}); - }); - describe("global state", () => { beforeEach(async () => { await when.click("nav:global-state"); diff --git a/e2e/playwright-helper.ts b/e2e/playwright-helper.ts index 0a815734..ad790e7e 100644 --- a/e2e/playwright-helper.ts +++ b/e2e/playwright-helper.ts @@ -145,6 +145,7 @@ async function typeSequence(page: Page, text: string): Promise { del: "Delete", tab: "Tab", home: "Home", + end: "End", rightarrow: "ArrowRight", }; diff --git a/src/components/LayerEditor.tsx b/src/components/LayerEditor.tsx index 86205d8f..947990b0 100644 --- a/src/components/LayerEditor.tsx +++ b/src/components/LayerEditor.tsx @@ -230,6 +230,7 @@ class LayerEditorInternal extends React.Component {this.props.layer.type !== "background" &&