mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 11:50:00 +00:00
commit
a6cbc9f0ef
@ -16,7 +16,7 @@
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2017,
|
||||
"ecmaVersion": 2018,
|
||||
"ecmaFeatures": {
|
||||
"globalReturn": true
|
||||
}
|
||||
|
16
.github/CONTRIBUTING.md
vendored
16
.github/CONTRIBUTING.md
vendored
@ -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.
|
||||
|
||||
|
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -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.
|
||||
|
||||
|
5
.github/workflows/automated-tests.yml
vendored
5
.github/workflows/automated-tests.yml
vendored
@ -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 &
|
||||
|
4
.github/workflows/codecov-test-suites.yml
vendored
4
.github/workflows/codecov-test-suites.yml
vendored
@ -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
|
||||
|
2
.github/workflows/enforce-changelog.yml
vendored
2
.github/workflows/enforce-changelog.yml
vendored
@ -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"
|
||||
|
44
CHANGELOG.md
44
CHANGELOG.md
@ -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
|
||||
|
||||
|
17
README.md
17
README.md
@ -1,12 +1,17 @@
|
||||

|
||||
|
||||
<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).
|
||||
|
@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
# This file is still here to keep PM2 working on older installations.
|
||||
cd ~/MagicMirror
|
||||
DISPLAY=:0 npm start
|
||||
|
@ -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)) {
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
5
modules/default/alert/styles/center.css
Normal file
5
modules/default/alert/styles/center.css
Normal file
@ -0,0 +1,5 @@
|
||||
.ns-box {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
4
modules/default/alert/styles/left.css
Normal file
4
modules/default/alert/styles/left.css
Normal file
@ -0,0 +1,4 @@
|
||||
.ns-box {
|
||||
margin-right: auto;
|
||||
text-align: left;
|
||||
}
|
@ -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,
|
4
modules/default/alert/styles/right.css
Normal file
4
modules/default/alert/styles/right.css
Normal file
@ -0,0 +1,4 @@
|
||||
.ns-box {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
18
modules/default/alert/templates/alert.njk
Normal file
18
modules/default/alert/templates/alert.njk
Normal 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 %}
|
9
modules/default/alert/templates/notification.njk
Normal file
9
modules/default/alert/templates/notification.njk
Normal 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 %}
|
@ -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;
|
||||
},
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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 }}
|
||||
|
@ -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"
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,3 @@
|
||||
.module.updatenotification a.difflink {
|
||||
text-decoration: none;
|
||||
}
|
@ -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 = " ";
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
15
modules/default/updatenotification/updatenotification.njk
Normal file
15
modules/default/updatenotification/updatenotification.njk
Normal 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 %}
|
@ -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 {
|
||||
|
@ -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
9579
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
48
package.json
48
package.json
@ -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"
|
||||
]
|
||||
|
@ -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;
|
||||
}
|
@ -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") {
|
||||
|
@ -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") {
|
||||
|
@ -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") {
|
||||
|
@ -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") {
|
||||
|
@ -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") {
|
||||
|
@ -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") {
|
||||
|
@ -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") {
|
||||
|
@ -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") {
|
||||
|
28
tests/configs/modules/newsfeed/ignore_items.js
Normal file
28
tests/configs/modules/newsfeed/ignore_items.js
Normal 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;
|
||||
}
|
@ -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") {
|
||||
|
@ -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") {
|
||||
|
@ -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") {
|
||||
|
@ -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") {
|
||||
|
@ -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") {
|
||||
|
@ -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") {
|
||||
|
@ -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") {
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
158
tests/e2e/modules/calendar_spec.js
Normal file
158
tests/e2e/modules/calendar_spec.js
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
@ -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.");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
243
tests/e2e/modules/weather_spec.js
Normal file
243
tests/e2e/modules/weather_spec.js
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
};
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
33
tests/node_modules/webdriverajaxstub/index.js
generated
vendored
33
tests/node_modules/webdriverajaxstub/index.js
generated
vendored
@ -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;
|
@ -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",
|
||||
}
|
||||
`;
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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.",
|
||||
|
@ -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.",
|
||||
|
@ -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} 분기에 해당됩니다.",
|
@ -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.",
|
||||
|
@ -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") {
|
||||
|
Loading…
x
Reference in New Issue
Block a user