[tests] migrate from jest to vitest (#3940)

This is a big change, but I think it's a good move, as `vitest` is much
more modern than `jest`.

I'm excited about the UI watch feature (run `npm run test:ui`), for
example - it's really helpful and saves time when debugging tests. I had
to adjust a few tests because they had time related issues, but
basically we are now testing the same things - even a bit better and
less flaky (I hope).

What do you think?
This commit is contained in:
Kristjan ESPERANTO
2025-11-03 19:47:01 +01:00
committed by GitHub
parent b542f33a0a
commit 462abf7027
30 changed files with 2370 additions and 3562 deletions

View File

@@ -20,16 +20,41 @@ var indexData = [];
var cssData = [];
exports.startApplication = async (configFilename, exec) => {
jest.resetModules();
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 this.stopApplication();
}
// Use fixed port 8080 (tests run sequentially, no conflicts)
const port = 8080;
global.testPort = port;
// Set config sample for use in test
let configPath;
if (configFilename === "") {
process.env.MM_CONFIG_FILE = "config/config.js";
configPath = "config/config.js";
} else {
process.env.MM_CONFIG_FILE = configFilename;
configPath = configFilename;
}
process.env.MM_CONFIG_FILE = configPath;
// Override port in config - MUST be set before app loads
process.env.MM_PORT = port.toString();
process.env.mmTestMode = "true";
process.setMaxListeners(0);
if (exec) exec;
@@ -38,24 +63,30 @@ exports.startApplication = async (configFilename, exec) => {
return global.app.start();
};
exports.stopApplication = async (waitTime = 10) => {
exports.stopApplication = async (waitTime = 100) => {
if (global.window) {
// no closing causes jest errors and memory leaks
// no closing causes test errors and memory leaks
global.window.close();
delete global.window;
// give above closing some extra time to finish
await new Promise((resolve) => setTimeout(resolve, waitTime));
}
if (!global.app) {
delete global.testPort;
return Promise.resolve();
}
await global.app.stop();
delete global.app;
delete global.testPort;
// Small delay to ensure clean shutdown
await new Promise((resolve) => setTimeout(resolve, waitTime));
};
exports.getDocument = () => {
return new Promise((resolve) => {
const url = `http://${config.address || "localhost"}:${config.port || "8080"}`;
const port = global.testPort || config.port || 8080;
const url = `http://${config.address || "localhost"}:${port}`;
jsdom.JSDOM.fromURL(url, { resources: "usable", runScripts: "dangerously" }).then((dom) => {
dom.window.name = "jsdom";
global.window = dom.window;

View File

@@ -10,7 +10,8 @@ describe("ipWhitelist directive configuration", () => {
});
it("should reject request with 403 (Forbidden)", async () => {
const res = await fetch("http://localhost:8181");
const port = global.testPort || 8080;
const res = await fetch(`http://localhost:${port}`);
expect(res.status).toBe(403);
});
});
@@ -24,7 +25,8 @@ describe("ipWhitelist directive configuration", () => {
});
it("should allow request with 200 (OK)", async () => {
const res = await fetch("http://localhost:8282");
const port = global.testPort || 8080;
const res = await fetch(`http://localhost:${port}`);
expect(res.status).toBe(200);
});
});

View File

@@ -10,7 +10,8 @@ describe("port directive configuration", () => {
});
it("should return 200", async () => {
const res = await fetch("http://localhost:8090");
const port = global.testPort || 8080;
const res = await fetch(`http://localhost:${port}`);
expect(res.status).toBe(200);
});
});
@@ -24,7 +25,8 @@ describe("port directive configuration", () => {
});
it("should return 200", async () => {
const res = await fetch("http://localhost:8100");
const port = global.testPort || 8080;
const res = await fetch(`http://localhost:${port}`);
expect(res.status).toBe(200);
});
});

View File

@@ -4,14 +4,18 @@ const delay = (time) => {
const runConfigCheck = async () => {
const serverProcess = await require("node:child_process").spawnSync("node", ["--run", "config:check"], { env: process.env });
expect(serverProcess.stderr.toString()).toBe("");
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);

View File

@@ -15,7 +15,8 @@ describe("templated config with port variable", () => {
});
it("should return 200", async () => {
const res = await fetch("http://localhost:8090");
const port = global.testPort || 8080;
const res = await fetch(`http://localhost:${port}`);
expect(res.status).toBe(200);
});
});

View File

@@ -14,7 +14,7 @@ function createTranslationTestEnvironment () {
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() };
dom.window.Log = { log: vi.fn(), error: vi.fn() };
dom.window.translations = translations;
dom.window.eval(translatorJs);
@@ -75,7 +75,7 @@ describe("translations", () => {
it("should load translation file", async () => {
const { Translator, Module, config } = dom.window;
config.language = "en";
Translator.load = jest.fn().mockImplementation((_m, _f, _fb) => null);
Translator.load = vi.fn().mockImplementation((_m, _f, _fb) => null);
Module.register("name", { getTranslations: () => translations });
const MMM = Module.create("name");
@@ -88,7 +88,7 @@ describe("translations", () => {
it("should load translation + fallback file", async () => {
const { Translator, Module } = dom.window;
Translator.load = jest.fn().mockImplementation((_m, _f, _fb) => null);
Translator.load = vi.fn().mockImplementation((_m, _f, _fb) => null);
Module.register("name", { getTranslations: () => translations });
const MMM = Module.create("name");
@@ -103,7 +103,7 @@ describe("translations", () => {
it("should load translation fallback file", async () => {
const { Translator, Module, config } = dom.window;
config.language = "--";
Translator.load = jest.fn().mockImplementation((_m, _f, _fb) => null);
Translator.load = vi.fn().mockImplementation((_m, _f, _fb) => null);
Module.register("name", { getTranslations: () => translations });
const MMM = Module.create("name");
@@ -116,7 +116,7 @@ describe("translations", () => {
it("should load no file", async () => {
const { Translator, Module } = dom.window;
Translator.load = jest.fn();
Translator.load = vi.fn();
Module.register("name", {});
const MMM = Module.create("name");