mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-12-01 02:21:39 +00:00
[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:
committed by
GitHub
parent
b542f33a0a
commit
462abf7027
16
.github/CONTRIBUTING.md
vendored
16
.github/CONTRIBUTING.md
vendored
@@ -30,9 +30,19 @@ To run markdownlint, use `node --run lint:markdown`.
|
||||
|
||||
## Testing
|
||||
|
||||
We use [Jest](https://jestjs.io) for JavaScript testing.
|
||||
We use [Vitest](https://vitest.dev) for JavaScript testing.
|
||||
|
||||
To run all tests, use `node --run test`.
|
||||
|
||||
The specific test commands are defined in `package.json`.
|
||||
So you can also run the specific tests with other commands, e.g. `node --run test:unit` or `npx jest tests/e2e/env_spec.js`.
|
||||
The `package.json` scripts expose finer-grained test commands:
|
||||
|
||||
- `test:unit` – run unit tests only
|
||||
- `test:e2e` – execute browser-driven end-to-end tests
|
||||
- `test:electron` – launch the Electron-based regression suite
|
||||
- `test:coverage` – collect coverage while running every suite
|
||||
- `test:watch` – keep Vitest in watch mode for fast local feedback
|
||||
- `test:ui` – open the Vitest UI dashboard (needs OS file-watch support enabled)
|
||||
- `test:calendar` – run the legacy calendar debug helper
|
||||
- `test:css`, `test:markdown`, `test:prettier`, `test:spelling`, `test:js` – lint-only scripts that enforce formatting, spelling, markdown style, and ESLint.
|
||||
|
||||
You can invoke any script with `node --run <script>` (or `npm run <script>`). Individual files can still be targeted directly, e.g. `npx vitest run tests/e2e/env_spec.js`.
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -88,3 +88,6 @@ js/positions.js
|
||||
# Ignore lock files other than package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
|
||||
# Vitest temporary test files
|
||||
tests/**/.tmp/
|
||||
|
||||
@@ -22,6 +22,7 @@ planned for 2026-01-01
|
||||
- [check_config] refactor: improve error handling (#3927)
|
||||
- [calendar] test: remove "Recurring event per timezone" test (#3929)
|
||||
- [calendar] chore: remove `requiresVersion: "2.1.0"` (#3932)
|
||||
- [tests] migrate from `jest` to `vitest` (#3940)
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {defineConfig, globalIgnores} from "eslint/config";
|
||||
import globals from "globals";
|
||||
import {flatConfigs as importX} from "eslint-plugin-import-x";
|
||||
import jest from "eslint-plugin-jest";
|
||||
import js from "@eslint/js";
|
||||
import jsdocPlugin from "eslint-plugin-jsdoc";
|
||||
import packageJson from "eslint-plugin-package-json";
|
||||
import stylistic from "@stylistic/eslint-plugin";
|
||||
import vitest from "eslint-plugin-vitest";
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(["config/**", "modules/**/*", "!modules/default/**", "js/positions.js"]),
|
||||
@@ -16,6 +16,7 @@ export default defineConfig([
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
...vitest.environments.env.globals,
|
||||
Log: "readonly",
|
||||
MM: "readonly",
|
||||
Module: "readonly",
|
||||
@@ -23,8 +24,8 @@ export default defineConfig([
|
||||
moment: "readonly"
|
||||
}
|
||||
},
|
||||
plugins: {js, stylistic},
|
||||
extends: [importX.recommended, jest.configs["flat/recommended"], "js/recommended", jsdocPlugin.configs["flat/recommended"], "stylistic/all"],
|
||||
plugins: {js, stylistic, vitest},
|
||||
extends: [importX.recommended, vitest.configs.recommended, "js/recommended", jsdocPlugin.configs["flat/recommended"], "stylistic/all"],
|
||||
rules: {
|
||||
"@stylistic/array-element-newline": ["error", "consistent"],
|
||||
"@stylistic/arrow-parens": ["error", "always"],
|
||||
@@ -57,12 +58,9 @@ export default defineConfig([
|
||||
"import-x/newline-after-import": "error",
|
||||
"import-x/order": "error",
|
||||
"init-declarations": "off",
|
||||
"jest/consistent-test-it": "warn",
|
||||
"jest/no-done-callback": "warn",
|
||||
"jest/prefer-expect-resolves": "warn",
|
||||
"jest/prefer-mock-promise-shorthand": "warn",
|
||||
"jest/prefer-to-be": "warn",
|
||||
"jest/prefer-to-have-length": "warn",
|
||||
"vitest/consistent-test-it": "warn",
|
||||
"vitest/prefer-to-be": "warn",
|
||||
"vitest/prefer-to-have-length": "warn",
|
||||
"max-lines-per-function": ["warn", 400],
|
||||
"max-statements": "off",
|
||||
"no-global-assign": "off",
|
||||
|
||||
@@ -65,8 +65,8 @@ function App () {
|
||||
async function loadConfig () {
|
||||
Log.log("Loading config ...");
|
||||
const defaults = require(`${__dirname}/defaults`);
|
||||
if (process.env.JEST_WORKER_ID !== undefined) {
|
||||
// if we are running with jest
|
||||
if (global.mmTestMode) {
|
||||
// if we are running in test mode
|
||||
defaults.address = "0.0.0.0";
|
||||
}
|
||||
|
||||
@@ -185,10 +185,10 @@ function App () {
|
||||
|
||||
if (defaultModules.includes(moduleName)) {
|
||||
const defaultModuleFolder = path.resolve(`${global.root_path}/modules/default/`, module);
|
||||
if (process.env.JEST_WORKER_ID === undefined) {
|
||||
if (!global.mmTestMode) {
|
||||
moduleFolder = defaultModuleFolder;
|
||||
} else {
|
||||
// running in Jest, allow defaultModules placed under moduleDir for testing
|
||||
// running in test mode, allow defaultModules placed under moduleDir for testing
|
||||
if (env.modulesDir === "modules" || env.modulesDir === "tests/mocks") {
|
||||
moduleFolder = defaultModuleFolder;
|
||||
}
|
||||
|
||||
@@ -76,8 +76,8 @@ function createWindow () {
|
||||
|
||||
const electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions);
|
||||
|
||||
if (process.env.JEST_WORKER_ID !== undefined && process.env.MOCK_DATE !== undefined) {
|
||||
// if we are running with jest and we want to mock the current date
|
||||
if (process.env.MOCK_DATE !== undefined) {
|
||||
// if we are running tests and we want to mock the current date
|
||||
const fakeNow = new Date(process.env.MOCK_DATE).valueOf();
|
||||
Date = class extends Date {
|
||||
constructor (...args) {
|
||||
@@ -114,8 +114,8 @@ function createWindow () {
|
||||
|
||||
// Open the DevTools if run with "node --run start:dev"
|
||||
if (process.argv.includes("dev")) {
|
||||
if (process.env.JEST_WORKER_ID !== undefined) {
|
||||
// if we are running with jest
|
||||
if (process.env.mmTestMode) {
|
||||
// if we are running tests
|
||||
const devtools = new BrowserWindow(electronOptions);
|
||||
mainWindow.webContents.setDevToolsWebContents(devtools.webContents);
|
||||
}
|
||||
@@ -169,8 +169,8 @@ function createWindow () {
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on("window-all-closed", function () {
|
||||
if (process.env.JEST_WORKER_ID !== undefined) {
|
||||
// if we are running with jest
|
||||
if (process.env.mmTestMode) {
|
||||
// if we are running tests
|
||||
app.quit();
|
||||
} else {
|
||||
createWindow();
|
||||
|
||||
@@ -84,7 +84,7 @@ const Loader = (function () {
|
||||
if (window.name !== "jsdom") {
|
||||
moduleFolder = defaultModuleFolder;
|
||||
} else {
|
||||
// running in Jest, allow defaultModules placed under moduleDir for testing
|
||||
// running in test mode, allow defaultModules placed under moduleDir for testing
|
||||
if (envVars.modulesDir === "modules") {
|
||||
moduleFolder = defaultModuleFolder;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// This logger is very simple, but needs to be extended.
|
||||
(function (root, factory) {
|
||||
if (typeof exports === "object") {
|
||||
if (process.env.JEST_WORKER_ID === undefined) {
|
||||
if (process.env.mmTestMode !== "true") {
|
||||
const { styleText } = require("node:util");
|
||||
|
||||
// add timestamps in front of log messages
|
||||
@@ -78,8 +78,8 @@
|
||||
let logLevel;
|
||||
let enableLog;
|
||||
if (typeof exports === "object") {
|
||||
// in nodejs and not running with jest
|
||||
enableLog = process.env.JEST_WORKER_ID === undefined;
|
||||
// in nodejs and not running in test mode
|
||||
enableLog = process.env.mmTestMode !== "true";
|
||||
} else {
|
||||
// in browser and not running with jsdom
|
||||
enableLog = typeof window === "object" && window.name !== "jsdom";
|
||||
@@ -97,7 +97,7 @@
|
||||
groupEnd: Function.prototype.bind.call(console.groupEnd, console),
|
||||
time: Function.prototype.bind.call(console.time, console),
|
||||
timeEnd: Function.prototype.bind.call(console.timeEnd, console),
|
||||
timeStamp: Function.prototype.bind.call(console.timeStamp, console)
|
||||
timeStamp: console.timeStamp ? Function.prototype.bind.call(console.timeStamp, console) : function () {}
|
||||
};
|
||||
|
||||
logLevel.setLogLevel = function (newLevel) {
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* @param {Promise} callback function to call when the timer expires
|
||||
*/
|
||||
const scheduleTimer = function (timer, intervalMS, callback) {
|
||||
if (process.env.JEST_WORKER_ID === undefined) {
|
||||
// only set timer when not running in jest
|
||||
if (process.env.mmTestMode !== "true") {
|
||||
// only set timer when not running in test mode
|
||||
let tmr = timer;
|
||||
clearTimeout(tmr);
|
||||
tmr = setTimeout(function () {
|
||||
|
||||
@@ -34,7 +34,7 @@ module.exports = {
|
||||
].join("\n");
|
||||
Log.info(systemDataString);
|
||||
|
||||
// Return is currently only for jest
|
||||
// Return is currently only for tests
|
||||
return systemDataString;
|
||||
} catch (error) {
|
||||
Log.error(error);
|
||||
@@ -65,9 +65,11 @@ module.exports = {
|
||||
if (results && results.length > 0) {
|
||||
// get the position parts and replace space with underscore
|
||||
const positionName = results[1].replace(" ", "_");
|
||||
// add it to the list
|
||||
// add it to the list only if not already present (avoid duplicates)
|
||||
if (!modulePositions.includes(positionName)) {
|
||||
modulePositions.push(positionName);
|
||||
}
|
||||
}
|
||||
});
|
||||
try {
|
||||
fs.writeFileSync(discoveredPositionsJSFilename, `const modulePositions=${JSON.stringify(modulePositions)}`);
|
||||
|
||||
4965
package-lock.json
generated
4965
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@@ -52,17 +52,19 @@
|
||||
"start:windows:dev": "node --run start:windows -- dev",
|
||||
"start:x11": "DISPLAY=\"${DISPLAY:=:0}\" ./node_modules/.bin/electron js/electron.js",
|
||||
"start:x11:dev": "node --run start:x11 -- dev",
|
||||
"test": "NODE_ENV=test jest -i --forceExit",
|
||||
"test": "vitest run",
|
||||
"test:calendar": "node ./modules/default/calendar/debug.js",
|
||||
"test:coverage": "NODE_ENV=test jest --coverage -i --verbose false --forceExit",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:css": "stylelint 'css/main.css' 'css/roboto.css' 'css/font-awesome.css' 'modules/default/**/*.css'",
|
||||
"test:e2e": "NODE_ENV=test jest --selectProjects e2e -i --forceExit",
|
||||
"test:electron": "NODE_ENV=test jest --selectProjects electron -i --forceExit",
|
||||
"test:e2e": "vitest run tests/e2e",
|
||||
"test:electron": "vitest run tests/electron",
|
||||
"test:js": "eslint",
|
||||
"test:markdown": "markdownlint-cli2 .",
|
||||
"test:prettier": "prettier . --check",
|
||||
"test:spelling": "cspell . --gitignore",
|
||||
"test:unit": "NODE_ENV=test jest --selectProjects unit"
|
||||
"test:ui": "vitest --ui",
|
||||
"test:unit": "vitest run tests/unit",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "prettier --ignore-unknown --write",
|
||||
@@ -98,15 +100,16 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@stylistic/eslint-plugin": "^5.5.0",
|
||||
"@vitest/coverage-v8": "^4.0.6",
|
||||
"@vitest/ui": "^4.0.6",
|
||||
"cspell": "^9.2.2",
|
||||
"eslint-plugin-import-x": "^4.16.1",
|
||||
"eslint-plugin-jest": "^29.0.1",
|
||||
"eslint-plugin-jsdoc": "^61.1.11",
|
||||
"eslint-plugin-package-json": "^0.59.1",
|
||||
"eslint-plugin-vitest": "^0.5.4",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^30.2.0",
|
||||
"jsdom": "27.0.0",
|
||||
"jsdom": "^27.1.0",
|
||||
"lint-staged": "^16.2.6",
|
||||
"markdownlint-cli2": "^0.18.1",
|
||||
"playwright": "^1.56.1",
|
||||
@@ -114,7 +117,8 @@
|
||||
"prettier-plugin-jinja-template": "^2.1.0",
|
||||
"stylelint": "^16.25.0",
|
||||
"stylelint-config-standard": "^39.0.1",
|
||||
"stylelint-prettier": "^5.0.3"
|
||||
"stylelint-prettier": "^5.0.3",
|
||||
"vitest": "^4.0.6"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"electron": "^38.3.0"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -20,7 +20,21 @@ exports.startApplication = async (configFilename, systemDate = null, electronPar
|
||||
electronParams.unshift("js/electron.js");
|
||||
}
|
||||
|
||||
global.electronApp = await electron.launch({ args: electronParams });
|
||||
// Pass environment variables to Electron process
|
||||
const env = {
|
||||
...process.env,
|
||||
MM_CONFIG_FILE: configFilename,
|
||||
TZ: timezone,
|
||||
mmTestMode: "true"
|
||||
};
|
||||
if (systemDate) {
|
||||
env.MOCK_DATE = systemDate;
|
||||
}
|
||||
|
||||
global.electronApp = await electron.launch({
|
||||
args: electronParams,
|
||||
env: env
|
||||
});
|
||||
|
||||
await global.electronApp.firstWindow();
|
||||
|
||||
@@ -40,13 +54,35 @@ exports.startApplication = async (configFilename, systemDate = null, electronPar
|
||||
}
|
||||
};
|
||||
|
||||
exports.stopApplication = async () => {
|
||||
if (global.electronApp) {
|
||||
await global.electronApp.close();
|
||||
}
|
||||
exports.stopApplication = async (timeout = 10000) => {
|
||||
const app = global.electronApp;
|
||||
global.electronApp = null;
|
||||
global.page = null;
|
||||
process.env.MOCK_DATE = undefined;
|
||||
|
||||
if (!app) {
|
||||
return;
|
||||
}
|
||||
|
||||
const killElectron = () => {
|
||||
try {
|
||||
const electronProcess = typeof app.process === "function" ? app.process() : null;
|
||||
if (electronProcess && !electronProcess.killed) {
|
||||
electronProcess.kill("SIGKILL");
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors caused by Playwright already tearing down the connection
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
app.close(),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error("Electron close timeout")), timeout))
|
||||
]);
|
||||
} catch (error) {
|
||||
killElectron();
|
||||
}
|
||||
};
|
||||
|
||||
exports.getElement = async (selector, state = "visible") => {
|
||||
|
||||
87
tests/mocks/calendar_duplicates_1.ics
Normal file
87
tests/mocks/calendar_duplicates_1.ics
Normal file
@@ -0,0 +1,87 @@
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//MagicMirror//Test Calendar//EN
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
X-WR-CALNAME:Duplicates Test Calendar 1
|
||||
BEGIN:VEVENT
|
||||
UID:duplicate-event-1@magicmirror.test
|
||||
DTSTART:20240916T100000Z
|
||||
DTEND:20240916T110000Z
|
||||
DTSTAMP:20240915T000000Z
|
||||
SUMMARY:Duplicate Event 1
|
||||
STATUS:CONFIRMED
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:duplicate-event-2@magicmirror.test
|
||||
DTSTART:20240917T140000Z
|
||||
DTEND:20240917T150000Z
|
||||
DTSTAMP:20240915T000000Z
|
||||
SUMMARY:Duplicate Event 2
|
||||
STATUS:CONFIRMED
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:duplicate-event-3@magicmirror.test
|
||||
DTSTART:20240918T080000Z
|
||||
DTEND:20240918T090000Z
|
||||
DTSTAMP:20240915T000000Z
|
||||
SUMMARY:Duplicate Event 3
|
||||
STATUS:CONFIRMED
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:duplicate-event-4@magicmirror.test
|
||||
DTSTART:20240919T120000Z
|
||||
DTEND:20240919T130000Z
|
||||
DTSTAMP:20240915T000000Z
|
||||
SUMMARY:Duplicate Event 4
|
||||
STATUS:CONFIRMED
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:duplicate-event-5@magicmirror.test
|
||||
DTSTART:20240920T160000Z
|
||||
DTEND:20240920T170000Z
|
||||
DTSTAMP:20240915T000000Z
|
||||
SUMMARY:Duplicate Event 5
|
||||
STATUS:CONFIRMED
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:duplicate-event-6@magicmirror.test
|
||||
DTSTART:20240921T100000Z
|
||||
DTEND:20240921T110000Z
|
||||
DTSTAMP:20240915T000000Z
|
||||
SUMMARY:Duplicate Event 6
|
||||
STATUS:CONFIRMED
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:duplicate-event-7@magicmirror.test
|
||||
DTSTART:20240922T140000Z
|
||||
DTEND:20240922T150000Z
|
||||
DTSTAMP:20240915T000000Z
|
||||
SUMMARY:Duplicate Event 7
|
||||
STATUS:CONFIRMED
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:duplicate-event-8@magicmirror.test
|
||||
DTSTART:20240923T080000Z
|
||||
DTEND:20240923T090000Z
|
||||
DTSTAMP:20240915T000000Z
|
||||
SUMMARY:Duplicate Event 8
|
||||
STATUS:CONFIRMED
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:duplicate-event-9@magicmirror.test
|
||||
DTSTART:20240924T120000Z
|
||||
DTEND:20240924T130000Z
|
||||
DTSTAMP:20240915T000000Z
|
||||
SUMMARY:Duplicate Event 9
|
||||
STATUS:CONFIRMED
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:duplicate-event-10@magicmirror.test
|
||||
DTSTART:20240925T160000Z
|
||||
DTEND:20240925T170000Z
|
||||
DTSTAMP:20240915T000000Z
|
||||
SUMMARY:Duplicate Event 10
|
||||
STATUS:CONFIRMED
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
87
tests/mocks/calendar_duplicates_2.ics
Normal file
87
tests/mocks/calendar_duplicates_2.ics
Normal file
@@ -0,0 +1,87 @@
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//MagicMirror//Test Calendar//EN
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
X-WR-CALNAME:Duplicates Test Calendar 2
|
||||
BEGIN:VEVENT
|
||||
UID:duplicate-event-1-clone@magicmirror.test
|
||||
DTSTART:20240916T100000Z
|
||||
DTEND:20240916T110000Z
|
||||
DTSTAMP:20240915T000000Z
|
||||
SUMMARY:Duplicate Event 1
|
||||
STATUS:CONFIRMED
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:duplicate-event-2-clone@magicmirror.test
|
||||
DTSTART:20240917T140000Z
|
||||
DTEND:20240917T150000Z
|
||||
DTSTAMP:20240915T000000Z
|
||||
SUMMARY:Duplicate Event 2
|
||||
STATUS:CONFIRMED
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:duplicate-event-3-clone@magicmirror.test
|
||||
DTSTART:20240918T080000Z
|
||||
DTEND:20240918T090000Z
|
||||
DTSTAMP:20240915T000000Z
|
||||
SUMMARY:Duplicate Event 3
|
||||
STATUS:CONFIRMED
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:duplicate-event-4-clone@magicmirror.test
|
||||
DTSTART:20240919T120000Z
|
||||
DTEND:20240919T130000Z
|
||||
DTSTAMP:20240915T000000Z
|
||||
SUMMARY:Duplicate Event 4
|
||||
STATUS:CONFIRMED
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:duplicate-event-5-clone@magicmirror.test
|
||||
DTSTART:20240920T160000Z
|
||||
DTEND:20240920T170000Z
|
||||
DTSTAMP:20240915T000000Z
|
||||
SUMMARY:Duplicate Event 5
|
||||
STATUS:CONFIRMED
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:duplicate-event-6-clone@magicmirror.test
|
||||
DTSTART:20240921T100000Z
|
||||
DTEND:20240921T110000Z
|
||||
DTSTAMP:20240915T000000Z
|
||||
SUMMARY:Duplicate Event 6
|
||||
STATUS:CONFIRMED
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:duplicate-event-7-clone@magicmirror.test
|
||||
DTSTART:20240922T140000Z
|
||||
DTEND:20240922T150000Z
|
||||
DTSTAMP:20240915T000000Z
|
||||
SUMMARY:Duplicate Event 7
|
||||
STATUS:CONFIRMED
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:duplicate-event-8-clone@magicmirror.test
|
||||
DTSTART:20240923T080000Z
|
||||
DTEND:20240923T090000Z
|
||||
DTSTAMP:20240915T000000Z
|
||||
SUMMARY:Duplicate Event 8
|
||||
STATUS:CONFIRMED
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:duplicate-event-9-clone@magicmirror.test
|
||||
DTSTART:20240924T120000Z
|
||||
DTEND:20240924T130000Z
|
||||
DTSTAMP:20240915T000000Z
|
||||
SUMMARY:Duplicate Event 9
|
||||
STATUS:CONFIRMED
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:duplicate-event-10-clone@magicmirror.test
|
||||
DTSTART:20240925T160000Z
|
||||
DTEND:20240925T170000Z
|
||||
DTSTAMP:20240915T000000Z
|
||||
SUMMARY:Duplicate Event 10
|
||||
STATUS:CONFIRMED
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
@@ -12,7 +12,7 @@ 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.Log = { log: jest.fn(), error: jest.fn() };
|
||||
dom.window.Log = { log: vi.fn(), error: vi.fn() };
|
||||
dom.window.eval(translatorJs);
|
||||
|
||||
return { window: dom.window, Translator: dom.window.Translator };
|
||||
|
||||
@@ -1,6 +1,94 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Updatenotification custom module returns status information without hash 1`] = `
|
||||
exports[`Updatenotification > MagicMirror on develop > returns status information 1`] = `
|
||||
{
|
||||
"behind": 5,
|
||||
"current": "develop",
|
||||
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
|
||||
"isBehindInStatus": false,
|
||||
"module": "MagicMirror",
|
||||
"tracking": "origin/develop",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Updatenotification > MagicMirror on develop > returns status information early if isBehindInStatus 1`] = `
|
||||
{
|
||||
"behind": 5,
|
||||
"current": "develop",
|
||||
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
|
||||
"isBehindInStatus": true,
|
||||
"module": "MagicMirror",
|
||||
"tracking": "origin/develop",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Updatenotification > MagicMirror on master (empty taglist) > returns status information 1`] = `
|
||||
{
|
||||
"behind": 5,
|
||||
"current": "master",
|
||||
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
|
||||
"isBehindInStatus": false,
|
||||
"module": "MagicMirror",
|
||||
"tracking": "origin/master",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Updatenotification > MagicMirror on master (empty taglist) > returns status information early if isBehindInStatus 1`] = `
|
||||
{
|
||||
"behind": 5,
|
||||
"current": "master",
|
||||
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
|
||||
"isBehindInStatus": true,
|
||||
"module": "MagicMirror",
|
||||
"tracking": "origin/master",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Updatenotification > MagicMirror on master with match in taglist > returns status information 1`] = `
|
||||
{
|
||||
"behind": 5,
|
||||
"current": "master",
|
||||
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
|
||||
"isBehindInStatus": false,
|
||||
"module": "MagicMirror",
|
||||
"tracking": "origin/master",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Updatenotification > MagicMirror on master with match in taglist > returns status information early if isBehindInStatus 1`] = `
|
||||
{
|
||||
"behind": 5,
|
||||
"current": "master",
|
||||
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
|
||||
"isBehindInStatus": true,
|
||||
"module": "MagicMirror",
|
||||
"tracking": "origin/master",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Updatenotification > MagicMirror on master without match in taglist > returns status information 1`] = `
|
||||
{
|
||||
"behind": 0,
|
||||
"current": "master",
|
||||
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
|
||||
"isBehindInStatus": false,
|
||||
"module": "MagicMirror",
|
||||
"tracking": "origin/master",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Updatenotification > MagicMirror on master without match in taglist > returns status information early if isBehindInStatus 1`] = `
|
||||
{
|
||||
"behind": 0,
|
||||
"current": "master",
|
||||
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
|
||||
"isBehindInStatus": true,
|
||||
"module": "MagicMirror",
|
||||
"tracking": "origin/master",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Updatenotification > custom module > returns status information without hash 1`] = `
|
||||
{
|
||||
"behind": 7,
|
||||
"current": "master",
|
||||
@@ -10,91 +98,3 @@ exports[`Updatenotification custom module returns status information without has
|
||||
"tracking": "origin/master",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Updatenotification MagicMirror on develop returns status information 1`] = `
|
||||
{
|
||||
"behind": 5,
|
||||
"current": "develop",
|
||||
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
|
||||
"isBehindInStatus": false,
|
||||
"module": "MagicMirror",
|
||||
"tracking": "origin/develop",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Updatenotification MagicMirror on develop returns status information early if isBehindInStatus 1`] = `
|
||||
{
|
||||
"behind": 5,
|
||||
"current": "develop",
|
||||
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
|
||||
"isBehindInStatus": true,
|
||||
"module": "MagicMirror",
|
||||
"tracking": "origin/develop",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Updatenotification MagicMirror on master (empty taglist) returns status information 1`] = `
|
||||
{
|
||||
"behind": 5,
|
||||
"current": "master",
|
||||
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
|
||||
"isBehindInStatus": false,
|
||||
"module": "MagicMirror",
|
||||
"tracking": "origin/master",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Updatenotification MagicMirror on master (empty taglist) returns status information early if isBehindInStatus 1`] = `
|
||||
{
|
||||
"behind": 5,
|
||||
"current": "master",
|
||||
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
|
||||
"isBehindInStatus": true,
|
||||
"module": "MagicMirror",
|
||||
"tracking": "origin/master",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Updatenotification MagicMirror on master with match in taglist returns status information 1`] = `
|
||||
{
|
||||
"behind": 5,
|
||||
"current": "master",
|
||||
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
|
||||
"isBehindInStatus": false,
|
||||
"module": "MagicMirror",
|
||||
"tracking": "origin/master",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Updatenotification MagicMirror on master with match in taglist returns status information early if isBehindInStatus 1`] = `
|
||||
{
|
||||
"behind": 5,
|
||||
"current": "master",
|
||||
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
|
||||
"isBehindInStatus": true,
|
||||
"module": "MagicMirror",
|
||||
"tracking": "origin/master",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Updatenotification MagicMirror on master without match in taglist returns status information 1`] = `
|
||||
{
|
||||
"behind": 0,
|
||||
"current": "master",
|
||||
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
|
||||
"isBehindInStatus": false,
|
||||
"module": "MagicMirror",
|
||||
"tracking": "origin/master",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Updatenotification MagicMirror on master without match in taglist returns status information early if isBehindInStatus 1`] = `
|
||||
{
|
||||
"behind": 0,
|
||||
"current": "master",
|
||||
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
|
||||
"isBehindInStatus": true,
|
||||
"module": "MagicMirror",
|
||||
"tracking": "origin/master",
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const { expect } = require("playwright/test");
|
||||
const { cors, getUserAgent } = require("#server_functions");
|
||||
|
||||
describe("server_functions tests", () => {
|
||||
@@ -8,12 +7,11 @@ describe("server_functions tests", () => {
|
||||
let fetchResponseHeadersText;
|
||||
let corsResponse;
|
||||
let request;
|
||||
|
||||
let fetchMock;
|
||||
|
||||
beforeEach(() => {
|
||||
fetchResponseHeadersGet = jest.fn(() => {});
|
||||
fetchResponseHeadersText = jest.fn(() => {});
|
||||
fetchResponseHeadersGet = vi.fn(() => {});
|
||||
fetchResponseHeadersText = vi.fn(() => {});
|
||||
fetchResponse = {
|
||||
headers: {
|
||||
get: fetchResponseHeadersGet
|
||||
@@ -21,14 +19,14 @@ describe("server_functions tests", () => {
|
||||
text: fetchResponseHeadersText
|
||||
};
|
||||
|
||||
fetch = jest.fn();
|
||||
fetch = vi.fn();
|
||||
fetch.mockImplementation(() => fetchResponse);
|
||||
|
||||
fetchMock = fetch;
|
||||
|
||||
corsResponse = {
|
||||
set: jest.fn(() => {}),
|
||||
send: jest.fn(() => {})
|
||||
set: vi.fn(() => {}),
|
||||
send: vi.fn(() => {})
|
||||
};
|
||||
|
||||
request = {
|
||||
@@ -77,7 +75,7 @@ describe("server_functions tests", () => {
|
||||
fetchResponseHeadersText.mockImplementation(() => responseData);
|
||||
|
||||
let sentData;
|
||||
corsResponse.send = jest.fn((input) => {
|
||||
corsResponse.send = vi.fn((input) => {
|
||||
sentData = input;
|
||||
});
|
||||
|
||||
@@ -94,7 +92,7 @@ describe("server_functions tests", () => {
|
||||
});
|
||||
|
||||
let sentData;
|
||||
corsResponse.send = jest.fn((input) => {
|
||||
corsResponse.send = vi.fn((input) => {
|
||||
sentData = input;
|
||||
});
|
||||
|
||||
@@ -145,17 +143,17 @@ describe("server_functions tests", () => {
|
||||
});
|
||||
|
||||
it("Gets User-Agent from configuration", async () => {
|
||||
config = {};
|
||||
global.config = {};
|
||||
let userAgent;
|
||||
|
||||
userAgent = getUserAgent();
|
||||
expect(userAgent).toContain("Mozilla/5.0 (Node.js ");
|
||||
|
||||
config.userAgent = "Mozilla/5.0 (Foo)";
|
||||
global.config.userAgent = "Mozilla/5.0 (Foo)";
|
||||
userAgent = getUserAgent();
|
||||
expect(userAgent).toBe("Mozilla/5.0 (Foo)");
|
||||
|
||||
config.userAgent = () => "Mozilla/5.0 (Bar)";
|
||||
global.config.userAgent = () => "Mozilla/5.0 (Bar)";
|
||||
userAgent = getUserAgent();
|
||||
expect(userAgent).toBe("Mozilla/5.0 (Bar)");
|
||||
});
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
jest.mock("node:util", () => ({
|
||||
...jest.requireActual("util"),
|
||||
promisify: jest.fn()
|
||||
}));
|
||||
import { vi, describe, beforeEach, afterEach, it, expect } from "vitest";
|
||||
|
||||
jest.mock("node:fs", () => ({
|
||||
...jest.requireActual("fs"),
|
||||
statSync: jest.fn()
|
||||
}));
|
||||
/**
|
||||
* Creates a fresh GitHelper instance with isolated mocks for each test run.
|
||||
* @param {{ current: import("vitest").Mock | null }} fsStatSyncMockRef reference to the mocked fs.statSync.
|
||||
* @param {{ current: { error: import("vitest").Mock; info: import("vitest").Mock } | null }} loggerMockRef reference to logger stubs.
|
||||
* @param {{ current: import("vitest").MockInstance | null }} execShellSpyRef reference to the execShell spy.
|
||||
* @returns {Promise<unknown>} resolved GitHelper instance.
|
||||
*/
|
||||
async function createGitHelper (fsStatSyncMockRef, loggerMockRef, execShellSpyRef) {
|
||||
vi.resetModules();
|
||||
|
||||
jest.mock("logger", () => ({
|
||||
...jest.requireActual("logger"),
|
||||
error: jest.fn(),
|
||||
info: jest.fn()
|
||||
}));
|
||||
fsStatSyncMockRef.current = vi.fn();
|
||||
loggerMockRef.current = { error: vi.fn(), info: vi.fn() };
|
||||
|
||||
vi.doMock("node:fs", () => ({
|
||||
statSync: fsStatSyncMockRef.current
|
||||
}));
|
||||
|
||||
vi.doMock("logger", () => loggerMockRef.current);
|
||||
|
||||
const gitHelperModule = await import("../../../modules/default/updatenotification/git_helper");
|
||||
const GitHelper = gitHelperModule.default || gitHelperModule;
|
||||
const instance = new GitHelper();
|
||||
execShellSpyRef.current = vi.spyOn(instance, "execShell");
|
||||
instance.__loggerMock = loggerMockRef.current;
|
||||
return instance;
|
||||
}
|
||||
|
||||
describe("Updatenotification", () => {
|
||||
const execMock = jest.fn();
|
||||
|
||||
const fsStatSyncMockRef = { current: null };
|
||||
const loggerMockRef = { current: null };
|
||||
const execShellSpyRef = { current: null };
|
||||
let gitHelper;
|
||||
|
||||
let gitRemoteOut;
|
||||
@@ -28,15 +42,13 @@ describe("Updatenotification", () => {
|
||||
let gitFetchErr;
|
||||
let gitTagListOut;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { promisify } = require("node:util");
|
||||
promisify.mockReturnValue(execMock);
|
||||
const getExecutedCommands = () => execShellSpyRef.current.mock.calls.map(([command]) => command);
|
||||
|
||||
const GitHelper = require("../../../modules/default/updatenotification/git_helper");
|
||||
gitHelper = new GitHelper();
|
||||
});
|
||||
beforeEach(async () => {
|
||||
gitHelper = await createGitHelper(fsStatSyncMockRef, loggerMockRef, execShellSpyRef);
|
||||
|
||||
fsStatSyncMockRef.current.mockReturnValue({ isDirectory: () => true });
|
||||
|
||||
beforeEach(() => {
|
||||
gitRemoteOut = "";
|
||||
gitRevParseOut = "";
|
||||
gitStatusOut = "";
|
||||
@@ -46,48 +58,72 @@ describe("Updatenotification", () => {
|
||||
gitFetchErr = "";
|
||||
gitTagListOut = "";
|
||||
|
||||
execMock.mockImplementation((command) => {
|
||||
execShellSpyRef.current.mockImplementation((command) => {
|
||||
if (command.includes("git remote -v")) {
|
||||
return { stdout: gitRemoteOut };
|
||||
} else if (command.includes("git rev-parse HEAD")) {
|
||||
return { stdout: gitRevParseOut };
|
||||
} else if (command.includes("git status -sb")) {
|
||||
return { stdout: gitStatusOut };
|
||||
} else if (command.includes("git fetch -n --dry-run")) {
|
||||
return { stdout: gitFetchOut, stderr: gitFetchErr };
|
||||
} else if (command.includes("git rev-list --ancestry-path --count")) {
|
||||
return { stdout: gitRevListCountOut };
|
||||
} else if (command.includes("git rev-list --ancestry-path")) {
|
||||
return { stdout: gitRevListOut };
|
||||
} else if (command.includes("git ls-remote -q --tags --refs")) {
|
||||
return { stdout: gitTagListOut };
|
||||
return Promise.resolve({ stdout: gitRemoteOut, stderr: "" });
|
||||
}
|
||||
|
||||
if (command.includes("git rev-parse HEAD")) {
|
||||
return Promise.resolve({ stdout: gitRevParseOut, stderr: "" });
|
||||
}
|
||||
|
||||
if (command.includes("git status -sb")) {
|
||||
return Promise.resolve({ stdout: gitStatusOut, stderr: "" });
|
||||
}
|
||||
|
||||
if (command.includes("git fetch -n --dry-run")) {
|
||||
return Promise.resolve({ stdout: gitFetchOut, stderr: gitFetchErr });
|
||||
}
|
||||
|
||||
if (command.includes("git rev-list --ancestry-path --count")) {
|
||||
return Promise.resolve({ stdout: gitRevListCountOut, stderr: "" });
|
||||
}
|
||||
|
||||
if (command.includes("git rev-list --ancestry-path")) {
|
||||
return Promise.resolve({ stdout: gitRevListOut, stderr: "" });
|
||||
}
|
||||
|
||||
if (command.includes("git ls-remote -q --tags --refs")) {
|
||||
return Promise.resolve({ stdout: gitTagListOut, stderr: "" });
|
||||
}
|
||||
|
||||
return Promise.resolve({ stdout: "", stderr: "" });
|
||||
});
|
||||
|
||||
if (gitHelper.execShell !== execShellSpyRef.current) {
|
||||
throw new Error("execShell spy not applied");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
afterEach(() => {
|
||||
gitHelper.gitRepos = [];
|
||||
|
||||
jest.clearAllMocks();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("MagicMirror on develop", () => {
|
||||
const moduleName = "MagicMirror";
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
gitRemoteOut = "origin\tgit@github.com:MagicMirrorOrg/MagicMirror.git (fetch)\norigin\tgit@github.com:MagicMirrorOrg/MagicMirror.git (push)\n";
|
||||
gitRevParseOut = "332e429a41f1a2339afd4f0ae96dd125da6beada";
|
||||
gitStatusOut = "## develop...origin/develop\n M tests/unit/functions/updatenotification_spec.js\n";
|
||||
gitFetchErr = "From github.com:MagicMirrorOrg/MagicMirror\n60e0377..332e429 develop -> origin/develop\n";
|
||||
gitRevListCountOut = "5";
|
||||
|
||||
await gitHelper.add(moduleName);
|
||||
gitHelper.gitRepos = [{ module: moduleName, folder: "mock-path" }];
|
||||
});
|
||||
|
||||
it("returns status information", async () => {
|
||||
const repos = await gitHelper.getRepos();
|
||||
expect(repos[0]).toMatchSnapshot();
|
||||
expect(execMock).toHaveBeenCalledTimes(5);
|
||||
expect(getExecutedCommands()).toMatchInlineSnapshot(`
|
||||
[
|
||||
"cd mock-path && git rev-parse HEAD",
|
||||
"cd mock-path && git status -sb",
|
||||
"cd mock-path && git fetch -n --dry-run",
|
||||
"cd mock-path && git rev-list --ancestry-path --count 60e0377..332e429 develop",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("returns status information early if isBehindInStatus", async () => {
|
||||
@@ -95,38 +131,51 @@ describe("Updatenotification", () => {
|
||||
|
||||
const repos = await gitHelper.getRepos();
|
||||
expect(repos[0]).toMatchSnapshot();
|
||||
expect(execMock).toHaveBeenCalledTimes(3);
|
||||
expect(getExecutedCommands()).toMatchInlineSnapshot(`
|
||||
[
|
||||
"cd mock-path && git rev-parse HEAD",
|
||||
"cd mock-path && git status -sb",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("excludes repo if status can't be retrieved", async () => {
|
||||
const errorMessage = "Failed to retrieve status";
|
||||
execMock.mockRejectedValueOnce(errorMessage);
|
||||
execShellSpyRef.current.mockImplementationOnce(() => Promise.reject(new Error(errorMessage)));
|
||||
|
||||
expect(gitHelper.gitRepos).toHaveLength(1);
|
||||
const repos = await gitHelper.getRepos();
|
||||
expect(repos).toHaveLength(0);
|
||||
|
||||
const { error } = require("logger");
|
||||
expect(error).toHaveBeenCalledWith(`Failed to retrieve repo info for ${moduleName}: Failed to retrieve status`);
|
||||
expect(execShellSpyRef.current.mock.calls.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MagicMirror on master (empty taglist)", () => {
|
||||
const moduleName = "MagicMirror";
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
gitRemoteOut = "origin\tgit@github.com:MagicMirrorOrg/MagicMirror.git (fetch)\norigin\tgit@github.com:MagicMirrorOrg/MagicMirror.git (push)\n";
|
||||
gitRevParseOut = "332e429a41f1a2339afd4f0ae96dd125da6beada";
|
||||
gitStatusOut = "## master...origin/master\n M tests/unit/functions/updatenotification_spec.js\n";
|
||||
gitFetchErr = "From github.com:MagicMirrorOrg/MagicMirror\n60e0377..332e429 master -> origin/master\n";
|
||||
gitRevListCountOut = "5";
|
||||
|
||||
await gitHelper.add(moduleName);
|
||||
gitHelper.gitRepos = [{ module: moduleName, folder: "mock-path" }];
|
||||
});
|
||||
|
||||
it("returns status information", async () => {
|
||||
const repos = await gitHelper.getRepos();
|
||||
expect(repos[0]).toMatchSnapshot();
|
||||
expect(execMock).toHaveBeenCalledTimes(7);
|
||||
expect(getExecutedCommands()).toMatchInlineSnapshot(`
|
||||
[
|
||||
"cd mock-path && git rev-parse HEAD",
|
||||
"cd mock-path && git status -sb",
|
||||
"cd mock-path && git fetch -n --dry-run",
|
||||
"cd mock-path && git rev-list --ancestry-path --count 60e0377..332e429 master",
|
||||
"cd mock-path && git ls-remote -q --tags --refs",
|
||||
"cd mock-path && git rev-list --ancestry-path 60e0377..332e429 master",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("returns status information early if isBehindInStatus", async () => {
|
||||
@@ -134,40 +183,55 @@ describe("Updatenotification", () => {
|
||||
|
||||
const repos = await gitHelper.getRepos();
|
||||
expect(repos[0]).toMatchSnapshot();
|
||||
expect(execMock).toHaveBeenCalledTimes(7);
|
||||
expect(getExecutedCommands()).toMatchInlineSnapshot(`
|
||||
[
|
||||
"cd mock-path && git rev-parse HEAD",
|
||||
"cd mock-path && git status -sb",
|
||||
"cd mock-path && git fetch -n --dry-run",
|
||||
"cd mock-path && git rev-list --ancestry-path --count 60e0377..332e429 master",
|
||||
"cd mock-path && git ls-remote -q --tags --refs",
|
||||
"cd mock-path && git rev-list --ancestry-path 60e0377..332e429 master",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("excludes repo if status can't be retrieved", async () => {
|
||||
const errorMessage = "Failed to retrieve status";
|
||||
execMock.mockRejectedValueOnce(errorMessage);
|
||||
execShellSpyRef.current.mockImplementationOnce(() => Promise.reject(new Error(errorMessage)));
|
||||
|
||||
const repos = await gitHelper.getRepos();
|
||||
expect(repos).toHaveLength(0);
|
||||
|
||||
const { error } = require("logger");
|
||||
expect(error).toHaveBeenCalledWith(`Failed to retrieve repo info for ${moduleName}: Failed to retrieve status`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MagicMirror on master with match in taglist", () => {
|
||||
const moduleName = "MagicMirror";
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
gitRemoteOut = "origin\tgit@github.com:MagicMirrorOrg/MagicMirror.git (fetch)\norigin\tgit@github.com:MagicMirrorOrg/MagicMirror.git (push)\n";
|
||||
gitRevParseOut = "332e429a41f1a2339afd4f0ae96dd125da6beada";
|
||||
gitStatusOut = "## master...origin/master\n M tests/unit/functions/updatenotification_spec.js\n";
|
||||
gitFetchErr = "From github.com:MagicMirrorOrg/MagicMirror\n60e0377..332e429 master -> origin/master\n";
|
||||
gitRevListCountOut = "5";
|
||||
gitTagListOut = "332e429a41f1a2339afd4f0ae96dd125da6beada...tag...\n";
|
||||
gitTagListOut = "332e429a41f1a2339afd4f0ae96dd125da6beada\ttag\n";
|
||||
gitRevListOut = "332e429a41f1a2339afd4f0ae96dd125da6beada\n";
|
||||
|
||||
await gitHelper.add(moduleName);
|
||||
gitHelper.gitRepos = [{ module: moduleName, folder: "mock-path" }];
|
||||
});
|
||||
|
||||
it("returns status information", async () => {
|
||||
const repos = await gitHelper.getRepos();
|
||||
expect(repos[0]).toMatchSnapshot();
|
||||
expect(execMock).toHaveBeenCalledTimes(7);
|
||||
expect(getExecutedCommands()).toMatchInlineSnapshot(`
|
||||
[
|
||||
"cd mock-path && git rev-parse HEAD",
|
||||
"cd mock-path && git status -sb",
|
||||
"cd mock-path && git fetch -n --dry-run",
|
||||
"cd mock-path && git rev-list --ancestry-path --count 60e0377..332e429 master",
|
||||
"cd mock-path && git ls-remote -q --tags --refs",
|
||||
"cd mock-path && git rev-list --ancestry-path 60e0377..332e429 master",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("returns status information early if isBehindInStatus", async () => {
|
||||
@@ -175,40 +239,55 @@ describe("Updatenotification", () => {
|
||||
|
||||
const repos = await gitHelper.getRepos();
|
||||
expect(repos[0]).toMatchSnapshot();
|
||||
expect(execMock).toHaveBeenCalledTimes(7);
|
||||
expect(getExecutedCommands()).toMatchInlineSnapshot(`
|
||||
[
|
||||
"cd mock-path && git rev-parse HEAD",
|
||||
"cd mock-path && git status -sb",
|
||||
"cd mock-path && git fetch -n --dry-run",
|
||||
"cd mock-path && git rev-list --ancestry-path --count 60e0377..332e429 master",
|
||||
"cd mock-path && git ls-remote -q --tags --refs",
|
||||
"cd mock-path && git rev-list --ancestry-path 60e0377..332e429 master",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("excludes repo if status can't be retrieved", async () => {
|
||||
const errorMessage = "Failed to retrieve status";
|
||||
execMock.mockRejectedValueOnce(errorMessage);
|
||||
execShellSpyRef.current.mockImplementationOnce(() => Promise.reject(new Error(errorMessage)));
|
||||
|
||||
const repos = await gitHelper.getRepos();
|
||||
expect(repos).toHaveLength(0);
|
||||
|
||||
const { error } = require("logger");
|
||||
expect(error).toHaveBeenCalledWith(`Failed to retrieve repo info for ${moduleName}: Failed to retrieve status`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MagicMirror on master without match in taglist", () => {
|
||||
const moduleName = "MagicMirror";
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
gitRemoteOut = "origin\tgit@github.com:MagicMirrorOrg/MagicMirror.git (fetch)\norigin\tgit@github.com:MagicMirrorOrg/MagicMirror.git (push)\n";
|
||||
gitRevParseOut = "332e429a41f1a2339afd4f0ae96dd125da6beada";
|
||||
gitStatusOut = "## master...origin/master\n M tests/unit/functions/updatenotification_spec.js\n";
|
||||
gitFetchErr = "From github.com:MagicMirrorOrg/MagicMirror\n60e0377..332e429 master -> origin/master\n";
|
||||
gitRevListCountOut = "5";
|
||||
gitTagListOut = "xxxe429a41f1a2339afd4f0ae96dd125da6beada...tag...\n";
|
||||
gitTagListOut = "xxxe429a41f1a2339afd4f0ae96dd125da6beada\ttag\n";
|
||||
gitRevListOut = "332e429a41f1a2339afd4f0ae96dd125da6beada\n";
|
||||
|
||||
await gitHelper.add(moduleName);
|
||||
gitHelper.gitRepos = [{ module: moduleName, folder: "mock-path" }];
|
||||
});
|
||||
|
||||
it("returns status information", async () => {
|
||||
const repos = await gitHelper.getRepos();
|
||||
expect(repos[0]).toMatchSnapshot();
|
||||
expect(execMock).toHaveBeenCalledTimes(7);
|
||||
expect(getExecutedCommands()).toMatchInlineSnapshot(`
|
||||
[
|
||||
"cd mock-path && git rev-parse HEAD",
|
||||
"cd mock-path && git status -sb",
|
||||
"cd mock-path && git fetch -n --dry-run",
|
||||
"cd mock-path && git rev-list --ancestry-path --count 60e0377..332e429 master",
|
||||
"cd mock-path && git ls-remote -q --tags --refs",
|
||||
"cd mock-path && git rev-list --ancestry-path 60e0377..332e429 master",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("returns status information early if isBehindInStatus", async () => {
|
||||
@@ -216,18 +295,24 @@ describe("Updatenotification", () => {
|
||||
|
||||
const repos = await gitHelper.getRepos();
|
||||
expect(repos[0]).toMatchSnapshot();
|
||||
expect(execMock).toHaveBeenCalledTimes(7);
|
||||
expect(getExecutedCommands()).toMatchInlineSnapshot(`
|
||||
[
|
||||
"cd mock-path && git rev-parse HEAD",
|
||||
"cd mock-path && git status -sb",
|
||||
"cd mock-path && git fetch -n --dry-run",
|
||||
"cd mock-path && git rev-list --ancestry-path --count 60e0377..332e429 master",
|
||||
"cd mock-path && git ls-remote -q --tags --refs",
|
||||
"cd mock-path && git rev-list --ancestry-path 60e0377..332e429 master",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("excludes repo if status can't be retrieved", async () => {
|
||||
const errorMessage = "Failed to retrieve status";
|
||||
execMock.mockRejectedValueOnce(errorMessage);
|
||||
execShellSpyRef.current.mockImplementationOnce(() => Promise.reject(new Error(errorMessage)));
|
||||
|
||||
const repos = await gitHelper.getRepos();
|
||||
expect(repos).toHaveLength(0);
|
||||
|
||||
const { error } = require("logger");
|
||||
expect(error).toHaveBeenCalledWith(`Failed to retrieve repo info for ${moduleName}: Failed to retrieve status`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -241,13 +326,19 @@ describe("Updatenotification", () => {
|
||||
gitFetchErr = `From https://github.com/fewieden/${moduleName}\n19f7faf..9d83101 master -> origin/master`;
|
||||
gitRevListCountOut = "7";
|
||||
|
||||
await gitHelper.add(moduleName);
|
||||
gitHelper.gitRepos = [{ module: moduleName, folder: "mock-path" }];
|
||||
});
|
||||
|
||||
it("returns status information without hash", async () => {
|
||||
const repos = await gitHelper.getRepos();
|
||||
expect(repos[0]).toMatchSnapshot();
|
||||
expect(execMock).toHaveBeenCalledTimes(4);
|
||||
expect(getExecutedCommands()).toMatchInlineSnapshot(`
|
||||
[
|
||||
"cd mock-path && git status -sb",
|
||||
"cd mock-path && git fetch -n --dry-run",
|
||||
"cd mock-path && git rev-list --ancestry-path --count 19f7faf..9d83101 master",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
global.moment = require("moment-timezone");
|
||||
|
||||
const ical = require("node-ical");
|
||||
const { expect } = require("playwright/test");
|
||||
const moment = require("moment-timezone");
|
||||
const CalendarFetcherUtils = require("../../../../../modules/default/calendar/calendarfetcherutils");
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ describe("Default modules utils tests", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
fetchResponse = new Response();
|
||||
global.fetch = jest.fn(() => Promise.resolve(fetchResponse));
|
||||
global.fetch = vi.fn(() => Promise.resolve(fetchResponse));
|
||||
fetchMock = global.fetch;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
const TestSequencer = require("@jest/test-sequencer").default;
|
||||
|
||||
class CustomSequencer extends TestSequencer {
|
||||
sort (tests) {
|
||||
const orderPath = ["unit", "electron", "e2e"];
|
||||
return tests.sort((testA, testB) => {
|
||||
let indexA = -1;
|
||||
let indexB = -1;
|
||||
const reg = ".*/tests/([^/]*).*";
|
||||
|
||||
// move calendar and newsfeed at the end
|
||||
if (testA.path.includes("e2e/modules/calendar_spec") || testA.path.includes("e2e/modules/newsfeed_spec")) return 1;
|
||||
if (testB.path.includes("e2e/modules/calendar_spec") || testB.path.includes("e2e/modules/newsfeed_spec")) return -1;
|
||||
|
||||
let matchA = new RegExp(reg, "g").exec(testA.path);
|
||||
if (matchA.length > 0) indexA = orderPath.indexOf(matchA[1]);
|
||||
|
||||
let matchB = new RegExp(reg, "g").exec(testB.path);
|
||||
if (matchB.length > 0) indexB = orderPath.indexOf(matchB[1]);
|
||||
|
||||
if (indexA === indexB) return 0;
|
||||
|
||||
if (indexA === -1) return 1;
|
||||
if (indexB === -1) return -1;
|
||||
return indexA < indexB ? -1 : 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CustomSequencer;
|
||||
21
tests/utils/vitest-setup.js
Normal file
21
tests/utils/vitest-setup.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Vitest setup file for module aliasing
|
||||
* This allows require("logger") to work in unit tests
|
||||
*/
|
||||
|
||||
const Module = require("node:module");
|
||||
const path = require("node:path");
|
||||
|
||||
// Store the original require
|
||||
const originalRequire = Module.prototype.require;
|
||||
|
||||
// Override require to handle our custom aliases
|
||||
Module.prototype.require = function (id) {
|
||||
// Handle "logger" alias
|
||||
if (id === "logger") {
|
||||
return originalRequire.call(this, path.resolve(__dirname, "../../js/logger.js"));
|
||||
}
|
||||
|
||||
// Handle all other requires normally
|
||||
return originalRequire.apply(this, arguments);
|
||||
};
|
||||
70
vitest.config.mjs
Normal file
70
vitest.config.mjs
Normal file
@@ -0,0 +1,70 @@
|
||||
import {defineConfig} from "vitest/config";
|
||||
|
||||
/*
|
||||
* Sequential execution keeps our shared test server stable:
|
||||
* - All suites bind to port 8080
|
||||
* - Fixtures and temp paths are reused between tests
|
||||
* - Debugging becomes predictable
|
||||
*
|
||||
* Parallel execution would require dynamic ports and isolated fixtures,
|
||||
* so we intentionally cap Vitest at a single worker for now.
|
||||
*/
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
// Global settings
|
||||
globals: true,
|
||||
environment: "node",
|
||||
// Setup files for require aliasing
|
||||
setupFiles: ["./tests/utils/vitest-setup.js"],
|
||||
// Increased from 20s to 60s for E2E tests, 120s for Electron tests
|
||||
testTimeout: 120000,
|
||||
// Increase hook timeout for Electron cleanup
|
||||
hookTimeout: 30000,
|
||||
// Stop test execution on first failure
|
||||
bail: 1,
|
||||
|
||||
// File patterns
|
||||
include: [
|
||||
"tests/**/*_spec.js",
|
||||
// Legacy regression test without the _spec suffix
|
||||
"tests/unit/modules/default/calendar/calendar_fetcher_utils_bad_rrule.js"
|
||||
],
|
||||
exclude: [
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"tests/unit/mocks/**",
|
||||
"tests/unit/helpers/**",
|
||||
"tests/electron/helpers/**",
|
||||
"tests/e2e/helpers/**",
|
||||
"tests/e2e/mocks/**",
|
||||
"tests/configs/**",
|
||||
"tests/utils/**"
|
||||
],
|
||||
|
||||
// Coverage configuration
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["lcov", "text"],
|
||||
include: [
|
||||
"clientonly/**/*.js",
|
||||
"js/**/*.js",
|
||||
"modules/default/**/*.js",
|
||||
"serveronly/**/*.js"
|
||||
],
|
||||
exclude: [
|
||||
"**/node_modules/**",
|
||||
"**/tests/**",
|
||||
"**/dist/**"
|
||||
]
|
||||
},
|
||||
|
||||
/*
|
||||
* Pool settings for isolated test execution. Keep maxWorkers at 1 so
|
||||
* port 8080 and shared fixtures remain safe across the full suite.
|
||||
*/
|
||||
pool: "forks",
|
||||
maxWorkers: 1,
|
||||
isolate: true
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user