chore: import upstream snapshot with attribution
Enforce Pull-Request Rules / check (push) Has been cancelled
Enforce Pull-Request Rules / check (push) Has been cancelled
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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**"]);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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²");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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!");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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()
|
||||
// });
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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!");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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 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°");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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/");
|
||||
});
|
||||
});
|
||||
@@ -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}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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}'`);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user