chore: import upstream snapshot with attribution
Enforce Pull-Request Rules / check (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:19:24 +08:00
commit dbe3ade0dc
449 changed files with 71828 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
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
const TEST_CONFIG_INVERTED = "tests/configs/modules/compliments/compliments_animateCSS_invertedAnimationName.js"; // in/out swapped
const TEST_CONFIG_NONE = "tests/configs/modules/compliments/compliments_anytime.js"; // no animations defined
/**
* Get the compliments container element (waits until available).
* @returns {Promise<void>}
*/
async function getComplimentsElement () {
await helpers.getDocument();
page = helpers.getPage();
await expect(page.locator(".compliments")).toBeVisible();
}
/**
* 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<void>}
*/
async function waitForAnimationClass (cls, { timeout = 6000 } = {}) {
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();
}
/**
* Assert that no Animate.css animation class is applied within a time window.
* @param {number} [ms] Observation period in ms (default 2000)
* @returns {Promise<void>}
*/
async function assertNoAnimationWithin (ms = 2000) {
const start = Date.now();
const locator = page.locator(".compliments.animate__animated");
while (Date.now() - start < ms) {
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));
}
}
/**
* Run one animation test scenario.
* @param {string} [animationIn] Expected animate-in name
* @param {string} [animationOut] Expected animate-out name
* @returns {Promise<void>} Throws on assertion failure
*/
async function runAnimationTest (animationIn, animationOut) {
await getComplimentsElement();
if (!animationIn && !animationOut) {
await assertNoAnimationWithin(2000);
return;
}
if (animationIn) await waitForAnimationClass(`animate__${animationIn}`);
if (animationOut) {
// Wait just beyond one update cycle (updateInterval=2000ms) before expecting animateOut.
await new Promise((r) => setTimeout(r, 2100));
await waitForAnimationClass(`animate__${animationOut}`);
}
}
afterEach(async () => {
await helpers.stopApplication();
});
describe("animateIn and animateOut Test", () => {
it("with flipInX and flipOutX animation", async () => {
await helpers.startApplication(TEST_CONFIG_ANIM);
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 runAnimationTest();
});
});
describe("false Animation name test", () => {
it("without animation (invalid names)", async () => {
await helpers.startApplication(TEST_CONFIG_FALLBACK);
await runAnimationTest();
});
});
describe("no Animation defined test", () => {
it("without animation (no config)", async () => {
await helpers.startApplication(TEST_CONFIG_NONE);
await runAnimationTest();
});
});
});
+179
View File
@@ -0,0 +1,179 @@
const { spawnSync, spawn } = require("node:child_process");
const delay = (time) => {
return new Promise((resolve) => setTimeout(resolve, time));
};
/**
* Run clientonly with given arguments and return result
* @param {string[]} args command line arguments
* @param {object} env environment variables to merge (replaces process.env)
* @returns {object} result with status and stderr
*/
const runClientOnly = (args = [], env = {}) => {
// Start with minimal env and merge provided env
const testEnv = {
PATH: process.env.PATH,
NODE_PATH: process.env.NODE_PATH,
...env
};
const result = spawnSync("node", ["clientonly/index.js", ...args], {
env: testEnv,
encoding: "utf-8",
timeout: 5000
});
return result;
};
describe("Clientonly parameter handling", () => {
describe("Missing parameters", () => {
it("should fail without any parameters", () => {
const result = runClientOnly();
expect(result.status).toBe(1);
expect(result.stderr).toContain("Usage:");
});
it("should fail with only address parameter", () => {
const result = runClientOnly(["--address", "192.168.1.10"]);
expect(result.status).toBe(1);
expect(result.stderr).toContain("Usage:");
});
it("should fail with only port parameter", () => {
const result = runClientOnly(["--port", "8080"]);
expect(result.status).toBe(1);
expect(result.stderr).toContain("Usage:");
});
});
describe("Local address rejection", () => {
it("should fail with localhost address", () => {
const result = runClientOnly(["--address", "localhost", "--port", "8080"]);
expect(result.status).toBe(1);
expect(result.stderr).toContain("Usage:");
});
it("should fail with 127.0.0.1 address", () => {
const result = runClientOnly(["--address", "127.0.0.1", "--port", "8080"]);
expect(result.status).toBe(1);
expect(result.stderr).toContain("Usage:");
});
it("should fail with ::1 address", () => {
const result = runClientOnly(["--address", "::1", "--port", "8080"]);
expect(result.status).toBe(1);
expect(result.stderr).toContain("Usage:");
});
it("should fail with ::ffff:127.0.0.1 address", () => {
const result = runClientOnly(["--address", "::ffff:127.0.0.1", "--port", "8080"]);
expect(result.status).toBe(1);
expect(result.stderr).toContain("Usage:");
});
});
describe("Port validation", () => {
it("should fail with port 0", () => {
const result = runClientOnly(["--address", "192.168.1.10", "--port", "0"]);
expect(result.status).toBe(1);
expect(result.stderr).toContain("Invalid port number");
});
it("should fail with negative port", () => {
const result = runClientOnly(["--address", "192.168.1.10", "--port", "-1"]);
expect(result.status).toBe(1);
expect(result.stderr).toContain("Invalid port number");
});
it("should fail with port above 65535", () => {
const result = runClientOnly(["--address", "192.168.1.10", "--port", "65536"]);
expect(result.status).toBe(1);
expect(result.stderr).toContain("Invalid port number");
});
it("should fail with non-numeric port", () => {
const result = runClientOnly(["--address", "192.168.1.10", "--port", "abc"]);
expect(result.status).toBe(1);
expect(result.stderr).toContain("Invalid port number");
});
it("should accept valid port 8080", () => {
const result = runClientOnly(["--address", "192.168.1.10", "--port", "8080"]);
// Should not fail on port validation (will fail on connection or display)
expect(result.stderr).not.toContain("Invalid port number");
});
it("should accept valid port 1", () => {
const result = runClientOnly(["--address", "192.168.1.10", "--port", "1"]);
expect(result.stderr).not.toContain("Invalid port number");
});
it("should accept valid port 65535", () => {
const result = runClientOnly(["--address", "192.168.1.10", "--port", "65535"]);
expect(result.stderr).not.toContain("Invalid port number");
});
});
describe("TLS flag parsing", () => {
// Note: These tests verify the flag is parsed, not the actual connection behavior
// Connection tests would timeout as they try to reach unreachable addresses
it("should not fail on port validation when using --use-tls", () => {
// Verify --use-tls doesn't interfere with other parameter parsing
const result = runClientOnly(["--address", "192.168.1.10", "--port", "443", "--use-tls"]);
expect(result.stderr).not.toContain("Invalid port number");
});
it("should accept --use-tls flag with valid parameters", () => {
const result = runClientOnly(["--address", "192.168.1.10", "--port", "443", "--use-tls"]);
// Should not fail on parameter parsing (will fail on connection or display)
expect(result.stderr).not.toContain("Usage:");
});
});
describe("Display environment check", () => {
it("should fail without DISPLAY or WAYLAND_DISPLAY when connecting to valid server", () => {
// This test needs a running server to get past the connection phase
// Without DISPLAY, it should fail with display error
// For now, we just verify it fails (connection error comes first without server)
const result = runClientOnly(["--address", "192.168.1.10", "--port", "1"]);
// Either exits with code 1 or times out (null status means killed/timeout)
expect(result.status === 1 || result.status === null).toBe(true);
});
});
});
describe("Clientonly with running server", () => {
let serverProcess;
const testPort = 8081;
beforeAll(async () => {
process.env.MM_CONFIG_FILE = "tests/configs/default.js";
process.env.MM_PORT = testPort.toString();
serverProcess = spawn("node", ["--run", "server"], {
env: process.env,
detached: true
});
// Wait for server to start
await delay(2000);
});
afterAll(() => {
if (serverProcess && serverProcess.pid) {
try {
process.kill(-serverProcess.pid);
} catch {
// Process may already be dead
}
}
});
it("should be able to fetch config from server", async () => {
const res = await fetch(`http://localhost:${testPort}/config/`);
expect(res.status).toBe(200);
const config = await res.json();
expect(config).toBeDefined();
expect(typeof config).toBe("object");
});
});
+21
View File
@@ -0,0 +1,21 @@
const helpers = require("./helpers/global-setup");
describe("config with module function", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/config_functions.js");
});
afterAll(async () => {
await helpers.stopApplication();
});
it("config should resolve module functions", () => {
expect(config.modules[0].config.moduleFunctions.roundToInt1(13.3)).toBe(13);
expect(config.modules[0].config.moduleFunctions.roundToInt2(13.3)).toBe(13);
});
it("config should not revive plain strings containing arrow or function keywords", () => {
expect(config.modules[0].config.stringWithArrow).toBe("a => b is not a function");
expect(config.modules[0].config.stringWithFunction).toBe("this function keyword is just text");
});
});
+34
View File
@@ -0,0 +1,34 @@
const helpers = require("./helpers/global-setup");
describe("config with variables and secrets", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/config_variables.js");
});
afterAll(async () => {
await helpers.stopApplication();
});
it("config.language should be \"de\"", () => {
expect(config.language).toBe("de");
});
it("config.loglevel should be [\"ERROR\", \"LOG\", \"WARN\", \"INFO\"]", () => {
expect(config.logLevel).toStrictEqual(["ERROR", "LOG", "WARN", "INFO"]);
});
it("config.ipWhitelist should be [\"::ffff:127.0.0.1\", \"::1\", \"127.0.0.1\"]", () => {
expect(config.ipWhitelist).toStrictEqual(["::ffff:127.0.0.1", "::1", "127.0.0.1"]);
});
it("config.timeFormat should be 12", () => {
expect(config.timeFormat).toBe(12); // default is 24
});
it("/config endpoint should show redacted secrets", async () => {
const res = await fetch(`http://localhost:${config.port}/config`);
expect(res.status).toBe(200);
const cfg = await res.json();
expect(cfg.ipWhitelist).toStrictEqual(["**SECRET_IP2**", "::**SECRET_IP3**", "**SECRET_IP1**"]);
});
});
+32
View File
@@ -0,0 +1,32 @@
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();
await helpers.restoreIndex();
});
const positions = ["row3_left", "top3_left1"];
let i = 0;
const className1 = positions[i].replace("_", ".");
let message1 = positions[i];
it(`should show text in ${message1}`, async () => {
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 () => {
await expect(page.locator(`.${className2} .module-content`)).toHaveCount(0);
});
});
+30
View File
@@ -0,0 +1,30 @@
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();
});
it("get request from http://localhost:8080 should return 200", async () => {
const res = await fetch("http://localhost:8080");
expect(res.status).toBe(200);
});
it("get request from http://localhost:8080/nothing should return 404", async () => {
const res = await fetch("http://localhost:8080/nothing");
expect(res.status).toBe(404);
});
it("should show the title MagicMirror²", async () => {
await expect(page).toHaveTitle("MagicMirror²");
});
});
+29
View File
@@ -0,0 +1,29 @@
const helpers = require("./helpers/global-setup");
describe("All font files from roboto.css should be downloadable", () => {
const fontFiles = [];
// Statements below filters out all 'url' lines in the CSS file
const fileContent = require("node:fs").readFileSync(`${global.root_path}/css/roboto.css`, "utf8");
const regex = /\burl\(['"]([^'"]+)['"]\)/g;
let match = regex.exec(fileContent);
while (match !== null) {
// Push 1st match group onto fontFiles stack
fontFiles.push(match[1]);
// Find the next one
match = regex.exec(fileContent);
}
beforeAll(async () => {
await helpers.startApplication("tests/configs/without_modules.js");
});
afterAll(async () => {
await helpers.stopApplication();
});
it.each(fontFiles)("should return 200 HTTP code for file '%s'", async (fontFile) => {
const fontUrl = `http://localhost:8080/fonts/${fontFile}`;
const res = await fetch(fontUrl);
expect(res.status).toBe(200);
});
});
+29
View File
@@ -0,0 +1,29 @@
const path = require("node:path");
const auth = require("express-basic-auth");
const express = require("express");
const app = express();
const basicAuth = auth({
realm: "MagicMirror² Area restricted.",
users: { MagicMirror: "CallMeADog" }
});
app.use(basicAuth);
// Set available directories
const directories = ["/tests/configs", "/tests/mocks"];
for (let directory of directories) {
app.use(directory, express.static(path.resolve(`${global.root_path}/${directory}`)));
}
let server;
exports.listen = (...args) => {
server = app.listen.apply(app, args);
};
exports.close = async () => {
await server.close();
};
+188
View File
@@ -0,0 +1,188 @@
const path = require("node:path");
const os = require("node:os");
const fs = require("node:fs");
const { chromium } = require("playwright");
// global absolute root path
global.root_path = path.resolve(`${__dirname}/../../../`);
const indexFile = `${global.root_path}/index.html`;
const cssFile = `${global.root_path}/config/custom.css`;
const sampleCss = [
".region.row3 {",
" top: 0;",
"}",
".region.row3.left {",
" top: 100%;",
"}"
];
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) {
// Additional args for CI stability to prevent crashes
const launchOptions = {
headless: true,
args: [
"--disable-dev-shm-usage", // Overcome limited resource problems in Docker/CI
"--disable-gpu", // Disable GPU hardware acceleration
"--no-sandbox", // Required for running as root in some CI environments
"--disable-setuid-sandbox",
"--single-process" // Run in single process mode for better stability in CI
]
};
browser = await chromium.launch(launchOptions);
}
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) => {
vi.resetModules();
// Clear Node's require cache for config and app files to prevent stale configs and middlewares
Object.keys(require.cache).forEach((key) => {
if (
key.includes("/tests/configs/")
|| key.includes("/config/config")
|| key.includes("/js/app.js")
|| key.includes("/js/server.js")
) {
delete require.cache[key];
}
});
if (global.app) {
await exports.stopApplication();
}
// Use MM_PORT if preset by a test, otherwise default to 8080.
const port = Number(process.env.MM_PORT) || 8080;
global.testPort = port;
// Set config sample for use in test
let configPath;
if (configFilename === "") {
configPath = "config/config.js";
} else {
configPath = configFilename;
}
process.env.MM_CONFIG_FILE = configPath;
// Ensure MM_PORT is set before app loads
process.env.MM_PORT = port.toString();
process.env.mmTestMode = "true";
process.setMaxListeners(0);
global.app = require(`${global.root_path}/js/app`);
return global.app.start();
};
exports.stopApplication = async (waitTime = 100) => {
await closeBrowser();
if (!global.app) {
delete global.testPort;
return Promise.resolve();
}
await global.app.stop();
delete global.app;
delete global.testPort;
delete process.env.MM_PORT;
// Wait for any pending async operations to complete before closing DOM
await new Promise((resolve) => setTimeout(resolve, waitTime));
};
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}`;
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
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")) {
// insert a new line with new region definition
workIndexLines.splice(l, 0, " <div class=\"region row3 left\"><div class=\"container\"></div></div>");
break;
}
}
// write out the new index.html file, not append
await fs.promises.writeFile(indexFile, workIndexLines.join(os.EOL), { flush: true });
// read in the current custom.css
cssData = (await fs.promises.readFile(cssFile)).toString();
// write out the custom.css for this testcase, matching the new region name
await fs.promises.writeFile(cssFile, sampleCss.join(os.EOL), { flush: true });
};
exports.restoreIndex = async () => {
// if we read in data
if (indexData.length > 0) {
//write out saved index.html
await fs.promises.writeFile(indexFile, indexData, { flush: true });
// write out saved custom.css
await fs.promises.writeFile(cssFile, cssData, { flush: true });
}
};
+108
View File
@@ -0,0 +1,108 @@
const fs = require("node:fs");
const path = require("node:path");
const weatherUtils = require("../../../defaultmodules/weather/provider-utils");
const helpers = require("./global-setup");
/**
* Inject mock weather data directly via socket communication
* This bypasses the weather provider and tests only client-side rendering
* @param {object} page - Playwright page
* @param {string} mockDataFile - Filename of mock data
*/
async function injectMockWeatherData (page, mockDataFile) {
const rawData = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../../mocks", mockDataFile)).toString());
// Validate that the fixture has at least one expected weather data type
if (!rawData.current && !rawData.daily && !rawData.hourly) {
throw new Error(
"Invalid weather fixture: missing current, daily, and hourly data. "
+ `Available keys: ${Object.keys(rawData).join(", ")}`
);
}
// Determine weather type from the mock data structure
let type = "current";
let data = null;
const timezoneOffset = rawData.timezone_offset ? rawData.timezone_offset / 60 : 0;
if (rawData.current) {
type = "current";
// Mock what the provider would send for current weather
data = {
date: weatherUtils.applyTimezoneOffset(new Date(rawData.current.dt * 1000), timezoneOffset),
windSpeed: rawData.current.wind_speed,
windFromDirection: rawData.current.wind_deg,
sunrise: weatherUtils.applyTimezoneOffset(new Date(rawData.current.sunrise * 1000), timezoneOffset),
sunset: weatherUtils.applyTimezoneOffset(new Date(rawData.current.sunset * 1000), timezoneOffset),
temperature: rawData.current.temp,
weatherType: weatherUtils.convertWeatherType(rawData.current.weather[0].icon),
humidity: rawData.current.humidity,
feelsLikeTemp: rawData.current.feels_like
};
} else if (rawData.daily) {
type = "forecast";
data = rawData.daily.map((day) => ({
date: weatherUtils.applyTimezoneOffset(new Date(day.dt * 1000), timezoneOffset),
minTemperature: day.temp.min,
maxTemperature: day.temp.max,
weatherType: weatherUtils.convertWeatherType(day.weather[0].icon),
rain: day.rain || 0,
snow: day.snow || 0,
precipitationAmount: (day.rain || 0) + (day.snow || 0)
}));
} else if (rawData.hourly) {
type = "hourly";
data = rawData.hourly.map((hour) => ({
date: weatherUtils.applyTimezoneOffset(new Date(hour.dt * 1000), timezoneOffset),
temperature: hour.temp,
feelsLikeTemp: hour.feels_like,
humidity: hour.humidity,
windSpeed: hour.wind_speed,
windFromDirection: hour.wind_deg,
weatherType: weatherUtils.convertWeatherType(hour.weather[0].icon),
precipitationProbability: hour.pop != null ? hour.pop * 100 : undefined,
precipitationAmount: (hour.rain?.["1h"] || 0) + (hour.snow?.["1h"] || 0)
}));
}
// Inject weather data by evaluating code in the browser context
await page.evaluate(({ type, data }) => {
// Find the weather module instance
const weatherModule = MM.getModules().find((m) => m.name === "weather");
if (weatherModule) {
// Send INITIALIZED first
weatherModule.socketNotificationReceived("WEATHER_INITIALIZED", {
instanceId: weatherModule.instanceId,
locationName: "Munich"
});
// Then send the actual data
weatherModule.socketNotificationReceived("WEATHER_DATA", {
instanceId: weatherModule.instanceId,
type: type,
data: data
});
}
}, { type, data });
}
exports.startApplication = async (configFileName, mockDataFile) => {
await helpers.startApplication(configFileName);
await helpers.getDocument();
// If mock data file is provided, inject it
if (mockDataFile) {
const page = helpers.getPage();
// Wait for weather module to initialize
// eslint-disable-next-line playwright/no-wait-for-selector
await page.waitForSelector(".weather", { timeout: 5000 });
await injectMockWeatherData(page, mockDataFile);
// Wait for data to be rendered
// eslint-disable-next-line playwright/no-wait-for-selector
await page.waitForSelector(".weather .weathericon", { timeout: 2000 });
}
};
exports.stopApplication = async () => {
await helpers.stopApplication();
};
+47
View File
@@ -0,0 +1,47 @@
const helpers = require("./helpers/global-setup");
describe("ipWhitelist directive configuration", () => {
describe("When IP is not in whitelist", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/noIpWhiteList.js");
});
afterAll(async () => {
await helpers.stopApplication();
});
it("should reject request with 403 (Forbidden)", async () => {
const port = global.testPort || 8080;
const res = await fetch(`http://localhost:${port}`);
expect(res.status).toBe(403);
});
it("should also reject Socket.IO handshake with 403 (Forbidden) — not just HTTP routes", async () => {
const port = global.testPort || 8080;
const res = await fetch(`http://localhost:${port}/socket.io/?EIO=4&transport=polling`);
expect(res.status).toBe(403);
});
});
describe("When whitelist is empty (allow all IPs)", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/empty_ipWhiteList.js");
});
afterAll(async () => {
await helpers.stopApplication();
});
it("should allow request with 200 (OK)", async () => {
const port = global.testPort || 8080;
const res = await fetch(`http://localhost:${port}`);
expect(res.status).toBe(200);
});
it("should also allow Socket.IO handshake with 200 (OK) — not just HTTP routes", async () => {
const port = global.testPort || 8080;
const res = await fetch(`http://localhost:${port}/socket.io/?EIO=4&transport=polling`);
expect(res.status).toBe(200);
});
});
});
+53
View File
@@ -0,0 +1,53 @@
const { expect } = require("playwright/test");
const helpers = require("../helpers/global-setup");
describe("Alert module", () => {
let page;
afterAll(async () => {
await helpers.stopApplication();
});
describe("with welcome_message set to false", () => {
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 () => {
// Wait a bit to ensure no message appears
await new Promise((resolve) => setTimeout(resolve, 1000));
// Check that no alert/notification elements are present
await expect(page.locator(".ns-box .ns-box-inner .light.bright.small")).toHaveCount(0);
});
});
describe("with welcome_message set to true", () => {
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 () => {
await expect(page.locator(".ns-box .ns-box-inner .light.bright.small")).toContainText("Welcome, start was successful!");
});
});
describe("with welcome_message set to custom string", () => {
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 () => {
await expect(page.locator(".ns-box .ns-box-inner .light.bright.small")).toContainText("Custom welcome message!");
});
});
});
+194
View File
@@ -0,0 +1,194 @@
const { expect } = require("playwright/test");
const helpers = require("../helpers/global-setup");
const serverBasicAuth = require("../helpers/basic-auth");
describe("Calendar module", () => {
let page;
/**
* 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<void>}
*/
const testElementLength = async (selector, expectedLength, not) => {
const locator = page.locator(selector);
if (not === "not") {
await expect(locator).not.toHaveCount(expectedLength);
} else {
await expect(locator).toHaveCount(expectedLength);
}
};
const testTextContain = async (selector, expectedText) => {
await expect(page.locator(selector).first()).toContainText(expectedText);
};
afterAll(async () => {
await helpers.stopApplication();
});
describe("Default configuration", () => {
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 () => {
await testElementLength(".calendar .event", 10);
});
it("should show the default calendar symbol in each event", async () => {
await testElementLength(".calendar .event .fa-calendar-days", 0, "not");
});
});
describe("Custom configuration", () => {
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 () => {
await testElementLength(".calendar .event", 5);
});
it("should show the custom calendar symbol in four events", async () => {
await testElementLength(".calendar .event .fa-birthday-cake", 4);
});
it("should show a customEvent calendar symbol in one event", async () => {
await testElementLength(".calendar .event .fa-dice", 1);
});
it("should show a customEvent calendar eventClass in one event", async () => {
await testElementLength(".calendar .event.undo", 1);
});
it("should show two custom icons for repeating events", async () => {
await testElementLength(".calendar .event .fa-undo", 2);
});
it("should show two custom icons for day events", async () => {
await testElementLength(".calendar .event .fa-calendar-day", 2);
});
});
describe("Recurring event", () => {
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 () => {
await testElementLength(".calendar .event", 6);
});
});
//Will contain everyday an fullDayEvent that starts today and ends tomorrow, and one starting tomorrow and ending the day after tomorrow
describe("FullDayEvent over several days should show how many days are left from the from the starting date on", () => {
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 () => {
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 testElementLength(".calendar .event", 3);
});
});
describe("FullDayEvent Single day, should show Today", () => {
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 () => {
await testTextContain(".calendar .time", "Today");
});
it("should contain in total two events", async () => {
await testElementLength(".calendar .event", 2);
});
});
describe("Changed port", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/calendar/changed-port.js");
serverBasicAuth.listen(8010);
await helpers.getDocument();
page = helpers.getPage();
});
afterAll(async () => {
await serverBasicAuth.close();
});
it("should return TestEvents", async () => {
await testElementLength(".calendar .event", 0, "not");
});
});
describe("Basic auth", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/calendar/basic-auth.js");
await helpers.getDocument();
page = helpers.getPage();
});
it("should return TestEvents", async () => {
await testElementLength(".calendar .event", 0, "not");
});
});
describe("Basic auth by default", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/calendar/auth-default.js");
await helpers.getDocument();
page = helpers.getPage();
});
it("should return TestEvents", async () => {
await testElementLength(".calendar .event", 0, "not");
});
});
describe("Basic auth backward compatibility configuration: DEPRECATED", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/calendar/old-basic-auth.js");
await helpers.getDocument();
page = helpers.getPage();
});
it("should return TestEvents", async () => {
await testElementLength(".calendar .event", 0, "not");
});
});
describe("Fail Basic auth", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/calendar/fail-basic-auth.js");
serverBasicAuth.listen(8020);
await helpers.getDocument();
page = helpers.getPage();
});
afterAll(async () => {
await serverBasicAuth.close();
});
it("should show Unauthorized error", async () => {
await testTextContain(".calendar", "Error in the calendar module. Authorization failed");
});
});
});
+36
View File
@@ -0,0 +1,36 @@
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();
});
describe("with showWeek config enabled", () => {
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$/;
await expect(page.locator(".clock .week")).toHaveText(weekRegex);
});
});
describe("with showWeek short config enabled", () => {
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$/;
await expect(page.locator(".clock .week")).toHaveText(weekRegex);
});
});
});
+85
View File
@@ -0,0 +1,85 @@
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();
});
describe("with default 24hr clock config", () => {
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}$/;
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$/;
await expect(page.locator(".clock .time")).toHaveText(timeRegex);
});
});
describe("with default 12hr clock config", () => {
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}$/;
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$/;
await expect(page.locator(".clock .time")).toHaveText(timeRegex);
});
});
describe("with showPeriodUpper config enabled", () => {
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$/;
await expect(page.locator(".clock .time")).toHaveText(timeRegex);
});
});
describe("with showWeek config enabled", () => {
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}$/;
await expect(page.locator(".clock .week")).toHaveText(weekRegex);
});
});
describe("with showWeek short config enabled", () => {
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}$/;
await expect(page.locator(".clock .week")).toHaveText(weekRegex);
});
});
});
+183
View File
@@ -0,0 +1,183 @@
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();
});
describe("with default 24hr clock config", () => {
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}$/;
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$/;
await expect(page.locator(".clock .time")).toHaveText(timeRegex);
});
});
describe("with default 12hr clock config", () => {
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}$/;
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$/;
await expect(page.locator(".clock .time")).toHaveText(timeRegex);
});
it("check for discreet elements of clock", async () => {
await expect(page.locator(".clock-hour-digital")).toBeVisible();
await expect(page.locator(".clock-minute-digital")).toBeVisible();
});
});
describe("with showPeriodUpper config enabled", () => {
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$/;
await expect(page.locator(".clock .time")).toHaveText(timeRegex);
});
});
describe("with displaySeconds config disabled", () => {
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$/;
await expect(page.locator(".clock .time")).toHaveText(timeRegex);
});
});
describe("with showTime config disabled", () => {
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 () => {
await expect(page.locator(".clock .digital .time")).toHaveCount(0);
});
});
describe("with showSun/MoonTime enabled", () => {
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 () => {
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 () => {
await expect(page.locator(".clock .digital .moon")).toBeVisible();
});
});
describe("with showSunNextEvent disabled", () => {
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 () => {
await expect(page.locator(".clock .digital .sun")).toBeVisible();
await expect(page.locator(".clock .digital .sun .fas.fa-sun")).toHaveCount(0);
});
});
describe("with showWeek config enabled", () => {
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}$/;
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}`;
await expect(page.locator(".clock .week")).toHaveText(weekToShow);
});
});
describe("with showWeek short config enabled", () => {
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}$/;
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}`;
await expect(page.locator(".clock .week")).toHaveText(weekToShow);
});
});
describe("with analog clock face enabled", () => {
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 () => {
await expect(page.locator(".clock-circle")).toBeVisible();
});
});
describe("with analog clock face and date enabled", () => {
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 () => {
await expect(page.locator(".clock-circle")).toBeVisible();
await expect(page.locator(".clock .date")).toBeVisible();
});
});
});
+136
View File
@@ -0,0 +1,136 @@
const { expect } = require("playwright/test");
const helpers = require("../helpers/global-setup");
describe("Compliments module", () => {
let page;
/**
* move similar tests in function doTest
* @param {Array} complimentsArray The array of compliments.
* @returns {Promise<void>}
*/
const doTest = async (complimentsArray) => {
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);
};
afterAll(async () => {
await helpers.stopApplication();
});
describe("Feature anytime in compliments module", () => {
describe("Set anytime and empty compliments for morning, evening and afternoon", () => {
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 () => {
await doTest(["Anytime here"]);
});
});
describe("Only anytime present in configuration compliments", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/compliments/compliments_only_anytime.js");
await helpers.getDocument();
page = helpers.getPage();
});
it("shows anytime compliments", async () => {
await doTest(["Anytime here"]);
});
});
});
describe("remoteFile option", () => {
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 () => {
await doTest(["Remote compliment file works!"]);
});
});
describe("Feature specialDayUnique in compliments module", () => {
describe("specialDayUnique is false", () => {
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 () => {
await doTest(["Special day message", "Typical message 1", "Typical message 2", "Typical message 3"]);
});
});
describe("specialDayUnique is true", () => {
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 () => {
await doTest(["Special day message"]);
});
});
describe("cron type key", () => {
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 () => {
await doTest(["anytime cron"]);
});
});
});
describe("Feature remote compliments file", () => {
describe("get list from remote file", () => {
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");
await doTest(["Remote compliment file works!"]);
});
// afterAll(async () =>{
// await helpers.stopApplication()
// });
});
describe("get list from remote file w update", () => {
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");
await doTest(["Remote compliment file works!"]);
await new Promise((r) => setTimeout(r, 10000));
await doTest(["test in morning"]);
});
// afterAll(async () =>{
// await helpers.stopApplication()
// });
});
});
});
+34
View File
@@ -0,0 +1,34 @@
const { expect } = require("playwright/test");
const helpers = require("../helpers/global-setup");
describe("Test helloworld module", () => {
let page;
afterAll(async () => {
await helpers.stopApplication();
});
describe("helloworld set config text", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/helloworld/helloworld.js");
await helpers.getDocument();
page = helpers.getPage();
});
it("Test message helloworld module", async () => {
await expect(page.locator(".helloworld")).toContainText("Test HelloWorld Module");
});
});
describe("helloworld default config text", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/helloworld/helloworld_default.js");
await helpers.getDocument();
page = helpers.getPage();
});
it("Test message helloworld module", async () => {
await expect(page.locator(".helloworld")).toContainText("Hello World!");
});
});
});
+260
View File
@@ -0,0 +1,260 @@
const fs = require("node:fs");
const { expect } = require("playwright/test");
const helpers = require("../helpers/global-setup");
const runTests = () => {
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 () => {
await expect(page.locator(".newsfeed .newsfeed-source")).toContainText("Rodrigo Ramirez Blog");
});
it("should show the newsfeed article", async () => {
await expect(page.locator(".newsfeed .newsfeed-title")).toContainText("QPanel");
});
it("should NOT show the newsfeed description", async () => {
await page.locator(".newsfeed").waitFor({ state: "visible" });
await expect(page.locator(".newsfeed .newsfeed-desc")).toHaveCount(0);
});
});
describe("Custom configuration", () => {
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 () => {
await expect(page.locator(".newsfeed .newsfeed-title")).toContainText("Problema VirtualBox");
});
it("should show the newsfeed description", async () => {
const locator = page.locator(".newsfeed .newsfeed-desc");
await expect(locator).toBeVisible();
const text = await locator.textContent();
expect(text).toMatch(/\S/);
});
});
describe("Basic HTML tags", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/newsfeed/basic_html.js");
await helpers.getDocument();
page = helpers.getPage();
});
it("should render allowlisted formatting tags in title and description", async () => {
await expect(page.locator(".newsfeed .newsfeed-desc")).toBeVisible();
const descHtml = await page.locator(".newsfeed .newsfeed-desc").innerHTML();
expect(descHtml).toContain("<em>");
expect(descHtml).toContain("<strong>");
expect(descHtml).toContain("<u>");
const titleHtml = await page.locator(".newsfeed .newsfeed-title").innerHTML();
expect(titleHtml).toContain("<em>");
});
it("should strip disallowed HTML and not execute injected scripts", async () => {
const descHtml = await page.locator(".newsfeed .newsfeed-desc").innerHTML();
expect(descHtml).not.toContain("<script");
expect(descHtml).not.toContain("onerror");
const xss = await page.evaluate(() => window.__newsfeedXss);
expect(xss).toBeUndefined();
});
});
describe("Invalid configuration", () => {
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 () => {
await expect(page.locator(".newsfeed .small")).toContainText("Error in the Newsfeed module. Malformed url.");
});
});
describe("Ignore items", () => {
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 () => {
await expect(page.locator(".newsfeed .small")).toContainText("No news at the moment.");
});
});
};
describe("Newsfeed module > Notifications", () => {
let page;
afterAll(async () => {
await helpers.stopApplication();
});
/**
* Helper: call notificationReceived on the newsfeed module directly.
* @param {object} p - playwright page
* @param {string} notification - notification name
* @param {object} payload - notification payload
* @returns {Promise<void>} resolves when the notification has been dispatched
*/
const notify = (p, notification, payload = {}) => p.evaluate(
({ n, pl }) => {
const nf = MM.getModules().find((m) => m.name === "newsfeed");
nf.notificationReceived(n, pl, nf);
},
{ n: notification, pl: payload }
);
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/newsfeed/notifications.js");
await helpers.getDocument();
page = helpers.getPage();
await expect(page.locator(".newsfeed .newsfeed-title")).toBeVisible();
});
it("ARTICLE_NEXT should show the next article", async () => {
const title1 = await page.locator(".newsfeed .newsfeed-title").textContent();
await notify(page, "ARTICLE_NEXT");
await expect(page.locator(".newsfeed .newsfeed-title")).not.toContainText(title1.trim());
});
it("ARTICLE_PREVIOUS should return to the previous article", async () => {
// Start at article 0, go to article 1, then back
await page.evaluate(() => {
const nf = MM.getModules().find((m) => m.name === "newsfeed");
nf.activeItem = 0;
nf.resetDescrOrFullArticleAndTimer();
nf.updateDom(0);
});
await expect(page.locator(".newsfeed .newsfeed-title")).toContainText("QPanel");
const title0 = await page.locator(".newsfeed .newsfeed-title").textContent();
await notify(page, "ARTICLE_NEXT");
await expect(page.locator(".newsfeed .newsfeed-title")).not.toContainText(title0.trim());
await notify(page, "ARTICLE_PREVIOUS");
await expect(page.locator(".newsfeed .newsfeed-title")).toContainText(title0.trim());
});
it("ARTICLE_NEXT should wrap around from the last article to the first", async () => {
// Jump to the last article
await page.evaluate(() => {
const nf = MM.getModules().find((m) => m.name === "newsfeed");
nf.activeItem = nf.newsItems.length - 1;
nf.resetDescrOrFullArticleAndTimer();
nf.updateDom(0);
});
await expect(page.locator(".newsfeed .newsfeed-title")).toBeVisible();
const titleLast = await page.locator(".newsfeed .newsfeed-title").textContent();
await notify(page, "ARTICLE_NEXT");
await expect(page.locator(".newsfeed .newsfeed-title")).not.toContainText(titleLast.trim());
// activeItem should now be 0
const activeItem = await page.evaluate(() => MM.getModules().find((m) => m.name === "newsfeed").activeItem);
expect(activeItem).toBe(0);
});
it("ARTICLE_PREVIOUS should wrap around from the first article to the last", async () => {
await page.evaluate(() => {
const nf = MM.getModules().find((m) => m.name === "newsfeed");
nf.activeItem = 0;
nf.resetDescrOrFullArticleAndTimer();
});
await notify(page, "ARTICLE_PREVIOUS");
const activeItem = await page.evaluate(() => {
const nf = MM.getModules().find((m) => m.name === "newsfeed");
return { activeItem: nf.activeItem, total: nf.newsItems.length };
});
expect(activeItem.activeItem).toBe(activeItem.total - 1);
});
it("ARTICLE_INFO_REQUEST should respond with title, source, date, desc and raw url", async () => {
await page.evaluate(() => {
const nf = MM.getModules().find((m) => m.name === "newsfeed");
nf.activeItem = 0;
nf.resetDescrOrFullArticleAndTimer();
});
const info = await page.evaluate(() => new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error("ARTICLE_INFO_RESPONSE timeout")), 3000);
const origSend = MM.sendNotification.bind(MM);
MM.sendNotification = function (n, p, s) {
if (n === "ARTICLE_INFO_RESPONSE") {
clearTimeout(timer);
MM.sendNotification = origSend;
resolve(p);
}
return origSend(n, p, s);
};
const nf = MM.getModules().find((m) => m.name === "newsfeed");
nf.notificationReceived("ARTICLE_INFO_REQUEST", {}, nf);
}));
expect(info).toHaveProperty("title");
expect(info).toHaveProperty("source");
expect(info).toHaveProperty("date");
expect(info).toHaveProperty("desc");
expect(info).toHaveProperty("url");
expect(info.title).toBe("QPanel 0.13.0");
expect(info.source).toBe("Rodrigo Ramirez Blog");
// URL must be the raw article URL, not a CORS proxy URL
expect(info.url).toMatch(/^https?:\/\//);
expect(info.url).not.toContain("localhost");
});
it("ARTICLE_LESS_DETAILS should reset the full article view", async () => {
// Simulate full article view being active
await page.evaluate(() => {
const nf = MM.getModules().find((m) => m.name === "newsfeed");
nf.config.showFullArticle = true;
nf.articleFrameCheckPending = false;
nf.articleUnavailable = false;
});
await notify(page, "ARTICLE_LESS_DETAILS");
const state = await page.evaluate(() => {
const nf = MM.getModules().find((m) => m.name === "newsfeed");
return { showFullArticle: nf.config.showFullArticle };
});
expect(state.showFullArticle).toBe(false);
// Normal newsfeed title should be visible again
await expect(page.locator(".newsfeed .newsfeed-title")).toBeVisible();
});
});
describe("Newsfeed module", () => {
afterAll(async () => {
await helpers.stopApplication();
});
runTests();
});
describe("Newsfeed module located in config directory", () => {
beforeAll(() => {
fs.cpSync(`${global.root_path}/${global.defaultModulesDir}/newsfeed`, `${global.root_path}/config/newsfeed`, { recursive: true });
process.env.MM_MODULES_DIR = "config";
});
afterAll(async () => {
await helpers.stopApplication();
});
runTests();
});
+98
View File
@@ -0,0 +1,98 @@
const { expect } = require("playwright/test");
const helpers = require("../helpers/global-setup");
const weatherFunc = require("../helpers/weather-functions");
describe("Weather module", () => {
let page;
afterAll(async () => {
await weatherFunc.stopApplication();
});
describe("Current weather", () => {
describe("Default configuration", () => {
beforeAll(async () => {
await weatherFunc.startApplication("tests/configs/modules/weather/currentweather_default.js", "weather_onecall_current.json");
page = helpers.getPage();
});
it("should render wind speed and wind direction", async () => {
await expect(page.locator(".weather .normal.medium span:nth-child(2)")).toHaveText("12 WSW");
});
it("should render temperature with icon", async () => {
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 &nbsp; which renders as \xa0
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(page.locator(".weather .normal.medium.feelslike span.dimmed .humidity")).toHaveText("93.7");
});
});
});
describe("Compliments Integration", () => {
beforeAll(async () => {
await weatherFunc.startApplication("tests/configs/modules/weather/currentweather_compliments.js", "weather_onecall_current.json");
page = helpers.getPage();
});
it("should render a compliment based on the current weather", async () => {
const compliment = page.locator(".compliments .module-content span");
await compliment.waitFor({ state: "visible" });
await expect(compliment).toHaveText("snow");
});
});
describe("Configuration Options", () => {
beforeAll(async () => {
await weatherFunc.startApplication("tests/configs/modules/weather/currentweather_options.js", "weather_onecall_current.json");
page = helpers.getPage();
});
it("should render windUnits in beaufort", async () => {
await expect(page.locator(".weather .normal.medium span:nth-child(2)")).toHaveText("6");
});
it("should render windDirection with an arrow", async () => {
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 () => {
await expect(page.locator(".weather .normal.medium .humidity")).toHaveText("93.7");
});
it("should render degreeLabel for temp", async () => {
await expect(page.locator(".weather .large span.bright.light")).toHaveText("1°C");
});
it("should render degreeLabel for feels like", async () => {
await expect(page.locator(".weather .normal.medium.feelslike span.dimmed")).toHaveText("Feels like -6°C");
});
});
describe("Current weather with imperial units", () => {
beforeAll(async () => {
await weatherFunc.startApplication("tests/configs/modules/weather/currentweather_units.js", "weather_onecall_current.json");
page = helpers.getPage();
});
it("should render wind in imperial units", async () => {
await expect(page.locator(".weather .normal.medium span:nth-child(2)")).toHaveText("26 WSW");
});
it("should render temperatures in fahrenheit", async () => {
await expect(page.locator(".weather .large span.bright.light")).toHaveText("34,7°");
});
it("should render 'feels like' in fahrenheit", async () => {
await expect(page.locator(".weather .normal.medium.feelslike span.dimmed")).toHaveText("Feels like 21,9°");
});
});
});
+128
View File
@@ -0,0 +1,128 @@
const { expect } = require("playwright/test");
const helpers = require("../helpers/global-setup");
const weatherFunc = require("../helpers/weather-functions");
describe("Weather module: Weather Forecast", () => {
let page;
afterAll(async () => {
await weatherFunc.stopApplication();
});
describe("Default configuration", () => {
beforeAll(async () => {
await weatherFunc.startApplication("tests/configs/modules/weather/forecastweather_default.js", "weather_onecall_forecast.json");
page = helpers.getPage();
});
const days = ["Today", "Tomorrow", "Sun", "Mon", "Tue"];
for (const [index, day] of days.entries()) {
it(`should render day ${day}`, async () => {
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 () => {
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 () => {
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 () => {
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 row = page.locator(`.weather table.small tr:nth-child(${index + 1})`);
await expect(row).toHaveAttribute("style", `opacity: ${opacity};`);
});
}
});
describe("Absolute configuration", () => {
beforeAll(async () => {
await weatherFunc.startApplication("tests/configs/modules/weather/forecastweather_absolute.js", "weather_onecall_forecast.json");
page = helpers.getPage();
});
const days = ["Fri", "Sat", "Sun", "Mon", "Tue"];
for (const [index, day] of days.entries()) {
it(`should render day ${day}`, async () => {
const dayCell = page.locator(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`);
await expect(dayCell).toHaveText(day);
});
}
});
describe("Configuration Options", () => {
beforeAll(async () => {
await weatherFunc.startApplication("tests/configs/modules/weather/forecastweather_options.js", "weather_onecall_forecast.json");
page = helpers.getPage();
});
it("should render custom table class", async () => {
await expect(page.locator(".weather table.myTableClass")).toBeVisible();
});
it("should render colored rows", async () => {
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 () => {
const precipCell = page.locator(`.weather table tr:nth-child(${index + 1}) td.precipitation-amount`);
await expect(precipCell).toHaveText(precipitation);
});
}
}
});
describe("Forecast weather with imperial units", () => {
beforeAll(async () => {
await weatherFunc.startApplication("tests/configs/modules/weather/forecastweather_units.js", "weather_onecall_forecast.json");
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 () => {
const tempCell = page.locator(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`);
await expect(tempCell).toHaveText(temp);
});
}
});
describe("Precipitation units", () => {
const precipitations = [undefined, "0.10 in"];
for (const [index, precipitation] of precipitations.entries()) {
if (precipitation) {
it(`should render precipitation amount ${precipitation}`, async () => {
const precipCell = page.locator(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-amount`);
await expect(precipCell).toHaveText(precipitation);
});
}
}
});
});
});
+74
View File
@@ -0,0 +1,74 @@
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;
afterAll(async () => {
await weatherFunc.stopApplication();
});
describe("Default configuration", () => {
beforeAll(async () => {
await weatherFunc.startApplication("tests/configs/modules/weather/hourlyweather_default.js", "weather_onecall_hourly.json");
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 () => {
const dayCell = page.locator(`.weather table.small tr:nth-child(${index + 1}) td.day`);
await expect(dayCell).toHaveText(hour);
});
}
});
describe("Hourly weather options", () => {
beforeAll(async () => {
await weatherFunc.startApplication("tests/configs/modules/weather/hourlyweather_options.js", "weather_onecall_hourly.json");
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 () => {
const dayCell = page.locator(`.weather table.small tr:nth-child(${index + 1}) td.day`);
await expect(dayCell).toHaveText(hour);
});
}
});
});
describe("Show precipitations", () => {
beforeAll(async () => {
await weatherFunc.startApplication("tests/configs/modules/weather/hourlyweather_showPrecipitation.js", "weather_onecall_hourly.json");
page = helpers.getPage();
});
describe("Shows precipitation amount", () => {
const amounts = [undefined, undefined, undefined, "0.13 mm", "0.13 mm"];
for (const [index, amount] of amounts.entries()) {
if (amount) {
it(`should render precipitation amount ${amount}`, async () => {
const amountCell = page.locator(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-amount`);
await expect(amountCell).toHaveText(amount);
});
}
}
});
describe("Shows precipitation probability", () => {
const probabilities = [undefined, undefined, "12 %", "36 %", "44 %"];
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(probability);
});
}
}
});
});
});
+25
View File
@@ -0,0 +1,25 @@
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();
});
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(page.locator("#module_0_helloworld .module-header")).toHaveText("test_header");
});
it("should show no header if no header text is specified", async () => {
await expect(page.locator("#module_1_helloworld .module-header")).toHaveText("undefined");
});
});
+24
View File
@@ -0,0 +1,24 @@
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 () => {
await expect(page.locator("#module_1_helloworld .module-content")).toContainText("MagicMirror²");
});
it("shows the project URL", async () => {
await expect(page.locator("#module_5_helloworld .module-content")).toContainText("https://magicmirror.builders/");
});
});
+27
View File
@@ -0,0 +1,27 @@
const helpers = require("./helpers/global-setup");
const getPage = () => helpers.getPage();
describe("Position of modules", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/positions.js");
await helpers.getDocument();
});
afterAll(async () => {
await helpers.stopApplication();
});
const positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
for (const position of positions) {
const className = position.replace("_", ".");
it(`should show text in ${position}`, async () => {
const locator = getPage().locator(`.${className} .module-content`).first();
await locator.waitFor({ state: "visible" });
const text = await locator.textContent();
expect(text).not.toBeNull();
expect(text).toContain(`Text in ${position}`);
});
}
});
+36
View File
@@ -0,0 +1,36 @@
const helpers = require("./helpers/global-setup");
describe("port directive configuration", () => {
describe("Set port 8090", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/port_8090.js");
});
afterAll(async () => {
await helpers.stopApplication();
});
it("should return 200", async () => {
const res = await fetch(`http://localhost:${global.testPort}`);
expect(res.status).toBe(200);
});
});
describe("Set port 8100 on environment variable MM_PORT", () => {
beforeAll(async () => {
process.env.MM_PORT = "8100";
await helpers.startApplication("tests/configs/port_8090.js");
});
afterAll(async () => {
await helpers.stopApplication();
delete process.env.MM_PORT;
});
it("should return 200", async () => {
expect(global.testPort).toBe(8100);
const res = await fetch(`http://localhost:${global.testPort}`);
expect(res.status).toBe(200);
});
});
});
+51
View File
@@ -0,0 +1,51 @@
const delay = (time) => {
return new Promise((resolve) => setTimeout(resolve, time));
};
const runConfigCheck = async () => {
const serverProcess = await require("node:child_process").spawnSync("node", ["--run", "config:check"], { env: process.env });
return await serverProcess.status;
};
describe("App environment", () => {
let serverProcess;
beforeAll(async () => {
// Use fixed port 8080 (tests run sequentially)
const testPort = 8080;
process.env.MM_CONFIG_FILE = "tests/configs/default.js";
process.env.MM_PORT = testPort.toString();
serverProcess = await require("node:child_process").spawn("node", ["--run", "server"], { env: process.env, detached: true });
// we have to wait until the server is started
await delay(2000);
});
afterAll(async () => {
await process.kill(-serverProcess.pid);
});
it("get request from http://localhost:8080 should return 200", async () => {
const res = await fetch("http://localhost:8080");
expect(res.status).toBe(200);
});
it("get request from http://localhost:8080/nothing should return 404", async () => {
const res = await fetch("http://localhost:8080/nothing");
expect(res.status).toBe(404);
});
});
describe("Check config", () => {
it("config check should return without errors", async () => {
process.env.MM_CONFIG_FILE = "tests/configs/default.js";
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";
const exitCode = await runConfigCheck();
expect(exitCode).toBe(1);
});
});
+242
View File
@@ -0,0 +1,242 @@
const fs = require("node:fs");
const path = require("node:path");
const { pathToFileURL } = require("node:url");
const helmet = require("helmet");
const { JSDOM } = require("jsdom");
const express = require("express");
const translations = require("../../translations/translations");
/**
* Helper function to create a fresh Translator instance with DOM environment.
* @returns {object} Object containing window and Translator
*/
function createTranslationTestEnvironment () {
// Setup DOM environment with Translator
const translatorJs = fs.readFileSync(path.join(__dirname, "..", "..", "js", "translator.js"), "utf-8");
const dom = new JSDOM("", { url: "http://localhost:3000", runScripts: "outside-only" });
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;
return { window, Translator: window.Translator };
}
describe("translations", () => {
let server;
beforeAll(() => {
const app = express();
app.use(helmet());
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
next();
});
app.use("/translations", express.static(path.join(__dirname, "..", "..", "translations")));
server = app.listen(3000);
});
afterAll(async () => {
await server.close();
});
it("should have a translation file in the specified path", () => {
for (const language in translations) {
const file = fs.statSync(translations[language]);
expect(file.isFile()).toBe(true);
}
});
describe("loadTranslations", () => {
let dom;
beforeEach(async () => {
// Create a new translation test environment for each test
const env = createTranslationTestEnvironment();
const window = env.window;
// Bridge JSDOM globals to Node.js so module.js (ES module) can access them
global.Log = window.Log;
global.Translator = window.Translator;
global.config = { language: "de" };
global.window = { name: "", mmVersion: "2.0.0" };
global.MM = { hideModule: () => {}, showModule: () => {}, sendNotification: () => {}, updateDom: () => {} };
global.nunjucks = {
Environment () {
this.addFilter = () => {};
this.renderString = () => "";
this.render = (_t, _d, cb) => cb(null, "");
},
WebLoader () {},
runtime: { markSafe: (str) => str }
};
// Import Module directly — eval can't handle ES module syntax
const modulePath = pathToFileURL(path.join(__dirname, "..", "..", "js", "module.js")).href;
const { Module } = await import(`${modulePath}?test=${Date.now()}`);
window.Module = Module;
// Expose config on window so tests can modify dom.window.config
window.config = global.config;
dom = { window };
});
afterEach(() => {
delete global.Log;
delete global.Translator;
delete global.config;
delete global.window;
delete global.MM;
delete global.nunjucks;
});
it("should load translation file", async () => {
const { Translator, Module, config } = dom.window;
config.language = "en";
Translator.load = vi.fn().mockImplementation(() => null);
Module.register("name", { getTranslations: () => translations });
const MMM = Module.create("name");
await MMM.loadTranslations();
expect(Translator.load.mock.calls).toHaveLength(1);
expect(Translator.load).toHaveBeenCalledWith(MMM, "translations/en.json", false);
});
it("should load translation + fallback file", async () => {
const { Translator, Module } = dom.window;
Translator.load = vi.fn().mockImplementation(() => null);
Module.register("name", { getTranslations: () => translations });
const MMM = Module.create("name");
await MMM.loadTranslations();
expect(Translator.load.mock.calls).toHaveLength(2);
expect(Translator.load).toHaveBeenCalledWith(MMM, "translations/de.json", false);
expect(Translator.load).toHaveBeenCalledWith(MMM, "translations/en.json", true);
});
it("should load translation fallback file", async () => {
const { Translator, Module, config } = dom.window;
config.language = "--";
Translator.load = vi.fn().mockImplementation(() => null);
Module.register("name", { getTranslations: () => translations });
const MMM = Module.create("name");
await MMM.loadTranslations();
expect(Translator.load.mock.calls).toHaveLength(1);
expect(Translator.load).toHaveBeenCalledWith(MMM, "translations/en.json", true);
});
it("should load no file", async () => {
const { Translator, Module } = dom.window;
Translator.load = vi.fn();
Module.register("name", {});
const MMM = Module.create("name");
await MMM.loadTranslations();
expect(Translator.load.mock.calls).toHaveLength(0);
});
});
const mmm = {
name: "TranslationTest",
file (file) {
return `http://localhost:3000/${file}`;
}
};
describe("parsing language files through the Translator class", () => {
for (const language in translations) {
it(`should parse ${language}`, async () => {
const { Translator } = createTranslationTestEnvironment();
await Translator.load(mmm, translations[language], false);
expect(typeof Translator.translations[mmm.name]).toBe("object");
expect(Object.keys(Translator.translations[mmm.name]).length).toBeGreaterThanOrEqual(1);
});
}
});
describe("same keys", () => {
let base;
// Some expressions are not easy to translate automatically. For the sake of a working test, we filter them out.
const COMMON_EXCEPTIONS = ["WEEK_SHORT"];
// Some languages don't have certain words, so we need to filter those language specific exceptions.
const LANGUAGE_EXCEPTIONS = {
ca: ["DAYBEFOREYESTERDAY"],
cv: ["DAYBEFOREYESTERDAY"],
cy: ["DAYBEFOREYESTERDAY"],
en: ["DAYAFTERTOMORROW", "DAYBEFOREYESTERDAY"],
fy: ["DAYBEFOREYESTERDAY"],
gl: ["DAYBEFOREYESTERDAY"],
hu: ["DAYBEFOREYESTERDAY"],
id: ["DAYBEFOREYESTERDAY"],
it: ["DAYBEFOREYESTERDAY"],
"pt-br": ["DAYAFTERTOMORROW"],
tr: ["DAYBEFOREYESTERDAY"]
};
// Function to initialize JSDOM and load translations
const initializeTranslationDOM = async (language) => {
const { Translator } = createTranslationTestEnvironment();
await Translator.load(mmm, translations[language], false);
return Translator.translations[mmm.name];
};
beforeAll(async () => {
// Using German as the base rather than English, since
// some words do not have a direct translation in English.
const germanTranslations = await initializeTranslationDOM("de");
base = Object.keys(germanTranslations).sort();
});
for (const language in translations) {
if (language === "de") continue;
describe(`Translation keys of ${language}`, () => {
let keys;
beforeAll(async () => {
const languageTranslations = await initializeTranslationDOM(language);
keys = Object.keys(languageTranslations).sort();
});
it(`${language} should not contain keys that are not in base language`, () => {
keys.forEach((key) => {
expect(base).toContain(key, `Translation key '${key}' in language '${language}' is not present in base language`);
});
});
it(`${language} should contain all base keys (excluding defined exceptions)`, () => {
let filteredBase = base.filter((key) => !COMMON_EXCEPTIONS.includes(key));
let filteredKeys = keys.filter((key) => !COMMON_EXCEPTIONS.includes(key));
if (LANGUAGE_EXCEPTIONS[language]) {
const exceptions = LANGUAGE_EXCEPTIONS[language];
filteredBase = filteredBase.filter((key) => !exceptions.includes(key));
filteredKeys = filteredKeys.filter((key) => !exceptions.includes(key));
}
filteredBase.forEach((baseKey) => {
expect(filteredKeys).toContain(baseKey, `Translation key '${baseKey}' is missing in language '${language}'`);
});
});
});
}
});
});
+23
View File
@@ -0,0 +1,23 @@
const helpers = require("./helpers/global-setup");
describe("Vendors", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/default.js");
});
afterAll(async () => {
await helpers.stopApplication();
});
describe("Get list vendors", () => {
const vendors = require(`${global.root_path}/js/vendor.js`);
Object.keys(vendors).forEach((vendor) => {
it(`should return 200 HTTP code for vendor "${vendor}"`, async () => {
const urlVendor = `http://localhost:8080/${vendors[vendor]}`;
const res = await fetch(urlVendor);
expect(res.status).toBe(200);
});
});
});
});