mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-12-12 09:52:37 +00:00
[core] refactor: replace XMLHttpRequest with fetch and migrate e2e tests to Playwright (#3950)
### 1. Replace `XMLHttpRequest` with the modern `fetch` API for loading translation files #### Changes - **translator.js**: Use `fetch` with `async/await` instead of XHR callbacks - **loader.js**: Align URL handling and add error handling (follow-up to fetch migration) - **Tests**: Update infrastructure for `fetch` compatibility #### Benefits - Modern standard API - Cleaner, more readable code - Better error handling and fallback mechanisms ### 2. Migrate e2e tests to Playwright This wasn't originally planned for this PR, but is related. While investigating suspicious log entries which surfaced after the fetch migration I kept running into JSDOM’s limitations. That pushed me to migrate the E2E suite to Playwright instead. #### Changes - switch e2e harness to Playwright (`tests/e2e/helpers/global-setup.js`) - rewrite specs to use Playwright locators + shared `expectTextContent` - install Chromium via `npx playwright install --with-deps` in CI #### Benefits - much closer to real browser behaviour - and no more fighting JSDOM’s quirks
This commit is contained in:
committed by
GitHub
parent
2b08288346
commit
f29f424a62
@@ -1,7 +1,7 @@
|
||||
const path = require("node:path");
|
||||
const os = require("node:os");
|
||||
const fs = require("node:fs");
|
||||
const jsdom = require("jsdom");
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
// global absolute root path
|
||||
global.root_path = path.resolve(`${__dirname}/../../../`);
|
||||
@@ -16,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<void>}
|
||||
*/
|
||||
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<import('playwright').Page>} 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<void>}
|
||||
*/
|
||||
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();
|
||||
@@ -35,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)
|
||||
@@ -64,11 +123,7 @@ 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;
|
||||
}
|
||||
await closeBrowser();
|
||||
|
||||
if (!global.app) {
|
||||
delete global.testPort;
|
||||
@@ -79,90 +134,23 @@ exports.stopApplication = async (waitTime = 100) => {
|
||||
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));
|
||||
};
|
||||
|
||||
exports.getDocument = () => {
|
||||
return new Promise((resolve) => {
|
||||
const port = global.testPort || config.port || 8080;
|
||||
const url = `http://${config.address || "localhost"}:${port}`;
|
||||
jsdom.JSDOM.fromURL(url, { resources: "usable", runScripts: "dangerously" }).then((dom) => {
|
||||
dom.window.name = "jsdom";
|
||||
global.window = dom.window;
|
||||
// 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;
|
||||
dom.window.onload = () => {
|
||||
global.document = dom.window.document;
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
exports.getDocument = async () => {
|
||||
const port = global.testPort || config.port || 8080;
|
||||
const address = config.address === "0.0.0.0" ? "localhost" : config.address || "localhost";
|
||||
const url = `http://${address}:${port}`;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
if (timeout !== 0) {
|
||||
setTimeout(() => {
|
||||
if (interval) clearInterval(interval);
|
||||
resolve(null);
|
||||
}, timeout);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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 (element, regex) => {
|
||||
const elem = await this.waitForElement(element);
|
||||
expect(elem).not.toBeNull();
|
||||
expect(elem.textContent).toMatch(regex);
|
||||
return true;
|
||||
await openPage(url);
|
||||
};
|
||||
|
||||
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")) {
|
||||
@@ -181,7 +169,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
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
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();
|
||||
expect(
|
||||
elem.textContent
|
||||
.trim()
|
||||
.replace(/(\r\n|\n|\r)/gm, "")
|
||||
.replace(/[ ]+/g, " ")
|
||||
).toBe(result);
|
||||
return true;
|
||||
};
|
||||
|
||||
exports.startApplication = async (configFileName, additionalMockData) => {
|
||||
await helpers.startApplication(injectMockData(configFileName, additionalMockData));
|
||||
await helpers.getDocument();
|
||||
|
||||
Reference in New Issue
Block a user