From 124ae98bf34422374bae9fdff9ab1407ed85f680 Mon Sep 17 00:00:00 2001 From: Harel M Date: Wed, 27 Dec 2023 20:58:24 +0200 Subject: [PATCH] E2E: Improve tests, lint, and add more drivers (#855) This PR introduces lint to cypress code, adds drivers to try and abstract the usage of cypress as much as possible. Nothing very interesting, mainly to try out the driver pattern for the e2e tests. --- .eslintrc | 3 +- cypress/e2e/accessibility.cy.ts | 4 +- cypress/e2e/cypress-wrapper-driver.ts | 41 ++++++ cypress/e2e/history.cy.ts | 12 +- cypress/e2e/keyboard.cy.ts | 4 +- cypress/e2e/layers.cy.ts | 60 +++++---- cypress/e2e/map.cy.ts | 8 +- cypress/e2e/{driver.ts => maputnik-driver.ts} | 118 ++++++------------ cypress/e2e/modal-driver.ts | 42 +++++++ cypress/e2e/modals.cy.ts | 18 +-- package.json | 2 +- tsconfig.json | 2 +- 12 files changed, 180 insertions(+), 134 deletions(-) create mode 100644 cypress/e2e/cypress-wrapper-driver.ts rename cypress/e2e/{driver.ts => maputnik-driver.ts} (53%) create mode 100644 cypress/e2e/modal-driver.ts diff --git a/.eslintrc b/.eslintrc index 996d01b4..d85160a0 100644 --- a/.eslintrc +++ b/.eslintrc @@ -37,7 +37,8 @@ "react/prop-types": ["off"], // Disable no-undef. It's covered by @typescript-eslint "no-undef": "off", - "indent": ["error", 2] + "indent": ["error", 2], + "no-var": ["error"] }, "globals": { "global": "readonly" diff --git a/cypress/e2e/accessibility.cy.ts b/cypress/e2e/accessibility.cy.ts index 30231ac9..b435ae14 100644 --- a/cypress/e2e/accessibility.cy.ts +++ b/cypress/e2e/accessibility.cy.ts @@ -1,7 +1,7 @@ -import MaputnikDriver from "./driver"; +import MaputnikDriver from "./maputnik-driver"; describe("accessibility", () => { - let { beforeAndAfter, given, when, get, should } = new MaputnikDriver(); + let { beforeAndAfter, when, should } = new MaputnikDriver(); beforeAndAfter(); describe("skip links", () => { diff --git a/cypress/e2e/cypress-wrapper-driver.ts b/cypress/e2e/cypress-wrapper-driver.ts new file mode 100644 index 00000000..c092ee28 --- /dev/null +++ b/cypress/e2e/cypress-wrapper-driver.ts @@ -0,0 +1,41 @@ +import { CypressHelper } from "@shellygo/cypress-test-utils"; + +export default class CypressWrapperDriver { + private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" }); + + public given = { + ...this.helper.given, + /** + * + * @param url a url to a file, this assumes the file name is the last part of the url + * @param alias + */ + interceptGetToFile(url: string) { + let fileNameAndAlias = url.split('/').pop(); + cy.intercept('GET', url, { fixture: fileNameAndAlias }).as(fileNameAndAlias!); + }, + + interceptAndIgnore(url: string) { + cy.intercept({ method: "GET", url }, []); + } + } + + public get = { + ...this.helper.get, + elementByClassOrType(slector: string) { + return cy.get(slector); + } + } + + public when = { + ...this.helper.when, + visit(address: string) { + cy.visit(address); + }, + confirmAlert() { + cy.on("window:confirm", () => true); + } + } + + public beforeAndAfter = this.helper.beforeAndAfter; +} \ No newline at end of file diff --git a/cypress/e2e/history.cy.ts b/cypress/e2e/history.cy.ts index 59b6001e..0d91174d 100644 --- a/cypress/e2e/history.cy.ts +++ b/cypress/e2e/history.cy.ts @@ -1,7 +1,7 @@ -import MaputnikDriver from "./driver"; +import MaputnikDriver from "./maputnik-driver"; describe("history", () => { - let { beforeAndAfter, given, when, get, should } = new MaputnikDriver(); + let { beforeAndAfter, when, get, should } = new MaputnikDriver(); beforeAndAfter(); let undoKeyCombo: string; @@ -15,11 +15,11 @@ describe("history", () => { it("undo/redo", () => { when.setStyle("geojson"); - when.openLayersModal(); + when.modal.open(); should.equalStyleStore((a: any) => a.layers, []); - when.fillLayersModal({ + when.modal.fillLayers({ id: "step 1", type: "background", }); @@ -34,8 +34,8 @@ describe("history", () => { ] ); - when.openLayersModal(); - when.fillLayersModal({ + when.modal.open(); + when.modal.fillLayers({ id: "step 2", type: "background", }); diff --git a/cypress/e2e/keyboard.cy.ts b/cypress/e2e/keyboard.cy.ts index 8cb5b1ce..46afbedd 100644 --- a/cypress/e2e/keyboard.cy.ts +++ b/cypress/e2e/keyboard.cy.ts @@ -1,7 +1,7 @@ -import MaputnikDriver from "./driver"; +import MaputnikDriver from "./maputnik-driver"; describe("keyboard", () => { - let { beforeAndAfter, given, when, get, should } = new MaputnikDriver(); + let { beforeAndAfter, given, when, should } = new MaputnikDriver(); beforeAndAfter(); describe("shortcuts", () => { beforeEach(() => { diff --git a/cypress/e2e/layers.cy.ts b/cypress/e2e/layers.cy.ts index ad685c42..4156eaba 100644 --- a/cypress/e2e/layers.cy.ts +++ b/cypress/e2e/layers.cy.ts @@ -1,17 +1,17 @@ import { v1 as uuid } from "uuid"; -import MaputnikDriver from "./driver"; +import MaputnikDriver from "./maputnik-driver"; describe("layers", () => { - let { beforeAndAfter, given, when, get, should } = new MaputnikDriver(); + let { beforeAndAfter, when, should } = new MaputnikDriver(); beforeAndAfter(); beforeEach(() => { when.setStyle("both"); - when.openLayersModal(); + when.modal.open(); }); describe("ops", () => { it("delete", () => { - var id = when.fillLayersModal({ + let id = when.modal.fillLayers({ type: "background", }); @@ -31,8 +31,7 @@ describe("layers", () => { }); it("duplicate", () => { - var styleObj; - var id = when.fillLayersModal({ + let id = when.modal.fillLayers({ type: "background", }); @@ -64,8 +63,7 @@ describe("layers", () => { }); it("hide", () => { - var styleObj; - var id = when.fillLayersModal({ + let id = when.modal.fillLayers({ type: "background", }); @@ -113,7 +111,7 @@ describe("layers", () => { describe("background", () => { it("add", () => { - var id = when.fillLayersModal({ + let id = when.modal.fillLayers({ type: "background", }); @@ -131,7 +129,7 @@ describe("layers", () => { describe("modify", () => { function createBackground() { // Setup - var id = uuid(); + let id = uuid(); when.selectWithin("add-layer.layer-type", "background"); when.setValue("add-layer.layer-id.input", "background:" + id); @@ -154,11 +152,11 @@ describe("layers", () => { describe("layer", () => { it("expand/collapse"); it("id", () => { - var bgId = createBackground(); + let bgId = createBackground(); when.click("layer-list-item:background:" + bgId); - var id = uuid(); + let id = uuid(); when.setValue("layer-editor.layer-id.input", "foobar:" + id); when.click("min-zoom"); @@ -174,7 +172,7 @@ describe("layers", () => { }); it("min-zoom", () => { - var bgId = createBackground(); + let bgId = createBackground(); when.click("layer-list-item:background:" + bgId); when.setValue("min-zoom.input-text", "1"); @@ -203,7 +201,7 @@ describe("layers", () => { }); it("max-zoom", () => { - var bgId = createBackground(); + let bgId = createBackground(); when.click("layer-list-item:background:" + bgId); when.setValue("max-zoom.input-text", "1"); @@ -223,8 +221,8 @@ describe("layers", () => { }); it("comments", () => { - var bgId = createBackground(); - var comment = "42"; + let bgId = createBackground(); + let comment = "42"; when.click("layer-list-item:background:" + bgId); when.setValue("layer-comment.input", comment); @@ -255,7 +253,7 @@ describe("layers", () => { }); it("color", () => { - var bgId = createBackground(); + let bgId = createBackground(); when.click("layer-list-item:background:" + bgId); @@ -292,11 +290,11 @@ describe("layers", () => { // TODO it.skip("parse error", () => { - var bgId = createBackground(); + let bgId = createBackground(); when.click("layer-list-item:background:" + bgId); - var errorSelector = ".CodeMirror-lint-marker-error"; + let errorSelector = ".CodeMirror-lint-marker-error"; should.notExist(errorSelector); when.click(".CodeMirror"); @@ -311,7 +309,7 @@ describe("layers", () => { describe("fill", () => { it("add", () => { - var id = when.fillLayersModal({ + let id = when.modal.fillLayers({ type: "fill", layer: "example", }); @@ -334,7 +332,7 @@ describe("layers", () => { describe("line", () => { it("add", () => { - var id = when.fillLayersModal({ + let id = when.modal.fillLayers({ type: "line", layer: "example", }); @@ -359,7 +357,7 @@ describe("layers", () => { describe("symbol", () => { it("add", () => { - var id = when.fillLayersModal({ + let id = when.modal.fillLayers({ type: "symbol", layer: "example", }); @@ -379,7 +377,7 @@ describe("layers", () => { describe("raster", () => { it("add", () => { - var id = when.fillLayersModal({ + let id = when.modal.fillLayers({ type: "raster", layer: "raster", }); @@ -399,7 +397,7 @@ describe("layers", () => { describe("circle", () => { it("add", () => { - var id = when.fillLayersModal({ + let id = when.modal.fillLayers({ type: "circle", layer: "example", }); @@ -419,7 +417,7 @@ describe("layers", () => { describe("fill extrusion", () => { it("add", () => { - var id = when.fillLayersModal({ + let id = when.modal.fillLayers({ type: "fill-extrusion", layer: "example", }); @@ -441,20 +439,20 @@ describe("layers", () => { it("simple", () => { when.setStyle("geojson"); - when.openLayersModal(); - when.fillLayersModal({ + when.modal.open(); + when.modal.fillLayers({ id: "foo", type: "background", }); - when.openLayersModal(); - when.fillLayersModal({ + when.modal.open(); + when.modal.fillLayers({ id: "foo_bar", type: "background", }); - when.openLayersModal(); - when.fillLayersModal({ + when.modal.open(); + when.modal.fillLayers({ id: "foo_bar_baz", type: "background", }); diff --git a/cypress/e2e/map.cy.ts b/cypress/e2e/map.cy.ts index afccd755..6ed22553 100644 --- a/cypress/e2e/map.cy.ts +++ b/cypress/e2e/map.cy.ts @@ -1,18 +1,18 @@ -import MaputnikDriver from "./driver"; +import MaputnikDriver from "./maputnik-driver"; describe("map", () => { - let { beforeAndAfter, given, when, get, should } = new MaputnikDriver(); + let { beforeAndAfter, when, should } = new MaputnikDriver(); beforeAndAfter(); describe("zoom level", () => { it("via url", () => { - var zoomLevel = 12.37; + let zoomLevel = 12.37; when.setStyle("geojson", zoomLevel); should.beVisible("maplibre:ctrl-zoom"); should.containText("maplibre:ctrl-zoom", "Zoom: " + zoomLevel); }); it("via map controls", () => { - var zoomLevel = 12.37; + let zoomLevel = 12.37; when.setStyle("geojson", zoomLevel); should.beVisible("maplibre:ctrl-zoom"); diff --git a/cypress/e2e/driver.ts b/cypress/e2e/maputnik-driver.ts similarity index 53% rename from cypress/e2e/driver.ts rename to cypress/e2e/maputnik-driver.ts index d1c142fb..965b1083 100644 --- a/cypress/e2e/driver.ts +++ b/cypress/e2e/maputnik-driver.ts @@ -1,7 +1,12 @@ -import { CypressHelper } from "@shellygo/cypress-test-utils"; -import { v1 as uuid } from "uuid"; +import CypressWrapperDriver from "./cypress-wrapper-driver"; +import ModalDriver from "./modal-driver"; + +const SERVER_ADDRESS = "http://localhost:8888/"; + export default class MaputnikDriver { - private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" }); + private helper = new CypressWrapperDriver(); + private modalDriver = new ModalDriver(); + public beforeAndAfter = () => { beforeEach(() => { this.given.setupInterception(); @@ -11,36 +16,28 @@ export default class MaputnikDriver { public given = { setupInterception: () => { - cy.intercept("GET", "http://localhost:8888/example-style.json", { - fixture: "example-style.json", - }).as("example-style.json"); - cy.intercept("GET", "http://localhost:8888/example-layer-style.json", { - fixture: "example-layer-style.json", - }); - cy.intercept("GET", "http://localhost:8888/geojson-style.json", { - fixture: "geojson-style.json", - }); - cy.intercept("GET", "http://localhost:8888/raster-style.json", { - fixture: "raster-style.json", - }); - cy.intercept("GET", "http://localhost:8888/geojson-raster-style.json", { - fixture: "geojson-raster-style.json", - }); - cy.intercept({ method: "GET", url: "*example.local/*" }, []); - cy.intercept({ method: "GET", url: "*example.com/*" }, []); + this.helper.given.interceptGetToFile(SERVER_ADDRESS + "example-style.json"); + this.helper.given.interceptGetToFile(SERVER_ADDRESS + "example-layer-style.json"); + this.helper.given.interceptGetToFile(SERVER_ADDRESS + "geojson-style.json"); + this.helper.given.interceptGetToFile(SERVER_ADDRESS + "raster-style.json"); + this.helper.given.interceptGetToFile(SERVER_ADDRESS + "geojson-raster-style.json"); + + this.helper.given.interceptAndIgnore("*example.local/*"); + this.helper.given.interceptAndIgnore("*example.com/*"); }, }; public when = { + modal: this.modalDriver.when, within: (selector: string, fn: () => void) => { this.helper.when.within(fn, selector); }, - tab: () => cy.get("body").tab(), + tab: () => this.helper.get.elementByClassOrType("body").tab(), waitForExampleFileRequset: () => { this.helper.when.waitForResponse("example-style.json"); }, chooseExampleFile: () => { - cy.get("input[type='file']").selectFile( + this.helper.get.elementByClassOrType("input[type='file']").selectFile( "cypress/fixtures/example-style.json", { force: true } ); @@ -51,55 +48,34 @@ export default class MaputnikDriver { ) => { let url = "?debug"; switch (styleProperties) { - case "geojson": - url += "&style=http://localhost:8888/geojson-style.json"; - break; - case "raster": - url += "&style=http://localhost:8888/raster-style.json"; - break; - case "both": - url += "&style=http://localhost:8888/geojson-raster-style.json"; - break; - case "layer": - url += "&style=http://localhost:8888/example-layer-style.json"; - break; + case "geojson": + url += `&style=${SERVER_ADDRESS}geojson-style.json`; + break; + case "raster": + url += `&style=${SERVER_ADDRESS}raster-style.json`; + break; + case "both": + url += `&style=${SERVER_ADDRESS}geojson-raster-style.json`; + break; + case "layer": + url += `&style=${SERVER_ADDRESS}/example-layer-style.json`; + break; } if (zoom) { - url += "#" + zoom + "/41.3805/2.1635"; + url += `#${zoom}/41.3805/2.1635`; } - cy.visit("http://localhost:8888/" + url); + this.helper.when.visit(SERVER_ADDRESS + url); if (styleProperties) { - cy.on("window:confirm", () => true); + this.helper.when.confirmAlert(); } this.helper.get.element("toolbar:link").should("be.visible"); }, - fillLayersModal: (opts: {type: string, layer?: string, id?: string}) => { - var type = opts.type; - var layer = opts.layer; - var id; - if (opts.id) { - id = opts.id; - } else { - id = `${type}:${uuid()}`; - } - - this.helper.get.element("add-layer.layer-type.select").select(type); - this.helper.get.element("add-layer.layer-id.input").type(id); - if (layer) { - this.when.within("add-layer.layer-source-block", () => { - cy.get("input").type(layer!); - }) - } - this.when.click("add-layer"); - - return id; - }, typeKeys: (keys: string, selector?: string) => { if (selector) { this.helper.get.element(selector).type(keys); } else { - cy.get("body").type(keys); + this.helper.get.elementByClassOrType("body").type(keys); } }, @@ -108,12 +84,12 @@ export default class MaputnikDriver { }, clickZoomin: () => { - cy.get(".maplibregl-ctrl-zoom-in").click(); + this.helper.get.elementByClassOrType(".maplibregl-ctrl-zoom-in").click(); }, selectWithin: (selector: string, value: string) => { this.when.within(selector, () => { - cy.get("select").select(value); + this.helper.get.elementByClassOrType("select").select(value); }); }, @@ -128,18 +104,6 @@ export default class MaputnikDriver { setValue: (selector: string, text: string) => { this.helper.get.element(selector).clear().type(text, { parseSpecialCharSequences: false }); }, - - closeModal: (key: string) => { - this.helper.when.waitUntil(() => this.helper.get.element(key)); - this.when.click(key + ".close-modal"); - }, - - openLayersModal: () => { - this.helper.when.click("layer-list:add-layer"); - - this.helper.get.element("modal:add-layer").should("exist"); - this.helper.get.element("modal:add-layer").should("be.visible"); - }, }; public get = { @@ -153,18 +117,18 @@ export default class MaputnikDriver { return obj; }, exampleFileUrl: () => { - return "http://localhost:8888/example-style.json"; + return SERVER_ADDRESS + "example-style.json"; }, }; public should = { canvasBeFocused: () => { this.when.within("maplibre:map", () => { - cy.get("canvas").should("be.focused"); + this.helper.get.elementByClassOrType("canvas").should("be.focused"); }); }, notExist: (selector: string) => { - cy.get(selector).should("not.exist"); + this.helper.get.elementByClassOrType(selector).should("not.exist"); }, beFocused: (selector: string) => { this.helper.get.element(selector).should("have.focus"); @@ -192,7 +156,7 @@ export default class MaputnikDriver { styleStoreEqualToExampleFileData: () => { cy.window().then((win: any) => { const obj = this.get.styleFromWindow(win); - cy.fixture("example-style.json").should("deep.equal", obj); + this.helper.given.fixture("example-style.json", "file:example-style.json").should("deep.equal", obj); }); }, diff --git a/cypress/e2e/modal-driver.ts b/cypress/e2e/modal-driver.ts new file mode 100644 index 00000000..aff69e7b --- /dev/null +++ b/cypress/e2e/modal-driver.ts @@ -0,0 +1,42 @@ +import { v1 as uuid } from "uuid"; +import CypressWrapperDriver from "./cypress-wrapper-driver"; + +export default class ModalDriver { + private helper = new CypressWrapperDriver(); + + public when = { + fillLayers: (opts: {type: string, layer?: string, id?: string}) => { + let type = opts.type; + let layer = opts.layer; + let id; + if (opts.id) { + id = opts.id; + } else { + id = `${type}:${uuid()}`; + } + + this.helper.get.element("add-layer.layer-type.select").select(type); + this.helper.get.element("add-layer.layer-id.input").type(id); + if (layer) { + this.helper.when.within(() => { + this.helper.get.elementByClassOrType("input").type(layer!); + }, "add-layer.layer-source-block") + } + this.helper.when.click("add-layer"); + + return id; + }, + + open: () => { + this.helper.when.click("layer-list:add-layer"); + + this.helper.get.element("modal:add-layer").should("exist"); + this.helper.get.element("modal:add-layer").should("be.visible"); + }, + + close: (key: string) => { + this.helper.when.waitUntil(() => this.helper.get.element(key)); + this.helper.when.click(key + ".close-modal"); + }, + } +} \ No newline at end of file diff --git a/cypress/e2e/modals.cy.ts b/cypress/e2e/modals.cy.ts index 117a159a..b0713fb0 100644 --- a/cypress/e2e/modals.cy.ts +++ b/cypress/e2e/modals.cy.ts @@ -1,7 +1,7 @@ -import MaputnikDriver from "./driver"; +import MaputnikDriver from "./maputnik-driver"; describe("modals", () => { - let { beforeAndAfter, given, when, get, should } = new MaputnikDriver(); + let { beforeAndAfter, when, get, should } = new MaputnikDriver(); beforeAndAfter(); beforeEach(() => { when.setStyle(""); @@ -12,7 +12,7 @@ describe("modals", () => { }); it("close", () => { - when.closeModal("modal:open"); + when.modal.close("modal:open"); should.notExist("modal:open"); }); @@ -24,7 +24,7 @@ describe("modals", () => { }); it("load from url", () => { - var styleFileUrl = get.exampleFileUrl(); + let styleFileUrl = get.exampleFileUrl(); when.setValue("modal:open.url.input", styleFileUrl); when.click("modal:open.url.button"); @@ -38,7 +38,7 @@ describe("modals", () => { it("open/close", () => { when.setStyle(""); when.typeKeys("?"); - when.closeModal("modal:shortcuts"); + when.modal.close("modal:shortcuts"); should.notExist("modal:shortcuts"); }); }); @@ -49,7 +49,7 @@ describe("modals", () => { }); it("close", () => { - when.closeModal("modal:export"); + when.modal.close("modal:export"); should.notExist("modal:export"); }); @@ -102,7 +102,7 @@ describe("modals", () => { should.equalStyleStore((obj) => obj.sprite, "http://example.com"); }); it("glyphs url", () => { - var glyphsUrl = "http://example.com/{fontstack}/{range}.pbf"; + let glyphsUrl = "http://example.com/{fontstack}/{range}.pbf"; when.setValue("modal:settings.glyphs", glyphsUrl); when.click("modal:settings.name"); @@ -110,7 +110,7 @@ describe("modals", () => { }); it("maptiler access token", () => { - var apiKey = "testing123"; + let apiKey = "testing123"; when.setValue( "modal:settings.maputnik:openmaptiles_access_token", apiKey @@ -124,7 +124,7 @@ describe("modals", () => { }); it("thunderforest access token", () => { - var apiKey = "testing123"; + let apiKey = "testing123"; when.setValue("modal:settings.maputnik:thunderforest_access_token", apiKey); when.click("modal:settings.name"); diff --git a/package.json b/package.json index 2678fa3d..223c6748 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "start": "vite", "build": "tsc && vite build", - "lint": "eslint ./src --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0", + "lint": "eslint ./src ./cypress --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0", "test": "cypress run", "cy:open": "cypress open", "lint-css": "stylelint \"src/styles/*.scss\"", diff --git a/tsconfig.json b/tsconfig.json index 4c098757..24f33888 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,7 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, - "include": ["src"], + "include": ["src", "cypress/e2e"], "references": [{ "path": "./tsconfig.node.json" }], // TODO: Remove when issue is resolved https://github.com/cypress-io/cypress/issues/27448 "ts-node": {