Skip to content

Commit 462abf7

Browse files
[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?
1 parent b542f33 commit 462abf7

30 files changed

+5650
-6842
lines changed

.github/CONTRIBUTING.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,19 @@ To run markdownlint, use `node --run lint:markdown`.
3030

3131
## Testing
3232

33-
We use [Jest](https://jestjs.io) for JavaScript testing.
33+
We use [Vitest](https://vitest.dev) for JavaScript testing.
3434

3535
To run all tests, use `node --run test`.
3636

37-
The specific test commands are defined in `package.json`.
38-
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`.
37+
The `package.json` scripts expose finer-grained test commands:
38+
39+
- `test:unit` – run unit tests only
40+
- `test:e2e` – execute browser-driven end-to-end tests
41+
- `test:electron` – launch the Electron-based regression suite
42+
- `test:coverage` – collect coverage while running every suite
43+
- `test:watch` – keep Vitest in watch mode for fast local feedback
44+
- `test:ui` – open the Vitest UI dashboard (needs OS file-watch support enabled)
45+
- `test:calendar` – run the legacy calendar debug helper
46+
- `test:css`, `test:markdown`, `test:prettier`, `test:spelling`, `test:js` – lint-only scripts that enforce formatting, spelling, markdown style, and ESLint.
47+
48+
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`.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,6 @@ js/positions.js
8888
# Ignore lock files other than package-lock.json
8989
pnpm-lock.yaml
9090
yarn.lock
91+
92+
# Vitest temporary test files
93+
tests/**/.tmp/

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ planned for 2026-01-01
2222
- [check_config] refactor: improve error handling (#3927)
2323
- [calendar] test: remove "Recurring event per timezone" test (#3929)
2424
- [calendar] chore: remove `requiresVersion: "2.1.0"` (#3932)
25+
- [tests] migrate from `jest` to `vitest` (#3940)
2526

2627
### Fixed
2728

eslint.config.mjs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import {defineConfig, globalIgnores} from "eslint/config";
22
import globals from "globals";
33
import {flatConfigs as importX} from "eslint-plugin-import-x";
4-
import jest from "eslint-plugin-jest";
54
import js from "@eslint/js";
65
import jsdocPlugin from "eslint-plugin-jsdoc";
76
import packageJson from "eslint-plugin-package-json";
87
import stylistic from "@stylistic/eslint-plugin";
8+
import vitest from "eslint-plugin-vitest";
99

1010
export default defineConfig([
1111
globalIgnores(["config/**", "modules/**/*", "!modules/default/**", "js/positions.js"]),
@@ -16,15 +16,16 @@ export default defineConfig([
1616
globals: {
1717
...globals.browser,
1818
...globals.node,
19+
...vitest.environments.env.globals,
1920
Log: "readonly",
2021
MM: "readonly",
2122
Module: "readonly",
2223
config: "readonly",
2324
moment: "readonly"
2425
}
2526
},
26-
plugins: {js, stylistic},
27-
extends: [importX.recommended, jest.configs["flat/recommended"], "js/recommended", jsdocPlugin.configs["flat/recommended"], "stylistic/all"],
27+
plugins: {js, stylistic, vitest},
28+
extends: [importX.recommended, vitest.configs.recommended, "js/recommended", jsdocPlugin.configs["flat/recommended"], "stylistic/all"],
2829
rules: {
2930
"@stylistic/array-element-newline": ["error", "consistent"],
3031
"@stylistic/arrow-parens": ["error", "always"],
@@ -57,12 +58,9 @@ export default defineConfig([
5758
"import-x/newline-after-import": "error",
5859
"import-x/order": "error",
5960
"init-declarations": "off",
60-
"jest/consistent-test-it": "warn",
61-
"jest/no-done-callback": "warn",
62-
"jest/prefer-expect-resolves": "warn",
63-
"jest/prefer-mock-promise-shorthand": "warn",
64-
"jest/prefer-to-be": "warn",
65-
"jest/prefer-to-have-length": "warn",
61+
"vitest/consistent-test-it": "warn",
62+
"vitest/prefer-to-be": "warn",
63+
"vitest/prefer-to-have-length": "warn",
6664
"max-lines-per-function": ["warn", 400],
6765
"max-statements": "off",
6866
"no-global-assign": "off",

js/app.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ function App () {
6565
async function loadConfig () {
6666
Log.log("Loading config ...");
6767
const defaults = require(`${__dirname}/defaults`);
68-
if (process.env.JEST_WORKER_ID !== undefined) {
69-
// if we are running with jest
68+
if (global.mmTestMode) {
69+
// if we are running in test mode
7070
defaults.address = "0.0.0.0";
7171
}
7272

@@ -185,10 +185,10 @@ function App () {
185185

186186
if (defaultModules.includes(moduleName)) {
187187
const defaultModuleFolder = path.resolve(`${global.root_path}/modules/default/`, module);
188-
if (process.env.JEST_WORKER_ID === undefined) {
188+
if (!global.mmTestMode) {
189189
moduleFolder = defaultModuleFolder;
190190
} else {
191-
// running in Jest, allow defaultModules placed under moduleDir for testing
191+
// running in test mode, allow defaultModules placed under moduleDir for testing
192192
if (env.modulesDir === "modules" || env.modulesDir === "tests/mocks") {
193193
moduleFolder = defaultModuleFolder;
194194
}

js/electron.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ function createWindow () {
7676

7777
const electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions);
7878

79-
if (process.env.JEST_WORKER_ID !== undefined && process.env.MOCK_DATE !== undefined) {
80-
// if we are running with jest and we want to mock the current date
79+
if (process.env.MOCK_DATE !== undefined) {
80+
// if we are running tests and we want to mock the current date
8181
const fakeNow = new Date(process.env.MOCK_DATE).valueOf();
8282
Date = class extends Date {
8383
constructor (...args) {
@@ -114,8 +114,8 @@ function createWindow () {
114114

115115
// Open the DevTools if run with "node --run start:dev"
116116
if (process.argv.includes("dev")) {
117-
if (process.env.JEST_WORKER_ID !== undefined) {
118-
// if we are running with jest
117+
if (process.env.mmTestMode) {
118+
// if we are running tests
119119
const devtools = new BrowserWindow(electronOptions);
120120
mainWindow.webContents.setDevToolsWebContents(devtools.webContents);
121121
}
@@ -169,8 +169,8 @@ function createWindow () {
169169

170170
// Quit when all windows are closed.
171171
app.on("window-all-closed", function () {
172-
if (process.env.JEST_WORKER_ID !== undefined) {
173-
// if we are running with jest
172+
if (process.env.mmTestMode) {
173+
// if we are running tests
174174
app.quit();
175175
} else {
176176
createWindow();

js/loader.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const Loader = (function () {
8484
if (window.name !== "jsdom") {
8585
moduleFolder = defaultModuleFolder;
8686
} else {
87-
// running in Jest, allow defaultModules placed under moduleDir for testing
87+
// running in test mode, allow defaultModules placed under moduleDir for testing
8888
if (envVars.modulesDir === "modules") {
8989
moduleFolder = defaultModuleFolder;
9090
}

js/logger.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// This logger is very simple, but needs to be extended.
22
(function (root, factory) {
33
if (typeof exports === "object") {
4-
if (process.env.JEST_WORKER_ID === undefined) {
4+
if (process.env.mmTestMode !== "true") {
55
const { styleText } = require("node:util");
66

77
// add timestamps in front of log messages
@@ -78,8 +78,8 @@
7878
let logLevel;
7979
let enableLog;
8080
if (typeof exports === "object") {
81-
// in nodejs and not running with jest
82-
enableLog = process.env.JEST_WORKER_ID === undefined;
81+
// in nodejs and not running in test mode
82+
enableLog = process.env.mmTestMode !== "true";
8383
} else {
8484
// in browser and not running with jsdom
8585
enableLog = typeof window === "object" && window.name !== "jsdom";
@@ -97,7 +97,7 @@
9797
groupEnd: Function.prototype.bind.call(console.groupEnd, console),
9898
time: Function.prototype.bind.call(console.time, console),
9999
timeEnd: Function.prototype.bind.call(console.timeEnd, console),
100-
timeStamp: Function.prototype.bind.call(console.timeStamp, console)
100+
timeStamp: console.timeStamp ? Function.prototype.bind.call(console.timeStamp, console) : function () {}
101101
};
102102

103103
logLevel.setLogLevel = function (newLevel) {

js/module_functions.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
* @param {Promise} callback function to call when the timer expires
66
*/
77
const scheduleTimer = function (timer, intervalMS, callback) {
8-
if (process.env.JEST_WORKER_ID === undefined) {
9-
// only set timer when not running in jest
8+
if (process.env.mmTestMode !== "true") {
9+
// only set timer when not running in test mode
1010
let tmr = timer;
1111
clearTimeout(tmr);
1212
tmr = setTimeout(function () {

js/utils.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ module.exports = {
3434
].join("\n");
3535
Log.info(systemDataString);
3636

37-
// Return is currently only for jest
37+
// Return is currently only for tests
3838
return systemDataString;
3939
} catch (error) {
4040
Log.error(error);
@@ -65,8 +65,10 @@ module.exports = {
6565
if (results && results.length > 0) {
6666
// get the position parts and replace space with underscore
6767
const positionName = results[1].replace(" ", "_");
68-
// add it to the list
69-
modulePositions.push(positionName);
68+
// add it to the list only if not already present (avoid duplicates)
69+
if (!modulePositions.includes(positionName)) {
70+
modulePositions.push(positionName);
71+
}
7072
}
7173
});
7274
try {

0 commit comments

Comments
 (0)