diff --git a/.prettierignore b/.prettierignore index e4a77657..3b87fcd2 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,4 @@ package-lock.json /config/**/* -/modules/default/calendar/vendor/ical.js/**/* /vendor/**/* !/vendor/vendor.js diff --git a/CHANGELOG.md b/CHANGELOG.md index a9824c81..0f75d5e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ _This release is scheduled to be released on 2020-10-01._ - Test coverage with Istanbul, run it with `npm run test:coverage`. - Add lithuanian language. - Added support in weatherforecast for OpenWeather onecall API. +- Added config option to calendar-icons for recurring- and fullday-events ### Updated diff --git a/modules/default/calendar/calendar.js b/modules/default/calendar/calendar.js index 5c6ce453..06026363 100755 --- a/modules/default/calendar/calendar.js +++ b/modules/default/calendar/calendar.js @@ -205,7 +205,7 @@ Module.register("calendar", { eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url); } - eventWrapper.className = "normal"; + eventWrapper.className = "normal event"; if (this.config.displaySymbol) { var symbolWrapper = document.createElement("td"); @@ -217,11 +217,7 @@ Module.register("calendar", { var symbolClass = this.symbolClassForUrl(event.url); symbolWrapper.className = "symbol align-right " + symbolClass; - var symbols = this.symbolsForUrl(event.url); - if (typeof symbols === "string") { - symbols = [symbols]; - } - + var symbols = this.symbolsForEvent(event); for (var i = 0; i < symbols.length; i++) { var symbol = document.createElement("span"); symbol.className = "fa fa-fw fa-" + symbols[i]; @@ -230,6 +226,7 @@ Module.register("calendar", { } symbolWrapper.appendChild(symbol); } + eventWrapper.appendChild(symbolWrapper); } else if (this.config.timeFormat === "dateheaders") { var blankCell = document.createElement("td"); @@ -559,15 +556,33 @@ Module.register("calendar", { }, /** - * symbolsForUrl(url) - * Retrieves the symbols for a specific url. + * symbolsForEvent(event) + * Retrieves the symbols for a specific event. * - * argument url string - Url to look for. + * argument event object - Event to look for. * - * return string/array - The Symbols + * return array - The Symbols */ - symbolsForUrl: function (url) { - return this.getCalendarProperty(url, "symbol", this.config.defaultSymbol); + symbolsForEvent: function (event) { + let symbols = this.getCalendarPropertyAsArray(event.url, "symbol", this.config.defaultSymbol); + + if (event.recurringEvent === true && this.hasCalendarProperty(event.url, "recurringSymbol")) { + symbols = this.mergeUnique(this.getCalendarPropertyAsArray(event.url, "recurringSymbol", this.config.defaultSymbol), symbols); + } + + if (event.fullDayEvent === true && this.hasCalendarProperty(event.url, "fullDaySymbol")) { + symbols = this.mergeUnique(this.getCalendarPropertyAsArray(event.url, "fullDaySymbol", this.config.defaultSymbol), symbols); + } + + return symbols; + }, + + mergeUnique: function (arr1, arr2) { + return arr1.concat( + arr2.filter(function (item) { + return arr1.indexOf(item) === -1; + }) + ); }, /** @@ -659,6 +674,16 @@ Module.register("calendar", { return defaultValue; }, + getCalendarPropertyAsArray: function (url, property, defaultValue) { + let p = this.getCalendarProperty(url, property, defaultValue); + if (!(p instanceof Array)) p = [p]; + return p; + }, + + hasCalendarProperty: function (url, property) { + return !!this.getCalendarProperty(url, property, undefined); + }, + /** * Shortens a string if it's longer than maxLength and add a ellipsis to the end * @@ -756,7 +781,7 @@ Module.register("calendar", { var calendar = this.calendarData[url]; for (var e in calendar) { var event = cloneObject(calendar[e]); - event.symbol = this.symbolsForUrl(url); + event.symbol = this.symbolsForEvent(event); event.calendarName = this.calendarNameForUrl(url); event.color = this.colorForUrl(url); delete event.url; diff --git a/modules/default/calendar/calendarfetcher.js b/modules/default/calendar/calendarfetcher.js index acae13dc..888830ba 100644 --- a/modules/default/calendar/calendarfetcher.js +++ b/modules/default/calendar/calendarfetcher.js @@ -9,7 +9,7 @@ const ical = require("ical"); const moment = require("moment"); const request = require("request"); -const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNumberOfDays, auth, includePastEvents) { +const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents) { const self = this; let reloadTimer = null; @@ -254,6 +254,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNu startDate: startDate.format("x"), endDate: endDate.format("x"), fullDayEvent: isFullDayEvent(event), + recurringEvent: true, class: event.class, firstYear: event.start.getFullYear(), location: location, @@ -317,7 +318,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNu return a.startDate - b.startDate; }); - events = newEvents; + events = newEvents.slice(0, maximumEntries); self.broadcastEvents(); scheduleTimer(); diff --git a/modules/default/calendar/node_helper.js b/modules/default/calendar/node_helper.js index 5248919b..e731ca6c 100644 --- a/modules/default/calendar/node_helper.js +++ b/modules/default/calendar/node_helper.js @@ -20,7 +20,7 @@ module.exports = NodeHelper.create({ // Override socketNotificationReceived method. socketNotificationReceived: function (notification, payload) { if (notification === "ADD_CALENDAR") { - this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.id); + this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.id); } }, @@ -31,7 +31,7 @@ module.exports = NodeHelper.create({ * attribute url string - URL of the news feed. * attribute reloadInterval number - Reload interval in milliseconds. */ - createFetcher: function (url, fetchInterval, excludedEvents, maximumNumberOfDays, auth, broadcastPastEvents, identifier) { + createFetcher: function (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, identifier) { var self = this; if (!validUrl.isUri(url)) { @@ -42,7 +42,7 @@ module.exports = NodeHelper.create({ var fetcher; if (typeof self.fetchers[identifier + url] === "undefined") { Log.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval); - fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumNumberOfDays, auth, broadcastPastEvents); + fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents); fetcher.onReceive(function (fetcher) { self.sendSocketNotification("CALENDAR_EVENTS", { diff --git a/tests/configs/data/StripComments.json b/tests/configs/data/StripComments.json deleted file mode 100644 index e9d1c403..00000000 --- a/tests/configs/data/StripComments.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - // Escaped - "FOO\"BAR": "Today", - - /* - * The following lines - * represent cardinal directions - */ - "N": "N", - "E": "E", - "S": "S", - "W": "W" -} diff --git a/tests/configs/data/calendar_test_icons.ics b/tests/configs/data/calendar_test_icons.ics new file mode 100644 index 00000000..7f24060d --- /dev/null +++ b/tests/configs/data/calendar_test_icons.ics @@ -0,0 +1,56 @@ +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 +END:VCALENDAR \ No newline at end of file diff --git a/tests/configs/modules/calendar/custom.js b/tests/configs/modules/calendar/custom.js new file mode 100644 index 00000000..2084419d --- /dev/null +++ b/tests/configs/modules/calendar/custom.js @@ -0,0 +1,41 @@ +/* Magic Mirror Test config custom calendar + * + * MIT Licensed. + */ +let config = { + port: 8080, + ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"], + + language: "en", + timeFormat: 12, + units: "metric", + electronOptions: { + webPreferences: { + nodeIntegration: true + } + }, + + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + calendars: [ + { + symbol: "birthday-cake", + fullDaySymbol: "calendar-day", + recurringSymbol: "undo", + maximumEntries: 4, + maximumNumberOfDays: 10000, + url: "http://localhost:8080/tests/configs/data/calendar_test_icons.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/e2e/modules/calendar_spec.js b/tests/e2e/modules/calendar_spec.js index 7af78201..3712871c 100644 --- a/tests/e2e/modules/calendar_spec.js +++ b/tests/e2e/modules/calendar_spec.js @@ -1,5 +1,6 @@ const helpers = require("../global-setup"); const serverBasicAuth = require("../../servers/basic-auth.js"); +const expect = require("chai").expect; const describe = global.describe; const it = global.it; @@ -31,8 +32,47 @@ describe("Calendar module", function () { process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/default.js"; }); - it("Should return TestEvents", function () { - return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000); + it("should show the default maximumEntries of 10", async () => { + await app.client.waitUntilTextExists(".calendar", "TestEvent", 10000); + const events = await app.client.$$(".calendar .event"); + return expect(events.length).equals(10); + }); + + it("should show the default calendar symbol in each event", async () => { + await app.client.waitUntilTextExists(".calendar", "TestEvent", 10000); + const icons = await app.client.$$(".calendar .event .fa-calendar"); + return expect(icons.length).not.equals(0); + }); + }); + + describe("Custom configuration", function () { + before(function () { + // Set config sample for use in test + process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/custom.js"; + }); + + it("should show the custom maximumEntries of 4", async () => { + await app.client.waitUntilTextExists(".calendar", "TestEvent", 10000); + const events = await app.client.$$(".calendar .event"); + return expect(events.length).equals(4); + }); + + it("should show the custom calendar symbol in each event", async () => { + await app.client.waitUntilTextExists(".calendar", "TestEvent", 10000); + const icons = await app.client.$$(".calendar .event .fa-birthday-cake"); + return expect(icons.length).equals(4); + }); + + it("should show two custom icons for repeating events", async () => { + await app.client.waitUntilTextExists(".calendar", "TestEventRepeat", 10000); + const icons = await app.client.$$(".calendar .event .fa-undo"); + return expect(icons.length).equals(2); + }); + + it("should show two custom icons for day events", async () => { + await app.client.waitUntilTextExists(".calendar", "TestEventDay", 10000); + const icons = await app.client.$$(".calendar .event .fa-calendar-day"); + return expect(icons.length).equals(2); }); }); @@ -47,7 +87,7 @@ describe("Calendar module", function () { serverBasicAuth.close(done()); }); - it("Should return TestEvents", function () { + it("should return TestEvents", function () { return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000); }); }); @@ -63,7 +103,7 @@ describe("Calendar module", function () { serverBasicAuth.close(done()); }); - it("Should return TestEvents", function () { + it("should return TestEvents", function () { return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000); }); }); @@ -79,7 +119,7 @@ describe("Calendar module", function () { serverBasicAuth.close(done()); }); - it("Should return TestEvents", function () { + it("should return TestEvents", function () { return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000); }); }); @@ -95,7 +135,7 @@ describe("Calendar module", function () { serverBasicAuth.close(done()); }); - it("Should return No upcoming events", function () { + it("should return No upcoming events", function () { return app.client.waitUntilTextExists(".calendar", "No upcoming events.", 10000); }); }); diff --git a/tests/unit/functions/calendar_spec.js b/tests/unit/functions/calendar_spec.js index e1d44e4a..44091816 100644 --- a/tests/unit/functions/calendar_spec.js +++ b/tests/unit/functions/calendar_spec.js @@ -32,54 +32,54 @@ describe("Functions into modules/default/calendar/calendar.js", function () { }); describe("getLocaleSpecification", function () { - it("Should return a valid moment.LocaleSpecification for a 12-hour format", function () { + it("should return a valid moment.LocaleSpecification for a 12-hour format", function () { expect(Module.definitions.calendar.getLocaleSpecification(12)).to.deep.equal({ longDateFormat: { LT: "h:mm A" } }); }); - it("Should return a valid moment.LocaleSpecification for a 24-hour format", function () { + it("should return a valid moment.LocaleSpecification for a 24-hour format", function () { expect(Module.definitions.calendar.getLocaleSpecification(24)).to.deep.equal({ longDateFormat: { LT: "HH:mm" } }); }); - it("Should return the current system locale when called without timeFormat number", function () { + it("should return the current system locale when called without timeFormat number", function () { expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: moment.localeData().longDateFormat("LT") } }); }); - it("Should return a 12-hour longDateFormat when using the 'en' locale", function () { + it("should return a 12-hour longDateFormat when using the 'en' locale", function () { var localeBackup = moment.locale(); moment.locale("en"); expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "h:mm A" } }); moment.locale(localeBackup); }); - it("Should return a 12-hour longDateFormat when using the 'au' locale", function () { + it("should return a 12-hour longDateFormat when using the 'au' locale", function () { var localeBackup = moment.locale(); moment.locale("au"); expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "h:mm A" } }); moment.locale(localeBackup); }); - it("Should return a 12-hour longDateFormat when using the 'eg' locale", function () { + it("should return a 12-hour longDateFormat when using the 'eg' locale", function () { var localeBackup = moment.locale(); moment.locale("eg"); expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "h:mm A" } }); moment.locale(localeBackup); }); - it("Should return a 24-hour longDateFormat when using the 'nl' locale", function () { + it("should return a 24-hour longDateFormat when using the 'nl' locale", function () { var localeBackup = moment.locale(); moment.locale("nl"); expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "HH:mm" } }); moment.locale(localeBackup); }); - it("Should return a 24-hour longDateFormat when using the 'fr' locale", function () { + it("should return a 24-hour longDateFormat when using the 'fr' locale", function () { var localeBackup = moment.locale(); moment.locale("fr"); expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "HH:mm" } }); moment.locale(localeBackup); }); - it("Should return a 24-hour longDateFormat when using the 'uk' locale", function () { + it("should return a 24-hour longDateFormat when using the 'uk' locale", function () { var localeBackup = moment.locale(); moment.locale("uk"); expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "HH:mm" } });