Better fixes for #3291 and the underlying exdate issues (#3342)

* 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)
* Renamed variables in *calendarfetcherutils.js* to be more clear about
use of `moment` and js's `Date` class.
* Added calendar config option `forceUseCurrentTime` (default:`false`)
which will ignore overridden `Date.now` in the config in order to keep
some tests consistent.
* Added several unit tests for crossing DST in different timezones with
excluded events.
This commit is contained in:
jkriegshauser 2024-01-26 22:56:54 -08:00 committed by GitHub
parent 27f3c86c41
commit 7f0b8e4054
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 528 additions and 219 deletions

View File

@ -36,6 +36,13 @@ _This release is scheduled to be released on 2024-04-01._
- Unneeded file headers (#3358) - Unneeded file headers (#3358)
## [2.27.0] - UNRELEASED
### Fixed
- 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 ## [2.26.0] - 01-01-2024
Thanks to: @bnitkin, @bugsounet, @dependabot, @jkriegshauser, @kaennchenstruggle, @KristjanESPERANTO and @Ybbet. Thanks to: @bnitkin, @bugsounet, @dependabot, @jkriegshauser, @kaennchenstruggle, @KristjanESPERANTO and @Ybbet.

View File

@ -36,6 +36,7 @@ Module.register("calendar", {
hideDuplicates: true, hideDuplicates: true,
showTimeToday: false, showTimeToday: false,
colored: false, colored: false,
forceUseCurrentTime: false,
tableClass: "small", tableClass: "small",
calendars: [ calendars: [
{ {
@ -567,9 +568,16 @@ Module.register("calendar", {
const ONE_HOUR = ONE_MINUTE * 60; const ONE_HOUR = ONE_MINUTE * 60;
const ONE_DAY = ONE_HOUR * 24; const ONE_DAY = ONE_HOUR * 24;
const now = new Date(); let now, today, future;
const today = moment().startOf("day"); if (this.config.forceUseCurrentTime || this.defaults.forceUseCurrentTime) {
const future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate(); 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 = []; let events = [];
for (const calendarUrl in this.calendarData) { for (const calendarUrl in this.calendarData) {

View File

@ -56,7 +56,7 @@ const CalendarFetcherUtils = {
event.start.tz = ""; event.start.tz = "";
Log.debug(`ical offset=${current_offset} date=${date}`); Log.debug(`ical offset=${current_offset} date=${date}`);
mm = moment(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}`); Log.debug(`net mins=${current_offset * 60 - x}`);
mm = mm.add(x - current_offset * 60, "minutes"); mm = mm.add(x - current_offset * 60, "minutes");
@ -128,24 +128,26 @@ const CalendarFetcherUtils = {
}; };
const eventDate = function (event, time) { 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.`); Log.debug(`There are ${Object.entries(data).length} calendar entries.`);
Object.entries(data).forEach(([key, event]) => {
Log.debug("Processing entry..."); const now = new Date(Date.now());
const now = new Date(); const todayLocal = moment(now).startOf("day").toDate();
const today = moment().startOf("day").toDate(); const futureLocalDate
const future = moment(now)
= moment()
.startOf("day") .startOf("day")
.add(config.maximumNumberOfDays, "days") .add(config.maximumNumberOfDays, "days")
.subtract(1, "seconds") // Subtract 1 second so that events that start on the middle of the night will not repeat. .subtract(1, "seconds") // Subtract 1 second so that events that start on the middle of the night will not repeat.
.toDate(); .toDate();
let past = today;
Object.entries(data).forEach(([key, event]) => {
Log.debug("Processing entry...");
let pastLocalDate = todayLocal;
if (config.includePastEvents) { if (config.includePastEvents) {
past = 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. // FIXME: Ugly fix to solve the facebook birthday issue.
@ -159,33 +161,33 @@ const CalendarFetcherUtils = {
if (event.type === "VEVENT") { if (event.type === "VEVENT") {
Log.debug(`Event:\n${JSON.stringify(event)}`); Log.debug(`Event:\n${JSON.stringify(event)}`);
let startDate = eventDate(event, "start"); let startMoment = eventDate(event, "start");
let endDate; let endMoment;
if (typeof event.end !== "undefined") { if (typeof event.end !== "undefined") {
endDate = eventDate(event, "end"); endMoment = eventDate(event, "end");
} else if (typeof event.duration !== "undefined") { } else if (typeof event.duration !== "undefined") {
endDate = startDate.clone().add(moment.duration(event.duration)); endMoment = startMoment.clone().add(moment.duration(event.duration));
} else { } else {
if (!isFacebookBirthday) { if (!isFacebookBirthday) {
// make copy of start date, separate storage area // make copy of start date, separate storage area
endDate = moment(startDate.format("x"), "x"); endMoment = moment(startMoment.valueOf());
} else { } else {
endDate = moment(startDate).add(1, "days"); endMoment = moment(startMoment).add(1, "days");
} }
} }
Log.debug(`start: ${startDate.toDate()}`); Log.debug(`start: ${startMoment.toDate()}`);
Log.debug(`end:: ${endDate.toDate()}`); Log.debug(`end:: ${endMoment.toDate()}`);
// Calculate the duration of the event for use with recurring events. // Calculate the duration of the event for use with recurring events.
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x")); const durationMs = endMoment.valueOf() - startMoment.valueOf();
Log.debug(`duration: ${duration}`); Log.debug(`duration: ${durationMs}`);
// FIXME: Since the parsed json object from node-ical comes with time information // FIXME: Since the parsed json object from node-ical comes with time information
// this check could be removed (?) // this check could be removed (?)
if (event.start.length === 8) { if (event.start.length === 8) {
startDate = startDate.startOf("day"); startMoment = startMoment.startOf("day");
} }
const title = CalendarFetcherUtils.getTitleFromEvent(event); const title = CalendarFetcherUtils.getTitleFromEvent(event);
@ -245,11 +247,11 @@ const CalendarFetcherUtils = {
const geo = event.geo || false; const geo = event.geo || false;
const description = event.description || 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 rule = event.rrule;
const pastMoment = moment(past); const pastMoment = moment(pastLocalDate);
const futureMoment = moment(future); const futureMoment = moment(futureLocalDate);
// can cause problems with e.g. birthdays before 1900 // 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)) { 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 +262,8 @@ const CalendarFetcherUtils = {
// For recurring events, get the set of start dates that fall within the range // For recurring events, get the set of start dates that fall within the range
// of dates we're looking for. // of dates we're looking for.
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time // kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
let pastLocal = 0; let pastLocal;
let futureLocal = 0; let futureLocal;
if (CalendarFetcherUtils.isFullDayEvent(event)) { if (CalendarFetcherUtils.isFullDayEvent(event)) {
Log.debug("fullday"); Log.debug("fullday");
// if full day event, only use the date part of the ranges // if full day event, only use the date part of the ranges
@ -277,17 +279,51 @@ const CalendarFetcherUtils = {
pastLocal = pastMoment.toDate(); pastLocal = pastMoment.toDate();
} else { } else {
// otherwise use NOW.. cause we shouldn't use any before now // otherwise use NOW.. cause we shouldn't use any before now
pastLocal = moment().toDate(); //now pastLocal = moment(now).toDate(); //now
} }
futureLocal = futureMoment.toDate(); // future futureLocal = futureMoment.toDate(); // future
} }
Log.debug(`Search for recurring events between: ${pastLocal} and ${futureLocal}`); 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()}`);
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)}`); Log.debug(`Title: ${event.summary}, with dates: ${JSON.stringify(dates)}`);
dates = dates.filter((d) => { dates = dates.filter((d) => {
if (JSON.stringify(d) === "null") return false; if (JSON.stringify(d) === "null") return false;
else return true; 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);
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 // 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 // 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, // had its date changed from outside the range to inside the range. For the time being,
@ -297,108 +333,35 @@ const CalendarFetcherUtils = {
// Would be great if there was a better way to handle this. // Would be great if there was a better way to handle this.
Log.debug(`event.recurrences: ${event.recurrences}`); Log.debug(`event.recurrences: ${event.recurrences}`);
if (event.recurrences !== undefined) { if (event.recurrences !== undefined) {
for (let r in event.recurrences) { for (let dateKey in event.recurrences) {
// Only add dates that weren't already in the range we added from the rrule so that // Only add dates that weren't already in the range we added from the rrule so that
// we don"t double-add those events. // we don't double-add those events.
if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) { let d = new Date(dateKey);
dates.push(new Date(r)); if (!moment(d).isBetween(pastMoment, futureMoment)) {
dates.push(d);
} }
} }
} }
// Lastly, sometimes rrule doesn't include the event.start even if it is in the requested range. Ensure
// 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; });
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. // Loop through the set of date entries to see which recurrences should be added to our event list.
for (let d in dates) { for (let d in dates) {
let date = dates[d]; let date = dates[d];
let curEvent = event; let curEvent = event;
let curDurationMs = durationMs;
let showRecurrence = true; let showRecurrence = true;
// set the time information in the date to equal the time information in the event startMoment = moment(date);
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}`);
}
//}
}
}
startDate = moment(date);
Log.debug(`Corrected startDate: ${startDate.toDate()}`);
let adjustDays = CalendarFetcherUtils.calculateTimezoneAdjustment(event, date);
// Remove the time information of each date by using its substring, using the following method: // Remove the time information of each date by using its substring, using the following method:
// .toISOString().substring(0,10). // .toISOString().substring(0,10).
@ -411,30 +374,30 @@ const CalendarFetcherUtils = {
if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) { 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. // We found an override, so for this recurrence, use a potentially different title, start date, and duration.
curEvent = curEvent.recurrences[dateKey]; curEvent = curEvent.recurrences[dateKey];
startDate = moment(curEvent.start); startMoment = moment(curEvent.start);
duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x")); curDurationMs = curEvent.end.valueOf() - startMoment.valueOf();
} }
// If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule. // 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) { 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. // This date is an exception date, which means we should skip it in the recurrence pattern.
showRecurrence = false; showRecurrence = false;
} }
Log.debug(`duration: ${duration}`); Log.debug(`duration: ${curDurationMs}`);
endDate = moment(parseInt(startDate.format("x")) + duration, "x"); endMoment = moment(startMoment.valueOf() + curDurationMs);
if (startDate.format("x") === endDate.format("x")) { if (startMoment.valueOf() === endMoment.valueOf()) {
endDate = endDate.endOf("day"); endMoment = endMoment.endOf("day");
} }
const recurrenceTitle = CalendarFetcherUtils.getTitleFromEvent(curEvent); 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 // 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. // it to the event list.
if (endDate.isBefore(past) || startDate.isAfter(future)) { if (endMoment.isBefore(pastLocal) || startMoment.isAfter(futureLocal)) {
showRecurrence = false; showRecurrence = false;
} }
if (CalendarFetcherUtils.timeFilterApplies(now, endDate, dateFilter)) { if (CalendarFetcherUtils.timeFilterApplies(now, endMoment, dateFilter)) {
showRecurrence = false; showRecurrence = false;
} }
@ -442,8 +405,8 @@ const CalendarFetcherUtils = {
Log.debug(`saving event: ${description}`); Log.debug(`saving event: ${description}`);
newEvents.push({ newEvents.push({
title: recurrenceTitle, title: recurrenceTitle,
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"), startDate: startMoment.format("x"),
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"), endDate: endMoment.format("x"),
fullDayEvent: CalendarFetcherUtils.isFullDayEvent(event), fullDayEvent: CalendarFetcherUtils.isFullDayEvent(event),
recurringEvent: true, recurringEvent: true,
class: event.class, class: event.class,
@ -461,43 +424,47 @@ const CalendarFetcherUtils = {
// Log.debug("full day event") // 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 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")) { if (fullDayEvent && startMoment.valueOf() === endMoment.valueOf()) {
endDate = endDate.endOf("day"); endMoment = endMoment.endOf("day");
} }
if (config.includePastEvents) { if (config.includePastEvents) {
// Past event is too far in the past, so skip. // Past event is too far in the past, so skip.
if (endDate < past) { if (endMoment < pastLocalDate) {
return; return;
} }
} else { } else {
// It's not a fullday event, and it is in the past, so skip. // It's not a fullday event, and it is in the past, so skip.
if (!fullDayEvent && endDate < new Date()) { if (!fullDayEvent && endMoment < now) {
return; return;
} }
// It's a fullday event, and it is before today, So skip. // It's a fullday event, and it is before today, So skip.
if (fullDayEvent && endDate <= today) { if (fullDayEvent && endMoment <= todayLocal) {
return; return;
} }
} }
// It exceeds the maximumNumberOfDays limit, so skip. // It exceeds the maximumNumberOfDays limit, so skip.
if (startDate > future) { if (startMoment > futureLocalDate) {
return; return;
} }
if (CalendarFetcherUtils.timeFilterApplies(now, endDate, dateFilter)) { if (CalendarFetcherUtils.timeFilterApplies(now, endMoment, dateFilter)) {
return; return;
} }
// get correction for date saving and dst change between now and then // 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());
// 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. // Every thing is good. Add it to the list.
newEvents.push({ newEvents.push({
title: title, title: title,
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"), startDate: startMoment.add(adjustHours, "hours").format("x"),
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"), endDate: endMoment.add(adjustHours, "hours").format("x"),
fullDayEvent: fullDayEvent, fullDayEvent: fullDayEvent,
class: event.class, class: event.class,
location: location, location: location,
@ -578,7 +545,7 @@ const CalendarFetcherUtils = {
increment = until[1].slice(-1) === "s" ? until[1] : `${until[1]}s`, // Massage the data for moment js increment = until[1].slice(-1) === "s" ? until[1] : `${until[1]}s`, // Massage the data for moment js
filterUntil = moment(endDate.format()).subtract(value, increment); filterUntil = moment(endDate.format()).subtract(value, increment);
return now < filterUntil.format("x"); return now < filterUntil.toDate();
} }
return false; return false;

View File

@ -7,6 +7,7 @@ let config = {
position: "bottom_bar", position: "bottom_bar",
config: { config: {
customEvents: [{ keyword: "CustomEvent", symbol: "dice", eventClass: "undo" }], customEvents: [{ keyword: "CustomEvent", symbol: "dice", eventClass: "undo" }],
forceUseCurrentTime: true,
calendars: [ calendars: [
{ {
maximumEntries: 5, maximumEntries: 5,

View File

@ -1,31 +0,0 @@
/* 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: 364,
url: "http://localhost:8080/tests/mocks/calendar_test_exdate.ics"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@ -0,0 +1,37 @@
/* MagicMirror² Test calendar exdate
*
* By jkriegshauser
* MIT Licensed.
*
* See issue #3250
* See tests/electron/modules/calendar_spec.js
*/
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_dst.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;
}

View File

@ -0,0 +1,37 @@
/* MagicMirror² Test calendar exdate
*
* By jkriegshauser
* MIT Licensed.
*
* See issue #3250
* See tests/electron/modules/calendar_spec.js
*/
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;
}

View File

@ -0,0 +1,37 @@
/* MagicMirror² Test calendar exdate
*
* By jkriegshauser
* MIT Licensed.
*
* See issue #3250
* See tests/electron/modules/calendar_spec.js
*/
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;
}

View File

@ -0,0 +1,37 @@
/* MagicMirror² Test calendar exdate
*
* By jkriegshauser
* MIT Licensed.
*
* See issue #3250
* See tests/electron/modules/calendar_spec.js
*/
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;
}

View File

@ -0,0 +1,37 @@
/* MagicMirror² Test calendar exdate
*
* By jkriegshauser
* MIT Licensed.
*
* See issue #3250
* See tests/electron/modules/calendar_spec.js
*/
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;
}

View File

@ -0,0 +1,37 @@
/* MagicMirror² Test calendar exdate
*
* By jkriegshauser
* MIT Licensed.
*
* See issue #3250
* See tests/electron/modules/calendar_spec.js
*/
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;
}

View File

@ -3,11 +3,11 @@
// https://www.anycodings.com/1questions/958135/can-i-set-the-date-for-playwright-browser // https://www.anycodings.com/1questions/958135/can-i-set-the-date-for-playwright-browser
const { _electron: electron } = require("playwright"); 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.electronApp = null;
global.page = null; global.page = null;
process.env.MM_CONFIG_FILE = configFilename; process.env.MM_CONFIG_FILE = configFilename;
process.env.TZ = "GMT"; process.env.TZ = timezone;
global.electronApp = await electron.launch({ args: electronParams }); global.electronApp = await electron.launch({ args: electronParams });
await global.electronApp.firstWindow(); await global.electronApp.firstWindow();
@ -20,7 +20,7 @@ exports.startApplication = async (configFilename, systemDate = null, electronPar
if (systemDate) { if (systemDate) {
await global.page.evaluate((systemDate) => { await global.page.evaluate((systemDate) => {
Date.now = () => { Date.now = () => {
return new Date(systemDate); return new Date(systemDate).valueOf();
}; };
}, systemDate); }, systemDate);
} }

View File

@ -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 () => { // LOS ANGELES TESTS:
// test must run on a Thursday // In 2023, DST (GMT-7) was until 5 Nov, after which is standard (STD) (GMT-8) time.
await helpers.startApplication("tests/configs/modules/calendar/exdate.js", "14 Dec 2023 12:30:00 GMT"); // 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(); expect(global.page).not.toBeNull();
const loc = await global.page.locator(".calendar .event"); const loc = await global.page.locator(".calendar .event");
const elem = loc.first(); const elem = loc.first();
await elem.waitFor(); await elem.waitFor();
expect(elem).not.toBeNull(); expect(elem).not.toBeNull();
const cnt = await loc.count(); 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);
}); });
}); });
}); });

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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