diff --git a/CHANGELOG.md b/CHANGELOG.md index 79a9ee8f..9da03d9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,13 @@ Thanks to: @dathbe. - [tests] refactor: add `setupDOMEnvironment` helper function to eliminate repetitive JSDOM setup code (#3860) - [tests] replace `console` with `Log` in calendar `debug.js` to avoid exception in eslint config (#3846) - [tests] speed up e2e tests, cleanup and stabilize weather e2e tests, update snapshot url (#3847, #3848, #3861) +- [tests] refactor translation tests (#3866) + - Remove `sinon` dependency in favor of Jest native mocking + - Unify test helper functions across translation test suites + - Rename `setupDOMEnvironment` to `createTranslationTestEnvironment` for consistency + - Simplify DOM setup by removing unnecessary Promise/async patterns + - Avoid potential port conflicts by using port 3001 for translator unit tests + - Improve test reliability and maintainability ### Updated diff --git a/package-lock.json b/package-lock.json index 148d42ea..283bbd9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,7 +52,6 @@ "markdownlint-cli2": "^0.18.1", "playwright": "^1.55.0", "prettier": "^3.6.2", - "sinon": "^21.0.0", "stylelint": "^16.23.1", "stylelint-config-standard": "^39.0.0", "stylelint-prettier": "^5.0.3" @@ -2717,28 +2716,6 @@ "@sinonjs/commons": "^3.0.1" } }, - "node_modules/@sinonjs/samsam": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", - "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "lodash.get": "^4.4.2", - "type-detect": "^4.1.0" - } - }, - "node_modules/@sinonjs/samsam/node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -9534,14 +9511,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -12791,34 +12760,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/sinon": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.0.tgz", - "integrity": "sha512-TOgRcwFPbfGtpqvZw+hyqJDvqfapr1qUlOizROIk4bBLjlsjlB00Pg6wMFXNtJRpu+eCZuVOaLatG7M8105kAw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^13.0.5", - "@sinonjs/samsam": "^8.0.1", - "diff": "^7.0.0", - "supports-color": "^7.2.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/sinon" - } - }, - "node_modules/sinon/node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", diff --git a/package.json b/package.json index 0ec1a006..7e2963cd 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,6 @@ "markdownlint-cli2": "^0.18.1", "playwright": "^1.55.0", "prettier": "^3.6.2", - "sinon": "^21.0.0", "stylelint": "^16.23.1", "stylelint-config-standard": "^39.0.0", "stylelint-prettier": "^5.0.3" diff --git a/tests/e2e/translations_spec.js b/tests/e2e/translations_spec.js index 9ea46205..c8489ace 100644 --- a/tests/e2e/translations_spec.js +++ b/tests/e2e/translations_spec.js @@ -3,22 +3,24 @@ const path = require("node:path"); const helmet = require("helmet"); const { JSDOM } = require("jsdom"); const express = require("express"); -const sinon = require("sinon"); const translations = require("../../translations/translations"); /** - * Helper function to setup DOM environment. - * @returns {object} The JSDOM window object + * Helper function to create a fresh Translator instance with DOM environment. + * @returns {object} Object containing window and Translator */ -function setupDOMEnvironment () { - const dom = new JSDOM("", { runScripts: "dangerously", resources: "usable" }); +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: jest.fn(), error: jest.fn() }; - const translatorJs = fs.readFileSync(path.join(__dirname, "..", "..", "js", "translator.js"), "utf-8"); dom.window.translations = translations; dom.window.eval(translatorJs); - return dom.window; + const window = dom.window; + + return { window, Translator: window.Translator }; } describe("translations", () => { @@ -52,91 +54,76 @@ describe("translations", () => { let dom; beforeEach(() => { - // Create a new JSDOM instance for each test - const window = setupDOMEnvironment(); - dom = { window }; + // Create a new translation test environment for each test + const env = createTranslationTestEnvironment(); + const window = env.window; - // Additional setup for loadTranslations tests - dom.window.Translator = {}; - dom.window.config = { language: "de" }; - - // Load class.js and module.js content directly + // Load class.js and module.js content directly for loadTranslations tests const classJs = fs.readFileSync(path.join(__dirname, "..", "..", "js", "class.js"), "utf-8"); const moduleJs = fs.readFileSync(path.join(__dirname, "..", "..", "js", "module.js"), "utf-8"); // Execute the scripts in the JSDOM context - dom.window.eval(classJs); - dom.window.eval(moduleJs); + window.eval(classJs); + window.eval(moduleJs); + + // Additional setup for loadTranslations tests + window.config = { language: "de" }; + + dom = { window }; }); it("should load translation file", async () => { - await new Promise((resolve) => { - dom.window.onload = resolve; - }); - const { Translator, Module, config } = dom.window; config.language = "en"; - Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null); + Translator.load = jest.fn().mockImplementation((_m, _f, _fb) => null); Module.register("name", { getTranslations: () => translations }); const MMM = Module.create("name"); await MMM.loadTranslations(); - expect(Translator.load.args).toHaveLength(1); - expect(Translator.load.calledWith(MMM, "translations/en.json", false)).toBe(true); + expect(Translator.load.mock.calls).toHaveLength(1); + expect(Translator.load).toHaveBeenCalledWith(MMM, "translations/en.json", false); }); it("should load translation + fallback file", async () => { - await new Promise((resolve) => { - dom.window.onload = resolve; - }); - const { Translator, Module } = dom.window; - Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null); + Translator.load = jest.fn().mockImplementation((_m, _f, _fb) => null); Module.register("name", { getTranslations: () => translations }); const MMM = Module.create("name"); await MMM.loadTranslations(); - expect(Translator.load.args).toHaveLength(2); - expect(Translator.load.calledWith(MMM, "translations/de.json", false)).toBe(true); - expect(Translator.load.calledWith(MMM, "translations/en.json", true)).toBe(true); + 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 () => { - await new Promise((resolve) => { - dom.window.onload = resolve; - }); - const { Translator, Module, config } = dom.window; config.language = "--"; - Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null); + Translator.load = jest.fn().mockImplementation((_m, _f, _fb) => null); Module.register("name", { getTranslations: () => translations }); const MMM = Module.create("name"); await MMM.loadTranslations(); - expect(Translator.load.args).toHaveLength(1); - expect(Translator.load.calledWith(MMM, "translations/en.json", true)).toBe(true); + expect(Translator.load.mock.calls).toHaveLength(1); + expect(Translator.load).toHaveBeenCalledWith(MMM, "translations/en.json", true); }); it("should load no file", async () => { - await new Promise((resolve) => { - dom.window.onload = resolve; - }); - const { Translator, Module } = dom.window; - Translator.load = sinon.stub(); + Translator.load = jest.fn(); Module.register("name", {}); const MMM = Module.create("name"); await MMM.loadTranslations(); - expect(Translator.load.callCount).toBe(0); + expect(Translator.load.mock.calls).toHaveLength(0); }); }); @@ -150,13 +137,7 @@ describe("translations", () => { describe("parsing language files through the Translator class", () => { for (const language in translations) { it(`should parse ${language}`, async () => { - const window = setupDOMEnvironment(); - - await new Promise((resolve) => { - window.onload = resolve; - }); - - const { Translator } = window; + const { Translator } = createTranslationTestEnvironment(); await Translator.load(mmm, translations[language], false); expect(typeof Translator.translations[mmm.name]).toBe("object"); @@ -187,16 +168,10 @@ describe("translations", () => { }; // Function to initialize JSDOM and load translations - const initializeTranslationDOM = (language) => { - const window = setupDOMEnvironment(); - - return new Promise((resolve) => { - window.onload = async () => { - const { Translator } = window; - await Translator.load(mmm, translations[language], false); - resolve(Translator.translations[mmm.name]); - }; - }); + const initializeTranslationDOM = async (language) => { + const { Translator } = createTranslationTestEnvironment(); + await Translator.load(mmm, translations[language], false); + return Translator.translations[mmm.name]; }; beforeAll(async () => { diff --git a/tests/unit/classes/translator_spec.js b/tests/unit/classes/translator_spec.js index a12c7c6a..37cb9990 100644 --- a/tests/unit/classes/translator_spec.js +++ b/tests/unit/classes/translator_spec.js @@ -5,25 +5,22 @@ const { JSDOM } = require("jsdom"); const express = require("express"); /** - * Helper function to setup DOM environment. - * @param {string} scriptContent - The script content to evaluate - * @returns {Promise} The JSDOM window object + * Helper function to create a fresh Translator instance with DOM environment. + * @returns {object} Object containing window and Translator */ -async function setupDOMEnvironment (scriptContent) { - const dom = new JSDOM("", { runScripts: "outside-only" }); +function createTranslationTestEnvironment () { + const translatorJs = fs.readFileSync(path.join(__dirname, "..", "..", "..", "js", "translator.js"), "utf-8"); + const dom = new JSDOM("", { url: "http://localhost:3001", runScripts: "outside-only" }); - dom.window.eval(scriptContent); dom.window.Log = { log: jest.fn(), error: jest.fn() }; + dom.window.eval(translatorJs); - await new Promise((resolve) => dom.window.onload = resolve); - return dom.window; + return { window: dom.window, Translator: dom.window.Translator }; } describe("Translator", () => { let server; const sockets = new Set(); - const translatorJsPath = path.join(__dirname, "..", "..", "..", "js", "translator.js"); - const translatorJsScriptContent = fs.readFileSync(translatorJsPath, "utf8"); const translationTestData = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "..", "..", "tests", "mocks", "translation_test.json"), "utf8")); beforeAll(() => { @@ -35,7 +32,7 @@ describe("Translator", () => { }); app.use("/translations", express.static(path.join(__dirname, "..", "..", "..", "tests", "mocks"))); - server = app.listen(3000); + server = app.listen(3001); server.on("connection", (socket) => { sockets.add(socket); @@ -96,8 +93,7 @@ describe("Translator", () => { }; it("should return custom module translation", async () => { - const window = await setupDOMEnvironment(translatorJsScriptContent); - const { Translator } = window; + const { Translator } = createTranslationTestEnvironment(); setTranslations(Translator); let translation = Translator.translate({ name: "MMM-Module" }, "Hello"); @@ -108,8 +104,7 @@ describe("Translator", () => { }); it("should return core translation", async () => { - const window = await setupDOMEnvironment(translatorJsScriptContent); - const { Translator } = window; + const { Translator } = createTranslationTestEnvironment(); setTranslations(Translator); let translation = Translator.translate({ name: "MMM-Module" }, "FOO"); expect(translation).toBe("Foo"); @@ -118,32 +113,28 @@ describe("Translator", () => { }); it("should return custom module translation fallback", async () => { - const window = await setupDOMEnvironment(translatorJsScriptContent); - const { Translator } = window; + const { Translator } = createTranslationTestEnvironment(); setTranslations(Translator); const translation = Translator.translate({ name: "MMM-Module" }, "A key"); expect(translation).toBe("A translation"); }); it("should return core translation fallback", async () => { - const window = await setupDOMEnvironment(translatorJsScriptContent); - const { Translator } = window; + const { Translator } = createTranslationTestEnvironment(); setTranslations(Translator); const translation = Translator.translate({ name: "MMM-Module" }, "Fallback"); expect(translation).toBe("core fallback"); }); it("should return translation with placeholder for missing variables", async () => { - const window = await setupDOMEnvironment(translatorJsScriptContent); - const { Translator } = window; + const { Translator } = createTranslationTestEnvironment(); setTranslations(Translator); const translation = Translator.translate({ name: "MMM-Module" }, "Hello {username}"); expect(translation).toBe("Hallo {username}"); }); it("should return key if no translation was found", async () => { - const window = await setupDOMEnvironment(translatorJsScriptContent); - const { Translator } = window; + const { Translator } = createTranslationTestEnvironment(); setTranslations(Translator); const translation = Translator.translate({ name: "MMM-Module" }, "MISSING"); expect(translation).toBe("MISSING"); @@ -154,13 +145,12 @@ describe("Translator", () => { const mmm = { name: "TranslationTest", file (file) { - return `http://localhost:3000/translations/${file}`; + return `http://localhost:3001/translations/${file}`; } }; it("should load translations", async () => { - const window = await setupDOMEnvironment(translatorJsScriptContent); - const { Translator } = window; + const { Translator } = createTranslationTestEnvironment(); const file = "translation_test.json"; await Translator.load(mmm, file, false); @@ -169,8 +159,7 @@ describe("Translator", () => { }); it("should load translation fallbacks", async () => { - const window = await setupDOMEnvironment(translatorJsScriptContent); - const { Translator } = window; + const { Translator } = createTranslationTestEnvironment(); const file = "translation_test.json"; await Translator.load(mmm, file, true); @@ -179,8 +168,7 @@ describe("Translator", () => { }); it("should not load translations, if module fallback exists", async () => { - const window = await setupDOMEnvironment(translatorJsScriptContent); - const { Translator } = window; + const { Translator } = createTranslationTestEnvironment(); const file = "translation_test.json"; Translator.translationsFallback[mmm.name] = { @@ -197,29 +185,23 @@ describe("Translator", () => { describe("loadCoreTranslations", () => { it("should load core translations and fallback", async () => { - const window = await setupDOMEnvironment(translatorJsScriptContent); - window.translations = { en: "http://localhost:3000/translations/translation_test.json" }; - const { Translator } = window; + const { window, Translator } = createTranslationTestEnvironment(); + window.translations = { en: "http://localhost:3001/translations/translation_test.json" }; await Translator.loadCoreTranslations("en"); const en = translationTestData; - await new Promise((resolve) => setTimeout(resolve, 500)); - expect(Translator.coreTranslations).toEqual(en); expect(Translator.coreTranslationsFallback).toEqual(en); }); it("should load core fallback if language cannot be found", async () => { - const window = await setupDOMEnvironment(translatorJsScriptContent); - window.translations = { en: "http://localhost:3000/translations/translation_test.json" }; - const { Translator } = window; + const { window, Translator } = createTranslationTestEnvironment(); + window.translations = { en: "http://localhost:3001/translations/translation_test.json" }; await Translator.loadCoreTranslations("MISSINGLANG"); const en = translationTestData; - await new Promise((resolve) => setTimeout(resolve, 500)); - expect(Translator.coreTranslations).toEqual({}); expect(Translator.coreTranslationsFallback).toEqual(en); }); @@ -227,26 +209,20 @@ describe("Translator", () => { describe("loadCoreTranslationsFallback", () => { it("should load core translations fallback", async () => { - const window = await setupDOMEnvironment(translatorJsScriptContent); - window.translations = { en: "http://localhost:3000/translations/translation_test.json" }; - const { Translator } = window; + const { window, Translator } = createTranslationTestEnvironment(); + window.translations = { en: "http://localhost:3001/translations/translation_test.json" }; await Translator.loadCoreTranslationsFallback(); const en = translationTestData; - await new Promise((resolve) => setTimeout(resolve, 500)); - expect(Translator.coreTranslationsFallback).toEqual(en); }); it("should load core fallback if language cannot be found", async () => { - const window = await setupDOMEnvironment(translatorJsScriptContent); + const { window, Translator } = createTranslationTestEnvironment(); window.translations = {}; - const { Translator } = window; await Translator.loadCoreTranslations(); - await new Promise((resolve) => setTimeout(resolve, 500)); - expect(Translator.coreTranslationsFallback).toEqual({}); }); });