[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 - 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 - 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 - 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 ### Fixed

View File

@@ -6,7 +6,7 @@ const express = require("express");
const sinon = require("sinon"); const sinon = require("sinon");
const translations = require("../../translations/translations"); const translations = require("../../translations/translations");
describe("Translations", () => { describe("translations", () => {
let server; let server;
beforeAll(() => { beforeAll(() => {
@@ -26,8 +26,9 @@ describe("Translations", () => {
}); });
it("should have a translation file in the specified path", () => { 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]); const file = fs.statSync(translations[language]);
expect(file.isFile()).toBe(true); expect(file.isFile()).toBe(true);
} }
}); });
@@ -36,17 +37,28 @@ describe("Translations", () => {
let dom; let dom;
beforeEach(() => { beforeEach(() => {
dom = new JSDOM( // Create a new JSDOM instance for each test
`<script>var Translator = {}; var Log = {log: () => {}}; var config = {language: 'de'};</script>\ dom = new JSDOM("", { runScripts: "dangerously", resources: "usable" });
<script src="file://${path.join(__dirname, "..", "..", "js", "class.js")}"></script>\
<script src="file://${path.join(__dirname, "..", "..", "js", "module.js")}"></script>`, // Mock the necessary global objects
{ runScripts: "dangerously", resources: "usable" } 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", async () => {
await new Promise((resolve) => {
dom.window.onload = resolve;
}); });
it("should load translation file", () => {
return new Promise((done) => {
dom.window.onload = async () => {
const { Translator, Module, config } = dom.window; const { Translator, Module, config } = dom.window;
config.language = "en"; config.language = "en";
Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null); Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null);
@@ -58,15 +70,13 @@ describe("Translations", () => {
expect(Translator.load.args).toHaveLength(1); expect(Translator.load.args).toHaveLength(1);
expect(Translator.load.calledWith(MMM, "translations/en.json", false)).toBe(true); expect(Translator.load.calledWith(MMM, "translations/en.json", false)).toBe(true);
done();
};
});
}); });
it("should load translation + fallback file", () => { it("should load translation + fallback file", async () => {
return new Promise((done) => { await new Promise((resolve) => {
dom.window.onload = async () => { dom.window.onload = resolve;
});
const { Translator, Module } = dom.window; const { Translator, Module } = dom.window;
Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null); Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null);
@@ -78,15 +88,13 @@ describe("Translations", () => {
expect(Translator.load.args).toHaveLength(2); expect(Translator.load.args).toHaveLength(2);
expect(Translator.load.calledWith(MMM, "translations/de.json", false)).toBe(true); 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.calledWith(MMM, "translations/en.json", true)).toBe(true);
done();
};
});
}); });
it("should load translation fallback file", () => { it("should load translation fallback file", async () => {
return new Promise((done) => { await new Promise((resolve) => {
dom.window.onload = async () => { dom.window.onload = resolve;
});
const { Translator, Module, config } = dom.window; const { Translator, Module, config } = dom.window;
config.language = "--"; config.language = "--";
Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null); Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null);
@@ -98,15 +106,13 @@ describe("Translations", () => {
expect(Translator.load.args).toHaveLength(1); expect(Translator.load.args).toHaveLength(1);
expect(Translator.load.calledWith(MMM, "translations/en.json", true)).toBe(true); expect(Translator.load.calledWith(MMM, "translations/en.json", true)).toBe(true);
done();
};
});
}); });
it("should load no file", () => { it("should load no file", async () => {
return new Promise((done) => { await new Promise((resolve) => {
dom.window.onload = async () => { dom.window.onload = resolve;
});
const { Translator, Module } = dom.window; const { Translator, Module } = dom.window;
Translator.load = sinon.stub(); Translator.load = sinon.stub();
@@ -116,10 +122,6 @@ describe("Translations", () => {
await MMM.loadTranslations(); await MMM.loadTranslations();
expect(Translator.load.callCount).toBe(0); expect(Translator.load.callCount).toBe(0);
done();
};
});
}); });
}); });
@@ -130,29 +132,30 @@ describe("Translations", () => {
} }
}; };
describe("Parsing language files through the Translator class", () => { const translatorJs = fs.readFileSync(path.join(__dirname, "..", "..", "js", "translator.js"), "utf-8");
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;
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); await Translator.load(mmm, translations[language], false);
expect(typeof Translator.translations[mmm.name]).toBe("object"); expect(typeof Translator.translations[mmm.name]).toBe("object");
expect(Object.keys(Translator.translations[mmm.name]).length).toBeGreaterThanOrEqual(1); expect(Object.keys(Translator.translations[mmm.name]).length).toBeGreaterThanOrEqual(1);
done();
};
});
}); });
} }
}); });
describe("Same keys", () => { describe("same keys", () => {
let base; let base;
// Some expressions are not easy to translate automatically. For the sake of a working test, we filter them out. // 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" }); const dom = new JSDOM("", { runScripts: "dangerously", resources: "usable" });
dom.window.Log = { log: jest.fn() }; dom.window.Log = { log: jest.fn() };
dom.window.translations = translations; dom.window.translations = translations;
const translatorJs = fs.readFileSync(path.join(__dirname, "..", "..", "js", "translator.js"), "utf-8");
dom.window.eval(translatorJs); dom.window.eval(translatorJs);
return new Promise((resolve) => { return new Promise((resolve) => {

View File

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