From 9f591bff03769f0b1e93ca514d157fdec8461ee8 Mon Sep 17 00:00:00 2001 From: Joshua Kriegshauser Date: Mon, 8 Jan 2024 17:52:44 -0800 Subject: [PATCH 01/15] Some cleanup --- .../default/calendar/calendarfetcherutils.js | 111 +++++++++--------- 1 file changed, 58 insertions(+), 53 deletions(-) diff --git a/modules/default/calendar/calendarfetcherutils.js b/modules/default/calendar/calendarfetcherutils.js index c2067296cd..a387777b21 100644 --- a/modules/default/calendar/calendarfetcherutils.js +++ b/modules/default/calendar/calendarfetcherutils.js @@ -3,6 +3,7 @@ */ const path = require("node:path"); const moment = require("moment"); +const assert = require("assert"); const zoneTable = require(path.join(__dirname, "windowsZones.json")); const Log = require("../../../js/logger"); @@ -128,24 +129,26 @@ const CalendarFetcherUtils = { }; const eventDate = function (event, time) { - return CalendarFetcherUtils.isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time])); + return CalendarFetcherUtils.isFullDayEvent(event) ? moment(event[time]).startOf("day") : moment(event[time]); }; Log.debug(`There are ${Object.entries(data).length} calendar entries.`); + + const now = new Date(); + const todayLocal = moment().startOf("day").toDate(); + const futureLocalDate + = moment() + .startOf("day") + .add(config.maximumNumberOfDays, "days") + .subtract(1, "seconds") // Subtract 1 second so that events that start on the middle of the night will not repeat. + .toDate(); + Object.entries(data).forEach(([key, event]) => { Log.debug("Processing entry..."); - const now = new Date(); - const today = moment().startOf("day").toDate(); - const future - = moment() - .startOf("day") - .add(config.maximumNumberOfDays, "days") - .subtract(1, "seconds") // Subtract 1 second so that events that start on the middle of the night will not repeat. - .toDate(); - let past = today; + let pastLocalDate = todayLocal; if (config.includePastEvents) { - past = moment().startOf("day").subtract(config.maximumNumberOfDays, "days").toDate(); + pastLocalDate = moment().startOf("day").subtract(config.maximumNumberOfDays, "days").toDate(); } // FIXME: Ugly fix to solve the facebook birthday issue. @@ -159,33 +162,33 @@ const CalendarFetcherUtils = { if (event.type === "VEVENT") { Log.debug(`Event:\n${JSON.stringify(event)}`); - let startDate = eventDate(event, "start"); - let endDate; + let startMoment = eventDate(event, "start"); + let endMoment; if (typeof event.end !== "undefined") { - endDate = eventDate(event, "end"); + endMoment = eventDate(event, "end"); } else if (typeof event.duration !== "undefined") { - endDate = startDate.clone().add(moment.duration(event.duration)); + endMoment = startMoment.clone().add(moment.duration(event.duration)); } else { if (!isFacebookBirthday) { // make copy of start date, separate storage area - endDate = moment(startDate.format("x"), "x"); + endMoment = moment(startMoment.valueOf()); } else { - endDate = moment(startDate).add(1, "days"); + endMoment = moment(startMoment).add(1, "days"); } } - Log.debug(`start: ${startDate.toDate()}`); - Log.debug(`end:: ${endDate.toDate()}`); + Log.debug(`start: ${startMoment.toDate()}`); + Log.debug(`end:: ${endMoment.toDate()}`); // Calculate the duration of the event for use with recurring events. - let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x")); - Log.debug(`duration: ${duration}`); + let durationMs = endMoment.valueOf() - startMoment.valueOf(); + Log.debug(`duration: ${durationMs}`); // FIXME: Since the parsed json object from node-ical comes with time information // this check could be removed (?) if (event.start.length === 8) { - startDate = startDate.startOf("day"); + startMoment = startMoment.startOf("day"); } const title = CalendarFetcherUtils.getTitleFromEvent(event); @@ -245,11 +248,11 @@ const CalendarFetcherUtils = { const geo = event.geo || false; const description = event.description || false; - if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) { + if (event.rrule && typeof event.rrule !== "undefined" && !isFacebookBirthday) { const rule = event.rrule; - const pastMoment = moment(past); - const futureMoment = moment(future); + const pastMoment = moment(pastLocalDate); + const futureMoment = moment(futureLocalDate); // can cause problems with e.g. birthdays before 1900 if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) { @@ -260,8 +263,8 @@ const CalendarFetcherUtils = { // For recurring events, get the set of start dates that fall within the range // of dates we're looking for. // kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time - let pastLocal = 0; - let futureLocal = 0; + let pastLocal; + let futureLocal; if (CalendarFetcherUtils.isFullDayEvent(event)) { Log.debug("fullday"); // if full day event, only use the date part of the ranges @@ -298,10 +301,11 @@ const CalendarFetcherUtils = { Log.debug(`event.recurrences: ${event.recurrences}`); if (event.recurrences !== undefined) { for (let r in event.recurrences) { + let ev = event.recurrences[r] // Only add dates that weren't already in the range we added from the rrule so that // we don"t double-add those events. - if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) { - dates.push(new Date(r)); + if (!dates.includes(ev.start) && moment(ev.start).isBetween(pastMoment, futureMoment)) { + dates.push(ev.start); } } } @@ -395,10 +399,11 @@ const CalendarFetcherUtils = { //} } } - startDate = moment(date); - Log.debug(`Corrected startDate: ${startDate.toDate()}`); - let adjustDays = CalendarFetcherUtils.calculateTimezoneAdjustment(event, date); + startMoment = moment(date); + Log.debug(`Corrected startDate: ${startMoment.toDate()}`); + + let adjustHours = CalendarFetcherUtils.calculateTimezoneAdjustment(event, date); // Remove the time information of each date by using its substring, using the following method: // .toISOString().substring(0,10). @@ -411,30 +416,30 @@ const CalendarFetcherUtils = { if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) { // We found an override, so for this recurrence, use a potentially different title, start date, and duration. curEvent = curEvent.recurrences[dateKey]; - startDate = moment(curEvent.start); - duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x")); + startMoment = moment(curEvent.start); + durationMs = curEvent.end.valueOf() - startMoment.valueOf(); } // If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule. else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) { // This date is an exception date, which means we should skip it in the recurrence pattern. showRecurrence = false; } - Log.debug(`duration: ${duration}`); + Log.debug(`duration: ${durationMs}`); - endDate = moment(parseInt(startDate.format("x")) + duration, "x"); - if (startDate.format("x") === endDate.format("x")) { - endDate = endDate.endOf("day"); + endMoment = moment(startMoment.valueOf() + durationMs); + if (startMoment.valueOf() === endMoment.valueOf()) { + endMoment = endMoment.endOf("day"); } const recurrenceTitle = CalendarFetcherUtils.getTitleFromEvent(curEvent); // If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add // it to the event list. - if (endDate.isBefore(past) || startDate.isAfter(future)) { + if (endMoment.isBefore(pastLocal) || startMoment.isAfter(futureLocal)) { showRecurrence = false; } - if (CalendarFetcherUtils.timeFilterApplies(now, endDate, dateFilter)) { + if (CalendarFetcherUtils.timeFilterApplies(now, endMoment, dateFilter)) { showRecurrence = false; } @@ -442,8 +447,8 @@ const CalendarFetcherUtils = { Log.debug(`saving event: ${description}`); newEvents.push({ title: recurrenceTitle, - startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"), - endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"), + startDate: (adjustHours ? (adjustHours > 0 ? startMoment.add(adjustHours, "hours") : startMoment.subtract(Math.abs(adjustHours), "hours")) : startMoment).format("x"), + endDate: (adjustHours ? (adjustHours > 0 ? endMoment.add(adjustHours, "hours") : endMoment.subtract(Math.abs(adjustHours), "hours")) : endMoment).format("x"), fullDayEvent: CalendarFetcherUtils.isFullDayEvent(event), recurringEvent: true, class: event.class, @@ -461,43 +466,43 @@ const CalendarFetcherUtils = { // Log.debug("full day event") // if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00) - if (fullDayEvent && startDate.format("x") === endDate.format("x")) { - endDate = endDate.endOf("day"); + if (fullDayEvent && startMoment.valueOf() === endMoment.valueOf()) { + endMoment = endMoment.endOf("day"); } if (config.includePastEvents) { // Past event is too far in the past, so skip. - if (endDate < past) { + if (endMoment < pastLocalDate) { return; } } else { // It's not a fullday event, and it is in the past, so skip. - if (!fullDayEvent && endDate < new Date()) { + if (!fullDayEvent && endMoment < new Date()) { return; } // It's a fullday event, and it is before today, So skip. - if (fullDayEvent && endDate <= today) { + if (fullDayEvent && endMoment <= todayLocal) { return; } } // It exceeds the maximumNumberOfDays limit, so skip. - if (startDate > future) { + if (startMoment > futureLocalDate) { return; } - if (CalendarFetcherUtils.timeFilterApplies(now, endDate, dateFilter)) { + if (CalendarFetcherUtils.timeFilterApplies(now, endMoment, dateFilter)) { return; } // get correction for date saving and dst change between now and then - let adjustDays = CalendarFetcherUtils.calculateTimezoneAdjustment(event, startDate.toDate()); + let adjustHours = CalendarFetcherUtils.calculateTimezoneAdjustment(event, startMoment.toDate()); // Every thing is good. Add it to the list. newEvents.push({ title: title, - startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"), - endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"), + startDate: (adjustHours ? (adjustHours > 0 ? startMoment.add(adjustHours, "hours") : startMoment.subtract(Math.abs(adjustHours), "hours")) : startMoment).format("x"), + endDate: (adjustHours ? (adjustHours > 0 ? endMoment.add(adjustHours, "hours") : endMoment.subtract(Math.abs(adjustHours), "hours")) : endMoment).format("x"), fullDayEvent: fullDayEvent, class: event.class, location: location, @@ -578,7 +583,7 @@ const CalendarFetcherUtils = { increment = until[1].slice(-1) === "s" ? until[1] : `${until[1]}s`, // Massage the data for moment js filterUntil = moment(endDate.format()).subtract(value, increment); - return now < filterUntil.format("x"); + return now < filterUntil.toDate(); } return false; From d04d2c9607c4e3f55791534af57e134ce094b536 Mon Sep 17 00:00:00 2001 From: Joshua Kriegshauser Date: Tue, 9 Jan 2024 08:43:40 -0800 Subject: [PATCH 02/15] Lint fixes --- modules/default/calendar/calendarfetcherutils.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/default/calendar/calendarfetcherutils.js b/modules/default/calendar/calendarfetcherutils.js index a387777b21..c42821e235 100644 --- a/modules/default/calendar/calendarfetcherutils.js +++ b/modules/default/calendar/calendarfetcherutils.js @@ -3,7 +3,6 @@ */ const path = require("node:path"); const moment = require("moment"); -const assert = require("assert"); const zoneTable = require(path.join(__dirname, "windowsZones.json")); const Log = require("../../../js/logger"); @@ -301,7 +300,7 @@ const CalendarFetcherUtils = { Log.debug(`event.recurrences: ${event.recurrences}`); if (event.recurrences !== undefined) { for (let r in event.recurrences) { - let ev = event.recurrences[r] + let ev = event.recurrences[r]; // Only add dates that weren't already in the range we added from the rrule so that // we don"t double-add those events. if (!dates.includes(ev.start) && moment(ev.start).isBetween(pastMoment, futureMoment)) { From fa0b9e2ca26f823dcfbfce7eef91294243cb6cf2 Mon Sep 17 00:00:00 2001 From: Joshua Kriegshauser Date: Tue, 9 Jan 2024 08:52:46 -0800 Subject: [PATCH 03/15] changelog update to fix test --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86a024297a..afe4e750ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,12 @@ _This release is scheduled to be released on 2024-04-01._ - Unneeded file headers (#3358) +## [2.27.0] - UNRELEASED + +### Fixed + +- WIP + ## [2.26.0] - 01-01-2024 Thanks to: @bnitkin, @bugsounet, @dependabot, @jkriegshauser, @kaennchenstruggle, @KristjanESPERANTO and @Ybbet. From 0fd36ebe432eec46dc6b34f9209adce1785f322c Mon Sep 17 00:00:00 2001 From: Joshua Kriegshauser Date: Fri, 12 Jan 2024 12:03:00 -0800 Subject: [PATCH 04/15] Fixing some DST wonkiness --- .../default/calendar/calendarfetcherutils.js | 123 ++++-------------- 1 file changed, 28 insertions(+), 95 deletions(-) diff --git a/modules/default/calendar/calendarfetcherutils.js b/modules/default/calendar/calendarfetcherutils.js index c42821e235..f39286dfb6 100644 --- a/modules/default/calendar/calendarfetcherutils.js +++ b/modules/default/calendar/calendarfetcherutils.js @@ -290,6 +290,15 @@ const CalendarFetcherUtils = { if (JSON.stringify(d) === "null") return false; else return true; }); + + // The dates array from rrule can be confused by DST. If the event was created during DST and we + // are querying a date range during non-DST, rrule can have the incorrect time for the date range. + // Reprocess the array here computing and applying the time offset. + dates.forEach((date, index, arr) => { + let adjustHours = CalendarFetcherUtils.calculateTimezoneAdjustment(event, date); + arr[index] = new Date(date.valueOf() + (adjustHours * 60 * 60 * 1000)); + }); + // The "dates" array contains the set of dates within our desired date range range that are valid // for the recurrence rule. *However*, it's possible for us to have a specific recurrence that // had its date changed from outside the range to inside the range. For the time being, @@ -302,9 +311,17 @@ const CalendarFetcherUtils = { for (let r in event.recurrences) { let ev = event.recurrences[r]; // Only add dates that weren't already in the range we added from the rrule so that - // we don"t double-add those events. - if (!dates.includes(ev.start) && moment(ev.start).isBetween(pastMoment, futureMoment)) { - dates.push(ev.start); + // we don't double-add those events. Unfortunately dates.includes doesn't do an exact match. + if (moment(ev.start).isBetween(pastMoment, futureMoment)) { + let found = false; + dates.forEach((d) => { + if (d.valueOf() == ev.start.valueOf()) { + found = true; + } + }); + if (!found) { + dates.push(ev.start); + } } } } @@ -314,95 +331,7 @@ const CalendarFetcherUtils = { let curEvent = event; let showRecurrence = true; - // set the time information in the date to equal the time information in the event - date.setUTCHours(curEvent.start.getUTCHours(), curEvent.start.getUTCMinutes(), curEvent.start.getUTCSeconds(), curEvent.start.getUTCMilliseconds()); - - // Get the offset of today where we are processing - // This will be the correction, we need to apply. - let nowOffset = new Date().getTimezoneOffset(); - // For full day events, the time might be off from RRULE/Luxon problem - // Get time zone offset of the rule calculated event - let dateoffset = date.getTimezoneOffset(); - - // Reduce the time by the following offset. - Log.debug(` recurring date is ${date} offset is ${dateoffset}`); - - let dh = moment(date).format("HH"); - Log.debug(` recurring date is ${date} offset is ${dateoffset / 60} Hour is ${dh}`); - - if (CalendarFetcherUtils.isFullDayEvent(event)) { - Log.debug("Fullday"); - // If the offset is negative (east of GMT), where the problem is - if (dateoffset < 0) { - if (dh < Math.abs(dateoffset / 60)) { - // if the rrule byweekday WAS explicitly set , correct it - // reduce the time by the offset - if (curEvent.rrule.origOptions.byweekday !== undefined) { - // Apply the correction to the date/time to get it UTC relative - date = new Date(date.getTime() - Math.abs(24 * 60) * 60000); - } - // the duration was calculated way back at the top before we could correct the start time.. - // fix it for this event entry - //duration = 24 * 60 * 60 * 1000; - Log.debug(`new recurring date1 fulldate is ${date}`); - } - } else { - // if the timezones are the same, correct date if needed - //if (event.start.tz === moment.tz.guess()) { - // if the date hour is less than the offset - if (24 - dh <= Math.abs(dateoffset / 60)) { - // if the rrule byweekday WAS explicitly set , correct it - if (curEvent.rrule.origOptions.byweekday !== undefined) { - // apply the correction to the date/time back to right day - date = new Date(date.getTime() + Math.abs(24 * 60) * 60000); - } - // the duration was calculated way back at the top before we could correct the start time.. - // fix it for this event entry - //duration = 24 * 60 * 60 * 1000; - Log.debug(`new recurring date2 fulldate is ${date}`); - } - //} - } - } else { - // not full day, but luxon can still screw up the date on the rule processing - // we need to correct the date to get back to the right event for - if (dateoffset < 0) { - // if the date hour is less than the offset - if (dh <= Math.abs(dateoffset / 60)) { - // if the rrule byweekday WAS explicitly set , correct it - if (curEvent.rrule.origOptions.byweekday !== undefined) { - // Reduce the time by t: - // Apply the correction to the date/time to get it UTC relative - date = new Date(date.getTime() - Math.abs(24 * 60) * 60000); - } - // the duration was calculated way back at the top before we could correct the start time.. - // fix it for this event entry - //duration = 24 * 60 * 60 * 1000; - Log.debug(`new recurring date1 is ${date}`); - } - } else { - // if the timezones are the same, correct date if needed - //if (event.start.tz === moment.tz.guess()) { - // if the date hour is less than the offset - if (24 - dh <= Math.abs(dateoffset / 60)) { - // if the rrule byweekday WAS explicitly set , correct it - if (curEvent.rrule.origOptions.byweekday !== undefined) { - // apply the correction to the date/time back to right day - date = new Date(date.getTime() + Math.abs(24 * 60) * 60000); - } - // the duration was calculated way back at the top before we could correct the start time.. - // fix it for this event entry - //duration = 24 * 60 * 60 * 1000; - Log.debug(`new recurring date2 is ${date}`); - } - //} - } - } - startMoment = moment(date); - Log.debug(`Corrected startDate: ${startMoment.toDate()}`); - - let adjustHours = CalendarFetcherUtils.calculateTimezoneAdjustment(event, date); // Remove the time information of each date by using its substring, using the following method: // .toISOString().substring(0,10). @@ -446,8 +375,8 @@ const CalendarFetcherUtils = { Log.debug(`saving event: ${description}`); newEvents.push({ title: recurrenceTitle, - startDate: (adjustHours ? (adjustHours > 0 ? startMoment.add(adjustHours, "hours") : startMoment.subtract(Math.abs(adjustHours), "hours")) : startMoment).format("x"), - endDate: (adjustHours ? (adjustHours > 0 ? endMoment.add(adjustHours, "hours") : endMoment.subtract(Math.abs(adjustHours), "hours")) : endMoment).format("x"), + startDate: startMoment.format("x"), + endDate: endMoment.format("x"), fullDayEvent: CalendarFetcherUtils.isFullDayEvent(event), recurringEvent: true, class: event.class, @@ -497,11 +426,15 @@ const CalendarFetcherUtils = { // get correction for date saving and dst change between now and then let adjustHours = CalendarFetcherUtils.calculateTimezoneAdjustment(event, startMoment.toDate()); + // This shouldn't happen + if (adjustHours) { + Log.warn(`Unexpected timezone adjustment of ${adjustHours} hours on non-recurring event`); + } // Every thing is good. Add it to the list. newEvents.push({ title: title, - startDate: (adjustHours ? (adjustHours > 0 ? startMoment.add(adjustHours, "hours") : startMoment.subtract(Math.abs(adjustHours), "hours")) : startMoment).format("x"), - endDate: (adjustHours ? (adjustHours > 0 ? endMoment.add(adjustHours, "hours") : endMoment.subtract(Math.abs(adjustHours), "hours")) : endMoment).format("x"), + startDate: startMoment.add(adjustHours, "hours").format("x"), + endDate: endMoment.add(adjustHours, "hours").format("x"), fullDayEvent: fullDayEvent, class: event.class, location: location, From 57ccbe6c97597741e239c6b3ed821c45f830e0d0 Mon Sep 17 00:00:00 2001 From: Joshua Kriegshauser Date: Wed, 17 Jan 2024 14:27:22 -0800 Subject: [PATCH 05/15] lint:js --- modules/default/calendar/calendarfetcherutils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/default/calendar/calendarfetcherutils.js b/modules/default/calendar/calendarfetcherutils.js index f39286dfb6..071b7b05f4 100644 --- a/modules/default/calendar/calendarfetcherutils.js +++ b/modules/default/calendar/calendarfetcherutils.js @@ -314,8 +314,8 @@ const CalendarFetcherUtils = { // we don't double-add those events. Unfortunately dates.includes doesn't do an exact match. if (moment(ev.start).isBetween(pastMoment, futureMoment)) { let found = false; - dates.forEach((d) => { - if (d.valueOf() == ev.start.valueOf()) { + dates.forEach((d) => { + if (d.valueOf() === ev.start.valueOf()) { found = true; } }); From 90dc84c424ec0ca670765a18aeb23cdd554df82e Mon Sep 17 00:00:00 2001 From: Joshua Kriegshauser Date: Fri, 19 Jan 2024 13:47:18 -0800 Subject: [PATCH 06/15] Allow passing in TZ --- tests/electron/helpers/global-setup.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/electron/helpers/global-setup.js b/tests/electron/helpers/global-setup.js index 164c7f6630..71dfd0ddbe 100644 --- a/tests/electron/helpers/global-setup.js +++ b/tests/electron/helpers/global-setup.js @@ -3,11 +3,11 @@ // 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"]) => { +exports.startApplication = async (configFilename, systemDate = null, electronParams = ["js/electron.js"], timezone = "GMT") => { global.electronApp = null; global.page = null; process.env.MM_CONFIG_FILE = configFilename; - process.env.TZ = "GMT"; + process.env.TZ = timezone; global.electronApp = await electron.launch({ args: electronParams }); await global.electronApp.firstWindow(); @@ -20,7 +20,7 @@ exports.startApplication = async (configFilename, systemDate = null, electronPar if (systemDate) { await global.page.evaluate((systemDate) => { Date.now = () => { - return new Date(systemDate); + return new Date(systemDate).valueOf(); }; }, systemDate); } From fac839da1ae0acb9c430bd4a6e73dba1f0ecc142 Mon Sep 17 00:00:00 2001 From: Joshua Kriegshauser Date: Fri, 19 Jan 2024 13:47:46 -0800 Subject: [PATCH 07/15] Tests can force using current time --- modules/default/calendar/calendar.js | 14 +++++++++++--- tests/configs/modules/calendar/custom.js | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/modules/default/calendar/calendar.js b/modules/default/calendar/calendar.js index fe08a883af..3b10b922d1 100644 --- a/modules/default/calendar/calendar.js +++ b/modules/default/calendar/calendar.js @@ -36,6 +36,7 @@ Module.register("calendar", { hideDuplicates: true, showTimeToday: false, colored: false, + forceUseCurrentTime: false, tableClass: "small", calendars: [ { @@ -567,9 +568,16 @@ Module.register("calendar", { const ONE_HOUR = ONE_MINUTE * 60; const ONE_DAY = ONE_HOUR * 24; - const now = new Date(); - const today = moment().startOf("day"); - const future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate(); + let now, today, future; + if (this.config.forceUseCurrentTime || this.defaults.forceUseCurrentTime) { + now = new Date(); + today = moment().startOf("day"); + future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate(); + } else { + now = new Date(Date.now()); // Can use overridden time + today = moment(now).startOf("day"); + future = moment(now).startOf("day").add(this.config.maximumNumberOfDays, "days").toDate(); + } let events = []; for (const calendarUrl in this.calendarData) { diff --git a/tests/configs/modules/calendar/custom.js b/tests/configs/modules/calendar/custom.js index 4c6deb7838..8552bcbe5c 100644 --- a/tests/configs/modules/calendar/custom.js +++ b/tests/configs/modules/calendar/custom.js @@ -7,6 +7,7 @@ let config = { position: "bottom_bar", config: { customEvents: [{ keyword: "CustomEvent", symbol: "dice", eventClass: "undo" }], + forceUseCurrentTime: true, calendars: [ { maximumEntries: 5, From 2b2715a7ba5e5973ad7c91c540ea66b38cd1f9e8 Mon Sep 17 00:00:00 2001 From: Joshua Kriegshauser Date: Fri, 19 Jan 2024 13:48:20 -0800 Subject: [PATCH 08/15] Revamp a bunch of rrule workarounds --- .../default/calendar/calendarfetcherutils.js | 58 ++++++++++++++++--- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/modules/default/calendar/calendarfetcherutils.js b/modules/default/calendar/calendarfetcherutils.js index 071b7b05f4..2d020c541a 100644 --- a/modules/default/calendar/calendarfetcherutils.js +++ b/modules/default/calendar/calendarfetcherutils.js @@ -56,7 +56,7 @@ const CalendarFetcherUtils = { event.start.tz = ""; Log.debug(`ical offset=${current_offset} date=${date}`); mm = moment(date); - let x = parseInt(moment(new Date()).utcOffset()); + let x = moment(new Date()).utcOffset(); Log.debug(`net mins=${current_offset * 60 - x}`); mm = mm.add(x - current_offset * 60, "minutes"); @@ -133,10 +133,10 @@ const CalendarFetcherUtils = { Log.debug(`There are ${Object.entries(data).length} calendar entries.`); - const now = new Date(); - const todayLocal = moment().startOf("day").toDate(); + const now = new Date(Date.now()); + const todayLocal = moment(now).startOf("day").toDate(); const futureLocalDate - = moment() + = moment(now) .startOf("day") .add(config.maximumNumberOfDays, "days") .subtract(1, "seconds") // Subtract 1 second so that events that start on the middle of the night will not repeat. @@ -147,7 +147,7 @@ const CalendarFetcherUtils = { let pastLocalDate = todayLocal; if (config.includePastEvents) { - pastLocalDate = moment().startOf("day").subtract(config.maximumNumberOfDays, "days").toDate(); + pastLocalDate = moment(now).startOf("day").subtract(config.maximumNumberOfDays, "days").toDate(); } // FIXME: Ugly fix to solve the facebook birthday issue. @@ -279,24 +279,48 @@ const CalendarFetcherUtils = { pastLocal = pastMoment.toDate(); } else { // otherwise use NOW.. cause we shouldn't use any before now - pastLocal = moment().toDate(); //now + pastLocal = moment(now).toDate(); //now } futureLocal = futureMoment.toDate(); // future } Log.debug(`Search for recurring events between: ${pastLocal} and ${futureLocal}`); - let dates = rule.between(pastLocal, futureLocal, true, limitFunction); + const hasByWeekdayRule = rule.options.byweekday !== undefined && rule.options.byweekday !== null; + const oneDayInMs = 24 * 60 * 60 * 1000; + Log.debug(`RRule: ${rule.toString()}`); + let dates = rule.between(new Date(pastLocal.valueOf() - oneDayInMs), new Date(futureLocal.valueOf() + oneDayInMs), true, () => { return true; }); Log.debug(`Title: ${event.summary}, with dates: ${JSON.stringify(dates)}`); dates = dates.filter((d) => { if (JSON.stringify(d) === "null") return false; else return true; }); + // RRule can generate dates with an incorrect recurrence date. Process the array here and apply date correction. + if (hasByWeekdayRule) { + Log.debug("Rule has byweekday, checking for correction"); + dates.forEach((date, index, arr) => { + // NOTE: getTimezoneOffset() is negative of the expected value. For America/Los_Angeles under DST (GMT-7), + // this value is +420. For Australia/Sydney under DST (GMT+11), this value is -660. + const tzOffset = date.getTimezoneOffset() / 60; + const hour = date.getHours(); + if ((tzOffset < 0) && (hour < -tzOffset)) { // east of GMT + Log.debug(`East of GMT (tzOffset: ${tzOffset}) and hour=${hour} < ${-tzOffset}, Subtracting 1 day from ${date}`); + arr[index] = new Date(date.valueOf() - oneDayInMs); + } else if ((tzOffset > 0) && (hour >= (24 - tzOffset))) { // west of GMT + Log.debug(`West of GMT (tzOffset: ${tzOffset}) and hour=${hour} >= 24-${tzOffset}, Adding 1 day to ${date}`); + arr[index] = new Date(date.valueOf() + oneDayInMs); + } + }); + } + // The dates array from rrule can be confused by DST. If the event was created during DST and we // are querying a date range during non-DST, rrule can have the incorrect time for the date range. // Reprocess the array here computing and applying the time offset. dates.forEach((date, index, arr) => { let adjustHours = CalendarFetcherUtils.calculateTimezoneAdjustment(event, date); - arr[index] = new Date(date.valueOf() + (adjustHours * 60 * 60 * 1000)); + if (adjustHours !== 0) { + Log.debug(`Applying timezone adjustment hours=${adjustHours} to ${date}`); + arr[index] = new Date(date.valueOf() + (adjustHours * 60 * 60 * 1000)); + } }); // The "dates" array contains the set of dates within our desired date range range that are valid @@ -325,12 +349,28 @@ const CalendarFetcherUtils = { } } } + + // Lastly, sometimes rrule doesn't include the event.start even if it is in the requested range. Ensure + // inclusion here. + { + let found = false; + dates.forEach((d) => { if (d.valueOf() === event.start.valueOf()) found = true; }); + if (!found) { + Log.debug(`event.start=${event.start} was not included in results from rrule; adding`); + dates.splice(0, 0, event.start); + } + } + // Loop through the set of date entries to see which recurrences should be added to our event list. for (let d in dates) { let date = dates[d]; let curEvent = event; let showRecurrence = true; + if (date < pastLocal || date > futureLocal) { + continue; + } + startMoment = moment(date); // Remove the time information of each date by using its substring, using the following method: @@ -405,7 +445,7 @@ const CalendarFetcherUtils = { } } else { // It's not a fullday event, and it is in the past, so skip. - if (!fullDayEvent && endMoment < new Date()) { + if (!fullDayEvent && endMoment < now) { return; } From 99f8dafa32d25b081452d2169b306fc91ebfa9b1 Mon Sep 17 00:00:00 2001 From: Joshua Kriegshauser Date: Fri, 19 Jan 2024 13:48:43 -0800 Subject: [PATCH 09/15] exdate tests --- ...exdate.js => exdate_la_at_midnight_dst.js} | 8 +- .../calendar/exdate_la_at_midnight_std.js | 40 +++++++++ .../calendar/exdate_la_before_midnight.js | 40 +++++++++ .../calendar/exdate_syd_at_midnight_dst.js | 40 +++++++++ .../calendar/exdate_syd_at_midnight_std.js | 40 +++++++++ .../calendar/exdate_syd_before_midnight.js | 40 +++++++++ tests/electron/modules/calendar_spec.js | 89 +++++++++++++++++-- tests/mocks/calendar_test_exdate.ics | 34 ------- tests/mocks/exdate_la_at_midnight_dst.ics | 15 ++++ tests/mocks/exdate_la_at_midnight_std.ics | 15 ++++ tests/mocks/exdate_la_before_midnight.ics | 15 ++++ tests/mocks/exdate_syd_at_midnight_dst.ics | 15 ++++ tests/mocks/exdate_syd_at_midnight_std.ics | 15 ++++ tests/mocks/exdate_syd_before_midnight.ics | 15 ++++ 14 files changed, 380 insertions(+), 41 deletions(-) rename tests/configs/modules/calendar/{exdate.js => exdate_la_at_midnight_dst.js} (75%) create mode 100644 tests/configs/modules/calendar/exdate_la_at_midnight_std.js create mode 100644 tests/configs/modules/calendar/exdate_la_before_midnight.js create mode 100644 tests/configs/modules/calendar/exdate_syd_at_midnight_dst.js create mode 100644 tests/configs/modules/calendar/exdate_syd_at_midnight_std.js create mode 100644 tests/configs/modules/calendar/exdate_syd_before_midnight.js delete mode 100644 tests/mocks/calendar_test_exdate.ics create mode 100644 tests/mocks/exdate_la_at_midnight_dst.ics create mode 100644 tests/mocks/exdate_la_at_midnight_std.ics create mode 100644 tests/mocks/exdate_la_before_midnight.ics create mode 100644 tests/mocks/exdate_syd_at_midnight_dst.ics create mode 100644 tests/mocks/exdate_syd_at_midnight_std.ics create mode 100644 tests/mocks/exdate_syd_before_midnight.ics diff --git a/tests/configs/modules/calendar/exdate.js b/tests/configs/modules/calendar/exdate_la_at_midnight_dst.js similarity index 75% rename from tests/configs/modules/calendar/exdate.js rename to tests/configs/modules/calendar/exdate_la_at_midnight_dst.js index bb52561aa0..f112943701 100644 --- a/tests/configs/modules/calendar/exdate.js +++ b/tests/configs/modules/calendar/exdate_la_at_midnight_dst.js @@ -16,8 +16,8 @@ let config = { calendars: [ { maximumEntries: 100, - maximumNumberOfDays: 364, - url: "http://localhost:8080/tests/mocks/calendar_test_exdate.ics" + maximumNumberOfDays: 28, // 4 weeks, 2 of which are skipped + url: "http://localhost:8080/tests/mocks/exdate_la_at_midnight_dst.ics" } ] } @@ -25,6 +25,10 @@ let config = { ] }; +Date.now = () => { + return new Date("19 Oct 2023 12:30:00 GMT-07:00").valueOf(); +}; + /*************** DO NOT EDIT THE LINE BELOW ***************/ if (typeof module !== "undefined") { module.exports = config; diff --git a/tests/configs/modules/calendar/exdate_la_at_midnight_std.js b/tests/configs/modules/calendar/exdate_la_at_midnight_std.js new file mode 100644 index 0000000000..2eb96fa643 --- /dev/null +++ b/tests/configs/modules/calendar/exdate_la_at_midnight_std.js @@ -0,0 +1,40 @@ +/* MagicMirror² Test calendar exdate + * + * By jkriegshauser + * MIT Licensed. + * + * NOTE: calendar_test_exdate.ics has exdate entries for the next 20 years, but without some + * way to set a debug date for tests, this test may become flaky on specific days (i.e. could + * not test easily on leap-years, the BYDAY specified in exdate, etc.) or when the 20 years + * elapses if this project is still in active development ;) + * See issue #3250 + */ +let config = { + timeFormat: 12, + + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + maximumEntries: 100, + calendars: [ + { + maximumEntries: 100, + maximumNumberOfDays: 28, // 4 weeks, 2 of which are skipped + url: "http://localhost:8080/tests/mocks/exdate_la_at_midnight_std.ics" + } + ] + } + } + ] +}; + +Date.now = () => { + return new Date("19 Oct 2023 12:30:00 GMT-07:00").valueOf(); +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/configs/modules/calendar/exdate_la_before_midnight.js b/tests/configs/modules/calendar/exdate_la_before_midnight.js new file mode 100644 index 0000000000..73cf500912 --- /dev/null +++ b/tests/configs/modules/calendar/exdate_la_before_midnight.js @@ -0,0 +1,40 @@ +/* MagicMirror² Test calendar exdate + * + * By jkriegshauser + * MIT Licensed. + * + * NOTE: calendar_test_exdate.ics has exdate entries for the next 20 years, but without some + * way to set a debug date for tests, this test may become flaky on specific days (i.e. could + * not test easily on leap-years, the BYDAY specified in exdate, etc.) or when the 20 years + * elapses if this project is still in active development ;) + * See issue #3250 + */ +let config = { + timeFormat: 12, + + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + maximumEntries: 100, + calendars: [ + { + maximumEntries: 100, + maximumNumberOfDays: 28, // 4 weeks, 2 of which are skipped + url: "http://localhost:8080/tests/mocks/exdate_la_before_midnight.ics" + } + ] + } + } + ] +}; + +Date.now = () => { + return new Date("19 Oct 2023 12:30:00 GMT-07:00").valueOf(); +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/configs/modules/calendar/exdate_syd_at_midnight_dst.js b/tests/configs/modules/calendar/exdate_syd_at_midnight_dst.js new file mode 100644 index 0000000000..000b7cbe55 --- /dev/null +++ b/tests/configs/modules/calendar/exdate_syd_at_midnight_dst.js @@ -0,0 +1,40 @@ +/* MagicMirror² Test calendar exdate + * + * By jkriegshauser + * MIT Licensed. + * + * NOTE: calendar_test_exdate.ics has exdate entries for the next 20 years, but without some + * way to set a debug date for tests, this test may become flaky on specific days (i.e. could + * not test easily on leap-years, the BYDAY specified in exdate, etc.) or when the 20 years + * elapses if this project is still in active development ;) + * See issue #3250 + */ +let config = { + timeFormat: 12, + + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + maximumEntries: 100, + calendars: [ + { + maximumEntries: 100, + maximumNumberOfDays: 28, // 4 weeks, 2 of which are skipped + url: "http://localhost:8080/tests/mocks/exdate_syd_at_midnight_dst.ics" + } + ] + } + } + ] +}; + +Date.now = () => { + return new Date("14 Sep 2023 12:30:00 GMT+10:00").valueOf(); +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/configs/modules/calendar/exdate_syd_at_midnight_std.js b/tests/configs/modules/calendar/exdate_syd_at_midnight_std.js new file mode 100644 index 0000000000..ccf18721c1 --- /dev/null +++ b/tests/configs/modules/calendar/exdate_syd_at_midnight_std.js @@ -0,0 +1,40 @@ +/* MagicMirror² Test calendar exdate + * + * By jkriegshauser + * MIT Licensed. + * + * NOTE: calendar_test_exdate.ics has exdate entries for the next 20 years, but without some + * way to set a debug date for tests, this test may become flaky on specific days (i.e. could + * not test easily on leap-years, the BYDAY specified in exdate, etc.) or when the 20 years + * elapses if this project is still in active development ;) + * See issue #3250 + */ +let config = { + timeFormat: 12, + + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + maximumEntries: 100, + calendars: [ + { + maximumEntries: 100, + maximumNumberOfDays: 28, // 4 weeks, 2 of which are skipped + url: "http://localhost:8080/tests/mocks/exdate_syd_at_midnight_std.ics" + } + ] + } + } + ] +}; + +Date.now = () => { + return new Date("14 Sep 2023 12:30:00 GMT+10:00").valueOf(); +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/configs/modules/calendar/exdate_syd_before_midnight.js b/tests/configs/modules/calendar/exdate_syd_before_midnight.js new file mode 100644 index 0000000000..1a6c87dc62 --- /dev/null +++ b/tests/configs/modules/calendar/exdate_syd_before_midnight.js @@ -0,0 +1,40 @@ +/* MagicMirror² Test calendar exdate + * + * By jkriegshauser + * MIT Licensed. + * + * NOTE: calendar_test_exdate.ics has exdate entries for the next 20 years, but without some + * way to set a debug date for tests, this test may become flaky on specific days (i.e. could + * not test easily on leap-years, the BYDAY specified in exdate, etc.) or when the 20 years + * elapses if this project is still in active development ;) + * See issue #3250 + */ +let config = { + timeFormat: 12, + + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + maximumEntries: 100, + calendars: [ + { + maximumEntries: 100, + maximumNumberOfDays: 28, // 4 weeks, 2 of which are skipped + url: "http://localhost:8080/tests/mocks/exdate_syd_before_midnight.ics" + } + ] + } + } + ] +}; + +Date.now = () => { + return new Date("14 Sep 2023 12:30:00 GMT+10:00").valueOf(); +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/electron/modules/calendar_spec.js b/tests/electron/modules/calendar_spec.js index 2af61a93df..3dcbd36f68 100644 --- a/tests/electron/modules/calendar_spec.js +++ b/tests/electron/modules/calendar_spec.js @@ -44,17 +44,96 @@ describe("Calendar module", () => { }); }); - describe("Exdate check", () => { - it("should show the recurring event 51 times (excluded once) in a 364-day (inclusive) period", async () => { - // test must run on a Thursday - await helpers.startApplication("tests/configs/modules/calendar/exdate.js", "14 Dec 2023 12:30:00 GMT"); + /****************************/ + // LOS ANGELES TESTS: + // In 2023, DST (GMT-7) was until 5 Nov, after which is standard (STD) (GMT-8) time. + // Test takes place on Thu 19 Oct, recurring event on a Wednesday. maximumNumberOfDays=28, so there should be + // 4 events (25 Oct, 1 Nov, (switch to STD), 8 Nov, Nov 15), but 1 Nov and 8 Nov are excluded. + // There are three separate tests: + // * before midnight GMT (3pm local time) + // * at midnight GMT in STD time (4pm local time) + // * at midnight GMT in DST time (5pm local time) + describe("Exdate: LA crossover DST before midnight GMT", () => { + it("LA crossover DST before midnight GMT should have 2 events", async () => { + await helpers.startApplication("tests/configs/modules/calendar/exdate_la_before_midnight.js", "19 Oct 2023 12:30:00 GMT-07:00", ["js/electron.js"], "America/Los_Angeles"); expect(global.page).not.toBeNull(); const loc = await global.page.locator(".calendar .event"); const elem = loc.first(); await elem.waitFor(); expect(elem).not.toBeNull(); const cnt = await loc.count(); - expect(cnt).toBe(51); + expect(cnt).toBe(2); + }); + }); + + describe("Exdate: LA crossover DST at midnight GMT local STD", () => { + it("LA crossover DST before midnight GMT should have 2 events", async () => { + await helpers.startApplication("tests/configs/modules/calendar/exdate_la_at_midnight_std.js", "19 Oct 2023 12:30:00 GMT-07:00", ["js/electron.js"], "America/Los_Angeles"); + expect(global.page).not.toBeNull(); + const loc = await global.page.locator(".calendar .event"); + const elem = loc.first(); + await elem.waitFor(); + expect(elem).not.toBeNull(); + const cnt = await loc.count(); + expect(cnt).toBe(2); + }); + }); + describe("Exdate: LA crossover DST at midnight GMT local DST", () => { + it("LA crossover DST before midnight GMT should have 2 events", async () => { + await helpers.startApplication("tests/configs/modules/calendar/exdate_la_at_midnight_dst.js", "19 Oct 2023 12:30:00 GMT-07:00", ["js/electron.js"], "America/Los_Angeles"); + expect(global.page).not.toBeNull(); + const loc = await global.page.locator(".calendar .event"); + const elem = loc.first(); + await elem.waitFor(); + expect(elem).not.toBeNull(); + const cnt = await loc.count(); + expect(cnt).toBe(2); + }); + }); + + /****************************/ + // SYDNEY TESTS: + // In 2023, standard time (STD) (GMT+10) was until 1 Oct, after which is DST (GMT+11). + // Test takes place on Thu 14 Sep, recurring event on a Wednesday. maximumNumberOfDays=28, so there should be + // 4 events (20 Sep, 27 Sep, (switch to DST), 4 Oct, 11 Oct), but 27 Sep and 4 Oct are excluded. + // There are three separate tests: + // * before midnight GMT (9am local time) + // * at midnight GMT in STD time (10am local time) + // * at midnight GMT in DST time (11am local time) + describe("Exdate: SYD crossover DST before midnight GMT", () => { + it("LA crossover DST before midnight GMT should have 2 events", async () => { + await helpers.startApplication("tests/configs/modules/calendar/exdate_syd_before_midnight.js", "14 Sep 2023 12:30:00 GMT+10:00", ["js/electron.js"], "Australia/Sydney"); + expect(global.page).not.toBeNull(); + const loc = await global.page.locator(".calendar .event"); + const elem = loc.first(); + await elem.waitFor(); + expect(elem).not.toBeNull(); + const cnt = await loc.count(); + expect(cnt).toBe(2); + }); + }); + describe("Exdate: SYD crossover DST at midnight GMT local STD", () => { + it("LA crossover DST before midnight GMT should have 2 events", async () => { + await helpers.startApplication("tests/configs/modules/calendar/exdate_syd_at_midnight_std.js", "14 Sep 2023 12:30:00 GMT+10:00", ["js/electron.js"], "Australia/Sydney"); + expect(global.page).not.toBeNull(); + const loc = await global.page.locator(".calendar .event"); + const elem = loc.first(); + await elem.waitFor(); + expect(elem).not.toBeNull(); + const cnt = await loc.count(); + expect(cnt).toBe(2); + }); + }); + describe("Exdate: SYD crossover DST at midnight GMT local DST", () => { + it("SYD crossover DST at midnight GMT local DST should have 2 events", async () => { + await helpers.startApplication("tests/configs/modules/calendar/exdate_syd_at_midnight_dst.js", "14 Sep 2023 12:30:00 GMT+10:00", ["js/electron.js"], "Australia/Sydney"); + expect(global.page).not.toBeNull(); + const loc = await global.page.locator(".calendar .event"); + const elem = loc.first(); + await elem.waitFor(); + expect(elem).not.toBeNull(); + const cnt = await loc.count(); + expect(cnt).toBe(2); }); }); }); diff --git a/tests/mocks/calendar_test_exdate.ics b/tests/mocks/calendar_test_exdate.ics deleted file mode 100644 index 8f1f8b9f32..0000000000 --- a/tests/mocks/calendar_test_exdate.ics +++ /dev/null @@ -1,34 +0,0 @@ -BEGIN:VEVENT -DTSTART;TZID=UTC:20231025T181000 -DTEND;TZID=UTC:20231025T195000 -RRULE:FREQ=WEEKLY;BYDAY=WE -EXDATE;TZID=UTC:20231101T181000 -EXDATE;TZID=UTC:20241030T181000 -EXDATE;TZID=UTC:20251029T181000 -EXDATE;TZID=UTC:20261028T181000 -EXDATE;TZID=UTC:20271027T181000 -EXDATE;TZID=UTC:20281025T181000 -EXDATE;TZID=UTC:20291024T181000 -EXDATE;TZID=UTC:20301023T181000 -EXDATE;TZID=UTC:20311022T181000 -EXDATE;TZID=UTC:20321020T181000 -EXDATE;TZID=UTC:20331019T181000 -EXDATE;TZID=UTC:20341018T181000 -EXDATE;TZID=UTC:20351017T181000 -EXDATE;TZID=UTC:20361015T181000 -EXDATE;TZID=UTC:20371014T181000 -EXDATE;TZID=UTC:20381013T181000 -EXDATE;TZID=UTC:20391012T181000 -EXDATE;TZID=UTC:20401010T181000 -EXDATE;TZID=UTC:20411009T181000 -EXDATE;TZID=UTC:20421008T181000 -EXDATE;TZID=UTC:20431007T181000 -DTSTAMP:20231025T233434Z -UID:sdflbkasuhdb5fkauglkb@google.com -CREATED:20230306T193128Z -LAST-MODIFIED:20231024T222515Z -SEQUENCE:0 -STATUS:CONFIRMED -SUMMARY:My Event -TRANSP:OPAQUE -END:VEVENT diff --git a/tests/mocks/exdate_la_at_midnight_dst.ics b/tests/mocks/exdate_la_at_midnight_dst.ics new file mode 100644 index 0000000000..6f8575f5d7 --- /dev/null +++ b/tests/mocks/exdate_la_at_midnight_dst.ics @@ -0,0 +1,15 @@ +BEGIN:VEVENT +DTSTART;TZID=America/Los_Angeles:20231025T170000 +DTEND;TZID=America/Los_Angeles:20231025T180000 +RRULE:FREQ=WEEKLY;BYDAY=WE +EXDATE;TZID=America/Los_Angeles:20231101T170000 +EXDATE;TZID=America/Los_Angeles:20231108T170000 +DTSTAMP:20231025T233434Z +UID:sdflbkasuhdb5fkauglkb@google.com +CREATED:20230306T193128Z +LAST-MODIFIED:20231024T222515Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:My Event +TRANSP:OPAQUE +END:VEVENT \ No newline at end of file diff --git a/tests/mocks/exdate_la_at_midnight_std.ics b/tests/mocks/exdate_la_at_midnight_std.ics new file mode 100644 index 0000000000..553ca10eef --- /dev/null +++ b/tests/mocks/exdate_la_at_midnight_std.ics @@ -0,0 +1,15 @@ +BEGIN:VEVENT +DTSTART;TZID=America/Los_Angeles:20231025T160000 +DTEND;TZID=America/Los_Angeles:20231025T170000 +RRULE:FREQ=WEEKLY;BYDAY=WE +EXDATE;TZID=America/Los_Angeles:20231101T160000 +EXDATE;TZID=America/Los_Angeles:20231108T160000 +DTSTAMP:20231025T233434Z +UID:sdflbkasuhdb5fkauglkb@google.com +CREATED:20230306T193128Z +LAST-MODIFIED:20231024T222515Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:My Event +TRANSP:OPAQUE +END:VEVENT \ No newline at end of file diff --git a/tests/mocks/exdate_la_before_midnight.ics b/tests/mocks/exdate_la_before_midnight.ics new file mode 100644 index 0000000000..5cdb069b45 --- /dev/null +++ b/tests/mocks/exdate_la_before_midnight.ics @@ -0,0 +1,15 @@ +BEGIN:VEVENT +DTSTART;TZID=America/Los_Angeles:20231025T150000 +DTEND;TZID=America/Los_Angeles:20231025T160000 +RRULE:FREQ=WEEKLY;BYDAY=WE +EXDATE;TZID=America/Los_Angeles:20231101T150000 +EXDATE;TZID=America/Los_Angeles:20231108T150000 +DTSTAMP:20231025T233434Z +UID:sdflbkasuhdb5fkauglkb@google.com +CREATED:20230306T193128Z +LAST-MODIFIED:20231024T222515Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:My Event +TRANSP:OPAQUE +END:VEVENT \ No newline at end of file diff --git a/tests/mocks/exdate_syd_at_midnight_dst.ics b/tests/mocks/exdate_syd_at_midnight_dst.ics new file mode 100644 index 0000000000..9a88a10b6d --- /dev/null +++ b/tests/mocks/exdate_syd_at_midnight_dst.ics @@ -0,0 +1,15 @@ +BEGIN:VEVENT +DTSTART;TZID=Australia/Sydney:20230920T110000 +DTEND;TZID=Australia/Sydney:20230920T111000 +RRULE:FREQ=WEEKLY;BYDAY=WE +EXDATE;TZID=Australia/Sydney:20230927T110000 +EXDATE;TZID=Australia/Sydney:20231004T110000 +DTSTAMP:20231025T233434Z +UID:sdflbkasuhdb5fkauglkb@google.com +CREATED:20230306T193128Z +LAST-MODIFIED:20231024T222515Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:My Event +TRANSP:OPAQUE +END:VEVENT \ No newline at end of file diff --git a/tests/mocks/exdate_syd_at_midnight_std.ics b/tests/mocks/exdate_syd_at_midnight_std.ics new file mode 100644 index 0000000000..af444e8cc0 --- /dev/null +++ b/tests/mocks/exdate_syd_at_midnight_std.ics @@ -0,0 +1,15 @@ +BEGIN:VEVENT +DTSTART;TZID=Australia/Sydney:20230920T100000 +DTEND;TZID=Australia/Sydney:20230920T110000 +RRULE:FREQ=WEEKLY;BYDAY=WE +EXDATE;TZID=Australia/Sydney:20230927T100000 +EXDATE;TZID=Australia/Sydney:20231004T100000 +DTSTAMP:20231025T233434Z +UID:sdflbkasuhdb5fkauglkb@google.com +CREATED:20230306T193128Z +LAST-MODIFIED:20231024T222515Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:My Event +TRANSP:OPAQUE +END:VEVENT \ No newline at end of file diff --git a/tests/mocks/exdate_syd_before_midnight.ics b/tests/mocks/exdate_syd_before_midnight.ics new file mode 100644 index 0000000000..5b5e53d2a2 --- /dev/null +++ b/tests/mocks/exdate_syd_before_midnight.ics @@ -0,0 +1,15 @@ +BEGIN:VEVENT +DTSTART;TZID=Australia/Sydney:20230920T090000 +DTEND;TZID=Australia/Sydney:20230920T100000 +RRULE:FREQ=WEEKLY;BYDAY=WE +EXDATE;TZID=Australia/Sydney:20230927T090000 +EXDATE;TZID=Australia/Sydney:20231004T090000 +DTSTAMP:20231025T233434Z +UID:sdflbkasuhdb5fkauglkb@google.com +CREATED:20230306T193128Z +LAST-MODIFIED:20231024T222515Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:My Event +TRANSP:OPAQUE +END:VEVENT \ No newline at end of file From 13805b04cbb9f9c4bebede06535188fad1d4868f Mon Sep 17 00:00:00 2001 From: Joshua Kriegshauser Date: Fri, 19 Jan 2024 21:04:23 -0800 Subject: [PATCH 10/15] Test fix --- modules/default/calendar/calendarfetcherutils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/default/calendar/calendarfetcherutils.js b/modules/default/calendar/calendarfetcherutils.js index 2d020c541a..9b09cfb943 100644 --- a/modules/default/calendar/calendarfetcherutils.js +++ b/modules/default/calendar/calendarfetcherutils.js @@ -287,6 +287,7 @@ const CalendarFetcherUtils = { const hasByWeekdayRule = rule.options.byweekday !== undefined && rule.options.byweekday !== null; const oneDayInMs = 24 * 60 * 60 * 1000; Log.debug(`RRule: ${rule.toString()}`); + rule.options.tzid = null; // RRule gets *very* confused with timezones let dates = rule.between(new Date(pastLocal.valueOf() - oneDayInMs), new Date(futureLocal.valueOf() + oneDayInMs), true, () => { return true; }); Log.debug(`Title: ${event.summary}, with dates: ${JSON.stringify(dates)}`); dates = dates.filter((d) => { From 0529a426f386ffa9d7ad27d04c05507bd08cbe2b Mon Sep 17 00:00:00 2001 From: Joshua Kriegshauser Date: Fri, 19 Jan 2024 21:19:12 -0800 Subject: [PATCH 11/15] changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afe4e750ff..6842a60592 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,8 @@ _This release is scheduled to be released on 2024-04-01._ ### Fixed -- WIP +- Worked around several issues in the RRULE library that were causing deleted calender events to still show, some + initial and recurring events to not show, and some event times to be off an hour. (#3291) ## [2.26.0] - 01-01-2024 From c8953c5e1876ee17c721095f2e643e12331d77bc Mon Sep 17 00:00:00 2001 From: Joshua Kriegshauser Date: Sat, 20 Jan 2024 13:58:22 -0800 Subject: [PATCH 12/15] Fix duplicates --- modules/default/calendar/calendarfetcherutils.js | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/modules/default/calendar/calendarfetcherutils.js b/modules/default/calendar/calendarfetcherutils.js index 9b09cfb943..732375e6a5 100644 --- a/modules/default/calendar/calendarfetcherutils.js +++ b/modules/default/calendar/calendarfetcherutils.js @@ -333,20 +333,12 @@ const CalendarFetcherUtils = { // Would be great if there was a better way to handle this. Log.debug(`event.recurrences: ${event.recurrences}`); if (event.recurrences !== undefined) { - for (let r in event.recurrences) { - let ev = event.recurrences[r]; + for (let dateKey in event.recurrences) { // Only add dates that weren't already in the range we added from the rrule so that // we don't double-add those events. Unfortunately dates.includes doesn't do an exact match. - if (moment(ev.start).isBetween(pastMoment, futureMoment)) { - let found = false; - dates.forEach((d) => { - if (d.valueOf() === ev.start.valueOf()) { - found = true; - } - }); - if (!found) { - dates.push(ev.start); - } + let d = new Date(dateKey); + if (!moment(d).isBetween(pastMoment, futureMoment)) { + dates.push(d); } } } From c98a41670d8d81972cfdbbd0a1d0b7e3045925ed Mon Sep 17 00:00:00 2001 From: Joshua Kriegshauser Date: Mon, 22 Jan 2024 10:34:29 -0800 Subject: [PATCH 13/15] Comment tweaks and remove redundant check --- modules/default/calendar/calendarfetcherutils.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/modules/default/calendar/calendarfetcherutils.js b/modules/default/calendar/calendarfetcherutils.js index 732375e6a5..7db318b026 100644 --- a/modules/default/calendar/calendarfetcherutils.js +++ b/modules/default/calendar/calendarfetcherutils.js @@ -335,7 +335,7 @@ const CalendarFetcherUtils = { if (event.recurrences !== undefined) { for (let dateKey in event.recurrences) { // Only add dates that weren't already in the range we added from the rrule so that - // we don't double-add those events. Unfortunately dates.includes doesn't do an exact match. + // we don't double-add those events. let d = new Date(dateKey); if (!moment(d).isBetween(pastMoment, futureMoment)) { dates.push(d); @@ -344,7 +344,7 @@ const CalendarFetcherUtils = { } // Lastly, sometimes rrule doesn't include the event.start even if it is in the requested range. Ensure - // inclusion here. + // inclusion here. Unfortunately dates.includes() doesn't find it so we have to do forEach(). { let found = false; dates.forEach((d) => { if (d.valueOf() === event.start.valueOf()) found = true; }); @@ -360,10 +360,6 @@ const CalendarFetcherUtils = { let curEvent = event; let showRecurrence = true; - if (date < pastLocal || date > futureLocal) { - continue; - } - startMoment = moment(date); // Remove the time information of each date by using its substring, using the following method: From 0986f6d50e8a02d2feba4439cccabf209b146d50 Mon Sep 17 00:00:00 2001 From: Joshua Kriegshauser Date: Mon, 22 Jan 2024 10:43:26 -0800 Subject: [PATCH 14/15] Fixed comments --- .../modules/calendar/exdate_la_at_midnight_dst.js | 10 ++++++---- .../modules/calendar/exdate_la_at_midnight_std.js | 5 +---- .../modules/calendar/exdate_la_before_midnight.js | 5 +---- .../modules/calendar/exdate_syd_at_midnight_dst.js | 5 +---- .../modules/calendar/exdate_syd_at_midnight_std.js | 5 +---- .../modules/calendar/exdate_syd_before_midnight.js | 5 +---- 6 files changed, 11 insertions(+), 24 deletions(-) diff --git a/tests/configs/modules/calendar/exdate_la_at_midnight_dst.js b/tests/configs/modules/calendar/exdate_la_at_midnight_dst.js index f112943701..608c71da96 100644 --- a/tests/configs/modules/calendar/exdate_la_at_midnight_dst.js +++ b/tests/configs/modules/calendar/exdate_la_at_midnight_dst.js @@ -1,8 +1,10 @@ -/* NOTE: calendar_test_exdate.ics has exdate entries for the next 20 years, but without some - * way to set a debug date for tests, this test may become flaky on specific days (i.e. could - * not test easily on leap-years, the BYDAY specified in exdate, etc.) or when the 20 years - * elapses if this project is still in active development ;) +/* MagicMirror² Test calendar exdate + * + * By jkriegshauser + * MIT Licensed. + * * See issue #3250 + * See tests/electron/modules/calendar_spec.js */ let config = { timeFormat: 12, diff --git a/tests/configs/modules/calendar/exdate_la_at_midnight_std.js b/tests/configs/modules/calendar/exdate_la_at_midnight_std.js index 2eb96fa643..6128c97f50 100644 --- a/tests/configs/modules/calendar/exdate_la_at_midnight_std.js +++ b/tests/configs/modules/calendar/exdate_la_at_midnight_std.js @@ -3,11 +3,8 @@ * By jkriegshauser * MIT Licensed. * - * NOTE: calendar_test_exdate.ics has exdate entries for the next 20 years, but without some - * way to set a debug date for tests, this test may become flaky on specific days (i.e. could - * not test easily on leap-years, the BYDAY specified in exdate, etc.) or when the 20 years - * elapses if this project is still in active development ;) * See issue #3250 + * See tests/electron/modules/calendar_spec.js */ let config = { timeFormat: 12, diff --git a/tests/configs/modules/calendar/exdate_la_before_midnight.js b/tests/configs/modules/calendar/exdate_la_before_midnight.js index 73cf500912..8c886f775a 100644 --- a/tests/configs/modules/calendar/exdate_la_before_midnight.js +++ b/tests/configs/modules/calendar/exdate_la_before_midnight.js @@ -3,11 +3,8 @@ * By jkriegshauser * MIT Licensed. * - * NOTE: calendar_test_exdate.ics has exdate entries for the next 20 years, but without some - * way to set a debug date for tests, this test may become flaky on specific days (i.e. could - * not test easily on leap-years, the BYDAY specified in exdate, etc.) or when the 20 years - * elapses if this project is still in active development ;) * See issue #3250 + * See tests/electron/modules/calendar_spec.js */ let config = { timeFormat: 12, diff --git a/tests/configs/modules/calendar/exdate_syd_at_midnight_dst.js b/tests/configs/modules/calendar/exdate_syd_at_midnight_dst.js index 000b7cbe55..f98b75f4c7 100644 --- a/tests/configs/modules/calendar/exdate_syd_at_midnight_dst.js +++ b/tests/configs/modules/calendar/exdate_syd_at_midnight_dst.js @@ -3,11 +3,8 @@ * By jkriegshauser * MIT Licensed. * - * NOTE: calendar_test_exdate.ics has exdate entries for the next 20 years, but without some - * way to set a debug date for tests, this test may become flaky on specific days (i.e. could - * not test easily on leap-years, the BYDAY specified in exdate, etc.) or when the 20 years - * elapses if this project is still in active development ;) * See issue #3250 + * See tests/electron/modules/calendar_spec.js */ let config = { timeFormat: 12, diff --git a/tests/configs/modules/calendar/exdate_syd_at_midnight_std.js b/tests/configs/modules/calendar/exdate_syd_at_midnight_std.js index ccf18721c1..06c7ff5319 100644 --- a/tests/configs/modules/calendar/exdate_syd_at_midnight_std.js +++ b/tests/configs/modules/calendar/exdate_syd_at_midnight_std.js @@ -3,11 +3,8 @@ * By jkriegshauser * MIT Licensed. * - * NOTE: calendar_test_exdate.ics has exdate entries for the next 20 years, but without some - * way to set a debug date for tests, this test may become flaky on specific days (i.e. could - * not test easily on leap-years, the BYDAY specified in exdate, etc.) or when the 20 years - * elapses if this project is still in active development ;) * See issue #3250 + * See tests/electron/modules/calendar_spec.js */ let config = { timeFormat: 12, diff --git a/tests/configs/modules/calendar/exdate_syd_before_midnight.js b/tests/configs/modules/calendar/exdate_syd_before_midnight.js index 1a6c87dc62..849c1f5363 100644 --- a/tests/configs/modules/calendar/exdate_syd_before_midnight.js +++ b/tests/configs/modules/calendar/exdate_syd_before_midnight.js @@ -3,11 +3,8 @@ * By jkriegshauser * MIT Licensed. * - * NOTE: calendar_test_exdate.ics has exdate entries for the next 20 years, but without some - * way to set a debug date for tests, this test may become flaky on specific days (i.e. could - * not test easily on leap-years, the BYDAY specified in exdate, etc.) or when the 20 years - * elapses if this project is still in active development ;) * See issue #3250 + * See tests/electron/modules/calendar_spec.js */ let config = { timeFormat: 12, From 7628e422f6224543dc20d182450f7296f8f7dea9 Mon Sep 17 00:00:00 2001 From: Joshua Kriegshauser Date: Mon, 22 Jan 2024 12:05:43 -0800 Subject: [PATCH 15/15] Fix an issue where a specific recurrence screws up duration for following events --- modules/default/calendar/calendarfetcherutils.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/modules/default/calendar/calendarfetcherutils.js b/modules/default/calendar/calendarfetcherutils.js index 7db318b026..4be451eb51 100644 --- a/modules/default/calendar/calendarfetcherutils.js +++ b/modules/default/calendar/calendarfetcherutils.js @@ -181,7 +181,7 @@ const CalendarFetcherUtils = { Log.debug(`end:: ${endMoment.toDate()}`); // Calculate the duration of the event for use with recurring events. - let durationMs = endMoment.valueOf() - startMoment.valueOf(); + const durationMs = endMoment.valueOf() - startMoment.valueOf(); Log.debug(`duration: ${durationMs}`); // FIXME: Since the parsed json object from node-ical comes with time information @@ -358,6 +358,7 @@ const CalendarFetcherUtils = { for (let d in dates) { let date = dates[d]; let curEvent = event; + let curDurationMs = durationMs; let showRecurrence = true; startMoment = moment(date); @@ -374,16 +375,16 @@ const CalendarFetcherUtils = { // We found an override, so for this recurrence, use a potentially different title, start date, and duration. curEvent = curEvent.recurrences[dateKey]; startMoment = moment(curEvent.start); - durationMs = curEvent.end.valueOf() - startMoment.valueOf(); + curDurationMs = curEvent.end.valueOf() - startMoment.valueOf(); } // If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule. else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) { // This date is an exception date, which means we should skip it in the recurrence pattern. showRecurrence = false; } - Log.debug(`duration: ${durationMs}`); + Log.debug(`duration: ${curDurationMs}`); - endMoment = moment(startMoment.valueOf() + durationMs); + endMoment = moment(startMoment.valueOf() + curDurationMs); if (startMoment.valueOf() === endMoment.valueOf()) { endMoment = endMoment.endOf("day"); }