From e25685bd8bcc29481227b0ba2fa0de96149a054f Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:46:34 +0100 Subject: [PATCH 01/17] [core] refactor: replace `XMLHttpRequest` with `fetch` --- CHANGELOG.md | 1 + js/loader.js | 27 +++++++++++++++++++-- js/translator.js | 34 +++++++++++---------------- tests/e2e/helpers/global-setup.js | 26 +++++++++++++------- tests/e2e/translations_spec.js | 1 + tests/unit/classes/translator_spec.js | 1 + 6 files changed, 59 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c6170f768..d42db02392 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ planned for 2026-01-01 - [tests] replace `node-libgpiod` with `serialport` in electron-rebuild workflow (#3945) - [calendar] hide repeatingCountTitle if the event count is zero (#3949) - [core] configure cspell to check default modules only and fix typos (#3955) +- [core] refactor: replace `XMLHttpRequest` with `fetch` in `translator.js` (#3950) ### Fixed diff --git a/js/loader.js b/js/loader.js index 62bc0447e4..49b0b4fc9b 100644 --- a/js/loader.js +++ b/js/loader.js @@ -10,13 +10,36 @@ const Loader = (function () { /* Private Methods */ + /** + * Get environment variables from config. + * @returns {object} Env vars with modulesDir and customCss paths from config. + */ + const getEnvVarsFromConfig = function () { + return { + modulesDir: config.foreignModulesDir || "modules", + customCss: config.customCss || "css/custom.css" + }; + }; + /** * Retrieve object of env variables. * @returns {object} with key: values as assembled in js/server_functions.js */ const getEnvVars = async function () { - const res = await fetch(`${location.protocol}//${location.host}${config.basePath}env`); - return JSON.parse(await res.text()); + // In test mode, skip server fetch and use config values directly + if (typeof process !== "undefined" && process.env && process.env.mmTestMode === "true") { + return getEnvVarsFromConfig(); + } + + // In production, fetch env vars from server + try { + const res = await fetch(`${config.basePath}env`); + return JSON.parse(await res.text()); + } catch (error) { + // Fallback to config values if server fetch fails + Log.error("Unable to retrieve env configuration", error); + return getEnvVarsFromConfig(); + } }; /** diff --git a/js/translator.js b/js/translator.js index bc7bad5f3d..497c42687d 100644 --- a/js/translator.js +++ b/js/translator.js @@ -3,30 +3,24 @@ const Translator = (function () { /** - * Load a JSON file via XHR. + * Load a JSON file via fetch. * @param {string} file Path of the file we want to load. * @returns {Promise} the translations in the specified file */ async function loadJSON (file) { - const xhr = new XMLHttpRequest(); - return new Promise(function (resolve) { - xhr.overrideMimeType("application/json"); - xhr.open("GET", file, true); - xhr.onreadystatechange = function () { - if (xhr.readyState === 4 && xhr.status === 200) { - // needs error handler try/catch at least - let fileInfo = null; - try { - fileInfo = JSON.parse(xhr.responseText); - } catch (exception) { - // nothing here, but don't die - Log.error(`[translator] loading json file =${file} failed`); - } - resolve(fileInfo); - } - }; - xhr.send(null); - }); + const baseHref = document.baseURI; + const url = new URL(file, baseHref); + + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Unexpected response status: ${response.status}`); + } + return await response.json(); + } catch (exception) { + Log.error(`Loading json file =${file} failed`); + return null; + } } return { diff --git a/tests/e2e/helpers/global-setup.js b/tests/e2e/helpers/global-setup.js index 185a0e8255..df4f27f8e3 100644 --- a/tests/e2e/helpers/global-setup.js +++ b/tests/e2e/helpers/global-setup.js @@ -64,32 +64,41 @@ exports.startApplication = async (configFilename, exec) => { }; exports.stopApplication = async (waitTime = 100) => { - if (global.window) { - // no closing causes test errors and memory leaks - global.window.close(); - delete global.window; - } - if (!global.app) { + if (global.window) { + global.window.close(); + delete global.window; + } delete global.testPort; return Promise.resolve(); } + // Stop server first await global.app.stop(); delete global.app; delete global.testPort; - // Small delay to ensure clean shutdown + // Wait for any pending async operations to complete before closing DOM await new Promise((resolve) => setTimeout(resolve, waitTime)); + + if (global.window) { + // Close window after async operations have settled + global.window.close(); + delete global.window; + delete global.document; + } }; exports.getDocument = () => { return new Promise((resolve) => { const port = global.testPort || config.port || 8080; - const url = `http://${config.address || "localhost"}:${port}`; + // JSDOM requires localhost instead of 0.0.0.0 for URL resolution + const address = config.address === "0.0.0.0" ? "localhost" : config.address || "localhost"; + const url = `http://${address}:${port}`; jsdom.JSDOM.fromURL(url, { resources: "usable", runScripts: "dangerously" }).then((dom) => { dom.window.name = "jsdom"; global.window = dom.window; + global.document = dom.window.document; // Following fixes `navigator is not defined` errors in e2e tests, found here // https://www.appsloveworld.com/reactjs/100/37/mocha-react-navigator-is-not-defined global.navigator = { @@ -97,7 +106,6 @@ exports.getDocument = () => { }; dom.window.fetch = fetch; dom.window.onload = () => { - global.document = dom.window.document; resolve(); }; }); diff --git a/tests/e2e/translations_spec.js b/tests/e2e/translations_spec.js index 969f659a79..12a364d089 100644 --- a/tests/e2e/translations_spec.js +++ b/tests/e2e/translations_spec.js @@ -16,6 +16,7 @@ function createTranslationTestEnvironment () { dom.window.Log = { log: vi.fn(), error: vi.fn() }; dom.window.translations = translations; + dom.window.fetch = fetch; dom.window.eval(translatorJs); const window = dom.window; diff --git a/tests/unit/classes/translator_spec.js b/tests/unit/classes/translator_spec.js index 06357916af..383f823bf8 100644 --- a/tests/unit/classes/translator_spec.js +++ b/tests/unit/classes/translator_spec.js @@ -13,6 +13,7 @@ function createTranslationTestEnvironment () { const dom = new JSDOM("", { url: "http://localhost:3001", runScripts: "outside-only" }); dom.window.Log = { log: vi.fn(), error: vi.fn() }; + dom.window.fetch = fetch; dom.window.eval(translatorJs); return { window: dom.window, Translator: dom.window.Translator }; From 7758dcf7207f10e1024ba81c7d6047e0910c9909 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:46:34 +0100 Subject: [PATCH 02/17] refactor(tests): remove unnecessary onload callback in JSDOM setup The onload callback in getDocument() was no longer needed since global.document is set immediately after JSDOM creation. Removing it simplifies the code and makes tests run faster. --- tests/e2e/helpers/global-setup.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/e2e/helpers/global-setup.js b/tests/e2e/helpers/global-setup.js index df4f27f8e3..3350713366 100644 --- a/tests/e2e/helpers/global-setup.js +++ b/tests/e2e/helpers/global-setup.js @@ -105,9 +105,7 @@ exports.getDocument = () => { useragent: "node.js" }; dom.window.fetch = fetch; - dom.window.onload = () => { - resolve(); - }; + resolve(); }); }); }; From fd02f5d2d221ef27bceb241b37c956cbe20c6a43 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:46:34 +0100 Subject: [PATCH 03/17] refactor(tests): modernize getDocument with async/await and events.once --- tests/e2e/helpers/global-setup.js | 42 +++++++++++++++++-------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/tests/e2e/helpers/global-setup.js b/tests/e2e/helpers/global-setup.js index 3350713366..1ef98e884c 100644 --- a/tests/e2e/helpers/global-setup.js +++ b/tests/e2e/helpers/global-setup.js @@ -1,6 +1,7 @@ const path = require("node:path"); const os = require("node:os"); const fs = require("node:fs"); +const { once } = require("node:events"); const jsdom = require("jsdom"); // global absolute root path @@ -89,25 +90,28 @@ exports.stopApplication = async (waitTime = 100) => { } }; -exports.getDocument = () => { - return new Promise((resolve) => { - const port = global.testPort || config.port || 8080; - // JSDOM requires localhost instead of 0.0.0.0 for URL resolution - const address = config.address === "0.0.0.0" ? "localhost" : config.address || "localhost"; - const url = `http://${address}:${port}`; - jsdom.JSDOM.fromURL(url, { resources: "usable", runScripts: "dangerously" }).then((dom) => { - dom.window.name = "jsdom"; - global.window = dom.window; - global.document = dom.window.document; - // Following fixes `navigator is not defined` errors in e2e tests, found here - // https://www.appsloveworld.com/reactjs/100/37/mocha-react-navigator-is-not-defined - global.navigator = { - useragent: "node.js" - }; - dom.window.fetch = fetch; - resolve(); - }); - }); +exports.getDocument = async () => { + const port = global.testPort || config.port || 8080; + // JSDOM requires localhost instead of 0.0.0.0 for URL resolution + const address = config.address === "0.0.0.0" ? "localhost" : config.address || "localhost"; + const url = `http://${address}:${port}`; + + const dom = await jsdom.JSDOM.fromURL(url, { resources: "usable", runScripts: "dangerously" }); + + dom.window.name = "jsdom"; + global.window = dom.window; + global.document = dom.window.document; + // Some modules access navigator.*, so provide a minimal stub for JSDOM-based tests. + global.navigator = { + useragent: "node.js" + }; + dom.window.fetch = fetch; + + // fromURL() resolves when HTML is loaded, but with resources: "usable", + // external scripts load asynchronously. Wait for the load event to ensure scripts are executed. + if (dom.window.document.readyState !== "complete") { + await once(dom.window, "load"); + } }; exports.waitForElement = (selector, ignoreValue = "", timeout = 0) => { From cd999278c9a370015df843144aab93e30aac850b Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:46:34 +0100 Subject: [PATCH 04/17] test: migrate E2E tests from JSDOM to Playwright --- tests/e2e/animateCSS_spec.js | 6 +- tests/e2e/custom_module_regions_spec.js | 10 +- tests/e2e/env_spec.js | 4 +- tests/e2e/helpers/global-setup.js | 225 ++++++++++++------ tests/e2e/helpers/weather-functions.js | 12 +- tests/e2e/modules/alert_spec.js | 14 +- tests/e2e/modules/calendar_spec.js | 33 ++- tests/e2e/modules/clock_spec.js | 8 +- tests/e2e/modules/compliments_spec.js | 4 +- tests/e2e/modules/helloworld_spec.js | 8 +- tests/e2e/modules/newsfeed_spec.js | 22 +- tests/e2e/modules/weather_current_spec.js | 3 +- tests/e2e/modules/weather_forecast_spec.js | 8 +- tests/e2e/modules_display_spec.js | 12 +- tests/e2e/modules_empty_spec.js | 12 +- tests/e2e/modules_position_spec.js | 8 +- .../calendar_fetcher_utils_bad_rrule.js | 9 +- 17 files changed, 235 insertions(+), 163 deletions(-) diff --git a/tests/e2e/animateCSS_spec.js b/tests/e2e/animateCSS_spec.js index f363bc43b4..6c4d496706 100644 --- a/tests/e2e/animateCSS_spec.js +++ b/tests/e2e/animateCSS_spec.js @@ -29,10 +29,10 @@ describe("AnimateCSS integration Test", () => { async function waitForAnimationClass (cls, { timeout = 6000 } = {}) { const start = Date.now(); while (Date.now() - start < timeout) { - if (document.querySelector(`.compliments.animate__animated.${cls}`)) { + if (await helpers.querySelector(`.compliments.animate__animated.${cls}`)) { // small stability wait await new Promise((r) => setTimeout(r, 50)); - if (document.querySelector(`.compliments.animate__animated.${cls}`)) return true; + if (await helpers.querySelector(`.compliments.animate__animated.${cls}`)) return true; } await new Promise((r) => setTimeout(r, 100)); } @@ -47,7 +47,7 @@ describe("AnimateCSS integration Test", () => { async function assertNoAnimationWithin (ms = 2000) { const start = Date.now(); while (Date.now() - start < ms) { - if (document.querySelector(".compliments.animate__animated")) { + if (await helpers.querySelector(".compliments.animate__animated")) { throw new Error("Unexpected animate__animated class present in non-animation scenario"); } await new Promise((r) => setTimeout(r, 100)); diff --git a/tests/e2e/custom_module_regions_spec.js b/tests/e2e/custom_module_regions_spec.js index 48d09712da..7701508609 100644 --- a/tests/e2e/custom_module_regions_spec.js +++ b/tests/e2e/custom_module_regions_spec.js @@ -16,15 +16,15 @@ describe("Custom Position of modules", () => { const className1 = positions[i].replace("_", "."); let message1 = positions[i]; it(`should show text in ${message1}`, async () => { - const elem = await helpers.waitForElement(`.${className1}`); - expect(elem).not.toBeNull(); - expect(elem.textContent).toContain(`Text in ${message1}`); + await expect( + helpers.expectTextContent(`.${className1} .module-content`, { contains: `Text in ${message1}` }) + ).resolves.toBe(true); }); i = 1; const className2 = positions[i].replace("_", "."); let message2 = positions[i]; it(`should NOT show text in ${message2}`, async () => { - const elem = await helpers.waitForElement(`.${className2}`, "", 1500); + const elem = await helpers.querySelector(`.${className2} .module-content`); expect(elem).toBeNull(); - }, 1510); + }); }); diff --git a/tests/e2e/env_spec.js b/tests/e2e/env_spec.js index eebc8912f0..55f28f40ce 100644 --- a/tests/e2e/env_spec.js +++ b/tests/e2e/env_spec.js @@ -20,8 +20,6 @@ describe("App environment", () => { }); it("should show the title MagicMirror²", async () => { - const elem = await helpers.waitForElement("title"); - expect(elem).not.toBeNull(); - expect(elem.textContent).toBe("MagicMirror²"); + await expect(helpers.expectTextContent("title", { equals: "MagicMirror²" })).resolves.toBe(true); }); }); diff --git a/tests/e2e/helpers/global-setup.js b/tests/e2e/helpers/global-setup.js index 1ef98e884c..4b9fed76ad 100644 --- a/tests/e2e/helpers/global-setup.js +++ b/tests/e2e/helpers/global-setup.js @@ -1,8 +1,7 @@ const path = require("node:path"); const os = require("node:os"); const fs = require("node:fs"); -const { once } = require("node:events"); -const jsdom = require("jsdom"); +const { chromium } = require("playwright"); // global absolute root path global.root_path = path.resolve(`${__dirname}/../../../`); @@ -17,8 +16,67 @@ const sampleCss = [ " top: 100%;", "}" ]; -var indexData = []; -var cssData = []; +let indexData = ""; +let cssData = ""; + +let browser; +let context; +let page; + +/** + * Ensure Playwright browser and context are available. + * @returns {Promise} + */ +async function ensureContext () { + if (!browser) { + browser = await chromium.launch({ headless: true }); + } + if (!context) { + context = await browser.newContext(); + } +} + +/** + * Open a fresh page pointing to the provided url. + * @param {string} url target url + * @returns {Promise} initialized page instance + */ +async function openPage (url) { + await ensureContext(); + if (page) { + await page.close(); + } + page = await context.newPage(); + await page.goto(url, { waitUntil: "load" }); + return page; +} + +/** + * Close page, context and browser if they exist. + * @returns {Promise} + */ +async function closeBrowser () { + if (page) { + await page.close(); + page = null; + } + if (context) { + await context.close(); + context = null; + } + if (browser) { + await browser.close(); + browser = null; + } +} + +exports.getPage = () => { + if (!page) { + throw new Error("Playwright page is not initialized. Call getDocument() first."); + } + return page; +}; + exports.startApplication = async (configFilename, exec) => { vi.resetModules(); @@ -36,7 +94,7 @@ exports.startApplication = async (configFilename, exec) => { }); if (global.app) { - await this.stopApplication(); + await exports.stopApplication(); } // Use fixed port 8080 (tests run sequentially, no conflicts) @@ -65,106 +123,117 @@ exports.startApplication = async (configFilename, exec) => { }; exports.stopApplication = async (waitTime = 100) => { + await closeBrowser(); + if (!global.app) { - if (global.window) { - global.window.close(); - delete global.window; - } delete global.testPort; return Promise.resolve(); } - // Stop server first await global.app.stop(); delete global.app; delete global.testPort; // Wait for any pending async operations to complete before closing DOM await new Promise((resolve) => setTimeout(resolve, waitTime)); - - if (global.window) { - // Close window after async operations have settled - global.window.close(); - delete global.window; - delete global.document; - } }; exports.getDocument = async () => { const port = global.testPort || config.port || 8080; - // JSDOM requires localhost instead of 0.0.0.0 for URL resolution const address = config.address === "0.0.0.0" ? "localhost" : config.address || "localhost"; const url = `http://${address}:${port}`; - const dom = await jsdom.JSDOM.fromURL(url, { resources: "usable", runScripts: "dangerously" }); + await openPage(url); +}; - dom.window.name = "jsdom"; - global.window = dom.window; - global.document = dom.window.document; - // Some modules access navigator.*, so provide a minimal stub for JSDOM-based tests. - global.navigator = { - useragent: "node.js" - }; - dom.window.fetch = fetch; +exports.waitForElement = async (selector, ignoreValue = "", timeout = 0) => { + const currentPage = exports.getPage(); + const locator = currentPage.locator(selector); + const effectiveTimeout = timeout && timeout > 0 ? timeout : 30000; + const deadline = Date.now() + effectiveTimeout; - // fromURL() resolves when HTML is loaded, but with resources: "usable", - // external scripts load asynchronously. Wait for the load event to ensure scripts are executed. - if (dom.window.document.readyState !== "complete") { - await once(dom.window, "load"); + while (Date.now() <= deadline) { + const count = await locator.count(); + if (count > 0) { + if (!ignoreValue) { + return locator.first(); + } + const text = await locator.first().textContent(); + if (!text || !text.includes(ignoreValue)) { + return locator.first(); + } + } + await currentPage.waitForTimeout(100); } + + return null; }; -exports.waitForElement = (selector, ignoreValue = "", timeout = 0) => { - return new Promise((resolve) => { - let oldVal = "dummy12345"; - let element = null; - const interval = setInterval(() => { - element = document.querySelector(selector); - if (element) { - let newVal = element.textContent; - if (newVal === oldVal) { - clearInterval(interval); - resolve(element); - } else { - if (ignoreValue === "") { - oldVal = newVal; - } else { - if (!newVal.includes(ignoreValue)) oldVal = newVal; - } - } +exports.waitForAllElements = async (selector, timeout = 30000) => { + const currentPage = exports.getPage(); + const locator = currentPage.locator(selector); + const effectiveTimeout = timeout && timeout > 0 ? timeout : 30000; + const deadline = Date.now() + effectiveTimeout; + + while (Date.now() <= deadline) { + const count = await locator.count(); + if (count > 0) { + const elements = []; + for (let i = 0; i < count; i++) { + elements.push(locator.nth(i)); } - }, 100); - if (timeout !== 0) { - setTimeout(() => { - if (interval) clearInterval(interval); - resolve(null); - }, timeout); + return elements; } - }); + await currentPage.waitForTimeout(100); + } + + return []; }; -exports.waitForAllElements = (selector) => { - return new Promise((resolve) => { - let oldVal = 999999; - const interval = setInterval(() => { - const element = document.querySelectorAll(selector); - if (element) { - let newVal = element.length; - if (newVal === oldVal) { - clearInterval(interval); - resolve(element); - } else { - if (newVal !== 0) oldVal = newVal; - } - } - }, 100); - }); +exports.testMatch = async (selector, regex) => { + await exports.expectTextContent(selector, { matches: regex }); + return true; }; -exports.testMatch = async (element, regex) => { - const elem = await this.waitForElement(element); - expect(elem).not.toBeNull(); - expect(elem.textContent).toMatch(regex); +exports.querySelector = async (selector) => { + const locator = exports.getPage().locator(selector); + return (await locator.count()) > 0 ? locator.first() : null; +}; + +exports.querySelectorAll = async (selector) => { + const locator = exports.getPage().locator(selector); + const count = await locator.count(); + const elements = []; + for (let i = 0; i < count; i++) { + elements.push(locator.nth(i)); + } + return elements; +}; + +exports.expectTextContent = async (target, expectation) => { + if (!expectation || (expectation.equals === undefined && expectation.contains === undefined && expectation.matches === undefined)) { + throw new Error("expectTextContent expects an object with equals, contains, or matches"); + } + + let locator = target; + if (typeof target === "string") { + locator = await exports.waitForElement(target); + } + + expect(locator).not.toBeNull(); + if (!locator) { + const description = typeof target === "string" ? target : "supplied locator"; + throw new Error(`No element found for ${description}`); + } + + const textPromise = locator.textContent(); + if (expectation.equals !== undefined) { + await expect(textPromise).resolves.toBe(expectation.equals); + } else if (expectation.contains !== undefined) { + await expect(textPromise).resolves.toContain(expectation.contains); + } else { + await expect(textPromise).resolves.toMatch(expectation.matches); + } return true; }; @@ -172,7 +241,7 @@ exports.fixupIndex = async () => { // read and save the git level index file indexData = (await fs.promises.readFile(indexFile)).toString(); // make lines of the content - let workIndexLines = indexData.split(os.EOL); + const workIndexLines = indexData.split(os.EOL); // loop thru the lines to find place to insert new region for (let l in workIndexLines) { if (workIndexLines[l].includes("region top right")) { @@ -191,7 +260,7 @@ exports.fixupIndex = async () => { exports.restoreIndex = async () => { // if we read in data - if (indexData.length > 1) { + if (indexData.length > 0) { //write out saved index.html await fs.promises.writeFile(indexFile, indexData, { flush: true }); // write out saved custom.css diff --git a/tests/e2e/helpers/weather-functions.js b/tests/e2e/helpers/weather-functions.js index 3a3e2d2742..a8b6535e05 100644 --- a/tests/e2e/helpers/weather-functions.js +++ b/tests/e2e/helpers/weather-functions.js @@ -4,12 +4,12 @@ const helpers = require("./global-setup"); exports.getText = async (element, result) => { const elem = await helpers.waitForElement(element); expect(elem).not.toBeNull(); - expect( - elem.textContent - .trim() - .replace(/(\r\n|\n|\r)/gm, "") - .replace(/[ ]+/g, " ") - ).toBe(result); + const rawText = await elem.textContent(); + const content = (rawText ?? "") + .trim() + .replace(/(\r\n|\n|\r)/gm, "") + .replace(/[ ]+/g, " "); + expect(content).toBe(result); return true; }; diff --git a/tests/e2e/modules/alert_spec.js b/tests/e2e/modules/alert_spec.js index a8125b8ab3..842eaf52a6 100644 --- a/tests/e2e/modules/alert_spec.js +++ b/tests/e2e/modules/alert_spec.js @@ -16,7 +16,7 @@ describe("Alert module", () => { await new Promise((resolve) => setTimeout(resolve, 1000)); // Check that no alert/notification elements are present - const alertElements = document.querySelectorAll(".ns-box .ns-box-inner .light.bright.small"); + const alertElements = await helpers.querySelectorAll(".ns-box .ns-box-inner .light.bright.small"); expect(alertElements).toHaveLength(0); }); }); @@ -31,9 +31,9 @@ describe("Alert module", () => { }); it("should show the translated welcome message", async () => { - const elem = await helpers.waitForElement(".ns-box .ns-box-inner .light.bright.small"); - expect(elem).not.toBeNull(); - expect(elem.textContent).toContain("Welcome, start was successful!"); + await expect( + helpers.expectTextContent(".ns-box .ns-box-inner .light.bright.small", { contains: "Welcome, start was successful!" }) + ).resolves.toBe(true); }); }); @@ -44,9 +44,9 @@ describe("Alert module", () => { }); it("should show the custom welcome message", async () => { - const elem = await helpers.waitForElement(".ns-box .ns-box-inner .light.bright.small"); - expect(elem).not.toBeNull(); - expect(elem.textContent).toContain("Custom welcome message!"); + await expect( + helpers.expectTextContent(".ns-box .ns-box-inner .light.bright.small", { contains: "Custom welcome message!" }) + ).resolves.toBe(true); }); }); }); diff --git a/tests/e2e/modules/calendar_spec.js b/tests/e2e/modules/calendar_spec.js index 10eb6b8425..30b2786b15 100644 --- a/tests/e2e/modules/calendar_spec.js +++ b/tests/e2e/modules/calendar_spec.js @@ -1,29 +1,38 @@ const helpers = require("../helpers/global-setup"); const serverBasicAuth = require("../helpers/basic-auth"); +const getPage = () => helpers.getPage(); + describe("Calendar module", () => { /** - * @param {string} element css selector - * @param {string} result expected number - * @param {string} not reverse result - * @returns {boolean} result + * Assert the number of matching elements. + * @param {string} selector css selector + * @param {number} expectedLength expected number of elements + * @param {string} [not] optional negation marker (use "not" to negate) + * @returns {Promise} assertion outcome */ - const testElementLength = async (element, result, not) => { - const elem = await helpers.waitForAllElements(element); - expect(elem).not.toBeNull(); + const testElementLength = async (selector, expectedLength, not) => { + const locator = getPage().locator(selector); + if (expectedLength === 0 && not !== "not") { + const count = await locator.count(); + expect(count).toBe(0); + return true; + } + + await locator.first().waitFor({ state: "attached" }); + const count = await locator.count(); if (not === "not") { - expect(elem).not.toHaveLength(result); + expect(count).not.toBe(expectedLength); } else { - expect(elem).toHaveLength(result); + expect(count).toBe(expectedLength); } return true; }; const testTextContain = async (element, text) => { - const elem = await helpers.waitForElement(element, "undefinedLoading"); - expect(elem).not.toBeNull(); - expect(elem.textContent).toContain(text); + const locator = await helpers.waitForElement(element, "undefinedLoading"); + await helpers.expectTextContent(locator, { contains: text }); return true; }; diff --git a/tests/e2e/modules/clock_spec.js b/tests/e2e/modules/clock_spec.js index 88ac4f4d36..564931751b 100644 --- a/tests/e2e/modules/clock_spec.js +++ b/tests/e2e/modules/clock_spec.js @@ -78,7 +78,7 @@ describe("Clock module", () => { }); it("should not show the time when digital clock is shown", async () => { - const elem = document.querySelector(".clock .digital .time"); + const elem = await helpers.querySelector(".clock .digital .time"); expect(elem).toBeNull(); }); }); @@ -113,7 +113,7 @@ describe("Clock module", () => { const elem = await helpers.waitForElement(".clock .digital .sun"); expect(elem).not.toBeNull(); - const elem2 = document.querySelector(".clock .digital .sun .fas.fa-sun"); + const elem2 = await helpers.querySelector(".clock .digital .sun .fas.fa-sun"); expect(elem2).toBeNull(); }); }); @@ -134,7 +134,7 @@ describe("Clock module", () => { const weekToShow = `Week ${currentWeekNumber}`; const elem = await helpers.waitForElement(".clock .week"); expect(elem).not.toBeNull(); - expect(elem.textContent).toBe(weekToShow); + await expect(helpers.expectTextContent(elem, { equals: weekToShow })).resolves.toBe(true); }); }); @@ -154,7 +154,7 @@ describe("Clock module", () => { const weekToShow = `W${currentWeekNumber}`; const elem = await helpers.waitForElement(".clock .week"); expect(elem).not.toBeNull(); - expect(elem.textContent).toBe(weekToShow); + await expect(helpers.expectTextContent(elem, { equals: weekToShow })).resolves.toBe(true); }); }); diff --git a/tests/e2e/modules/compliments_spec.js b/tests/e2e/modules/compliments_spec.js index 47214781ab..3e605d1ce0 100644 --- a/tests/e2e/modules/compliments_spec.js +++ b/tests/e2e/modules/compliments_spec.js @@ -12,7 +12,9 @@ describe("Compliments module", () => { expect(elem).not.toBeNull(); elem = await helpers.waitForElement(".module-content"); expect(elem).not.toBeNull(); - expect(complimentsArray).toContain(elem.textContent); + const content = await elem.textContent(); + expect(content).not.toBeNull(); + expect(complimentsArray).toContain(content); return true; }; diff --git a/tests/e2e/modules/helloworld_spec.js b/tests/e2e/modules/helloworld_spec.js index 413c60e509..6592813a9a 100644 --- a/tests/e2e/modules/helloworld_spec.js +++ b/tests/e2e/modules/helloworld_spec.js @@ -12,9 +12,7 @@ describe("Test helloworld module", () => { }); it("Test message helloworld module", async () => { - const elem = await helpers.waitForElement(".helloworld"); - expect(elem).not.toBeNull(); - expect(elem.textContent).toContain("Test HelloWorld Module"); + await expect(helpers.expectTextContent(".helloworld", { contains: "Test HelloWorld Module" })).resolves.toBe(true); }); }); @@ -25,9 +23,7 @@ describe("Test helloworld module", () => { }); it("Test message helloworld module", async () => { - const elem = await helpers.waitForElement(".helloworld"); - expect(elem).not.toBeNull(); - expect(elem.textContent).toContain("Hello World!"); + await expect(helpers.expectTextContent(".helloworld", { contains: "Hello World!" })).resolves.toBe(true); }); }); }); diff --git a/tests/e2e/modules/newsfeed_spec.js b/tests/e2e/modules/newsfeed_spec.js index dc317c91eb..3dfd371798 100644 --- a/tests/e2e/modules/newsfeed_spec.js +++ b/tests/e2e/modules/newsfeed_spec.js @@ -9,20 +9,16 @@ const runTests = async () => { }); it("should show the newsfeed title", async () => { - const elem = await helpers.waitForElement(".newsfeed .newsfeed-source"); - expect(elem).not.toBeNull(); - expect(elem.textContent).toContain("Rodrigo Ramirez Blog"); + await expect(helpers.expectTextContent(".newsfeed .newsfeed-source", { contains: "Rodrigo Ramirez Blog" })).resolves.toBe(true); }); it("should show the newsfeed article", async () => { - const elem = await helpers.waitForElement(".newsfeed .newsfeed-title"); - expect(elem).not.toBeNull(); - expect(elem.textContent).toContain("QPanel"); + await expect(helpers.expectTextContent(".newsfeed .newsfeed-title", { contains: "QPanel" })).resolves.toBe(true); }); it("should NOT show the newsfeed description", async () => { await helpers.waitForElement(".newsfeed"); - const elem = document.querySelector(".newsfeed .newsfeed-desc"); + const elem = await helpers.querySelector(".newsfeed .newsfeed-desc"); expect(elem).toBeNull(); }); }); @@ -34,15 +30,11 @@ const runTests = async () => { }); it("should not show articles with prohibited words", async () => { - const elem = await helpers.waitForElement(".newsfeed .newsfeed-title"); - expect(elem).not.toBeNull(); - expect(elem.textContent).toContain("Problema VirtualBox"); + await expect(helpers.expectTextContent(".newsfeed .newsfeed-title", { contains: "Problema VirtualBox" })).resolves.toBe(true); }); it("should show the newsfeed description", async () => { - const elem = await helpers.waitForElement(".newsfeed .newsfeed-desc"); - expect(elem).not.toBeNull(); - expect(elem.textContent).not.toHaveLength(0); + await expect(helpers.expectTextContent(".newsfeed .newsfeed-desc", { matches: /\S/ })).resolves.toBe(true); }); }); @@ -55,7 +47,7 @@ const runTests = async () => { it("should show malformed url warning", async () => { const elem = await helpers.waitForElement(".newsfeed .small", "No news at the moment."); expect(elem).not.toBeNull(); - expect(elem.textContent).toContain("Error in the Newsfeed module. Malformed url."); + await expect(helpers.expectTextContent(elem, { contains: "Error in the Newsfeed module. Malformed url." })).resolves.toBe(true); }); }); @@ -68,7 +60,7 @@ const runTests = async () => { it("should show empty items info message", async () => { const elem = await helpers.waitForElement(".newsfeed .small"); expect(elem).not.toBeNull(); - expect(elem.textContent).toContain("No news at the moment."); + await expect(helpers.expectTextContent(elem, { contains: "No news at the moment." })).resolves.toBe(true); }); }); }; diff --git a/tests/e2e/modules/weather_current_spec.js b/tests/e2e/modules/weather_current_spec.js index 78d3278491..73beaf4414 100644 --- a/tests/e2e/modules/weather_current_spec.js +++ b/tests/e2e/modules/weather_current_spec.js @@ -56,7 +56,8 @@ describe("Weather module", () => { it("should render windDirection with an arrow", async () => { const elem = await helpers.waitForElement(".weather .normal.medium sup i.fa-long-arrow-alt-down"); expect(elem).not.toBeNull(); - expect(elem.outerHTML).toContain("transform:rotate(250deg)"); + const html = await elem.evaluate((node) => node.outerHTML); + expect(html).toContain("transform:rotate(250deg)"); }); it("should render humidity next to wind", async () => { diff --git a/tests/e2e/modules/weather_forecast_spec.js b/tests/e2e/modules/weather_forecast_spec.js index f974264318..3be7d68417 100644 --- a/tests/e2e/modules/weather_forecast_spec.js +++ b/tests/e2e/modules/weather_forecast_spec.js @@ -45,7 +45,8 @@ describe("Weather module: Weather Forecast", () => { it(`should render fading of rows with opacity=${opacity}`, async () => { const elem = await helpers.waitForElement(`.weather table.small tr:nth-child(${index + 1})`); expect(elem).not.toBeNull(); - expect(elem.outerHTML).toContain(``); + const html = await elem.evaluate((node) => node.outerHTML); + expect(html).toContain(`style="opacity: ${opacity};"`); }); } }); @@ -76,8 +77,9 @@ describe("Weather module: Weather Forecast", () => { it("should render colored rows", async () => { const table = await helpers.waitForElement(".weather table.myTableClass"); expect(table).not.toBeNull(); - expect(table.rows).not.toBeNull(); - expect(table.rows).toHaveLength(5); + const rows = table.locator("tr"); + await rows.first().waitFor({ state: "visible" }); + await expect(rows.count()).resolves.toBe(5); }); const precipitations = [undefined, "2.51 mm"]; diff --git a/tests/e2e/modules_display_spec.js b/tests/e2e/modules_display_spec.js index 88b9ff50be..dc6c172e57 100644 --- a/tests/e2e/modules_display_spec.js +++ b/tests/e2e/modules_display_spec.js @@ -10,15 +10,15 @@ describe("Display of modules", () => { }); it("should show the test header", async () => { - const elem = await helpers.waitForElement("#module_0_helloworld .module-header"); - expect(elem).not.toBeNull(); // textContent returns lowercase here, the uppercase is realized by CSS, which therefore does not end up in textContent - expect(elem.textContent).toBe("test_header"); + await expect( + helpers.expectTextContent("#module_0_helloworld .module-header", { equals: "test_header" }) + ).resolves.toBe(true); }); it("should show no header if no header text is specified", async () => { - const elem = await helpers.waitForElement("#module_1_helloworld .module-header"); - expect(elem).not.toBeNull(); - expect(elem.textContent).toBe("undefined"); + await expect( + helpers.expectTextContent("#module_1_helloworld .module-header", { equals: "undefined" }) + ).resolves.toBe(true); }); }); diff --git a/tests/e2e/modules_empty_spec.js b/tests/e2e/modules_empty_spec.js index e5a4cd6670..dfe060eac9 100644 --- a/tests/e2e/modules_empty_spec.js +++ b/tests/e2e/modules_empty_spec.js @@ -10,14 +10,14 @@ describe("Check configuration without modules", () => { }); it("shows the message MagicMirror² title", async () => { - const elem = await helpers.waitForElement("#module_1_helloworld .module-content"); - expect(elem).not.toBeNull(); - expect(elem.textContent).toContain("MagicMirror²"); + await expect( + helpers.expectTextContent("#module_1_helloworld .module-content", { contains: "MagicMirror²" }) + ).resolves.toBe(true); }); it("shows the project URL", async () => { - const elem = await helpers.waitForElement("#module_5_helloworld .module-content"); - expect(elem).not.toBeNull(); - expect(elem.textContent).toContain("https://magicmirror.builders/"); + await expect( + helpers.expectTextContent("#module_5_helloworld .module-content", { contains: "https://magicmirror.builders/" }) + ).resolves.toBe(true); }); }); diff --git a/tests/e2e/modules_position_spec.js b/tests/e2e/modules_position_spec.js index 0aa8379092..5107b5bdf1 100644 --- a/tests/e2e/modules_position_spec.js +++ b/tests/e2e/modules_position_spec.js @@ -1,5 +1,7 @@ const helpers = require("./helpers/global-setup"); +const getPage = () => helpers.getPage(); + describe("Position of modules", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/positions.js"); @@ -14,9 +16,9 @@ describe("Position of modules", () => { for (const position of positions) { const className = position.replace("_", "."); it(`should show text in ${position}`, async () => { - const elem = await helpers.waitForElement(`.${className}`); - expect(elem).not.toBeNull(); - expect(elem.textContent).toContain(`Text in ${position}`); + const locator = getPage().locator(`.${className} .module-content`).first(); + await locator.waitFor({ state: "visible" }); + await expect(helpers.expectTextContent(locator, { contains: `Text in ${position}` })).resolves.toBe(true); }); } }); diff --git a/tests/unit/modules/default/calendar/calendar_fetcher_utils_bad_rrule.js b/tests/unit/modules/default/calendar/calendar_fetcher_utils_bad_rrule.js index e4508332e5..4475c65c26 100644 --- a/tests/unit/modules/default/calendar/calendar_fetcher_utils_bad_rrule.js +++ b/tests/unit/modules/default/calendar/calendar_fetcher_utils_bad_rrule.js @@ -9,10 +9,11 @@ describe("Calendar fetcher utils test", () => { describe("filterEvents", () => { it("no events, not crash", () => { - const minusOneHour = moment().subtract(1, "hours").toDate(); - const minusTwoHours = moment().subtract(2, "hours").toDate(); - const plusOneHour = moment().add(1, "hours").toDate(); - const plusTwoHours = moment().add(2, "hours").toDate(); + const base = moment().startOf("day").add(12, "hours"); + const minusOneHour = base.clone().subtract(1, "hours").toDate(); + const minusTwoHours = base.clone().subtract(2, "hours").toDate(); + const plusOneHour = base.clone().add(1, "hours").toDate(); + const plusTwoHours = base.clone().add(2, "hours").toDate(); const filteredEvents = CalendarFetcherUtils.filterEvents( { From d639140809c34ab7f38a291c8a82e6e0805583fe Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:46:34 +0100 Subject: [PATCH 05/17] ci: install Playwright browsers in automated test workflow --- .github/workflows/automated-tests.yaml | 3 +++ CHANGELOG.md | 1 + 2 files changed, 4 insertions(+) diff --git a/.github/workflows/automated-tests.yaml b/.github/workflows/automated-tests.yaml index c11103043e..9908744741 100644 --- a/.github/workflows/automated-tests.yaml +++ b/.github/workflows/automated-tests.yaml @@ -59,6 +59,9 @@ jobs: - name: "Install MagicMirror²" run: | node --run install-mm:dev + - name: "Install Playwright browsers" + run: | + npx playwright install --with-deps chromium - name: "Prepare environment for tests" run: | # Fix chrome-sandbox permissions: diff --git a/CHANGELOG.md b/CHANGELOG.md index d42db02392..212928adb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ planned for 2026-01-01 - [calendar] hide repeatingCountTitle if the event count is zero (#3949) - [core] configure cspell to check default modules only and fix typos (#3955) - [core] refactor: replace `XMLHttpRequest` with `fetch` in `translator.js` (#3950) +- [tests] migrate e2e tests to Playwright (#3950) ### Fixed From 7ab1eb8dddcc67133ca5f09a9a82f8b941e4c9cf Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:46:34 +0100 Subject: [PATCH 06/17] refactor(tests): remove helper abstractions and assert text content directly Replace `expectTextContent` and `testMatch` helpers with explicit `waitForElement` + `textContent()` + standard expect assertions. Makes test logic transparent and easier to follow without hidden indirection. --- tests/e2e/custom_module_regions_spec.js | 8 ++-- tests/e2e/env_spec.js | 6 ++- tests/e2e/helpers/global-setup.js | 32 -------------- tests/e2e/modules/alert_spec.js | 16 ++++--- tests/e2e/modules/calendar_spec.js | 9 ++-- tests/e2e/modules/clock_de_spec.js | 12 +++++- tests/e2e/modules/clock_es_spec.js | 42 +++++++++++++++---- tests/e2e/modules/clock_spec.js | 56 ++++++++++++++++++++----- tests/e2e/modules/helloworld_spec.js | 12 +++++- tests/e2e/modules/newsfeed_spec.js | 32 +++++++++++--- tests/e2e/modules_display_spec.js | 16 ++++--- tests/e2e/modules_empty_spec.js | 16 ++++--- tests/e2e/modules_position_spec.js | 4 +- 13 files changed, 176 insertions(+), 85 deletions(-) diff --git a/tests/e2e/custom_module_regions_spec.js b/tests/e2e/custom_module_regions_spec.js index 7701508609..b62f794d88 100644 --- a/tests/e2e/custom_module_regions_spec.js +++ b/tests/e2e/custom_module_regions_spec.js @@ -16,9 +16,11 @@ describe("Custom Position of modules", () => { const className1 = positions[i].replace("_", "."); let message1 = positions[i]; it(`should show text in ${message1}`, async () => { - await expect( - helpers.expectTextContent(`.${className1} .module-content`, { contains: `Text in ${message1}` }) - ).resolves.toBe(true); + const elem = await helpers.waitForElement(`.${className1} .module-content`); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toContain(`Text in ${message1}`); }); i = 1; const className2 = positions[i].replace("_", "."); diff --git a/tests/e2e/env_spec.js b/tests/e2e/env_spec.js index 55f28f40ce..e9733c5abd 100644 --- a/tests/e2e/env_spec.js +++ b/tests/e2e/env_spec.js @@ -20,6 +20,10 @@ describe("App environment", () => { }); it("should show the title MagicMirror²", async () => { - await expect(helpers.expectTextContent("title", { equals: "MagicMirror²" })).resolves.toBe(true); + const elem = await helpers.waitForElement("title"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toBe("MagicMirror²"); }); }); diff --git a/tests/e2e/helpers/global-setup.js b/tests/e2e/helpers/global-setup.js index 4b9fed76ad..0d515855f5 100644 --- a/tests/e2e/helpers/global-setup.js +++ b/tests/e2e/helpers/global-setup.js @@ -190,11 +190,6 @@ exports.waitForAllElements = async (selector, timeout = 30000) => { return []; }; -exports.testMatch = async (selector, regex) => { - await exports.expectTextContent(selector, { matches: regex }); - return true; -}; - exports.querySelector = async (selector) => { const locator = exports.getPage().locator(selector); return (await locator.count()) > 0 ? locator.first() : null; @@ -210,33 +205,6 @@ exports.querySelectorAll = async (selector) => { return elements; }; -exports.expectTextContent = async (target, expectation) => { - if (!expectation || (expectation.equals === undefined && expectation.contains === undefined && expectation.matches === undefined)) { - throw new Error("expectTextContent expects an object with equals, contains, or matches"); - } - - let locator = target; - if (typeof target === "string") { - locator = await exports.waitForElement(target); - } - - expect(locator).not.toBeNull(); - if (!locator) { - const description = typeof target === "string" ? target : "supplied locator"; - throw new Error(`No element found for ${description}`); - } - - const textPromise = locator.textContent(); - if (expectation.equals !== undefined) { - await expect(textPromise).resolves.toBe(expectation.equals); - } else if (expectation.contains !== undefined) { - await expect(textPromise).resolves.toContain(expectation.contains); - } else { - await expect(textPromise).resolves.toMatch(expectation.matches); - } - return true; -}; - exports.fixupIndex = async () => { // read and save the git level index file indexData = (await fs.promises.readFile(indexFile)).toString(); diff --git a/tests/e2e/modules/alert_spec.js b/tests/e2e/modules/alert_spec.js index 842eaf52a6..efef170d0e 100644 --- a/tests/e2e/modules/alert_spec.js +++ b/tests/e2e/modules/alert_spec.js @@ -31,9 +31,11 @@ describe("Alert module", () => { }); it("should show the translated welcome message", async () => { - await expect( - helpers.expectTextContent(".ns-box .ns-box-inner .light.bright.small", { contains: "Welcome, start was successful!" }) - ).resolves.toBe(true); + const elem = await helpers.waitForElement(".ns-box .ns-box-inner .light.bright.small"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toContain("Welcome, start was successful!"); }); }); @@ -44,9 +46,11 @@ describe("Alert module", () => { }); it("should show the custom welcome message", async () => { - await expect( - helpers.expectTextContent(".ns-box .ns-box-inner .light.bright.small", { contains: "Custom welcome message!" }) - ).resolves.toBe(true); + const elem = await helpers.waitForElement(".ns-box .ns-box-inner .light.bright.small"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toContain("Custom welcome message!"); }); }); }); diff --git a/tests/e2e/modules/calendar_spec.js b/tests/e2e/modules/calendar_spec.js index 30b2786b15..6b794060ac 100644 --- a/tests/e2e/modules/calendar_spec.js +++ b/tests/e2e/modules/calendar_spec.js @@ -30,9 +30,12 @@ describe("Calendar module", () => { return true; }; - const testTextContain = async (element, text) => { - const locator = await helpers.waitForElement(element, "undefinedLoading"); - await helpers.expectTextContent(locator, { contains: text }); + const testTextContain = async (selector, expectedText) => { + const locator = await helpers.waitForElement(selector, "undefinedLoading"); + expect(locator).not.toBeNull(); + const text = await locator.textContent(); + expect(text).not.toBeNull(); + expect(text).toContain(expectedText); return true; }; diff --git a/tests/e2e/modules/clock_de_spec.js b/tests/e2e/modules/clock_de_spec.js index e46b37abbc..030fa0b314 100644 --- a/tests/e2e/modules/clock_de_spec.js +++ b/tests/e2e/modules/clock_de_spec.js @@ -13,7 +13,11 @@ describe("Clock set to german language module", () => { it("shows week with correct format", async () => { const weekRegex = /^[0-9]{1,2}. Kalenderwoche$/; - await expect(helpers.testMatch(".clock .week", weekRegex)).resolves.toBe(true); + const elem = await helpers.waitForElement(".clock .week"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toMatch(weekRegex); }); }); @@ -25,7 +29,11 @@ describe("Clock set to german language module", () => { it("shows week with correct format", async () => { const weekRegex = /^[0-9]{1,2}KW$/; - await expect(helpers.testMatch(".clock .week", weekRegex)).resolves.toBe(true); + const elem = await helpers.waitForElement(".clock .week"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toMatch(weekRegex); }); }); }); diff --git a/tests/e2e/modules/clock_es_spec.js b/tests/e2e/modules/clock_es_spec.js index 38b997a50b..788356c627 100644 --- a/tests/e2e/modules/clock_es_spec.js +++ b/tests/e2e/modules/clock_es_spec.js @@ -13,12 +13,20 @@ describe("Clock set to spanish language module", () => { it("shows date with correct format", async () => { const dateRegex = /^(?:lunes|martes|miércoles|jueves|viernes|sábado|domingo), \d{1,2} de (?:enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre) de \d{4}$/; - await expect(helpers.testMatch(".clock .date", dateRegex)).resolves.toBe(true); + const elem = await helpers.waitForElement(".clock .date"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toMatch(dateRegex); }); it("shows time in 24hr format", async () => { const timeRegex = /^(?:2[0-3]|[01]\d):[0-5]\d[0-5]\d$/; - await expect(helpers.testMatch(".clock .time", timeRegex)).resolves.toBe(true); + const elem = await helpers.waitForElement(".clock .time"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toMatch(timeRegex); }); }); @@ -30,12 +38,20 @@ describe("Clock set to spanish language module", () => { it("shows date with correct format", async () => { const dateRegex = /^(?:lunes|martes|miércoles|jueves|viernes|sábado|domingo), \d{1,2} de (?:enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre) de \d{4}$/; - await expect(helpers.testMatch(".clock .date", dateRegex)).resolves.toBe(true); + const elem = await helpers.waitForElement(".clock .date"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toMatch(dateRegex); }); it("shows time in 12hr format", async () => { const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[ap]m$/; - await expect(helpers.testMatch(".clock .time", timeRegex)).resolves.toBe(true); + const elem = await helpers.waitForElement(".clock .time"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toMatch(timeRegex); }); }); @@ -47,7 +63,11 @@ describe("Clock set to spanish language module", () => { it("shows 12hr time with upper case AM/PM", async () => { const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[AP]M$/; - await expect(helpers.testMatch(".clock .time", timeRegex)).resolves.toBe(true); + const elem = await helpers.waitForElement(".clock .time"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toMatch(timeRegex); }); }); @@ -59,7 +79,11 @@ describe("Clock set to spanish language module", () => { it("shows week with correct format", async () => { const weekRegex = /^Semana [0-9]{1,2}$/; - await expect(helpers.testMatch(".clock .week", weekRegex)).resolves.toBe(true); + const elem = await helpers.waitForElement(".clock .week"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toMatch(weekRegex); }); }); @@ -71,7 +95,11 @@ describe("Clock set to spanish language module", () => { it("shows week with correct format", async () => { const weekRegex = /^S[0-9]{1,2}$/; - await expect(helpers.testMatch(".clock .week", weekRegex)).resolves.toBe(true); + const elem = await helpers.waitForElement(".clock .week"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toMatch(weekRegex); }); }); }); diff --git a/tests/e2e/modules/clock_spec.js b/tests/e2e/modules/clock_spec.js index 564931751b..dca756eee8 100644 --- a/tests/e2e/modules/clock_spec.js +++ b/tests/e2e/modules/clock_spec.js @@ -14,12 +14,20 @@ describe("Clock module", () => { it("should show the date in the correct format", async () => { const dateRegex = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (?:January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}, \d{4}$/; - await expect(helpers.testMatch(".clock .date", dateRegex)).resolves.toBe(true); + const elem = await helpers.waitForElement(".clock .date"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toMatch(dateRegex); }); it("should show the time in 24hr format", async () => { const timeRegex = /^(?:2[0-3]|[01]\d):[0-5]\d[0-5]\d$/; - await expect(helpers.testMatch(".clock .time", timeRegex)).resolves.toBe(true); + const elem = await helpers.waitForElement(".clock .time"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toMatch(timeRegex); }); }); @@ -31,12 +39,20 @@ describe("Clock module", () => { it("should show the date in the correct format", async () => { const dateRegex = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (?:January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}, \d{4}$/; - await expect(helpers.testMatch(".clock .date", dateRegex)).resolves.toBe(true); + const elem = await helpers.waitForElement(".clock .date"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toMatch(dateRegex); }); it("should show the time in 12hr format", async () => { const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[ap]m$/; - await expect(helpers.testMatch(".clock .time", timeRegex)).resolves.toBe(true); + const elem = await helpers.waitForElement(".clock .time"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toMatch(timeRegex); }); it("check for discreet elements of clock", async () => { @@ -55,7 +71,11 @@ describe("Clock module", () => { it("should show 12hr time with upper case AM/PM", async () => { const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[AP]M$/; - await expect(helpers.testMatch(".clock .time", timeRegex)).resolves.toBe(true); + const elem = await helpers.waitForElement(".clock .time"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toMatch(timeRegex); }); }); @@ -67,7 +87,11 @@ describe("Clock module", () => { it("should show 12hr time without seconds am/pm", async () => { const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[ap]m$/; - await expect(helpers.testMatch(".clock .time", timeRegex)).resolves.toBe(true); + const elem = await helpers.waitForElement(".clock .time"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toMatch(timeRegex); }); }); @@ -126,7 +150,11 @@ describe("Clock module", () => { it("should show the week in the correct format", async () => { const weekRegex = /^Week [0-9]{1,2}$/; - await expect(helpers.testMatch(".clock .week", weekRegex)).resolves.toBe(true); + const elem = await helpers.waitForElement(".clock .week"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toMatch(weekRegex); }); it("should show the week with the correct number of week of year", async () => { @@ -134,7 +162,9 @@ describe("Clock module", () => { const weekToShow = `Week ${currentWeekNumber}`; const elem = await helpers.waitForElement(".clock .week"); expect(elem).not.toBeNull(); - await expect(helpers.expectTextContent(elem, { equals: weekToShow })).resolves.toBe(true); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toBe(weekToShow); }); }); @@ -146,7 +176,11 @@ describe("Clock module", () => { it("should show the week in the correct format", async () => { const weekRegex = /^W[0-9]{1,2}$/; - await expect(helpers.testMatch(".clock .week", weekRegex)).resolves.toBe(true); + const elem = await helpers.waitForElement(".clock .week"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toMatch(weekRegex); }); it("should show the week with the correct number of week of year", async () => { @@ -154,7 +188,9 @@ describe("Clock module", () => { const weekToShow = `W${currentWeekNumber}`; const elem = await helpers.waitForElement(".clock .week"); expect(elem).not.toBeNull(); - await expect(helpers.expectTextContent(elem, { equals: weekToShow })).resolves.toBe(true); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toBe(weekToShow); }); }); diff --git a/tests/e2e/modules/helloworld_spec.js b/tests/e2e/modules/helloworld_spec.js index 6592813a9a..b6e1c11b64 100644 --- a/tests/e2e/modules/helloworld_spec.js +++ b/tests/e2e/modules/helloworld_spec.js @@ -12,7 +12,11 @@ describe("Test helloworld module", () => { }); it("Test message helloworld module", async () => { - await expect(helpers.expectTextContent(".helloworld", { contains: "Test HelloWorld Module" })).resolves.toBe(true); + const elem = await helpers.waitForElement(".helloworld"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toContain("Test HelloWorld Module"); }); }); @@ -23,7 +27,11 @@ describe("Test helloworld module", () => { }); it("Test message helloworld module", async () => { - await expect(helpers.expectTextContent(".helloworld", { contains: "Hello World!" })).resolves.toBe(true); + const elem = await helpers.waitForElement(".helloworld"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toContain("Hello World!"); }); }); }); diff --git a/tests/e2e/modules/newsfeed_spec.js b/tests/e2e/modules/newsfeed_spec.js index 3dfd371798..b6f8eed540 100644 --- a/tests/e2e/modules/newsfeed_spec.js +++ b/tests/e2e/modules/newsfeed_spec.js @@ -9,11 +9,19 @@ const runTests = async () => { }); it("should show the newsfeed title", async () => { - await expect(helpers.expectTextContent(".newsfeed .newsfeed-source", { contains: "Rodrigo Ramirez Blog" })).resolves.toBe(true); + const elem = await helpers.waitForElement(".newsfeed .newsfeed-source"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toContain("Rodrigo Ramirez Blog"); }); it("should show the newsfeed article", async () => { - await expect(helpers.expectTextContent(".newsfeed .newsfeed-title", { contains: "QPanel" })).resolves.toBe(true); + const elem = await helpers.waitForElement(".newsfeed .newsfeed-title"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toContain("QPanel"); }); it("should NOT show the newsfeed description", async () => { @@ -30,11 +38,19 @@ const runTests = async () => { }); it("should not show articles with prohibited words", async () => { - await expect(helpers.expectTextContent(".newsfeed .newsfeed-title", { contains: "Problema VirtualBox" })).resolves.toBe(true); + const elem = await helpers.waitForElement(".newsfeed .newsfeed-title"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toContain("Problema VirtualBox"); }); it("should show the newsfeed description", async () => { - await expect(helpers.expectTextContent(".newsfeed .newsfeed-desc", { matches: /\S/ })).resolves.toBe(true); + const elem = await helpers.waitForElement(".newsfeed .newsfeed-desc"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toMatch(/\S/); }); }); @@ -47,7 +63,9 @@ const runTests = async () => { it("should show malformed url warning", async () => { const elem = await helpers.waitForElement(".newsfeed .small", "No news at the moment."); expect(elem).not.toBeNull(); - await expect(helpers.expectTextContent(elem, { contains: "Error in the Newsfeed module. Malformed url." })).resolves.toBe(true); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toContain("Error in the Newsfeed module. Malformed url."); }); }); @@ -60,7 +78,9 @@ const runTests = async () => { it("should show empty items info message", async () => { const elem = await helpers.waitForElement(".newsfeed .small"); expect(elem).not.toBeNull(); - await expect(helpers.expectTextContent(elem, { contains: "No news at the moment." })).resolves.toBe(true); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toContain("No news at the moment."); }); }); }; diff --git a/tests/e2e/modules_display_spec.js b/tests/e2e/modules_display_spec.js index dc6c172e57..76c26f23c4 100644 --- a/tests/e2e/modules_display_spec.js +++ b/tests/e2e/modules_display_spec.js @@ -11,14 +11,18 @@ describe("Display of modules", () => { it("should show the test header", async () => { // textContent returns lowercase here, the uppercase is realized by CSS, which therefore does not end up in textContent - await expect( - helpers.expectTextContent("#module_0_helloworld .module-header", { equals: "test_header" }) - ).resolves.toBe(true); + const elem = await helpers.waitForElement("#module_0_helloworld .module-header"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toBe("test_header"); }); it("should show no header if no header text is specified", async () => { - await expect( - helpers.expectTextContent("#module_1_helloworld .module-header", { equals: "undefined" }) - ).resolves.toBe(true); + const elem = await helpers.waitForElement("#module_1_helloworld .module-header"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toBe("undefined"); }); }); diff --git a/tests/e2e/modules_empty_spec.js b/tests/e2e/modules_empty_spec.js index dfe060eac9..7694bcf755 100644 --- a/tests/e2e/modules_empty_spec.js +++ b/tests/e2e/modules_empty_spec.js @@ -10,14 +10,18 @@ describe("Check configuration without modules", () => { }); it("shows the message MagicMirror² title", async () => { - await expect( - helpers.expectTextContent("#module_1_helloworld .module-content", { contains: "MagicMirror²" }) - ).resolves.toBe(true); + const elem = await helpers.waitForElement("#module_1_helloworld .module-content"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toContain("MagicMirror²"); }); it("shows the project URL", async () => { - await expect( - helpers.expectTextContent("#module_5_helloworld .module-content", { contains: "https://magicmirror.builders/" }) - ).resolves.toBe(true); + const elem = await helpers.waitForElement("#module_5_helloworld .module-content"); + expect(elem).not.toBeNull(); + const text = await elem.textContent(); + expect(text).not.toBeNull(); + expect(text).toContain("https://magicmirror.builders/"); }); }); diff --git a/tests/e2e/modules_position_spec.js b/tests/e2e/modules_position_spec.js index 5107b5bdf1..30a5878911 100644 --- a/tests/e2e/modules_position_spec.js +++ b/tests/e2e/modules_position_spec.js @@ -18,7 +18,9 @@ describe("Position of modules", () => { it(`should show text in ${position}`, async () => { const locator = getPage().locator(`.${className} .module-content`).first(); await locator.waitFor({ state: "visible" }); - await expect(helpers.expectTextContent(locator, { contains: `Text in ${position}` })).resolves.toBe(true); + const text = await locator.textContent(); + expect(text).not.toBeNull(); + expect(text).toContain(`Text in ${position}`); }); } }); From 4fadfaf22c30847862453a4e1d585b9e9e10a0da Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:46:34 +0100 Subject: [PATCH 07/17] refactor(tests): replace nullish coalescing with explicit null check --- tests/e2e/helpers/weather-functions.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/e2e/helpers/weather-functions.js b/tests/e2e/helpers/weather-functions.js index a8b6535e05..0be9a49458 100644 --- a/tests/e2e/helpers/weather-functions.js +++ b/tests/e2e/helpers/weather-functions.js @@ -5,7 +5,8 @@ exports.getText = async (element, result) => { const elem = await helpers.waitForElement(element); expect(elem).not.toBeNull(); const rawText = await elem.textContent(); - const content = (rawText ?? "") + expect(rawText).not.toBeNull(); + const content = rawText .trim() .replace(/(\r\n|\n|\r)/gm, "") .replace(/[ ]+/g, " "); From a4997916a8d627df859181e47009a8b76658cba7 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:46:34 +0100 Subject: [PATCH 08/17] refactor(tests): simplify querySelectorAll using Playwright's all() method --- tests/e2e/helpers/global-setup.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/e2e/helpers/global-setup.js b/tests/e2e/helpers/global-setup.js index 0d515855f5..7fa88f1732 100644 --- a/tests/e2e/helpers/global-setup.js +++ b/tests/e2e/helpers/global-setup.js @@ -197,12 +197,7 @@ exports.querySelector = async (selector) => { exports.querySelectorAll = async (selector) => { const locator = exports.getPage().locator(selector); - const count = await locator.count(); - const elements = []; - for (let i = 0; i < count; i++) { - elements.push(locator.nth(i)); - } - return elements; + return await locator.all(); }; exports.fixupIndex = async () => { From b028698141dec379fd82d5067f24338539ff0e06 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:46:34 +0100 Subject: [PATCH 09/17] refactor(test): Improve readability of wait helper functions --- tests/e2e/helpers/global-setup.js | 39 ++++++++++++++----------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/tests/e2e/helpers/global-setup.js b/tests/e2e/helpers/global-setup.js index 7fa88f1732..3c49b83c74 100644 --- a/tests/e2e/helpers/global-setup.js +++ b/tests/e2e/helpers/global-setup.js @@ -146,48 +146,45 @@ exports.getDocument = async () => { await openPage(url); }; -exports.waitForElement = async (selector, ignoreValue = "", timeout = 0) => { +exports.waitForElement = async (selector, ignoreValue = "", timeout = 30000) => { const currentPage = exports.getPage(); const locator = currentPage.locator(selector); - const effectiveTimeout = timeout && timeout > 0 ? timeout : 30000; - const deadline = Date.now() + effectiveTimeout; + const deadline = Date.now() + timeout; while (Date.now() <= deadline) { - const count = await locator.count(); - if (count > 0) { + const elements = await locator.all(); + if (elements.length > 0) { + const firstElement = elements[0]; if (!ignoreValue) { - return locator.first(); + return firstElement; } - const text = await locator.first().textContent(); + const text = await firstElement.textContent(); if (!text || !text.includes(ignoreValue)) { - return locator.first(); + return firstElement; } } + // Wait a bit before retrying await currentPage.waitForTimeout(100); } + // If the loop completes without finding the element, it timed out. return null; }; exports.waitForAllElements = async (selector, timeout = 30000) => { const currentPage = exports.getPage(); const locator = currentPage.locator(selector); - const effectiveTimeout = timeout && timeout > 0 ? timeout : 30000; - const deadline = Date.now() + effectiveTimeout; - while (Date.now() <= deadline) { - const count = await locator.count(); - if (count > 0) { - const elements = []; - for (let i = 0; i < count; i++) { - elements.push(locator.nth(i)); - } - return elements; - } - await currentPage.waitForTimeout(100); + try { + // Wait for at least one element to be attached before returning all of them. + await locator.first().waitFor({ state: "attached", timeout }); + } catch { + // If no element is found within the timeout, return an empty array. + return []; } - return []; + // Playwright's .all() returns an array of locators, similar to a NodeList. + return await locator.all(); }; exports.querySelector = async (selector) => { From 8443e514f5ea48db65b95bf9113cec2b73a08ab1 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:46:34 +0100 Subject: [PATCH 10/17] test: migrate E2E tests to Playwright assertions Replace manual DOM helpers with Playwright's built-in assertions for more reliable tests. Use page.locator() with toBeVisible(), toContainText(), and toHaveText() instead of waitForElement() and manual text checks. --- tests/e2e/animateCSS_spec.js | 30 +++--- tests/e2e/custom_module_regions_spec.js | 13 ++- tests/e2e/env_spec.js | 11 ++- tests/e2e/helpers/global-setup.js | 51 ---------- tests/e2e/helpers/weather-functions.js | 18 ++-- tests/e2e/modules/alert_spec.js | 21 ++-- tests/e2e/modules/calendar_spec.js | 34 +++---- tests/e2e/modules/clock_de_spec.js | 17 ++-- tests/e2e/modules/clock_es_spec.js | 50 +++------- tests/e2e/modules/clock_spec.js | 109 +++++++-------------- tests/e2e/modules/compliments_spec.js | 20 ++-- tests/e2e/modules/helloworld_spec.js | 17 ++-- tests/e2e/modules/newsfeed_spec.js | 49 ++++----- tests/e2e/modules/weather_current_spec.js | 42 ++++---- tests/e2e/modules/weather_forecast_spec.js | 41 ++++---- tests/e2e/modules/weather_hourly_spec.js | 16 ++- tests/e2e/modules_display_spec.js | 16 ++- tests/e2e/modules_empty_spec.js | 16 ++- 18 files changed, 229 insertions(+), 342 deletions(-) diff --git a/tests/e2e/animateCSS_spec.js b/tests/e2e/animateCSS_spec.js index 6c4d496706..4a3e4b3fc7 100644 --- a/tests/e2e/animateCSS_spec.js +++ b/tests/e2e/animateCSS_spec.js @@ -1,8 +1,11 @@ +const { expect } = require("playwright/test"); const helpers = require("./helpers/global-setup"); // Validate Animate.css integration for compliments module using class toggling. // We intentionally ignore computed animation styles (jsdom doesn't simulate real animations). describe("AnimateCSS integration Test", () => { + let page; + // Config variants under test const TEST_CONFIG_ANIM = "tests/configs/modules/compliments/compliments_animateCSS.js"; const TEST_CONFIG_FALLBACK = "tests/configs/modules/compliments/compliments_animateCSS_fallbackToDefault.js"; // invalid animation names @@ -11,13 +14,12 @@ describe("AnimateCSS integration Test", () => { /** * Get the compliments container element (waits until available). - * @returns {Promise} compliments root element + * @returns {Promise} */ async function getComplimentsElement () { await helpers.getDocument(); - const el = await helpers.waitForElement(".compliments"); - expect(el).not.toBeNull(); - return el; + page = helpers.getPage(); + await expect(page.locator(".compliments")).toBeVisible(); } /** @@ -27,16 +29,12 @@ describe("AnimateCSS integration Test", () => { * @returns {Promise} true if class detected in time */ async function waitForAnimationClass (cls, { timeout = 6000 } = {}) { - const start = Date.now(); - while (Date.now() - start < timeout) { - if (await helpers.querySelector(`.compliments.animate__animated.${cls}`)) { - // small stability wait - await new Promise((r) => setTimeout(r, 50)); - if (await helpers.querySelector(`.compliments.animate__animated.${cls}`)) return true; - } - await new Promise((r) => setTimeout(r, 100)); - } - throw new Error(`Timeout waiting for class ${cls}`); + const locator = page.locator(`.compliments.animate__animated.${cls}`); + await locator.waitFor({ state: "attached", timeout }); + // small stability wait + await new Promise((r) => setTimeout(r, 50)); + await expect(locator).toBeAttached(); + return true; } /** @@ -46,8 +44,10 @@ describe("AnimateCSS integration Test", () => { */ async function assertNoAnimationWithin (ms = 2000) { const start = Date.now(); + const locator = page.locator(".compliments.animate__animated"); while (Date.now() - start < ms) { - if (await helpers.querySelector(".compliments.animate__animated")) { + const count = await locator.count(); + if (count > 0) { throw new Error("Unexpected animate__animated class present in non-animation scenario"); } await new Promise((r) => setTimeout(r, 100)); diff --git a/tests/e2e/custom_module_regions_spec.js b/tests/e2e/custom_module_regions_spec.js index b62f794d88..243f8e489a 100644 --- a/tests/e2e/custom_module_regions_spec.js +++ b/tests/e2e/custom_module_regions_spec.js @@ -1,10 +1,14 @@ +const { expect } = require("playwright/test"); const helpers = require("./helpers/global-setup"); describe("Custom Position of modules", () => { + let page; + beforeAll(async () => { await helpers.fixupIndex(); await helpers.startApplication("tests/configs/customregions.js"); await helpers.getDocument(); + page = helpers.getPage(); }); afterAll(async () => { await helpers.stopApplication(); @@ -16,17 +20,12 @@ describe("Custom Position of modules", () => { const className1 = positions[i].replace("_", "."); let message1 = positions[i]; it(`should show text in ${message1}`, async () => { - const elem = await helpers.waitForElement(`.${className1} .module-content`); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toContain(`Text in ${message1}`); + await expect(page.locator(`.${className1} .module-content`)).toContainText(`Text in ${message1}`); }); i = 1; const className2 = positions[i].replace("_", "."); let message2 = positions[i]; it(`should NOT show text in ${message2}`, async () => { - const elem = await helpers.querySelector(`.${className2} .module-content`); - expect(elem).toBeNull(); + await expect(page.locator(`.${className2} .module-content`)).toHaveCount(0); }); }); diff --git a/tests/e2e/env_spec.js b/tests/e2e/env_spec.js index e9733c5abd..03f5f76007 100644 --- a/tests/e2e/env_spec.js +++ b/tests/e2e/env_spec.js @@ -1,9 +1,13 @@ +const { expect } = require("playwright/test"); const helpers = require("./helpers/global-setup"); describe("App environment", () => { + let page; + beforeAll(async () => { await helpers.startApplication("tests/configs/default.js"); await helpers.getDocument(); + page = helpers.getPage(); }); afterAll(async () => { await helpers.stopApplication(); @@ -20,10 +24,7 @@ describe("App environment", () => { }); it("should show the title MagicMirror²", async () => { - const elem = await helpers.waitForElement("title"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toBe("MagicMirror²"); + const title = await page.locator("title").textContent(); + expect(title).toBe("MagicMirror²"); }); }); diff --git a/tests/e2e/helpers/global-setup.js b/tests/e2e/helpers/global-setup.js index 3c49b83c74..098f4895ce 100644 --- a/tests/e2e/helpers/global-setup.js +++ b/tests/e2e/helpers/global-setup.js @@ -146,57 +146,6 @@ exports.getDocument = async () => { await openPage(url); }; -exports.waitForElement = async (selector, ignoreValue = "", timeout = 30000) => { - const currentPage = exports.getPage(); - const locator = currentPage.locator(selector); - const deadline = Date.now() + timeout; - - while (Date.now() <= deadline) { - const elements = await locator.all(); - if (elements.length > 0) { - const firstElement = elements[0]; - if (!ignoreValue) { - return firstElement; - } - const text = await firstElement.textContent(); - if (!text || !text.includes(ignoreValue)) { - return firstElement; - } - } - // Wait a bit before retrying - await currentPage.waitForTimeout(100); - } - - // If the loop completes without finding the element, it timed out. - return null; -}; - -exports.waitForAllElements = async (selector, timeout = 30000) => { - const currentPage = exports.getPage(); - const locator = currentPage.locator(selector); - - try { - // Wait for at least one element to be attached before returning all of them. - await locator.first().waitFor({ state: "attached", timeout }); - } catch { - // If no element is found within the timeout, return an empty array. - return []; - } - - // Playwright's .all() returns an array of locators, similar to a NodeList. - return await locator.all(); -}; - -exports.querySelector = async (selector) => { - const locator = exports.getPage().locator(selector); - return (await locator.count()) > 0 ? locator.first() : null; -}; - -exports.querySelectorAll = async (selector) => { - const locator = exports.getPage().locator(selector); - return await locator.all(); -}; - exports.fixupIndex = async () => { // read and save the git level index file indexData = (await fs.promises.readFile(indexFile)).toString(); diff --git a/tests/e2e/helpers/weather-functions.js b/tests/e2e/helpers/weather-functions.js index 0be9a49458..99b0c8eeb1 100644 --- a/tests/e2e/helpers/weather-functions.js +++ b/tests/e2e/helpers/weather-functions.js @@ -1,16 +1,22 @@ const { injectMockData, cleanupMockData } = require("../../utils/weather_mocker"); const helpers = require("./global-setup"); -exports.getText = async (element, result) => { - const elem = await helpers.waitForElement(element); - expect(elem).not.toBeNull(); - const rawText = await elem.textContent(); - expect(rawText).not.toBeNull(); +/** + * Get normalized text from element (trimmed, no line breaks, single spaces) + * @param {import("playwright").Page} page Playwright page instance + * @param {string} selector css selector + * @param {string} expectedText expected text content + * @returns {Promise} assertion outcome + */ +exports.getText = async (page, selector, expectedText) => { + const locator = page.locator(selector); + await locator.waitFor({ state: "visible" }); + const rawText = await locator.textContent(); const content = rawText .trim() .replace(/(\r\n|\n|\r)/gm, "") .replace(/[ ]+/g, " "); - expect(content).toBe(result); + expect(content).toBe(expectedText); return true; }; diff --git a/tests/e2e/modules/alert_spec.js b/tests/e2e/modules/alert_spec.js index efef170d0e..367f2eb204 100644 --- a/tests/e2e/modules/alert_spec.js +++ b/tests/e2e/modules/alert_spec.js @@ -1,6 +1,9 @@ +const { expect } = require("playwright/test"); const helpers = require("../helpers/global-setup"); describe("Alert module", () => { + let page; + afterAll(async () => { await helpers.stopApplication(); }); @@ -9,6 +12,7 @@ describe("Alert module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/alert/welcome_false.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should not show any welcome message", async () => { @@ -16,8 +20,7 @@ describe("Alert module", () => { await new Promise((resolve) => setTimeout(resolve, 1000)); // Check that no alert/notification elements are present - const alertElements = await helpers.querySelectorAll(".ns-box .ns-box-inner .light.bright.small"); - expect(alertElements).toHaveLength(0); + await expect(page.locator(".ns-box .ns-box-inner .light.bright.small")).toHaveCount(0); }); }); @@ -25,17 +28,14 @@ describe("Alert module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/alert/welcome_true.js"); await helpers.getDocument(); + page = helpers.getPage(); // Wait for the application to initialize await new Promise((resolve) => setTimeout(resolve, 1000)); }); it("should show the translated welcome message", async () => { - const elem = await helpers.waitForElement(".ns-box .ns-box-inner .light.bright.small"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toContain("Welcome, start was successful!"); + await expect(page.locator(".ns-box .ns-box-inner .light.bright.small")).toContainText("Welcome, start was successful!"); }); }); @@ -43,14 +43,11 @@ describe("Alert module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/alert/welcome_string.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should show the custom welcome message", async () => { - const elem = await helpers.waitForElement(".ns-box .ns-box-inner .light.bright.small"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toContain("Custom welcome message!"); + await expect(page.locator(".ns-box .ns-box-inner .light.bright.small")).toContainText("Custom welcome message!"); }); }); }); diff --git a/tests/e2e/modules/calendar_spec.js b/tests/e2e/modules/calendar_spec.js index 6b794060ac..4b3a5d6ed1 100644 --- a/tests/e2e/modules/calendar_spec.js +++ b/tests/e2e/modules/calendar_spec.js @@ -1,9 +1,9 @@ +const { expect } = require("playwright/test"); const helpers = require("../helpers/global-setup"); const serverBasicAuth = require("../helpers/basic-auth"); -const getPage = () => helpers.getPage(); - describe("Calendar module", () => { + let page; /** * Assert the number of matching elements. @@ -13,29 +13,17 @@ describe("Calendar module", () => { * @returns {Promise} assertion outcome */ const testElementLength = async (selector, expectedLength, not) => { - const locator = getPage().locator(selector); - if (expectedLength === 0 && not !== "not") { - const count = await locator.count(); - expect(count).toBe(0); - return true; - } - - await locator.first().waitFor({ state: "attached" }); - const count = await locator.count(); + const locator = page.locator(selector); if (not === "not") { - expect(count).not.toBe(expectedLength); + await expect(locator).not.toHaveCount(expectedLength); } else { - expect(count).toBe(expectedLength); + await expect(locator).toHaveCount(expectedLength); } return true; }; const testTextContain = async (selector, expectedText) => { - const locator = await helpers.waitForElement(selector, "undefinedLoading"); - expect(locator).not.toBeNull(); - const text = await locator.textContent(); - expect(text).not.toBeNull(); - expect(text).toContain(expectedText); + await expect(page.locator(selector).first()).toContainText(expectedText); return true; }; @@ -47,6 +35,7 @@ describe("Calendar module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/calendar/default.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should show the default maximumEntries of 10", async () => { @@ -62,6 +51,7 @@ describe("Calendar module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/calendar/custom.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should show the custom maximumEntries of 5", async () => { @@ -93,6 +83,7 @@ describe("Calendar module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/calendar/recurring.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should show the recurring birthday event 6 times", async () => { @@ -105,6 +96,7 @@ describe("Calendar module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/calendar/long-fullday-event.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should contain text 'Ends in' with the left days", async () => { @@ -121,6 +113,7 @@ describe("Calendar module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/calendar/single-fullday-event.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should contain text 'Today'", async () => { @@ -136,6 +129,7 @@ describe("Calendar module", () => { await helpers.startApplication("tests/configs/modules/calendar/changed-port.js"); serverBasicAuth.listen(8010); await helpers.getDocument(); + page = helpers.getPage(); }); afterAll(async () => { @@ -151,6 +145,7 @@ describe("Calendar module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/calendar/basic-auth.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should return TestEvents", async () => { @@ -162,6 +157,7 @@ describe("Calendar module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/calendar/auth-default.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should return TestEvents", async () => { @@ -173,6 +169,7 @@ describe("Calendar module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/calendar/old-basic-auth.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should return TestEvents", async () => { @@ -185,6 +182,7 @@ describe("Calendar module", () => { await helpers.startApplication("tests/configs/modules/calendar/fail-basic-auth.js"); serverBasicAuth.listen(8020); await helpers.getDocument(); + page = helpers.getPage(); }); afterAll(async () => { diff --git a/tests/e2e/modules/clock_de_spec.js b/tests/e2e/modules/clock_de_spec.js index 030fa0b314..79d3bb4b5d 100644 --- a/tests/e2e/modules/clock_de_spec.js +++ b/tests/e2e/modules/clock_de_spec.js @@ -1,6 +1,9 @@ +const { expect } = require("playwright/test"); const helpers = require("../helpers/global-setup"); describe("Clock set to german language module", () => { + let page; + afterAll(async () => { await helpers.stopApplication(); }); @@ -9,15 +12,12 @@ describe("Clock set to german language module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/clock/de/clock_showWeek.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("shows week with correct format", async () => { const weekRegex = /^[0-9]{1,2}. Kalenderwoche$/; - const elem = await helpers.waitForElement(".clock .week"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toMatch(weekRegex); + await expect(page.locator(".clock .week")).toHaveText(weekRegex); }); }); @@ -25,15 +25,12 @@ describe("Clock set to german language module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/clock/de/clock_showWeek_short.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("shows week with correct format", async () => { const weekRegex = /^[0-9]{1,2}KW$/; - const elem = await helpers.waitForElement(".clock .week"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toMatch(weekRegex); + await expect(page.locator(".clock .week")).toHaveText(weekRegex); }); }); }); diff --git a/tests/e2e/modules/clock_es_spec.js b/tests/e2e/modules/clock_es_spec.js index 788356c627..6749bda550 100644 --- a/tests/e2e/modules/clock_es_spec.js +++ b/tests/e2e/modules/clock_es_spec.js @@ -1,6 +1,9 @@ +const { expect } = require("playwright/test"); const helpers = require("../helpers/global-setup"); describe("Clock set to spanish language module", () => { + let page; + afterAll(async () => { await helpers.stopApplication(); }); @@ -9,24 +12,17 @@ describe("Clock set to spanish language module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/clock/es/clock_24hr.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("shows date with correct format", async () => { const dateRegex = /^(?:lunes|martes|miércoles|jueves|viernes|sábado|domingo), \d{1,2} de (?:enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre) de \d{4}$/; - const elem = await helpers.waitForElement(".clock .date"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toMatch(dateRegex); + await expect(page.locator(".clock .date")).toHaveText(dateRegex); }); it("shows time in 24hr format", async () => { const timeRegex = /^(?:2[0-3]|[01]\d):[0-5]\d[0-5]\d$/; - const elem = await helpers.waitForElement(".clock .time"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toMatch(timeRegex); + await expect(page.locator(".clock .time")).toHaveText(timeRegex); }); }); @@ -34,24 +30,17 @@ describe("Clock set to spanish language module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/clock/es/clock_12hr.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("shows date with correct format", async () => { const dateRegex = /^(?:lunes|martes|miércoles|jueves|viernes|sábado|domingo), \d{1,2} de (?:enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre) de \d{4}$/; - const elem = await helpers.waitForElement(".clock .date"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toMatch(dateRegex); + await expect(page.locator(".clock .date")).toHaveText(dateRegex); }); it("shows time in 12hr format", async () => { const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[ap]m$/; - const elem = await helpers.waitForElement(".clock .time"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toMatch(timeRegex); + await expect(page.locator(".clock .time")).toHaveText(timeRegex); }); }); @@ -59,15 +48,12 @@ describe("Clock set to spanish language module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/clock/es/clock_showPeriodUpper.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("shows 12hr time with upper case AM/PM", async () => { const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[AP]M$/; - const elem = await helpers.waitForElement(".clock .time"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toMatch(timeRegex); + await expect(page.locator(".clock .time")).toHaveText(timeRegex); }); }); @@ -75,15 +61,12 @@ describe("Clock set to spanish language module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/clock/es/clock_showWeek.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("shows week with correct format", async () => { const weekRegex = /^Semana [0-9]{1,2}$/; - const elem = await helpers.waitForElement(".clock .week"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toMatch(weekRegex); + await expect(page.locator(".clock .week")).toHaveText(weekRegex); }); }); @@ -91,15 +74,12 @@ describe("Clock set to spanish language module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/clock/es/clock_showWeek_short.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("shows week with correct format", async () => { const weekRegex = /^S[0-9]{1,2}$/; - const elem = await helpers.waitForElement(".clock .week"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toMatch(weekRegex); + await expect(page.locator(".clock .week")).toHaveText(weekRegex); }); }); }); diff --git a/tests/e2e/modules/clock_spec.js b/tests/e2e/modules/clock_spec.js index dca756eee8..6fb72fa35b 100644 --- a/tests/e2e/modules/clock_spec.js +++ b/tests/e2e/modules/clock_spec.js @@ -1,7 +1,10 @@ +const { expect } = require("playwright/test"); const moment = require("moment"); const helpers = require("../helpers/global-setup"); describe("Clock module", () => { + let page; + afterAll(async () => { await helpers.stopApplication(); }); @@ -10,24 +13,17 @@ describe("Clock module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/clock/clock_24hr.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should show the date in the correct format", async () => { const dateRegex = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (?:January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}, \d{4}$/; - const elem = await helpers.waitForElement(".clock .date"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toMatch(dateRegex); + await expect(page.locator(".clock .date")).toHaveText(dateRegex); }); it("should show the time in 24hr format", async () => { const timeRegex = /^(?:2[0-3]|[01]\d):[0-5]\d[0-5]\d$/; - const elem = await helpers.waitForElement(".clock .time"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toMatch(timeRegex); + await expect(page.locator(".clock .time")).toHaveText(timeRegex); }); }); @@ -35,31 +31,22 @@ describe("Clock module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/clock/clock_12hr.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should show the date in the correct format", async () => { const dateRegex = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (?:January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}, \d{4}$/; - const elem = await helpers.waitForElement(".clock .date"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toMatch(dateRegex); + await expect(page.locator(".clock .date")).toHaveText(dateRegex); }); it("should show the time in 12hr format", async () => { const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[ap]m$/; - const elem = await helpers.waitForElement(".clock .time"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toMatch(timeRegex); + await expect(page.locator(".clock .time")).toHaveText(timeRegex); }); it("check for discreet elements of clock", async () => { - let elemClock = await helpers.waitForElement(".clock-hour-digital"); - await expect(elemClock).not.toBeNull(); - elemClock = await helpers.waitForElement(".clock-minute-digital"); - await expect(elemClock).not.toBeNull(); + await expect(page.locator(".clock-hour-digital")).toBeVisible(); + await expect(page.locator(".clock-minute-digital")).toBeVisible(); }); }); @@ -67,15 +54,12 @@ describe("Clock module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/clock/clock_showPeriodUpper.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should show 12hr time with upper case AM/PM", async () => { const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[AP]M$/; - const elem = await helpers.waitForElement(".clock .time"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toMatch(timeRegex); + await expect(page.locator(".clock .time")).toHaveText(timeRegex); }); }); @@ -83,15 +67,12 @@ describe("Clock module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/clock/clock_displaySeconds_false.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should show 12hr time without seconds am/pm", async () => { const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[ap]m$/; - const elem = await helpers.waitForElement(".clock .time"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toMatch(timeRegex); + await expect(page.locator(".clock .time")).toHaveText(timeRegex); }); }); @@ -99,11 +80,11 @@ describe("Clock module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/clock/clock_showTime.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should not show the time when digital clock is shown", async () => { - const elem = await helpers.querySelector(".clock .digital .time"); - expect(elem).toBeNull(); + await expect(page.locator(".clock .digital .time")).toHaveCount(0); }); }); @@ -111,19 +92,16 @@ describe("Clock module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/clock/clock_showSunMoon.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should show the sun times", async () => { - const elem = await helpers.waitForElement(".clock .digital .sun"); - expect(elem).not.toBeNull(); - - const elem2 = await helpers.waitForElement(".clock .digital .sun .fas.fa-sun"); - expect(elem2).not.toBeNull(); + await expect(page.locator(".clock .digital .sun")).toBeVisible(); + await expect(page.locator(".clock .digital .sun .fas.fa-sun")).toBeVisible(); }); it("should show the moon times", async () => { - const elem = await helpers.waitForElement(".clock .digital .moon"); - expect(elem).not.toBeNull(); + await expect(page.locator(".clock .digital .moon")).toBeVisible(); }); }); @@ -131,14 +109,12 @@ describe("Clock module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/clock/clock_showSunNoEvent.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should show the sun times", async () => { - const elem = await helpers.waitForElement(".clock .digital .sun"); - expect(elem).not.toBeNull(); - - const elem2 = await helpers.querySelector(".clock .digital .sun .fas.fa-sun"); - expect(elem2).toBeNull(); + await expect(page.locator(".clock .digital .sun")).toBeVisible(); + await expect(page.locator(".clock .digital .sun .fas.fa-sun")).toHaveCount(0); }); }); @@ -146,25 +122,18 @@ describe("Clock module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/clock/clock_showWeek.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should show the week in the correct format", async () => { const weekRegex = /^Week [0-9]{1,2}$/; - const elem = await helpers.waitForElement(".clock .week"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toMatch(weekRegex); + await expect(page.locator(".clock .week")).toHaveText(weekRegex); }); it("should show the week with the correct number of week of year", async () => { const currentWeekNumber = moment().week(); const weekToShow = `Week ${currentWeekNumber}`; - const elem = await helpers.waitForElement(".clock .week"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toBe(weekToShow); + await expect(page.locator(".clock .week")).toHaveText(weekToShow); }); }); @@ -172,25 +141,18 @@ describe("Clock module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/clock/clock_showWeek_short.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should show the week in the correct format", async () => { const weekRegex = /^W[0-9]{1,2}$/; - const elem = await helpers.waitForElement(".clock .week"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toMatch(weekRegex); + await expect(page.locator(".clock .week")).toHaveText(weekRegex); }); it("should show the week with the correct number of week of year", async () => { const currentWeekNumber = moment().week(); const weekToShow = `W${currentWeekNumber}`; - const elem = await helpers.waitForElement(".clock .week"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toBe(weekToShow); + await expect(page.locator(".clock .week")).toHaveText(weekToShow); }); }); @@ -198,11 +160,11 @@ describe("Clock module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/clock/clock_analog.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should show the analog clock face", async () => { - const elem = await helpers.waitForElement(".clock-circle"); - expect(elem).not.toBeNull(); + await expect(page.locator(".clock-circle")).toBeVisible(); }); }); @@ -210,13 +172,12 @@ describe("Clock module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/clock/clock_showDateAnalog.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should show the analog clock face and the date", async () => { - const elemClock = await helpers.waitForElement(".clock-circle"); - await expect(elemClock).not.toBeNull(); - const elemDate = await helpers.waitForElement(".clock .date"); - await expect(elemDate).not.toBeNull(); + await expect(page.locator(".clock-circle")).toBeVisible(); + await expect(page.locator(".clock .date")).toBeVisible(); }); }); }); diff --git a/tests/e2e/modules/compliments_spec.js b/tests/e2e/modules/compliments_spec.js index 3e605d1ce0..e50d1e5895 100644 --- a/tests/e2e/modules/compliments_spec.js +++ b/tests/e2e/modules/compliments_spec.js @@ -1,6 +1,8 @@ +const { expect } = require("playwright/test"); const helpers = require("../helpers/global-setup"); describe("Compliments module", () => { + let page; /** * move similar tests in function doTest @@ -8,12 +10,10 @@ describe("Compliments module", () => { * @returns {boolean} result */ const doTest = async (complimentsArray) => { - let elem = await helpers.waitForElement(".compliments"); - expect(elem).not.toBeNull(); - elem = await helpers.waitForElement(".module-content"); - expect(elem).not.toBeNull(); - const content = await elem.textContent(); - expect(content).not.toBeNull(); + await expect(page.locator(".compliments")).toBeVisible(); + const contentLocator = page.locator(".module-content"); + await contentLocator.waitFor({ state: "visible" }); + const content = await contentLocator.textContent(); expect(complimentsArray).toContain(content); return true; }; @@ -27,6 +27,7 @@ describe("Compliments module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/compliments/compliments_anytime.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("shows anytime because if configure empty parts of day compliments and set anytime compliments", async () => { @@ -38,6 +39,7 @@ describe("Compliments module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/compliments/compliments_only_anytime.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("shows anytime compliments", async () => { @@ -50,6 +52,7 @@ describe("Compliments module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/compliments/compliments_remote.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should show compliments from a remote file", async () => { @@ -62,6 +65,7 @@ describe("Compliments module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/compliments/compliments_specialDayUnique_false.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("compliments array can contain all values", async () => { @@ -73,6 +77,7 @@ describe("Compliments module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/compliments/compliments_specialDayUnique_true.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("compliments array contains only special value", async () => { @@ -84,6 +89,7 @@ describe("Compliments module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/compliments/compliments_e2e_cron_entry.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("compliments array contains only special value", async () => { @@ -97,6 +103,7 @@ describe("Compliments module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/compliments/compliments_file.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("shows 'Remote compliment file works!' as only anytime list set", async () => { //await helpers.startApplication("tests/configs/modules/compliments/compliments_file.js", "01 Jan 2022 10:00:00 GMT"); @@ -111,6 +118,7 @@ describe("Compliments module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/compliments/compliments_file_change.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("shows 'test in morning' as test time set to 10am", async () => { //await helpers.startApplication("tests/configs/modules/compliments/compliments_file_change.js", "01 Jan 2022 10:00:00 GMT"); diff --git a/tests/e2e/modules/helloworld_spec.js b/tests/e2e/modules/helloworld_spec.js index b6e1c11b64..2db8facb89 100644 --- a/tests/e2e/modules/helloworld_spec.js +++ b/tests/e2e/modules/helloworld_spec.js @@ -1,6 +1,9 @@ +const { expect } = require("playwright/test"); const helpers = require("../helpers/global-setup"); describe("Test helloworld module", () => { + let page; + afterAll(async () => { await helpers.stopApplication(); }); @@ -9,14 +12,11 @@ describe("Test helloworld module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/helloworld/helloworld.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("Test message helloworld module", async () => { - const elem = await helpers.waitForElement(".helloworld"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toContain("Test HelloWorld Module"); + await expect(page.locator(".helloworld")).toContainText("Test HelloWorld Module"); }); }); @@ -24,14 +24,11 @@ describe("Test helloworld module", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/helloworld/helloworld_default.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("Test message helloworld module", async () => { - const elem = await helpers.waitForElement(".helloworld"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toContain("Hello World!"); + await expect(page.locator(".helloworld")).toContainText("Hello World!"); }); }); }); diff --git a/tests/e2e/modules/newsfeed_spec.js b/tests/e2e/modules/newsfeed_spec.js index b6f8eed540..4cee4a75b4 100644 --- a/tests/e2e/modules/newsfeed_spec.js +++ b/tests/e2e/modules/newsfeed_spec.js @@ -1,33 +1,28 @@ const fs = require("node:fs"); +const { expect } = require("playwright/test"); const helpers = require("../helpers/global-setup"); const runTests = async () => { + let page; + describe("Default configuration", () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/newsfeed/default.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should show the newsfeed title", async () => { - const elem = await helpers.waitForElement(".newsfeed .newsfeed-source"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toContain("Rodrigo Ramirez Blog"); + await expect(page.locator(".newsfeed .newsfeed-source")).toContainText("Rodrigo Ramirez Blog"); }); it("should show the newsfeed article", async () => { - const elem = await helpers.waitForElement(".newsfeed .newsfeed-title"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toContain("QPanel"); + await expect(page.locator(".newsfeed .newsfeed-title")).toContainText("QPanel"); }); it("should NOT show the newsfeed description", async () => { - await helpers.waitForElement(".newsfeed"); - const elem = await helpers.querySelector(".newsfeed .newsfeed-desc"); - expect(elem).toBeNull(); + await page.locator(".newsfeed").waitFor({ state: "visible" }); + await expect(page.locator(".newsfeed .newsfeed-desc")).toHaveCount(0); }); }); @@ -35,21 +30,17 @@ const runTests = async () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/newsfeed/prohibited_words.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should not show articles with prohibited words", async () => { - const elem = await helpers.waitForElement(".newsfeed .newsfeed-title"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toContain("Problema VirtualBox"); + await expect(page.locator(".newsfeed .newsfeed-title")).toContainText("Problema VirtualBox"); }); it("should show the newsfeed description", async () => { - const elem = await helpers.waitForElement(".newsfeed .newsfeed-desc"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); + const locator = page.locator(".newsfeed .newsfeed-desc"); + await expect(locator).toBeVisible(); + const text = await locator.textContent(); expect(text).toMatch(/\S/); }); }); @@ -58,14 +49,11 @@ const runTests = async () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/newsfeed/incorrect_url.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should show malformed url warning", async () => { - const elem = await helpers.waitForElement(".newsfeed .small", "No news at the moment."); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toContain("Error in the Newsfeed module. Malformed url."); + await expect(page.locator(".newsfeed .small")).toContainText("Error in the Newsfeed module. Malformed url."); }); }); @@ -73,14 +61,11 @@ const runTests = async () => { beforeAll(async () => { await helpers.startApplication("tests/configs/modules/newsfeed/ignore_items.js"); await helpers.getDocument(); + page = helpers.getPage(); }); it("should show empty items info message", async () => { - const elem = await helpers.waitForElement(".newsfeed .small"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toContain("No news at the moment."); + await expect(page.locator(".newsfeed .small")).toContainText("No news at the moment."); }); }); }; diff --git a/tests/e2e/modules/weather_current_spec.js b/tests/e2e/modules/weather_current_spec.js index 73beaf4414..4f26765d11 100644 --- a/tests/e2e/modules/weather_current_spec.js +++ b/tests/e2e/modules/weather_current_spec.js @@ -1,7 +1,11 @@ +const { expect } = require("playwright/test"); const helpers = require("../helpers/global-setup"); const weatherFunc = require("../helpers/weather-functions"); describe("Weather module", () => { + let page; + const getText = (selector, expectedText) => weatherFunc.getText(page, selector, expectedText); + afterAll(async () => { await weatherFunc.stopApplication(); }); @@ -10,26 +14,25 @@ describe("Weather module", () => { describe("Default configuration", () => { beforeAll(async () => { await weatherFunc.startApplication("tests/configs/modules/weather/currentweather_default.js", {}); + page = helpers.getPage(); }); it("should render wind speed and wind direction", async () => { - await expect(weatherFunc.getText(".weather .normal.medium span:nth-child(2)", "12 WSW")).resolves.toBe(true); + await expect(getText(".weather .normal.medium span:nth-child(2)", "12 WSW")).resolves.toBe(true); }); it("should render temperature with icon", async () => { - await expect(weatherFunc.getText(".weather .large span.light.bright", "1.5°")).resolves.toBe(true); - - const elem = await helpers.waitForElement(".weather .large span.weathericon"); - expect(elem).not.toBeNull(); + await expect(getText(".weather .large span.light.bright", "1.5°")).resolves.toBe(true); + await expect(page.locator(".weather .large span.weathericon")).toBeVisible(); }); it("should render feels like temperature", async () => { // Template contains   which renders as \xa0 - await expect(weatherFunc.getText(".weather .normal.medium.feelslike span.dimmed", "93.7\xa0 Feels like -5.6°")).resolves.toBe(true); + await expect(getText(".weather .normal.medium.feelslike span.dimmed", "93.7\xa0 Feels like -5.6°")).resolves.toBe(true); }); it("should render humidity next to feels-like", async () => { - await expect(weatherFunc.getText(".weather .normal.medium.feelslike span.dimmed .humidity", "93.7")).resolves.toBe(true); + await expect(getText(".weather .normal.medium.feelslike span.dimmed .humidity", "93.7")).resolves.toBe(true); }); }); }); @@ -37,57 +40,60 @@ describe("Weather module", () => { describe("Compliments Integration", () => { beforeAll(async () => { await weatherFunc.startApplication("tests/configs/modules/weather/currentweather_compliments.js", {}); + page = helpers.getPage(); }); it("should render a compliment based on the current weather", async () => { - await expect(weatherFunc.getText(".compliments .module-content span", "snow")).resolves.toBe(true); + await expect(getText(".compliments .module-content span", "snow")).resolves.toBe(true); }); }); describe("Configuration Options", () => { beforeAll(async () => { await weatherFunc.startApplication("tests/configs/modules/weather/currentweather_options.js", {}); + page = helpers.getPage(); }); it("should render windUnits in beaufort", async () => { - await expect(weatherFunc.getText(".weather .normal.medium span:nth-child(2)", "6")).resolves.toBe(true); + await expect(getText(".weather .normal.medium span:nth-child(2)", "6")).resolves.toBe(true); }); it("should render windDirection with an arrow", async () => { - const elem = await helpers.waitForElement(".weather .normal.medium sup i.fa-long-arrow-alt-down"); - expect(elem).not.toBeNull(); - const html = await elem.evaluate((node) => node.outerHTML); + const locator = page.locator(".weather .normal.medium sup i.fa-long-arrow-alt-down"); + await locator.waitFor({ state: "visible" }); + const html = await locator.evaluate((node) => node.outerHTML); expect(html).toContain("transform:rotate(250deg)"); }); it("should render humidity next to wind", async () => { - await expect(weatherFunc.getText(".weather .normal.medium .humidity", "93.7")).resolves.toBe(true); + await expect(getText(".weather .normal.medium .humidity", "93.7")).resolves.toBe(true); }); it("should render degreeLabel for temp", async () => { - await expect(weatherFunc.getText(".weather .large span.bright.light", "1°C")).resolves.toBe(true); + await expect(getText(".weather .large span.bright.light", "1°C")).resolves.toBe(true); }); it("should render degreeLabel for feels like", async () => { - await expect(weatherFunc.getText(".weather .normal.medium.feelslike span.dimmed", "Feels like -6°C")).resolves.toBe(true); + await expect(getText(".weather .normal.medium.feelslike span.dimmed", "Feels like -6°C")).resolves.toBe(true); }); }); describe("Current weather with imperial units", () => { beforeAll(async () => { await weatherFunc.startApplication("tests/configs/modules/weather/currentweather_units.js", {}); + page = helpers.getPage(); }); it("should render wind in imperial units", async () => { - await expect(weatherFunc.getText(".weather .normal.medium span:nth-child(2)", "26 WSW")).resolves.toBe(true); + await expect(getText(".weather .normal.medium span:nth-child(2)", "26 WSW")).resolves.toBe(true); }); it("should render temperatures in fahrenheit", async () => { - await expect(weatherFunc.getText(".weather .large span.bright.light", "34,7°")).resolves.toBe(true); + await expect(getText(".weather .large span.bright.light", "34,7°")).resolves.toBe(true); }); it("should render 'feels like' in fahrenheit", async () => { - await expect(weatherFunc.getText(".weather .normal.medium.feelslike span.dimmed", "Feels like 21,9°")).resolves.toBe(true); + await expect(getText(".weather .normal.medium.feelslike span.dimmed", "Feels like 21,9°")).resolves.toBe(true); }); }); }); diff --git a/tests/e2e/modules/weather_forecast_spec.js b/tests/e2e/modules/weather_forecast_spec.js index 3be7d68417..a2a6e9a2c3 100644 --- a/tests/e2e/modules/weather_forecast_spec.js +++ b/tests/e2e/modules/weather_forecast_spec.js @@ -1,7 +1,11 @@ +const { expect } = require("playwright/test"); const helpers = require("../helpers/global-setup"); const weatherFunc = require("../helpers/weather-functions"); describe("Weather module: Weather Forecast", () => { + let page; + const getText = (selector, expectedText) => weatherFunc.getText(page, selector, expectedText); + afterAll(async () => { await weatherFunc.stopApplication(); }); @@ -9,43 +13,43 @@ describe("Weather module: Weather Forecast", () => { describe("Default configuration", () => { beforeAll(async () => { await weatherFunc.startApplication("tests/configs/modules/weather/forecastweather_default.js", {}); + page = helpers.getPage(); }); const days = ["Today", "Tomorrow", "Sun", "Mon", "Tue"]; for (const [index, day] of days.entries()) { it(`should render day ${day}`, async () => { - await expect(weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day)).resolves.toBe(true); + await expect(getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day)).resolves.toBe(true); }); } const icons = ["day-cloudy", "rain", "day-sunny", "day-sunny", "day-sunny"]; for (const [index, icon] of icons.entries()) { it(`should render icon ${icon}`, async () => { - const elem = await helpers.waitForElement(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(2) span.wi-${icon}`); - expect(elem).not.toBeNull(); + await expect(page.locator(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(2) span.wi-${icon}`)).toBeVisible(); }); } const maxTemps = ["24.4°", "21.0°", "22.9°", "23.4°", "20.6°"]; for (const [index, temp] of maxTemps.entries()) { it(`should render max temperature ${temp}`, async () => { - await expect(weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`, temp)).resolves.toBe(true); + await expect(getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`, temp)).resolves.toBe(true); }); } const minTemps = ["15.3°", "13.6°", "13.8°", "13.9°", "10.9°"]; for (const [index, temp] of minTemps.entries()) { it(`should render min temperature ${temp}`, async () => { - await expect(weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(4)`, temp)).resolves.toBe(true); + await expect(getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(4)`, temp)).resolves.toBe(true); }); } const opacities = [1, 1, 0.8, 0.5333333333333333, 0.2666666666666667]; for (const [index, opacity] of opacities.entries()) { it(`should render fading of rows with opacity=${opacity}`, async () => { - const elem = await helpers.waitForElement(`.weather table.small tr:nth-child(${index + 1})`); - expect(elem).not.toBeNull(); - const html = await elem.evaluate((node) => node.outerHTML); + const locator = page.locator(`.weather table.small tr:nth-child(${index + 1})`); + await locator.waitFor({ state: "visible" }); + const html = await locator.evaluate((node) => node.outerHTML); expect(html).toContain(`style="opacity: ${opacity};"`); }); } @@ -54,12 +58,13 @@ describe("Weather module: Weather Forecast", () => { describe("Absolute configuration", () => { beforeAll(async () => { await weatherFunc.startApplication("tests/configs/modules/weather/forecastweather_absolute.js", {}); + page = helpers.getPage(); }); const days = ["Fri", "Sat", "Sun", "Mon", "Tue"]; for (const [index, day] of days.entries()) { it(`should render day ${day}`, async () => { - await expect(weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day)).resolves.toBe(true); + await expect(getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day)).resolves.toBe(true); }); } }); @@ -67,26 +72,23 @@ describe("Weather module: Weather Forecast", () => { describe("Configuration Options", () => { beforeAll(async () => { await weatherFunc.startApplication("tests/configs/modules/weather/forecastweather_options.js", {}); + page = helpers.getPage(); }); it("should render custom table class", async () => { - const elem = await helpers.waitForElement(".weather table.myTableClass"); - expect(elem).not.toBeNull(); + await expect(page.locator(".weather table.myTableClass")).toBeVisible(); }); it("should render colored rows", async () => { - const table = await helpers.waitForElement(".weather table.myTableClass"); - expect(table).not.toBeNull(); - const rows = table.locator("tr"); - await rows.first().waitFor({ state: "visible" }); - await expect(rows.count()).resolves.toBe(5); + const rows = page.locator(".weather table.myTableClass tr"); + await expect(rows).toHaveCount(5); }); const precipitations = [undefined, "2.51 mm"]; for (const [index, precipitation] of precipitations.entries()) { if (precipitation) { it(`should render precipitation amount ${precipitation}`, async () => { - await expect(weatherFunc.getText(`.weather table tr:nth-child(${index + 1}) td.precipitation-amount`, precipitation)).resolves.toBe(true); + await expect(getText(`.weather table tr:nth-child(${index + 1}) td.precipitation-amount`, precipitation)).resolves.toBe(true); }); } } @@ -95,13 +97,14 @@ describe("Weather module: Weather Forecast", () => { describe("Forecast weather with imperial units", () => { beforeAll(async () => { await weatherFunc.startApplication("tests/configs/modules/weather/forecastweather_units.js", {}); + page = helpers.getPage(); }); describe("Temperature units", () => { const temperatures = ["75_9°", "69_8°", "73_2°", "74_1°", "69_1°"]; for (const [index, temp] of temperatures.entries()) { it(`should render custom decimalSymbol = '_' for temp ${temp}`, async () => { - await expect(weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`, temp)).resolves.toBe(true); + await expect(getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`, temp)).resolves.toBe(true); }); } }); @@ -111,7 +114,7 @@ describe("Weather module: Weather Forecast", () => { for (const [index, precipitation] of precipitations.entries()) { if (precipitation) { it(`should render precipitation amount ${precipitation}`, async () => { - await expect(weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-amount`, precipitation)).resolves.toBe(true); + await expect(getText(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-amount`, precipitation)).resolves.toBe(true); }); } } diff --git a/tests/e2e/modules/weather_hourly_spec.js b/tests/e2e/modules/weather_hourly_spec.js index bce715bca0..def928cc41 100644 --- a/tests/e2e/modules/weather_hourly_spec.js +++ b/tests/e2e/modules/weather_hourly_spec.js @@ -1,6 +1,11 @@ +const { expect } = require("playwright/test"); +const helpers = require("../helpers/global-setup"); const weatherFunc = require("../helpers/weather-functions"); describe("Weather module: Weather Hourly Forecast", () => { + let page; + const getText = (selector, expectedText) => weatherFunc.getText(page, selector, expectedText); + afterAll(async () => { await weatherFunc.stopApplication(); }); @@ -8,12 +13,13 @@ describe("Weather module: Weather Hourly Forecast", () => { describe("Default configuration", () => { beforeAll(async () => { await weatherFunc.startApplication("tests/configs/modules/weather/hourlyweather_default.js", {}); + page = helpers.getPage(); }); const minTemps = ["7:00 pm", "8:00 pm", "9:00 pm", "10:00 pm", "11:00 pm"]; for (const [index, hour] of minTemps.entries()) { it(`should render forecast for hour ${hour}`, async () => { - await expect(weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td.day`, hour)).resolves.toBe(true); + await expect(getText(`.weather table.small tr:nth-child(${index + 1}) td.day`, hour)).resolves.toBe(true); }); } }); @@ -21,13 +27,14 @@ describe("Weather module: Weather Hourly Forecast", () => { describe("Hourly weather options", () => { beforeAll(async () => { await weatherFunc.startApplication("tests/configs/modules/weather/hourlyweather_options.js", {}); + page = helpers.getPage(); }); describe("Hourly increments of 2", () => { const minTemps = ["7:00 pm", "9:00 pm", "11:00 pm", "1:00 am", "3:00 am"]; for (const [index, hour] of minTemps.entries()) { it(`should render forecast for hour ${hour}`, async () => { - await expect(weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td.day`, hour)).resolves.toBe(true); + await expect(getText(`.weather table.small tr:nth-child(${index + 1}) td.day`, hour)).resolves.toBe(true); }); } }); @@ -36,6 +43,7 @@ describe("Weather module: Weather Hourly Forecast", () => { describe("Show precipitations", () => { beforeAll(async () => { await weatherFunc.startApplication("tests/configs/modules/weather/hourlyweather_showPrecipitation.js", {}); + page = helpers.getPage(); }); describe("Shows precipitation amount", () => { @@ -43,7 +51,7 @@ describe("Weather module: Weather Hourly Forecast", () => { for (const [index, amount] of amounts.entries()) { if (amount) { it(`should render precipitation amount ${amount}`, async () => { - await expect(weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-amount`, amount)).resolves.toBe(true); + await expect(getText(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-amount`, amount)).resolves.toBe(true); }); } } @@ -54,7 +62,7 @@ describe("Weather module: Weather Hourly Forecast", () => { for (const [index, pop] of probabilities.entries()) { if (pop) { it(`should render probability ${pop}`, async () => { - await expect(weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-prob`, pop)).resolves.toBe(true); + await expect(getText(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-prob`, pop)).resolves.toBe(true); }); } } diff --git a/tests/e2e/modules_display_spec.js b/tests/e2e/modules_display_spec.js index 76c26f23c4..698fe59262 100644 --- a/tests/e2e/modules_display_spec.js +++ b/tests/e2e/modules_display_spec.js @@ -1,9 +1,13 @@ +const { expect } = require("playwright/test"); const helpers = require("./helpers/global-setup"); describe("Display of modules", () => { + let page; + beforeAll(async () => { await helpers.startApplication("tests/configs/modules/display.js"); await helpers.getDocument(); + page = helpers.getPage(); }); afterAll(async () => { await helpers.stopApplication(); @@ -11,18 +15,10 @@ describe("Display of modules", () => { it("should show the test header", async () => { // textContent returns lowercase here, the uppercase is realized by CSS, which therefore does not end up in textContent - const elem = await helpers.waitForElement("#module_0_helloworld .module-header"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toBe("test_header"); + await expect(page.locator("#module_0_helloworld .module-header")).toHaveText("test_header"); }); it("should show no header if no header text is specified", async () => { - const elem = await helpers.waitForElement("#module_1_helloworld .module-header"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toBe("undefined"); + await expect(page.locator("#module_1_helloworld .module-header")).toHaveText("undefined"); }); }); diff --git a/tests/e2e/modules_empty_spec.js b/tests/e2e/modules_empty_spec.js index 7694bcf755..3957dba743 100644 --- a/tests/e2e/modules_empty_spec.js +++ b/tests/e2e/modules_empty_spec.js @@ -1,27 +1,23 @@ +const { expect } = require("playwright/test"); const helpers = require("./helpers/global-setup"); describe("Check configuration without modules", () => { + let page; + beforeAll(async () => { await helpers.startApplication("tests/configs/without_modules.js"); await helpers.getDocument(); + page = helpers.getPage(); }); afterAll(async () => { await helpers.stopApplication(); }); it("shows the message MagicMirror² title", async () => { - const elem = await helpers.waitForElement("#module_1_helloworld .module-content"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toContain("MagicMirror²"); + await expect(page.locator("#module_1_helloworld .module-content")).toContainText("MagicMirror²"); }); it("shows the project URL", async () => { - const elem = await helpers.waitForElement("#module_5_helloworld .module-content"); - expect(elem).not.toBeNull(); - const text = await elem.textContent(); - expect(text).not.toBeNull(); - expect(text).toContain("https://magicmirror.builders/"); + await expect(page.locator("#module_5_helloworld .module-content")).toContainText("https://magicmirror.builders/"); }); }); From 086634383420ae14fd60debf0b78fab789dfdf17 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:46:34 +0100 Subject: [PATCH 11/17] test(e2e): use native Playwright assertions in weather tests --- tests/e2e/helpers/weather-functions.js | 19 --------------- tests/e2e/modules/weather_current_spec.js | 27 +++++++++++----------- tests/e2e/modules/weather_forecast_spec.js | 15 ++++++------ tests/e2e/modules/weather_hourly_spec.js | 13 +++++++---- 4 files changed, 29 insertions(+), 45 deletions(-) diff --git a/tests/e2e/helpers/weather-functions.js b/tests/e2e/helpers/weather-functions.js index 99b0c8eeb1..6780ea42c1 100644 --- a/tests/e2e/helpers/weather-functions.js +++ b/tests/e2e/helpers/weather-functions.js @@ -1,25 +1,6 @@ const { injectMockData, cleanupMockData } = require("../../utils/weather_mocker"); const helpers = require("./global-setup"); -/** - * Get normalized text from element (trimmed, no line breaks, single spaces) - * @param {import("playwright").Page} page Playwright page instance - * @param {string} selector css selector - * @param {string} expectedText expected text content - * @returns {Promise} assertion outcome - */ -exports.getText = async (page, selector, expectedText) => { - const locator = page.locator(selector); - await locator.waitFor({ state: "visible" }); - const rawText = await locator.textContent(); - const content = rawText - .trim() - .replace(/(\r\n|\n|\r)/gm, "") - .replace(/[ ]+/g, " "); - expect(content).toBe(expectedText); - return true; -}; - exports.startApplication = async (configFileName, additionalMockData) => { await helpers.startApplication(injectMockData(configFileName, additionalMockData)); await helpers.getDocument(); diff --git a/tests/e2e/modules/weather_current_spec.js b/tests/e2e/modules/weather_current_spec.js index 4f26765d11..d6910680e8 100644 --- a/tests/e2e/modules/weather_current_spec.js +++ b/tests/e2e/modules/weather_current_spec.js @@ -4,7 +4,6 @@ const weatherFunc = require("../helpers/weather-functions"); describe("Weather module", () => { let page; - const getText = (selector, expectedText) => weatherFunc.getText(page, selector, expectedText); afterAll(async () => { await weatherFunc.stopApplication(); @@ -18,21 +17,21 @@ describe("Weather module", () => { }); it("should render wind speed and wind direction", async () => { - await expect(getText(".weather .normal.medium span:nth-child(2)", "12 WSW")).resolves.toBe(true); + await expect(page.locator(".weather .normal.medium span:nth-child(2)")).toHaveText("12 WSW"); }); it("should render temperature with icon", async () => { - await expect(getText(".weather .large span.light.bright", "1.5°")).resolves.toBe(true); + await expect(page.locator(".weather .large span.light.bright")).toHaveText("1.5°"); await expect(page.locator(".weather .large span.weathericon")).toBeVisible(); }); it("should render feels like temperature", async () => { // Template contains   which renders as \xa0 - await expect(getText(".weather .normal.medium.feelslike span.dimmed", "93.7\xa0 Feels like -5.6°")).resolves.toBe(true); + await expect(page.locator(".weather .normal.medium.feelslike span.dimmed")).toHaveText("93.7\xa0 Feels like -5.6°"); }); it("should render humidity next to feels-like", async () => { - await expect(getText(".weather .normal.medium.feelslike span.dimmed .humidity", "93.7")).resolves.toBe(true); + await expect(page.locator(".weather .normal.medium.feelslike span.dimmed .humidity")).toHaveText("93.7"); }); }); }); @@ -44,7 +43,9 @@ describe("Weather module", () => { }); it("should render a compliment based on the current weather", async () => { - await expect(getText(".compliments .module-content span", "snow")).resolves.toBe(true); + const compliment = page.locator(".compliments .module-content span"); + await compliment.waitFor({ state: "visible" }); + await expect(compliment).toHaveText("snow"); }); }); @@ -55,7 +56,7 @@ describe("Weather module", () => { }); it("should render windUnits in beaufort", async () => { - await expect(getText(".weather .normal.medium span:nth-child(2)", "6")).resolves.toBe(true); + await expect(page.locator(".weather .normal.medium span:nth-child(2)")).toHaveText("6"); }); it("should render windDirection with an arrow", async () => { @@ -66,15 +67,15 @@ describe("Weather module", () => { }); it("should render humidity next to wind", async () => { - await expect(getText(".weather .normal.medium .humidity", "93.7")).resolves.toBe(true); + await expect(page.locator(".weather .normal.medium .humidity")).toHaveText("93.7"); }); it("should render degreeLabel for temp", async () => { - await expect(getText(".weather .large span.bright.light", "1°C")).resolves.toBe(true); + await expect(page.locator(".weather .large span.bright.light")).toHaveText("1°C"); }); it("should render degreeLabel for feels like", async () => { - await expect(getText(".weather .normal.medium.feelslike span.dimmed", "Feels like -6°C")).resolves.toBe(true); + await expect(page.locator(".weather .normal.medium.feelslike span.dimmed")).toHaveText("Feels like -6°C"); }); }); @@ -85,15 +86,15 @@ describe("Weather module", () => { }); it("should render wind in imperial units", async () => { - await expect(getText(".weather .normal.medium span:nth-child(2)", "26 WSW")).resolves.toBe(true); + await expect(page.locator(".weather .normal.medium span:nth-child(2)")).toHaveText("26 WSW"); }); it("should render temperatures in fahrenheit", async () => { - await expect(getText(".weather .large span.bright.light", "34,7°")).resolves.toBe(true); + await expect(page.locator(".weather .large span.bright.light")).toHaveText("34,7°"); }); it("should render 'feels like' in fahrenheit", async () => { - await expect(getText(".weather .normal.medium.feelslike span.dimmed", "Feels like 21,9°")).resolves.toBe(true); + await expect(page.locator(".weather .normal.medium.feelslike span.dimmed")).toHaveText("Feels like 21,9°"); }); }); }); diff --git a/tests/e2e/modules/weather_forecast_spec.js b/tests/e2e/modules/weather_forecast_spec.js index a2a6e9a2c3..a64fab7697 100644 --- a/tests/e2e/modules/weather_forecast_spec.js +++ b/tests/e2e/modules/weather_forecast_spec.js @@ -4,7 +4,6 @@ const weatherFunc = require("../helpers/weather-functions"); describe("Weather module: Weather Forecast", () => { let page; - const getText = (selector, expectedText) => weatherFunc.getText(page, selector, expectedText); afterAll(async () => { await weatherFunc.stopApplication(); @@ -19,7 +18,7 @@ describe("Weather module: Weather Forecast", () => { const days = ["Today", "Tomorrow", "Sun", "Mon", "Tue"]; for (const [index, day] of days.entries()) { it(`should render day ${day}`, async () => { - await expect(getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day)).resolves.toBe(true); + await expect(page.locator(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`)).toHaveText(day); }); } @@ -33,14 +32,14 @@ describe("Weather module: Weather Forecast", () => { const maxTemps = ["24.4°", "21.0°", "22.9°", "23.4°", "20.6°"]; for (const [index, temp] of maxTemps.entries()) { it(`should render max temperature ${temp}`, async () => { - await expect(getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`, temp)).resolves.toBe(true); + await expect(page.locator(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`)).toHaveText(temp); }); } const minTemps = ["15.3°", "13.6°", "13.8°", "13.9°", "10.9°"]; for (const [index, temp] of minTemps.entries()) { it(`should render min temperature ${temp}`, async () => { - await expect(getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(4)`, temp)).resolves.toBe(true); + await expect(page.locator(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(4)`)).toHaveText(temp); }); } @@ -64,7 +63,7 @@ describe("Weather module: Weather Forecast", () => { const days = ["Fri", "Sat", "Sun", "Mon", "Tue"]; for (const [index, day] of days.entries()) { it(`should render day ${day}`, async () => { - await expect(getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day)).resolves.toBe(true); + await expect(page.locator(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`)).toHaveText(day); }); } }); @@ -88,7 +87,7 @@ describe("Weather module: Weather Forecast", () => { for (const [index, precipitation] of precipitations.entries()) { if (precipitation) { it(`should render precipitation amount ${precipitation}`, async () => { - await expect(getText(`.weather table tr:nth-child(${index + 1}) td.precipitation-amount`, precipitation)).resolves.toBe(true); + await expect(page.locator(`.weather table tr:nth-child(${index + 1}) td.precipitation-amount`)).toHaveText(precipitation); }); } } @@ -104,7 +103,7 @@ describe("Weather module: Weather Forecast", () => { const temperatures = ["75_9°", "69_8°", "73_2°", "74_1°", "69_1°"]; for (const [index, temp] of temperatures.entries()) { it(`should render custom decimalSymbol = '_' for temp ${temp}`, async () => { - await expect(getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`, temp)).resolves.toBe(true); + await expect(page.locator(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`)).toHaveText(temp); }); } }); @@ -114,7 +113,7 @@ describe("Weather module: Weather Forecast", () => { for (const [index, precipitation] of precipitations.entries()) { if (precipitation) { it(`should render precipitation amount ${precipitation}`, async () => { - await expect(getText(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-amount`, precipitation)).resolves.toBe(true); + await expect(page.locator(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-amount`)).toHaveText(precipitation); }); } } diff --git a/tests/e2e/modules/weather_hourly_spec.js b/tests/e2e/modules/weather_hourly_spec.js index def928cc41..0d63729d14 100644 --- a/tests/e2e/modules/weather_hourly_spec.js +++ b/tests/e2e/modules/weather_hourly_spec.js @@ -4,7 +4,6 @@ const weatherFunc = require("../helpers/weather-functions"); describe("Weather module: Weather Hourly Forecast", () => { let page; - const getText = (selector, expectedText) => weatherFunc.getText(page, selector, expectedText); afterAll(async () => { await weatherFunc.stopApplication(); @@ -19,7 +18,8 @@ describe("Weather module: Weather Hourly Forecast", () => { const minTemps = ["7:00 pm", "8:00 pm", "9:00 pm", "10:00 pm", "11:00 pm"]; for (const [index, hour] of minTemps.entries()) { it(`should render forecast for hour ${hour}`, async () => { - await expect(getText(`.weather table.small tr:nth-child(${index + 1}) td.day`, hour)).resolves.toBe(true); + const dayCell = page.locator(`.weather table.small tr:nth-child(${index + 1}) td.day`); + await expect(dayCell).toHaveText(hour); }); } }); @@ -34,7 +34,8 @@ describe("Weather module: Weather Hourly Forecast", () => { const minTemps = ["7:00 pm", "9:00 pm", "11:00 pm", "1:00 am", "3:00 am"]; for (const [index, hour] of minTemps.entries()) { it(`should render forecast for hour ${hour}`, async () => { - await expect(getText(`.weather table.small tr:nth-child(${index + 1}) td.day`, hour)).resolves.toBe(true); + const dayCell = page.locator(`.weather table.small tr:nth-child(${index + 1}) td.day`); + await expect(dayCell).toHaveText(hour); }); } }); @@ -51,7 +52,8 @@ describe("Weather module: Weather Hourly Forecast", () => { for (const [index, amount] of amounts.entries()) { if (amount) { it(`should render precipitation amount ${amount}`, async () => { - await expect(getText(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-amount`, amount)).resolves.toBe(true); + const amountCell = page.locator(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-amount`); + await expect(amountCell).toHaveText(amount); }); } } @@ -62,7 +64,8 @@ describe("Weather module: Weather Hourly Forecast", () => { for (const [index, pop] of probabilities.entries()) { if (pop) { it(`should render probability ${pop}`, async () => { - await expect(getText(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-prob`, pop)).resolves.toBe(true); + const probabilityCell = page.locator(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-prob`); + await expect(probabilityCell).toHaveText(pop); }); } } From c584a1bd03ebaf5bb798052be52ff0b197f61523 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:46:34 +0100 Subject: [PATCH 12/17] test(e2e): use native Playwright assertions for style and opacity checks --- tests/e2e/modules/weather_current_spec.js | 6 ++--- tests/e2e/modules/weather_forecast_spec.js | 30 +++++++++++++--------- tests/e2e/modules/weather_hourly_spec.js | 8 +++--- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/tests/e2e/modules/weather_current_spec.js b/tests/e2e/modules/weather_current_spec.js index d6910680e8..9b2928792e 100644 --- a/tests/e2e/modules/weather_current_spec.js +++ b/tests/e2e/modules/weather_current_spec.js @@ -60,10 +60,8 @@ describe("Weather module", () => { }); it("should render windDirection with an arrow", async () => { - const locator = page.locator(".weather .normal.medium sup i.fa-long-arrow-alt-down"); - await locator.waitFor({ state: "visible" }); - const html = await locator.evaluate((node) => node.outerHTML); - expect(html).toContain("transform:rotate(250deg)"); + const arrow = page.locator(".weather .normal.medium sup i.fa-long-arrow-alt-down"); + await expect(arrow).toHaveAttribute("style", "transform:rotate(250deg)"); }); it("should render humidity next to wind", async () => { diff --git a/tests/e2e/modules/weather_forecast_spec.js b/tests/e2e/modules/weather_forecast_spec.js index a64fab7697..011ed35f49 100644 --- a/tests/e2e/modules/weather_forecast_spec.js +++ b/tests/e2e/modules/weather_forecast_spec.js @@ -18,38 +18,40 @@ describe("Weather module: Weather Forecast", () => { const days = ["Today", "Tomorrow", "Sun", "Mon", "Tue"]; for (const [index, day] of days.entries()) { it(`should render day ${day}`, async () => { - await expect(page.locator(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`)).toHaveText(day); + const dayCell = page.locator(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`); + await expect(dayCell).toHaveText(day); }); } const icons = ["day-cloudy", "rain", "day-sunny", "day-sunny", "day-sunny"]; for (const [index, icon] of icons.entries()) { it(`should render icon ${icon}`, async () => { - await expect(page.locator(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(2) span.wi-${icon}`)).toBeVisible(); + const iconElement = page.locator(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(2) span.wi-${icon}`); + await expect(iconElement).toBeVisible(); }); } const maxTemps = ["24.4°", "21.0°", "22.9°", "23.4°", "20.6°"]; for (const [index, temp] of maxTemps.entries()) { it(`should render max temperature ${temp}`, async () => { - await expect(page.locator(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`)).toHaveText(temp); + const maxTempCell = page.locator(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`); + await expect(maxTempCell).toHaveText(temp); }); } const minTemps = ["15.3°", "13.6°", "13.8°", "13.9°", "10.9°"]; for (const [index, temp] of minTemps.entries()) { it(`should render min temperature ${temp}`, async () => { - await expect(page.locator(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(4)`)).toHaveText(temp); + const minTempCell = page.locator(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(4)`); + await expect(minTempCell).toHaveText(temp); }); } const opacities = [1, 1, 0.8, 0.5333333333333333, 0.2666666666666667]; for (const [index, opacity] of opacities.entries()) { it(`should render fading of rows with opacity=${opacity}`, async () => { - const locator = page.locator(`.weather table.small tr:nth-child(${index + 1})`); - await locator.waitFor({ state: "visible" }); - const html = await locator.evaluate((node) => node.outerHTML); - expect(html).toContain(`style="opacity: ${opacity};"`); + const row = page.locator(`.weather table.small tr:nth-child(${index + 1})`); + await expect(row).toHaveAttribute("style", `opacity: ${opacity};`); }); } }); @@ -63,7 +65,8 @@ describe("Weather module: Weather Forecast", () => { const days = ["Fri", "Sat", "Sun", "Mon", "Tue"]; for (const [index, day] of days.entries()) { it(`should render day ${day}`, async () => { - await expect(page.locator(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`)).toHaveText(day); + const dayCell = page.locator(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`); + await expect(dayCell).toHaveText(day); }); } }); @@ -87,7 +90,8 @@ describe("Weather module: Weather Forecast", () => { for (const [index, precipitation] of precipitations.entries()) { if (precipitation) { it(`should render precipitation amount ${precipitation}`, async () => { - await expect(page.locator(`.weather table tr:nth-child(${index + 1}) td.precipitation-amount`)).toHaveText(precipitation); + const precipCell = page.locator(`.weather table tr:nth-child(${index + 1}) td.precipitation-amount`); + await expect(precipCell).toHaveText(precipitation); }); } } @@ -103,7 +107,8 @@ describe("Weather module: Weather Forecast", () => { const temperatures = ["75_9°", "69_8°", "73_2°", "74_1°", "69_1°"]; for (const [index, temp] of temperatures.entries()) { it(`should render custom decimalSymbol = '_' for temp ${temp}`, async () => { - await expect(page.locator(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`)).toHaveText(temp); + const tempCell = page.locator(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`); + await expect(tempCell).toHaveText(temp); }); } }); @@ -113,7 +118,8 @@ describe("Weather module: Weather Forecast", () => { for (const [index, precipitation] of precipitations.entries()) { if (precipitation) { it(`should render precipitation amount ${precipitation}`, async () => { - await expect(page.locator(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-amount`)).toHaveText(precipitation); + const precipCell = page.locator(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-amount`); + await expect(precipCell).toHaveText(precipitation); }); } } diff --git a/tests/e2e/modules/weather_hourly_spec.js b/tests/e2e/modules/weather_hourly_spec.js index 0d63729d14..a33503f3b2 100644 --- a/tests/e2e/modules/weather_hourly_spec.js +++ b/tests/e2e/modules/weather_hourly_spec.js @@ -61,11 +61,11 @@ describe("Weather module: Weather Hourly Forecast", () => { describe("Shows precipitation probability", () => { const probabilities = [undefined, undefined, "12 %", "36 %", "44 %"]; - for (const [index, pop] of probabilities.entries()) { - if (pop) { - it(`should render probability ${pop}`, async () => { + for (const [index, probability] of probabilities.entries()) { + if (probability) { + it(`should render probability ${probability}`, async () => { const probabilityCell = page.locator(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-prob`); - await expect(probabilityCell).toHaveText(pop); + await expect(probabilityCell).toHaveText(probability); }); } } From df504798d6110580e3075bc8eba5e2fd4940c167 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:46:34 +0100 Subject: [PATCH 13/17] test(e2e): simplify E2E test assertions Remove unnecessary .resolves.toBe(true) wrappers from helper functions and configure ESLint to recognize custom assertion helpers. Functions already throw on failure via Playwright's expect(). --- eslint.config.mjs | 14 +++++++++ tests/e2e/animateCSS_spec.js | 16 +++++----- tests/e2e/modules/calendar_spec.js | 44 +++++++++++++-------------- tests/e2e/modules/compliments_spec.js | 21 ++++++------- tests/e2e/serveronly_spec.js | 6 ++-- 5 files changed, 56 insertions(+), 45 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 5b9b0e24e9..75df4fb8c7 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -59,6 +59,20 @@ export default defineConfig([ "import-x/order": "error", "init-declarations": "off", "vitest/consistent-test-it": "warn", + "vitest/expect-expect": [ + "warn", + { + assertFunctionNames: [ + "expect", + "testElementLength", + "testTextContain", + "doTest", + "runAnimationTest", + "waitForAnimationClass", + "assertNoAnimationWithin" + ] + } + ], "vitest/prefer-to-be": "warn", "vitest/prefer-to-have-length": "warn", "max-lines-per-function": ["warn", 400], diff --git a/tests/e2e/animateCSS_spec.js b/tests/e2e/animateCSS_spec.js index 4a3e4b3fc7..eed01f4926 100644 --- a/tests/e2e/animateCSS_spec.js +++ b/tests/e2e/animateCSS_spec.js @@ -26,7 +26,7 @@ describe("AnimateCSS integration Test", () => { * Wait for an Animate.css class to appear and persist briefly. * @param {string} cls Animation class name without leading dot (e.g. animate__flipInX) * @param {{timeout?: number}} [options] Poll timeout in ms (default 6000) - * @returns {Promise} true if class detected in time + * @returns {Promise} */ async function waitForAnimationClass (cls, { timeout = 6000 } = {}) { const locator = page.locator(`.compliments.animate__animated.${cls}`); @@ -34,7 +34,6 @@ describe("AnimateCSS integration Test", () => { // small stability wait await new Promise((r) => setTimeout(r, 50)); await expect(locator).toBeAttached(); - return true; } /** @@ -58,13 +57,13 @@ describe("AnimateCSS integration Test", () => { * Run one animation test scenario. * @param {string} [animationIn] Expected animate-in name * @param {string} [animationOut] Expected animate-out name - * @returns {Promise} true when scenario assertions pass + * @returns {Promise} Throws on assertion failure */ async function runAnimationTest (animationIn, animationOut) { await getComplimentsElement(); if (!animationIn && !animationOut) { await assertNoAnimationWithin(2000); - return true; + return; } if (animationIn) await waitForAnimationClass(`animate__${animationIn}`); if (animationOut) { @@ -72,7 +71,6 @@ describe("AnimateCSS integration Test", () => { await new Promise((r) => setTimeout(r, 2100)); await waitForAnimationClass(`animate__${animationOut}`); } - return true; } afterEach(async () => { @@ -82,28 +80,28 @@ describe("AnimateCSS integration Test", () => { describe("animateIn and animateOut Test", () => { it("with flipInX and flipOutX animation", async () => { await helpers.startApplication(TEST_CONFIG_ANIM); - await expect(runAnimationTest("flipInX", "flipOutX")).resolves.toBe(true); + await runAnimationTest("flipInX", "flipOutX"); }); }); describe("use animateOut name for animateIn (vice versa) Test", () => { it("without animation (inverted names)", async () => { await helpers.startApplication(TEST_CONFIG_INVERTED); - await expect(runAnimationTest()).resolves.toBe(true); + await runAnimationTest(); }); }); describe("false Animation name test", () => { it("without animation (invalid names)", async () => { await helpers.startApplication(TEST_CONFIG_FALLBACK); - await expect(runAnimationTest()).resolves.toBe(true); + await runAnimationTest(); }); }); describe("no Animation defined test", () => { it("without animation (no config)", async () => { await helpers.startApplication(TEST_CONFIG_NONE); - await expect(runAnimationTest()).resolves.toBe(true); + await runAnimationTest(); }); }); }); diff --git a/tests/e2e/modules/calendar_spec.js b/tests/e2e/modules/calendar_spec.js index 4b3a5d6ed1..d24bae110b 100644 --- a/tests/e2e/modules/calendar_spec.js +++ b/tests/e2e/modules/calendar_spec.js @@ -10,7 +10,7 @@ describe("Calendar module", () => { * @param {string} selector css selector * @param {number} expectedLength expected number of elements * @param {string} [not] optional negation marker (use "not" to negate) - * @returns {Promise} assertion outcome + * @returns {Promise} */ const testElementLength = async (selector, expectedLength, not) => { const locator = page.locator(selector); @@ -19,12 +19,10 @@ describe("Calendar module", () => { } else { await expect(locator).toHaveCount(expectedLength); } - return true; }; const testTextContain = async (selector, expectedText) => { await expect(page.locator(selector).first()).toContainText(expectedText); - return true; }; afterAll(async () => { @@ -39,11 +37,11 @@ describe("Calendar module", () => { }); it("should show the default maximumEntries of 10", async () => { - await expect(testElementLength(".calendar .event", 10)).resolves.toBe(true); + await testElementLength(".calendar .event", 10); }); it("should show the default calendar symbol in each event", async () => { - await expect(testElementLength(".calendar .event .fa-calendar-days", 0, "not")).resolves.toBe(true); + await testElementLength(".calendar .event .fa-calendar-days", 0, "not"); }); }); @@ -55,27 +53,27 @@ describe("Calendar module", () => { }); it("should show the custom maximumEntries of 5", async () => { - await expect(testElementLength(".calendar .event", 5)).resolves.toBe(true); + await testElementLength(".calendar .event", 5); }); it("should show the custom calendar symbol in four events", async () => { - await expect(testElementLength(".calendar .event .fa-birthday-cake", 4)).resolves.toBe(true); + await testElementLength(".calendar .event .fa-birthday-cake", 4); }); it("should show a customEvent calendar symbol in one event", async () => { - await expect(testElementLength(".calendar .event .fa-dice", 1)).resolves.toBe(true); + await testElementLength(".calendar .event .fa-dice", 1); }); it("should show a customEvent calendar eventClass in one event", async () => { - await expect(testElementLength(".calendar .event.undo", 1)).resolves.toBe(true); + await testElementLength(".calendar .event.undo", 1); }); it("should show two custom icons for repeating events", async () => { - await expect(testElementLength(".calendar .event .fa-undo", 2)).resolves.toBe(true); + await testElementLength(".calendar .event .fa-undo", 2); }); it("should show two custom icons for day events", async () => { - await expect(testElementLength(".calendar .event .fa-calendar-day", 2)).resolves.toBe(true); + await testElementLength(".calendar .event .fa-calendar-day", 2); }); }); @@ -87,7 +85,7 @@ describe("Calendar module", () => { }); it("should show the recurring birthday event 6 times", async () => { - await expect(testElementLength(".calendar .event", 6)).resolves.toBe(true); + await testElementLength(".calendar .event", 6); }); }); @@ -100,12 +98,12 @@ describe("Calendar module", () => { }); it("should contain text 'Ends in' with the left days", async () => { - await expect(testTextContain(".calendar .today .time", "Ends in")).resolves.toBe(true); - await expect(testTextContain(".calendar .yesterday .time", "Today")).resolves.toBe(true); - await expect(testTextContain(".calendar .tomorrow .time", "Tomorrow")).resolves.toBe(true); + await testTextContain(".calendar .today .time", "Ends in"); + await testTextContain(".calendar .yesterday .time", "Today"); + await testTextContain(".calendar .tomorrow .time", "Tomorrow"); }); it("should contain in total three events", async () => { - await expect(testElementLength(".calendar .event", 3)).resolves.toBe(true); + await testElementLength(".calendar .event", 3); }); }); @@ -117,10 +115,10 @@ describe("Calendar module", () => { }); it("should contain text 'Today'", async () => { - await expect(testTextContain(".calendar .time", "Today")).resolves.toBe(true); + await testTextContain(".calendar .time", "Today"); }); it("should contain in total two events", async () => { - await expect(testElementLength(".calendar .event", 2)).resolves.toBe(true); + await testElementLength(".calendar .event", 2); }); }); @@ -137,7 +135,7 @@ describe("Calendar module", () => { }); it("should return TestEvents", async () => { - await expect(testElementLength(".calendar .event", 0, "not")).resolves.toBe(true); + await testElementLength(".calendar .event", 0, "not"); }); }); @@ -149,7 +147,7 @@ describe("Calendar module", () => { }); it("should return TestEvents", async () => { - await expect(testElementLength(".calendar .event", 0, "not")).resolves.toBe(true); + await testElementLength(".calendar .event", 0, "not"); }); }); @@ -161,7 +159,7 @@ describe("Calendar module", () => { }); it("should return TestEvents", async () => { - await expect(testElementLength(".calendar .event", 0, "not")).resolves.toBe(true); + await testElementLength(".calendar .event", 0, "not"); }); }); @@ -173,7 +171,7 @@ describe("Calendar module", () => { }); it("should return TestEvents", async () => { - await expect(testElementLength(".calendar .event", 0, "not")).resolves.toBe(true); + await testElementLength(".calendar .event", 0, "not"); }); }); @@ -190,7 +188,7 @@ describe("Calendar module", () => { }); it("should show Unauthorized error", async () => { - await expect(testTextContain(".calendar", "Error in the calendar module. Authorization failed")).resolves.toBe(true); + await testTextContain(".calendar", "Error in the calendar module. Authorization failed"); }); }); }); diff --git a/tests/e2e/modules/compliments_spec.js b/tests/e2e/modules/compliments_spec.js index e50d1e5895..bb8f13a33b 100644 --- a/tests/e2e/modules/compliments_spec.js +++ b/tests/e2e/modules/compliments_spec.js @@ -7,7 +7,7 @@ describe("Compliments module", () => { /** * move similar tests in function doTest * @param {Array} complimentsArray The array of compliments. - * @returns {boolean} result + * @returns {Promise} */ const doTest = async (complimentsArray) => { await expect(page.locator(".compliments")).toBeVisible(); @@ -15,7 +15,6 @@ describe("Compliments module", () => { await contentLocator.waitFor({ state: "visible" }); const content = await contentLocator.textContent(); expect(complimentsArray).toContain(content); - return true; }; afterAll(async () => { @@ -31,7 +30,7 @@ describe("Compliments module", () => { }); it("shows anytime because if configure empty parts of day compliments and set anytime compliments", async () => { - await expect(doTest(["Anytime here"])).resolves.toBe(true); + await doTest(["Anytime here"]); }); }); @@ -43,7 +42,7 @@ describe("Compliments module", () => { }); it("shows anytime compliments", async () => { - await expect(doTest(["Anytime here"])).resolves.toBe(true); + await doTest(["Anytime here"]); }); }); }); @@ -56,7 +55,7 @@ describe("Compliments module", () => { }); it("should show compliments from a remote file", async () => { - await expect(doTest(["Remote compliment file works!"])).resolves.toBe(true); + await doTest(["Remote compliment file works!"]); }); }); @@ -69,7 +68,7 @@ describe("Compliments module", () => { }); it("compliments array can contain all values", async () => { - await expect(doTest(["Special day message", "Typical message 1", "Typical message 2", "Typical message 3"])).resolves.toBe(true); + await doTest(["Special day message", "Typical message 1", "Typical message 2", "Typical message 3"]); }); }); @@ -81,7 +80,7 @@ describe("Compliments module", () => { }); it("compliments array contains only special value", async () => { - await expect(doTest(["Special day message"])).resolves.toBe(true); + await doTest(["Special day message"]); }); }); @@ -93,7 +92,7 @@ describe("Compliments module", () => { }); it("compliments array contains only special value", async () => { - await expect(doTest(["anytime cron"])).resolves.toBe(true); + await doTest(["anytime cron"]); }); }); }); @@ -107,7 +106,7 @@ describe("Compliments module", () => { }); it("shows 'Remote compliment file works!' as only anytime list set", async () => { //await helpers.startApplication("tests/configs/modules/compliments/compliments_file.js", "01 Jan 2022 10:00:00 GMT"); - await expect(doTest(["Remote compliment file works!"])).resolves.toBe(true); + await doTest(["Remote compliment file works!"]); }); // afterAll(async () =>{ // await helpers.stopApplication() @@ -122,9 +121,9 @@ describe("Compliments module", () => { }); it("shows 'test in morning' as test time set to 10am", async () => { //await helpers.startApplication("tests/configs/modules/compliments/compliments_file_change.js", "01 Jan 2022 10:00:00 GMT"); - await expect(doTest(["Remote compliment file works!"])).resolves.toBe(true); + await doTest(["Remote compliment file works!"]); await new Promise((r) => setTimeout(r, 10000)); - await expect(doTest(["test in morning"])).resolves.toBe(true); + await doTest(["test in morning"]); }); // afterAll(async () =>{ // await helpers.stopApplication() diff --git a/tests/e2e/serveronly_spec.js b/tests/e2e/serveronly_spec.js index f48d152bf6..dbad0daa99 100644 --- a/tests/e2e/serveronly_spec.js +++ b/tests/e2e/serveronly_spec.js @@ -38,11 +38,13 @@ describe("App environment", () => { describe("Check config", () => { it("config check should return without errors", async () => { process.env.MM_CONFIG_FILE = "tests/configs/default.js"; - await expect(runConfigCheck()).resolves.toBe(0); + const exitCode = await runConfigCheck(); + expect(exitCode).toBe(0); }); it("config check should fail with non existent config file", async () => { process.env.MM_CONFIG_FILE = "tests/configs/not_exists.js"; - await expect(runConfigCheck()).resolves.toBe(1); + const exitCode = await runConfigCheck(); + expect(exitCode).toBe(1); }); }); From c765aaab544ea7be621a4d05c9ee5ee9bfe3f786 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:46:34 +0100 Subject: [PATCH 14/17] test(e2e): add Playwright linting and handle detected issue --- eslint.config.mjs | 8 ++++++++ package-lock.json | 30 ++++++++++++++++++++++++++++++ package.json | 1 + tests/e2e/env_spec.js | 3 +-- 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 75df4fb8c7..897777943a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,6 +4,7 @@ import {flatConfigs as importX} from "eslint-plugin-import-x"; import js from "@eslint/js"; import jsdocPlugin from "eslint-plugin-jsdoc"; import packageJson from "eslint-plugin-package-json"; +import playwright from "eslint-plugin-playwright"; import stylistic from "@stylistic/eslint-plugin"; import vitest from "eslint-plugin-vitest"; @@ -139,5 +140,12 @@ export default defineConfig([ rules: { "@stylistic/quotes": "off" } + }, + { + files: ["tests/e2e/**/*.js"], + extends: [playwright.configs["flat/recommended"]], + rules: { + "playwright/no-standalone-expect": "off" + } } ]); diff --git a/package-lock.json b/package-lock.json index ce7729f1bf..79658bd2a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "eslint-plugin-import-x": "^4.16.1", "eslint-plugin-jsdoc": "^61.1.11", "eslint-plugin-package-json": "^0.59.1", + "eslint-plugin-playwright": "^2.3.0", "eslint-plugin-vitest": "^0.5.4", "express-basic-auth": "^1.2.1", "husky": "^9.1.7", @@ -5496,6 +5497,35 @@ "jsonc-eslint-parser": "^2.0.0" } }, + "node_modules/eslint-plugin-playwright": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-2.3.0.tgz", + "integrity": "sha512-7UeUuIb5SZrNkrUGb2F+iwHM97kn33/huajcVtAaQFCSMUYGNFvjzRPil5C0OIppslPfuOV68M/zsisXx+/ZvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "globals": "^16.4.0" + }, + "engines": { + "node": ">=16.9.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/eslint-plugin-playwright/node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-plugin-vitest": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/eslint-plugin-vitest/-/eslint-plugin-vitest-0.5.4.tgz", diff --git a/package.json b/package.json index 58cefcdedd..2292ca809a 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "eslint-plugin-import-x": "^4.16.1", "eslint-plugin-jsdoc": "^61.1.11", "eslint-plugin-package-json": "^0.59.1", + "eslint-plugin-playwright": "^2.3.0", "eslint-plugin-vitest": "^0.5.4", "express-basic-auth": "^1.2.1", "husky": "^9.1.7", diff --git a/tests/e2e/env_spec.js b/tests/e2e/env_spec.js index 03f5f76007..5e642ed476 100644 --- a/tests/e2e/env_spec.js +++ b/tests/e2e/env_spec.js @@ -24,7 +24,6 @@ describe("App environment", () => { }); it("should show the title MagicMirror²", async () => { - const title = await page.locator("title").textContent(); - expect(title).toBe("MagicMirror²"); + await expect(page).toHaveTitle("MagicMirror²"); }); }); From af21fcd9372d1beb27a16b726268342def15ee10 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:46:35 +0100 Subject: [PATCH 15/17] test: configure Vitest projects for optimized test timeouts --- vitest.config.mjs | 59 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/vitest.config.mjs b/vitest.config.mjs index 45e061b943..d8957c739e 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -8,28 +8,21 @@ import {defineConfig} from "vitest/config"; * * Parallel execution would require dynamic ports and isolated fixtures, * so we intentionally cap Vitest at a single worker for now. + * + * Projects separate unit, e2e (Playwright), and electron tests with + * appropriate timeouts for each test type. */ export default defineConfig({ test: { - // Global settings + // Shared settings for all test types globals: true, environment: "node", - // Setup files for require aliasing setupFiles: ["./tests/utils/vitest-setup.js"], - // Increased from 20s to 60s for E2E tests, 120s for Electron tests - testTimeout: 120000, - // Increase hook timeout for Electron cleanup - hookTimeout: 30000, // Stop test execution on first failure - bail: 1, + bail: 3, - // File patterns - include: [ - "tests/**/*_spec.js", - // Legacy regression test without the _spec suffix - "tests/unit/modules/default/calendar/calendar_fetcher_utils_bad_rrule.js" - ], + // Shared exclude patterns exclude: [ "**/node_modules/**", "**/dist/**", @@ -42,6 +35,46 @@ export default defineConfig({ "tests/utils/**" ], + // Projects with specific configurations per test type + projects: [ + { + test: { + name: "unit", + globals: true, + environment: "node", + setupFiles: ["./tests/utils/vitest-setup.js"], + include: [ + "tests/unit/**/*_spec.js", + "tests/unit/modules/default/calendar/calendar_fetcher_utils_bad_rrule.js" + ], + testTimeout: 20000, + hookTimeout: 10000 + } + }, + { + test: { + name: "e2e", + globals: true, + environment: "node", + setupFiles: ["./tests/utils/vitest-setup.js"], + include: ["tests/e2e/**/*_spec.js"], + testTimeout: 60000, + hookTimeout: 30000 + } + }, + { + test: { + name: "electron", + globals: true, + environment: "node", + setupFiles: ["./tests/utils/vitest-setup.js"], + include: ["tests/electron/**/*_spec.js"], + testTimeout: 120000, + hookTimeout: 30000 + } + } + ], + // Coverage configuration coverage: { provider: "v8", From 17c5629b3c85862e487c493d90a78cb1238f19a5 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:46:35 +0100 Subject: [PATCH 16/17] refactor(loader): use URL constructor for robust env endpoint fetch --- js/loader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/loader.js b/js/loader.js index 49b0b4fc9b..b3cfd71910 100644 --- a/js/loader.js +++ b/js/loader.js @@ -33,7 +33,7 @@ const Loader = (function () { // In production, fetch env vars from server try { - const res = await fetch(`${config.basePath}env`); + const res = await fetch(new URL("env", `${location.origin}${config.basePath}`)); return JSON.parse(await res.text()); } catch (error) { // Fallback to config values if server fetch fails From 98a256efe9798d1b7c7dd21c69fed5a7741f20cd Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:49:14 +0100 Subject: [PATCH 17/17] [tests] add auto-install Playwright browser in `install-mm:dev` script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2292ca809a..9a6ecd17d1 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "config:check": "node js/check_config.js", "postinstall": "git clean -df fonts vendor", "install-mm": "npm install --no-audit --no-fund --no-update-notifier --only=prod --omit=dev", - "install-mm:dev": "npm install --no-audit --no-fund --no-update-notifier", + "install-mm:dev": "npm install --no-audit --no-fund --no-update-notifier && npx playwright install chromium", "lint:css": "stylelint 'css/main.css' 'css/roboto.css' 'css/font-awesome.css' 'modules/default/**/*.css' --fix", "lint:js": "eslint --fix", "lint:markdown": "markdownlint-cli2 . --fix",