Release 2.29.0 (#3568)

## [2.29.0] - 2024-10-01

Thanks to: @bugsounet, @dkallen78, @jargordon, @khassel,
@KristjanESPERANTO, @MarcLandis, @rejas, @ryan-d-williams, @sdetweil,
@skpanagiotis.

> ⚠️ This release needs nodejs version `v20` or `v22`, minimum version
is `v20.9.0`

### Added

- [compliments] Added support for cron type date/time format entries mm
hh DD MM dow (minutes/hours/days/months and day of week) see
https://crontab.cronhub.io for construction (#3481)
- [core] Check config at every start of MagicMirror² (#3450)
- [core] Add spelling check (cspell): `npm run test:spelling` and handle
spelling issues (#3544)
- [core] removed `config.paths.vendor` (could not work because `vendor`
is hardcoded in `index.html`), renamed `config.paths.modules` to
`config.foreignModulesDir`, added variable `MM_CUSTOMCSS_FILE` which -
if set - overrides `config.customCss`, added variable `MM_MODULES_DIR`
which - if set - overrides `config.foreignModulesDir`, added test for
`MM_MODULES_DIR` (#3530)
- [core] elements are now removed from `index.html` when loading script
or stylesheet files fails
- [core] Added `MODULE_DOM_UPDATED` notification each time the DOM is
re-rendered via `updateDom` (#3534)
- [tests] added minimal needed node version to tests (currently v20.9.0)
to avoid releases with wrong node version info
- [tests] Added `node-libgpiod` library to electron-rebuild tests
(#3563)

### Removed

- [core] removed installer only files (#3492)
- [core] removed raspberry object from systeminformation (#3505)
- [linter] removed `eslint-plugin-import`, because it doesn't support
ESLint v9. We will reenter it later when it does.
- [tests] removed `onoff` library from electron-rebuild tests (#3563)

### Updated

- [weather] Updated `apiVersion` default from 2.5 to 3.0 (#3424)
- [core] Updated dependencies including stylistic-eslint
- [core] nail down `node-ical` version to `0.18.0` with exception
`allow-ghsas: GHSA-8hc4-vh64-cxmj` in `dep-review.yaml` (which should
removed after next `node-ical` update)
- [core] Updated SocketIO catch all to new API
- [core] Allow custom modules positions by scanning index.html for the
defined regions, instead of hard coded (PR #3518 fixes issue #3504)
- [core] Detail optimizations in `config_check.js`
- [core] Updated minimal needed node version in `package.json`
(currently v20.9.0) (#3559) and except for v21 (no security updates)
(#3561)
- [linter] Switch to ESLint v9 and flat config and replace
`eslint-plugin-unicorn` by `@eslint/js`
- [core] fix discovering module positions twice after #3450

### Fixed

- Fixed `checks` badge in README.md
- [weather] Fixed issue with the UK Met Office provider following a
change in their API paths and header info.
- [core] add check for node_helper loading for multiple instances of
same module (#3502)
- [weather] Fixed issue for respecting unit config on broadcasted
notifications
- [tests] Fixes calendar test by moving it from e2e to electron with
fixed date (#3532)
- [calendar] fixed sliceMultiDayEvents getting wrong count and
displaying incorrect entries, Europe/Berlin (#3542)
- [tests] ignore `js/positions.js` when linting (this file is created at
runtime)
- [calendar] fixed sliceMultiDayEvents showing previous day without
config enabled

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Michael Teeuw <michael@xonaymedia.nl>
Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ross Younger <crazyscot@gmail.com>
Co-authored-by: Veeck <github@veeck.de>
Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr>
Co-authored-by: jkriegshauser <joshuakr@nvidia.com>
Co-authored-by: illimarkangur <116028111+illimarkangur@users.noreply.github.com>
Co-authored-by: sam detweiler <sdetweil@gmail.com>
Co-authored-by: vppencilsharpener <tim.pray@gmail.com>
Co-authored-by: veeck <michael.veeck@nebenan.de>
Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com>
Co-authored-by: Brian O'Connor <btoconnor@users.noreply.github.com>
Co-authored-by: WallysWellies <59727507+WallysWellies@users.noreply.github.com>
Co-authored-by: Jason Stieber <jrstieber@gmail.com>
Co-authored-by: jargordon <50050429+jargordon@users.noreply.github.com>
Co-authored-by: Daniel <32464403+dkallen78@users.noreply.github.com>
Co-authored-by: Ryan Williams <65094007+ryan-d-williams@users.noreply.github.com>
Co-authored-by: Panagiotis Skias <panagiotis.skias@gmail.com>
Co-authored-by: Marc Landis <dirk.rettschlag@gmail.com>
This commit is contained in:
Karsten Hassel
2024-10-01 00:02:17 +02:00
committed by GitHub
parent 53fc814ff8
commit 94c3c699e8
84 changed files with 3899 additions and 2830 deletions

View File

@@ -5,7 +5,7 @@ describe("AnimateCSS integration Test", () => {
let testConfigFile = "tests/configs/modules/compliments/compliments_animateCSS.js";
// define config file to fallback to default: wrong animation name (must return no animation)
let testConfigFileFallbackToDefault = "tests/configs/modules/compliments/compliments_animateCSS_fallbackToDefault.js";
// define config file with an inversed name animation : in for out and vice versa (must return no animation)
// define config file with an inverted name animation : in for out and vice versa (must return no animation)
let testConfigFileInvertedAnimationName = "tests/configs/modules/compliments/compliments_animateCSS_invertedAnimationName.js";
// define config file with no animation defined
let testConfigByDefault = "tests/configs/modules/compliments/compliments_anytime.js";

View File

@@ -0,0 +1,30 @@
const helpers = require("./helpers/global-setup");
describe("Custom Position of modules", () => {
beforeAll(async () => {
await helpers.fixupIndex();
await helpers.startApplication("tests/configs/customregions.js");
await helpers.getDocument();
});
afterAll(async () => {
await helpers.stopApplication();
await helpers.restoreIndex();
});
const positions = ["row3_left", "top3_left1"];
let i = 0;
const className1 = positions[i].replace("_", ".");
let message1 = positions[i];
it(`should show text in ${message1}`, async () => {
const elem = await helpers.waitForElement(`.${className1}`);
expect(elem).not.toBeNull();
expect(elem.textContent).toContain(`Text in ${message1}`);
});
i = 1;
const className2 = positions[i].replace("_", ".");
let message2 = positions[i];
it(`should NOT show text in ${message2}`, async () => {
const elem = await helpers.waitForElement(`.${className2}`, "", 1500);
expect(elem).toBeNull();
}, 1510);
});

View File

@@ -1,5 +1,20 @@
const os = require("node:os");
const fs = require("node:fs");
const jsdom = require("jsdom");
const indexFile = `${__dirname}/../../../index.html`;
const cssFile = `${__dirname}/../../../css/custom.css`;
const sampleCss = [
".region.row3 {",
" top: 0;",
"}",
".region.row3.left {",
" top: 100%;",
"}"
];
var indexData = [];
var cssData = [];
exports.startApplication = async (configFilename, exec) => {
jest.resetModules();
if (global.app) {
@@ -45,11 +60,12 @@ exports.getDocument = () => {
});
};
exports.waitForElement = (selector, ignoreValue = "") => {
exports.waitForElement = (selector, ignoreValue = "", timeout = 0) => {
return new Promise((resolve) => {
let oldVal = "dummy12345";
let element = null;
const interval = setInterval(() => {
const element = document.querySelector(selector);
element = document.querySelector(selector);
if (element) {
let newVal = element.textContent;
if (newVal === oldVal) {
@@ -64,6 +80,12 @@ exports.waitForElement = (selector, ignoreValue = "") => {
}
}
}, 100);
if (timeout !== 0) {
setTimeout(() => {
if (interval) clearInterval(interval);
resolve(null);
}, timeout);
}
});
};
@@ -91,3 +113,34 @@ exports.testMatch = async (element, regex) => {
expect(elem.textContent).toMatch(regex);
return true;
};
exports.fixupIndex = async () => {
// read and save the git level index file
indexData = (await fs.promises.readFile(indexFile)).toString();
// make lines of the content
let workIndexLines = indexData.split(os.EOL);
// loop thru the lines to find place to insert new region
for (let l in workIndexLines) {
if (workIndexLines[l].includes("region top right")) {
// insert a new line with new region definition
workIndexLines.splice(l, 0, " <div class=\"region row3 left\"><div class=\"container\"></div></div>");
break;
}
}
// write out the new index.html file, not append
await fs.promises.writeFile(indexFile, workIndexLines.join(os.EOL), { flush: true });
// read in the current custom.css
cssData = (await fs.promises.readFile(cssFile)).toString();
// write out the custom.css for this testcase, matching the new region name
await fs.promises.writeFile(cssFile, sampleCss.join(os.EOL), { flush: true });
};
exports.restoreIndex = async () => {
// if we read in data
if (indexData.length > 1) {
//write out saved index.html
await fs.promises.writeFile(indexFile, indexData, { flush: true });
// write out saved custom.css
await fs.promises.writeFile(cssFile, cssData, { flush: true });
}
};

View File

@@ -88,17 +88,6 @@ describe("Calendar module", () => {
});
});
describe("Events from multiple calendars", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/calendar/show-duplicates-in-calendar.js");
await helpers.getDocument();
});
it("should show multiple events with the same title and start time from different calendars", async () => {
await expect(testElementLength(".calendar .event", 22)).resolves.toBe(true);
});
});
//Will contain everyday an fullDayEvent that starts today and ends tomorrow, and one starting tomorrow and ending the day after tomorrow
describe("FullDayEvent over several days should show how many days are left from the from the starting date on", () => {
beforeAll(async () => {

View File

@@ -77,5 +77,16 @@ describe("Compliments module", () => {
await expect(doTest(["Special day message"])).resolves.toBe(true);
});
});
describe("cron type key", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/compliments/compliments_e2e_cron_entry.js");
await helpers.getDocument();
});
it("compliments array contains only special value", async () => {
await expect(doTest(["anytime cron"])).resolves.toBe(true);
});
});
});
});

View File

@@ -1,10 +1,7 @@
const fs = require("node:fs");
const helpers = require("../helpers/global-setup");
describe("Newsfeed module", () => {
afterAll(async () => {
await helpers.stopApplication();
});
const runTests = async () => {
describe("Default configuration", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/newsfeed/default.js");
@@ -74,4 +71,28 @@ describe("Newsfeed module", () => {
expect(elem.textContent).toContain("No news at the moment.");
});
});
};
describe("Newsfeed module", () => {
afterAll(async () => {
await helpers.stopApplication();
});
runTests();
});
describe("Newsfeed module located in config directory", () => {
beforeAll(() => {
const baseDir = `${__dirname}/../../..`;
if (!fs.existsSync(`${baseDir}/config/newsfeed`)) {
fs.cpSync(`${baseDir}/modules/default/newsfeed`, `${baseDir}/config/newsfeed`, { recursive: true });
}
process.env.MM_MODULES_DIR = "config";
});
afterAll(async () => {
await helpers.stopApplication();
});
runTests();
});

View File

@@ -53,8 +53,8 @@ describe("Weather module: Weather Hourly Forecast", () => {
});
describe("Shows precipitation probability", () => {
const propabilities = [undefined, undefined, "12 %", "36 %", "44 %"];
for (const [index, pop] of propabilities.entries()) {
const probabilities = [undefined, undefined, "12 %", "36 %", "44 %"];
for (const [index, pop] of probabilities.entries()) {
if (pop) {
it(`should render probability ${pop}`, async () => {
await expect(weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-prob`, pop)).resolves.toBe(true);

View File

@@ -12,7 +12,7 @@ describe("Display of modules", () => {
it("should show the test header", async () => {
const elem = await helpers.waitForElement("#module_0_helloworld .module-header");
expect(elem).not.toBeNull();
// textContent gibt hier lowercase zurück, das uppercase wird durch css realisiert, was daher nicht in textContent landet
// textContent returns lowercase here, the uppercase is realized by CSS, which therefore does not end up in textContent
expect(elem.textContent).toBe("test_header");
});

View File

@@ -7,7 +7,7 @@ describe("App environment", () => {
beforeAll(async () => {
process.env.MM_CONFIG_FILE = "tests/configs/default.js";
serverProcess = await require("node:child_process").spawn("npm", ["run", "server"], { env: process.env, detached: true });
// we have to wait until the server is startet
// we have to wait until the server is started
await delay(2000);
});
afterAll(async () => {