Release 2.23.0 (#3078)

## [2.23.0] - 2023-04-04

Thanks to: @angeldeejay, @buxxi, @CarJem, @dariom, @DaveChild, @dWoolridge, @grenagit, @Hirschberger, @KristjanESPERANTO, @MagMar94, @naveensrinivasan, @nfogal, @psieg, @rajniszp, @retroflex, @SkySails and @tomzt.

Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not all) of the work on this release as project collaborators. This version would not be there without their effort. Thank you guys! You are awesome!

### Added

- Added increments for hourly forecasts in weather module (#2996)
- Added tests for hourly weather forecast
- Added possibility to ignore MagicMirror repo in updatenotification module
- Added Pirate Weather as new weather provider (#3005)
- Added possibility to use your own templates in Alert module
- Added error message if `<modulename>.js` file is missing in module folder to get a hint in the logs (#2403)
- Added possibility to use environment variables in `config.js` (#1756)
- Added option `pastDaysCount` to default calendar module to control of how many days past events should be displayed
- Added thai language to alert module
- Added option `sendNotifications` in clock module (#3056)

### Removed

- Removed darksky weather provider
- Removed unneeded (and unwanted) '.' after the year in calendar repeatingCountTitle (#2896)

### Updated

- Use develop as target branch for dependabot
- Update issue template, contributing doc and sample config
- The weather modules clearly separates precipitation amount and probability (risk of rain/snow)
  - This requires all providers that only supports probability to change the config from `showPrecipitationAmount` to `showPrecipitationProbability`.
- Update tests for weather and calendar module
- Changed updatenotification module for MagicMirror repo only: Send only notifications for `master` if there is a tag on a newer commit
- Update dates in Calendar widgets every minute
- Cleanup jest coverage for patches
- Update `stylelint` dependencies, switch to `stylelint-config-standard` and handle `stylelint` issues, update `main.css` matching new rules
- Update Eslint config, add new rule and handle issue
- Convert lots of callbacks to async/await
- Revise require imports (#3071 and #3072)

### Fixed

- Fix wrong day labels in envcanada forecast (#2987)
- Fix for missing default class name prefix for customEvents in calendar
- Fix electron flashing white screen on startup (#1919)
- Fix weathergov provider hourly forecast (#3008)
- Fix message display with HTML code into alert module (#2828)
- Fix typo in french translation
- Yr wind direction is no longer inverted
- Fix async node_helper stopping electron start (#2487)
- The wind direction arrow now points in the direction the wind is flowing, not into the wind (#3019)
- Fix precipitation css styles and rounding value
- Fix wrong vertical alignment of calendar title column when wrapEvents is true (#3053)
- Fix empty news feed stopping the reload forever
- Fix e2e tests (failed after async changes) by running calendar and newsfeed tests last
- Lint: Use template literals instead of string concatenation
- Fix default alert module to render HTML for title and message
- Fix Open-Meteo wind speed units
This commit is contained in:
Michael Teeuw
2023-04-04 20:44:32 +02:00
committed by GitHub
parent f14e956166
commit abe5c08a52
162 changed files with 6619 additions and 3019 deletions

View File

@@ -3,7 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
let config = require(`${process.cwd()}/tests/configs/default.js`).configFactory({
ipWhitelist: []
});

View File

@@ -11,9 +11,10 @@ let config = {
module: "calendar",
position: "bottom_bar",
config: {
customEvents: [{ keyword: "CustomEvent", symbol: "dice" }],
calendars: [
{
maximumEntries: 4,
maximumEntries: 5,
maximumNumberOfDays: 10000,
symbol: "birthday-cake",
fullDaySymbol: "calendar-day",

View File

@@ -14,7 +14,7 @@ let config = {
module: "helloworld",
position: positions[idx],
config: {
text: "Text in " + positions[idx]
text: `Text in ${positions[idx]}`
}
});
}

View File

@@ -15,7 +15,8 @@ let config = {
location: "Munich",
mockData: '"#####WEATHERDATA#####"',
weatherEndpoint: "/forecast/daily",
decimalSymbol: "_"
decimalSymbol: "_",
showPrecipitationAmount: true
}
}
]

View File

@@ -0,0 +1,25 @@
/* MagicMirror² Test config hourly weather
*
* By rejas https://github.com/rejas
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "weather",
position: "bottom_bar",
config: {
type: "hourly",
location: "Berlin",
mockData: '"#####WEATHERDATA#####"'
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -0,0 +1,26 @@
/* MagicMirror² Test config hourly weather options
*
* By rejas https://github.com/rejas
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "weather",
position: "bottom_bar",
config: {
type: "hourly",
location: "Berlin",
mockData: '"#####WEATHERDATA#####"',
hourlyForecastIncrements: 2
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -0,0 +1,27 @@
/* MagicMirror² Test config hourly weather
*
* By rejas https://github.com/rejas
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "weather",
position: "bottom_bar",
config: {
type: "hourly",
location: "Berlin",
mockData: '"#####WEATHERDATA#####"',
showPrecipitationAmount: true,
showPrecipitationProbability: true
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -3,7 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
let config = require(`${process.cwd()}/tests/configs/default.js`).configFactory({
ipWhitelist: ["x.x.x.x"]
});

View File

@@ -3,7 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
let config = require(`${process.cwd()}/tests/configs/default.js`).configFactory({
port: 8090
});

View File

@@ -0,0 +1 @@
MM_PORT=8090

View File

@@ -0,0 +1,13 @@
/* MagicMirror² Test config sample environment set port 8090
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
port: ${MM_PORT}
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -3,7 +3,7 @@ const helpers = require("./helpers/global-setup");
describe("All font files from roboto.css should be downloadable", () => {
const fontFiles = [];
// Statements below filters out all 'url' lines in the CSS file
const fileContent = require("fs").readFileSync(__dirname + "/../../fonts/roboto.css", "utf8");
const fileContent = require("fs").readFileSync(`${__dirname}/../../fonts/roboto.css`, "utf8");
const regex = /\burl\(['"]([^'"]+)['"]\)/g;
let match = regex.exec(fileContent);
while (match !== null) {
@@ -21,7 +21,7 @@ describe("All font files from roboto.css should be downloadable", () => {
});
test.each(fontFiles)("should return 200 HTTP code for file '%s'", async (fontFile) => {
const fontUrl = "http://localhost:8080/fonts/" + fontFile;
const fontUrl = `http://localhost:8080/fonts/${fontFile}`;
const res = await helpers.fetch(fontUrl);
expect(res.status).toBe(200);
});

View File

@@ -12,7 +12,7 @@ app.use(basicAuth);
// Set available directories
const directories = ["/tests/configs", "/tests/mocks"];
const rootPath = path.resolve(__dirname + "/../../../");
const rootPath = path.resolve(`${__dirname}/../../../`);
for (let directory of directories) {
app.use(directory, express.static(path.resolve(rootPath + directory)));

View File

@@ -13,26 +13,22 @@ exports.startApplication = async (configFilename, exec) => {
process.env.MM_CONFIG_FILE = configFilename;
}
if (exec) exec;
global.app = require("app.js");
global.app = require("../../../js/app");
return new Promise((resolve) => {
global.app.start(resolve);
});
return global.app.start();
};
exports.stopApplication = async () => {
if (global.app) {
return new Promise((resolve) => {
global.app.stop(resolve);
delete global.app;
});
if (!global.app) {
return Promise.resolve();
}
return Promise.resolve();
await global.app.stop();
delete global.app;
};
exports.getDocument = () => {
return new Promise((resolve) => {
const url = "http://" + (config.address || "localhost") + ":" + (config.port || "8080");
const url = `http://${config.address || "localhost"}:${config.port || "8080"}`;
jsdom.JSDOM.fromURL(url, { resources: "usable", runScripts: "dangerously" }).then((dom) => {
dom.window.name = "jsdom";
dom.window.fetch = corefetch;

View File

@@ -1,7 +1,5 @@
const { injectMockData } = require("../../utils/weather_mocker");
const helpers = require("./global-setup");
const path = require("path");
const fs = require("fs");
const { generateWeather, generateWeatherForecast } = require("../../mocks/weather_test");
exports.getText = async (element, result) => {
const elem = await helpers.waitForElement(element);
@@ -14,16 +12,8 @@ exports.getText = async (element, result) => {
).toBe(result);
};
exports.startApp = async (configFile, additionalMockData) => {
let mockWeather;
if (configFile.includes("forecast")) {
mockWeather = generateWeatherForecast(additionalMockData);
} else {
mockWeather = generateWeather(additionalMockData);
}
let content = fs.readFileSync(path.resolve(__dirname + "../../../../" + configFile)).toString();
content = content.replace("#####WEATHERDATA#####", mockWeather);
fs.writeFileSync(path.resolve(__dirname + "../../../../config/config.js"), content);
exports.startApp = async (configFileName, additionalMockData) => {
injectMockData(configFileName, additionalMockData);
await helpers.startApplication("");
await helpers.getDocument();
};

View File

@@ -1,5 +1,5 @@
const helpers = require("../helpers/global-setup");
const serverBasicAuth = require("../helpers/basic-auth.js");
const serverBasicAuth = require("../helpers/basic-auth");
describe("Calendar module", () => {
/**
@@ -48,14 +48,18 @@ describe("Calendar module", () => {
await helpers.getDocument();
});
it("should show the custom maximumEntries of 4", async () => {
await testElementLength(".calendar .event", 4);
it("should show the custom maximumEntries of 5", async () => {
await testElementLength(".calendar .event", 5);
});
it("should show the custom calendar symbol in each event", async () => {
it("should show the custom calendar symbol in four events", async () => {
await testElementLength(".calendar .event .fa-birthday-cake", 4);
});
it("should show a customEvent calendar symbol in one event", async () => {
await testElementLength(".calendar .event .fa-dice", 1);
});
it("should show two custom icons for repeating events", async () => {
await testElementLength(".calendar .event .fa-undo", 2);
});
@@ -87,7 +91,7 @@ describe("Calendar module", () => {
await helpers.getDocument();
});
it('should contain text "Mar 25th" in timezone UTC ' + -i, async () => {
it(`should contain text "Mar 25th" in timezone UTC ${-i}`, async () => {
await testTextContain(".calendar", "Mar 25th");
});
});

View File

@@ -1,5 +1,5 @@
const helpers = require("../helpers/global-setup");
const moment = require("moment");
const helpers = require("../helpers/global-setup");
describe("Clock module", () => {
afterAll(async () => {
@@ -89,7 +89,7 @@ describe("Clock module", () => {
it("should show the week with the correct number of week of year", async () => {
const currentWeekNumber = moment().week();
const weekToShow = "Week " + currentWeekNumber;
const weekToShow = `Week ${currentWeekNumber}`;
const elem = await helpers.waitForElement(".clock .week");
expect(elem).not.toBe(null);
expect(elem.textContent).toBe(weekToShow);
@@ -103,7 +103,7 @@ describe("Clock module", () => {
});
it("should show the analog clock face", async () => {
const elem = helpers.waitForElement(".clockCircle");
const elem = helpers.waitForElement(".clock-circle");
expect(elem).not.toBe(null);
});
});

View File

@@ -46,7 +46,7 @@ describe("Weather module", () => {
});
it("should render windDirection with an arrow", async () => {
const elem = await helpers.waitForElement(".weather .normal.medium sup i.fa-long-arrow-alt-up");
const elem = await helpers.waitForElement(".weather .normal.medium sup i.fa-long-arrow-alt-down");
expect(elem).not.toBe(null);
expect(elem.outerHTML).toContain("transform:rotate(250deg);");
});

View File

@@ -13,14 +13,14 @@ describe("Weather module: Weather Forecast", () => {
const days = ["Today", "Tomorrow", "Sun", "Mon", "Tue"];
for (const [index, day] of days.entries()) {
it("should render day " + day, async () => {
it(`should render day ${day}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day);
});
}
const icons = ["day-cloudy", "rain", "day-sunny", "day-sunny", "day-sunny"];
for (const [index, icon] of icons.entries()) {
it("should render icon " + icon, async () => {
it(`should render icon ${icon}`, async () => {
const elem = await helpers.waitForElement(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(2) span.wi-${icon}`);
expect(elem).not.toBe(null);
});
@@ -28,21 +28,21 @@ describe("Weather module: Weather Forecast", () => {
const maxTemps = ["24.4°", "21.0°", "22.9°", "23.4°", "20.6°"];
for (const [index, temp] of maxTemps.entries()) {
it("should render max temperature " + temp, async () => {
it(`should render max temperature ${temp}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`, temp);
});
}
const minTemps = ["15.3°", "13.6°", "13.8°", "13.9°", "10.9°"];
for (const [index, temp] of minTemps.entries()) {
it("should render min temperature " + temp, async () => {
it(`should render min temperature ${temp}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(4)`, temp);
});
}
const opacities = [1, 1, 0.8, 0.5333333333333333, 0.2666666666666667];
for (const [index, opacity] of opacities.entries()) {
it("should render fading of rows with opacity=" + opacity, async () => {
it(`should render fading of rows with opacity=${opacity}`, async () => {
const elem = await helpers.waitForElement(`.weather table.small tr:nth-child(${index + 1})`);
expect(elem).not.toBe(null);
expect(elem.outerHTML).toContain(`<tr style="opacity: ${opacity};">`);
@@ -57,7 +57,7 @@ describe("Weather module: Weather Forecast", () => {
const days = ["Fri", "Sat", "Sun", "Mon", "Tue"];
for (const [index, day] of days.entries()) {
it("should render day " + day, async () => {
it(`should render day ${day}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day);
});
}
@@ -79,18 +79,40 @@ describe("Weather module: Weather Forecast", () => {
expect(table.rows).not.toBe(null);
expect(table.rows.length).toBe(5);
});
const precipitations = [undefined, "2.51 mm"];
for (const [index, precipitation] of precipitations.entries()) {
if (precipitation) {
it(`should render precipitation amount ${precipitation}`, async () => {
await weatherFunc.getText(`.weather table tr:nth-child(${index + 1}) td.precipitation-amount`, precipitation);
});
}
}
});
describe("Forecast weather units", () => {
describe("Forecast weather with imperial units", () => {
beforeAll(async () => {
await weatherFunc.startApp("tests/configs/modules/weather/forecastweather_units.js", {});
});
const temperatures = ["75_9°", "69_8°", "73_2°", "74_1°", "69_1°"];
for (const [index, temp] of temperatures.entries()) {
it("should render custom decimalSymbol = '_' for temp " + temp, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`, temp);
});
}
describe("Temperature units", () => {
const temperatures = ["75_9°", "69_8°", "73_2°", "74_1°", "69_1°"];
for (const [index, temp] of temperatures.entries()) {
it(`should render custom decimalSymbol = '_' for temp ${temp}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`, temp);
});
}
});
describe("Precipitation units", () => {
const precipitations = [undefined, "0.10 in"];
for (const [index, precipitation] of precipitations.entries()) {
if (precipitation) {
it(`should render precipitation amount ${precipitation}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-amount`, precipitation);
});
}
}
});
});
});

View File

@@ -0,0 +1,64 @@
const helpers = require("../helpers/global-setup");
const weatherFunc = require("../helpers/weather-functions");
describe("Weather module: Weather Hourly Forecast", () => {
afterAll(async () => {
await helpers.stopApplication();
});
describe("Default configuration", () => {
beforeAll(async () => {
await weatherFunc.startApp("tests/configs/modules/weather/hourlyweather_default.js", {});
});
const minTemps = ["7:00 pm", "8:00 pm", "9:00 pm", "10:00 pm", "11:00 pm"];
for (const [index, hour] of minTemps.entries()) {
it(`should render forecast for hour ${hour}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td.day`, hour);
});
}
});
describe("Hourly weather options", () => {
beforeAll(async () => {
await weatherFunc.startApp("tests/configs/modules/weather/hourlyweather_options.js", {});
});
describe("Hourly increments of 2", () => {
const minTemps = ["7:00 pm", "9:00 pm", "11:00 pm", "1:00 am", "3:00 am"];
for (const [index, hour] of minTemps.entries()) {
it(`should render forecast for hour ${hour}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td.day`, hour);
});
}
});
});
describe("Show precipitations", () => {
beforeAll(async () => {
await weatherFunc.startApp("tests/configs/modules/weather/hourlyweather_showPrecipitation.js", {});
});
describe("Shows precipitation amount", () => {
const amounts = [undefined, undefined, undefined, "0.13 mm", "0.13 mm"];
for (const [index, amount] of amounts.entries()) {
if (amount) {
it(`should render precipitation amount ${amount}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-amount`, amount);
});
}
}
});
describe("Shows precipitation probability", () => {
const propabilities = [undefined, undefined, "12 %", "36 %", "44 %"];
for (const [index, pop] of propabilities.entries()) {
if (pop) {
it(`should render probability ${pop}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-prob`, pop);
});
}
}
});
});
});

View File

@@ -13,10 +13,10 @@ describe("Position of modules", () => {
for (const position of positions) {
const className = position.replace("_", ".");
it("should show text in " + position, async () => {
const elem = await helpers.waitForElement("." + className);
it(`should show text in ${position}`, async () => {
const elem = await helpers.waitForElement(`.${className}`);
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("Text in " + position);
expect(elem.textContent).toContain(`Text in ${position}`);
});
}
});

View File

@@ -0,0 +1,15 @@
const helpers = require("./helpers/global-setup");
describe("templated config with port variable", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/port_variable.js");
});
afterAll(async () => {
await helpers.stopApplication();
});
it("should return 200", async () => {
const res = await helpers.fetch("http://localhost:8090");
expect(res.status).toBe(200);
});
});

View File

@@ -1,10 +1,10 @@
const fs = require("fs");
const path = require("path");
const translations = require("../../translations/translations.js");
const helmet = require("helmet");
const { JSDOM } = require("jsdom");
const express = require("express");
const sinon = require("sinon");
const translations = require("../../translations/translations");
describe("Translations", () => {
let server;
@@ -21,8 +21,8 @@ describe("Translations", () => {
server = app.listen(3000);
});
afterAll(() => {
server.close();
afterAll(async () => {
await server.close();
});
it("should have a translation file in the specified path", () => {
@@ -48,17 +48,15 @@ describe("Translations", () => {
dom.window.onload = async () => {
const { Translator, Module, config } = dom.window;
config.language = "en";
Translator.load = sinon.stub().callsFake((_m, _f, _fb, callback) => callback());
Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null);
Module.register("name", { getTranslations: () => translations });
const MMM = Module.create("name");
const loaded = sinon.stub();
MMM.loadTranslations(loaded);
await MMM.loadTranslations();
expect(loaded.callCount).toBe(1);
expect(Translator.load.args.length).toBe(1);
expect(Translator.load.calledWith(MMM, "translations/en.json", false, sinon.match.func)).toBe(true);
expect(Translator.load.calledWith(MMM, "translations/en.json", false)).toBe(true);
done();
};
@@ -67,18 +65,16 @@ describe("Translations", () => {
it("should load translation + fallback file", (done) => {
dom.window.onload = async () => {
const { Translator, Module } = dom.window;
Translator.load = sinon.stub().callsFake((_m, _f, _fb, callback) => callback());
Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null);
Module.register("name", { getTranslations: () => translations });
const MMM = Module.create("name");
const loaded = sinon.stub();
MMM.loadTranslations(loaded);
await MMM.loadTranslations();
expect(loaded.callCount).toBe(1);
expect(Translator.load.args.length).toBe(2);
expect(Translator.load.calledWith(MMM, "translations/de.json", false, sinon.match.func)).toBe(true);
expect(Translator.load.calledWith(MMM, "translations/en.json", true, sinon.match.func)).toBe(true);
expect(Translator.load.calledWith(MMM, "translations/de.json", false)).toBe(true);
expect(Translator.load.calledWith(MMM, "translations/en.json", true)).toBe(true);
done();
};
@@ -88,17 +84,15 @@ describe("Translations", () => {
dom.window.onload = async () => {
const { Translator, Module, config } = dom.window;
config.language = "--";
Translator.load = sinon.stub().callsFake((_m, _f, _fb, callback) => callback());
Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null);
Module.register("name", { getTranslations: () => translations });
const MMM = Module.create("name");
const loaded = sinon.stub();
MMM.loadTranslations(loaded);
await MMM.loadTranslations();
expect(loaded.callCount).toBe(1);
expect(Translator.load.args.length).toBe(1);
expect(Translator.load.calledWith(MMM, "translations/en.json", true, sinon.match.func)).toBe(true);
expect(Translator.load.calledWith(MMM, "translations/en.json", true)).toBe(true);
done();
};
@@ -112,10 +106,8 @@ describe("Translations", () => {
Module.register("name", {});
const MMM = Module.create("name");
const loaded = sinon.stub();
MMM.loadTranslations(loaded);
await MMM.loadTranslations();
expect(loaded.callCount).toBe(1);
expect(Translator.load.callCount).toBe(0);
done();
@@ -138,14 +130,13 @@ describe("Translations", () => {
<script src="file://${path.join(__dirname, "..", "..", "js", "translator.js")}">`,
{ runScripts: "dangerously", resources: "usable" }
);
dom.window.onload = () => {
dom.window.onload = async () => {
const { Translator } = dom.window;
Translator.load(mmm, translations[language], false, () => {
expect(typeof Translator.translations[mmm.name]).toBe("object");
expect(Object.keys(Translator.translations[mmm.name]).length).toBeGreaterThanOrEqual(1);
done();
});
await Translator.load(mmm, translations[language], false);
expect(typeof Translator.translations[mmm.name]).toBe("object");
expect(Object.keys(Translator.translations[mmm.name]).length).toBeGreaterThanOrEqual(1);
done();
};
});
}
@@ -161,13 +152,12 @@ describe("Translations", () => {
<script src="file://${path.join(__dirname, "..", "..", "js", "translator.js")}">`,
{ runScripts: "dangerously", resources: "usable" }
);
dom.window.onload = () => {
dom.window.onload = async () => {
const { Translator } = dom.window;
Translator.load(mmm, translations.de, false, () => {
base = Object.keys(Translator.translations[mmm.name]).sort();
done();
});
await Translator.load(mmm, translations.de, false);
base = Object.keys(Translator.translations[mmm.name]).sort();
done();
};
});
@@ -191,13 +181,12 @@ describe("Translations", () => {
<script src="file://${path.join(__dirname, "..", "..", "js", "translator.js")}">`,
{ runScripts: "dangerously", resources: "usable" }
);
dom.window.onload = () => {
dom.window.onload = async () => {
const { Translator } = dom.window;
Translator.load(mmm, translations[language], false, () => {
keys = Object.keys(Translator.translations[mmm.name]).sort();
done();
});
await Translator.load(mmm, translations[language], false);
keys = Object.keys(Translator.translations[mmm.name]).sort();
done();
};
});

View File

@@ -9,11 +9,11 @@ describe("Vendors", () => {
});
describe("Get list vendors", () => {
const vendors = require(__dirname + "/../../vendor/vendor.js");
const vendors = require(`${__dirname}/../../vendor/vendor.js`);
Object.keys(vendors).forEach((vendor) => {
it(`should return 200 HTTP code for vendor "${vendor}"`, async () => {
const urlVendor = "http://localhost:8080/vendor/" + vendors[vendor];
const urlVendor = `http://localhost:8080/vendor/${vendors[vendor]}`;
const res = await helpers.fetch(urlVendor);
expect(res.status).toBe(200);
});
@@ -21,7 +21,7 @@ describe("Vendors", () => {
Object.keys(vendors).forEach((vendor) => {
it(`should return 404 HTTP code for vendor https://localhost/"${vendor}"`, async () => {
const urlVendor = "http://localhost:8080/" + vendors[vendor];
const urlVendor = `http://localhost:8080/${vendors[vendor]}`;
const res = await helpers.fetch(urlVendor);
expect(res.status).toBe(404);
});

View File

@@ -1,5 +1,5 @@
const helpers = require("./helpers/global-setup");
const events = require("events");
const helpers = require("./helpers/global-setup");
describe("Electron app environment", () => {
beforeEach(async () => {
@@ -13,7 +13,7 @@ describe("Electron app environment", () => {
it("should open browserwindow", async () => {
const module = await helpers.getElement("#module_0_helloworld");
expect(await module.textContent()).toContain("Test Display Header");
expect(await global.electronApp.windows().length).toBe(1);
expect(global.electronApp.windows().length).toBe(1);
});
});

View File

@@ -8,7 +8,6 @@ exports.startApplication = async (configFilename, systemDate = null, electronPar
global.page = null;
process.env.MM_CONFIG_FILE = configFilename;
process.env.TZ = "GMT";
jest.retryTimes(3);
global.electronApp = await electron.launch({ args: electronParams });
await global.electronApp.firstWindow();

View File

@@ -1,7 +1,5 @@
const { injectMockData } = require("../../utils/weather_mocker");
const helpers = require("./global-setup");
const path = require("path");
const fs = require("fs");
const { generateWeather, generateWeatherForecast } = require("../../mocks/weather_test");
exports.getText = async (element, result) => {
const elem = await helpers.getElement(element);
@@ -15,15 +13,7 @@ exports.getText = async (element, result) => {
).toBe(result);
};
exports.startApp = async (configFile, systemDate) => {
let mockWeather;
if (configFile.includes("forecast")) {
mockWeather = generateWeatherForecast();
} else {
mockWeather = generateWeather();
}
let content = fs.readFileSync(path.resolve(__dirname + "../../../../" + configFile)).toString();
content = content.replace("#####WEATHERDATA#####", mockWeather);
fs.writeFileSync(path.resolve(__dirname + "../../../../config/config.js"), content);
exports.startApp = async (configFileNameName, systemDate) => {
injectMockData(configFileNameName);
await helpers.startApplication("", systemDate);
};

View File

@@ -7,11 +7,8 @@ describe("Calendar module", () => {
* @param {string} cssClass css selector
*/
const doTest = async (cssClass) => {
await helpers.getElement(".calendar");
await helpers.getElement(".module-content");
const events = await global.page.locator(".event");
const elem = await events.locator(cssClass);
expect(elem).not.toBe(null);
let elem = await helpers.getElement(`.calendar .module-content .event${cssClass}`);
expect(await elem.isVisible()).toBe(true);
};
afterEach(async () => {
@@ -19,14 +16,29 @@ describe("Calendar module", () => {
});
describe("Test css classes", () => {
it("has css class dayBeforeYesterday", async () => {
await helpers.startApplication("tests/configs/modules/calendar/custom.js", "03 Jan 2030 12:30:00 GMT");
await doTest(".dayBeforeYesterday");
});
it("has css class yesterday", async () => {
await helpers.startApplication("tests/configs/modules/calendar/custom.js", "02 Jan 2030 12:30:00 GMT");
await doTest(".yesterday");
});
it("has css class today", async () => {
await helpers.startApplication("tests/configs/modules/calendar/custom.js", "01 Jan 2030 12:30:00 GMT");
await doTest(".today");
});
it("has css class tomorrow", async () => {
await helpers.startApplication("tests/configs/modules/calendar/custom.js", "31 Dez 2029 12:30:00 GMT");
await helpers.startApplication("tests/configs/modules/calendar/custom.js", "31 Dec 2029 12:30:00 GMT");
await doTest(".tomorrow");
});
it("has css class dayAfterTomorrow", async () => {
await helpers.startApplication("tests/configs/modules/calendar/custom.js", "30 Dec 2029 12:30:00 GMT");
await doTest(".dayAfterTomorrow");
});
});
});

View File

@@ -53,4 +53,11 @@ RRULE:FREQ=YEARLY;BYMONTH=10;BYMONTHDAY=1
DTEND;VALUE=DATE:20301002
SUMMARY:TestEventRepeatDay
END:VEVENT
END:VCALENDAR
BEGIN:VEVENT
DTSTAMP:20200721T094531Z
UID:20200719T094531Z-167389794@marudot.com
DTSTART;TZID=Europe/Berlin:20301112T120000
DTEND;TZID=Europe/Berlin:20301112T130000
SUMMARY:TestEventCustomEventIcon
END:VEVENT
END:VCALENDAR

View File

@@ -0,0 +1,48 @@
{
"coord": {
"lon": 11.58,
"lat": 48.14
},
"weather": [
{
"id": 615,
"main": "Snow",
"description": "light rain and snow",
"icon": "13d"
},
{
"id": 500,
"main": "Rain",
"description": "light rain",
"icon": "10d"
}
],
"base": "stations",
"main": {
"temp": 1.49,
"pressure": 1005,
"humidity": 93.7,
"temp_min": 1,
"temp_max": 2
},
"visibility": 7000,
"wind": {
"speed": 11.8,
"deg": 250
},
"clouds": {
"all": 75
},
"dt": 1547387400,
"sys": {
"type": 1,
"id": 1267,
"message": 0.0031,
"country": "DE",
"sunrise": 1547362817,
"sunset": 1547394301
},
"id": 2867714,
"name": "Munich",
"cod": 200
}

View File

@@ -0,0 +1,202 @@
{
"city": {
"id": 2867714,
"name": "Munich",
"coord": {
"lon": 11.5754,
"lat": 48.1371
},
"country": "DE",
"population": 1260391,
"timezone": 7200
},
"cod": "200",
"message": 0.9653487,
"cnt": 7,
"list": [
{
"dt": 1568372400,
"sunrise": 1568350044,
"sunset": 1568395948,
"temp": {
"day": 24.44,
"min": 15.35,
"max": 24.44,
"night": 15.35,
"eve": 18,
"morn": 23.03
},
"pressure": 1031.65,
"humidity": 70,
"weather": [
{
"id": 801,
"main": "Clouds",
"description": "few clouds",
"icon": "02d"
}
],
"speed": 3.35,
"deg": 314,
"clouds": 21
},
{
"dt": 1568458800,
"sunrise": 1568436525,
"sunset": 1568482223,
"temp": {
"day": 20.81,
"min": 13.56,
"max": 21.02,
"night": 13.56,
"eve": 16.6,
"morn": 15.88
},
"pressure": 1028.81,
"humidity": 72,
"weather": [
{
"id": 500,
"main": "Rain",
"description": "light rain",
"icon": "10d"
}
],
"speed": 2.21,
"deg": 81,
"clouds": 100,
"pop": 0.7,
"rain": 2.51
},
{
"dt": 1568545200,
"sunrise": 1568523007,
"sunset": 1568568497,
"temp": {
"day": 22.65,
"min": 13.76,
"max": 22.88,
"night": 15.27,
"eve": 17.45,
"morn": 13.76
},
"pressure": 1023.75,
"humidity": 64,
"weather": [
{
"id": 800,
"main": "Clear",
"description": "sky is clear",
"icon": "01d"
}
],
"speed": 1.15,
"deg": 7,
"clouds": 0
},
{
"dt": 1568631600,
"sunrise": 1568609489,
"sunset": 1568654771,
"temp": {
"day": 23.45,
"min": 13.95,
"max": 23.45,
"night": 13.95,
"eve": 17.75,
"morn": 15.21
},
"pressure": 1020.41,
"humidity": 64,
"weather": [
{
"id": 800,
"main": "Clear",
"description": "sky is clear",
"icon": "01d"
}
],
"speed": 3.07,
"deg": 298,
"clouds": 7
},
{
"dt": 1568718000,
"sunrise": 1568695970,
"sunset": 1568741045,
"temp": {
"day": 20.55,
"min": 10.95,
"max": 20.55,
"night": 10.95,
"eve": 14.82,
"morn": 13.24
},
"pressure": 1019.4,
"humidity": 66,
"weather": [
{
"id": 800,
"main": "Clear",
"description": "sky is clear",
"icon": "01d"
}
],
"speed": 2.8,
"deg": 333,
"clouds": 2
},
{
"dt": 1568804400,
"sunrise": 1568782452,
"sunset": 1568827319,
"temp": {
"day": 18.15,
"min": 7.75,
"max": 18.15,
"night": 7.75,
"eve": 12.45,
"morn": 9.41
},
"pressure": 1017.56,
"humidity": 52,
"weather": [
{
"id": 800,
"main": "Clear",
"description": "sky is clear",
"icon": "01d"
}
],
"speed": 2.92,
"deg": 34,
"clouds": 0
},
{
"dt": 1568890800,
"sunrise": 1568868934,
"sunset": 1568913593,
"temp": {
"day": 14.85,
"min": 5.56,
"max": 15.05,
"night": 5.56,
"eve": 9.56,
"morn": 6.25
},
"pressure": 1022.7,
"humidity": 59,
"weather": [
{
"id": 800,
"main": "Clear",
"description": "sky is clear",
"icon": "01d"
}
],
"speed": 2.89,
"deg": 51,
"clouds": 1
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,176 +0,0 @@
const _ = require("lodash");
/**
* @param {object} extendedData extra data to add to the default mock data
* @returns {string} mocked current weather data
*/
const generateWeather = (extendedData = {}) => {
return JSON.stringify(
_.merge(
{},
{
coord: {
lon: 11.58,
lat: 48.14
},
weather: [
{
id: 615,
main: "Snow",
description: "light rain and snow",
icon: "13d"
},
{
id: 500,
main: "Rain",
description: "light rain",
icon: "10d"
}
],
base: "stations",
main: {
temp: 1.49,
pressure: 1005,
humidity: 93.7,
temp_min: 1,
temp_max: 2
},
visibility: 7000,
wind: {
speed: 11.8,
deg: 250
},
clouds: {
all: 75
},
dt: 1547387400,
sys: {
type: 1,
id: 1267,
message: 0.0031,
country: "DE",
sunrise: 1547362817,
sunset: 1547394301
},
id: 2867714,
name: "Munich",
cod: 200
},
extendedData
)
);
};
/**
* @param {object} extendedData extra data to add to the default mock data
* @returns {string} mocked forecast weather data
*/
const generateWeatherForecast = (extendedData = {}) => {
return JSON.stringify(
_.merge(
{},
{
city: {
id: 2867714,
name: "Munich",
coord: { lon: 11.5754, lat: 48.1371 },
country: "DE",
population: 1260391,
timezone: 7200
},
cod: "200",
message: 0.9653487,
cnt: 7,
list: [
{
dt: 1568372400,
sunrise: 1568350044,
sunset: 1568395948,
temp: { day: 24.44, min: 15.35, max: 24.44, night: 15.35, eve: 18, morn: 23.03 },
pressure: 1031.65,
humidity: 70,
weather: [{ id: 801, main: "Clouds", description: "few clouds", icon: "02d" }],
speed: 3.35,
deg: 314,
clouds: 21
},
{
dt: 1568458800,
sunrise: 1568436525,
sunset: 1568482223,
temp: { day: 20.81, min: 13.56, max: 21.02, night: 13.56, eve: 16.6, morn: 15.88 },
pressure: 1028.81,
humidity: 72,
weather: [{ id: 500, main: "Rain", description: "light rain", icon: "10d" }],
speed: 2.21,
deg: 81,
clouds: 100
},
{
dt: 1568545200,
sunrise: 1568523007,
sunset: 1568568497,
temp: { day: 22.65, min: 13.76, max: 22.88, night: 15.27, eve: 17.45, morn: 13.76 },
pressure: 1023.75,
humidity: 64,
weather: [{ id: 800, main: "Clear", description: "sky is clear", icon: "01d" }],
speed: 1.15,
deg: 7,
clouds: 0
},
{
dt: 1568631600,
sunrise: 1568609489,
sunset: 1568654771,
temp: { day: 23.45, min: 13.95, max: 23.45, night: 13.95, eve: 17.75, morn: 15.21 },
pressure: 1020.41,
humidity: 64,
weather: [{ id: 800, main: "Clear", description: "sky is clear", icon: "01d" }],
speed: 3.07,
deg: 298,
clouds: 7
},
{
dt: 1568718000,
sunrise: 1568695970,
sunset: 1568741045,
temp: { day: 20.55, min: 10.95, max: 20.55, night: 10.95, eve: 14.82, morn: 13.24 },
pressure: 1019.4,
humidity: 66,
weather: [{ id: 800, main: "Clear", description: "sky is clear", icon: "01d" }],
speed: 2.8,
deg: 333,
clouds: 2
},
{
dt: 1568804400,
sunrise: 1568782452,
sunset: 1568827319,
temp: { day: 18.15, min: 7.75, max: 18.15, night: 7.75, eve: 12.45, morn: 9.41 },
pressure: 1017.56,
humidity: 52,
weather: [{ id: 800, main: "Clear", description: "sky is clear", icon: "01d" }],
speed: 2.92,
deg: 34,
clouds: 0
},
{
dt: 1568890800,
sunrise: 1568868934,
sunset: 1568913593,
temp: { day: 14.85, min: 5.56, max: 15.05, night: 5.56, eve: 9.56, morn: 6.25 },
pressure: 1022.7,
humidity: 59,
weather: [{ id: 800, main: "Clear", description: "sky is clear", icon: "01d" }],
speed: 2.89,
deg: 51,
clouds: 1
}
]
},
extendedData
)
);
};
module.exports = { generateWeather, generateWeatherForecast };

View File

@@ -2,6 +2,7 @@ const path = require("path");
const helmet = require("helmet");
const { JSDOM } = require("jsdom");
const express = require("express");
const sockets = new Set();
describe("Translator", () => {
@@ -23,14 +24,13 @@ describe("Translator", () => {
});
});
afterAll(() => {
afterAll(async () => {
for (const socket of sockets) {
socket.destroy();
sockets.delete(socket);
}
server.close();
await server.close();
});
describe("translate", () => {
@@ -158,53 +158,50 @@ describe("Translator", () => {
it("should load translations", (done) => {
const dom = new JSDOM(`<script>var Log = {log: () => {}};</script><script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = () => {
dom.window.onload = async () => {
const { Translator } = dom.window;
const file = "translation_test.json";
Translator.load(mmm, file, false, () => {
const json = require(path.join(__dirname, "..", "..", "..", "tests", "mocks", file));
expect(Translator.translations[mmm.name]).toEqual(json);
done();
});
await Translator.load(mmm, file, false);
const json = require(path.join(__dirname, "..", "..", "..", "tests", "mocks", file));
expect(Translator.translations[mmm.name]).toEqual(json);
done();
};
});
it("should load translation fallbacks", (done) => {
const dom = new JSDOM(`<script>var Log = {log: () => {}};</script><script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = () => {
dom.window.onload = async () => {
const { Translator } = dom.window;
const file = "translation_test.json";
Translator.load(mmm, file, true, () => {
const json = require(path.join(__dirname, "..", "..", "..", "tests", "mocks", file));
expect(Translator.translationsFallback[mmm.name]).toEqual(json);
done();
});
await Translator.load(mmm, file, true);
const json = require(path.join(__dirname, "..", "..", "..", "tests", "mocks", file));
expect(Translator.translationsFallback[mmm.name]).toEqual(json);
done();
};
});
it("should not load translations, if module fallback exists", (done) => {
const dom = new JSDOM(`<script>var Log = {log: () => {}};</script><script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = () => {
dom.window.onload = async () => {
const { Translator, XMLHttpRequest } = dom.window;
const file = "translation_test.json";
XMLHttpRequest.prototype.send = () => {
throw "Shouldn't load files";
throw new Error("Shouldn't load files");
};
Translator.translationsFallback[mmm.name] = {
Hello: "Hallo"
};
Translator.load(mmm, file, false, () => {
expect(Translator.translations[mmm.name]).toBe(undefined);
expect(Translator.translationsFallback[mmm.name]).toEqual({
Hello: "Hallo"
});
done();
await Translator.load(mmm, file, false);
expect(Translator.translations[mmm.name]).toBe(undefined);
expect(Translator.translationsFallback[mmm.name]).toEqual({
Hello: "Hallo"
});
done();
};
});
});
@@ -216,9 +213,9 @@ describe("Translator", () => {
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
{ runScripts: "dangerously", resources: "usable" }
);
dom.window.onload = () => {
dom.window.onload = async () => {
const { Translator } = dom.window;
Translator.loadCoreTranslations("en");
await Translator.loadCoreTranslations("en");
const en = require(path.join(__dirname, "..", "..", "..", "tests", "mocks", "translation_test.json"));
setTimeout(() => {
@@ -235,9 +232,9 @@ describe("Translator", () => {
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
{ runScripts: "dangerously", resources: "usable" }
);
dom.window.onload = () => {
dom.window.onload = async () => {
const { Translator } = dom.window;
Translator.loadCoreTranslations("MISSINGLANG");
await Translator.loadCoreTranslations("MISSINGLANG");
const en = require(path.join(__dirname, "..", "..", "..", "tests", "mocks", "translation_test.json"));
setTimeout(() => {
@@ -256,9 +253,9 @@ describe("Translator", () => {
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
{ runScripts: "dangerously", resources: "usable" }
);
dom.window.onload = () => {
dom.window.onload = async () => {
const { Translator } = dom.window;
Translator.loadCoreTranslationsFallback();
await Translator.loadCoreTranslationsFallback();
const en = require(path.join(__dirname, "..", "..", "..", "tests", "mocks", "translation_test.json"));
setTimeout(() => {
@@ -274,9 +271,9 @@ describe("Translator", () => {
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
{ runScripts: "dangerously", resources: "usable" }
);
dom.window.onload = () => {
dom.window.onload = async () => {
const { Translator } = dom.window;
Translator.loadCoreTranslations();
await Translator.loadCoreTranslations();
setTimeout(() => {
expect(Translator.coreTranslationsFallback).toEqual({});

View File

@@ -1,5 +1,5 @@
const Utils = require("../../../js/utils.js");
const colors = require("colors/safe");
const Utils = require("../../../js/utils");
describe("Utils", () => {
describe("colors", () => {

View File

@@ -11,24 +11,90 @@ exports[`Updatenotification custom module returns status information without has
}
`;
exports[`Updatenotification default returns status information 1`] = `
exports[`Updatenotification MagicMirror on develop returns status information 1`] = `
{
"behind": 5,
"current": "develop",
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
"isBehindInStatus": false,
"module": "default",
"module": "MagicMirror",
"tracking": "origin/develop",
}
`;
exports[`Updatenotification default returns status information early if isBehindInStatus 1`] = `
exports[`Updatenotification MagicMirror on develop returns status information early if isBehindInStatus 1`] = `
{
"behind": 5,
"current": "develop",
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
"isBehindInStatus": true,
"module": "default",
"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",
}
`;

View File

@@ -10,7 +10,7 @@ describe("Functions into modules/default/calendar/calendar.js", () => {
beforeAll(() => {
// load calendar.js
require("../../../modules/default/calendar/calendar.js");
require("../../../modules/default/calendar/calendar");
});
describe("capFirst", () => {

View File

@@ -23,12 +23,10 @@ describe("Updatenotification", () => {
let gitRevParseOut;
let gitStatusOut;
let gitFetchOut;
let gitRevListCountOut;
let gitRevListOut;
let gitRemoteErr;
let gitRevParseErr;
let gitStatusErr;
let gitFetchErr;
let gitRevListErr;
let gitTagListOut;
beforeAll(async () => {
const { promisify } = require("util");
@@ -43,24 +41,26 @@ describe("Updatenotification", () => {
gitRevParseOut = "";
gitStatusOut = "";
gitFetchOut = "";
gitRevListCountOut = "";
gitRevListOut = "";
gitRemoteErr = "";
gitRevParseErr = "";
gitStatusErr = "";
gitFetchErr = "";
gitRevListErr = "";
gitTagListOut = "";
execMock.mockImplementation((command) => {
if (command.includes("git remote -v")) {
return { stdout: gitRemoteOut, stderr: gitRemoteErr };
return { stdout: gitRemoteOut };
} else if (command.includes("git rev-parse HEAD")) {
return { stdout: gitRevParseOut, stderr: gitRevParseErr };
return { stdout: gitRevParseOut };
} else if (command.includes("git status -sb")) {
return { stdout: gitStatusOut, stderr: gitStatusErr };
} else if (command.includes("git fetch --dry-run")) {
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: gitRevListOut, stderr: gitRevListErr };
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 };
}
});
});
@@ -71,15 +71,15 @@ describe("Updatenotification", () => {
jest.clearAllMocks();
});
describe("default", () => {
const moduleName = "default";
describe("MagicMirror on develop", () => {
const moduleName = "MagicMirror";
beforeEach(async () => {
gitRemoteOut = "origin\tgit@github.com:MichMich/MagicMirror.git (fetch)\norigin\tgit@github.com:MichMich/MagicMirror.git (push)\n";
gitRevParseOut = "332e429a41f1a2339afd4f0ae96dd125da6beada";
gitStatusOut = "## develop...origin/develop\n M tests/unit/functions/updatenotification_spec.js\n";
gitFetchErr = "From github.com:MichMich/MagicMirror\n60e0377..332e429 develop -> origin/develop\n";
gitRevListOut = "5";
gitRevListCountOut = "5";
await gitHelper.add(moduleName);
});
@@ -108,12 +108,126 @@ describe("Updatenotification", () => {
const { error } = require("logger");
expect(error).toHaveBeenCalledWith(`Failed to retrieve repo info for ${moduleName}: Failed to retrieve status`);
});
});
it("excludes repo if refs don't match regex", async () => {
gitFetchErr = "";
describe("MagicMirror on master (empty taglist)", () => {
const moduleName = "MagicMirror";
beforeEach(async () => {
gitRemoteOut = "origin\tgit@github.com:MichMich/MagicMirror.git (fetch)\norigin\tgit@github.com:MichMich/MagicMirror.git (push)\n";
gitRevParseOut = "332e429a41f1a2339afd4f0ae96dd125da6beada";
gitStatusOut = "## master...origin/master\n M tests/unit/functions/updatenotification_spec.js\n";
gitFetchErr = "From github.com:MichMich/MagicMirror\n60e0377..332e429 master -> origin/master\n";
gitRevListCountOut = "5";
await gitHelper.add(moduleName);
});
it("returns status information", async () => {
const repos = await gitHelper.getRepos();
expect(repos[0]).toMatchSnapshot();
expect(execMock).toHaveBeenCalledTimes(7);
});
it("returns status information early if isBehindInStatus", async () => {
gitStatusOut = "## master...origin/master [behind 5]";
const repos = await gitHelper.getRepos();
expect(repos[0]).toMatchSnapshot();
expect(execMock).toHaveBeenCalledTimes(7);
});
it("excludes repo if status can't be retrieved", async () => {
const errorMessage = "Failed to retrieve status";
execMock.mockRejectedValueOnce(errorMessage);
const repos = await gitHelper.getRepos();
expect(repos.length).toBe(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 () => {
gitRemoteOut = "origin\tgit@github.com:MichMich/MagicMirror.git (fetch)\norigin\tgit@github.com:MichMich/MagicMirror.git (push)\n";
gitRevParseOut = "332e429a41f1a2339afd4f0ae96dd125da6beada";
gitStatusOut = "## master...origin/master\n M tests/unit/functions/updatenotification_spec.js\n";
gitFetchErr = "From github.com:MichMich/MagicMirror\n60e0377..332e429 master -> origin/master\n";
gitRevListCountOut = "5";
gitTagListOut = "332e429a41f1a2339afd4f0ae96dd125da6beada...tag...\n";
gitRevListOut = "332e429a41f1a2339afd4f0ae96dd125da6beada\n";
await gitHelper.add(moduleName);
});
it("returns status information", async () => {
const repos = await gitHelper.getRepos();
expect(repos[0]).toMatchSnapshot();
expect(execMock).toHaveBeenCalledTimes(7);
});
it("returns status information early if isBehindInStatus", async () => {
gitStatusOut = "## master...origin/master [behind 5]";
const repos = await gitHelper.getRepos();
expect(repos[0]).toMatchSnapshot();
expect(execMock).toHaveBeenCalledTimes(7);
});
it("excludes repo if status can't be retrieved", async () => {
const errorMessage = "Failed to retrieve status";
execMock.mockRejectedValueOnce(errorMessage);
const repos = await gitHelper.getRepos();
expect(repos.length).toBe(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 () => {
gitRemoteOut = "origin\tgit@github.com:MichMich/MagicMirror.git (fetch)\norigin\tgit@github.com:MichMich/MagicMirror.git (push)\n";
gitRevParseOut = "332e429a41f1a2339afd4f0ae96dd125da6beada";
gitStatusOut = "## master...origin/master\n M tests/unit/functions/updatenotification_spec.js\n";
gitFetchErr = "From github.com:MichMich/MagicMirror\n60e0377..332e429 master -> origin/master\n";
gitRevListCountOut = "5";
gitTagListOut = "xxxe429a41f1a2339afd4f0ae96dd125da6beada...tag...\n";
gitRevListOut = "332e429a41f1a2339afd4f0ae96dd125da6beada\n";
await gitHelper.add(moduleName);
});
it("returns status information", async () => {
const repos = await gitHelper.getRepos();
expect(repos[0]).toMatchSnapshot();
expect(execMock).toHaveBeenCalledTimes(7);
});
it("returns status information early if isBehindInStatus", async () => {
gitStatusOut = "## master...origin/master [behind 5]";
const repos = await gitHelper.getRepos();
expect(repos[0]).toMatchSnapshot();
expect(execMock).toHaveBeenCalledTimes(7);
});
it("excludes repo if status can't be retrieved", async () => {
const errorMessage = "Failed to retrieve status";
execMock.mockRejectedValueOnce(errorMessage);
const repos = await gitHelper.getRepos();
expect(repos.length).toBe(0);
const { error } = require("logger");
expect(error).toHaveBeenCalledWith(`Failed to retrieve repo info for ${moduleName}: Failed to retrieve status`);
});
});
@@ -125,7 +239,7 @@ describe("Updatenotification", () => {
gitRevParseOut = "9d8310163da94441073a93cead711ba43e8888d0";
gitStatusOut = "## master...origin/master";
gitFetchErr = `From https://github.com/fewieden/${moduleName}\n19f7faf..9d83101 master -> origin/master`;
gitRevListOut = "7";
gitRevListCountOut = "7";
await gitHelper.add(moduleName);
});

View File

@@ -1,5 +1,5 @@
const WeatherObject = require("../../../modules/default/weather/weatherobject.js");
const WeatherUtils = require("../../../modules/default/weather/weatherutils.js");
const WeatherObject = require("../../../modules/default/weather/weatherobject");
const WeatherUtils = require("../../../modules/default/weather/weatherutils");
global.moment = require("moment-timezone");
global.SunCalc = require("suncalc");
@@ -15,23 +15,53 @@ describe("WeatherObject", () => {
});
it("should return true for daytime at noon", () => {
weatherobject.date = moment(12, "HH");
weatherobject.date = moment("12:00", "HH:mm");
weatherobject.updateSunTime(-6.774877582342688, 37.63345667023327);
expect(weatherobject.isDayTime()).toBe(true);
});
it("should return false for daytime at midnight", () => {
weatherobject.date = moment(0, "HH");
weatherobject.date = moment("00:00", "HH:mm");
weatherobject.updateSunTime(-6.774877582342688, 37.63345667023327);
expect(weatherobject.isDayTime()).toBe(false);
});
it("should return sunrise as the next sunaction", () => {
weatherobject.updateSunTime(-6.774877582342688, 37.63345667023327);
let midnight = moment("00:00", "HH:mm");
expect(weatherobject.nextSunAction(midnight)).toBe("sunrise");
});
it("should return sunset as the next sunaction", () => {
weatherobject.updateSunTime(-6.774877582342688, 37.63345667023327);
let noon = moment(weatherobject.sunrise).hour(14);
expect(weatherobject.nextSunAction(noon)).toBe("sunset");
});
it("should return an already defined feelsLike info", () => {
weatherobject.feelsLikeTemp = "feelsLikeTempValue";
expect(weatherobject.feelsLike()).toBe("feelsLikeTempValue");
});
afterAll(() => {
moment.tz.setDefault(originalTimeZone);
});
});
describe("WeatherObject", () => {
describe("WeatherUtils", () => {
it("should convert windspeed correctly from mps to beaufort", () => {
expect(Math.round(WeatherUtils.convertWind(5, "beaufort"))).toBe(3);
expect(Math.round(WeatherUtils.convertWind(300, "beaufort"))).toBe(12);
});
it("should convert windspeed correctly from mps to kmh", () => {
expect(Math.round(WeatherUtils.convertWind(11.75, "kmh"))).toBe(42);
});
it("should convert windspeed correctly from mps to knots", () => {
expect(Math.round(WeatherUtils.convertWind(10, "knots"))).toBe(19);
});
it("should convert windspeed correctly from mph to mps", () => {
expect(Math.round(WeatherUtils.convertWindToMetric(93.951324266285))).toBe(42);
});
@@ -43,4 +73,12 @@ describe("WeatherObject", () => {
it("should convert wind direction correctly from cardinal to value", () => {
expect(WeatherUtils.convertWindDirection("SSE")).toBe(157);
});
it("should return a calculated feelsLike info", () => {
expect(WeatherUtils.calculateFeelsLike(0, 20, 40)).toBe(-9.444444444444445);
});
it("should return a calculated feelsLike info", () => {
expect(WeatherUtils.calculateFeelsLike(30, 0, 60)).toBe(32.8320322777777);
});
});

View File

@@ -1,4 +1,5 @@
const { performWebRequest } = require("../../../../modules/default/utils.js");
const { performWebRequest } = require("../../../../modules/default/utils");
const nodeVersion = process.version.match(/^v(\d+)\.*/)[1];
describe("Utils tests", () => {

View File

@@ -0,0 +1,45 @@
const weather = require("../../../../../modules/default/weather/weatherutils");
describe("Weather utils tests", () => {
describe("convertPrecipitationUnit tests", () => {
it("Should keep value and unit if outputUnit is undefined", () => {
const values = [1, 2];
const units = ["mm", "cm"];
for (let i = 0; i < values.length; i++) {
var result = weather.convertPrecipitationUnit(values[i], units[i], undefined);
expect(result).toBe(`${values[i].toFixed(2)} ${units[i]}`);
}
});
it("Should keep value and unit if outputUnit is metric", () => {
const values = [1, 2];
const units = ["mm", "cm"];
for (let i = 0; i < values.length; i++) {
var result = weather.convertPrecipitationUnit(values[i], units[i], "metric");
expect(result).toBe(`${values[i].toFixed(2)} ${units[i]}`);
}
});
it("Should use mm unit if input unit is undefined", () => {
const values = [1, 2];
for (let i = 0; i < values.length; i++) {
var result = weather.convertPrecipitationUnit(values[i], undefined, "metric");
expect(result).toBe(`${values[i].toFixed(2)} mm`);
}
});
it("Should convert value and unit if outputUnit is imperial", () => {
const values = [1, 2];
const units = ["mm", "cm"];
const expectedValues = [0.04, 0.79];
for (let i = 0; i < values.length; i++) {
var result = weather.convertPrecipitationUnit(values[i], units[i], "imperial");
expect(result).toBe(`${expectedValues[i]} in`);
}
});
});
});

View File

@@ -2,12 +2,16 @@ const TestSequencer = require("@jest/test-sequencer").default;
class CustomSequencer extends TestSequencer {
sort(tests) {
const orderPath = ["unit", "e2e", "electron"];
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]);

View File

@@ -0,0 +1,43 @@
const fs = require("fs");
const path = require("path");
const _ = require("lodash");
/**
* @param {string} type what data to read, can be "current" "forecast" or "hourly
* @param {object} extendedData extra data to add to the default mock data
* @returns {string} mocked current weather data
*/
const readMockData = (type, extendedData = {}) => {
let fileName;
switch (type) {
case "forecast":
fileName = "weather_forecast.json";
break;
case "hourly":
fileName = "weather_hourly.json";
break;
case "current":
default:
fileName = "weather_current.json";
break;
}
return JSON.stringify(_.merge({}, JSON.parse(fs.readFileSync(path.resolve(`${__dirname}/../mocks/${fileName}`)).toString()), extendedData));
};
const injectMockData = (configFileName, extendedData = {}) => {
let mockWeather;
if (configFileName.includes("forecast")) {
mockWeather = readMockData("forecast", extendedData);
} else if (configFileName.includes("hourly")) {
mockWeather = readMockData("hourly", extendedData);
} else {
mockWeather = readMockData("current", extendedData);
}
let content = fs.readFileSync(path.resolve(`${__dirname}../../../${configFileName}`)).toString();
content = content.replace("#####WEATHERDATA#####", mockWeather);
fs.writeFileSync(path.resolve(`${__dirname}../../../config/config.js`), content);
};
module.exports = { injectMockData };