first commit

This commit is contained in:
2023-09-30 20:18:17 +02:00
commit d5047f6ee0
293 changed files with 40054 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
/* MagicMirror² Test default config for modules
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
exports.configFactory = (options) => {
return Object.assign(
{
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
contextIsolation: false
}
},
modules: []
},
options
);
};
+13
View File
@@ -0,0 +1,13 @@
/* MagicMirror² Test config sample ipWhitelist
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = require(`${process.cwd()}/tests/configs/default.js`).configFactory({
ipWhitelist: []
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
+21
View File
@@ -0,0 +1,21 @@
/* MagicMirror² Test config sample module alert
*
* By rejas
* MIT Licensed.
*/
let config = {
modules: [
{
module: "alert",
config: {
display_time: 1000000,
welcome_message: true
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,32 @@
/* MagicMirror² Test config default calendar with auth by default
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
calendars: [
{
maximumNumberOfDays: 10000,
url: "http://localhost:8080/tests/mocks/calendar_test.ics",
auth: {
user: "MagicMirror",
pass: "CallMeADog"
}
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,33 @@
/* MagicMirror² Test config default calendar
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
calendars: [
{
maximumNumberOfDays: 10000,
url: "http://localhost:8080/tests/mocks/calendar_test.ics",
auth: {
user: "MagicMirror",
pass: "CallMeADog",
method: "basic"
}
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,32 @@
/* MagicMirror² Test config default calendar with auth by default
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
calendars: [
{
maximumNumberOfDays: 10000,
url: "http://localhost:8010/tests/mocks/calendar_test.ics",
auth: {
user: "MagicMirror",
pass: "CallMeADog"
}
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
+33
View File
@@ -0,0 +1,33 @@
/* MagicMirror² Test config custom calendar
*
* By Rejas
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
customEvents: [{ keyword: "CustomEvent", symbol: "dice" }],
calendars: [
{
maximumEntries: 5,
maximumNumberOfDays: 10000,
symbol: "birthday-cake",
fullDaySymbol: "calendar-day",
recurringSymbol: "undo",
url: "http://localhost:8080/tests/mocks/calendar_test_icons.ics"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
+28
View File
@@ -0,0 +1,28 @@
/* MagicMirror² Test config default calendar
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
calendars: [
{
maximumNumberOfDays: 10000,
url: "http://localhost:8080/tests/mocks/calendar_test.ics"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,35 @@
/* MagicMirror² Test calendar calendar
*
* This configuration is a wrong authentication
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
calendars: [
{
maximumNumberOfDays: 10000,
url: "http://localhost:8020/tests/mocks/calendar_test.ics",
auth: {
user: "MagicMirror",
pass: "StairwayToHeaven",
method: "basic"
}
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,30 @@
/* MagicMirror² Test config default calendar
* with authentication old config
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
calendars: [
{
maximumNumberOfDays: 10000,
url: "http://localhost:8080/tests/mocks/calendar_test.ics",
user: "MagicMirror",
pass: "CallMeADog"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,29 @@
/* MagicMirror² Test config custom calendar
*
* By Rejas
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
calendars: [
{
maximumEntries: 6,
maximumNumberOfDays: 3650,
url: "http://localhost:8080/tests/mocks/calendar_test_recurring.ics"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
+20
View File
@@ -0,0 +1,20 @@
/* MagicMirror² Test config for default clock module
*
* By Sergey Morozov
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "clock",
position: "middle_center"
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
+18
View File
@@ -0,0 +1,18 @@
/* MagicMirror² Test config for default clock module
*
* By Sergey Morozov
* MIT Licensed.
*/
let config = {
modules: [
{
module: "clock",
position: "middle_center"
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,22 @@
/* MagicMirror² Test config for analog clock face
*
* MIT Licensed.
*/
let config = {
modules: [
{
module: "clock",
position: "middle_center",
config: {
displayType: "analog",
analogFace: "face-006",
showDate: false
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,23 @@
/* MagicMirror² Test config for default clock module
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "clock",
position: "middle_center",
config: {
displaySeconds: false
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,25 @@
/* MagicMirror² Test config for default clock module
*
* By Johan Hammar
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "clock",
position: "middle_center",
config: {
showTime: true,
showDate: true,
displayType: "analog"
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,23 @@
/* MagicMirror² Test config for default clock module
*
* By Sergey Morozov
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "clock",
position: "middle_center",
config: {
showPeriodUpper: true
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,23 @@
/* MagicMirror² Test config for default clock module
*
* By Johan Hammar
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "clock",
position: "middle_center",
config: {
showTime: false
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,23 @@
/* MagicMirror² Test config for default clock module
*
* By Johan Hammar
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "clock",
position: "middle_center",
config: {
showWeek: true
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,21 @@
/* MagicMirror² Test config for default clock module
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
language: "es",
timeFormat: 12,
modules: [
{
module: "clock",
position: "middle_center"
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,20 @@
/* MagicMirror² Test config for default clock module
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
language: "es",
modules: [
{
module: "clock",
position: "middle_center"
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,24 @@
/* MagicMirror² Test config for default clock module
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
language: "es",
timeFormat: 12,
modules: [
{
module: "clock",
position: "middle_center",
config: {
showPeriodUpper: true
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,25 @@
/* MagicMirror² Test config for default clock module
* Language es for showWeek feature
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
language: "es",
timeFormat: 12,
modules: [
{
module: "clock",
position: "middle_center",
config: {
showWeek: true
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,28 @@
/* MagicMirror² Test config compliments with anytime type
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "compliments",
position: "middle_center",
config: {
compliments: {
morning: [],
afternoon: [],
evening: [],
anytime: ["Anytime here"]
}
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,28 @@
/* MagicMirror² Test config compliments with date type
*
* By Rejas
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "compliments",
position: "middle_center",
config: {
compliments: {
morning: [],
afternoon: [],
evening: [],
"....-01-01": ["Happy new year!"]
}
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,25 @@
/* MagicMirror² Test config compliments with anytime type
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "compliments",
position: "middle_center",
config: {
compliments: {
anytime: ["Anytime here"]
}
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,27 @@
/* MagicMirror² Test config for default compliments
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "compliments",
position: "middle_center",
config: {
compliments: {
morning: ["Hi", "Good Morning", "Morning test"],
afternoon: ["Hello", "Good Afternoon", "Afternoon test"],
evening: ["Hello There", "Good Evening", "Evening test"]
}
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,21 @@
/* MagicMirror² Test config compliments with remote file
*
* By Rejas
* MIT Licensed.
*/
let config = {
modules: [
{
module: "compliments",
position: "middle_center",
config: {
remoteFile: "http://localhost:8080/tests/mocks/compliments_test.json"
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
+29
View File
@@ -0,0 +1,29 @@
/* MagicMirror² Test config for display setters module using the helloworld module
*
* By Rejas
* MIT Licensed.
*/
let config = {
modules: [
{
module: "helloworld",
position: "top_bar",
header: "test_header",
config: {
text: "Test Display Header"
}
},
{
module: "helloworld",
position: "bottom_bar",
config: {
text: "Test Hide Header"
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,21 @@
/* MagicMirror² Test config sample module hello world
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
modules: [
{
module: "helloworld",
position: "bottom_bar",
config: {
text: "Test HelloWorld Module"
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,18 @@
/* MagicMirror² Test config sample module hello world default config
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
modules: [
{
module: "helloworld",
position: "bottom_bar"
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
+28
View File
@@ -0,0 +1,28 @@
/* MagicMirror² Test config newsfeed module
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "newsfeed",
position: "bottom_bar",
config: {
feeds: [
{
title: "Rodrigo Ramirez Blog",
url: "http://localhost:8080/tests/mocks/newsfeed_test.xml"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,28 @@
/* MagicMirror² 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/mocks/newsfeed_test.xml"
}
],
ignoreOldItems: true
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,27 @@
/* MagicMirror² Test config newsfeed module
*
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "newsfeed",
position: "bottom_bar",
config: {
feeds: [
{
title: "Incorrect Url",
url: "this is not a valid url"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,29 @@
/* MagicMirror² 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/mocks/newsfeed_test.xml"
}
],
prohibitedWords: ["QPanel"],
showDescription: true
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
+28
View File
@@ -0,0 +1,28 @@
/* MagicMirror² Test config for position setters module using the helloworld module
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
modules:
// Using exotic content. This is why don't accept go to JSON configuration file
(() => {
let positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
let modules = Array();
for (let idx in positions) {
modules.push({
module: "helloworld",
position: positions[idx],
config: {
text: `Text in ${positions[idx]}`
}
});
}
return modules;
})()
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,32 @@
/* MagicMirror² Test config current weather compliments
*
* By rejas https://github.com/rejas
* MIT Licensed.
*/
let config = {
modules: [
{
module: "compliments",
position: "top_bar",
config: {
compliments: {
snow: ["snow"]
},
updateInterval: 3000
}
},
{
module: "weather",
position: "bottom_bar",
config: {
location: "Munich",
mockData: '"#####WEATHERDATA#####"'
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,24 @@
/* MagicMirror² Test config default weather
*
* By fewieden https://github.com/fewieden
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "weather",
position: "bottom_bar",
config: {
location: "Munich",
mockData: '"#####WEATHERDATA#####"'
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,28 @@
/* MagicMirror² Test config default weather
*
* By fewieden https://github.com/fewieden
* MIT Licensed.
*/
let config = {
modules: [
{
module: "weather",
position: "bottom_bar",
config: {
location: "Munich",
mockData: '"#####WEATHERDATA#####"',
windUnits: "beaufort",
showWindDirectionAsArrow: true,
showSun: false,
showHumidity: true,
roundTemp: true,
degreeLabel: true
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,26 @@
/* MagicMirror² Test config default weather
*
* By fewieden https://github.com/fewieden
* MIT Licensed.
*/
let config = {
units: "imperial",
modules: [
{
module: "weather",
position: "bottom_bar",
config: {
location: "Munich",
mockData: '"#####WEATHERDATA#####"',
decimalSymbol: ",",
showHumidity: true
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,27 @@
/* MagicMirror² Test config default weather
*
* By fewieden https://github.com/fewieden
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "weather",
position: "bottom_bar",
config: {
type: "forecast",
location: "Munich",
mockData: '"#####WEATHERDATA#####"',
weatherEndpoint: "/forecast/daily",
absoluteDates: true
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,26 @@
/* MagicMirror² Test config default weather
*
* By fewieden https://github.com/fewieden
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "weather",
position: "bottom_bar",
config: {
type: "forecast",
location: "Munich",
mockData: '"#####WEATHERDATA#####"',
weatherEndpoint: "/forecast/daily"
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,29 @@
/* MagicMirror² Test config default weather
*
* By fewieden https://github.com/fewieden
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "weather",
position: "bottom_bar",
config: {
type: "forecast",
location: "Munich",
mockData: '"#####WEATHERDATA#####"',
weatherEndpoint: "/forecast/daily",
showPrecipitationAmount: true,
colored: true,
tableClass: "myTableClass"
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,28 @@
/* MagicMirror² Test config default weather
*
* By rejas
* MIT Licensed.
*/
let config = {
units: "imperial",
modules: [
{
module: "weather",
position: "bottom_bar",
config: {
type: "forecast",
location: "Munich",
mockData: '"#####WEATHERDATA#####"',
weatherEndpoint: "/forecast/daily",
decimalSymbol: "_",
showPrecipitationAmount: true
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,25 @@
/* MagicMirror² Test config hourly weather
*
* By rejas https://github.com/rejas
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "weather",
position: "bottom_bar",
config: {
type: "hourly",
location: "Berlin",
mockData: '"#####WEATHERDATA#####"'
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,26 @@
/* MagicMirror² Test config hourly weather options
*
* By rejas https://github.com/rejas
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "weather",
position: "bottom_bar",
config: {
type: "hourly",
location: "Berlin",
mockData: '"#####WEATHERDATA#####"',
hourlyForecastIncrements: 2
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
@@ -0,0 +1,27 @@
/* MagicMirror² Test config hourly weather
*
* By rejas https://github.com/rejas
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "weather",
position: "bottom_bar",
config: {
type: "hourly",
location: "Berlin",
mockData: '"#####WEATHERDATA#####"',
showPrecipitationAmount: true,
showPrecipitationProbability: true
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
+13
View File
@@ -0,0 +1,13 @@
/* MagicMirror² Test config sample ipWhitelist
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = require(`${process.cwd()}/tests/configs/default.js`).configFactory({
ipWhitelist: ["x.x.x.x"]
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
+13
View File
@@ -0,0 +1,13 @@
/* MagicMirror² Test config sample environment set port 8090
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = require(`${process.cwd()}/tests/configs/default.js`).configFactory({
port: 8090
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
+1
View File
@@ -0,0 +1 @@
MM_PORT=8090
+13
View File
@@ -0,0 +1,13 @@
/* MagicMirror² Test config sample environment set port 8090
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = require(`${process.cwd()}/tests/configs/default.js`).configFactory({
port: ${MM_PORT}
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
+13
View File
@@ -0,0 +1,13 @@
/* MagicMirror² Test default config for modules
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.10.1"]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
+27
View File
@@ -0,0 +1,27 @@
const helpers = require("./helpers/global-setup");
describe("App environment", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/default.js");
await helpers.getDocument();
});
afterAll(async () => {
await helpers.stopApplication();
});
it("get request from http://localhost:8080 should return 200", async () => {
const res = await helpers.fetch("http://localhost:8080");
expect(res.status).toBe(200);
});
it("get request from http://localhost:8080/nothing should return 404", async () => {
const res = await helpers.fetch("http://localhost:8080/nothing");
expect(res.status).toBe(404);
});
it("should show the title MagicMirror²", async () => {
const elem = await helpers.waitForElement("title");
expect(elem).not.toBe(null);
expect(elem.textContent).toBe("MagicMirror²");
});
});
+28
View File
@@ -0,0 +1,28 @@
const helpers = require("./helpers/global-setup");
describe("All font files from roboto.css should be downloadable", () => {
const fontFiles = [];
// Statements below filters out all 'url' lines in the CSS file
const fileContent = require("fs").readFileSync(`${__dirname}/../../fonts/roboto.css`, "utf8");
const regex = /\burl\(['"]([^'"]+)['"]\)/g;
let match = regex.exec(fileContent);
while (match !== null) {
// Push 1st match group onto fontFiles stack
fontFiles.push(match[1]);
// Find the next one
match = regex.exec(fileContent);
}
beforeAll(async () => {
await helpers.startApplication("tests/configs/without_modules.js");
});
afterAll(async () => {
await helpers.stopApplication();
});
test.each(fontFiles)("should return 200 HTTP code for file '%s'", async (fontFile) => {
const fontUrl = `http://localhost:8080/fonts/${fontFile}`;
const res = await helpers.fetch(fontUrl);
expect(res.status).toBe(200);
});
});
+29
View File
@@ -0,0 +1,29 @@
const path = require("path");
const auth = require("express-basic-auth");
const express = require("express");
const app = express();
const basicAuth = auth({
realm: "MagicMirror² Area restricted.",
users: { MagicMirror: "CallMeADog" }
});
app.use(basicAuth);
// Set available directories
const directories = ["/tests/configs", "/tests/mocks"];
const rootPath = path.resolve(`${__dirname}/../../../`);
for (let directory of directories) {
app.use(directory, express.static(path.resolve(rootPath + directory)));
}
let server;
exports.listen = (...args) => {
server = app.listen.apply(app, args);
};
exports.close = async () => {
await server.close();
};
+95
View File
@@ -0,0 +1,95 @@
const jsdom = require("jsdom");
const corefetch = require("fetch");
exports.startApplication = async (configFilename, exec) => {
jest.resetModules();
if (global.app) {
await this.stopApplication();
}
// 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("../../../js/app");
return global.app.start();
};
exports.stopApplication = async () => {
if (!global.app) {
return Promise.resolve();
}
await global.app.stop();
delete global.app;
};
exports.getDocument = () => {
return new Promise((resolve) => {
const url = `http://${config.address || "localhost"}:${config.port || "8080"}`;
jsdom.JSDOM.fromURL(url, { resources: "usable", runScripts: "dangerously" }).then((dom) => {
dom.window.name = "jsdom";
dom.window.fetch = corefetch;
dom.window.onload = () => {
global.document = dom.window.document;
resolve();
};
});
});
};
exports.waitForElement = (selector, ignoreValue = "") => {
return new Promise((resolve) => {
let oldVal = "dummy12345";
const interval = setInterval(() => {
const element = document.querySelector(selector);
if (element) {
let newVal = element.textContent;
if (newVal === oldVal) {
clearInterval(interval);
resolve(element);
} else {
if (ignoreValue === "") {
oldVal = newVal;
} else {
if (!newVal.includes(ignoreValue)) oldVal = newVal;
}
}
}
}, 100);
});
};
exports.waitForAllElements = (selector) => {
return new Promise((resolve) => {
let oldVal = 999999;
const interval = setInterval(() => {
const element = document.querySelectorAll(selector);
if (element) {
let newVal = element.length;
if (newVal === oldVal) {
clearInterval(interval);
resolve(element);
} else {
if (newVal !== 0) oldVal = newVal;
}
}
}, 100);
});
};
exports.fetch = (url) => {
return new Promise((resolve) => {
corefetch(url).then((res) => {
resolve(res);
});
});
};
exports.testMatch = async (element, regex) => {
const elem = await this.waitForElement(element);
expect(elem).not.toBe(null);
expect(elem.textContent).toMatch(regex);
};
+28
View File
@@ -0,0 +1,28 @@
/**
* Suppresses errors concerning web server already shut down.
* @param {string} err The error message.
*/
const mockError = (err) => {
if (
err.includes("ECONNREFUSED") ||
err.includes("ECONNRESET") ||
err.includes("socket hang up") ||
err.includes("exports is not defined") ||
err.includes("write EPIPE") ||
err.includes("AggregateError") ||
err.includes("ERR_SOCKET_CONNECTION_TIMEOUT")
) {
jest.fn();
} else {
console.dir(err);
}
};
global.console = {
log: jest.fn(),
dir: console.dir,
error: mockError,
warn: console.warn,
info: jest.fn(),
debug: console.debug
};
+19
View File
@@ -0,0 +1,19 @@
const { injectMockData } = require("../../utils/weather_mocker");
const helpers = require("./global-setup");
exports.getText = async (element, result) => {
const elem = await helpers.waitForElement(element);
expect(elem).not.toBe(null);
expect(
elem.textContent
.trim()
.replace(/(\r\n|\n|\r)/gm, "")
.replace(/[ ]+/g, " ")
).toBe(result);
};
exports.startApp = async (configFileName, additionalMockData) => {
injectMockData(configFileName, additionalMockData);
await helpers.startApplication("");
await helpers.getDocument();
};
+31
View File
@@ -0,0 +1,31 @@
const helpers = require("./helpers/global-setup");
describe("ipWhitelist directive configuration", () => {
describe("Set ipWhitelist without access", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/noIpWhiteList.js");
});
afterAll(async () => {
await helpers.stopApplication();
});
it("should return 403", async () => {
const res = await helpers.fetch("http://localhost:8080");
expect(res.status).toBe(403);
});
});
describe("Set ipWhitelist []", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/empty_ipWhiteList.js");
});
afterAll(async () => {
await helpers.stopApplication();
});
it("should return 200", async () => {
const res = await helpers.fetch("http://localhost:8080");
expect(res.status).toBe(200);
});
});
});
+17
View File
@@ -0,0 +1,17 @@
const helpers = require("../helpers/global-setup");
describe("Alert module", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/alert/default.js");
await helpers.getDocument();
});
afterAll(async () => {
await helpers.stopApplication();
});
it("should show the welcome message", async () => {
const elem = await helpers.waitForElement(".ns-box .ns-box-inner .light.bright.small");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("Welcome, start was successful!");
});
});
+164
View File
@@ -0,0 +1,164 @@
const helpers = require("../helpers/global-setup");
const serverBasicAuth = require("../helpers/basic-auth");
describe("Calendar module", () => {
/**
* @param {string} element css selector
* @param {string} result expected number
* @param {string} not reverse result
*/
const testElementLength = async (element, result, not) => {
const elem = await helpers.waitForAllElements(element);
expect(elem).not.toBe(null);
if (not === "not") {
expect(elem.length).not.toBe(result);
} else {
expect(elem.length).toBe(result);
}
};
const testTextContain = async (element, text) => {
const elem = await helpers.waitForElement(element, "undefinedLoading");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain(text);
};
afterAll(async () => {
await helpers.stopApplication();
});
describe("Default configuration", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/calendar/default.js");
await helpers.getDocument();
});
it("should show the default maximumEntries of 10", async () => {
await testElementLength(".calendar .event", 10);
});
it("should show the default calendar symbol in each event", async () => {
await testElementLength(".calendar .event .fa-calendar-alt", 0, "not");
});
});
describe("Custom configuration", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/calendar/custom.js");
await helpers.getDocument();
});
it("should show the custom maximumEntries of 5", async () => {
await testElementLength(".calendar .event", 5);
});
it("should show the custom calendar symbol in four events", async () => {
await testElementLength(".calendar .event .fa-birthday-cake", 4);
});
it("should show a customEvent calendar symbol in one event", async () => {
await testElementLength(".calendar .event .fa-dice", 1);
});
it("should show two custom icons for repeating events", async () => {
await testElementLength(".calendar .event .fa-undo", 2);
});
it("should show two custom icons for day events", async () => {
await testElementLength(".calendar .event .fa-calendar-day", 2);
});
});
describe("Recurring event", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/calendar/recurring.js");
await helpers.getDocument();
});
it("should show the recurring birthday event 6 times", async () => {
await testElementLength(".calendar .event", 6);
});
});
process.setMaxListeners(0);
for (let i = -12; i < 12; i++) {
describe("Recurring event per timezone", () => {
beforeAll(async () => {
Date.prototype.getTimezoneOffset = () => {
return i * 60;
};
await helpers.startApplication("tests/configs/modules/calendar/recurring.js");
await helpers.getDocument();
});
it(`should contain text "Mar 25th" in timezone UTC ${-i}`, async () => {
await testTextContain(".calendar", "Mar 25th");
});
});
}
describe("Changed port", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/calendar/changed-port.js");
serverBasicAuth.listen(8010);
await helpers.getDocument();
});
afterAll(async () => {
await serverBasicAuth.close();
});
it("should return TestEvents", async () => {
await testElementLength(".calendar .event", 0, "not");
});
});
describe("Basic auth", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/calendar/basic-auth.js");
await helpers.getDocument();
});
it("should return TestEvents", async () => {
await testElementLength(".calendar .event", 0, "not");
});
});
describe("Basic auth by default", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/calendar/auth-default.js");
await helpers.getDocument();
});
it("should return TestEvents", async () => {
await testElementLength(".calendar .event", 0, "not");
});
});
describe("Basic auth backward compatibility configuration: DEPRECATED", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/calendar/old-basic-auth.js");
await helpers.getDocument();
});
it("should return TestEvents", async () => {
await testElementLength(".calendar .event", 0, "not");
});
});
describe("Fail Basic auth", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/calendar/fail-basic-auth.js");
serverBasicAuth.listen(8020);
await helpers.getDocument();
});
afterAll(async () => {
await serverBasicAuth.close();
});
it("should show Unauthorized error", async () => {
await testTextContain(".calendar", "Error in the calendar module. Authorization failed");
});
});
});
+65
View File
@@ -0,0 +1,65 @@
const helpers = require("../helpers/global-setup");
describe("Clock set to spanish language module", () => {
afterAll(async () => {
await helpers.stopApplication();
});
describe("with default 24hr clock config", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/clock/es/clock_24hr.js");
await helpers.getDocument();
});
it("shows date with correct format", async () => {
const dateRegex = /^(?:lunes|martes|miércoles|jueves|viernes|sábado|domingo), \d{1,2} de (?:enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre) de \d{4}$/;
await helpers.testMatch(".clock .date", dateRegex);
});
it("shows time in 24hr format", async () => {
const timeRegex = /^(?:2[0-3]|[01]\d):[0-5]\d[0-5]\d$/;
await helpers.testMatch(".clock .time", timeRegex);
});
});
describe("with default 12hr clock config", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/clock/es/clock_12hr.js");
await helpers.getDocument();
});
it("shows date with correct format", async () => {
const dateRegex = /^(?:lunes|martes|miércoles|jueves|viernes|sábado|domingo), \d{1,2} de (?:enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre) de \d{4}$/;
await helpers.testMatch(".clock .date", dateRegex);
});
it("shows time in 12hr format", async () => {
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[ap]m$/;
await helpers.testMatch(".clock .time", timeRegex);
});
});
describe("with showPeriodUpper config enabled", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/clock/es/clock_showPeriodUpper.js");
await helpers.getDocument();
});
it("shows 12hr time with upper case AM/PM", async () => {
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[AP]M$/;
await helpers.testMatch(".clock .time", timeRegex);
});
});
describe("with showWeek config enabled", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/clock/es/clock_showWeek.js");
await helpers.getDocument();
});
it("shows week with correct format", async () => {
const weekRegex = /^Semana [0-9]{1,2}$/;
await helpers.testMatch(".clock .week", weekRegex);
});
});
});
+124
View File
@@ -0,0 +1,124 @@
const moment = require("moment");
const helpers = require("../helpers/global-setup");
describe("Clock module", () => {
afterAll(async () => {
await helpers.stopApplication();
});
describe("with default 24hr clock config", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/clock/clock_24hr.js");
await helpers.getDocument();
});
it("should show the date in the correct format", async () => {
const dateRegex = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (?:January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}, \d{4}$/;
await helpers.testMatch(".clock .date", dateRegex);
});
it("should show the time in 24hr format", async () => {
const timeRegex = /^(?:2[0-3]|[01]\d):[0-5]\d[0-5]\d$/;
await helpers.testMatch(".clock .time", timeRegex);
});
});
describe("with default 12hr clock config", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/clock/clock_12hr.js");
await helpers.getDocument();
});
it("should show the date in the correct format", async () => {
const dateRegex = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (?:January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}, \d{4}$/;
await helpers.testMatch(".clock .date", dateRegex);
});
it("should show the time in 12hr format", async () => {
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[ap]m$/;
await helpers.testMatch(".clock .time", timeRegex);
});
});
describe("with showPeriodUpper config enabled", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/clock/clock_showPeriodUpper.js");
await helpers.getDocument();
});
it("should show 12hr time with upper case AM/PM", async () => {
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[AP]M$/;
await helpers.testMatch(".clock .time", timeRegex);
});
});
describe("with displaySeconds config disabled", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/clock/clock_displaySeconds_false.js");
await helpers.getDocument();
});
it("should show 12hr time without seconds am/pm", async () => {
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[ap]m$/;
await helpers.testMatch(".clock .time", timeRegex);
});
});
describe("with showTime config disabled", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/clock/clock_showTime.js");
await helpers.getDocument();
});
it("should not show the time when digital clock is shown", async () => {
const elem = await document.querySelector(".clock .digital .time");
expect(elem).toBe(null);
});
});
describe("with showWeek config enabled", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/clock/clock_showWeek.js");
await helpers.getDocument();
});
it("should show the week in the correct format", async () => {
const weekRegex = /^Week [0-9]{1,2}$/;
await helpers.testMatch(".clock .week", weekRegex);
});
it("should show the week with the correct number of week of year", async () => {
const currentWeekNumber = moment().week();
const weekToShow = `Week ${currentWeekNumber}`;
const elem = await helpers.waitForElement(".clock .week");
expect(elem).not.toBe(null);
expect(elem.textContent).toBe(weekToShow);
});
});
describe("with analog clock face enabled", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/clock/clock_analog.js");
await helpers.getDocument();
});
it("should show the analog clock face", async () => {
const elem = helpers.waitForElement(".clock-circle");
expect(elem).not.toBe(null);
});
});
describe("with analog clock face and date enabled", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/clock/clock_showDateAnalog.js");
await helpers.getDocument();
});
it("should show the analog clock face and the date", async () => {
const elemClock = helpers.waitForElement(".clock-circle");
await expect(elemClock).not.toBe(null);
const elemDate = helpers.waitForElement(".clock .date");
await expect(elemDate).not.toBe(null);
});
});
});
+54
View File
@@ -0,0 +1,54 @@
const helpers = require("../helpers/global-setup");
describe("Compliments module", () => {
/**
* move similar tests in function doTest
* @param {Array} complimentsArray The array of compliments.
*/
const doTest = async (complimentsArray) => {
let elem = await helpers.waitForElement(".compliments");
expect(elem).not.toBe(null);
elem = await helpers.waitForElement(".module-content");
expect(elem).not.toBe(null);
expect(complimentsArray).toContain(elem.textContent);
};
afterAll(async () => {
await helpers.stopApplication();
});
describe("Feature anytime in compliments module", () => {
describe("Set anytime and empty compliments for morning, evening and afternoon ", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/compliments/compliments_anytime.js");
await helpers.getDocument();
});
it("shows anytime because if configure empty parts of day compliments and set anytime compliments", async () => {
await doTest(["Anytime here"]);
});
});
describe("Only anytime present in configuration compliments", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/compliments/compliments_only_anytime.js");
await helpers.getDocument();
});
it("shows anytime compliments", async () => {
await doTest(["Anytime here"]);
});
});
});
describe("remoteFile option", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/compliments/compliments_remote.js");
await helpers.getDocument();
});
it("should show compliments from a remote file", async () => {
await doTest(["Remote compliment file works!"]);
});
});
});
+33
View File
@@ -0,0 +1,33 @@
const helpers = require("../helpers/global-setup");
describe("Test helloworld module", () => {
afterAll(async () => {
await helpers.stopApplication();
});
describe("helloworld set config text", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/helloworld/helloworld.js");
await helpers.getDocument();
});
it("Test message helloworld module", async () => {
const elem = await helpers.waitForElement(".helloworld");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("Test HelloWorld Module");
});
});
describe("helloworld default config text", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/helloworld/helloworld_default.js");
await helpers.getDocument();
});
it("Test message helloworld module", async () => {
const elem = await helpers.waitForElement(".helloworld");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("Hello World!");
});
});
});
+77
View File
@@ -0,0 +1,77 @@
const helpers = require("../helpers/global-setup");
describe("Newsfeed module", () => {
afterAll(async () => {
await helpers.stopApplication();
});
describe("Default configuration", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/newsfeed/default.js");
await helpers.getDocument();
});
it("should show the newsfeed title", async () => {
const elem = await helpers.waitForElement(".newsfeed .newsfeed-source");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("Rodrigo Ramirez Blog");
});
it("should show the newsfeed article", async () => {
const elem = await helpers.waitForElement(".newsfeed .newsfeed-title");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("QPanel");
});
it("should NOT show the newsfeed description", async () => {
await helpers.waitForElement(".newsfeed");
const element = document.querySelector(".newsfeed .newsfeed-desc");
expect(element).toBe(null);
});
});
describe("Custom configuration", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/newsfeed/prohibited_words.js");
await helpers.getDocument();
});
it("should not show articles with prohibited words", async () => {
const elem = await helpers.waitForElement(".newsfeed .newsfeed-title");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("Problema VirtualBox");
});
it("should show the newsfeed description", async () => {
const elem = await helpers.waitForElement(".newsfeed .newsfeed-desc");
expect(elem).not.toBe(null);
expect(elem.textContent.length).not.toBe(0);
});
});
describe("Invalid configuration", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/newsfeed/incorrect_url.js");
await helpers.getDocument();
});
it("should show malformed url warning", async () => {
const elem = await helpers.waitForElement(".newsfeed .small", "No news at the moment.");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("Error in the Newsfeed module. Malformed url.");
});
});
describe("Ignore items", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/newsfeed/ignore_items.js");
await helpers.getDocument();
});
it("should show empty items info message", async () => {
const elem = await helpers.waitForElement(".newsfeed .small");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("No news at the moment.");
});
});
});
+84
View File
@@ -0,0 +1,84 @@
const helpers = require("../helpers/global-setup");
const weatherFunc = require("../helpers/weather-functions");
describe("Weather module", () => {
afterAll(async () => {
await helpers.stopApplication();
});
describe("Current weather", () => {
describe("Default configuration", () => {
beforeAll(async () => {
await weatherFunc.startApp("tests/configs/modules/weather/currentweather_default.js", {});
});
it("should render wind speed and wind direction", async () => {
await weatherFunc.getText(".weather .normal.medium span:nth-child(2)", "12 WSW");
});
it("should render temperature with icon", async () => {
await weatherFunc.getText(".weather .large.light span.bright", "1.5°");
});
it("should render feels like temperature", async () => {
await weatherFunc.getText(".weather .normal.medium.feelslike span.dimmed", "Feels like -5.6°");
});
});
});
describe("Compliments Integration", () => {
beforeAll(async () => {
await weatherFunc.startApp("tests/configs/modules/weather/currentweather_compliments.js", {});
});
it("should render a compliment based on the current weather", async () => {
await weatherFunc.getText(".compliments .module-content span", "snow");
});
});
describe("Configuration Options", () => {
beforeAll(async () => {
await weatherFunc.startApp("tests/configs/modules/weather/currentweather_options.js", {});
});
it("should render windUnits in beaufort", async () => {
await weatherFunc.getText(".weather .normal.medium span:nth-child(2)", "6");
});
it("should render windDirection with an arrow", async () => {
const elem = await helpers.waitForElement(".weather .normal.medium sup i.fa-long-arrow-alt-down");
expect(elem).not.toBe(null);
expect(elem.outerHTML).toContain("transform:rotate(250deg);");
});
it("should render humidity", async () => {
await weatherFunc.getText(".weather .normal.medium span:nth-child(3)", "93.7");
});
it("should render degreeLabel for temp", async () => {
await weatherFunc.getText(".weather .large.light span.bright", "1°C");
});
it("should render degreeLabel for feels like", async () => {
await weatherFunc.getText(".weather .normal.medium.feelslike span.dimmed", "Feels like -6°C");
});
});
describe("Current weather with imperial units", () => {
beforeAll(async () => {
await weatherFunc.startApp("tests/configs/modules/weather/currentweather_units.js", {});
});
it("should render wind in imperial units", async () => {
await weatherFunc.getText(".weather .normal.medium span:nth-child(2)", "26 WSW");
});
it("should render temperatures in fahrenheit", async () => {
await weatherFunc.getText(".weather .large.light span.bright", "34,7°");
});
it("should render 'feels like' in fahrenheit", async () => {
await weatherFunc.getText(".weather .normal.medium.feelslike span.dimmed", "Feels like 21,9°");
});
});
});
+118
View File
@@ -0,0 +1,118 @@
const helpers = require("../helpers/global-setup");
const weatherFunc = require("../helpers/weather-functions");
describe("Weather module: Weather Forecast", () => {
afterAll(async () => {
await helpers.stopApplication();
});
describe("Default configuration", () => {
beforeAll(async () => {
await weatherFunc.startApp("tests/configs/modules/weather/forecastweather_default.js", {});
});
const days = ["Today", "Tomorrow", "Sun", "Mon", "Tue"];
for (const [index, day] of days.entries()) {
it(`should render day ${day}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day);
});
}
const icons = ["day-cloudy", "rain", "day-sunny", "day-sunny", "day-sunny"];
for (const [index, icon] of icons.entries()) {
it(`should render icon ${icon}`, async () => {
const elem = await helpers.waitForElement(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(2) span.wi-${icon}`);
expect(elem).not.toBe(null);
});
}
const maxTemps = ["24.4°", "21.0°", "22.9°", "23.4°", "20.6°"];
for (const [index, temp] of maxTemps.entries()) {
it(`should render max temperature ${temp}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`, temp);
});
}
const minTemps = ["15.3°", "13.6°", "13.8°", "13.9°", "10.9°"];
for (const [index, temp] of minTemps.entries()) {
it(`should render min temperature ${temp}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(4)`, temp);
});
}
const opacities = [1, 1, 0.8, 0.5333333333333333, 0.2666666666666667];
for (const [index, opacity] of opacities.entries()) {
it(`should render fading of rows with opacity=${opacity}`, async () => {
const elem = await helpers.waitForElement(`.weather table.small tr:nth-child(${index + 1})`);
expect(elem).not.toBe(null);
expect(elem.outerHTML).toContain(`<tr style="opacity: ${opacity};">`);
});
}
});
describe("Absolute configuration", () => {
beforeAll(async () => {
await weatherFunc.startApp("tests/configs/modules/weather/forecastweather_absolute.js", {});
});
const days = ["Fri", "Sat", "Sun", "Mon", "Tue"];
for (const [index, day] of days.entries()) {
it(`should render day ${day}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day);
});
}
});
describe("Configuration Options", () => {
beforeAll(async () => {
await weatherFunc.startApp("tests/configs/modules/weather/forecastweather_options.js", {});
});
it("should render custom table class", async () => {
const elem = await helpers.waitForElement(".weather table.myTableClass");
expect(elem).not.toBe(null);
});
it("should render colored rows", async () => {
const table = await helpers.waitForElement(".weather table.myTableClass");
expect(table).not.toBe(null);
expect(table.rows).not.toBe(null);
expect(table.rows.length).toBe(5);
});
const precipitations = [undefined, "2.51 mm"];
for (const [index, precipitation] of precipitations.entries()) {
if (precipitation) {
it(`should render precipitation amount ${precipitation}`, async () => {
await weatherFunc.getText(`.weather table tr:nth-child(${index + 1}) td.precipitation-amount`, precipitation);
});
}
}
});
describe("Forecast weather with imperial units", () => {
beforeAll(async () => {
await weatherFunc.startApp("tests/configs/modules/weather/forecastweather_units.js", {});
});
describe("Temperature units", () => {
const temperatures = ["75_9°", "69_8°", "73_2°", "74_1°", "69_1°"];
for (const [index, temp] of temperatures.entries()) {
it(`should render custom decimalSymbol = '_' for temp ${temp}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`, temp);
});
}
});
describe("Precipitation units", () => {
const precipitations = [undefined, "0.10 in"];
for (const [index, precipitation] of precipitations.entries()) {
if (precipitation) {
it(`should render precipitation amount ${precipitation}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-amount`, precipitation);
});
}
}
});
});
});
+64
View File
@@ -0,0 +1,64 @@
const helpers = require("../helpers/global-setup");
const weatherFunc = require("../helpers/weather-functions");
describe("Weather module: Weather Hourly Forecast", () => {
afterAll(async () => {
await helpers.stopApplication();
});
describe("Default configuration", () => {
beforeAll(async () => {
await weatherFunc.startApp("tests/configs/modules/weather/hourlyweather_default.js", {});
});
const minTemps = ["7:00 pm", "8:00 pm", "9:00 pm", "10:00 pm", "11:00 pm"];
for (const [index, hour] of minTemps.entries()) {
it(`should render forecast for hour ${hour}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td.day`, hour);
});
}
});
describe("Hourly weather options", () => {
beforeAll(async () => {
await weatherFunc.startApp("tests/configs/modules/weather/hourlyweather_options.js", {});
});
describe("Hourly increments of 2", () => {
const minTemps = ["7:00 pm", "9:00 pm", "11:00 pm", "1:00 am", "3:00 am"];
for (const [index, hour] of minTemps.entries()) {
it(`should render forecast for hour ${hour}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td.day`, hour);
});
}
});
});
describe("Show precipitations", () => {
beforeAll(async () => {
await weatherFunc.startApp("tests/configs/modules/weather/hourlyweather_showPrecipitation.js", {});
});
describe("Shows precipitation amount", () => {
const amounts = [undefined, undefined, undefined, "0.13 mm", "0.13 mm"];
for (const [index, amount] of amounts.entries()) {
if (amount) {
it(`should render precipitation amount ${amount}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-amount`, amount);
});
}
}
});
describe("Shows precipitation probability", () => {
const propabilities = [undefined, undefined, "12 %", "36 %", "44 %"];
for (const [index, pop] of propabilities.entries()) {
if (pop) {
it(`should render probability ${pop}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-prob`, pop);
});
}
}
});
});
});
+24
View File
@@ -0,0 +1,24 @@
const helpers = require("./helpers/global-setup");
describe("Display of modules", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/display.js");
await helpers.getDocument();
});
afterAll(async () => {
await helpers.stopApplication();
});
it("should show the test header", async () => {
const elem = await helpers.waitForElement("#module_0_helloworld .module-header");
expect(elem).not.toBe(null);
// textContent gibt hier lowercase zurück, das uppercase wird durch css realisiert, was daher nicht in textContent landet
expect(elem.textContent).toBe("test_header");
});
it("should show no header if no header text is specified", async () => {
const elem = await helpers.waitForElement("#module_1_helloworld .module-header");
expect(elem).not.toBe(null);
expect(elem.textContent).toBe("undefined");
});
});
+23
View File
@@ -0,0 +1,23 @@
const helpers = require("./helpers/global-setup");
describe("Check configuration without modules", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/without_modules.js");
await helpers.getDocument();
});
afterAll(async () => {
await helpers.stopApplication();
});
it("shows the message MagicMirror² title", async () => {
const elem = await helpers.waitForElement("#module_1_helloworld .module-content");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("MagicMirror²");
});
it("shows the url of michael's website", async () => {
const elem = await helpers.waitForElement("#module_5_helloworld .module-content");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("www.michaelteeuw.nl");
});
});
+22
View File
@@ -0,0 +1,22 @@
const helpers = require("./helpers/global-setup");
describe("Position of modules", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/positions.js");
await helpers.getDocument();
});
afterAll(async () => {
await helpers.stopApplication();
});
const positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
for (const position of positions) {
const className = position.replace("_", ".");
it(`should show text in ${position}`, async () => {
const elem = await helpers.waitForElement(`.${className}`);
expect(elem).not.toBe(null);
expect(elem.textContent).toContain(`Text in ${position}`);
});
}
});
+31
View File
@@ -0,0 +1,31 @@
const helpers = require("./helpers/global-setup");
describe("port directive configuration", () => {
describe("Set port 8090", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/port_8090.js");
});
afterAll(async () => {
await helpers.stopApplication();
});
it("should return 200", async () => {
const res = await helpers.fetch("http://localhost:8090");
expect(res.status).toBe(200);
});
});
describe("Set port 8100 on environment variable MM_PORT", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/port_8090.js", (process.env.MM_PORT = 8100));
});
afterAll(async () => {
await helpers.stopApplication();
});
it("should return 200", async () => {
const res = await helpers.fetch("http://localhost:8100");
expect(res.status).toBe(200);
});
});
});
+28
View File
@@ -0,0 +1,28 @@
const helpers = require("./helpers/global-setup");
const delay = (time) => {
return new Promise((resolve) => setTimeout(resolve, time));
};
describe("App environment", () => {
let serverProcess;
beforeAll(async () => {
process.env.MM_CONFIG_FILE = "tests/configs/default.js";
serverProcess = await require("child_process").spawn("npm", ["run", "server"], { env: process.env, detached: true });
// we have to wait until the server is startet
await delay(2000);
});
afterAll(async () => {
await process.kill(-serverProcess.pid);
});
it("get request from http://localhost:8080 should return 200", async () => {
const res = await helpers.fetch("http://localhost:8080");
expect(res.status).toBe(200);
});
it("get request from http://localhost:8080/nothing should return 404", async () => {
const res = await helpers.fetch("http://localhost:8080/nothing");
expect(res.status).toBe(404);
});
});
+15
View File
@@ -0,0 +1,15 @@
const helpers = require("./helpers/global-setup");
describe("templated config with port variable", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/port_variable.js");
});
afterAll(async () => {
await helpers.stopApplication();
});
it("should return 200", async () => {
const res = await helpers.fetch("http://localhost:8090");
expect(res.status).toBe(200);
});
});
+218
View File
@@ -0,0 +1,218 @@
const fs = require("fs");
const path = require("path");
const helmet = require("helmet");
const { JSDOM } = require("jsdom");
const express = require("express");
const sinon = require("sinon");
const translations = require("../../translations/translations");
describe("Translations", () => {
let server;
beforeAll(() => {
const app = express();
app.use(helmet());
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
next();
});
app.use("/translations", express.static(path.join(__dirname, "..", "..", "translations")));
server = app.listen(3000);
});
afterAll(async () => {
await server.close();
});
it("should have a translation file in the specified path", () => {
for (let language in translations) {
const file = fs.statSync(translations[language]);
expect(file.isFile()).toBe(true);
}
});
describe("loadTranslations", () => {
let dom;
beforeEach(() => {
dom = new JSDOM(
`<script>var Translator = {}; var Log = {log: () => {}}; var config = {language: 'de'};</script>\
<script src="file://${path.join(__dirname, "..", "..", "js", "class.js")}"></script>\
<script src="file://${path.join(__dirname, "..", "..", "js", "module.js")}"></script>`,
{ runScripts: "dangerously", resources: "usable" }
);
});
it("should load translation file", (done) => {
dom.window.onload = async () => {
const { Translator, Module, config } = dom.window;
config.language = "en";
Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null);
Module.register("name", { getTranslations: () => translations });
const MMM = Module.create("name");
await MMM.loadTranslations();
expect(Translator.load.args.length).toBe(1);
expect(Translator.load.calledWith(MMM, "translations/en.json", false)).toBe(true);
done();
};
});
it("should load translation + fallback file", (done) => {
dom.window.onload = async () => {
const { Translator, Module } = dom.window;
Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null);
Module.register("name", { getTranslations: () => translations });
const MMM = Module.create("name");
await MMM.loadTranslations();
expect(Translator.load.args.length).toBe(2);
expect(Translator.load.calledWith(MMM, "translations/de.json", false)).toBe(true);
expect(Translator.load.calledWith(MMM, "translations/en.json", true)).toBe(true);
done();
};
});
it("should load translation fallback file", (done) => {
dom.window.onload = async () => {
const { Translator, Module, config } = dom.window;
config.language = "--";
Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null);
Module.register("name", { getTranslations: () => translations });
const MMM = Module.create("name");
await MMM.loadTranslations();
expect(Translator.load.args.length).toBe(1);
expect(Translator.load.calledWith(MMM, "translations/en.json", true)).toBe(true);
done();
};
});
it("should load no file", (done) => {
dom.window.onload = async () => {
const { Translator, Module } = dom.window;
Translator.load = sinon.stub();
Module.register("name", {});
const MMM = Module.create("name");
await MMM.loadTranslations();
expect(Translator.load.callCount).toBe(0);
done();
};
});
});
const mmm = {
name: "TranslationTest",
file(file) {
return `http://localhost:3000/${file}`;
}
};
describe("Parsing language files through the Translator class", () => {
for (let language in translations) {
it(`should parse ${language}`, (done) => {
const dom = new JSDOM(
`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: () => {}};</script>\
<script src="file://${path.join(__dirname, "..", "..", "js", "translator.js")}">`,
{ runScripts: "dangerously", resources: "usable" }
);
dom.window.onload = async () => {
const { Translator } = dom.window;
await Translator.load(mmm, translations[language], false);
expect(typeof Translator.translations[mmm.name]).toBe("object");
expect(Object.keys(Translator.translations[mmm.name]).length).toBeGreaterThanOrEqual(1);
done();
};
});
}
});
describe("Same keys", () => {
let base;
let missing = [];
beforeAll((done) => {
const dom = new JSDOM(
`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: () => {}};</script>\
<script src="file://${path.join(__dirname, "..", "..", "js", "translator.js")}">`,
{ runScripts: "dangerously", resources: "usable" }
);
dom.window.onload = async () => {
const { Translator } = dom.window;
await Translator.load(mmm, translations.de, false);
base = Object.keys(Translator.translations[mmm.name]).sort();
done();
};
});
afterAll(() => {
console.log(missing);
});
// Using German as the base rather than English, since
// at least one translated word doesn't exist in English.
for (let language in translations) {
if (language === "de") {
continue;
}
describe(`Translation keys of ${language}`, () => {
let keys;
beforeAll((done) => {
const dom = new JSDOM(
`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: () => {}};</script>\
<script src="file://${path.join(__dirname, "..", "..", "js", "translator.js")}">`,
{ runScripts: "dangerously", resources: "usable" }
);
dom.window.onload = async () => {
const { Translator } = dom.window;
await Translator.load(mmm, translations[language], false);
keys = Object.keys(Translator.translations[mmm.name]).sort();
done();
};
});
it(`${language} keys should be in base`, () => {
keys.forEach((key) => {
expect(base.indexOf(key)).toBeGreaterThanOrEqual(0);
});
});
it(`${language} should contain all base keys`, () => {
// TODO: when all translations are fixed, use
// expect(keys).toEqual(base);
// instead of the try-catch-block
try {
expect(keys).toEqual(base);
} catch (e) {
if (e.message.match(/expect.*toEqual/)) {
const diff = base.filter((key) => !keys.includes(key));
missing.push(`Missing Translations for language ${language}: ${diff}`);
} else {
throw e;
}
}
});
});
}
});
});
+30
View File
@@ -0,0 +1,30 @@
const helpers = require("./helpers/global-setup");
describe("Vendors", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/default.js");
});
afterAll(async () => {
await helpers.stopApplication();
});
describe("Get list vendors", () => {
const vendors = require(`${__dirname}/../../vendor/vendor.js`);
Object.keys(vendors).forEach((vendor) => {
it(`should return 200 HTTP code for vendor "${vendor}"`, async () => {
const urlVendor = `http://localhost:8080/vendor/${vendors[vendor]}`;
const res = await helpers.fetch(urlVendor);
expect(res.status).toBe(200);
});
});
Object.keys(vendors).forEach((vendor) => {
it(`should return 404 HTTP code for vendor https://localhost/"${vendor}"`, async () => {
const urlVendor = `http://localhost:8080/${vendors[vendor]}`;
const res = await helpers.fetch(urlVendor);
expect(res.status).toBe(404);
});
});
});
});
+37
View File
@@ -0,0 +1,37 @@
const events = require("events");
const helpers = require("./helpers/global-setup");
describe("Electron app environment", () => {
beforeEach(async () => {
await helpers.startApplication("tests/configs/modules/display.js");
});
afterEach(async () => {
await helpers.stopApplication();
});
it("should open browserwindow", async () => {
const module = await helpers.getElement("#module_0_helloworld");
expect(await module.textContent()).toContain("Test Display Header");
expect(global.electronApp.windows().length).toBe(1);
});
});
describe("Development console tests", () => {
beforeEach(async () => {
await helpers.startApplication("tests/configs/modules/display.js", null, ["js/electron.js", "dev"]);
});
afterEach(async () => {
await helpers.stopApplication();
});
it("should open browserwindow and dev console", async () => {
while (global.electronApp.windows().length < 2) await events.once(global.electronApp, "window");
const pageArray = await global.electronApp.windows();
expect(pageArray.length).toBe(2);
for (const page of pageArray) {
expect(["MagicMirror²", "DevTools"]).toContain(await page.title());
}
});
});
+45
View File
@@ -0,0 +1,45 @@
// see https://playwright.dev/docs/api/class-electronapplication
// https://github.com/microsoft/playwright/issues/6347#issuecomment-1085850728
// https://www.anycodings.com/1questions/958135/can-i-set-the-date-for-playwright-browser
const { _electron: electron } = require("playwright");
exports.startApplication = async (configFilename, systemDate = null, electronParams = ["js/electron.js"]) => {
global.electronApp = null;
global.page = null;
process.env.MM_CONFIG_FILE = configFilename;
process.env.TZ = "GMT";
global.electronApp = await electron.launch({ args: electronParams });
await global.electronApp.firstWindow();
for (const win of global.electronApp.windows()) {
const title = await win.title();
expect(["MagicMirror²", "DevTools"]).toContain(title);
if (title === "MagicMirror²") {
global.page = win;
if (systemDate) {
await global.page.evaluate((systemDate) => {
Date.now = () => {
return new Date(systemDate);
};
}, systemDate);
}
}
}
};
exports.stopApplication = async () => {
if (global.electronApp) {
await global.electronApp.close();
}
global.electronApp = null;
global.page = null;
};
exports.getElement = async (selector) => {
expect(global.page);
let elem = global.page.locator(selector);
await elem.waitFor();
expect(elem).not.toBe(null);
return elem;
};
+19
View File
@@ -0,0 +1,19 @@
const { injectMockData } = require("../../utils/weather_mocker");
const helpers = require("./global-setup");
exports.getText = async (element, result) => {
const elem = await helpers.getElement(element);
await expect(elem).not.toBe(null);
const text = await elem.textContent();
await expect(
text
.trim()
.replace(/(\r\n|\n|\r)/gm, "")
.replace(/[ ]+/g, " ")
).toBe(result);
};
exports.startApp = async (configFileNameName, systemDate) => {
injectMockData(configFileNameName);
await helpers.startApplication("", systemDate);
};
+43
View File
@@ -0,0 +1,43 @@
const helpers = require("../helpers/global-setup");
describe("Calendar module", () => {
/**
* move similar tests in function doTest
* @param {string} cssClass css selector
*/
const doTest = async (cssClass) => {
let elem = await helpers.getElement(`.calendar .module-content .event${cssClass}`);
expect(await elem.isVisible()).toBe(true);
};
afterEach(async () => {
await helpers.stopApplication();
});
describe("Test css classes", () => {
it("has css class dayBeforeYesterday", async () => {
await helpers.startApplication("tests/configs/modules/calendar/custom.js", "03 Jan 2030 12:30:00 GMT");
await doTest(".dayBeforeYesterday");
});
it("has css class yesterday", async () => {
await helpers.startApplication("tests/configs/modules/calendar/custom.js", "02 Jan 2030 12:30:00 GMT");
await doTest(".yesterday");
});
it("has css class today", async () => {
await helpers.startApplication("tests/configs/modules/calendar/custom.js", "01 Jan 2030 12:30:00 GMT");
await doTest(".today");
});
it("has css class tomorrow", async () => {
await helpers.startApplication("tests/configs/modules/calendar/custom.js", "31 Dec 2029 12:30:00 GMT");
await doTest(".tomorrow");
});
it("has css class dayAfterTomorrow", async () => {
await helpers.startApplication("tests/configs/modules/calendar/custom.js", "30 Dec 2029 12:30:00 GMT");
await doTest(".dayAfterTomorrow");
});
});
});
@@ -0,0 +1,44 @@
const helpers = require("../helpers/global-setup");
describe("Compliments module", () => {
/**
* move similar tests in function doTest
* @param {Array} complimentsArray The array of compliments.
*/
const doTest = async (complimentsArray) => {
await helpers.getElement(".compliments");
const elem = await helpers.getElement(".module-content");
expect(elem).not.toBe(null);
expect(complimentsArray).toContain(await elem.textContent());
};
afterEach(async () => {
await helpers.stopApplication();
});
describe("parts of days", () => {
it("Morning compliments for that part of day", async () => {
await helpers.startApplication("tests/configs/modules/compliments/compliments_parts_day.js", "01 Oct 2022 10:00:00 GMT");
await doTest(["Hi", "Good Morning", "Morning test"]);
});
it("Afternoon show Compliments for that part of day", async () => {
await helpers.startApplication("tests/configs/modules/compliments/compliments_parts_day.js", "01 Oct 2022 15:00:00 GMT");
await doTest(["Hello", "Good Afternoon", "Afternoon test"]);
});
it("Evening show Compliments for that part of day", async () => {
await helpers.startApplication("tests/configs/modules/compliments/compliments_parts_day.js", "01 Oct 2022 20:00:00 GMT");
await doTest(["Hello There", "Good Evening", "Evening test"]);
});
});
describe("Feature date in compliments module", () => {
describe("Set date and empty compliments for anytime, morning, evening and afternoon", () => {
it("shows happy new year compliment on new years day", async () => {
await helpers.startApplication("tests/configs/modules/compliments/compliments_date.js", "01 Jan 2022 10:00:00 GMT");
await doTest(["Happy new year!"]);
});
});
});
});
+28
View File
@@ -0,0 +1,28 @@
const helpers = require("../helpers/global-setup");
const weatherHelper = require("../helpers/weather-setup");
describe("Weather module", () => {
afterEach(async () => {
await helpers.stopApplication();
});
describe("Current weather with sunrise", () => {
beforeAll(async () => {
await weatherHelper.startApp("tests/configs/modules/weather/currentweather_default.js", "13 Jan 2019 00:30:00 GMT");
});
it("should render sunrise", async () => {
await weatherHelper.getText(".weather .normal.medium span:nth-child(4)", "7:00 am");
});
});
describe("Current weather with sunset", () => {
beforeAll(async () => {
await weatherHelper.startApp("tests/configs/modules/weather/currentweather_default.js", "13 Jan 2019 12:30:00 GMT");
});
it("should render sunset", async () => {
await weatherHelper.getText(".weather .normal.medium span:nth-child(4)", "3:45 pm");
});
});
});
+190
View File
@@ -0,0 +1,190 @@
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:MagicMirrorTest
X-WR-TIMEZONE:America/Santiago
X-WR-CALDESC:Testing propose MagicMirror
BEGIN:VTIMEZONE
TZID:America/Santiago
X-LIC-LOCATION:America/Santiago
BEGIN:STANDARD
TZOFFSETFROM:-0300
TZOFFSETTO:-0400
TZNAME:-04
DTSTART:19700510T000000
RDATE:19700510T030000
RDATE:19710509T030000
RDATE:19720514T030000
RDATE:19730513T030000
RDATE:19740512T030000
RDATE:19750511T030000
RDATE:19760509T030000
RDATE:19770515T030000
RDATE:19780514T030000
RDATE:19790513T030000
RDATE:19800511T030000
RDATE:19810510T030000
RDATE:19820509T030000
RDATE:19830515T030000
RDATE:19840513T030000
RDATE:19850512T030000
RDATE:19860511T030000
RDATE:19870510T030000
RDATE:19880515T030000
RDATE:19890514T030000
RDATE:19900513T030000
RDATE:19910512T030000
RDATE:19920510T030000
RDATE:19930509T030000
RDATE:19940515T030000
RDATE:19950514T030000
RDATE:19960512T030000
RDATE:19970511T030000
RDATE:19980510T030000
RDATE:19990509T030000
RDATE:20000514T030000
RDATE:20010513T030000
RDATE:20020512T030000
RDATE:20030511T030000
RDATE:20040509T030000
RDATE:20050515T030000
RDATE:20060514T030000
RDATE:20070513T030000
RDATE:20080511T030000
RDATE:20090510T030000
RDATE:20100509T030000
RDATE:20110515T030000
RDATE:20120513T030000
RDATE:20130512T030000
RDATE:20140511T030000
RDATE:20150510T030000
RDATE:20160515T030000
RDATE:20170514T030000
RDATE:20180513T030000
RDATE:20190512T030000
RDATE:20200510T030000
RDATE:20210509T030000
RDATE:20220515T030000
RDATE:20230514T030000
RDATE:20240512T030000
RDATE:20250511T030000
RDATE:20260510T030000
RDATE:20270509T030000
RDATE:20280514T030000
RDATE:20290513T030000
RDATE:20300512T030000
RDATE:20310511T030000
RDATE:20320509T030000
RDATE:20330515T030000
RDATE:20340514T030000
RDATE:20350513T030000
RDATE:20360511T030000
RDATE:20370510T030000
END:STANDARD
BEGIN:STANDARD
TZOFFSETFROM:-0300
TZOFFSETTO:-0400
TZNAME:-04
DTSTART:20380509T000000
RRULE:FREQ=YEARLY;BYMONTH=5;BYDAY=2SU
END:STANDARD
BEGIN:DAYLIGHT
TZOFFSETFROM:-0400
TZOFFSETTO:-0300
TZNAME:-03
DTSTART:19700809T000000
RDATE:19700809T040000
RDATE:19710815T040000
RDATE:19720813T040000
RDATE:19730812T040000
RDATE:19740811T040000
RDATE:19750810T040000
RDATE:19760815T040000
RDATE:19770814T040000
RDATE:19780813T040000
RDATE:19790812T040000
RDATE:19800810T040000
RDATE:19810809T040000
RDATE:19820815T040000
RDATE:19830814T040000
RDATE:19840812T040000
RDATE:19850811T040000
RDATE:19860810T040000
RDATE:19870809T040000
RDATE:19880814T040000
RDATE:19890813T040000
RDATE:19900812T040000
RDATE:19910811T040000
RDATE:19920809T040000
RDATE:19930815T040000
RDATE:19940814T040000
RDATE:19950813T040000
RDATE:19960811T040000
RDATE:19970810T040000
RDATE:19980809T040000
RDATE:19990815T040000
RDATE:20000813T040000
RDATE:20010812T040000
RDATE:20020811T040000
RDATE:20030810T040000
RDATE:20040815T040000
RDATE:20050814T040000
RDATE:20060813T040000
RDATE:20070812T040000
RDATE:20080810T040000
RDATE:20090809T040000
RDATE:20100815T040000
RDATE:20110814T040000
RDATE:20120812T040000
RDATE:20130811T040000
RDATE:20140810T040000
RDATE:20150809T040000
RDATE:20160814T040000
RDATE:20170813T040000
RDATE:20180812T040000
RDATE:20190811T040000
RDATE:20200809T040000
RDATE:20210815T040000
RDATE:20220814T040000
RDATE:20230813T040000
RDATE:20240811T040000
RDATE:20250810T040000
RDATE:20260809T040000
RDATE:20270815T040000
RDATE:20280813T040000
RDATE:20290812T040000
RDATE:20300811T040000
RDATE:20310810T040000
RDATE:20320815T040000
RDATE:20330814T040000
RDATE:20340813T040000
RDATE:20350812T040000
RDATE:20360810T040000
RDATE:20370809T040000
END:DAYLIGHT
BEGIN:DAYLIGHT
TZOFFSETFROM:-0400
TZOFFSETTO:-0300
TZNAME:-03
DTSTART:20380815T000000
RRULE:FREQ=YEARLY;BYMONTH=8;BYDAY=2SU
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=America/Santiago:20170309T100000
DTEND;TZID=America/Santiago:20170309T110000
RRULE:FREQ=MONTHLY;INTERVAL=30;BYMONTHDAY=9
DTSTAMP:20170310T172720Z
UID:80rl9kuu5bq49gme99eklov27k@google.com
CREATED:20170310T172400Z
DESCRIPTION:
LAST-MODIFIED:20170310T172400Z
LOCATION:
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:TestEvent
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR
+63
View File
@@ -0,0 +1,63 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//ical.marudot.com//iCal Event Maker
X-WR-CALNAME:TestEvents
NAME:TestEvents
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
TZURL:http://tzurl.org/zoneinfo-outlook/Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTAMP:20200719T094531Z
UID:20200719T094531Z-1871115387@marudot.com
DTSTART;TZID=Europe/Berlin:20300101T120000
DTEND;TZID=Europe/Berlin:20300101T130000
SUMMARY:TestEvent
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20200719T094531Z
UID:20200719T094531Z-1929725136@marudot.com
DTSTART;TZID=Europe/Berlin:20300701T120000
RRULE:FREQ=YEARLY;BYMONTH=7;BYMONTHDAY=1
DTEND;TZID=Europe/Berlin:20300701T130000
SUMMARY:TestEventRepeat
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20200719T094531Z
UID:20200719T094531Z-371801474@marudot.com
DTSTART;VALUE=DATE:20300401
DTEND;VALUE=DATE:20300402
SUMMARY:TestEventDay
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20200719T094531Z
UID:20200719T094531Z-133401084@marudot.com
DTSTART;VALUE=DATE:20301001
RRULE:FREQ=YEARLY;BYMONTH=10;BYMONTHDAY=1
DTEND;VALUE=DATE:20301002
SUMMARY:TestEventRepeatDay
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20200721T094531Z
UID:20200719T094531Z-167389794@marudot.com
DTSTART;TZID=Europe/Berlin:20301112T120000
DTEND;TZID=Europe/Berlin:20301112T130000
SUMMARY:TestEventCustomEventIcon
END:VEVENT
END:VCALENDAR
+37
View File
@@ -0,0 +1,37 @@
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:xxx@gmail.com
X-WR-TIMEZONE:Europe/Zurich
BEGIN:VTIMEZONE
TZID:Etc/UTC
X-LIC-LOCATION:Etc/UTC
BEGIN:STANDARD
TZOFFSETFROM:+0000
TZOFFSETTO:+0000
TZNAME:GMT
DTSTART:19700101T000000
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;VALUE=DATE:20210325
DTEND;VALUE=DATE:20210326
RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1
DTSTAMP:20210421T154106Z
UID:zzz@google.com
REATED:20200831T200244Z
DESCRIPTION:
LAST-MODIFIED:20200831T200244Z
LOCATION:
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:Birthday
TRANSP:OPAQUE
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:This is an event reminder
TRIGGER:-P0DT7H0M0S
END:VALARM
END:VEVENT
+3
View File
@@ -0,0 +1,3 @@
{
"anytime": ["Remote compliment file works!"]
}
+410
View File
@@ -0,0 +1,410 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:wfw="http://wellformedweb.org/CommentAPI/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
>
<channel>
<title>Rodrigo Ramírez Norambuena</title>
<atom:link href="https://rodrigoramirez.com/feed/" rel="self" type="application/rss+xml"/>
<link>https://rodrigoramirez.com</link>
<description>Temas sobre Linux, VoIP, Open Source, tecnología y lo relacionado.</description>
<lastBuildDate>Fri, 21 Oct 2016 21:30:22 +0000</lastBuildDate>
<language>es-ES</language>
<sy:updatePeriod>hourly</sy:updatePeriod>
<sy:updateFrequency>1</sy:updateFrequency>
<generator>https://wordpress.org/?v=4.7.3</generator>
<item>
<title>QPanel 0.13.0</title>
<link>https://rodrigoramirez.com/qpanel-0-13-0/</link>
<comments>https://rodrigoramirez.com/qpanel-0-13-0/#comments</comments>
<pubDate>Tue, 20 Sep 2016 11:16:08 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Software]]></category>
<category><![CDATA[app_queue]]></category>
<category><![CDATA[asterisk]]></category>
<category><![CDATA[FreeSWITCH]]></category>
<category><![CDATA[qpanel]]></category>
<category><![CDATA[queue]]></category>
<category><![CDATA[spy]]></category>
<category><![CDATA[supervision]]></category>
<category><![CDATA[templates]]></category>
<category><![CDATA[whisper]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1299</guid>
<description><![CDATA[<p>Ya está disponible la versión 0.13.0 de QPanel Para instalar esta nueva versión, la debes descargar de https://github.com/roramirez/qpanel/tree/0.13.0 En al README.md puedes encontrar las instrucciones para hacer que funcione en tu sistema. En esta nueva versión cuenta con los siguientes cambios: Se establece un limite para el reciclado del tiempo de conexión a la base [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-13-0/">QPanel 0.13.0</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description>
<content:encoded><![CDATA[<p><img class="aligncenter" src="https://raw.githubusercontent.com/roramirez/qpanel/e55aa16bbd85b579ee82e56469526270c5afa462/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="685" height="385" />Ya está disponible la versión 0.13.0 de QPanel</p>
<p>Para instalar esta nueva versión, la debes descargar de</p>
<ul>
<li><a href="https://github.com/roramirez/qpanel/tree/0.13.0">https://github.com/roramirez/qpanel/tree/0.13.0</a></li>
</ul>
<p>En al README.md puedes encontrar las instrucciones para hacer que funcione en tu sistema.</p>
<p>En esta nueva versión cuenta con los siguientes cambios:</p>
<ul>
<li>Se establece un limite para el reciclado del tiempo de conexión a la base de datos que contenga QueueLog. Esto evita problemas en bases de datos como MySQL que finaliza o da timeout a las conexiones.</li>
<li>Ahora la py-asterisk va dentro del archivo requirements.txt y no como submodulo del proyecto.</li>
<li>Se remueven la mayoría de las libs externas para Javascript y CSS para manejarlos desde ahora con <a href="https://bower.io/">Bower</a>.</li>
<li>Se incluye un script para WSGI que permite su utilización con Apache.</li>
<li>Actualización para los idiomas Ruso y Portugues.</li>
</ul>
<p>Si deseas colaborar con el proyecto puedes agregar nuevas sugerencias mediante un <a href="https://github.com/roramirez/qpanel/issues/new?title=[Feature]">issue</a> ó colaborar mediante <a href="https://github.com/roramirez/qpanel/blob/dd42cf0f534408505f57b0d387dffee2f3688711/README.md#how-to-contribute">mediante un Pull Request.</a></p>
<p>Ahora si necesitas <a href="https://boxtub.com/qpanel/">soporte comercial para instalaciones, personalizaciones o nuevas características  lo puedes solicitar en https://boxtub.com/qpanel/</a></p>
<p>&nbsp;</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-13-0/">QPanel 0.13.0</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-13-0/feed/</wfw:commentRss>
<slash:comments>3</slash:comments>
</item>
<item>
<title>Problema VirtualBox &#8220;starting virtual machine&#8221; &#8230;</title>
<link>https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/</link>
<comments>https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/#respond</comments>
<pubDate>Sat, 10 Sep 2016 22:50:13 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Linux]]></category>
<category><![CDATA[no arranca]]></category>
<category><![CDATA[Problema]]></category>
<category><![CDATA[VirtualBox]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1284</guid>
<description><![CDATA[<p>Después de una actualización de Debian, de la rama stretch/sid, tuve un problema con VirtualBox.  La versión que se actualizó fue a la virtualbox 5.1.4-dfsg-1+b1. El gran problema era que ninguna maquina virtual quería arrancar, se quedaba en un largo limbo con el mensaje &#8220;starting virtual machine&#8221;, como el de la imagen de a continuación. [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/">Problema VirtualBox &#8220;starting virtual machine&#8221; &#8230;</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description>
<content:encoded><![CDATA[<p>Después de una actualización de Debian, de la rama stretch/sid, tuve un problema con VirtualBox.  La versión que se actualizó fue a la virtualbox 5.1.4-dfsg-1+b1. El gran problema era que ninguna maquina virtual quería arrancar, se quedaba en un largo limbo con el mensaje &#8220;starting virtual machine&#8221;, como el de la imagen de a continuación.</p>
<p><a href="https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-10-19-25-09.png"><img class="aligncenter wp-image-1290 size-full" src="https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-10-19-25-09.png" alt="Starting virtual machine ... VirtualBox" width="648" height="554" srcset="https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-10-19-25-09.png 648w, https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-10-19-25-09-300x256.png 300w" sizes="(max-width: 648px) 100vw, 648px" /></a></p>
<p>Ninguna, pero ninguna maquina arrancó, se quedaban en ese mensaje. Fue de esos instantes en que sudas helado &#8230; <img src="https://s.w.org/images/core/emoji/2.2.1/72x72/1f609.png" alt="😉" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>Con un poco de investigación fue a parar al archivo<em> ~/.VirtualBox/VBoxSVC.log </em>que indicaba</p>
<pre>$ tail -f ~/.VirtualBox/VBoxSVC.log
00:08:32.932717 nspr-7 Failed to open "/dev/vboxdrvu", errno=13, rc=VERR_VM_DRIVER_NOT_ACCESSIBLE
00:08:33.555836 nspr-6 Failed to open "/dev/vboxdrvu", errno=13, rc=VERR_VM_DRIVER_NOT_ACCESSIBLE</pre>
<p>&nbsp;</p>
<p>Fui&#8230; algo de donde agarrarse. Mirando un poco mas se trataba de problemas con los permisos al vboxdrvu, mirando indicaba que tenía 0600.</p>
<p>&nbsp;</p>
<pre>$ ls -lh /dev/vboxdrvu
crw------- 1 root root 10, 56 Sep 10 12:47 /dev/vboxdrvu</pre>
<p>&nbsp;</p>
<p>El tema es que deben estar en 0666,  le cambias los permisos y eso soluciona el problema <img src="https://s.w.org/images/core/emoji/2.2.1/72x72/1f642.png" alt="🙂" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<pre>
$ sudo chmod 0666 /dev/vboxdrvu
$ ls -lh /dev/vboxdrvu
crw-rw-rw- 1 root root 10, 56 Sep 10 12:47 /dev/vboxdrvu</pre>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/">Problema VirtualBox &#8220;starting virtual machine&#8221; &#8230;</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/feed/</wfw:commentRss>
<slash:comments>0</slash:comments>
</item>
<item>
<title>Mejorando la consola interactiva de Python</title>
<link>https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/</link>
<comments>https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/#comments</comments>
<pubDate>Tue, 06 Sep 2016 04:24:43 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[desarrollo]]></category>
<category><![CDATA[Desarrollo]]></category>
<category><![CDATA[Python]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1247</guid>
<description><![CDATA[<p>Cuando estás desarrollando en Python es muy cool estar utilizando la consola interactiva para ir probando cosas antes de ponerlas dentro del archivo de código fuente. La consola de Python funciona y cumple su cometido. Solo al tipear  python  te permite entrar en modo interactivo e ir probando cosas. El punto es que a veces [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/">Mejorando la consola interactiva de Python</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description>
<content:encoded><![CDATA[<p>Cuando estás desarrollando en Python es muy <em>cool</em> estar utilizando la consola interactiva para ir probando cosas antes de ponerlas dentro del archivo de código fuente.</p>
<p>La consola de Python funciona y cumple su cometido. Solo al tipear  <em>python  </em>te permite entrar en modo interactivo e ir probando cosas.</p>
<p>El punto es que a veces uno necesita ir un poco más allá. Como autocomentado de código o resaltado de sintaxis, para eso tengo dos truco que utilizo generalmente.</p>
<h2>Truco a)</h2>
<p>Este permite añadirle algunos esteriodes a la consolta, en realidad uno, el autocompletado. Esto es de gran ayuda para ir conociendo los metodo que puede tener un objecto, funciones u operaciones.</p>
<p>Para esto se ocupo <em>rlcompleter</em> y  <em>readline. </em></p>
<p>&nbsp;</p>
<p>Lo que hace que hacer luego de tipear python es agregar lo siguiente dentro de la consola interativa</p>
<p><em>import rlcompleter, readline</em><br />
<em>readline.parse_and_bind(&#8216;tab:complete&#8217;)</em></p>
<p>Ya con esto te permite autocomentar código <img src="https://s.w.org/images/core/emoji/2.2.1/72x72/1f642.png" alt="🙂" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p><a href="https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-03-01-14-32.png"><img class="aligncenter wp-image-1279 size-full" src="https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-03-01-14-32.png" width="689" height="421" srcset="https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-03-01-14-32.png 689w, https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-03-01-14-32-300x183.png 300w" sizes="(max-width: 689px) 100vw, 689px" /></a></p>
<p>&nbsp;</p>
<h2>Truco b)</h2>
<p>Esto es mejorar un poco más. Es utilizar embed de <a href="https://ipython.org/">IPython,</a>  ya en la consola digita (copias o pegas) lo siguiente</p>
<p><em>from IPython import embed</em><br />
<em>embed()</em></p>
<p>Y el resultado será lo que se ve a continuación&#8230; bueno, no?</p>
<p>&nbsp;</p>
<p><a href="https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-07-25-15-48-39.png"><img class="aligncenter wp-image-1262 size-full" src="https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-07-25-15-48-39.png" width="743" height="293" srcset="https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-07-25-15-48-39.png 743w, https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-07-25-15-48-39-300x118.png 300w" sizes="(max-width: 743px) 100vw, 743px" /></a></p>
<p>&nbsp;</p>
<p>Si no quieres estar escribiendo cada vez que entras, agregas estas instrucciones en tu archivo  <em>~/.pythonrc.py </em> y lo hará cada vez que entras en el modo interactivo de la consola de Python. Lo que si, tu archivo pythonrc.py debe estar seteado en variable de entorno PYTHONSTARTUP</p>
<p>ejemplo</p>
<p><em>export  PYTHONSTARTUP=~/.pythonrc.py</em></p>
<p>O lo agregas a un bashrc, zshrc o la shell que ocupes.</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/">Mejorando la consola interactiva de Python</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/feed/</wfw:commentRss>
<slash:comments>4</slash:comments>
</item>
<item>
<title>QPanel 0.12.0 con estadísticas</title>
<link>https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/</link>
<comments>https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/#respond</comments>
<pubDate>Mon, 22 Aug 2016 04:19:03 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Software]]></category>
<category><![CDATA[app_queue]]></category>
<category><![CDATA[asterisk]]></category>
<category><![CDATA[FreeSWITCH]]></category>
<category><![CDATA[qpanel]]></category>
<category><![CDATA[queue]]></category>
<category><![CDATA[spy]]></category>
<category><![CDATA[supervision]]></category>
<category><![CDATA[templates]]></category>
<category><![CDATA[whisper]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1268</guid>
<description><![CDATA[<p>Ya está disponible una nueva versión de QPanel, esta es la 0.12.0 Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.12.0 En esta nueva versión las funcionalidades agregadas son: Permite remover los agentes de las cola Posibilidad de cancelar llamadas que están en espera de atención Estadísticas por rango de fecha obtenidas desde [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/">QPanel 0.12.0 con estadísticas</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description>
<content:encoded><![CDATA[<p><img class="aligncenter" src="https://raw.githubusercontent.com/roramirez/qpanel/e55aa16bbd85b579ee82e56469526270c5afa462/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="685" height="385" />Ya está disponible una nueva versión de QPanel, esta es la 0.12.0</p>
<p>Para instalar esta nueva versión, debes visitar la siguiente URL</p>
<ul>
<li><a href="https://github.com/roramirez/qpanel/tree/0.12.0">https://github.com/roramirez/qpanel/tree/0.12.0</a></li>
</ul>
<p>En esta nueva versión las funcionalidades agregadas son:</p>
<ul>
<li>Permite remover los agentes de las cola</li>
<li>Posibilidad de cancelar llamadas que están en espera de atención</li>
<li>Estadísticas por rango de fecha obtenidas desde el queue_log de Asterisk</li>
<li>Se actualiza a Flask 0.11</li>
</ul>
<p>Si deseas colaborar con el proyecto puedes agregar nuevas sugerencias mediante un <a href="https://github.com/roramirez/qpanel/issues/new?title=[Feature]">issue</a> ó colaborar mediante <a href="https://github.com/roramirez/qpanel/blob/dd42cf0f534408505f57b0d387dffee2f3688711/README.md#how-to-contribute">mediante un Pull Request</a></p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/">QPanel 0.12.0 con estadísticas</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/feed/</wfw:commentRss>
<slash:comments>0</slash:comments>
</item>
<item>
<title>QPanel 0.11.0 con Spy, Whisper y mas</title>
<link>https://rodrigoramirez.com/qpanel-spy-supervisor/</link>
<comments>https://rodrigoramirez.com/qpanel-spy-supervisor/#comments</comments>
<pubDate>Thu, 21 Jul 2016 01:53:21 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Software]]></category>
<category><![CDATA[app_queue]]></category>
<category><![CDATA[asterisk]]></category>
<category><![CDATA[FreeSWITCH]]></category>
<category><![CDATA[qpanel]]></category>
<category><![CDATA[queue]]></category>
<category><![CDATA[spy]]></category>
<category><![CDATA[supervision]]></category>
<category><![CDATA[templates]]></category>
<category><![CDATA[whisper]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1245</guid>
<description><![CDATA[<p>Ya está disponible una nueva versión de QPanel, esta es la 0.11.0 Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.11.0 Esta versión hemos agregado  algunas funcionalidades que los usuarios  han ido solicitando. Para esta versión es posible realizar Spy, Whisper o Barge a un canal para la supervisión de los miembros que [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-spy-supervisor/">QPanel 0.11.0 con Spy, Whisper y mas</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description>
<content:encoded><![CDATA[<p><img class="aligncenter" src="https://raw.githubusercontent.com/roramirez/qpanel/e55aa16bbd85b579ee82e56469526270c5afa462/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="685" height="385" />Ya está disponible una nueva versión de QPanel, esta es la 0.11.0</p>
<p>Para instalar esta nueva versión, debes visitar la siguiente URL</p>
<ul>
<li><a href="https://github.com/roramirez/qpanel/tree/0.11.0">https://github.com/roramirez/qpanel/tree/0.11.0</a></li>
</ul>
<p>Esta versión hemos agregado  algunas funcionalidades que los usuarios  han ido solicitando.</p>
<p>Para esta versión es posible realizar Spy, Whisper o Barge a un canal para la supervisión de los miembros que están en una cola.</p>
<p>También el sistema de plantillas se hecho una refactorización para eliminar exceso de codigo HTML usando uno de base.</p>
<p>Se han agregado una suite de tests unitarios que al contar del avance del proyecto deberían ir incrementando.</p>
<p>Se ha solucionado un bug con la actualización del color del estado del agente cuando es uno nuevo agregado a la cola.</p>
<p>&nbsp;</p>
<p>El proyecto siempre está abierto a nuevas sugerencias las cuales puedes agregar mediante un <a href="https://github.com/roramirez/qpanel/issues/new?title=[Feature]">issue</a>.</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-spy-supervisor/">QPanel 0.11.0 con Spy, Whisper y mas</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/qpanel-spy-supervisor/feed/</wfw:commentRss>
<slash:comments>4</slash:comments>
</item>
<item>
<title>Añadir Swap a un sistema</title>
<link>https://rodrigoramirez.com/crear-swap/</link>
<comments>https://rodrigoramirez.com/crear-swap/#respond</comments>
<pubDate>Fri, 15 Jul 2016 05:07:43 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Linux]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1234</guid>
<description><![CDATA[<p>Algo que me toma generalmente hacer es cuando trabajo con maquina virtuales es asignar una cantidad determinada de Swap. La  memoria swap es un espacio de intercambio en disco para cuando el sistema ya no puede utilizar más memoria RAM. El problema para mi es que algunos sistemas de maquinas virtuales no asignan por defecto [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/crear-swap/">Añadir Swap a un sistema</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description>
<content:encoded><![CDATA[<p>Algo que me toma generalmente hacer es cuando trabajo con maquina virtuales es asignar una cantidad determinada de Swap.</p>
<p>La  memoria swap es un espacio de intercambio en disco para cuando el sistema ya no puede utilizar más memoria RAM.</p>
<p>El problema para mi es que algunos sistemas de maquinas virtuales no asignan por defecto un espacio para la Swap, lo que te lleva a que el sistema pueda tener crash durante la ejecución.</p>
<p>Para comprobar la asignación de memoria, al ejecutar el comando <em>free</em> nos debería mostrar como algo similar a lo siguiente</p>
<p>&nbsp;</p>
<pre>$  free -m
             total       used       free     shared    buffers     cached
Mem:           494        488          6          1         54         75
-/+ buffers/cache:        357        136
Swap:            0          0          0</pre>
<p>En la zona de swap indica que no asignada, valor 0.</p>
<p>Para asignar swap al sistema se debe  un archivo en disco para que sea utilizado como espacio de intercambio, en este caso lo vamos  crear uno  de 3GB en la raíz del sistema</p>
<pre class="code-pre "><code>fallocate -l 3G /swapfile</code></pre>
<p>Comprobamos que ha sido creado</p>
<pre>$ ls -lh /swapfile
-rw-r--r-- 1 root root 3.0G Jul 11 13:10 /swapfile
</pre>
<h3>Habilitación del archivo Swap</h3>
<p>Ahora nos toca habilitar el archivo creado. Para eso le asignaremos los permisos</p>
<pre>chmod 600 /swapfile</pre>
<p>Lo siguiente es para convertir el  archivo para swap</p>
<pre>mkswap /swapfile</pre>
<p>Para habilitar y asignarla eso como memoria swap al sistema usamos</p>
<pre>swapon /swapfile</pre>
<p>Ya con esto podrémos ver en nuestro sistema la memoria asignada para swap</p>
<pre>$ free -m
             total       used       free     shared    buffers     cached
Mem:           494        486          7          1         51         77
-/+ buffers/cache:        358        136
Swap:         3071          0       3071</pre>
<p>&nbsp;</p>
<p>Para que al reiniciar el sistema esto se mantenga, debemos agregar la siguiente línea al archivo /etc/fstab</p>
<pre><span class="pl-s">/swapfile none swap sw 0 0</span></pre>
<p>&nbsp;</p>
<p>Podemos editar /etc/fstab con algún editor como vim, nano o podemos agregar la linea directamente en la desde la cli de la siguiente manera</p>
<pre><span class="pl-c1">echo</span> <span class="pl-s"><span class="pl-pds">"</span>/swapfile none swap sw 0 0<span class="pl-pds">"</span></span> <span class="pl-k">&gt;&gt;</span> /etc/fstab</pre>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/crear-swap/">Añadir Swap a un sistema</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/crear-swap/feed/</wfw:commentRss>
<slash:comments>0</slash:comments>
</item>
<item>
<title>QPanel 0.10.0 con vista consolidada</title>
<link>https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/</link>
<comments>https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/#respond</comments>
<pubDate>Mon, 20 Jun 2016 19:32:55 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Linux]]></category>
<category><![CDATA[app_queue]]></category>
<category><![CDATA[asterisk]]></category>
<category><![CDATA[FreeSWITCH]]></category>
<category><![CDATA[qpanel]]></category>
<category><![CDATA[queue]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1227</guid>
<description><![CDATA[<p>Ya con la release numero 28 la nueva versión 0.10.0 de QPanel ya está disponible. Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.10.0 Esta versión versión nos preocupamos de realizar mejoras, refactorizaciones y agregamos una nueva funcionalidad. La nueva funcionalidad incluida es  que ahora es posible contar con una vista consolidada para [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/">QPanel 0.10.0 con vista consolidada</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description>
<content:encoded><![CDATA[<p><img class="alignleft" src="https://raw.githubusercontent.com/roramirez/qpanel/0.10.0/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="403" height="227" />Ya con la release numero 28 la nueva versión 0.10.0 de QPanel ya está disponible.</p>
<p>Para instalar esta nueva versión, debes visitar la siguiente URL</p>
<ul>
<li><a href="https://github.com/roramirez/qpanel/tree/0.10.0">https://github.com/roramirez/qpanel/tree/0.10.0</a></li>
</ul>
<p>Esta versión versión nos preocupamos de realizar mejoras, refactorizaciones y agregamos una nueva funcionalidad.</p>
<p>La nueva funcionalidad incluida es  que ahora es posible contar con una vista consolidada para la información de todas las colas. Que hace tener un mejor control y visualización de lo que está pasando en las colas.</p>
<p>El proyecto siempre está abierto a nuevas sugerencias las cuales puedes agregar mediante un <a href="https://github.com/roramirez/qpanel/issues/new?title=[Feature]">issue</a>.</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/">QPanel 0.10.0 con vista consolidada</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/feed/</wfw:commentRss>
<slash:comments>0</slash:comments>
</item>
<item>
<title>Nerdearla 2016, WebRTC Glue</title>
<link>https://rodrigoramirez.com/nerdearla-2016/</link>
<comments>https://rodrigoramirez.com/nerdearla-2016/#respond</comments>
<pubDate>Wed, 15 Jun 2016 17:55:41 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Linux]]></category>
<category><![CDATA[baires]]></category>
<category><![CDATA[charla]]></category>
<category><![CDATA[Computación]]></category>
<category><![CDATA[informatica]]></category>
<category><![CDATA[tech]]></category>
<category><![CDATA[ti]]></category>
<category><![CDATA[webrtc]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1218</guid>
<description><![CDATA[<p>Días atrás estuve participando en el evento llamado Nerdearla en Buenos Aires.  El ambiente era genial si eres de esas personas que desde niño sintio curiosidad por ver como funcionan las cosas, donde desarmabas para volver armar lo juguetes. Habían muchas cosas interesantes tanto en las presentaciones, co-working y workshop que se hubieron. Si te [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/nerdearla-2016/">Nerdearla 2016, WebRTC Glue</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description>
<content:encoded><![CDATA[<p>Días atrás estuve participando en el evento llamado <a href="https://nerdear.la/">Nerdearla</a> en Buenos Aires.  El ambiente era genial si eres de esas personas que desde niño sintio curiosidad por ver como funcionan las cosas, donde desarmabas para volver armar lo juguetes.</p>
<p>Habían muchas cosas interesantes tanto en las presentaciones, co-working y workshop que se hubieron. Si te lo perdiste te recomiendo que estés pendiente para el proximo año.</p>
<p>&nbsp;</p>
<p>Te podias encontrar con una nuestra como esta<a href="https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS.jpg"><img class="aligncenter size-medium wp-image-1221" src="https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS-300x169.jpg" alt="Kaypro II" width="300" height="169" srcset="https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS-300x169.jpg 300w, https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS-768x432.jpg 768w, https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS-1024x576.jpg 1024w, https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS.jpg 1200w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>
<p>Puedes dar un vistaso a lo registrado por algunos <a href="https://twitter.com/hashtag/nerdearla?f=tweets&amp;vertical=default&amp;src=hash">usuarios en Twitter</a></p>
<p>El primer día hice un workshop denominado WebRTC Glue, donde muestra como hacer como unificar la experiencia de atención del centro de contacto directamente en la web. Es una presentación práctica donde puedes ver los ejemplos y usarlos como gustes. Están en <a href="https://gitlab.com/roramirez/webrtc-glue">el repositorio en Gitlab</a>. La presentación <a href="/charlas/webrtc-glue/index.html">la puedes ver aquí</a></p>
<p>&nbsp;</p>
<p><a href="https://rodrigoramirez.com/wp-content/uploads/CkhxulgWYAAp7fW.jpg"><img class="aligncenter size-medium wp-image-1222" src="https://rodrigoramirez.com/wp-content/uploads/CkhxulgWYAAp7fW-300x169.jpg" alt="WebRTC Glue" width="300" height="169" srcset="https://rodrigoramirez.com/wp-content/uploads/CkhxulgWYAAp7fW-300x169.jpg 300w, https://rodrigoramirez.com/wp-content/uploads/CkhxulgWYAAp7fW-768x432.jpg 768w, https://rodrigoramirez.com/wp-content/uploads/CkhxulgWYAAp7fW-1024x576.jpg 1024w, https://rodrigoramirez.com/wp-content/uploads/CkhxulgWYAAp7fW.jpg 1200w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>
<p>Haber si nos vemos el próximo año.</p>
<p>&nbsp;</p>
<p><strong>Update</strong>: Puedes ver una parte sin la demostración del workshop</p>
<p><iframe src="https://www.youtube.com/embed/qDreeYk-UK4?list=PLTTdzfRyGY1vQukK8L4QeAfYdbURSzFq5" width="560" height="315" frameborder="0" allowfullscreen="allowfullscreen"></iframe><br />
&nbsp;</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/nerdearla-2016/">Nerdearla 2016, WebRTC Glue</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/nerdearla-2016/feed/</wfw:commentRss>
<slash:comments>0</slash:comments>
</item>
<item>
<title>QPanel 0.9.0</title>
<link>https://rodrigoramirez.com/qpanel-0-9-0/</link>
<comments>https://rodrigoramirez.com/qpanel-0-9-0/#respond</comments>
<pubDate>Mon, 09 May 2016 18:40:23 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Software]]></category>
<category><![CDATA[asterisk]]></category>
<category><![CDATA[callcenter]]></category>
<category><![CDATA[colas]]></category>
<category><![CDATA[monitor]]></category>
<category><![CDATA[monitoreo]]></category>
<category><![CDATA[panel]]></category>
<category><![CDATA[qpanel]]></category>
<category><![CDATA[queues]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1206</guid>
<description><![CDATA[<p>El Panel monitor callcenter para colas de Asterisk ya cuenta con una nueva versión, la 0.9.0 Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.9.0 Esta versión versión nos preocupamos de realizar mejoras y refactorizaciones en el codigo para dar un mejor rendimiento, como también de la compatibilidad con la versión 11 de [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-9-0/">QPanel 0.9.0</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description>
<content:encoded><![CDATA[<p><img class="alignleft" src="https://raw.githubusercontent.com/roramirez/qpanel/0.9.0/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="403" height="227" />El Panel monitor callcenter para colas de Asterisk ya cuenta con una nueva versión, la 0.9.0</p>
<p>Para instalar esta nueva versión, debes visitar la siguiente URL</p>
<ul>
<li><a href="https://github.com/roramirez/qpanel/tree/0.9.0">https://github.com/roramirez/qpanel/tree/0.9.0</a></li>
</ul>
<p>Esta versión versión nos preocupamos de realizar mejoras y refactorizaciones en el codigo para dar un mejor rendimiento, como también de la compatibilidad con la versión 11 de Asterisk.</p>
<p>Dentro de las cosas que podamos mencionar:</p>
<ul>
<li> Actualización del repositorio y versión de py-asterisk, biblioteca para trabajar con Asterisk. Acá la ocupamos principalmente para uso del Manager.</li>
<li>Portación de parche de funcionalidades como pausa, tiempo, razón de una pausa para Asterisk 11.</li>
<li>Cambio del comportamiento en el conteo cuando el participante en una cola está ocupado (busy)</li>
</ul>
<p>El proyecto siempre está abierto a nuevas sugerencias las cuales puedes agregar mediante un <a href="https://github.com/roramirez/qpanel/issues/new?title=[Feature]">issue</a>.</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-9-0/">QPanel 0.9.0</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-9-0/feed/</wfw:commentRss>
<slash:comments>0</slash:comments>
</item>
<item>
<title>Mandar un email desde la shell</title>
<link>https://rodrigoramirez.com/mandar-un-email-desde-la-shell/</link>
<comments>https://rodrigoramirez.com/mandar-un-email-desde-la-shell/#comments</comments>
<pubDate>Wed, 13 Apr 2016 13:05:13 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Linux]]></category>
<category><![CDATA[mini-tips]]></category>
<category><![CDATA[bash]]></category>
<category><![CDATA[cli]]></category>
<category><![CDATA[Email]]></category>
<category><![CDATA[mail]]></category>
<category><![CDATA[sh]]></category>
<category><![CDATA[shell]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1172</guid>
<description><![CDATA[<p>Dejo esto por acá ya que es algo que siempre me olvido como es. El tema es enviar un email mediante el comando mail en un servidor con Linux. Si usas mail a secas te va pidiendo los datos para crear el correo, principalmente el body del correo. Para automatizar esto a través de un [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/mandar-un-email-desde-la-shell/">Mandar un email desde la shell</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description>
<content:encoded><![CDATA[<p>Dejo esto por acá ya que es algo que siempre me olvido como es. El tema es enviar un email mediante el comando <em>mail</em> en un servidor con Linux.</p>
<p>Si usas mail a secas te va pidiendo los datos para crear el correo, principalmente el body del correo. Para automatizar esto a través de un <em>echo</em> le pasas por pipe a <em>mail</em></p>
<pre>echo "Cuerpo del mensaje" | mail -s Asunto a@rodrigoramirez.com</pre>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/mandar-un-email-desde-la-shell/">Mandar un email desde la shell</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/mandar-un-email-desde-la-shell/feed/</wfw:commentRss>
<slash:comments>4</slash:comments>
</item>
</channel>
</rss>
+33
View File
@@ -0,0 +1,33 @@
{
"LOADING": "Loading …",
"TODAY": "Today",
"TOMORROW": "Tomorrow",
"DAYAFTERTOMORROW": "In 2 days",
"RUNNING": "Ends in",
"EMPTY": "No upcoming events.",
"WEEK": "Week {weekNumber}",
"N": "N",
"NNE": "NNE",
"NE": "NE",
"ENE": "ENE",
"E": "E",
"ESE": "ESE",
"SE": "SE",
"SSE": "SSE",
"S": "S",
"SSW": "SSW",
"SW": "SW",
"WSW": "WSW",
"W": "W",
"WNW": "WNW",
"NW": "NW",
"NNW": "NNW",
"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.",
"UPDATE_INFO_MULTIPLE": "The current installation is COMMIT_COUNT commits behind on the BRANCH_NAME branch."
}
+48
View File
@@ -0,0 +1,48 @@
{
"coord": {
"lon": 11.58,
"lat": 48.14
},
"weather": [
{
"id": 615,
"main": "Snow",
"description": "light rain and snow",
"icon": "13d"
},
{
"id": 500,
"main": "Rain",
"description": "light rain",
"icon": "10d"
}
],
"base": "stations",
"main": {
"temp": 1.49,
"pressure": 1005,
"humidity": 93.7,
"temp_min": 1,
"temp_max": 2
},
"visibility": 7000,
"wind": {
"speed": 11.8,
"deg": 250
},
"clouds": {
"all": 75
},
"dt": 1547387400,
"sys": {
"type": 1,
"id": 1267,
"message": 0.0031,
"country": "DE",
"sunrise": 1547362817,
"sunset": 1547394301
},
"id": 2867714,
"name": "Munich",
"cod": 200
}
+202
View File
@@ -0,0 +1,202 @@
{
"city": {
"id": 2867714,
"name": "Munich",
"coord": {
"lon": 11.5754,
"lat": 48.1371
},
"country": "DE",
"population": 1260391,
"timezone": 7200
},
"cod": "200",
"message": 0.9653487,
"cnt": 7,
"list": [
{
"dt": 1568372400,
"sunrise": 1568350044,
"sunset": 1568395948,
"temp": {
"day": 24.44,
"min": 15.35,
"max": 24.44,
"night": 15.35,
"eve": 18,
"morn": 23.03
},
"pressure": 1031.65,
"humidity": 70,
"weather": [
{
"id": 801,
"main": "Clouds",
"description": "few clouds",
"icon": "02d"
}
],
"speed": 3.35,
"deg": 314,
"clouds": 21
},
{
"dt": 1568458800,
"sunrise": 1568436525,
"sunset": 1568482223,
"temp": {
"day": 20.81,
"min": 13.56,
"max": 21.02,
"night": 13.56,
"eve": 16.6,
"morn": 15.88
},
"pressure": 1028.81,
"humidity": 72,
"weather": [
{
"id": 500,
"main": "Rain",
"description": "light rain",
"icon": "10d"
}
],
"speed": 2.21,
"deg": 81,
"clouds": 100,
"pop": 0.7,
"rain": 2.51
},
{
"dt": 1568545200,
"sunrise": 1568523007,
"sunset": 1568568497,
"temp": {
"day": 22.65,
"min": 13.76,
"max": 22.88,
"night": 15.27,
"eve": 17.45,
"morn": 13.76
},
"pressure": 1023.75,
"humidity": 64,
"weather": [
{
"id": 800,
"main": "Clear",
"description": "sky is clear",
"icon": "01d"
}
],
"speed": 1.15,
"deg": 7,
"clouds": 0
},
{
"dt": 1568631600,
"sunrise": 1568609489,
"sunset": 1568654771,
"temp": {
"day": 23.45,
"min": 13.95,
"max": 23.45,
"night": 13.95,
"eve": 17.75,
"morn": 15.21
},
"pressure": 1020.41,
"humidity": 64,
"weather": [
{
"id": 800,
"main": "Clear",
"description": "sky is clear",
"icon": "01d"
}
],
"speed": 3.07,
"deg": 298,
"clouds": 7
},
{
"dt": 1568718000,
"sunrise": 1568695970,
"sunset": 1568741045,
"temp": {
"day": 20.55,
"min": 10.95,
"max": 20.55,
"night": 10.95,
"eve": 14.82,
"morn": 13.24
},
"pressure": 1019.4,
"humidity": 66,
"weather": [
{
"id": 800,
"main": "Clear",
"description": "sky is clear",
"icon": "01d"
}
],
"speed": 2.8,
"deg": 333,
"clouds": 2
},
{
"dt": 1568804400,
"sunrise": 1568782452,
"sunset": 1568827319,
"temp": {
"day": 18.15,
"min": 7.75,
"max": 18.15,
"night": 7.75,
"eve": 12.45,
"morn": 9.41
},
"pressure": 1017.56,
"humidity": 52,
"weather": [
{
"id": 800,
"main": "Clear",
"description": "sky is clear",
"icon": "01d"
}
],
"speed": 2.92,
"deg": 34,
"clouds": 0
},
{
"dt": 1568890800,
"sunrise": 1568868934,
"sunset": 1568913593,
"temp": {
"day": 14.85,
"min": 5.56,
"max": 15.05,
"night": 5.56,
"eve": 9.56,
"morn": 6.25
},
"pressure": 1022.7,
"humidity": 59,
"weather": [
{
"id": 800,
"main": "Clear",
"description": "sky is clear",
"icon": "01d"
}
],
"speed": 2.89,
"deg": 51,
"clouds": 1
}
]
}
File diff suppressed because it is too large Load Diff
+108
View File
@@ -0,0 +1,108 @@
const path = require("path");
const { JSDOM } = require("jsdom");
describe("File js/class", () => {
describe("Test function cloneObject", () => {
let clone;
let dom;
beforeAll((done) => {
dom = new JSDOM(
`<script>var Log = {log: () => {}};</script>\
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "class.js")}">`,
{ runScripts: "dangerously", resources: "usable" }
);
dom.window.onload = () => {
const { cloneObject } = dom.window;
clone = cloneObject;
done();
};
});
it("should clone object", () => {
const expected = { name: "Rodrigo", web: "https://rodrigoramirez.com", project: "MagicMirror" };
const obj = clone(expected);
expect(obj).toEqual(expected);
expect(expected === obj).toBe(false);
});
it("should clone array", () => {
const expected = [1, null, undefined, "TEST"];
const obj = clone(expected);
expect(obj).toEqual(expected);
expect(expected === obj).toBe(false);
});
it("should clone number", () => {
let expected = 1;
let obj = clone(expected);
expect(obj).toBe(expected);
expected = 1.23;
obj = clone(expected);
expect(obj).toBe(expected);
});
it("should clone string", () => {
const expected = "Perfect stranger";
const obj = clone(expected);
expect(obj).toBe(expected);
});
it("should clone undefined", () => {
const expected = undefined;
const obj = clone(expected);
expect(obj).toBe(expected);
});
it("should clone null", () => {
const expected = null;
const obj = clone(expected);
expect(obj).toBe(expected);
});
it("should clone nested object", () => {
const expected = {
name: "fewieden",
link: "https://github.com/fewieden",
versions: ["2.0", "2.1", "2.2"],
answerForAllQuestions: 42,
properties: {
items: [{ foo: "bar" }, { lorem: "ipsum" }],
invalid: undefined,
nothing: null
}
};
const obj = clone(expected);
expect(obj).toEqual(expected);
expect(expected === obj).toBe(false);
expect(expected.versions === obj.versions).toBe(false);
expect(expected.properties === obj.properties).toBe(false);
expect(expected.properties.items === obj.properties.items).toBe(false);
expect(expected.properties.items[0] === obj.properties.items[0]).toBe(false);
expect(expected.properties.items[1] === obj.properties.items[1]).toBe(false);
});
describe("Test lockstring code", () => {
let log;
beforeAll(() => {
log = dom.window.Log.log;
dom.window.Log.log = (str) => {
expect(str).toBe("lockStrings");
};
});
afterAll(() => {
dom.window.Log.log = log;
});
it("should clone object and log lockStrings", () => {
const expected = { name: "Module", lockStrings: "stringLock" };
const obj = clone(expected);
expect(obj).toEqual(expected);
expect(expected === obj).toBe(false);
});
});
});
});
+15
View File
@@ -0,0 +1,15 @@
const deprecated = require("../../../js/deprecated");
describe("Deprecated", () => {
it("should be an object", () => {
expect(typeof deprecated).toBe("object");
});
it("should contain configs array with deprecated options as strings", () => {
expect(Array.isArray(["deprecated.configs"])).toBe(true);
for (let option of deprecated.configs) {
expect(typeof option).toBe("string");
}
expect(deprecated.configs).toEqual(expect.arrayContaining(["kioskmode"]));
});
});
+285
View File
@@ -0,0 +1,285 @@
const path = require("path");
const helmet = require("helmet");
const { JSDOM } = require("jsdom");
const express = require("express");
const sockets = new Set();
describe("Translator", () => {
let server;
beforeAll(() => {
const app = express();
app.use(helmet());
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
next();
});
app.use("/translations", express.static(path.join(__dirname, "..", "..", "..", "tests", "mocks")));
server = app.listen(3000);
server.on("connection", (socket) => {
sockets.add(socket);
});
});
afterAll(async () => {
for (const socket of sockets) {
socket.destroy();
sockets.delete(socket);
}
await server.close();
});
describe("translate", () => {
const translations = {
"MMM-Module": {
Hello: "Hallo",
"Hello {username}": "Hallo {username}"
}
};
const coreTranslations = {
Hello: "XXX",
"Hello {username}": "XXX",
FOO: "Foo",
"BAR {something}": "Bar {something}"
};
const translationsFallback = {
"MMM-Module": {
Hello: "XXX",
"Hello {username}": "XXX",
FOO: "XXX",
"BAR {something}": "XXX",
"A key": "A translation"
}
};
const coreTranslationsFallback = {
FOO: "XXX",
"BAR {something}": "XXX",
Hello: "XXX",
"Hello {username}": "XXX",
"A key": "XXX",
Fallback: "core fallback"
};
/**
* @param {object} Translator the global Translator object
*/
const setTranslations = (Translator) => {
Translator.translations = translations;
Translator.coreTranslations = coreTranslations;
Translator.translationsFallback = translationsFallback;
Translator.coreTranslationsFallback = coreTranslationsFallback;
};
it("should return custom module translation", (done) => {
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = () => {
const { Translator } = dom.window;
setTranslations(Translator);
let translation = Translator.translate({ name: "MMM-Module" }, "Hello");
expect(translation).toBe("Hallo");
translation = Translator.translate({ name: "MMM-Module" }, "Hello {username}", { username: "fewieden" });
expect(translation).toBe("Hallo fewieden");
done();
};
});
it("should return core translation", (done) => {
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = () => {
const { Translator } = dom.window;
setTranslations(Translator);
let translation = Translator.translate({ name: "MMM-Module" }, "FOO");
expect(translation).toBe("Foo");
translation = Translator.translate({ name: "MMM-Module" }, "BAR {something}", { something: "Lorem Ipsum" });
expect(translation).toBe("Bar Lorem Ipsum");
done();
};
});
it("should return custom module translation fallback", (done) => {
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = () => {
const { Translator } = dom.window;
setTranslations(Translator);
const translation = Translator.translate({ name: "MMM-Module" }, "A key");
expect(translation).toBe("A translation");
done();
};
});
it("should return core translation fallback", (done) => {
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = () => {
const { Translator } = dom.window;
setTranslations(Translator);
const translation = Translator.translate({ name: "MMM-Module" }, "Fallback");
expect(translation).toBe("core fallback");
done();
};
});
it("should return translation with placeholder for missing variables", (done) => {
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = () => {
const { Translator } = dom.window;
setTranslations(Translator);
const translation = Translator.translate({ name: "MMM-Module" }, "Hello {username}");
expect(translation).toBe("Hallo {username}");
done();
};
});
it("should return key if no translation was found", (done) => {
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = () => {
const { Translator } = dom.window;
setTranslations(Translator);
const translation = Translator.translate({ name: "MMM-Module" }, "MISSING");
expect(translation).toBe("MISSING");
done();
};
});
});
describe("load", () => {
const mmm = {
name: "TranslationTest",
file(file) {
return `http://localhost:3000/translations/${file}`;
}
};
it("should load translations", (done) => {
const dom = new JSDOM(`<script>var Log = {log: () => {}};</script><script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = async () => {
const { Translator } = dom.window;
const file = "translation_test.json";
await Translator.load(mmm, file, false);
const json = require(path.join(__dirname, "..", "..", "..", "tests", "mocks", file));
expect(Translator.translations[mmm.name]).toEqual(json);
done();
};
});
it("should load translation fallbacks", (done) => {
const dom = new JSDOM(`<script>var Log = {log: () => {}};</script><script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = async () => {
const { Translator } = dom.window;
const file = "translation_test.json";
await Translator.load(mmm, file, true);
const json = require(path.join(__dirname, "..", "..", "..", "tests", "mocks", file));
expect(Translator.translationsFallback[mmm.name]).toEqual(json);
done();
};
});
it("should not load translations, if module fallback exists", (done) => {
const dom = new JSDOM(`<script>var Log = {log: () => {}};</script><script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = async () => {
const { Translator, XMLHttpRequest } = dom.window;
const file = "translation_test.json";
XMLHttpRequest.prototype.send = () => {
throw new Error("Shouldn't load files");
};
Translator.translationsFallback[mmm.name] = {
Hello: "Hallo"
};
await Translator.load(mmm, file, false);
expect(Translator.translations[mmm.name]).toBe(undefined);
expect(Translator.translationsFallback[mmm.name]).toEqual({
Hello: "Hallo"
});
done();
};
});
});
describe("loadCoreTranslations", () => {
it("should load core translations and fallback", (done) => {
const dom = new JSDOM(
`<script>var translations = {en: "http://localhost:3000/translations/translation_test.json"}; var Log = {log: () => {}};</script>\
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
{ runScripts: "dangerously", resources: "usable" }
);
dom.window.onload = async () => {
const { Translator } = dom.window;
await Translator.loadCoreTranslations("en");
const en = require(path.join(__dirname, "..", "..", "..", "tests", "mocks", "translation_test.json"));
setTimeout(() => {
expect(Translator.coreTranslations).toEqual(en);
expect(Translator.coreTranslationsFallback).toEqual(en);
done();
}, 500);
};
});
it("should load core fallback if language cannot be found", (done) => {
const dom = new JSDOM(
`<script>var translations = {en: "http://localhost:3000/translations/translation_test.json"}; var Log = {log: () => {}};</script>\
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
{ runScripts: "dangerously", resources: "usable" }
);
dom.window.onload = async () => {
const { Translator } = dom.window;
await Translator.loadCoreTranslations("MISSINGLANG");
const en = require(path.join(__dirname, "..", "..", "..", "tests", "mocks", "translation_test.json"));
setTimeout(() => {
expect(Translator.coreTranslations).toEqual({});
expect(Translator.coreTranslationsFallback).toEqual(en);
done();
}, 500);
};
});
});
describe("loadCoreTranslationsFallback", () => {
it("should load core translations fallback", (done) => {
const dom = new JSDOM(
`<script>var translations = {en: "http://localhost:3000/translations/translation_test.json"}; var Log = {log: () => {}};</script>\
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
{ runScripts: "dangerously", resources: "usable" }
);
dom.window.onload = async () => {
const { Translator } = dom.window;
await Translator.loadCoreTranslationsFallback();
const en = require(path.join(__dirname, "..", "..", "..", "tests", "mocks", "translation_test.json"));
setTimeout(() => {
expect(Translator.coreTranslationsFallback).toEqual(en);
done();
}, 500);
};
});
it("should load core fallback if language cannot be found", (done) => {
const dom = new JSDOM(
`<script>var translations = {}; var Log = {log: () => {}};</script>\
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
{ runScripts: "dangerously", resources: "usable" }
);
dom.window.onload = async () => {
const { Translator } = dom.window;
await Translator.loadCoreTranslations();
setTimeout(() => {
expect(Translator.coreTranslationsFallback).toEqual({});
done();
}, 500);
};
});
});
});
+38
View File
@@ -0,0 +1,38 @@
const colors = require("colors/safe");
const Utils = require("../../../js/utils");
describe("Utils", () => {
describe("colors", () => {
const colorsEnabled = colors.enabled;
afterEach(() => {
colors.enabled = colorsEnabled;
});
it("should have info, warn and error properties", () => {
expect(Utils.colors).toHaveProperty("info");
expect(Utils.colors).toHaveProperty("warn");
expect(Utils.colors).toHaveProperty("error");
});
it("properties should be functions", () => {
expect(typeof Utils.colors.info).toBe("function");
expect(typeof Utils.colors.warn).toBe("function");
expect(typeof Utils.colors.error).toBe("function");
});
it("should print colored message in supported consoles", () => {
colors.enabled = true;
expect(Utils.colors.info("some informations")).toBe("\u001b[34msome informations\u001b[39m");
expect(Utils.colors.warn("a warning")).toBe("\u001b[33ma warning\u001b[39m");
expect(Utils.colors.error("ERROR!")).toBe("\u001b[31mERROR!\u001b[39m");
});
it("should print message in unsupported consoles", () => {
colors.enabled = false;
expect(Utils.colors.info("some informations")).toBe("some informations");
expect(Utils.colors.warn("a warning")).toBe("a warning");
expect(Utils.colors.error("ERROR!")).toBe("ERROR!");
});
});
});
@@ -0,0 +1,100 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Updatenotification custom module returns status information without hash 1`] = `
{
"behind": 7,
"current": "master",
"hash": "",
"isBehindInStatus": false,
"module": "MMM-Fuel",
"tracking": "origin/master",
}
`;
exports[`Updatenotification MagicMirror on develop returns status information 1`] = `
{
"behind": 5,
"current": "develop",
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
"isBehindInStatus": false,
"module": "MagicMirror",
"tracking": "origin/develop",
}
`;
exports[`Updatenotification MagicMirror on develop returns status information early if isBehindInStatus 1`] = `
{
"behind": 5,
"current": "develop",
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
"isBehindInStatus": true,
"module": "MagicMirror",
"tracking": "origin/develop",
}
`;
exports[`Updatenotification MagicMirror on master (empty taglist) returns status information 1`] = `
{
"behind": 5,
"current": "master",
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
"isBehindInStatus": false,
"module": "MagicMirror",
"tracking": "origin/master",
}
`;
exports[`Updatenotification MagicMirror on master (empty taglist) returns status information early if isBehindInStatus 1`] = `
{
"behind": 5,
"current": "master",
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
"isBehindInStatus": true,
"module": "MagicMirror",
"tracking": "origin/master",
}
`;
exports[`Updatenotification MagicMirror on master with match in taglist returns status information 1`] = `
{
"behind": 5,
"current": "master",
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
"isBehindInStatus": false,
"module": "MagicMirror",
"tracking": "origin/master",
}
`;
exports[`Updatenotification MagicMirror on master with match in taglist returns status information early if isBehindInStatus 1`] = `
{
"behind": 5,
"current": "master",
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
"isBehindInStatus": true,
"module": "MagicMirror",
"tracking": "origin/master",
}
`;
exports[`Updatenotification MagicMirror on master without match in taglist returns status information 1`] = `
{
"behind": 0,
"current": "master",
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
"isBehindInStatus": false,
"module": "MagicMirror",
"tracking": "origin/master",
}
`;
exports[`Updatenotification MagicMirror on master without match in taglist returns status information early if isBehindInStatus 1`] = `
{
"behind": 0,
"current": "master",
"hash": "332e429a41f1a2339afd4f0ae96dd125da6beada",
"isBehindInStatus": true,
"module": "MagicMirror",
"tracking": "origin/master",
}
`;
+31
View File
@@ -0,0 +1,31 @@
const path = require("path");
const { JSDOM } = require("jsdom");
describe("Test function cmpVersions in js/module.js", () => {
let cmp;
beforeAll((done) => {
const dom = new JSDOM(
`<script>var Class = {extend: () => { return {}; }};</script>\
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "module.js")}">`,
{ runScripts: "dangerously", resources: "usable" }
);
dom.window.onload = () => {
const { cmpVersions } = dom.window;
cmp = cmpVersions;
done();
};
});
it("should return -1 when comparing 2.1 to 2.2", () => {
expect(cmp("2.1", "2.2")).toBe(-1);
});
it("should be return 0 when comparing 2.2 to 2.2", () => {
expect(cmp("2.2", "2.2")).toBe(0);
});
it("should be return 1 when comparing 1.1 to 1.0", () => {
expect(cmp("1.1", "1.0")).toBe(1);
});
});
@@ -0,0 +1,149 @@
const { cors } = require("../../../js/server_functions");
describe("server_functions tests", () => {
describe("The cors method", () => {
let fetchResponse;
let fetchResponseHeadersGet;
let fetchResponseHeadersText;
let corsResponse;
let request;
jest.mock("node-fetch");
let nodefetch = require("node-fetch");
let fetchMock;
beforeEach(() => {
nodefetch.mockReset();
fetchResponseHeadersGet = jest.fn(() => {});
fetchResponseHeadersText = jest.fn(() => {});
fetchResponse = {
headers: {
get: fetchResponseHeadersGet
},
text: fetchResponseHeadersText
};
jest.mock("node-fetch", () => jest.fn());
nodefetch.mockImplementation(() => fetchResponse);
fetchMock = nodefetch;
corsResponse = {
set: jest.fn(() => {}),
send: jest.fn(() => {})
};
request = {
url: `/cors?url=www.test.com`
};
});
test("Calls correct URL once", async () => {
const urlToCall = "http://www.test.com/path?param1=value1";
request.url = `/cors?url=${urlToCall}`;
await cors(request, corsResponse);
expect(fetchMock.mock.calls.length).toBe(1);
expect(fetchMock.mock.calls[0][0]).toBe(urlToCall);
});
test("Forewards Content-Type if json", async () => {
fetchResponseHeadersGet.mockImplementation(() => "json");
await cors(request, corsResponse);
expect(fetchResponseHeadersGet.mock.calls.length).toBe(1);
expect(fetchResponseHeadersGet.mock.calls[0][0]).toBe("Content-Type");
expect(corsResponse.set.mock.calls.length).toBe(1);
expect(corsResponse.set.mock.calls[0][0]).toBe("Content-Type");
expect(corsResponse.set.mock.calls[0][1]).toBe("json");
});
test("Forewards Content-Type if xml", async () => {
fetchResponseHeadersGet.mockImplementation(() => "xml");
await cors(request, corsResponse);
expect(fetchResponseHeadersGet.mock.calls.length).toBe(1);
expect(fetchResponseHeadersGet.mock.calls[0][0]).toBe("Content-Type");
expect(corsResponse.set.mock.calls.length).toBe(1);
expect(corsResponse.set.mock.calls[0][0]).toBe("Content-Type");
expect(corsResponse.set.mock.calls[0][1]).toBe("xml");
});
test("Sends correct data from response", async () => {
const responseData = "some data";
fetchResponseHeadersText.mockImplementation(() => responseData);
let sentData;
corsResponse.send = jest.fn((input) => {
sentData = input;
});
await cors(request, corsResponse);
expect(fetchResponseHeadersText.mock.calls.length).toBe(1);
expect(sentData).toBe(responseData);
});
test("Sends error data from response", async () => {
const error = new Error("error data");
fetchResponseHeadersText.mockImplementation(() => {
throw error;
});
let sentData;
corsResponse.send = jest.fn((input) => {
sentData = input;
});
await cors(request, corsResponse);
expect(fetchResponseHeadersText.mock.calls.length).toBe(1);
expect(sentData).toBe(error);
});
test("Fetches with user agent by default", async () => {
await cors(request, corsResponse);
expect(fetchMock.mock.calls.length).toBe(1);
expect(fetchMock.mock.calls[0][1]).toHaveProperty("headers");
expect(fetchMock.mock.calls[0][1].headers).toHaveProperty("User-Agent");
});
test("Fetches with specified headers", async () => {
const headersParam = "sendheaders=header1:value1,header2:value2";
const urlParam = "http://www.test.com/path?param1=value1";
request.url = `/cors?${headersParam}&url=${urlParam}`;
await cors(request, corsResponse);
expect(fetchMock.mock.calls.length).toBe(1);
expect(fetchMock.mock.calls[0][1]).toHaveProperty("headers");
expect(fetchMock.mock.calls[0][1].headers).toHaveProperty("header1", "value1");
expect(fetchMock.mock.calls[0][1].headers).toHaveProperty("header2", "value2");
});
test("Sends specified headers", async () => {
fetchResponseHeadersGet.mockImplementation((input) => input.replace("header", "value"));
const expectedheaders = "expectedheaders=header1,header2";
const urlParam = "http://www.test.com/path?param1=value1";
request.url = `/cors?${expectedheaders}&url=${urlParam}`;
await cors(request, corsResponse);
expect(fetchMock.mock.calls.length).toBe(1);
expect(fetchMock.mock.calls[0][1]).toHaveProperty("headers");
expect(corsResponse.set.mock.calls.length).toBe(3);
expect(corsResponse.set.mock.calls[0][0]).toBe("Content-Type");
expect(corsResponse.set.mock.calls[1][0]).toBe("header1");
expect(corsResponse.set.mock.calls[1][1]).toBe("value1");
expect(corsResponse.set.mock.calls[2][0]).toBe("header2");
expect(corsResponse.set.mock.calls[2][1]).toBe("value2");
});
});
});
@@ -0,0 +1,253 @@
jest.mock("util", () => ({
...jest.requireActual("util"),
promisify: jest.fn()
}));
jest.mock("fs", () => ({
...jest.requireActual("fs"),
statSync: jest.fn()
}));
jest.mock("logger", () => ({
...jest.requireActual("logger"),
error: jest.fn(),
info: jest.fn()
}));
describe("Updatenotification", () => {
const execMock = jest.fn();
let gitHelper;
let gitRemoteOut;
let gitRevParseOut;
let gitStatusOut;
let gitFetchOut;
let gitRevListCountOut;
let gitRevListOut;
let gitFetchErr;
let gitTagListOut;
beforeAll(async () => {
const { promisify } = require("util");
promisify.mockReturnValue(execMock);
const GitHelper = require(`../../../modules/default/updatenotification/git_helper`);
gitHelper = new GitHelper();
});
beforeEach(() => {
gitRemoteOut = "";
gitRevParseOut = "";
gitStatusOut = "";
gitFetchOut = "";
gitRevListCountOut = "";
gitRevListOut = "";
gitFetchErr = "";
gitTagListOut = "";
execMock.mockImplementation((command) => {
if (command.includes("git remote -v")) {
return { stdout: gitRemoteOut };
} else if (command.includes("git rev-parse HEAD")) {
return { stdout: gitRevParseOut };
} else if (command.includes("git status -sb")) {
return { stdout: gitStatusOut };
} else if (command.includes("git fetch -n --dry-run")) {
return { stdout: gitFetchOut, stderr: gitFetchErr };
} else if (command.includes("git rev-list --ancestry-path --count")) {
return { stdout: gitRevListCountOut };
} else if (command.includes("git rev-list --ancestry-path")) {
return { stdout: gitRevListOut };
} else if (command.includes("git ls-remote -q --tags --refs")) {
return { stdout: gitTagListOut };
}
});
});
afterEach(async () => {
gitHelper.gitRepos = [];
jest.clearAllMocks();
});
describe("MagicMirror on develop", () => {
const moduleName = "MagicMirror";
beforeEach(async () => {
gitRemoteOut = "origin\tgit@github.com:MichMich/MagicMirror.git (fetch)\norigin\tgit@github.com:MichMich/MagicMirror.git (push)\n";
gitRevParseOut = "332e429a41f1a2339afd4f0ae96dd125da6beada";
gitStatusOut = "## develop...origin/develop\n M tests/unit/functions/updatenotification_spec.js\n";
gitFetchErr = "From github.com:MichMich/MagicMirror\n60e0377..332e429 develop -> origin/develop\n";
gitRevListCountOut = "5";
await gitHelper.add(moduleName);
});
it("returns status information", async () => {
const repos = await gitHelper.getRepos();
expect(repos[0]).toMatchSnapshot();
expect(execMock).toHaveBeenCalledTimes(5);
});
it("returns status information early if isBehindInStatus", async () => {
gitStatusOut = "## develop...origin/develop [behind 5]";
const repos = await gitHelper.getRepos();
expect(repos[0]).toMatchSnapshot();
expect(execMock).toHaveBeenCalledTimes(3);
});
it("excludes repo if status can't be retrieved", async () => {
const errorMessage = "Failed to retrieve status";
execMock.mockRejectedValueOnce(errorMessage);
const repos = await gitHelper.getRepos();
expect(repos.length).toBe(0);
const { error } = require("logger");
expect(error).toHaveBeenCalledWith(`Failed to retrieve repo info for ${moduleName}: Failed to retrieve status`);
});
});
describe("MagicMirror on master (empty taglist)", () => {
const moduleName = "MagicMirror";
beforeEach(async () => {
gitRemoteOut = "origin\tgit@github.com:MichMich/MagicMirror.git (fetch)\norigin\tgit@github.com:MichMich/MagicMirror.git (push)\n";
gitRevParseOut = "332e429a41f1a2339afd4f0ae96dd125da6beada";
gitStatusOut = "## master...origin/master\n M tests/unit/functions/updatenotification_spec.js\n";
gitFetchErr = "From github.com:MichMich/MagicMirror\n60e0377..332e429 master -> origin/master\n";
gitRevListCountOut = "5";
await gitHelper.add(moduleName);
});
it("returns status information", async () => {
const repos = await gitHelper.getRepos();
expect(repos[0]).toMatchSnapshot();
expect(execMock).toHaveBeenCalledTimes(7);
});
it("returns status information early if isBehindInStatus", async () => {
gitStatusOut = "## master...origin/master [behind 5]";
const repos = await gitHelper.getRepos();
expect(repos[0]).toMatchSnapshot();
expect(execMock).toHaveBeenCalledTimes(7);
});
it("excludes repo if status can't be retrieved", async () => {
const errorMessage = "Failed to retrieve status";
execMock.mockRejectedValueOnce(errorMessage);
const repos = await gitHelper.getRepos();
expect(repos.length).toBe(0);
const { error } = require("logger");
expect(error).toHaveBeenCalledWith(`Failed to retrieve repo info for ${moduleName}: Failed to retrieve status`);
});
});
describe("MagicMirror on master with match in taglist", () => {
const moduleName = "MagicMirror";
beforeEach(async () => {
gitRemoteOut = "origin\tgit@github.com:MichMich/MagicMirror.git (fetch)\norigin\tgit@github.com:MichMich/MagicMirror.git (push)\n";
gitRevParseOut = "332e429a41f1a2339afd4f0ae96dd125da6beada";
gitStatusOut = "## master...origin/master\n M tests/unit/functions/updatenotification_spec.js\n";
gitFetchErr = "From github.com:MichMich/MagicMirror\n60e0377..332e429 master -> origin/master\n";
gitRevListCountOut = "5";
gitTagListOut = "332e429a41f1a2339afd4f0ae96dd125da6beada...tag...\n";
gitRevListOut = "332e429a41f1a2339afd4f0ae96dd125da6beada\n";
await gitHelper.add(moduleName);
});
it("returns status information", async () => {
const repos = await gitHelper.getRepos();
expect(repos[0]).toMatchSnapshot();
expect(execMock).toHaveBeenCalledTimes(7);
});
it("returns status information early if isBehindInStatus", async () => {
gitStatusOut = "## master...origin/master [behind 5]";
const repos = await gitHelper.getRepos();
expect(repos[0]).toMatchSnapshot();
expect(execMock).toHaveBeenCalledTimes(7);
});
it("excludes repo if status can't be retrieved", async () => {
const errorMessage = "Failed to retrieve status";
execMock.mockRejectedValueOnce(errorMessage);
const repos = await gitHelper.getRepos();
expect(repos.length).toBe(0);
const { error } = require("logger");
expect(error).toHaveBeenCalledWith(`Failed to retrieve repo info for ${moduleName}: Failed to retrieve status`);
});
});
describe("MagicMirror on master without match in taglist", () => {
const moduleName = "MagicMirror";
beforeEach(async () => {
gitRemoteOut = "origin\tgit@github.com:MichMich/MagicMirror.git (fetch)\norigin\tgit@github.com:MichMich/MagicMirror.git (push)\n";
gitRevParseOut = "332e429a41f1a2339afd4f0ae96dd125da6beada";
gitStatusOut = "## master...origin/master\n M tests/unit/functions/updatenotification_spec.js\n";
gitFetchErr = "From github.com:MichMich/MagicMirror\n60e0377..332e429 master -> origin/master\n";
gitRevListCountOut = "5";
gitTagListOut = "xxxe429a41f1a2339afd4f0ae96dd125da6beada...tag...\n";
gitRevListOut = "332e429a41f1a2339afd4f0ae96dd125da6beada\n";
await gitHelper.add(moduleName);
});
it("returns status information", async () => {
const repos = await gitHelper.getRepos();
expect(repos[0]).toMatchSnapshot();
expect(execMock).toHaveBeenCalledTimes(7);
});
it("returns status information early if isBehindInStatus", async () => {
gitStatusOut = "## master...origin/master [behind 5]";
const repos = await gitHelper.getRepos();
expect(repos[0]).toMatchSnapshot();
expect(execMock).toHaveBeenCalledTimes(7);
});
it("excludes repo if status can't be retrieved", async () => {
const errorMessage = "Failed to retrieve status";
execMock.mockRejectedValueOnce(errorMessage);
const repos = await gitHelper.getRepos();
expect(repos.length).toBe(0);
const { error } = require("logger");
expect(error).toHaveBeenCalledWith(`Failed to retrieve repo info for ${moduleName}: Failed to retrieve status`);
});
});
describe("custom module", () => {
const moduleName = "MMM-Fuel";
beforeEach(async () => {
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`;
gitRevListCountOut = "7";
await gitHelper.add(moduleName);
});
it("returns status information without hash", async () => {
const repos = await gitHelper.getRepos();
expect(repos[0]).toMatchSnapshot();
expect(execMock).toHaveBeenCalledTimes(4);
});
});
});

Some files were not shown because too many files have changed in this diff Show More