Skip to content

Commit 5f8397e

Browse files
test: migrate E2E tests from JSDOM to Playwright
1 parent 259b84c commit 5f8397e

17 files changed

+235
-163
lines changed

tests/e2e/animateCSS_spec.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ describe("AnimateCSS integration Test", () => {
2929
async function waitForAnimationClass (cls, { timeout = 6000 } = {}) {
3030
const start = Date.now();
3131
while (Date.now() - start < timeout) {
32-
if (document.querySelector(`.compliments.animate__animated.${cls}`)) {
32+
if (await helpers.querySelector(`.compliments.animate__animated.${cls}`)) {
3333
// small stability wait
3434
await new Promise((r) => setTimeout(r, 50));
35-
if (document.querySelector(`.compliments.animate__animated.${cls}`)) return true;
35+
if (await helpers.querySelector(`.compliments.animate__animated.${cls}`)) return true;
3636
}
3737
await new Promise((r) => setTimeout(r, 100));
3838
}
@@ -47,7 +47,7 @@ describe("AnimateCSS integration Test", () => {
4747
async function assertNoAnimationWithin (ms = 2000) {
4848
const start = Date.now();
4949
while (Date.now() - start < ms) {
50-
if (document.querySelector(".compliments.animate__animated")) {
50+
if (await helpers.querySelector(".compliments.animate__animated")) {
5151
throw new Error("Unexpected animate__animated class present in non-animation scenario");
5252
}
5353
await new Promise((r) => setTimeout(r, 100));

tests/e2e/custom_module_regions_spec.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ describe("Custom Position of modules", () => {
1616
const className1 = positions[i].replace("_", ".");
1717
let message1 = positions[i];
1818
it(`should show text in ${message1}`, async () => {
19-
const elem = await helpers.waitForElement(`.${className1}`);
20-
expect(elem).not.toBeNull();
21-
expect(elem.textContent).toContain(`Text in ${message1}`);
19+
await expect(
20+
helpers.expectTextContent(`.${className1} .module-content`, { contains: `Text in ${message1}` })
21+
).resolves.toBe(true);
2222
});
2323
i = 1;
2424
const className2 = positions[i].replace("_", ".");
2525
let message2 = positions[i];
2626
it(`should NOT show text in ${message2}`, async () => {
27-
const elem = await helpers.waitForElement(`.${className2}`, "", 1500);
27+
const elem = await helpers.querySelector(`.${className2} .module-content`);
2828
expect(elem).toBeNull();
29-
}, 1510);
29+
});
3030
});

tests/e2e/env_spec.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ describe("App environment", () => {
2020
});
2121

2222
it("should show the title MagicMirror²", async () => {
23-
const elem = await helpers.waitForElement("title");
24-
expect(elem).not.toBeNull();
25-
expect(elem.textContent).toBe("MagicMirror²");
23+
await expect(helpers.expectTextContent("title", { equals: "MagicMirror²" })).resolves.toBe(true);
2624
});
2725
});

tests/e2e/helpers/global-setup.js

Lines changed: 147 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
const path = require("node:path");
22
const os = require("node:os");
33
const fs = require("node:fs");
4-
const { once } = require("node:events");
5-
const jsdom = require("jsdom");
4+
const { chromium } = require("playwright");
65

76
// global absolute root path
87
global.root_path = path.resolve(`${__dirname}/../../../`);
@@ -17,8 +16,67 @@ const sampleCss = [
1716
" top: 100%;",
1817
"}"
1918
];
20-
var indexData = [];
21-
var cssData = [];
19+
let indexData = "";
20+
let cssData = "";
21+
22+
let browser;
23+
let context;
24+
let page;
25+
26+
/**
27+
* Ensure Playwright browser and context are available.
28+
* @returns {Promise<void>}
29+
*/
30+
async function ensureContext () {
31+
if (!browser) {
32+
browser = await chromium.launch({ headless: true });
33+
}
34+
if (!context) {
35+
context = await browser.newContext();
36+
}
37+
}
38+
39+
/**
40+
* Open a fresh page pointing to the provided url.
41+
* @param {string} url target url
42+
* @returns {Promise<import('playwright').Page>} initialized page instance
43+
*/
44+
async function openPage (url) {
45+
await ensureContext();
46+
if (page) {
47+
await page.close();
48+
}
49+
page = await context.newPage();
50+
await page.goto(url, { waitUntil: "load" });
51+
return page;
52+
}
53+
54+
/**
55+
* Close page, context and browser if they exist.
56+
* @returns {Promise<void>}
57+
*/
58+
async function closeBrowser () {
59+
if (page) {
60+
await page.close();
61+
page = null;
62+
}
63+
if (context) {
64+
await context.close();
65+
context = null;
66+
}
67+
if (browser) {
68+
await browser.close();
69+
browser = null;
70+
}
71+
}
72+
73+
exports.getPage = () => {
74+
if (!page) {
75+
throw new Error("Playwright page is not initialized. Call getDocument() first.");
76+
}
77+
return page;
78+
};
79+
2280

2381
exports.startApplication = async (configFilename, exec) => {
2482
vi.resetModules();
@@ -36,7 +94,7 @@ exports.startApplication = async (configFilename, exec) => {
3694
});
3795

3896
if (global.app) {
39-
await this.stopApplication();
97+
await exports.stopApplication();
4098
}
4199

42100
// Use fixed port 8080 (tests run sequentially, no conflicts)
@@ -65,114 +123,125 @@ exports.startApplication = async (configFilename, exec) => {
65123
};
66124

67125
exports.stopApplication = async (waitTime = 100) => {
126+
await closeBrowser();
127+
68128
if (!global.app) {
69-
if (global.window) {
70-
global.window.close();
71-
delete global.window;
72-
}
73129
delete global.testPort;
74130
return Promise.resolve();
75131
}
76132

77-
// Stop server first
78133
await global.app.stop();
79134
delete global.app;
80135
delete global.testPort;
81136

82137
// Wait for any pending async operations to complete before closing DOM
83138
await new Promise((resolve) => setTimeout(resolve, waitTime));
84-
85-
if (global.window) {
86-
// Close window after async operations have settled
87-
global.window.close();
88-
delete global.window;
89-
delete global.document;
90-
}
91139
};
92140

93141
exports.getDocument = async () => {
94142
const port = global.testPort || config.port || 8080;
95-
// JSDOM requires localhost instead of 0.0.0.0 for URL resolution
96143
const address = config.address === "0.0.0.0" ? "localhost" : config.address || "localhost";
97144
const url = `http://${address}:${port}`;
98145

99-
const dom = await jsdom.JSDOM.fromURL(url, { resources: "usable", runScripts: "dangerously" });
146+
await openPage(url);
147+
};
100148

101-
dom.window.name = "jsdom";
102-
global.window = dom.window;
103-
global.document = dom.window.document;
104-
// Some modules access navigator.*, so provide a minimal stub for JSDOM-based tests.
105-
global.navigator = {
106-
useragent: "node.js"
107-
};
108-
dom.window.fetch = fetch;
149+
exports.waitForElement = async (selector, ignoreValue = "", timeout = 0) => {
150+
const currentPage = exports.getPage();
151+
const locator = currentPage.locator(selector);
152+
const effectiveTimeout = timeout && timeout > 0 ? timeout : 30000;
153+
const deadline = Date.now() + effectiveTimeout;
109154

110-
// fromURL() resolves when HTML is loaded, but with resources: "usable",
111-
// external scripts load asynchronously. Wait for the load event to ensure scripts are executed.
112-
if (dom.window.document.readyState !== "complete") {
113-
await once(dom.window, "load");
155+
while (Date.now() <= deadline) {
156+
const count = await locator.count();
157+
if (count > 0) {
158+
if (!ignoreValue) {
159+
return locator.first();
160+
}
161+
const text = await locator.first().textContent();
162+
if (!text || !text.includes(ignoreValue)) {
163+
return locator.first();
164+
}
165+
}
166+
await currentPage.waitForTimeout(100);
114167
}
168+
169+
return null;
115170
};
116171

117-
exports.waitForElement = (selector, ignoreValue = "", timeout = 0) => {
118-
return new Promise((resolve) => {
119-
let oldVal = "dummy12345";
120-
let element = null;
121-
const interval = setInterval(() => {
122-
element = document.querySelector(selector);
123-
if (element) {
124-
let newVal = element.textContent;
125-
if (newVal === oldVal) {
126-
clearInterval(interval);
127-
resolve(element);
128-
} else {
129-
if (ignoreValue === "") {
130-
oldVal = newVal;
131-
} else {
132-
if (!newVal.includes(ignoreValue)) oldVal = newVal;
133-
}
134-
}
172+
exports.waitForAllElements = async (selector, timeout = 30000) => {
173+
const currentPage = exports.getPage();
174+
const locator = currentPage.locator(selector);
175+
const effectiveTimeout = timeout && timeout > 0 ? timeout : 30000;
176+
const deadline = Date.now() + effectiveTimeout;
177+
178+
while (Date.now() <= deadline) {
179+
const count = await locator.count();
180+
if (count > 0) {
181+
const elements = [];
182+
for (let i = 0; i < count; i++) {
183+
elements.push(locator.nth(i));
135184
}
136-
}, 100);
137-
if (timeout !== 0) {
138-
setTimeout(() => {
139-
if (interval) clearInterval(interval);
140-
resolve(null);
141-
}, timeout);
185+
return elements;
142186
}
143-
});
187+
await currentPage.waitForTimeout(100);
188+
}
189+
190+
return [];
144191
};
145192

146-
exports.waitForAllElements = (selector) => {
147-
return new Promise((resolve) => {
148-
let oldVal = 999999;
149-
const interval = setInterval(() => {
150-
const element = document.querySelectorAll(selector);
151-
if (element) {
152-
let newVal = element.length;
153-
if (newVal === oldVal) {
154-
clearInterval(interval);
155-
resolve(element);
156-
} else {
157-
if (newVal !== 0) oldVal = newVal;
158-
}
159-
}
160-
}, 100);
161-
});
193+
exports.testMatch = async (selector, regex) => {
194+
await exports.expectTextContent(selector, { matches: regex });
195+
return true;
162196
};
163197

164-
exports.testMatch = async (element, regex) => {
165-
const elem = await this.waitForElement(element);
166-
expect(elem).not.toBeNull();
167-
expect(elem.textContent).toMatch(regex);
198+
exports.querySelector = async (selector) => {
199+
const locator = exports.getPage().locator(selector);
200+
return (await locator.count()) > 0 ? locator.first() : null;
201+
};
202+
203+
exports.querySelectorAll = async (selector) => {
204+
const locator = exports.getPage().locator(selector);
205+
const count = await locator.count();
206+
const elements = [];
207+
for (let i = 0; i < count; i++) {
208+
elements.push(locator.nth(i));
209+
}
210+
return elements;
211+
};
212+
213+
exports.expectTextContent = async (target, expectation) => {
214+
if (!expectation || (expectation.equals === undefined && expectation.contains === undefined && expectation.matches === undefined)) {
215+
throw new Error("expectTextContent expects an object with equals, contains, or matches");
216+
}
217+
218+
let locator = target;
219+
if (typeof target === "string") {
220+
locator = await exports.waitForElement(target);
221+
}
222+
223+
expect(locator).not.toBeNull();
224+
if (!locator) {
225+
const description = typeof target === "string" ? target : "supplied locator";
226+
throw new Error(`No element found for ${description}`);
227+
}
228+
229+
const textPromise = locator.textContent();
230+
if (expectation.equals !== undefined) {
231+
await expect(textPromise).resolves.toBe(expectation.equals);
232+
} else if (expectation.contains !== undefined) {
233+
await expect(textPromise).resolves.toContain(expectation.contains);
234+
} else {
235+
await expect(textPromise).resolves.toMatch(expectation.matches);
236+
}
168237
return true;
169238
};
170239

171240
exports.fixupIndex = async () => {
172241
// read and save the git level index file
173242
indexData = (await fs.promises.readFile(indexFile)).toString();
174243
// make lines of the content
175-
let workIndexLines = indexData.split(os.EOL);
244+
const workIndexLines = indexData.split(os.EOL);
176245
// loop thru the lines to find place to insert new region
177246
for (let l in workIndexLines) {
178247
if (workIndexLines[l].includes("region top right")) {
@@ -191,7 +260,7 @@ exports.fixupIndex = async () => {
191260

192261
exports.restoreIndex = async () => {
193262
// if we read in data
194-
if (indexData.length > 1) {
263+
if (indexData.length > 0) {
195264
//write out saved index.html
196265
await fs.promises.writeFile(indexFile, indexData, { flush: true });
197266
// write out saved custom.css

tests/e2e/helpers/weather-functions.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ const helpers = require("./global-setup");
44
exports.getText = async (element, result) => {
55
const elem = await helpers.waitForElement(element);
66
expect(elem).not.toBeNull();
7-
expect(
8-
elem.textContent
9-
.trim()
10-
.replace(/(\r\n|\n|\r)/gm, "")
11-
.replace(/[ ]+/g, " ")
12-
).toBe(result);
7+
const rawText = await elem.textContent();
8+
const content = (rawText ?? "")
9+
.trim()
10+
.replace(/(\r\n|\n|\r)/gm, "")
11+
.replace(/[ ]+/g, " ");
12+
expect(content).toBe(result);
1313
return true;
1414
};
1515

0 commit comments

Comments
 (0)