Merge pull request #2761 from MichMich/develop

release v2.18.0
This commit is contained in:
Michael Teeuw 2022-01-01 19:31:41 +01:00 committed by GitHub
commit a6cbc9f0ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 4338 additions and 7651 deletions

View File

@ -16,7 +16,7 @@
},
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2017,
"ecmaVersion": 2018,
"ecmaFeatures": {
"globalReturn": true
}

View File

@ -4,13 +4,15 @@ Thanks for contributing to MagicMirror²!
We hold our code to standard, and these standards are documented below.
## Linters
If you wish to run our linters, use `npm run lint` without any arguments.
### JavaScript: Run ESLint
We use [ESLint](https://eslint.org) on our JavaScript files.
Our ESLint configuration is in our .eslintrc.json and .eslintignore files.
Our ESLint configuration is in our `.eslintrc.json` and `.eslintignore` files.
To run ESLint, use `npm run lint:js`.
@ -20,7 +22,15 @@ We use [StyleLint](https://stylelint.io) to lint our CSS. Our configuration is i
To run StyleLint, use `npm run lint:css`.
### Submitting Issues
## Testing
We use [Jest](https://jestjs.io) for JavaScript testing.
To run all tests, use `npm run test`.
The specific test commands are defined in `package.json`. So you can also run the specific tests with other commands, e.g. `npm run test:unit` or `npx jest tests/e2e/env_spec.js`.
## Submitting Issues
Please only submit reproducible issues.
@ -32,7 +42,7 @@ When submitting a new issue, please supply the following information:
**Platform**: Place your platform here... give us your web browser/Electron version _and_ your hardware (Raspberry Pi 2/3/4, Windows, Mac, Linux, System V UNIX).
**Node Version**: Make sure it's version 12 or later (recommended is 14).
**Node Version**: Make sure it's version 14 or later (recommended is 16).
**MagicMirror Version**: Please let us know which version of MagicMirror you are running. It can be found in the `package.json` file.

View File

@ -33,7 +33,7 @@ When submitting a new issue, please supply the following information:
**Platform**: Place your platform here... give us your web browser/Electron version _and_ your hardware (Raspberry Pi 2/3/4, Windows, Mac, Linux, System V UNIX).
**Node Version**: Make sure it's version 12 or later (recommended is 14).
**Node Version**: Make sure it's version 14 or later (recommended is 16).
**MagicMirror Version**: Please let us know which version of MagicMirror you are running. It can be found in the `package.json` file.

View File

@ -15,14 +15,15 @@ jobs:
timeout-minutes: 30
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
node-version: [14.x, 16.x, 17.x]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- name: Install dependencies and run tests
run: |
Xvfb :99 -screen 0 1024x768x16 &

View File

@ -23,7 +23,7 @@ jobs:
touch css/custom.css
npm run test:coverage
- name: Upload coverage results to codecov
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v2
with:
file: ./coverage/lcov.info
files: ./coverage/lcov.info
fail_ci_if_error: true

View File

@ -14,7 +14,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@v2
- name: Enforce changelog
uses: dangoslen/changelog-enforcer@v1.6.1
uses: dangoslen/changelog-enforcer@v2
with:
changeLogPath: "CHANGELOG.md"
skipLabels: "Skip Changelog"

View File

@ -3,13 +3,55 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror²
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror².
## [2.18.0] - 2022-01-01
Special thanks to the following contributors: @AmpioRosso, @eouia, @fewieden, @jupadin, @khassel, @kolbyjack, @KristjanESPERANTO, @MariusVaice, @rejas, @rico24 and @sdetweil.
### Added
- Added test for calendar recurring event with checks the correct date displayed (related to #2752).
### Updated
- ESLint version supports now ECMAScript 2018.
- Cleaned up `updatenotification` module and switched to nunjuck template.
- Moved calendar tests from category `electron` to `e2e`.
- Update missed translations for Korean language (ko.json).
- Update missed translations for Dutch language (nl.json).
- Cleaned up `alert` module and switched to nunjuck template.
- Moved weather tests from category `electron` to `e2e`.
- Updated github actions.
- Replace spectron with playwright, update dependencies including electron update to v16.
- Added lithuanian language to translations.js.
- Show info message if newsfeed is empty (fixes #2731).
- Added dangerouslyDisableAutoEscaping config option for newsfeed templates (fixes #2712).
- Added missing shebang to `installers/mm.sh`.
- Node versions in templates and github workflows.
### Fixed
- Fixed wrong file `kr.json` to `ko.json`. Use language code 'ko' instead of 'kr' for Korean language.
- Fixed `feels_like` data from openweathermaps current weather being ignored (#2678).
- Fixed chaotic newsfeed display after network connection loss thanks to @jalibu (#2638).
- Fixed incorrect time zone correction of recurring full day events (#2632 and #2634).
- Fixed e2e tests by increasing testTimeout.
- Revert node-ical update due to missing luxon package.
- Fixed User-Agent-Header for newsfeed and calendar module (#2729).
- Replace broken shields in Readme and use https for links.
- Fixed electron tests with retry.
- Fixed Calendar recurring cross timezone error (add/subtract a day, not just offset hours) (#2632).
- Fixed Calendar showEnd and Full Date overlay (#2629).
- Fixed regression on #2632, #2752.
- Broadcast custom symbols in CALENDAR_EVENTS.
## [2.17.1] - 2021-10-01
### Fixed
- Fixed error when accessing letsencrypt certificates
- Fixed Calendar module enhancement: displaying full events without time (#2424)
## [2.17.0] - 2021-10-01

View File

@ -1,12 +1,17 @@
![MagicMirror²: The open source modular smart mirror platform. ](.github/header.png)
<p style="text-align: center">
<a href="https://david-dm.org/MichMich/MagicMirror"><img src="https://david-dm.org/MichMich/MagicMirror.svg" alt="Dependency Status"></a>
<a href="https://david-dm.org/MichMich/MagicMirror?type=dev"><img src="https://david-dm.org/MichMich/MagicMirror/dev-status.svg" alt="devDependency Status"></a>
<a href="https://bestpractices.coreinfrastructure.org/projects/347"><img src="https://bestpractices.coreinfrastructure.org/projects/347/badge" alt="CLI Best Practices"></a>
<a href="https://codecov.io/gh/MichMich/MagicMirror"><img src="https://codecov.io/gh/MichMich/MagicMirror/branch/master/graph/badge.svg?token=LEG1KitZR6" alt="CodeCov Status"/></a>
<a href="https://choosealicense.com/licenses/mit"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
<a href="https://github.com/MichMich/MagicMirror/actions?query=workflow%3A%22Automated+Tests%22"><img src="https://github.com/MichMich/MagicMirror/workflows/Automated%20Tests/badge.svg" alt="Tests"></a>
<a href="https://choosealicense.com/licenses/mit">
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License">
</a>
<img src="https://img.shields.io/github/workflow/status/michmich/magicmirror/Run%20Automated%20Tests" alt="GitHub Actions">
<img src="https://img.shields.io/github/checks-status/michmich/magicmirror/master" alt="Build Status">
<a href="https://codecov.io/gh/MichMich/MagicMirror">
<img src="https://codecov.io/gh/MichMich/MagicMirror/branch/master/graph/badge.svg?token=LEG1KitZR6" alt="CodeCov Status"/>
</a>
<a href="https://github.com/MichMich/MagicMirror">
<img src="https://img.shields.io/github/stars/michmich/magicmirror?style=social">
</a>
</p>
**MagicMirror²** is an open source modular smart mirror platform. With a growing list of installable modules, the **MagicMirror²** allows you to convert your hallway or bathroom mirror into your personal assistant. **MagicMirror²** is built by the creator of [the original MagicMirror](https://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MichMich/MagicMirror/graphs/contributors).

View File

@ -1,3 +1,4 @@
#!/bin/bash
# This file is still here to keep PM2 working on older installations.
cd ~/MagicMirror
DISPLAY=:0 npm start

View File

@ -53,7 +53,7 @@ function createWindow() {
// If config.address is not defined or is an empty string (listening on all interfaces), connect to localhost
let prefix;
if (config["tls"] !== null && config["tls"]) {
if ((config["tls"] !== null && config["tls"]) || config.useHttps) {
prefix = "https://";
} else {
prefix = "http://";
@ -140,6 +140,13 @@ app.on("before-quit", (event) => {
process.exit(0);
});
/* handle errors from self signed certificates */
app.on("certificate-error", (event, webContents, url, error, certificate, callback) => {
event.preventDefault();
callback(true);
});
// Start the core application if server is run on localhost
// This starts all node helpers and starts the webserver.
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].includes(config.address)) {

View File

@ -7,100 +7,97 @@
* MIT Licensed.
*/
Module.register("alert", {
alerts: {},
defaults: {
// scale|slide|genie|jelly|flip|bouncyflip|exploader
effect: "slide",
// scale|slide|genie|jelly|flip|bouncyflip|exploader
alert_effect: "jelly",
//time a notification is displayed in seconds
display_time: 3500,
//Position
effect: "slide", // scale|slide|genie|jelly|flip|bouncyflip|exploader
alert_effect: "jelly", // scale|slide|genie|jelly|flip|bouncyflip|exploader
display_time: 3500, // time a notification is displayed in seconds
position: "center",
//shown at startup
welcome_message: false
welcome_message: false // shown at startup
},
getScripts: function () {
getScripts() {
return ["notificationFx.js"];
},
getStyles: function () {
return ["notificationFx.css", "font-awesome.css"];
getStyles() {
return ["font-awesome.css", this.file(`./styles/notificationFx.css`), this.file(`./styles/${this.config.position}.css`)];
},
// Define required translations.
getTranslations: function () {
getTranslations() {
return {
en: "translations/en.json",
bg: "translations/bg.json",
da: "translations/da.json",
de: "translations/de.json",
nl: "translations/nl.json"
en: "translations/en.json",
es: "translations/es.json",
fr: "translations/fr.json",
hu: "translations/hu.json",
nl: "translations/nl.json",
ru: "translations/ru.json"
};
},
show_notification: function (message) {
getTemplate(type) {
return `templates/${type}.njk`;
},
start() {
Log.info(`Starting module: ${this.name}`);
if (this.config.effect === "slide") {
this.config.effect = this.config.effect + "-" + this.config.position;
this.config.effect = `${this.config.effect}-${this.config.position}`;
}
let msg = "";
if (message.title) {
msg += "<span class='thin dimmed medium'>" + message.title + "</span>";
if (this.config.welcome_message) {
const message = this.config.welcome_message === true ? this.translate("welcome") : this.config.welcome_message;
this.showNotification({ title: this.translate("sysTitle"), message });
}
if (message.message) {
if (msg !== "") {
msg += "<br />";
},
notificationReceived(notification, payload, sender) {
if (notification === "SHOW_ALERT") {
if (payload.type === "notification") {
this.showNotification(payload);
} else {
this.showAlert(payload, sender);
}
msg += "<span class='light bright small'>" + message.message + "</span>";
} else if (notification === "HIDE_ALERT") {
this.hideAlert(sender);
}
},
async showNotification(notification) {
const message = await this.renderMessage("notification", notification);
new NotificationFx({
message: msg,
message,
layout: "growl",
effect: this.config.effect,
ttl: message.timer !== undefined ? message.timer : this.config.display_time
ttl: notification.timer || this.config.display_time
}).show();
},
show_alert: function (params, sender) {
let image = "";
//Set standard params if not provided by module
if (typeof params.timer === "undefined") {
params.timer = null;
}
if (typeof params.imageHeight === "undefined") {
params.imageHeight = "80px";
}
if (typeof params.imageUrl === "undefined" && typeof params.imageFA === "undefined") {
params.imageUrl = null;
} else if (typeof params.imageFA === "undefined") {
image = "<img src='" + params.imageUrl.toString() + "' height='" + params.imageHeight.toString() + "' style='margin-bottom: 10px;'/><br />";
} else if (typeof params.imageUrl === "undefined") {
image = "<span class='bright " + "fa fa-" + params.imageFA + "' style='margin-bottom: 10px;font-size:" + params.imageHeight.toString() + ";'/></span><br />";
}
//Create overlay
const overlay = document.createElement("div");
overlay.id = "overlay";
overlay.innerHTML += '<div class="black_overlay"></div>';
document.body.insertBefore(overlay, document.body.firstChild);
async showAlert(alert, sender) {
// If module already has an open alert close it
if (this.alerts[sender.name]) {
this.hide_alert(sender, false);
this.hideAlert(sender, false);
}
//Display title and message only if they are provided in notification parameters
let message = "";
if (params.title) {
message += "<span class='light dimmed medium'>" + params.title + "</span>";
}
if (params.message) {
if (message !== "") {
message += "<br />";
// Add overlay
if (!Object.keys(this.alerts).length) {
this.toggleBlur(true);
}
message += "<span class='thin bright small'>" + params.message + "</span>";
}
const message = await this.renderMessage("alert", alert);
// Store alert in this.alerts
this.alerts[sender.name] = new NotificationFx({
message: image + message,
message,
effect: this.config.alert_effect,
ttl: params.timer,
onClose: () => this.hide_alert(sender),
ttl: alert.timer,
onClose: () => this.hideAlert(sender),
al_no: "ns-alert"
});
@ -108,60 +105,42 @@ Module.register("alert", {
this.alerts[sender.name].show();
// Add timer to dismiss alert and overlay
if (params.timer) {
if (alert.timer) {
setTimeout(() => {
this.hide_alert(sender);
}, params.timer);
this.hideAlert(sender);
}, alert.timer);
}
},
hide_alert: function (sender, close = true) {
hideAlert(sender, close = true) {
// Dismiss alert and remove from this.alerts
if (this.alerts[sender.name]) {
this.alerts[sender.name].dismiss(close);
this.alerts[sender.name] = null;
delete this.alerts[sender.name];
// Remove overlay
const overlay = document.getElementById("overlay");
overlay.parentNode.removeChild(overlay);
if (!Object.keys(this.alerts).length) {
this.toggleBlur(false);
}
}
},
setPosition: function (pos) {
//Add css to body depending on the set position for notifications
const sheet = document.createElement("style");
if (pos === "center") {
sheet.innerHTML = ".ns-box {margin-left: auto; margin-right: auto;text-align: center;}";
renderMessage(type, data) {
return new Promise((resolve) => {
this.nunjucksEnvironment().render(this.getTemplate(type), data, function (err, res) {
if (err) {
Log.error("Failed to render alert", err);
}
if (pos === "right") {
sheet.innerHTML = ".ns-box {margin-left: auto;text-align: right;}";
}
if (pos === "left") {
sheet.innerHTML = ".ns-box {margin-right: auto;text-align: left;}";
}
document.body.appendChild(sheet);
resolve(res);
});
});
},
notificationReceived: function (notification, payload, sender) {
if (notification === "SHOW_ALERT") {
if (typeof payload.type === "undefined") {
payload.type = "alert";
toggleBlur(add = false) {
const method = add ? "add" : "remove";
const modules = document.querySelectorAll(".module");
for (const module of modules) {
module.classList[method]("alert-blur");
}
if (payload.type === "alert") {
this.show_alert(payload, sender);
} else if (payload.type === "notification") {
this.show_notification(payload);
}
} else if (notification === "HIDE_ALERT") {
this.hide_alert(sender);
}
},
start: function () {
this.alerts = {};
this.setPosition(this.config.position);
if (this.config.welcome_message) {
if (this.config.welcome_message === true) {
this.show_notification({ title: this.translate("sysTitle"), message: this.translate("welcome") });
} else {
this.show_notification({ title: this.translate("sysTitle"), message: this.config.welcome_message });
}
}
Log.info("Starting module: " + this.name);
}
});

View File

@ -0,0 +1,5 @@
.ns-box {
margin-left: auto;
margin-right: auto;
text-align: center;
}

View File

@ -0,0 +1,4 @@
.ns-box {
margin-right: auto;
text-align: left;
}

View File

@ -39,12 +39,8 @@
border-radius: 20px;
}
.black_overlay {
position: fixed;
z-index: 2;
background-color: rgba(0, 0, 0, 0.93);
width: 100%;
height: 100%;
.alert-blur {
filter: blur(2px) brightness(50%);
}
[class^="ns-effect-"].ns-growl.ns-hide,

View File

@ -0,0 +1,4 @@
.ns-box {
margin-left: auto;
text-align: right;
}

View File

@ -0,0 +1,18 @@
{% if imageUrl or imageFA %}
{% set imageHeight = imageHeight if imageHeight else "80px" %}
{% if imageUrl %}
<img src="{{ imageUrl }}" height="{{ imageHeight }}" style="margin-bottom: 10px;"/>
{% else %}
<span class="bright fa fa-{{ imageFA }}" style='margin-bottom: 10px; font-size: {{ imageHeight }};'/></span>
{% endif %}
<br/>
{% endif %}
{% if title %}
<span class="thin dimmed medium">{{ title }}</span>
{% endif %}
{% if message %}
{% if title %}
<br/>
{% endif %}
<span class="light bright small">{{ message }}</span>
{% endif %}

View File

@ -0,0 +1,9 @@
{% if title %}
<span class="thin dimmed medium">{{ title }}</span>
{% endif %}
{% if message %}
{% if title %}
<br/>
{% endif %}
<span class="light bright small">{{ message }}</span>
{% endif %}

View File

@ -237,18 +237,6 @@ Module.register("calendar", {
symbolWrapper.className = "symbol align-right " + symbolClass;
const symbols = this.symbolsForEvent(event);
// If symbols are displayed and custom symbol is set, replace event symbol
if (this.config.displaySymbol && this.config.customEvents.length > 0) {
for (let ev in this.config.customEvents) {
if (typeof this.config.customEvents[ev].symbol !== "undefined" && this.config.customEvents[ev].symbol !== "") {
let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
if (needle.test(event.title)) {
symbols[0] = this.config.customEvents[ev].symbol;
break;
}
}
}
}
symbols.forEach((s, index) => {
const symbol = document.createElement("span");
symbol.className = "fa fa-fw fa-" + s;
@ -368,9 +356,9 @@ Module.register("calendar", {
}
} else {
// Show relative times
if (event.startDate >= now) {
if (event.startDate >= now || (event.fullDayEvent && event.today)) {
// Use relative time
if (!this.config.hideTime) {
if (!this.config.hideTime && !event.fullDayEvent) {
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }));
} else {
timeWrapper.innerHTML = this.capFirst(
@ -378,11 +366,22 @@ Module.register("calendar", {
sameDay: "[" + this.translate("TODAY") + "]",
nextDay: "[" + this.translate("TOMORROW") + "]",
nextWeek: "dddd",
sameElse: this.config.dateFormat
sameElse: event.fullDayEvent ? this.config.fullDayEventDateFormat : this.config.dateFormat
})
);
}
if (event.startDate - now < this.config.getRelative * oneHour) {
if (event.fullDayEvent) {
// Full days events within the next two days
if (event.today) {
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
} else if (event.startDate - now < oneDay && event.startDate - now > 0) {
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
} else if (event.startDate - now < 2 * oneDay && event.startDate - now > 0) {
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
}
}
} else if (event.startDate - now < this.config.getRelative * oneHour) {
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
}
@ -629,6 +628,17 @@ Module.register("calendar", {
symbols = this.mergeUnique(this.getCalendarPropertyAsArray(event.url, "fullDaySymbol", this.config.defaultSymbol), symbols);
}
// If custom symbol is set, replace event symbol
for (let ev of this.config.customEvents) {
if (typeof ev.symbol !== "undefined" && ev.symbol !== "") {
let needle = new RegExp(ev.keyword, "gi");
if (needle.test(event.title)) {
symbols[0] = ev.symbol;
break;
}
}
}
return symbols;
},

View File

@ -41,7 +41,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
let fetcher = null;
let httpsAgent = null;
let headers = {
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)"
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version
};
if (selfSignedCert) {

View File

@ -98,7 +98,7 @@ const CalendarUtils = {
if (h > 0 && h < Math.abs(current_offset) / 60) {
// if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time)
// we need to fix that
adjustHours = 24;
//adjustHours = 24;
// Log.debug("adjusting date")
}
//-300 > -240
@ -160,7 +160,7 @@ const CalendarUtils = {
}
if (event.type === "VEVENT") {
Log.debug("\nEvent: " + JSON.stringify(event));
Log.debug("Event:\n" + JSON.stringify(event));
let startDate = eventDate(event, "start");
let endDate;
@ -177,8 +177,8 @@ const CalendarUtils = {
}
}
Log.debug("startDate (local): " + startDate.toDate());
Log.debug("endDate (local): " + endDate.toDate());
Log.debug("start: " + startDate.toDate());
Log.debug("end:: " + endDate.toDate());
// Calculate the duration of the event for use with recurring events.
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
@ -332,40 +332,38 @@ const CalendarUtils = {
Log.debug("Fullday");
// If the offset is negative (east of GMT), where the problem is
if (dateoffset < 0) {
// Remove the offset, independently of the comparison between the date hour and the offset,
// since in the case that *date houre < offset*, the *new Date* command will handle this by
// representing the day before.
// Reduce the time by the offset:
if (dh < Math.abs(dateoffset / 60)) {
// reduce the time by the offset
// Apply the correction to the date/time to get it UTC relative
date = new Date(date.getTime() - Math.abs(nowOffset) * 60000);
date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
// the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry
//duration = 24 * 60 * 60 * 1000;
Log.debug("new recurring date1 is " + date);
Log.debug("new recurring date1 fulldate is " + date);
}
} else {
// if the timezones are the same, correct date if needed
if (event.start.tz === moment.tz.guess()) {
//if (event.start.tz === moment.tz.guess()) {
// if the date hour is less than the offset
if (24 - dh < Math.abs(dateoffset / 60)) {
if (24 - dh <= Math.abs(dateoffset / 60)) {
// apply the correction to the date/time back to right day
date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
// the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry
//duration = 24 * 60 * 60 * 1000;
Log.debug("new recurring date2 is " + date);
}
Log.debug("new recurring date2 fulldate is " + date);
}
//}
}
} else {
// not full day, but luxon can still screw up the date on the rule processing
// we need to correct the date to get back to the right event for
if (dateoffset < 0) {
// if the date hour is less than the offset
if (dh < Math.abs(dateoffset / 60)) {
if (dh <= Math.abs(dateoffset / 60)) {
// Reduce the time by the offset:
// Apply the correction to the date/time to get it UTC relative
date = new Date(date.getTime() - Math.abs(nowOffset) * 60000);
date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
// the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry
//duration = 24 * 60 * 60 * 1000;
@ -373,9 +371,9 @@ const CalendarUtils = {
}
} else {
// if the timezones are the same, correct date if needed
if (event.start.tz === moment.tz.guess()) {
//if (event.start.tz === moment.tz.guess()) {
// if the date hour is less than the offset
if (24 - dh < Math.abs(dateoffset / 60)) {
if (24 - dh <= Math.abs(dateoffset / 60)) {
// apply the correction to the date/time back to right day
date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
// the duration was calculated way back at the top before we could correct the start time..
@ -383,11 +381,11 @@ const CalendarUtils = {
//duration = 24 * 60 * 60 * 1000;
Log.debug("new recurring date2 is " + date);
}
}
//}
}
}
startDate = moment(date);
Log.debug("Corrected startDate (local): " + startDate.toDate());
Log.debug("Corrected startDate: " + startDate.toDate());
let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, date);

View File

@ -37,7 +37,8 @@ Module.register("newsfeed", {
endTags: [],
prohibitedWords: [],
scrollLength: 500,
logFeedWarnings: false
logFeedWarnings: false,
dangerouslyDisableAutoEscaping: false
},
// Define required scripts.
@ -121,7 +122,7 @@ Module.register("newsfeed", {
}
if (this.newsItems.length === 0) {
return {
loaded: false
empty: true
};
}
if (this.activeItem >= this.newsItems.length) {
@ -184,6 +185,7 @@ Module.register("newsfeed", {
const dateB = new Date(b.pubdate);
return dateB - dateA;
});
if (this.config.maxNewsItems > 0) {
newsItems = newsItems.slice(0, this.config.maxNewsItems);
}
@ -219,7 +221,6 @@ Module.register("newsfeed", {
}
//Remove selected tags from the end of rss feed items (title or description)
if (this.config.removeEndTags) {
for (let endTag of this.config.endTags) {
if (item.title.slice(-endTag.length) === endTag) {
@ -295,6 +296,9 @@ Module.register("newsfeed", {
this.sendNotification("NEWS_FEED", { items: this.newsItems });
}
// #2638 Clear timer if it already exists
if (this.timer) clearInterval(this.timer);
this.timer = setInterval(() => {
this.activeItem++;
this.updateDom(this.config.animationSpeed);

View File

@ -1,3 +1,11 @@
{% macro escapeText(text, dangerouslyDisableAutoEscaping=false) %}
{% if dangerouslyDisableAutoEscaping %}
{{ text | safe}}
{% else %}
{{ text }}
{% endif %}
{% endmacro %}
{% if loaded %}
{% if config.showAsList %}
<ul class="newsfeed-list">
@ -14,14 +22,14 @@
</div>
{% endif %}
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
{{ item.title }}
{{ escapeText(item.title, config.dangerouslyDisableAutoEscaping) }}
</div>
{% if config.showDescription %}
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
{% if config.truncDescription %}
{{ item.description | truncate(config.lengthDescription) }}
{{ escapeText(item.description | truncate(config.lengthDescription), config.dangerouslyDisableAutoEscaping) }}
{% else %}
{{ item.description }}
{{ escapeText(item.description, config.dangerouslyDisableAutoEscaping) }}
{% endif %}
</div>
{% endif %}
@ -33,7 +41,7 @@
{% if (config.showSourceTitle and sourceTitle) or config.showPublishDate %}
<div class="newsfeed-source light small dimmed">
{% if sourceTitle and config.showSourceTitle %}
{{ sourceTitle }}{% if config.showPublishDate %}, {% else %}: {% endif %}
{{ escapeText(sourceTitle, config.dangerouslyDisableAutoEscaping) }}{% if config.showPublishDate %}, {% else %}: {% endif %}
{% endif %}
{% if config.showPublishDate %}
{{ publishDate }}:
@ -41,19 +49,23 @@
</div>
{% endif %}
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
{{ title }}
{{ escapeText(title, config.dangerouslyDisableAutoEscaping) }}
</div>
{% if config.showDescription %}
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
{% if config.truncDescription %}
{{ description | truncate(config.lengthDescription) }}
{{ escapeText(description | truncate(config.lengthDescription), config.dangerouslyDisableAutoEscaping) }}
{% else %}
{{ description }}
{{ escapeText(description, config.dangerouslyDisableAutoEscaping) }}
{% endif %}
</div>
{% endif %}
</div>
{% endif %}
{% elseif empty %}
<div class="small dimmed">
{{ "NEWSFEED_NO_ITEMS" | translate | safe }}
</div>
{% elseif error %}
<div class="small dimmed">
{{ "MODULE_CONFIG_ERROR" | translate({MODULE_NAME: "Newsfeed", ERROR: error}) | safe }}

View File

@ -79,7 +79,7 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
const headers = {
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)",
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version,
"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
Pragma: "no-cache"
};

View File

@ -4,48 +4,53 @@ const fs = require("fs");
const path = require("path");
const Log = require("logger");
class gitHelper {
const BASE_DIR = path.normalize(`${__dirname}/../../../`);
class GitHelper {
constructor() {
this.gitRepos = [];
this.baseDir = path.normalize(__dirname + "/../../../");
}
getRefRegex(branch) {
return new RegExp("s*([a-z,0-9]+[.][.][a-z,0-9]+) " + branch, "g");
return new RegExp(`s*([a-z,0-9]+[.][.][a-z,0-9]+) ${branch}`, "g");
}
async execShell(command) {
let res = { stdout: "", stderr: "" };
const { stdout, stderr } = await exec(command);
const { stdout = "", stderr = "" } = await exec(command);
res.stdout = stdout;
res.stderr = stderr;
return res;
return { stdout, stderr };
}
async isGitRepo(moduleFolder) {
let res = await this.execShell("cd " + moduleFolder + " && git remote -v");
if (res.stderr) {
Log.error("Failed to fetch git data for " + moduleFolder + ": " + res.stderr);
const { stderr } = await this.execShell(`cd ${moduleFolder} && git remote -v`);
if (stderr) {
Log.error(`Failed to fetch git data for ${moduleFolder}: ${stderr}`);
return false;
} else {
return true;
}
}
add(moduleName) {
let moduleFolder = this.baseDir;
if (moduleName !== "default") {
moduleFolder = moduleFolder + "modules/" + moduleName;
return true;
}
async add(moduleName) {
let moduleFolder = BASE_DIR;
if (moduleName !== "default") {
moduleFolder = `${moduleFolder}modules/${moduleName}`;
}
try {
Log.info("Checking git for module: " + moduleName);
Log.info(`Checking git for module: ${moduleName}`);
// Throws error if file doesn't exist
fs.statSync(path.join(moduleFolder, ".git"));
// Fetch the git or throw error if no remotes
if (this.isGitRepo(moduleFolder)) {
const isGitRepo = await this.isGitRepo(moduleFolder);
if (isGitRepo) {
// Folder has .git and has at least one git remote, watch this folder
this.gitRepos.unshift({ module: moduleName, folder: moduleFolder });
this.gitRepos.push({ module: moduleName, folder: moduleFolder });
}
} catch (err) {
// Error when directory .git doesn't exist or doesn't have any remotes
@ -56,38 +61,34 @@ class gitHelper {
async getStatusInfo(repo) {
let gitInfo = {
module: repo.module,
// commits behind:
behind: 0,
// branch name:
current: "",
// current hash:
hash: "",
// remote branch:
tracking: "",
behind: 0, // commits behind
current: "", // branch name
hash: "", // current hash
tracking: "", // remote branch
isBehindInStatus: false
};
let res;
if (repo.module === "default") {
// the hash is only needed for the mm repo
res = await this.execShell("cd " + repo.folder + " && git rev-parse HEAD");
if (res.stderr) {
Log.error("Failed to get current commit hash for " + repo.module + ": " + res.stderr);
const { stderr, stdout } = await this.execShell(`cd ${repo.folder} && git rev-parse HEAD`);
if (stderr) {
Log.error(`Failed to get current commit hash for ${repo.module}: ${stderr}`);
}
gitInfo.hash = res.stdout;
gitInfo.hash = stdout;
}
if (repo.res) {
// mocking
res = repo.res;
} else {
res = await this.execShell("cd " + repo.folder + " && git status -sb");
}
if (res.stderr) {
Log.error("Failed to get git status for " + repo.module + ": " + res.stderr);
const { stderr, stdout } = await this.execShell(`cd ${repo.folder} && git status -sb`);
if (stderr) {
Log.error(`Failed to get git status for ${repo.module}: ${stderr}`);
// exit without git status info
return;
}
// only the first line of stdout is evaluated
let status = res.stdout.split("\n")[0];
let status = stdout.split("\n")[0];
// examples for status:
// ## develop...origin/develop
// ## master...origin/master [behind 8]
@ -101,72 +102,63 @@ class gitHelper {
// [ 'origin/develop' ]
// [ 'origin/master', '[behind', '8]' ]
gitInfo.tracking = status[0].trim();
if (status[2]) {
// git fetch was already called before so `git status -sb` delivers already the behind number
gitInfo.behind = parseInt(status[2].substring(0, status[2].length - 1));
gitInfo.isBehindInStatus = true;
}
return gitInfo;
}
async getRepoInfo(repo) {
let gitInfo;
if (repo.gitInfo) {
// mocking
gitInfo = repo.gitInfo;
} else {
gitInfo = await this.getStatusInfo(repo);
}
const gitInfo = await this.getStatusInfo(repo);
if (!gitInfo) {
return;
}
if (gitInfo.isBehindInStatus) {
return gitInfo;
}
let res;
if (repo.res) {
// mocking
res = repo.res;
} else {
res = await this.execShell("cd " + repo.folder + " && git fetch --dry-run");
}
const { stderr } = await this.execShell(`cd ${repo.folder} && git fetch --dry-run`);
// example output:
// From https://github.com/MichMich/MagicMirror
// e40ddd4..06389e3 develop -> origin/develop
// here the result is in stderr (this is a git default, don't ask why ...)
const matches = res.stderr.match(this.getRefRegex(gitInfo.current));
const matches = stderr.match(this.getRefRegex(gitInfo.current));
if (!matches || !matches[0]) {
// no refs found, nothing to do
return;
}
// get behind with refs
try {
res = await this.execShell("cd " + repo.folder + " && git rev-list --ancestry-path --count " + matches[0]);
gitInfo.behind = parseInt(res.stdout);
const { stdout } = await this.execShell(`cd ${repo.folder} && git rev-list --ancestry-path --count ${matches[0]}`);
gitInfo.behind = parseInt(stdout);
return gitInfo;
} catch (err) {
Log.error("Failed to get git revisions for " + repo.module + ": " + err);
Log.error(`Failed to get git revisions for ${repo.module}: ${err}`);
}
}
async getStatus() {
const gitResultList = [];
for (let repo of this.gitRepos) {
const gitInfo = await this.getStatusInfo(repo);
if (gitInfo) {
gitResultList.unshift(gitInfo);
}
}
return gitResultList;
}
async getRepos() {
const gitResultList = [];
for (let repo of this.gitRepos) {
for (const repo of this.gitRepos) {
try {
const gitInfo = await this.getRepoInfo(repo);
if (gitInfo) {
gitResultList.unshift(gitInfo);
gitResultList.push(gitInfo);
}
} catch (e) {
Log.error(`Failed to retrieve repo info for ${repo.module}: ${e}`);
}
}
@ -174,4 +166,4 @@ class gitHelper {
}
}
module.exports.gitHelper = gitHelper;
module.exports = GitHelper;

View File

@ -1,70 +1,66 @@
const GitHelper = require(__dirname + "/git_helper.js");
const defaultModules = require(__dirname + "/../defaultmodules.js");
const GitHelper = require("./git_helper");
const defaultModules = require("../defaultmodules");
const NodeHelper = require("node_helper");
const ONE_MINUTE = 60 * 1000;
module.exports = NodeHelper.create({
config: {},
updateTimer: null,
updateProcessStarted: false,
gitHelper: new GitHelper.gitHelper(),
gitHelper: new GitHelper(),
start: function () {},
configureModules: async function (modules) {
// Push MagicMirror itself , biggest chance it'll show up last in UI and isn't overwritten
// others will be added in front
// this method returns promises so we can't wait for every one to resolve before continuing
this.gitHelper.add("default");
for (let moduleName in modules) {
async configureModules(modules) {
for (const moduleName of modules) {
if (!this.ignoreUpdateChecking(moduleName)) {
this.gitHelper.add(moduleName);
await this.gitHelper.add(moduleName);
}
}
await this.gitHelper.add("default");
},
socketNotificationReceived: function (notification, payload) {
async socketNotificationReceived(notification, payload) {
if (notification === "CONFIG") {
this.config = payload;
} else if (notification === "MODULES") {
// if this is the 1st time thru the update check process
if (!this.updateProcessStarted) {
this.updateProcessStarted = true;
this.configureModules(payload).then(() => this.performFetch());
await this.configureModules(payload);
await this.performFetch();
}
}
},
performFetch: async function () {
for (let gitInfo of await this.gitHelper.getRepos()) {
this.sendSocketNotification("STATUS", gitInfo);
async performFetch() {
const repos = await this.gitHelper.getRepos();
for (const repo of repos) {
this.sendSocketNotification("STATUS", repo);
}
this.scheduleNextFetch(this.config.updateInterval);
},
scheduleNextFetch: function (delay) {
if (delay < 60 * 1000) {
delay = 60 * 1000;
}
let self = this;
scheduleNextFetch(delay) {
clearTimeout(this.updateTimer);
this.updateTimer = setTimeout(function () {
self.performFetch();
}, delay);
this.updateTimer = setTimeout(() => {
this.performFetch();
}, Math.max(delay, ONE_MINUTE));
},
ignoreUpdateChecking: function (moduleName) {
ignoreUpdateChecking(moduleName) {
// Should not check for updates for default modules
if (defaultModules.indexOf(moduleName) >= 0) {
if (defaultModules.includes(moduleName)) {
return true;
}
// Should not check for updates for ignored modules
if (this.config.ignoreModules.indexOf(moduleName) >= 0) {
if (this.config.ignoreModules.includes(moduleName)) {
return true;
}

View File

@ -0,0 +1,3 @@
.module.updatenotification a.difflink {
text-decoration: none;
}

View File

@ -5,42 +5,59 @@
* MIT Licensed.
*/
Module.register("updatenotification", {
// Define module defaults
defaults: {
updateInterval: 10 * 60 * 1000, // every 10 minutes
refreshInterval: 24 * 60 * 60 * 1000, // one day
ignoreModules: [],
timeout: 5000
ignoreModules: []
},
suspended: false,
moduleList: {},
// Override start method.
start: function () {
Log.info("Starting module: " + this.name);
start() {
Log.info(`Starting module: ${this.name}`);
this.addFilters();
setInterval(() => {
this.moduleList = {};
this.updateDom(2);
}, this.config.refreshInterval);
},
notificationReceived: function (notification, payload, sender) {
suspend() {
this.suspended = true;
},
resume() {
this.suspended = false;
this.updateDom(2);
},
notificationReceived(notification) {
if (notification === "DOM_OBJECTS_CREATED") {
this.sendSocketNotification("CONFIG", this.config);
this.sendSocketNotification("MODULES", Module.definitions);
//this.hide(0, { lockString: this.identifier });
this.sendSocketNotification("MODULES", Object.keys(Module.definitions));
}
},
// Override socket notification handler.
socketNotificationReceived: function (notification, payload) {
socketNotificationReceived(notification, payload) {
if (notification === "STATUS") {
this.updateUI(payload);
}
},
updateUI: function (payload) {
getStyles() {
return [`${this.name}.css`];
},
getTemplate() {
return `${this.name}.njk`;
},
getTemplateData() {
return { moduleList: this.moduleList, suspended: this.suspended };
},
updateUI(payload) {
if (payload && payload.behind > 0) {
// if we haven't seen info for this module
if (this.moduleList[payload.module] === undefined) {
@ -48,7 +65,6 @@ Module.register("updatenotification", {
this.moduleList[payload.module] = payload;
this.updateDom(2);
}
//self.show(1000, { lockString: self.identifier });
} else if (payload && payload.behind === 0) {
// if the module WAS in the list, but shouldn't be
if (this.moduleList[payload.module] !== undefined) {
@ -59,62 +75,15 @@ Module.register("updatenotification", {
}
},
diffLink: function (module, text) {
const localRef = module.hash;
const remoteRef = module.tracking.replace(/.*\//, "");
return '<a href="https://github.com/MichMich/MagicMirror/compare/' + localRef + "..." + remoteRef + '" ' + 'class="xsmall dimmed" ' + 'style="text-decoration: none;" ' + 'target="_blank" >' + text + "</a>";
},
addFilters() {
this.nunjucksEnvironment().addFilter("diffLink", (text, status) => {
if (status.module !== "default") {
return text;
}
// Override dom generator.
getDom: function () {
const wrapper = document.createElement("div");
if (this.suspended === false) {
// process the hash of module info found
for (const key of Object.keys(this.moduleList)) {
let m = this.moduleList[key];
const message = document.createElement("div");
message.className = "small bright";
const icon = document.createElement("i");
icon.className = "fa fa-exclamation-circle";
icon.innerHTML = "&nbsp;";
message.appendChild(icon);
const updateInfoKeyName = m.behind === 1 ? "UPDATE_INFO_SINGLE" : "UPDATE_INFO_MULTIPLE";
let subtextHtml = this.translate(updateInfoKeyName, {
COMMIT_COUNT: m.behind,
BRANCH_NAME: m.current
});
const text = document.createElement("span");
if (m.module === "default") {
text.innerHTML = this.translate("UPDATE_NOTIFICATION");
subtextHtml = this.diffLink(m, subtextHtml);
} else {
text.innerHTML = this.translate("UPDATE_NOTIFICATION_MODULE", {
MODULE_NAME: m.module
const localRef = status.hash;
const remoteRef = status.tracking.replace(/.*\//, "");
return `<a href="https://github.com/MichMich/MagicMirror/compare/${localRef}...${remoteRef}" class="xsmall dimmed difflink" target="_blank">${text}</a>`;
});
}
message.appendChild(text);
wrapper.appendChild(message);
const subtext = document.createElement("div");
subtext.innerHTML = subtextHtml;
subtext.className = "xsmall dimmed";
wrapper.appendChild(subtext);
}
}
return wrapper;
},
suspend: function () {
this.suspended = true;
},
resume: function () {
this.suspended = false;
this.updateDom(2);
}
});

View File

@ -0,0 +1,15 @@
{% if not suspended %}
{% for name, status in moduleList %}
<div class="small bright">
<i class="fa fa-exclamation-circle"></i>
<span>
{% set mainTextLabel = "UPDATE_NOTIFICATION" if name === "default" else "UPDATE_NOTIFICATION_MODULE" %}
{{ mainTextLabel | translate({MODULE_NAME: name}) }}
</span>
</div>
<div class="xsmall dimmed">
{% set subTextLabel = "UPDATE_INFO_SINGLE" if status.behind === 1 else "UPDATE_INFO_MULTIPLE" %}
{{ subTextLabel | translate({COMMIT_COUNT: status.behind, BRANCH_NAME: status.current}) | diffLink(status) | safe }}
</div>
{% endfor %}
{% endif %}

View File

@ -127,6 +127,8 @@ WeatherProvider.register("openweathermap", {
currentWeather.humidity = currentWeatherData.main.humidity;
currentWeather.temperature = currentWeatherData.main.temp;
currentWeather.feelsLikeTemp = currentWeatherData.main.feels_like;
if (this.config.windUnits === "metric") {
currentWeather.windSpeed = this.config.useKmh ? currentWeatherData.wind.speed * 3.6 : currentWeatherData.wind.speed;
} else {

View File

@ -113,7 +113,13 @@ const WeatherProvider = Class.extend({
// A convenience function to make requests. It returns a promise.
fetchData: function (url, method = "GET", data = null) {
const getData = function (mockData) {
return new Promise(function (resolve, reject) {
if (mockData) {
let data = mockData;
data = data.substring(1, data.length - 1);
resolve(JSON.parse(data));
} else {
const request = new XMLHttpRequest();
request.open(method, url, true);
request.onreadystatechange = function () {
@ -126,7 +132,11 @@ const WeatherProvider = Class.extend({
}
};
request.send();
}
});
};
return getData(this.config.mockData);
}
});

9579
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "magicmirror",
"version": "2.17.1",
"version": "2.18.0",
"description": "The open source modular smart mirror platform.",
"main": "js/electron.js",
"scripts": {
@ -47,43 +47,43 @@
"homepage": "https://magicmirror.builders",
"devDependencies": {
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-jest": "^24.4.2",
"eslint-plugin-jsdoc": "^36.1.0",
"eslint-plugin-jest": "^25.3.0",
"eslint-plugin-jsdoc": "^37.4.0",
"eslint-plugin-prettier": "^4.0.0",
"express-basic-auth": "^1.2.0",
"husky": "^7.0.2",
"jest": "^27.2.2",
"jsdom": "^17.0.0",
"express-basic-auth": "^1.2.1",
"husky": "^7.0.4",
"jest": "^27.4.5",
"jsdom": "^19.0.0",
"lodash": "^4.17.21",
"nyc": "^15.1.0",
"prettier": "^2.4.1",
"pretty-quick": "^3.1.1",
"sinon": "^11.1.2",
"spectron": "^15.0.0",
"stylelint": "^13.13.1",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^22.0.0",
"stylelint-prettier": "^1.2.0",
"playwright": "^1.17.1",
"prettier": "^2.5.1",
"pretty-quick": "^3.1.3",
"sinon": "^12.0.1",
"stylelint": "^14.2.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-standard": "^24.0.0",
"stylelint-prettier": "^2.0.0",
"suncalc": "^1.8.0"
},
"optionalDependencies": {
"electron": "^13.5.1"
"electron": "^16.0.5"
},
"dependencies": {
"colors": "^1.4.0",
"console-stamp": "^3.0.3",
"digest-fetch": "^1.2.1",
"eslint": "^7.32.0",
"express": "^4.17.1",
"eslint": "^8.5.0",
"express": "^4.17.2",
"express-ipfilter": "^1.2.0",
"feedme": "^2.0.2",
"helmet": "^4.6.0",
"iconv-lite": "^0.6.3",
"module-alias": "^2.2.2",
"moment": "^2.29.1",
"node-fetch": "^2.6.5",
"node-fetch": "^2.6.6",
"node-ical": "^0.13.0",
"socket.io": "^4.2.0"
"socket.io": "^4.4.0"
},
"_moduleAliases": {
"node_helper": "js/node_helper.js",
@ -94,6 +94,7 @@
},
"jest": {
"verbose": true,
"testTimeout": 10000,
"projects": [
{
"displayName": "unit",
@ -111,11 +112,6 @@
"displayName": "electron",
"testMatch": [
"**/tests/electron/**/*.[jt]s?(x)"
],
"testPathIgnorePatterns": [
"<rootDir>/tests/electron/modules/mocks",
"<rootDir>/tests/electron/global-setup.js",
"<rootDir>/tests/electron/modules/basic-auth.js"
]
},
{
@ -130,6 +126,8 @@
"<rootDir>/js/"
],
"testPathIgnorePatterns": [
"<rootDir>/tests/e2e/modules/mocks",
"<rootDir>/tests/e2e/modules/basic-auth.js",
"<rootDir>/tests/e2e/global-setup.js",
"<rootDir>/tests/e2e/mock-console.js"
]

View File

@ -1,11 +0,0 @@
/* Magic Mirror Test config sample environment
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = require(process.cwd() + "/tests/configs/default.js").configFactory();
/*************** 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 = {
timeFormat: 12,
modules: [
@ -24,7 +24,7 @@ let config = require(process.cwd() + "/tests/configs/default.js").configFactory(
}
}
]
});
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

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 = {
timeFormat: 12,
modules: [
@ -25,7 +25,7 @@ let config = require(process.cwd() + "/tests/configs/default.js").configFactory(
}
}
]
});
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

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 = {
timeFormat: 12,
modules: [
@ -24,7 +24,7 @@ let config = require(process.cwd() + "/tests/configs/default.js").configFactory(
}
}
]
});
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@ -3,7 +3,7 @@
* By Rejas
* MIT Licensed.
*/
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
let config = {
timeFormat: 12,
modules: [
@ -24,7 +24,7 @@ let config = require(process.cwd() + "/tests/configs/default.js").configFactory(
}
}
]
});
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

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 = {
timeFormat: 12,
modules: [
@ -20,7 +20,7 @@ let config = require(process.cwd() + "/tests/configs/default.js").configFactory(
}
}
]
});
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@ -5,7 +5,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
let config = {
timeFormat: 12,
modules: [
@ -27,7 +27,7 @@ let config = require(process.cwd() + "/tests/configs/default.js").configFactory(
}
}
]
});
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

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 = {
timeFormat: 12,
modules: [
@ -22,7 +22,7 @@ let config = require(process.cwd() + "/tests/configs/default.js").configFactory(
}
}
]
});
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@ -3,7 +3,7 @@
* By Rejas
* MIT Licensed.
*/
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
let config = {
timeFormat: 12,
modules: [
@ -21,7 +21,7 @@ let config = require(process.cwd() + "/tests/configs/default.js").configFactory(
}
}
]
});
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@ -0,0 +1,28 @@
/* Magic Mirror Test config newsfeed module
*
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "newsfeed",
position: "bottom_bar",
config: {
feeds: [
{
title: "Rodrigo Ramirez Blog",
url: "http://localhost:8080/tests/configs/data/feed_test_rodrigoramirez.xml"
}
],
ignoreOldItems: true
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@ -3,7 +3,7 @@
* By rejas https://github.com/rejas
* MIT Licensed.
*/
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
let config = {
modules: [
{
module: "compliments",
@ -12,7 +12,7 @@ let config = require(process.cwd() + "/tests/configs/default.js").configFactory(
compliments: {
snow: ["snow"]
},
updateInterval: 4000
updateInterval: 3000
}
},
{
@ -20,12 +20,11 @@ let config = require(process.cwd() + "/tests/configs/default.js").configFactory(
position: "bottom_bar",
config: {
location: "Munich",
apiKey: "fake key",
initialLoadDelay: 3000
mockData: '"#####WEATHERDATA#####"'
}
}
]
});
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@ -3,7 +3,7 @@
* By fewieden https://github.com/fewieden
* MIT Licensed.
*/
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
let config = {
timeFormat: 12,
modules: [
@ -12,12 +12,11 @@ let config = require(process.cwd() + "/tests/configs/default.js").configFactory(
position: "bottom_bar",
config: {
location: "Munich",
apiKey: "fake key",
initialLoadDelay: 3000
mockData: '"#####WEATHERDATA#####"'
}
}
]
});
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@ -3,15 +3,14 @@
* By fewieden https://github.com/fewieden
* MIT Licensed.
*/
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
let config = {
modules: [
{
module: "weather",
position: "bottom_bar",
config: {
location: "Munich",
apiKey: "fake key",
initialLoadDelay: 3000,
mockData: '"#####WEATHERDATA#####"',
useBeaufort: false,
showWindDirectionAsArrow: true,
showSun: false,
@ -21,7 +20,7 @@ let config = require(process.cwd() + "/tests/configs/default.js").configFactory(
}
}
]
});
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@ -3,7 +3,7 @@
* By fewieden https://github.com/fewieden
* MIT Licensed.
*/
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
let config = {
units: "imperial",
modules: [
@ -12,14 +12,13 @@ let config = require(process.cwd() + "/tests/configs/default.js").configFactory(
position: "bottom_bar",
config: {
location: "Munich",
apiKey: "fake key",
initialLoadDelay: 3000,
mockData: '"#####WEATHERDATA#####"',
decimalSymbol: ",",
showHumidity: true
}
}
]
});
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@ -3,7 +3,7 @@
* By fewieden https://github.com/fewieden
* MIT Licensed.
*/
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
let config = {
timeFormat: 12,
modules: [
@ -13,13 +13,12 @@ let config = require(process.cwd() + "/tests/configs/default.js").configFactory(
config: {
type: "forecast",
location: "Munich",
apiKey: "fake key",
weatherEndpoint: "/forecast/daily",
initialLoadDelay: 3000
mockData: '"#####WEATHERDATA#####"',
weatherEndpoint: "/forecast/daily"
}
}
]
});
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@ -3,7 +3,7 @@
* By fewieden https://github.com/fewieden
* MIT Licensed.
*/
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
let config = {
timeFormat: 12,
modules: [
@ -13,16 +13,15 @@ let config = require(process.cwd() + "/tests/configs/default.js").configFactory(
config: {
type: "forecast",
location: "Munich",
apiKey: "fake key",
mockData: '"#####WEATHERDATA#####"',
weatherEndpoint: "/forecast/daily",
initialLoadDelay: 3000,
showPrecipitationAmount: true,
colored: true,
tableClass: "myTableClass"
}
}
]
});
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@ -3,7 +3,7 @@
* By rejas
* MIT Licensed.
*/
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
let config = {
units: "imperial",
modules: [
@ -13,14 +13,13 @@ let config = require(process.cwd() + "/tests/configs/default.js").configFactory(
config: {
type: "forecast",
location: "Munich",
apiKey: "fake key",
mockData: '"#####WEATHERDATA#####"',
weatherEndpoint: "/forecast/daily",
initialLoadDelay: 3000,
decimalSymbol: "_"
}
}
]
});
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@ -1,7 +1,7 @@
const fetch = require("node-fetch");
const helpers = require("./global-setup");
describe("Electron app environment", function () {
describe("App environment", function () {
beforeAll(function (done) {
helpers.startApplication("tests/configs/default.js");
helpers.getDocument(done);

View File

@ -6,7 +6,11 @@ exports.startApplication = function (configFilename, exec) {
global.app.stop();
}
// Set config sample for use in test
if (configFilename === "") {
process.env.MM_CONFIG_FILE = "config/config.js";
} else {
process.env.MM_CONFIG_FILE = configFilename;
}
if (exec) exec;
global.app = require("app.js");
global.app.start();

View File

@ -4,7 +4,7 @@
* @param {string} err The error message.
*/
function mockError(err) {
if (err.includes("ECONNREFUSED") || err.includes("ECONNRESET") || err.includes("socket hang up") || err.includes("exports is not defined")) {
if (err.includes("ECONNREFUSED") || err.includes("ECONNRESET") || err.includes("socket hang up") || err.includes("exports is not defined") || err.includes("write EPIPE")) {
jest.fn();
} else {
console.dir(err);

View File

@ -3,7 +3,7 @@ const helpers = require("../global-setup");
describe("Alert module", function () {
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/alert/default.js");
helpers.getDocument(done, 1000);
helpers.getDocument(done, 3000);
});
afterAll(function () {
helpers.stopApplication();

View File

@ -0,0 +1,158 @@
const helpers = require("../global-setup");
const serverBasicAuth = require("./basic-auth.js");
describe("Calendar module", function () {
/**
* @param {string} element css selector
* @param {string} result expected number
* @param {string} not reverse result
*/
function testElementLength(element, result, not) {
const elem = document.querySelectorAll(element);
expect(elem).not.toBe(null);
if (not === "not") {
expect(elem.length).not.toBe(result);
} else {
expect(elem.length).toBe(result);
}
}
afterAll(function () {
helpers.stopApplication();
});
describe("Default configuration", function () {
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/calendar/default.js");
helpers.getDocument(done, 3000);
});
it("should show the default maximumEntries of 10", () => {
testElementLength(".calendar .event", 10);
});
it("should show the default calendar symbol in each event", () => {
testElementLength(".calendar .event .fa-calendar", 0, "not");
});
});
describe("Custom configuration", function () {
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/calendar/custom.js");
helpers.getDocument(done, 3000);
});
it("should show the custom maximumEntries of 4", () => {
testElementLength(".calendar .event", 4);
});
it("should show the custom calendar symbol in each event", () => {
testElementLength(".calendar .event .fa-birthday-cake", 4);
});
it("should show two custom icons for repeating events", () => {
testElementLength(".calendar .event .fa-undo", 2);
});
it("should show two custom icons for day events", () => {
testElementLength(".calendar .event .fa-calendar-day", 2);
});
});
describe("Recurring event", function () {
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/calendar/recurring.js");
helpers.getDocument(done, 3000);
});
it("should show the recurring birthday event 6 times", () => {
testElementLength(".calendar .event", 6);
});
});
process.setMaxListeners(0);
for (let i = -12; i < 12; i++) {
describe("Recurring event per timezone", function () {
beforeAll(function (done) {
Date.prototype.getTimezoneOffset = function () {
return i * 60;
};
helpers.startApplication("tests/configs/modules/calendar/recurring.js");
helpers.getDocument(done, 3000);
});
it('should contain text "Mar 25th" in timezone UTC ' + -i, () => {
const elem = document.querySelector(".calendar");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("Mar 25th");
});
});
}
describe("Changed port", function () {
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/calendar/changed-port.js");
serverBasicAuth.listen(8010);
helpers.getDocument(done, 3000);
});
afterAll(function (done) {
serverBasicAuth.close(done());
});
it("should return TestEvents", function () {
testElementLength(".calendar .event", 0, "not");
});
});
describe("Basic auth", function () {
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/calendar/basic-auth.js");
helpers.getDocument(done, 3000);
});
it("should return TestEvents", function () {
testElementLength(".calendar .event", 0, "not");
});
});
describe("Basic auth by default", function () {
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/calendar/auth-default.js");
helpers.getDocument(done, 3000);
});
it("should return TestEvents", function () {
testElementLength(".calendar .event", 0, "not");
});
});
describe("Basic auth backward compatibility configuration: DEPRECATED", function () {
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/calendar/old-basic-auth.js");
helpers.getDocument(done, 3000);
});
it("should return TestEvents", function () {
testElementLength(".calendar .event", 0, "not");
});
});
describe("Fail Basic auth", function () {
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/calendar/fail-basic-auth.js");
serverBasicAuth.listen(8020);
helpers.getDocument(done, 3000);
});
afterAll(function (done) {
serverBasicAuth.close(done());
});
it("should show Unauthorized error", function () {
const elem = document.querySelector(".calendar");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("Error in the calendar module. Authorization failed");
});
});
});

View File

@ -60,4 +60,17 @@ describe("Newsfeed module", function () {
expect(elem.textContent).toContain("Error in the Newsfeed module. Malformed url.");
});
});
describe("Ignore items", function () {
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/newsfeed/ignore_items.js");
helpers.getDocument(done, 3000);
});
it("should show empty items info message", function () {
const elem = document.querySelector(".newsfeed .small");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("No news at the moment.");
});
});
});

View File

@ -0,0 +1,243 @@
const moment = require("moment");
const helpers = require("../global-setup");
const path = require("path");
const fs = require("fs");
const { generateWeather, generateWeatherForecast } = require("./mocks");
describe("Weather module", function () {
/**
*
* @param {string} element css selector
* @returns {Promise<Element>} Promise with the element once it is rendered
*/
function getElement(element) {
const elem = document.querySelector(element);
expect(elem).not.toBe(null);
return elem;
}
/**
* @param {string} element css selector
* @param {string} result Expected text in given selector
*/
function getText(element, result) {
const elem = getElement(element);
expect(
elem.textContent
.trim()
.replace(/(\r\n|\n|\r)/gm, "")
.replace(/[ ]+/g, " ")
).toBe(result);
}
/**
* @param {string} configFile path to configuration file
* @param {string} additionalMockData special data for mocking
* @param {string} callback callback
*/
function startApp(configFile, additionalMockData, callback) {
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);
helpers.startApplication("");
helpers.getDocument(callback, 3000);
}
afterAll(function () {
helpers.stopApplication();
});
describe("Current weather", function () {
describe("Default configuration", function () {
beforeAll(function (done) {
startApp("tests/configs/modules/weather/currentweather_default.js", {}, done);
});
it("should render wind speed and wind direction", function () {
getText(".weather .normal.medium span:nth-child(2)", "6 WSW");
});
it("should render temperature with icon", function () {
getText(".weather .large.light span.bright", "1.5°");
});
it("should render feels like temperature", function () {
getText(".weather .normal.medium.feelslike span.dimmed", "Feels like -5.6°");
});
});
describe("Default configuration with sunrise", function () {
beforeAll(function (done) {
const sunrise = moment().startOf("day").unix();
const sunset = moment().startOf("day").unix();
startApp("tests/configs/modules/weather/currentweather_default.js", { sys: { sunrise, sunset } }, done);
});
it("should render sunrise", function () {
getText(".weather .normal.medium span:nth-child(4)", "12:00 am");
});
});
describe("Default configuration with sunset", function () {
beforeAll(function (done) {
const sunrise = moment().startOf("day").unix();
const sunset = moment().endOf("day").unix();
startApp("tests/configs/modules/weather/currentweather_default.js", { sys: { sunrise, sunset } }, done);
});
it("should render sunset", function () {
getText(".weather .normal.medium span:nth-child(4)", "11:59 pm");
});
});
});
describe("Compliments Integration", function () {
beforeAll(function (done) {
startApp("tests/configs/modules/weather/currentweather_compliments.js", {}, done);
});
it("should render a compliment based on the current weather", function () {
getText(".compliments .module-content span", "snow");
});
});
describe("Configuration Options", function () {
beforeAll(function (done) {
startApp("tests/configs/modules/weather/currentweather_options.js", {}, done);
});
it("should render useBeaufort = false", function () {
getText(".weather .normal.medium span:nth-child(2)", "12");
});
it("should render showWindDirectionAsArrow = true", function () {
const elem = getElement(".weather .normal.medium sup i.fa-long-arrow-up");
expect(elem.outerHTML).toContain("transform:rotate(250deg);");
});
it("should render showHumidity = true", function () {
getText(".weather .normal.medium span:nth-child(3)", "93.7");
});
it("should render degreeLabel = true", function () {
getText(".weather .large.light span.bright", "1°C");
getText(".weather .normal.medium.feelslike span.dimmed", "Feels like -6°C");
});
});
describe("Current weather units", function () {
beforeAll(function (done) {
startApp(
"tests/configs/modules/weather/currentweather_units.js",
{
main: {
temp: (1.49 * 9) / 5 + 32,
temp_min: (1 * 9) / 5 + 32,
temp_max: (2 * 9) / 5 + 32
},
wind: {
speed: 11.8 * 2.23694
}
},
done
);
});
it("should render imperial units", function () {
getText(".weather .normal.medium span:nth-child(2)", "6 WSW");
getText(".weather .large.light span.bright", "34,7°");
getText(".weather .normal.medium.feelslike span.dimmed", "Feels like 22,0°");
});
it("should render custom decimalSymbol = ','", function () {
getText(".weather .normal.medium span:nth-child(3)", "93,7");
getText(".weather .large.light span.bright", "34,7°");
getText(".weather .normal.medium.feelslike span.dimmed", "Feels like 22,0°");
});
});
describe("Weather Forecast", function () {
describe("Default configuration", function () {
beforeAll(function (done) {
startApp("tests/configs/modules/weather/forecastweather_default.js", {}, done);
});
it("should render days", function () {
const days = ["Today", "Tomorrow", "Sun", "Mon", "Tue"];
for (const [index, day] of days.entries()) {
getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day);
}
});
it("should render icons", function () {
const icons = ["day-cloudy", "rain", "day-sunny", "day-sunny", "day-sunny"];
for (const [index, icon] of icons.entries()) {
getElement(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(2) span.wi-${icon}`);
}
});
it("should render max temperatures", function () {
const temperatures = ["24.4°", "21.0°", "22.9°", "23.4°", "20.6°"];
for (const [index, temp] of temperatures.entries()) {
getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`, temp);
}
});
it("should render min temperatures", function () {
const temperatures = ["15.3°", "13.6°", "13.8°", "13.9°", "10.9°"];
for (const [index, temp] of temperatures.entries()) {
getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(4)`, temp);
}
});
it("should render fading of rows", function () {
const opacities = [1, 1, 0.8, 0.5333333333333333, 0.2666666666666667];
for (const [index, opacity] of opacities.entries()) {
const elem = getElement(`.weather table.small tr:nth-child(${index + 1})`);
expect(elem.outerHTML).toContain(`<tr style="opacity: ${opacity};">`);
}
});
});
describe("Configuration Options", function () {
beforeAll(function (done) {
startApp("tests/configs/modules/weather/forecastweather_options.js", {}, done);
});
it("should render custom table class", function () {
getElement(".weather table.myTableClass");
});
it("should render colored rows", function () {
const table = getElement(".weather table.myTableClass");
expect(table.rows).not.toBe(null);
expect(table.rows.length).toBe(5);
});
});
describe("Forecast weather units", function () {
beforeAll(function (done) {
startApp("tests/configs/modules/weather/forecastweather_units.js", {}, done);
});
it("should render custom decimalSymbol = '_'", function () {
const temperatures = ["24_4°", "21_0°", "22_9°", "23_4°", "20_6°"];
for (const [index, temp] of temperatures.entries()) {
getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`, temp);
}
});
});
});
});

View File

@ -3,7 +3,7 @@ const helpers = require("./global-setup");
describe("Display of modules", function () {
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/display.js");
helpers.getDocument(done);
helpers.getDocument(done, 3000);
});
afterAll(function () {
helpers.stopApplication();

View File

@ -3,7 +3,7 @@ const helpers = require("./global-setup");
describe("Position of modules", function () {
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/positions.js");
helpers.getDocument(done, 1000);
helpers.getDocument(done, 3000);
});
afterAll(function () {
helpers.stopApplication();

View File

@ -1,53 +0,0 @@
const helpers = require("./global-setup");
describe("Development console tests", function () {
helpers.setupTimeout(this);
let app = null;
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/env.js";
});
describe("Without 'dev' commandline argument", function () {
beforeAll(function () {
return helpers
.startApplication({
args: ["js/electron.js"]
})
.then(function (startedApp) {
app = startedApp;
});
});
afterAll(function () {
return helpers.stopApplication(app);
});
it("should not open dev console when absent", async function () {
await app.client.waitUntilWindowLoaded();
return expect(await app.browserWindow.isDevToolsOpened()).toBe(false);
});
});
describe("With 'dev' commandline argument", function () {
beforeAll(function () {
return helpers
.startApplication({
args: ["js/electron.js", "dev"]
})
.then(function (startedApp) {
app = startedApp;
});
});
afterAll(function () {
return helpers.stopApplication(app);
});
it("should open dev console when provided", async function () {
expect(await app.client.getWindowCount()).toBe(2);
});
});
});

View File

@ -1,40 +1,45 @@
const helpers = require("./global-setup");
// see https://playwright.dev/docs/api/class-electronapplication
const { _electron: electron } = require("playwright");
let electronApp = null;
process.env.MM_CONFIG_FILE = "tests/configs/modules/display.js";
jest.retryTimes(3);
describe("Electron app environment", function () {
helpers.setupTimeout(this);
let app = null;
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/env.js";
beforeEach(async function () {
electronApp = await electron.launch({ args: ["js/electron.js"] });
});
beforeEach(function () {
return helpers
.startApplication({
args: ["js/electron.js"]
})
.then(function (startedApp) {
app = startedApp;
afterEach(async function () {
await electronApp.close();
});
it("should open browserwindow", async function () {
expect(await electronApp.windows().length).toBe(1);
const page = await electronApp.firstWindow();
expect(await page.title()).toBe("MagicMirror²");
expect(await page.isVisible("body")).toBe(true);
const module = page.locator("#module_0_helloworld");
await module.waitFor();
expect(await module.textContent()).toContain("Test Display Header");
});
});
afterEach(function () {
return helpers.stopApplication(app);
describe("Development console tests", function () {
beforeEach(async function () {
electronApp = await electron.launch({ args: ["js/electron.js", "dev"] });
});
it("should open a browserwindow", async function () {
await app.client.waitUntilWindowLoaded();
app.browserWindow.focus();
expect(await app.client.getWindowCount()).toBe(1);
expect(await app.browserWindow.isMinimized()).toBe(false);
expect(await app.browserWindow.isDevToolsOpened()).toBe(false);
expect(await app.browserWindow.isVisible()).toBe(true);
expect(await app.browserWindow.isFocused()).toBe(true);
const bounds = await app.browserWindow.getBounds();
expect(bounds.width).toBeGreaterThan(0);
expect(bounds.height).toBeGreaterThan(0);
expect(await app.browserWindow.getTitle()).toBe("MagicMirror²");
afterEach(async function () {
await electronApp.close();
});
it("should open browserwindow and dev console", async function () {
const pageArray = await electronApp.windows();
expect(pageArray.length).toBe(2);
for (const page of pageArray) {
expect(["MagicMirror²", "DevTools"]).toContain(await page.title());
}
});
});

View File

@ -1,53 +0,0 @@
/*
* Magic Mirror Global Setup Test Suite
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
const Application = require("spectron").Application;
const assert = require("assert");
const path = require("path");
const EventEmitter = require("events");
exports.getElectronPath = function () {
let electronPath = path.join(__dirname, "..", "..", "node_modules", ".bin", "electron");
if (process.platform === "win32") {
electronPath += ".cmd";
}
return electronPath;
};
// Set timeout - if this is run as CI Job, increase timeout
exports.setupTimeout = function (test) {
if (process.env.CI) {
jest.setTimeout(30000);
} else {
jest.setTimeout(10000);
}
};
exports.startApplication = function (options) {
const emitter = new EventEmitter();
emitter.setMaxListeners(100);
options.path = exports.getElectronPath();
if (process.env.CI) {
options.startTimeout = 30000;
}
const app = new Application(options);
return app.start().then(function () {
assert.strictEqual(app.isRunning(), true);
return app;
});
};
exports.stopApplication = function (app) {
if (!app || !app.isRunning()) {
return;
}
return app.stop().then(function () {
assert.strictEqual(app.isRunning(), false);
});
};

View File

@ -1,150 +0,0 @@
const helpers = require("../global-setup");
const serverBasicAuth = require("./basic-auth.js");
describe("Calendar module", function () {
helpers.setupTimeout(this);
let app = null;
beforeEach(function () {
return helpers
.startApplication({
args: ["js/electron.js"]
})
.then(function (startedApp) {
app = startedApp;
});
});
afterEach(function () {
return helpers.stopApplication(app);
});
describe("Default configuration", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/default.js";
});
it("should show the default maximumEntries of 10", async () => {
await app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
const events = await app.client.$$(".calendar .event");
return expect(events.length).toBe(10);
});
it("should show the default calendar symbol in each event", async () => {
await app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
const icons = await app.client.$$(".calendar .event .fa-calendar");
return expect(icons.length).not.toBe(0);
});
});
describe("Custom configuration", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/custom.js";
});
it("should show the custom maximumEntries of 4", async () => {
await app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
const events = await app.client.$$(".calendar .event");
return expect(events.length).toBe(4);
});
it("should show the custom calendar symbol in each event", async () => {
await app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
const icons = await app.client.$$(".calendar .event .fa-birthday-cake");
return expect(icons.length).toBe(4);
});
it("should show two custom icons for repeating events", async () => {
await app.client.waitUntilTextExists(".calendar", "TestEventRepeat", 10000);
const icons = await app.client.$$(".calendar .event .fa-undo");
return expect(icons.length).toBe(2);
});
it("should show two custom icons for day events", async () => {
await app.client.waitUntilTextExists(".calendar", "TestEventDay", 10000);
const icons = await app.client.$$(".calendar .event .fa-calendar-day");
return expect(icons.length).toBe(2);
});
});
describe("Recurring event", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/recurring.js";
});
it("should show the recurring birthday event 6 times", async () => {
await app.client.waitUntilTextExists(".calendar", "Mar 25th", 10000);
const events = await app.client.$$(".calendar .event");
return expect(events.length).toBe(6);
});
});
describe("Changed port", function () {
beforeAll(function () {
serverBasicAuth.listen(8010);
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/changed-port.js";
});
afterAll(function (done) {
serverBasicAuth.close(done());
});
it("should return TestEvents", function () {
return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
});
});
describe("Basic auth", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/basic-auth.js";
});
it("should return TestEvents", function () {
return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
});
});
describe("Basic auth by default", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/auth-default.js";
});
it("should return TestEvents", function () {
return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
});
});
describe("Basic auth backward compatibility configuration: DEPRECATED", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/old-basic-auth.js";
});
it("should return TestEvents", function () {
return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
});
});
describe("Fail Basic auth", function () {
beforeAll(function () {
serverBasicAuth.listen(8020);
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/fail-basic-auth.js";
});
afterAll(function (done) {
serverBasicAuth.close(done());
});
it("should show Unauthorized error", function () {
return app.client.waitUntilTextExists(".calendar", "Error in the calendar module. Authorization failed", 10000);
});
});
});

View File

@ -1,309 +0,0 @@
const fs = require("fs");
const moment = require("moment");
const path = require("path");
const wdajaxstub = require("webdriverajaxstub");
const helpers = require("../global-setup");
const { generateWeather, generateWeatherForecast } = require("./mocks");
describe("Weather module", function () {
let app;
helpers.setupTimeout(this);
/**
*
* @param {object} responses mocked data to be returned
* @returns {Promise} Resolved once the electron app is started
*/
async function setup(responses) {
app = await helpers.startApplication({
args: ["js/electron.js"],
waitTimeout: 100000
});
wdajaxstub.init(app.client, responses);
app.client.setupStub();
}
/**
*
* @param {string} element css selector
* @returns {Promise<Element>} Promise with the element once it is rendered
*/
async function getElement(element) {
return await app.client.$(element);
}
/**
* @param {string} element css selector
* @param {string} result Expected text in given selector
* @returns {Promise<boolean>} Promise with True if the text matches
*/
async function getText(element, result) {
const elem = await getElement(element);
return await elem.getText(element).then(function (text) {
expect(text.trim()).toBe(result);
});
}
afterEach(function () {
return helpers.stopApplication(app);
});
describe("Current weather", function () {
let template;
beforeAll(function () {
template = fs.readFileSync(path.join(__dirname, "..", "..", "..", "modules", "default", "weather", "current.njk"), "utf8");
});
describe("Default configuration", function () {
beforeAll(function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/weather/currentweather_default.js";
});
it("should render wind speed and wind direction", async function () {
const weather = generateWeather();
await setup({ template, data: weather });
return getText(".weather .normal.medium span:nth-child(2)", "6 WSW");
});
it("should render sunrise", async function () {
const sunrise = moment().startOf("day").unix();
const sunset = moment().startOf("day").unix();
const weather = generateWeather({ sys: { sunrise, sunset } });
await setup({ template, data: weather });
return getText(".weather .normal.medium span:nth-child(4)", "12:00 am");
});
it("should render sunset", async function () {
const sunrise = moment().startOf("day").unix();
const sunset = moment().endOf("day").unix();
const weather = generateWeather({ sys: { sunrise, sunset } });
await setup({ template, data: weather });
return getText(".weather .normal.medium span:nth-child(4)", "11:59 pm");
});
it("should render temperature with icon", async function () {
const weather = generateWeather();
await setup({ template, data: weather });
return getText(".weather .large.light span.bright", "1.5°");
});
it("should render feels like temperature", async function () {
const weather = generateWeather();
await setup({ template, data: weather });
return getText(".weather .normal.medium.feelslike span.dimmed", "Feels like -5.6°");
});
});
describe("Compliments Integration", function () {
beforeAll(function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/weather/currentweather_compliments.js";
});
it("should render a compliment based on the current weather", async function () {
const weather = generateWeather();
await setup({ template, data: weather });
return app.client.waitUntilTextExists(".compliments .module-content span", "snow");
});
});
describe("Configuration Options", function () {
beforeAll(function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/weather/currentweather_options.js";
});
it("should render useBeaufort = false", async function () {
const weather = generateWeather();
await setup({ template, data: weather });
return getText(".weather .normal.medium span:nth-child(2)", "12");
});
it("should render showWindDirectionAsArrow = true", async function () {
const weather = generateWeather();
await setup({ template, data: weather });
const elem = await getElement(".weather .normal.medium sup i.fa-long-arrow-up");
return elem.getHTML(".weather .normal.medium sup i.fa-long-arrow-up").then(function (text) {
expect(text).toContain("transform:rotate(250deg);");
});
});
it("should render showHumidity = true", async function () {
const weather = generateWeather();
await setup({ template, data: weather });
return getText(".weather .normal.medium span:nth-child(3)", "93.7");
});
it("should render degreeLabel = true", async function () {
const weather = generateWeather();
await setup({ template, data: weather });
return (await getText(".weather .large.light span.bright", "1°C")) && (await getText(".weather .normal.medium.feelslike span.dimmed", "Feels like -6°C"));
});
});
describe("Current weather units", function () {
beforeAll(function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/weather/currentweather_units.js";
});
it("should render imperial units", async function () {
const weather = generateWeather({
main: {
temp: (1.49 * 9) / 5 + 32,
temp_min: (1 * 9) / 5 + 32,
temp_max: (2 * 9) / 5 + 32
},
wind: {
speed: 11.8 * 2.23694
}
});
await setup({ template, data: weather });
return (await getText(".weather .normal.medium span:nth-child(2)", "6 WSW")) && (await getText(".weather .large.light span.bright", "34,7°")) && getText(".weather .normal.medium.feelslike span.dimmed", "Feels like 22,0°");
});
it("should render custom decimalSymbol = ','", async function () {
const weather = generateWeather({
main: {
temp: (1.49 * 9) / 5 + 32,
temp_min: (1 * 9) / 5 + 32,
temp_max: (2 * 9) / 5 + 32
},
wind: {
speed: 11.8 * 2.23694
}
});
await setup({ template, data: weather });
return (await getText(".weather .normal.medium span:nth-child(3)", "93,7")) && (await getText(".weather .large.light span.bright", "34,7°")) && getText(".weather .normal.medium.feelslike span.dimmed", "Feels like 22,0°");
});
});
});
describe("Weather Forecast", function () {
let template;
beforeAll(function () {
template = fs.readFileSync(path.join(__dirname, "..", "..", "..", "modules", "default", "weather", "forecast.njk"), "utf8");
});
describe("Default configuration", function () {
beforeAll(function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/weather/forecastweather_default.js";
});
it("should render days", async function () {
const weather = generateWeatherForecast();
await setup({ template, data: weather });
const days = ["Today", "Tomorrow", "Sun", "Mon", "Tue"];
for (const [index, day] of days.entries()) {
await getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day);
}
});
it("should render icons", async function () {
const weather = generateWeatherForecast();
await setup({ template, data: weather });
const icons = ["day-cloudy", "rain", "day-sunny", "day-sunny", "day-sunny"];
for (const [index, icon] of icons.entries()) {
await getElement(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(2) span.wi-${icon}`);
}
});
it("should render max temperatures", async function () {
const weather = generateWeatherForecast();
await setup({ template, data: weather });
const temperatures = ["24.4°", "21.0°", "22.9°", "23.4°", "20.6°"];
for (const [index, temp] of temperatures.entries()) {
await getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`, temp);
}
});
it("should render min temperatures", async function () {
const weather = generateWeatherForecast();
await setup({ template, data: weather });
const temperatures = ["15.3°", "13.6°", "13.8°", "13.9°", "10.9°"];
for (const [index, temp] of temperatures.entries()) {
await getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(4)`, temp);
}
});
it("should render fading of rows", async function () {
const weather = generateWeatherForecast();
await setup({ template, data: weather });
const opacities = [1, 1, 0.8, 0.5333333333333333, 0.2666666666666667];
const elem = await getElement(".weather table.small");
for (const [index, opacity] of opacities.entries()) {
const html = await elem.getHTML(`.weather table.small tr:nth-child(${index + 1})`);
expect(html).toContain(`<tr style="opacity: ${opacity};">`);
}
});
});
describe("Configuration Options", function () {
beforeAll(function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/weather/forecastweather_options.js";
});
it("should render custom table class", async function () {
const weather = generateWeatherForecast();
await setup({ template, data: weather });
await getElement(".weather table.myTableClass");
});
it("should render colored rows", async function () {
const weather = generateWeatherForecast();
await setup({ template, data: weather });
const rows = await app.client.$$(".weather table.myTableClass tr.colored");
expect(rows.length).toBe(5);
});
});
describe("Forecast weather units", function () {
beforeAll(function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/weather/forecastweather_units.js";
});
it("should render custom decimalSymbol = '_'", async function () {
const weather = generateWeatherForecast();
await setup({ template, data: weather });
const temperatures = ["24_4°", "21_0°", "22_9°", "23_4°", "20_6°"];
for (const [index, temp] of temperatures.entries()) {
await getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`, temp);
}
});
});
});
});

View File

@ -1,33 +0,0 @@
function plugin (wdInstance, requests) {
if (typeof wdInstance.addCommand !== "function") {
throw new Error("You can't use WebdriverAjaxStub with this version of WebdriverIO");
}
function stub({template, data}, done) {
window.XMLHttpRequest = function () {
this.open = function (method, url) {
this.method = method;
this.url = url;
};
this.send = function () {
this.status = 200;
this.readyState = 4;
const response = this.url.includes(".njk") ? template : data;
this.response = response;
this.responseText = response;
this.onreadystatechange();
};
return this;
};
done();
}
wdInstance.addCommand("setupStub", function() {
return wdInstance.executeAsync(stub, requests);
});
}
module.exports.init = plugin;

View File

@ -0,0 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Updatenotification custom module returns status information without hash 1`] = `
Object {
"behind": 7,
"current": "master",
"hash": "",
"isBehindInStatus": false,
"module": "MMM-Fuel",
"tracking": "origin/master",
}
`;
exports[`Updatenotification default returns status information 1`] = `
Object {
"behind": 5,
"current": "develop",
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
"isBehindInStatus": false,
"module": "default",
"tracking": "origin/develop",
}
`;
exports[`Updatenotification default returns status information early if isBehindInStatus 1`] = `
Object {
"behind": 5,
"current": "develop",
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
"isBehindInStatus": true,
"module": "default",
"tracking": "origin/develop",
}
`;

View File

@ -1,126 +1,139 @@
const path = require("path");
const git_Helper = require("../../../modules/default/updatenotification/git_helper.js");
const gitHelper = new git_Helper.gitHelper();
gitHelper.add("default");
jest.mock("util", () => ({
...jest.requireActual("util"),
promisify: jest.fn()
}));
const test1 = {
module: "test1",
folder: "",
res: {
stdout: "## master...origin/master [behind 8]",
stderr: ""
},
gitInfo: {
module: "default",
// commits behind:
behind: 0,
// branch name:
current: "develop",
// current hash:
hash: "",
// remote branch:
tracking: "",
isBehindInStatus: false
}
};
jest.mock("fs", () => ({
...jest.requireActual("fs"),
statSync: jest.fn()
}));
const test2 = {
module: "test2",
folder: "",
res: {
stdout: "## develop...origin/develop",
stderr: ""
}
};
const test3 = {
module: "test3",
folder: "",
res: {
stdout: "",
stderr: "error"
},
gitInfo: {
module: "default",
// commits behind:
behind: 2,
// branch name:
current: "develop",
// current hash:
hash: "",
// remote branch:
tracking: "",
isBehindInStatus: true
}
};
const test4 = {
module: "default",
folder: path.join(__dirname, "../../.."),
res: {
stdout: "",
stderr: " e40ddd4..06389e3 develop -> origin/develop"
},
gitInfo: {
module: "default",
// commits behind:
behind: 0,
// branch name:
current: "develop",
// current hash:
hash: "",
// remote branch:
tracking: "",
isBehindInStatus: false
}
};
jest.mock("logger", () => ({
...jest.requireActual("logger"),
error: jest.fn(),
info: jest.fn()
}));
describe("Updatenotification", function () {
it("should return valid output for git status", async function () {
const arr = await gitHelper.getStatus();
expect(arr.length).toBe(1);
const gitInfo = arr[0];
expect(gitInfo.current).not.toBe("");
expect(gitInfo.hash).not.toBe("");
}, 15000);
const execMock = jest.fn();
it("should return behind=8 for test1", async function () {
const gitInfo = await gitHelper.getStatusInfo(test1);
expect(gitInfo.behind).toBe(8);
expect(gitInfo.isBehindInStatus).toBe(true);
let gitHelper;
let gitRemoteOut;
let gitRevParseOut;
let gitStatusOut;
let gitFetchOut;
let gitRevListOut;
let gitRemoteErr;
let gitRevParseErr;
let gitStatusErr;
let gitFetchErr;
let gitRevListErr;
beforeAll(async function () {
const { promisify } = require("util");
promisify.mockReturnValue(execMock);
const GitHelper = require(`../../../modules/default/updatenotification/git_helper`);
gitHelper = new GitHelper();
});
it("should return behind=0 for test2", async function () {
const gitInfo = await gitHelper.getStatusInfo(test2);
expect(gitInfo.behind).toBe(0);
expect(gitInfo.isBehindInStatus).toBe(false);
beforeEach(function () {
gitRemoteOut = "";
gitRevParseOut = "";
gitStatusOut = "";
gitFetchOut = "";
gitRevListOut = "";
gitRemoteErr = "";
gitRevParseErr = "";
gitStatusErr = "";
gitFetchErr = "";
gitRevListErr = "";
execMock.mockImplementation(function (command) {
if (command.includes("git remote -v")) {
return { stdout: gitRemoteOut, stderr: gitRemoteErr };
} else if (command.includes("git rev-parse HEAD")) {
return { stdout: gitRevParseOut, stderr: gitRevParseErr };
} else if (command.includes("git status -sb")) {
return { stdout: gitStatusOut, stderr: gitStatusErr };
} else if (command.includes("git fetch --dry-run")) {
return { stdout: gitFetchOut, stderr: gitFetchErr };
} else if (command.includes("git rev-list --ancestry-path --count")) {
return { stdout: gitRevListOut, stderr: gitRevListErr };
}
});
});
it("should return empty status object for test3", async function () {
const gitInfo = await gitHelper.getStatusInfo(test3);
expect(gitInfo).toBe(undefined);
afterEach(async function () {
gitHelper.gitRepos = [];
jest.clearAllMocks();
});
it("should return empty repo object for test2", async function () {
// no gitInfo provided in res, so returns undefined
const gitInfo = await gitHelper.getRepoInfo(test2);
expect(gitInfo).toBe(undefined);
describe("default", () => {
const moduleName = "default";
beforeEach(async function () {
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";
await gitHelper.add(moduleName);
});
it("should return empty repo object for test1", async function () {
// no regex match for refs in empty string, so returns undefined
const gitInfo = await gitHelper.getRepoInfo(test1);
expect(gitInfo).toBe(undefined);
it("returns status information", async function () {
const repos = await gitHelper.getRepos();
expect(repos[0]).toMatchSnapshot();
expect(execMock).toHaveBeenCalledTimes(5);
});
it("should return empty repo object for test4", async function () {
// git ref list throws error, so returns undefined
const gitInfo = await gitHelper.getRepoInfo(test4);
expect(gitInfo).toBe(undefined);
it("returns status information early if isBehindInStatus", async function () {
gitStatusOut = "## develop...origin/develop [behind 5]";
const repos = await gitHelper.getRepos();
expect(repos[0]).toMatchSnapshot();
expect(execMock).toHaveBeenCalledTimes(3);
});
it("should return behind=2 for test3", async function () {
const gitInfo = await gitHelper.getRepoInfo(test3);
expect(gitInfo.behind).toBe(2);
it("excludes repo if status can't be retrieved", async function () {
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`);
});
it("excludes repo if refs don't match regex", async function () {
gitFetchErr = "";
const repos = await gitHelper.getRepos();
expect(repos.length).toBe(0);
});
});
describe("custom module", () => {
const moduleName = "MMM-Fuel";
beforeEach(async function () {
gitRemoteOut = `origin\thttps://github.com/fewieden/${moduleName}.git (fetch)\norigin\thttps://github.com/fewieden/${moduleName}.git (push)\n`;
gitRevParseOut = "9d8310163da94441073a93cead711ba43e8888d0";
gitStatusOut = "## master...origin/master";
gitFetchErr = `From https://github.com/fewieden/${moduleName}\n19f7faf..9d83101 master -> origin/master`;
gitRevListOut = "7";
await gitHelper.add(moduleName);
});
it("returns status information without hash", async function () {
const repos = await gitHelper.getRepos();
expect(repos[0]).toMatchSnapshot();
expect(execMock).toHaveBeenCalledTimes(4);
});
});
});

View File

@ -31,6 +31,8 @@
"MODULE_CONFIG_CHANGED": "Die Konfigurationsoptionen für das {MODULE_NAME} Modul haben sich geändert. \nBitte überprüfen Sie die Dokumentation.",
"MODULE_CONFIG_ERROR": "Fehler im {MODULE_NAME} Modul. {ERROR}",
"NEWSFEED_NO_ITEMS": "Keine Neuigkeiten momentan.",
"UPDATE_NOTIFICATION": "Aktualisierung für MagicMirror² verfügbar.",
"UPDATE_NOTIFICATION_MODULE": "Aktualisierung für das {MODULE_NAME} Modul verfügbar.",
"UPDATE_INFO_SINGLE": "Die aktuelle Installation ist {COMMIT_COUNT} Commit hinter dem {BRANCH_NAME} Branch.",

View File

@ -35,6 +35,8 @@
"MODULE_ERROR_UNAUTHORIZED": "Authorization failed.",
"MODULE_ERROR_UNSPECIFIED": "Check logs for more details.",
"NEWSFEED_NO_ITEMS": "No news at the moment.",
"UPDATE_NOTIFICATION": "MagicMirror² update available.",
"UPDATE_NOTIFICATION_MODULE": "Update available for {MODULE_NAME} module.",
"UPDATE_INFO_SINGLE": "The current installation is {COMMIT_COUNT} commit behind on the {BRANCH_NAME} branch.",

View File

@ -6,6 +6,7 @@
"DAYAFTERTOMORROW": "모레",
"RUNNING": "종료 일",
"EMPTY": "예정된 이벤트가 없습니다.",
"WEEK": "{weekNumber}주차",
"N": "북풍",
"NNE": "북북동풍",
@ -24,6 +25,16 @@
"NW": "북서풍",
"NNW": "북북서풍",
"FEELS": "체감온도 {DEGREE}",
"PRECIP": "PoP",
"MODULE_CONFIG_CHANGED": "모듈 {MODULE_NAME}의 설정값이 바뀌었습니다.\n매뉴얼을 참고하세요.",
"MODULE_CONFIG_ERROR": "에러 : {MODULE_NAME} - {ERROR}",
"MODULE_ERROR_MALFORMED_URL": "잘못된 URL 형식입니다.",
"MODULE_ERROR_NO_CONNECTION": "인터넷이 연결되지 않았습니다.",
"MODULE_ERROR_UNAUTHORIZED": "인증이 실패했습니다.",
"MODULE_ERROR_UNSPECIFIED": "상세 내용은 로그를 확인하세요.",
"UPDATE_NOTIFICATION": "새로운 MagicMirror² 업데이트가 있습니다.",
"UPDATE_NOTIFICATION_MODULE": "{MODULE_NAME} 모듈에서 사용 가능한 업데이트 입니다.",
"UPDATE_INFO_SINGLE": "설치할 {COMMIT_COUNT} commit 는 {BRANCH_NAME} 분기에 해당됩니다.",

View File

@ -30,6 +30,12 @@
"MODULE_CONFIG_CHANGED": "De configuratie opties voor de module {MODULE_NAME} zijn gewijzigd.\nControleer de documentatie.",
"MODULE_CONFIG_ERROR": "Fout in de {MODULE_NAME} module. {ERROR}",
"MODULE_ERROR_MALFORMED_URL": "Ongeldige url.",
"MODULE_ERROR_NO_CONNECTION": "Geen internet verbinding.",
"MODULE_ERROR_UNAUTHORIZED": "Authenticatie mislukt.",
"MODULE_ERROR_UNSPECIFIED": "Bekijk de logs voor meer informatie.",
"NEWSFEED_NO_ITEMS": "Geen nieuws op dit moment.",
"UPDATE_NOTIFICATION": "MagicMirror² update beschikbaar.",
"UPDATE_NOTIFICATION_MODULE": "Update beschikbaar voor {MODULE_NAME} module.",

View File

@ -34,7 +34,7 @@ let translations = {
hu: "translations/hu.json", // Hungarian
is: "translations/is.json", // Icelandic
et: "translations/et.json", // Estonian
kr: "translations/kr.json", // Korean
ko: "translations/ko.json", // Korean
ro: "translations/ro.json", // Romanian
cy: "translations/cy.json", // Welsh (Cymraeg)
bg: "translations/bg.json", // Bulgarian
@ -47,7 +47,8 @@ let translations = {
uk: "translations/uk.json", // Ukrainian
hi: "translations/hi.json", // Hindi
gu: "translations/gu.json", // Gujarati
gl: "translations/gl.json" // Galego
gl: "translations/gl.json", // Galego
lt: "translations/lt.json" // Lithuanian
};
if (typeof module !== "undefined") {