[tests] Review and refactor translation tests (#3792)

I have refactored the translations tests, they should now be clearer and
easier to understand. There should be no functional impact.

I have discarded the original approach of also replacing
`XMLHttpRequest` with `fetch` in the file `js/translator.js`. I had
managed to get it to work functionally, but I couldn't get the tests to
work.
This commit is contained in:
Kristjan ESPERANTO
2025-06-21 00:37:15 +02:00
committed by GitHub
parent c7c0e67c1d
commit 2809ed1750
3 changed files with 262 additions and 277 deletions

View File

@@ -38,7 +38,8 @@ planned for 2025-07-01
- Removed as many of the date conversions as possible
- Use `moment-timezone` when calculating recurring events, this will fix problems from the past with offsets and DST not being handled properly
- Added some tests to test the behavior of the refactored methods to make sure the correct event dates are returned
- [linter] Enable ESLint rule `no-console` and replace `console` with `Log` in some files
- [linter] Enable ESLint rule `no-console` and replace `console` with `Log` in some files (#3810)
- [tests] Review and refactor translation tests (#3792)
### Fixed

View File

@@ -6,7 +6,7 @@ const express = require("express");
const sinon = require("sinon");
const translations = require("../../translations/translations");
describe("Translations", () => {
describe("translations", () => {
let server;
beforeAll(() => {
@@ -26,8 +26,9 @@ describe("Translations", () => {
});
it("should have a translation file in the specified path", () => {
for (let language in translations) {
for (const language in translations) {
const file = fs.statSync(translations[language]);
expect(file.isFile()).toBe(true);
}
});
@@ -36,90 +37,91 @@ describe("Translations", () => {
let dom;
beforeEach(() => {
dom = new JSDOM(
`<script>var Translator = {}; var Log = {log: () => {}}; var config = {language: 'de'};</script>\
<script src="file://${path.join(__dirname, "..", "..", "js", "class.js")}"></script>\
<script src="file://${path.join(__dirname, "..", "..", "js", "module.js")}"></script>`,
{ runScripts: "dangerously", resources: "usable" }
);
// Create a new JSDOM instance for each test
dom = new JSDOM("", { runScripts: "dangerously", resources: "usable" });
// Mock the necessary global objects
dom.window.Log = { log: jest.fn(), error: jest.fn() };
dom.window.Translator = {};
dom.window.config = { language: "de" };
// Load class.js and module.js content directly
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);
});
it("should load translation file", () => {
return new Promise((done) => {
dom.window.onload = async () => {
const { Translator, Module, config } = dom.window;
config.language = "en";
Translator.load = sinon.stub().callsFake((_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);
done();
};
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);
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);
});
it("should load translation + fallback file", () => {
return new Promise((done) => {
dom.window.onload = async () => {
const { Translator, Module } = dom.window;
Translator.load = sinon.stub().callsFake((_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);
done();
};
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);
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);
});
it("should load translation fallback file", () => {
return new Promise((done) => {
dom.window.onload = async () => {
const { Translator, Module, config } = dom.window;
config.language = "--";
Translator.load = sinon.stub().callsFake((_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);
done();
};
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);
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);
});
it("should load no file", () => {
return new Promise((done) => {
dom.window.onload = async () => {
const { Translator, Module } = dom.window;
Translator.load = sinon.stub();
Module.register("name", {});
const MMM = Module.create("name");
await MMM.loadTranslations();
expect(Translator.load.callCount).toBe(0);
done();
};
it("should load no file", async () => {
await new Promise((resolve) => {
dom.window.onload = resolve;
});
const { Translator, Module } = dom.window;
Translator.load = sinon.stub();
Module.register("name", {});
const MMM = Module.create("name");
await MMM.loadTranslations();
expect(Translator.load.callCount).toBe(0);
});
});
@@ -130,29 +132,30 @@ describe("Translations", () => {
}
};
describe("Parsing language files through the Translator class", () => {
for (let language in translations) {
it(`should parse ${language}`, () => {
return new Promise((done) => {
const dom = new JSDOM(
`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: () => {}};</script>\
<script src="file://${path.join(__dirname, "..", "..", "js", "translator.js")}">`,
{ runScripts: "dangerously", resources: "usable" }
);
dom.window.onload = async () => {
const { Translator } = dom.window;
const translatorJs = fs.readFileSync(path.join(__dirname, "..", "..", "js", "translator.js"), "utf-8");
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);
done();
};
describe("parsing language files through the Translator class", () => {
for (const language in translations) {
it(`should parse ${language}`, async () => {
const dom = new JSDOM("", { runScripts: "dangerously", resources: "usable" });
dom.window.Log = { log: jest.fn() };
dom.window.translations = translations;
dom.window.eval(translatorJs);
await new Promise((resolve) => {
dom.window.onload = resolve;
});
const { Translator } = dom.window;
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", () => {
describe("same keys", () => {
let base;
// Some expressions are not easy to translate automatically. For the sake of a working test, we filter them out.
@@ -178,7 +181,6 @@ describe("Translations", () => {
const dom = new JSDOM("", { runScripts: "dangerously", resources: "usable" });
dom.window.Log = { log: jest.fn() };
dom.window.translations = translations;
const translatorJs = fs.readFileSync(path.join(__dirname, "..", "..", "js", "translator.js"), "utf-8");
dom.window.eval(translatorJs);
return new Promise((resolve) => {

View File

@@ -1,12 +1,15 @@
const fs = require("node:fs");
const path = require("node:path");
const helmet = require("helmet");
const { JSDOM } = require("jsdom");
const express = require("express");
const sockets = new Set();
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(() => {
const app = express();
@@ -77,86 +80,82 @@ describe("Translator", () => {
Translator.coreTranslationsFallback = coreTranslationsFallback;
};
it("should return custom module translation", () => {
return new Promise((done) => {
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = () => {
const { Translator } = dom.window;
setTranslations(Translator);
let translation = Translator.translate({ name: "MMM-Module" }, "Hello");
expect(translation).toBe("Hallo");
translation = Translator.translate({ name: "MMM-Module" }, "Hello {username}", { username: "fewieden" });
expect(translation).toBe("Hallo fewieden");
done();
};
});
it("should return custom module translation", async () => {
const dom = new JSDOM("", { runScripts: "outside-only" });
dom.window.eval(translatorJsScriptContent);
await new Promise((resolve) => dom.window.onload = resolve);
const { Translator } = dom.window;
setTranslations(Translator);
let translation = Translator.translate({ name: "MMM-Module" }, "Hello");
expect(translation).toBe("Hallo");
translation = Translator.translate({ name: "MMM-Module" }, "Hello {username}", { username: "fewieden" });
expect(translation).toBe("Hallo fewieden");
});
it("should return core translation", () => {
return new Promise((done) => {
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = () => {
const { Translator } = dom.window;
setTranslations(Translator);
let translation = Translator.translate({ name: "MMM-Module" }, "FOO");
expect(translation).toBe("Foo");
translation = Translator.translate({ name: "MMM-Module" }, "BAR {something}", { something: "Lorem Ipsum" });
expect(translation).toBe("Bar Lorem Ipsum");
done();
};
});
it("should return core translation", async () => {
const dom = new JSDOM("", { runScripts: "outside-only" });
dom.window.eval(translatorJsScriptContent);
await new Promise((resolve) => dom.window.onload = resolve);
const { Translator } = dom.window;
setTranslations(Translator);
let translation = Translator.translate({ name: "MMM-Module" }, "FOO");
expect(translation).toBe("Foo");
translation = Translator.translate({ name: "MMM-Module" }, "BAR {something}", { something: "Lorem Ipsum" });
expect(translation).toBe("Bar Lorem Ipsum");
});
it("should return custom module translation fallback", () => {
return new Promise((done) => {
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = () => {
const { Translator } = dom.window;
setTranslations(Translator);
const translation = Translator.translate({ name: "MMM-Module" }, "A key");
expect(translation).toBe("A translation");
done();
};
});
it("should return custom module translation fallback", async () => {
const dom = new JSDOM("", { runScripts: "outside-only" });
dom.window.eval(translatorJsScriptContent);
await new Promise((resolve) => dom.window.onload = resolve);
const { Translator } = dom.window;
setTranslations(Translator);
const translation = Translator.translate({ name: "MMM-Module" }, "A key");
expect(translation).toBe("A translation");
});
it("should return core translation fallback", () => {
return new Promise((done) => {
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = () => {
const { Translator } = dom.window;
setTranslations(Translator);
const translation = Translator.translate({ name: "MMM-Module" }, "Fallback");
expect(translation).toBe("core fallback");
done();
};
});
it("should return core translation fallback", async () => {
const dom = new JSDOM("", { runScripts: "outside-only" });
dom.window.eval(translatorJsScriptContent);
await new Promise((resolve) => dom.window.onload = resolve);
const { Translator } = dom.window;
setTranslations(Translator);
const translation = Translator.translate({ name: "MMM-Module" }, "Fallback");
expect(translation).toBe("core fallback");
});
it("should return translation with placeholder for missing variables", () => {
return new Promise((done) => {
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = () => {
const { Translator } = dom.window;
setTranslations(Translator);
const translation = Translator.translate({ name: "MMM-Module" }, "Hello {username}");
expect(translation).toBe("Hallo {username}");
done();
};
});
it("should return translation with placeholder for missing variables", async () => {
const dom = new JSDOM("", { runScripts: "outside-only" });
dom.window.eval(translatorJsScriptContent);
await new Promise((resolve) => dom.window.onload = resolve);
const { Translator } = dom.window;
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", () => {
return new Promise((done) => {
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = () => {
const { Translator } = dom.window;
setTranslations(Translator);
const translation = Translator.translate({ name: "MMM-Module" }, "MISSING");
expect(translation).toBe("MISSING");
done();
};
});
it("should return key if no translation was found", async () => {
const dom = new JSDOM("", { runScripts: "outside-only" });
dom.window.eval(translatorJsScriptContent);
await new Promise((resolve) => dom.window.onload = resolve);
const { Translator } = dom.window;
setTranslations(Translator);
const translation = Translator.translate({ name: "MMM-Module" }, "MISSING");
expect(translation).toBe("MISSING");
});
});
@@ -168,144 +167,127 @@ describe("Translator", () => {
}
};
it("should load translations", () => {
return new Promise((done) => {
const dom = new JSDOM(`<script>var Log = {log: () => {}};</script><script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = async () => {
const { Translator } = dom.window;
const file = "translation_test.json";
it("should load translations", async () => {
const dom = new JSDOM("", { runScripts: "outside-only" });
dom.window.eval(translatorJsScriptContent);
dom.window.Log = { log: jest.fn() };
await new Promise((resolve) => dom.window.onload = resolve);
await Translator.load(mmm, file, false);
const json = require(path.join(__dirname, "..", "..", "..", "tests", "mocks", file));
expect(Translator.translations[mmm.name]).toEqual(json);
done();
};
});
const { Translator } = dom.window;
const file = "translation_test.json";
await Translator.load(mmm, file, false);
const json = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "..", "..", "tests", "mocks", file), "utf8"));
expect(Translator.translations[mmm.name]).toEqual(json);
});
it("should load translation fallbacks", () => {
return new Promise((done) => {
const dom = new JSDOM(`<script>var Log = {log: () => {}};</script><script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = async () => {
const { Translator } = dom.window;
const file = "translation_test.json";
it("should load translation fallbacks", async () => {
const dom = new JSDOM("", { runScripts: "outside-only" });
dom.window.eval(translatorJsScriptContent);
await Translator.load(mmm, file, true);
const json = require(path.join(__dirname, "..", "..", "..", "tests", "mocks", file));
expect(Translator.translationsFallback[mmm.name]).toEqual(json);
done();
};
});
await new Promise((resolve) => dom.window.onload = resolve);
const { Translator } = dom.window;
const file = "translation_test.json";
dom.window.Log = { log: jest.fn() };
await Translator.load(mmm, file, true);
const json = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "..", "..", "tests", "mocks", file), "utf8"));
expect(Translator.translationsFallback[mmm.name]).toEqual(json);
});
it("should not load translations, if module fallback exists", () => {
return new Promise((done) => {
const dom = new JSDOM(`<script>var Log = {log: () => {}};</script><script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = async () => {
const { Translator, XMLHttpRequest } = dom.window;
const file = "translation_test.json";
it("should not load translations, if module fallback exists", async () => {
const dom = new JSDOM("", { runScripts: "outside-only" });
dom.window.eval(translatorJsScriptContent);
await new Promise((resolve) => dom.window.onload = resolve);
XMLHttpRequest.prototype.send = () => {
throw new Error("Shouldn't load files");
};
const { Translator } = dom.window;
const file = "translation_test.json";
Translator.translationsFallback[mmm.name] = {
Hello: "Hallo"
};
await Translator.load(mmm, file, false);
expect(Translator.translations[mmm.name]).toBeUndefined();
expect(Translator.translationsFallback[mmm.name]).toEqual({
Hello: "Hallo"
});
done();
};
dom.window.Log = { log: jest.fn() };
Translator.translationsFallback[mmm.name] = {
Hello: "Hallo"
};
await Translator.load(mmm, file, false);
expect(Translator.translations[mmm.name]).toBeUndefined();
expect(Translator.translationsFallback[mmm.name]).toEqual({
Hello: "Hallo"
});
});
});
describe("loadCoreTranslations", () => {
it("should load core translations and fallback", () => {
return new Promise((done) => {
const dom = new JSDOM(
`<script>var translations = {en: "http://localhost:3000/translations/translation_test.json"}; var Log = {log: () => {}};</script>\
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
{ runScripts: "dangerously", resources: "usable" }
);
dom.window.onload = async () => {
const { Translator } = dom.window;
await Translator.loadCoreTranslations("en");
it("should load core translations and fallback", async () => {
const dom = new JSDOM("", { runScripts: "outside-only" });
dom.window.eval(translatorJsScriptContent);
dom.window.translations = { en: "http://localhost:3000/translations/translation_test.json" };
dom.window.Log = { log: jest.fn() };
await new Promise((resolve) => dom.window.onload = resolve);
const en = require(path.join(__dirname, "..", "..", "..", "tests", "mocks", "translation_test.json"));
setTimeout(() => {
expect(Translator.coreTranslations).toEqual(en);
expect(Translator.coreTranslationsFallback).toEqual(en);
done();
}, 500);
};
});
const { Translator } = dom.window;
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", () => {
return new Promise((done) => {
const dom = new JSDOM(
`<script>var translations = {en: "http://localhost:3000/translations/translation_test.json"}; var Log = {log: () => {}};</script>\
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
{ runScripts: "dangerously", resources: "usable" }
);
dom.window.onload = async () => {
const { Translator } = dom.window;
await Translator.loadCoreTranslations("MISSINGLANG");
it("should load core fallback if language cannot be found", async () => {
const dom = new JSDOM("", { runScripts: "outside-only" });
dom.window.eval(translatorJsScriptContent);
dom.window.translations = { en: "http://localhost:3000/translations/translation_test.json" };
dom.window.Log = { log: jest.fn() };
await new Promise((resolve) => dom.window.onload = resolve);
const en = require(path.join(__dirname, "..", "..", "..", "tests", "mocks", "translation_test.json"));
setTimeout(() => {
expect(Translator.coreTranslations).toEqual({});
expect(Translator.coreTranslationsFallback).toEqual(en);
done();
}, 500);
};
});
const { Translator } = dom.window;
await Translator.loadCoreTranslations("MISSINGLANG");
const en = translationTestData;
await new Promise((resolve) => setTimeout(resolve, 500));
expect(Translator.coreTranslations).toEqual({});
expect(Translator.coreTranslationsFallback).toEqual(en);
});
});
describe("loadCoreTranslationsFallback", () => {
it("should load core translations fallback", () => {
return new Promise((done) => {
const dom = new JSDOM(
`<script>var translations = {en: "http://localhost:3000/translations/translation_test.json"}; var Log = {log: () => {}};</script>\
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
{ runScripts: "dangerously", resources: "usable" }
);
dom.window.onload = async () => {
const { Translator } = dom.window;
await Translator.loadCoreTranslationsFallback();
it("should load core translations fallback", async () => {
const dom = new JSDOM("", { runScripts: "outside-only" });
dom.window.eval(translatorJsScriptContent);
dom.window.translations = { en: "http://localhost:3000/translations/translation_test.json" };
dom.window.Log = { log: jest.fn() };
await new Promise((resolve) => dom.window.onload = resolve);
const en = require(path.join(__dirname, "..", "..", "..", "tests", "mocks", "translation_test.json"));
setTimeout(() => {
expect(Translator.coreTranslationsFallback).toEqual(en);
done();
}, 500);
};
});
const { Translator } = dom.window;
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", () => {
return new Promise((done) => {
const dom = new JSDOM(
`<script>var translations = {}; var Log = {log: () => {}};</script>\
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
{ runScripts: "dangerously", resources: "usable" }
);
dom.window.onload = async () => {
const { Translator } = dom.window;
await Translator.loadCoreTranslations();
it("should load core fallback if language cannot be found", async () => {
const dom = new JSDOM("", { runScripts: "outside-only" });
dom.window.eval(translatorJsScriptContent);
dom.window.translations = {};
dom.window.Log = { log: jest.fn() };
setTimeout(() => {
expect(Translator.coreTranslationsFallback).toEqual({});
done();
}, 500);
};
});
await new Promise((resolve) => dom.window.onload = resolve);
const { Translator } = dom.window;
await Translator.loadCoreTranslations();
await new Promise((resolve) => setTimeout(resolve, 500));
expect(Translator.coreTranslationsFallback).toEqual({});
});
});
});