Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
<script type="text/javascript" src="defaultmodules/utils.js"></script>
<script type="text/javascript" src="js/logger.js"></script>
<script type="text/javascript" src="translations/translations.js"></script>
<script type="text/javascript" src="js/translator.js"></script>
<script type="text/javascript" src="config/basepath.js"></script>
<script type="text/javascript" src="js/animateCSS.js"></script>
<script type="text/javascript" src="js/positions.js"></script>
Expand Down
4 changes: 3 additions & 1 deletion js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

// eslint-disable-next-line import-x/extensions
import { loadModules } from "./loader.js";
// eslint-disable-next-line import-x/extensions
import { Translator } from "./translator.js";

let modules = [];

Expand Down Expand Up @@ -571,7 +573,7 @@

Log.setLogLevel(config.logLevel);

await globalThis.Translator.loadCoreTranslations(config.language);
await Translator.loadCoreTranslations(config.language);

Check failure

Code scanning / CodeQL

Property access on null or undefined Error

The base expression of this property access is always undefined.
Comment thread
KristjanESPERANTO marked this conversation as resolved.
Dismissed
await loadModules();
},

Expand Down
2 changes: 2 additions & 0 deletions js/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import { loadFileForModule } from "./loader.js";
// eslint-disable-next-line import-x/extensions
import { MMSocket } from "./socketclient.js";
// eslint-disable-next-line import-x/extensions
import { Translator } from "./translator.js";

/*
* Module Blueprint.
Expand Down
4 changes: 1 addition & 3 deletions js/translator.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* global translations */

const Translator = (function () {
export const Translator = (function () {

/**
* Load a JSON file via fetch.
Expand Down Expand Up @@ -125,5 +125,3 @@ const Translator = (function () {
}
};
}());

window.Translator = Translator;
114 changes: 43 additions & 71 deletions tests/e2e/translations_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,23 @@ 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");
const {
setupTranslationTestEnvironment,
TRANSLATOR_MODULE_URL,
resetTranslatorState
} = require("../utils/translation_test_environment");

/**
* Helper function to create a fresh Translator instance with DOM environment.
* @returns {object} Object containing window and Translator
* Create a fresh Translator state for each test.
* @returns {Promise<object>} Shared Translator singleton with cleared state
*/
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 };
async function getFreshTranslator () {
setupTranslationTestEnvironment(3000);
const { Translator } = await import(TRANSLATOR_MODULE_URL);
resetTranslatorState(Translator);
return Translator;
}

describe("translations", () => {
Expand All @@ -40,6 +36,13 @@ describe("translations", () => {
server = app.listen(3000);
});

afterEach(() => {
vi.restoreAllMocks();
delete global.document;
delete global.Log;
delete global.config;
});

afterAll(async () => {
await server.close();
});
Expand All @@ -53,101 +56,70 @@ describe("translations", () => {
});

describe("loadTranslations", () => {
let dom;
let Translator;
let Module;
let config;

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;
global.Log = { log: vi.fn(), error: vi.fn(), warn: vi.fn() };
config = { language: "de" };
global.config = config;

dom = { window };
});
// module.js and translator.js are ES modules that read these globals at call time.
Translator = await getFreshTranslator();

afterEach(() => {
delete global.Log;
delete global.Translator;
delete global.config;
delete global.window;
delete global.MM;
delete global.nunjucks;
const modulePath = pathToFileURL(path.join(__dirname, "..", "..", "js", "module.js")).href;
({ Module } = await import(modulePath));
});

it("should load translation file", async () => {
const { Translator, Module, config } = dom.window;
config.language = "en";
Translator.load = vi.fn().mockImplementation(() => null);
const loadSpy = vi.spyOn(Translator, "load").mockResolvedValue(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);
expect(loadSpy.mock.calls).toHaveLength(1);
expect(loadSpy).toHaveBeenCalledWith(MMM, "translations/en.json", false);
});

it("should load translation + fallback file", async () => {
const { Translator, Module } = dom.window;
Translator.load = vi.fn().mockImplementation(() => null);
const loadSpy = vi.spyOn(Translator, "load").mockResolvedValue(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);
expect(loadSpy.mock.calls).toHaveLength(2);
expect(loadSpy).toHaveBeenCalledWith(MMM, "translations/de.json", false);
expect(loadSpy).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);
const loadSpy = vi.spyOn(Translator, "load").mockResolvedValue(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);
expect(loadSpy.mock.calls).toHaveLength(1);
expect(loadSpy).toHaveBeenCalledWith(MMM, "translations/en.json", true);
});

it("should load no file", async () => {
const { Translator, Module } = dom.window;
Translator.load = vi.fn();
const loadSpy = vi.spyOn(Translator, "load").mockResolvedValue(null);

Module.register("name", {});
const MMM = Module.create("name");

await MMM.loadTranslations();

expect(Translator.load.mock.calls).toHaveLength(0);
expect(loadSpy.mock.calls).toHaveLength(0);
});
});

Expand All @@ -161,7 +133,7 @@ describe("translations", () => {
describe("parsing language files through the Translator class", () => {
for (const language in translations) {
it(`should parse ${language}`, async () => {
const { Translator } = createTranslationTestEnvironment();
const Translator = await getFreshTranslator();
await Translator.load(mmm, translations[language], false);

expect(typeof Translator.translations[mmm.name]).toBe("object");
Expand Down Expand Up @@ -193,7 +165,7 @@ describe("translations", () => {

// Function to initialize JSDOM and load translations
const initializeTranslationDOM = async (language) => {
const { Translator } = createTranslationTestEnvironment();
const Translator = await getFreshTranslator();
await Translator.load(mmm, translations[language], false);
return Translator.translations[mmm.name];
};
Expand Down
7 changes: 0 additions & 7 deletions tests/unit/classes/module_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,13 @@ describe("File js/module (cloneObject)", () => {
let originalLog;
let originalConfig;
let originalMM;
let originalTranslator;
let originalNunjucks;

beforeAll(async () => {
originalWindow = global.window;
originalLog = global.Log;
originalConfig = global.config;
originalMM = global.MM;
originalTranslator = global.Translator;
originalNunjucks = global.nunjucks;

global.window = { mmVersion: "2.0.0" };
Expand All @@ -29,10 +27,6 @@ describe("File js/module (cloneObject)", () => {
sendNotification: () => {},
updateDom: () => {}
};
global.Translator = {
load: () => Promise.resolve(),
translate: () => ""
};
global.nunjucks = {
Environment () {
this.addFilter = () => {};
Expand All @@ -57,7 +51,6 @@ describe("File js/module (cloneObject)", () => {
global.Log = originalLog;
global.config = originalConfig;
global.MM = originalMM;
global.Translator = originalTranslator;
global.nunjucks = originalNunjucks;
});

Expand Down
Loading