diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c0bc1b2b8..af4a01a28f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,10 +22,11 @@ _This release is scheduled to be released on 2025-01-01._ - [linter] Re-add `eslint-plugin-import`now that it supports ESLint v9 (#3586) - [linter] Re-activate `eslint-plugin-package-json` to lint `package.json` (#3643) - [linter] Add linting for markdown files. +- [calendar] - added ability to display end date for full date events, where end is not same day (showEnd=true) ### Removed -- [tests] Removed node-pty and drivelist from rebuilded test (#3575) +- [tests] Remove `node-pty` and `drivelist` from rebuilded test (#3575) - [deps] Remove `@eslint/js` dependency. Already installed with `eslint` in deep (#3636) ### Updated @@ -39,12 +40,17 @@ _This release is scheduled to be released on 2025-01-01._ - [updatenotification] Fix pm2 using detection when pm2 script is inside or outside MagicMirror root folder (#3576) (#3605) (#3626) (#3628) - [core] Fix loading node_helper of modules: avoid black screen, display errors and continue loading with next module (#3578) -- [weather] Changed default value for weatherEndpoint of provider openweathermap to "/onecall" (#3574) -- [tests] Fix electron tests with mock dates, the mock on server side was missing (#3597) -- [tests] Fix test cases with hard coded Date.now (#3597) +- [weather] changed default value for weatherEndpoint of provider openweathermap to "/onecall" (#3574) +- [tests] fix electron tests with mock dates, the mock on server side was missing (#3597) +- [tests] fix testcases with hard coded Date.now (#3597) - [core] Fix missing `basePath` where `location.host` is used (#3613) - [compliments] croner library changed filenames used in latest version (#3624) -- [linter] Fix ESLint ignore pattern which caused that default modules not to be linted. (#3632). +- [linter] Fix ESLint ignore pattern which caused that default modules not to be linted (#3632) +- [calendar] - update to resolve issues #3098 #3144 #3351 #3422 #3443 #3467 #3537 related to timezone changes +- [calendar] - fixes #3267 (styles array), also fixes event with both exdate AND recurrence(and testcase) +- [calendar] - fix showEndsOnlyWithDuration not working, #3598, applies ONLY to full day events +- [calendar] - fix showEnd for Full Day events #3602 +- [tests] Suppress "module is not defined" in e2e tests ## [2.29.0] - 2024-10-01 diff --git a/modules/default/calendar/calendar.js b/modules/default/calendar/calendar.js index 31b863eb28..6af652ba77 100644 --- a/modules/default/calendar/calendar.js +++ b/modules/default/calendar/calendar.js @@ -168,12 +168,17 @@ Module.register("calendar", { this.selfUpdate(); }, + notificationReceived (notification, payload, sender) { - // Override socket notification handler. - socketNotificationReceived (notification, payload) { if (notification === "FETCH_CALENDAR") { - this.sendSocketNotification(notification, { url: payload.url, id: this.identifier }); + if (this.hasCalendarURL(payload.url)) { + this.sendSocketNotification(notification, { url: payload.url, id: this.identifier }); + } } + }, + + // Override socket notification handler. + socketNotificationReceived (notification, payload) { if (this.identifier !== payload.id) { return; @@ -417,18 +422,26 @@ Module.register("calendar", { timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").format(this.config.dateFormat)); // Add end time if showEnd if (this.config.showEnd) { - if (this.config.showEndsOnlyWithDuration && event.startDate === event.endDate) { - // no duration here, don't display end - } else { + // and has a duation + if (event.startDate !== event.endDate) { timeWrapper.innerHTML += "-"; timeWrapper.innerHTML += CalendarUtils.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat)); } } + // For full day events we use the fullDayEventDateFormat if (event.fullDayEvent) { //subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day event.endDate -= ONE_SECOND; timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat)); + // only show end if requested and allowed and the dates are different + if (this.config.showEnd && !this.config.showEndsOnlyWithDuration && moment(event.startDate, "x").format("YYYYMMDD") !== moment(event.endDate, "x").format("YYYYMMDD")) { + timeWrapper.innerHTML += "-"; + timeWrapper.innerHTML += CalendarUtils.capFirst(moment(event.endDate, "x").format(this.config.fullDayEventDateFormat)); + } else + if ((moment(event.startDate, "x").format("YYYYMMDD") !== moment(event.endDate, "x").format("YYYYMMDD")) && (moment(event.startDate, "x") < moment(now, "x"))) { + timeWrapper.innerHTML = CalendarUtils.capFirst(moment(now, "x").format(this.config.fullDayEventDateFormat)); + } } else if (this.config.getRelative > 0 && event.startDate < now) { // Ongoing and getRelative is set timeWrapper.innerHTML = CalendarUtils.capFirst( @@ -460,16 +473,18 @@ Module.register("calendar", { if (event.startDate >= now || (event.fullDayEvent && this.eventEndingWithinNextFullTimeUnit(event, ONE_DAY))) { // Use relative time if (!this.config.hideTime && !event.fullDayEvent) { - timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat })); + Log.debug("event not hidden and not fullday"); + timeWrapper.innerHTML = `${CalendarUtils.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }))}`; } else { - timeWrapper.innerHTML = CalendarUtils.capFirst( + Log.debug("event full day or hidden"); + timeWrapper.innerHTML = `${CalendarUtils.capFirst( moment(event.startDate, "x").calendar(null, { sameDay: this.config.showTimeToday ? "LT" : `[${this.translate("TODAY")}]`, nextDay: `[${this.translate("TOMORROW")}]`, nextWeek: "dddd", sameElse: event.fullDayEvent ? this.config.fullDayEventDateFormat : this.config.dateFormat }) - ); + )}`; } if (event.fullDayEvent) { // Full days events within the next two days @@ -488,9 +503,11 @@ Module.register("calendar", { timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW")); } } + Log.info("event fullday"); } else if (event.startDate - now < this.config.getRelative * ONE_HOUR) { + Log.info("not full day but within getrelative size"); // If event is within getRelative hours, display 'in xxx' time format or moment.fromNow() - timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").fromNow()); + timeWrapper.innerHTML = `${CalendarUtils.capFirst(moment(event.startDate, "x").fromNow())}`; } } else { // Ongoing event @@ -603,6 +620,7 @@ Module.register("calendar", { const calendar = this.calendarData[calendarUrl]; let remainingEntries = this.maximumEntriesForUrl(calendarUrl); let maxPastDaysCompare = now - this.maximumPastDaysForUrl(calendarUrl) * ONE_DAY; + let by_url_calevents = []; for (const e in calendar) { const event = JSON.parse(JSON.stringify(calendar[e])); // clone object @@ -620,9 +638,6 @@ Module.register("calendar", { if (this.config.hideDuplicates && this.listContainsEvent(events, event)) { continue; } - if (--remainingEntries < 0) { - break; - } } event.url = calendarUrl; @@ -667,15 +682,21 @@ Module.register("calendar", { for (let splitEvent of splitEvents) { if (splitEvent.endDate > now && splitEvent.endDate <= future) { - events.push(splitEvent); + by_url_calevents.push(splitEvent); } } } else { - events.push(event); + by_url_calevents.push(event); } } + by_url_calevents.sort(function (a, b) { + return a.startDate - b.startDate; + }); + Log.debug(`pushing ${by_url_calevents.length} events to total with room for ${remainingEntries}`); + events = events.concat(by_url_calevents.slice(0, remainingEntries)); + Log.debug(`events for calendar=${events.length}`); } - + Log.info(`sorting events count=${events.length}`); events.sort(function (a, b) { return a.startDate - b.startDate; }); @@ -715,7 +736,7 @@ Module.register("calendar", { } events = newEvents; } - + Log.info(`slicing events total maxcount=${this.config.maximumEntries}`); return events.slice(0, this.config.maximumEntries); }, diff --git a/modules/default/calendar/calendarfetcher.js b/modules/default/calendar/calendarfetcher.js index 824c7c5b4f..2a56ff5b4d 100644 --- a/modules/default/calendar/calendarfetcher.js +++ b/modules/default/calendar/calendarfetcher.js @@ -56,7 +56,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn try { data = ical.parseICS(responseData); - Log.debug(`parsed data=${JSON.stringify(data)}`); + Log.debug(`parsed data=${JSON.stringify(data, null, 2)}`); events = CalendarFetcherUtils.filterEvents(data, { excludedEvents, includePastEvents, diff --git a/modules/default/calendar/calendarfetcherutils.js b/modules/default/calendar/calendarfetcherutils.js index eec3540f29..b89f5962c2 100644 --- a/modules/default/calendar/calendarfetcherutils.js +++ b/modules/default/calendar/calendarfetcherutils.js @@ -160,7 +160,7 @@ const CalendarFetcherUtils = { } if (event.type === "VEVENT") { - Log.debug(`Event:\n${JSON.stringify(event)}`); + Log.debug(`Event:\n${JSON.stringify(event, null, 2)}`); let startMoment = eventDate(event, "start"); let endMoment; @@ -246,6 +246,8 @@ const CalendarFetcherUtils = { const location = event.location || false; const geo = event.geo || false; const description = event.description || false; + let d1; + let d2; if (event.rrule && typeof event.rrule !== "undefined" && !isFacebookBirthday) { const rule = event.rrule; @@ -261,9 +263,10 @@ 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; let futureLocal; + if (CalendarFetcherUtils.isFullDayEvent(event)) { Log.debug("fullday"); // if full day event, only use the date part of the ranges @@ -283,52 +286,52 @@ const CalendarFetcherUtils = { } futureLocal = futureMoment.toDate(); // future } - Log.debug(`Search for recurring events between: ${pastLocal} and ${futureLocal}`); - const hasByWeekdayRule = rule.options.byweekday !== undefined && rule.options.byweekday !== null; const oneDayInMs = 24 * 60 * 60 * 1000; + d1 = new Date(new Date(pastLocal.valueOf() - oneDayInMs).getTime()); + d2 = new Date(new Date(futureLocal.valueOf() + oneDayInMs).getTime()); + Log.debug(`Search for recurring events between: ${d1} and ${d2}`); + + event.start = rule.options.dtstart; + + Log.debug("fix rrule start=", rule.options.dtstart); + Log.debug("event before rrule.between=", JSON.stringify(event, null, 2), "exdates=", event.exdate); + // fixup the exdate and recurrence date to local time too for post between() handling + CalendarFetcherUtils.fixEventtoLocal(event); + 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)}`); + + let dates = rule.between(d1, d2, true, () => { return true; }); + + Log.debug(`Title: ${event.summary}, with dates: \n\n${JSON.stringify(dates)}\n`); + + // shouldn't need this anymore, as RRULE not passed junk 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); - } - }); - // Adjusting the dates could push it beyond the 'until' date, so filter those out here. - if (rule.options.until !== null) { - dates = dates.filter((date) => { - return date.valueOf() <= rule.options.until.valueOf(); - }); - } - } - - // 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)); - } + // go thru all the rrule.between() dates and put back the tz offset removed so rrule.between would work + let datesLocal = []; + let offset = d1.getTimezoneOffset(); + Log.debug("offset =", offset); + dates.forEach((d) => { + let dtext = d.toISOString().slice(0, -5); + Log.debug(" date text form without tz=", dtext); + let dLocal = new Date(d.valueOf() + (offset * 60000)); + let offset2 = dLocal.getTimezoneOffset(); + Log.debug("date after offset applied=", dLocal); + if (offset !== offset2) { + // woops, dst/std switch + let delta = offset - offset2; + Log.debug("offset delta=", delta); + dLocal = new Date(d.valueOf() + ((offset - delta) * 60000)); + Log.debug("corrected normalized date=", dLocal); + } else Log.debug(" neutralized date=", dLocal); + datesLocal.push(dLocal); }); + dates = datesLocal; + // 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 @@ -337,29 +340,22 @@ const CalendarFetcherUtils = { // because the logic below will filter out any recurrences that don't actually belong within // our display range. // Would be great if there was a better way to handle this. - Log.debug(`event.recurrences: ${event.recurrences}`); + // + // i don't think we will ever see this anymore (oct 2024) due to code fixes for rrule.between() + // + Log.debug("event.recurrences:", event.recurrences); 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. let d = new Date(dateKey); - if (!moment(d).isBetween(pastMoment, futureMoment)) { + if (!moment(d).isBetween(d1, d2)) { + Log.debug("adding recurring event not found in between list =", d, " should not happen now using local dates oct 17,24"); 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. for (let d in dates) { let date = dates[d]; @@ -367,30 +363,42 @@ const CalendarFetcherUtils = { let curDurationMs = durationMs; let showRecurrence = true; - startMoment = moment(date); + let startMoment = moment(date); - // Remove the time information of each date by using its substring, using the following method: - // .toISOString().substring(0,10). - // since the date is given as ISOString with YYYY-MM-DDTHH:MM:SS.SSSZ - // (see https://momentjs.com/docs/#/displaying/as-iso-string/). - // This must be done after `date` is adjusted - const dateKey = date.toISOString().substring(0, 10); + let dateKey = CalendarFetcherUtils.getDateKeyFromDate(date); + Log.debug("event date dateKey=", dateKey); // For each date that we're checking, it's possible that there is a recurrence override for that one day. - 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]; - startMoment = moment(curEvent.start); - curDurationMs = curEvent.end.valueOf() - startMoment.valueOf(); + if (curEvent.recurrences !== undefined) { + Log.debug("have recurrences=", curEvent.recurrences); + if (curEvent.recurrences[dateKey] !== undefined) { + Log.debug("have a recurrence match for dateKey=", dateKey); + // We found an override, so for this recurrence, use a potentially different title, start date, and duration. + curEvent = curEvent.recurrences[dateKey]; + curEvent.start = new Date(new Date(curEvent.start.valueOf()).getTime()); + curEvent.end = new Date(new Date(curEvent.end.valueOf()).getTime()); + startMoment = CalendarFetcherUtils.getAdjustedStartMoment(curEvent.start, event); + endMoment = CalendarFetcherUtils.getAdjustedStartMoment(curEvent.end, event); + date = curEvent.start; + curDurationMs = new Date(endMoment).valueOf() - startMoment.valueOf(); + } else { + Log.debug("recurrence key ", dateKey, " doesn't match"); + } } // 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; + if (curEvent.exdate !== undefined) { + Log.debug("have datekey=", dateKey, " exdates=", curEvent.exdate); + if (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: ${curDurationMs}`); + startMoment = CalendarFetcherUtils.getAdjustedStartMoment(date, event); + endMoment = moment(startMoment.valueOf() + curDurationMs); + if (startMoment.valueOf() === endMoment.valueOf()) { endMoment = endMoment.endOf("day"); } @@ -408,7 +416,7 @@ const CalendarFetcherUtils = { } if (showRecurrence === true) { - Log.debug(`saving event: ${description}`); + Log.debug(`saving event: ${recurrenceTitle}`); newEvents.push({ title: recurrenceTitle, startDate: startMoment.format("x"), @@ -421,7 +429,10 @@ const CalendarFetcherUtils = { geo: geo, description: description }); + } else { + Log.debug("not saving event ", recurrenceTitle, new Date(startMoment)); } + Log.debug(" "); } // End recurring event parsing. } else { @@ -472,7 +483,9 @@ const CalendarFetcherUtils = { startDate: startMoment.add(adjustHours, "hours").format("x"), endDate: endMoment.add(adjustHours, "hours").format("x"), fullDayEvent: fullDayEvent, + recurringEvent: false, class: event.class, + firstYear: event.start.getFullYear(), location: location, geo: geo, description: description @@ -488,6 +501,200 @@ const CalendarFetcherUtils = { return newEvents; }, + /** + * fixup thew event fields that have dates to use local time + * BEFORE calling rrule.between + * @param the event being processed + * @returns nothing + */ + fixEventtoLocal (event) { + // if there are excluded dates, their date is incorrect and possibly key as well. + if (event.exdate !== undefined) { + Object.keys(event.exdate).forEach((dateKey) => { + // get the date + let exdate = event.exdate[dateKey]; + Log.debug("exdate w key=", exdate); + //exdate=CalendarFetcherUtils.convertDateToLocalTime(exdate, event.end.tz) + exdate = new Date(new Date(exdate.valueOf() - ((120 * 60 * 1000))).getTime()); + Log.debug("new exDate item=", exdate, " with old key=", dateKey); + let newkey = exdate.toISOString().slice(0, 10); + if (newkey !== dateKey) { + Log.debug("new exDate item=", exdate, ` key=${newkey}`); + event.exdate[newkey] = exdate; + //delete event.exdate[dateKey] + } + }); + Log.debug("updated exdate list=", event.exdate); + } + if (event.recurrences) { + Object.keys(event.recurrences).forEach((dateKey) => { + let exdate = event.recurrences[dateKey]; + //exdate=new Date(new Date(exdate.valueOf()-(60*60*1000)).getTime()) + Log.debug("new recurrence item=", exdate, " with old key=", dateKey); + exdate.start = CalendarFetcherUtils.convertDateToLocalTime(exdate.start, exdate.start.tz); + exdate.end = CalendarFetcherUtils.convertDateToLocalTime(exdate.end, exdate.end.tz); + Log.debug("adjusted recurringEvent start=", exdate.start, " end=", exdate.end); + }); + } + Log.debug("modified recurrences before rrule.between", event.recurrences); + }, + + /** + * convert a UTC date to local time + * BEFORE calling rrule.between + * @param date ti conert + * tz event is currently in + * @returns updated date object + */ + convertDateToLocalTime (date, tz) { + let delta_tz_offset = 0; + let now_offset = CalendarFetcherUtils.getTimezoneOffsetFromTimezone(moment.tz.guess()); + let event_offset = CalendarFetcherUtils.getTimezoneOffsetFromTimezone(tz); + Log.debug("date to convert=", date); + if (Math.sign(now_offset) !== Math.sign(event_offset)) { + delta_tz_offset = Math.abs(now_offset) + Math.abs(event_offset); + } else { + // signs are the same + // if negative + if (Math.sign(now_offset) === -1) { + // la looking at chicago + if (now_offset < event_offset) { // 5 -7 + delta_tz_offset = now_offset - event_offset; + } + else { //7 -5 , chicago looking at LA + delta_tz_offset = event_offset - now_offset; + } + } + else { + // berlin looking at sydney + if (now_offset < event_offset) { // 5 -7 + delta_tz_offset = event_offset - now_offset; + Log.debug("less delta=", delta_tz_offset); + } + else { // 11 - 2, sydney looking at berlin + delta_tz_offset = -(now_offset - event_offset); + Log.debug("more delta=", delta_tz_offset); + } + } + } + const newdate = new Date(new Date(date.valueOf() + (delta_tz_offset * 60 * 1000)).getTime()); + Log.debug("modified date =", newdate); + return newdate; + }, + + /** + * get the exdate/recurrence hash key from the date object + * BEFORE calling rrule.between + * @param the date of the event + * @returns string date key YYYY-MM-DD + */ + getDateKeyFromDate (date) { + // get our runtime timezone offset + const nowDiff = CalendarFetcherUtils.getTimezoneOffsetFromTimezone(moment.tz.guess()); + let startday = date.getDate(); + let adjustment = 0; + Log.debug(" day of month=", (`0${startday}`).slice(-2), " nowDiff=", nowDiff, ` start time=${date.toString().split(" ")[4].slice(0, 2)}`); + Log.debug("date string= ", date.toString()); + Log.debug("date iso string ", date.toISOString()); + // if the dates are different + if (date.toString().slice(8, 10) < date.toISOString().slice(8, 10)) { + startday = date.toString().slice(8, 10); + Log.debug("< ", startday); + } else { // tostring is more + if (date.toString().slice(8, 10) > date.toISOString().slice(8, 10)) { + startday = date.toISOString().slice(8, 10); + Log.debug("> ", startday); + } + } + return date.toISOString().substring(0, 8) + (`0${startday}`).slice(-2); + }, + + /** + * get the timezone offset from the timezone string + * + * @param the timezone string + * @returns the numerical offset + */ + getTimezoneOffsetFromTimezone (timeZone) { + const str = new Date().toLocaleString("en", { timeZone, timeZoneName: "longOffset" }); + Log.debug("tz offset=", str); + const [_, h, m] = str.match(/([+-]\d+):(\d+)$/) || ["", "+00", "00"]; + return h * 60 + (h > 0 ? +m : -m); + }, + + /** + * fixup the date start moment after rrule.between returns date array + * + * @param date object from rrule.between results + * the event object it came from + * @returns moment object + */ + getAdjustedStartMoment (date, event) { + + let startMoment = moment(date); + + Log.debug("startMoment pre=", startMoment); + // get our runtime timezone offset + const nowDiff = CalendarFetcherUtils.getTimezoneOffsetFromTimezone(moment.tz.guess()); // 10/18 16:49, 300 + let eventDiff = CalendarFetcherUtils.getTimezoneOffsetFromTimezone(event.end.tz); // watch out, start tz is cleared to handle rrule 120 23:49 + + Log.debug("tz diff event=", eventDiff, " local=", nowDiff, " end event timezone=", event.end.tz); + + // if the diffs are different (not same tz for processing as event) + if (nowDiff !== eventDiff) { + // if signs are different + if (Math.sign(nowDiff) !== Math.sign(eventDiff)) { + // its the accumulated total + Log.debug("diff signs, accumulate"); + eventDiff = Math.abs(eventDiff) + Math.abs(nowDiff); + // sign of diff depends on where you are looking at which event. + // australia looking at US, add to get same time + Log.debug("new different event diff=", eventDiff); + if (Math.sign(nowDiff) === -1) { + eventDiff *= -1; + // US looking at australia event have to subtract + Log.debug("new diff, same sign, total event diff=", eventDiff); + } + } + else { + // signs are the same, all east of UTC or all west of UTC + // if the signs are negative (west of UTC) + Log.debug("signs are the same"); + if (Math.sign(eventDiff) === -1) { + //if west, looking at more west + if (nowDiff < eventDiff) { + //-600 -420 + eventDiff = -(eventDiff - (nowDiff - eventDiff)); //-180 + Log.debug("now looking back east delta diff=", eventDiff); + } + else { + Log.debug("now looking more west"); + eventDiff = Math.abs(eventDiff - nowDiff); + } + } else { + Log.debug("signs are both positive"); + // signs are positive (east of UTC) + // berlin < sydney + if (nowDiff < eventDiff) { + // germany vs australia + eventDiff = -(eventDiff - nowDiff); + } + else { + // australia vs germany + //eventDiff = eventDiff; //- nowDiff + } + } + } + startMoment = moment.tz(new Date(date.valueOf() + (eventDiff * (60 * 1000))), event.end.tz); + } else { + Log.debug("same tz event and display"); + eventDiff = 0; + startMoment = moment.tz(new Date(date.valueOf() - (eventDiff * (60 * 1000))), event.end.tz); + } + Log.debug("startMoment post=", startMoment); + return startMoment; + }, + /** * Lookup iana tz from windows * @param {string} msTZName the timezone name to lookup diff --git a/modules/default/compliments/compliments.js b/modules/default/compliments/compliments.js index 9f9270f199..b7dc1295b0 100644 --- a/modules/default/compliments/compliments.js +++ b/modules/default/compliments/compliments.js @@ -49,7 +49,12 @@ Module.register("compliments", { if ((this.config.remoteFileRefreshInterval >= this.refreshMinimumDelay) || window.mmTestMode === "true") { setInterval(async () => { const response = await this.loadComplimentFile(); - this.compliments_new = JSON.parse(response); + if (response) { + this.compliments_new = JSON.parse(response); + } + else { + Log.error(`${this.name} remoteFile refresh failed`); + } }, this.config.remoteFileRefreshInterval); } else { @@ -204,10 +209,14 @@ Module.register("compliments", { // we need to force the server to not give us the cached result // create an extra property (ignored by the server handler) just so the url string is different // that will never be the same, using the ms value of date - if (this.config.remoteFileRefreshInterval !== 0) this.urlSuffix = `?dummy=${Date.now()}`; + if (isRemote && this.config.remoteFileRefreshInterval !== 0) this.urlSuffix = `?dummy=${Date.now()}`; // - const response = await fetch(url + this.urlSuffix); - return await response.text(); + try { + const response = await fetch(url + this.urlSuffix); + return await response.text(); + } catch (error) { + Log.info(`${this.name} fetch failed error=`, error); + } }, /** diff --git a/package-lock.json b/package-lock.json index 3b7874cc2a..312e72de94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "iconv-lite": "^0.6.3", "module-alias": "^2.2.3", "moment": "^2.30.1", - "node-ical": "0.18.0", + "node-ical": "^0.20.1", "pm2": "^5.4.2", "socket.io": "^4.8.1", "suncalc": "^1.9.0", @@ -3299,12 +3299,12 @@ } }, "node_modules/axios": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", - "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.4", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -9540,15 +9540,14 @@ } }, "node_modules/node-ical": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/node-ical/-/node-ical-0.18.0.tgz", - "integrity": "sha512-FrOUPztjw9OUgSB9o/ffhl86BiVClQTut97C2NqCwKIgOAcKPEw5UQMuSuNJO/Y4hqTyJdKZh2TCqNHQnE9YFg==", - "license": "Apache-2.0", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/node-ical/-/node-ical-0.20.1.tgz", + "integrity": "sha512-NrXgzDJd6XcyX9kDMJVA3xYCZmntY7ghA2BOdBeYr3iu8tydHOAb+68jPQhF9V2CRQ0/386X05XhmLzQUN0+Hw==", "dependencies": { - "axios": "1.6.7", - "moment-timezone": "^0.5.44", + "axios": "^1.7.7", + "moment-timezone": "^0.5.45", "rrule": "2.8.1", - "uuid": "^9.0.0" + "uuid": "^10.0.0" } }, "node_modules/node-int64": { @@ -12823,9 +12822,9 @@ } }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" diff --git a/package.json b/package.json index b4cd85086b..d2200a6149 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "iconv-lite": "^0.6.3", "module-alias": "^2.2.3", "moment": "^2.30.1", - "node-ical": "0.18.0", + "node-ical": "^0.20.1", "pm2": "^5.4.2", "socket.io": "^4.8.1", "suncalc": "^1.9.0", diff --git a/tests/configs/modules/calendar/3_move_first_allday_repeating_event.js b/tests/configs/modules/calendar/3_move_first_allday_repeating_event.js new file mode 100644 index 0000000000..30afae2ccb --- /dev/null +++ b/tests/configs/modules/calendar/3_move_first_allday_repeating_event.js @@ -0,0 +1,35 @@ +let config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 24, + units: "metric", + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + hideDuplicates: false, + maximumEntries: 100, + urgency: 0, + dateFormat: "Do.MMM, HH:mm", + fullDayEventDateFormat: "Do.MMM", + timeFormat: "absolute", + getRelative: 0, + maximumNumberOfDays: 28, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/3_move_first_allday_repeating_event.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/configs/modules/calendar/berlin_end_of_day_repeating.js b/tests/configs/modules/calendar/berlin_end_of_day_repeating.js new file mode 100644 index 0000000000..b706347897 --- /dev/null +++ b/tests/configs/modules/calendar/berlin_end_of_day_repeating.js @@ -0,0 +1,33 @@ +let config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "Do.MMM, HH:mm", + fullDayEventDateFormat: "Do.MMM", + timeFormat: "absolute", + getRelative: 0, + maximumNumberOfDays: 28, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/end_of_day_berlin_moved.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/configs/modules/calendar/berlin_multi.js b/tests/configs/modules/calendar/berlin_multi.js new file mode 100644 index 0000000000..50069371ba --- /dev/null +++ b/tests/configs/modules/calendar/berlin_multi.js @@ -0,0 +1,33 @@ +let config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "Do.MMM, HH:mm", + fullDayEventDateFormat: "Do.MMM", + timeFormat: "absolute", + getRelative: 0, + maximumNumberOfDays: 28, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/RepeatingEvent.Oct21.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/configs/modules/calendar/berlin_whole_day_event_moved_over_dst_change.js b/tests/configs/modules/calendar/berlin_whole_day_event_moved_over_dst_change.js new file mode 100644 index 0000000000..255bbd950e --- /dev/null +++ b/tests/configs/modules/calendar/berlin_whole_day_event_moved_over_dst_change.js @@ -0,0 +1,33 @@ +let config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "Do.MMM, HH:mm", + fullDayEventDateFormat: "Do.MMM", + timeFormat: "absolute", + getRelative: 0, + maximumNumberOfDays: 28, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/whole_day_moved_over_dst_change_berlin.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/configs/modules/calendar/chicago_late_in_timezone.js b/tests/configs/modules/calendar/chicago_late_in_timezone.js new file mode 100644 index 0000000000..3591b4cb79 --- /dev/null +++ b/tests/configs/modules/calendar/chicago_late_in_timezone.js @@ -0,0 +1,33 @@ +let config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "Do.MMM, HH:mm", + fullDayEventDateFormat: "Do.MMM", + timeFormat: "absolute", + getRelative: 0, + maximumNumberOfDays: 20, + calendars: [ + { + maximumEntries: 100, + //url: "http://localhost:8080/tests/mocks/chicago_late_in_timezone.ics" + url: "http://localhost:8080/tests/mocks/chicago_late_in_timezone.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/configs/modules/calendar/diff_tz_start_end.js b/tests/configs/modules/calendar/diff_tz_start_end.js new file mode 100644 index 0000000000..829ef3a0ad --- /dev/null +++ b/tests/configs/modules/calendar/diff_tz_start_end.js @@ -0,0 +1,34 @@ +let config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "Do.MMM, HH:mm", + dateEndFormat: "Do.MMM, HH:mm", + fullDayEventDateFormat: "Do.MMM", + timeFormat: "absolute", + getRelative: 0, + maximumNumberOfDays: 28, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/diff_tz_start_end.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/configs/modules/calendar/end_of_day_berlin_moved.js b/tests/configs/modules/calendar/end_of_day_berlin_moved.js new file mode 100644 index 0000000000..b706347897 --- /dev/null +++ b/tests/configs/modules/calendar/end_of_day_berlin_moved.js @@ -0,0 +1,33 @@ +let config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "Do.MMM, HH:mm", + fullDayEventDateFormat: "Do.MMM", + timeFormat: "absolute", + getRelative: 0, + maximumNumberOfDays: 28, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/end_of_day_berlin_moved.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end.js b/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end.js new file mode 100644 index 0000000000..95989648ca --- /dev/null +++ b/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end.js @@ -0,0 +1,33 @@ +let config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "Do.MMM, HH:mm", + dateEndFormat: "Do.MMM, HH:mm", + fullDayEventDateFormat: "Do.MMM", + timeFormat: "absolute", + getRelative: 0, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/event_with_time_over_multiple_days_non_repeating.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_no_display_end.js b/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_no_display_end.js new file mode 100644 index 0000000000..ef60df4c94 --- /dev/null +++ b/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_no_display_end.js @@ -0,0 +1,34 @@ +let config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "Do.MMM, HH:mm", + dateEndFormat: "Do.MMM, HH:mm", + fullDayEventDateFormat: "Do.MMM", + timeFormat: "absolute", + getRelative: 0, + showEnd: true, + showEndsOnlyWithDuration: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/event_with_time_over_multiple_days_non_repeating.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/configs/modules/calendar/exdate_and_recurrence_together.js b/tests/configs/modules/calendar/exdate_and_recurrence_together.js new file mode 100644 index 0000000000..b1dc06389b --- /dev/null +++ b/tests/configs/modules/calendar/exdate_and_recurrence_together.js @@ -0,0 +1,33 @@ +let config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "Do.MMM, HH:mm", + fullDayEventDateFormat: "Do.MMM", + timeFormat: "absolute", + getRelative: 0, + maximumNumberOfDays: 28, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/exdate_and_recurrence_together.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/configs/modules/calendar/fullday_event_over_multiple_days_nonrepeating.js b/tests/configs/modules/calendar/fullday_event_over_multiple_days_nonrepeating.js new file mode 100644 index 0000000000..7ec5885cb7 --- /dev/null +++ b/tests/configs/modules/calendar/fullday_event_over_multiple_days_nonrepeating.js @@ -0,0 +1,32 @@ +let config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "Do.MMM, HH:mm", + fullDayEventDateFormat: "Do.MMM", + timeFormat: "absolute", + getRelative: 0, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/fullday_event_over_multiple_days_nonrepeating.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/configs/modules/calendar/germany_at_end_of_day_repeating.js b/tests/configs/modules/calendar/germany_at_end_of_day_repeating.js new file mode 100644 index 0000000000..e9d7e88337 --- /dev/null +++ b/tests/configs/modules/calendar/germany_at_end_of_day_repeating.js @@ -0,0 +1,33 @@ +let config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 12, + + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + hideDuplicates: false, + maximumEntries: 100, + sliceMultiDayEvents: true, + dateFormat: "MMM Do, HH:mm", + timeFormat: "absolute", + getRelative: 0, + urgency: 0, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/germany_at_end_of_day_repeating.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/e2e/helpers/mock-console.js b/tests/e2e/helpers/mock-console.js index c593b7062e..b903b7ac51 100644 --- a/tests/e2e/helpers/mock-console.js +++ b/tests/e2e/helpers/mock-console.js @@ -8,6 +8,7 @@ const mockError = (err) => { || err.includes("ECONNRESET") || err.includes("socket hang up") || err.includes("exports is not defined") + || err.includes("module is not defined") || err.includes("write EPIPE") || err.includes("AggregateError") || err.includes("ERR_SOCKET_CONNECTION_TIMEOUT") diff --git a/tests/e2e/modules/compliments_spec.js b/tests/e2e/modules/compliments_spec.js index 7763dc689b..47214781ab 100644 --- a/tests/e2e/modules/compliments_spec.js +++ b/tests/e2e/modules/compliments_spec.js @@ -89,4 +89,37 @@ describe("Compliments module", () => { }); }); }); + + describe("Feature remote compliments file", () => { + describe("get list from remote file", () => { + beforeAll(async () => { + await helpers.startApplication("tests/configs/modules/compliments/compliments_file.js"); + await helpers.getDocument(); + }); + it("shows 'Remote compliment file works!' as only anytime list set", async () => { + //await helpers.startApplication("tests/configs/modules/compliments/compliments_file.js", "01 Jan 2022 10:00:00 GMT"); + await expect(doTest(["Remote compliment file works!"])).resolves.toBe(true); + }); + // afterAll(async () =>{ + // await helpers.stopApplication() + // }); + }); + + describe("get list from remote file w update", () => { + beforeAll(async () => { + await helpers.startApplication("tests/configs/modules/compliments/compliments_file_change.js"); + await helpers.getDocument(); + }); + it("shows 'test in morning' as test time set to 10am", async () => { + //await helpers.startApplication("tests/configs/modules/compliments/compliments_file_change.js", "01 Jan 2022 10:00:00 GMT"); + await expect(doTest(["Remote compliment file works!"])).resolves.toBe(true); + await new Promise((r) => setTimeout(r, 10000)); + await expect(doTest(["test in morning"])).resolves.toBe(true); + }); + // afterAll(async () =>{ + // await helpers.stopApplication() + // }); + }); + }); + }); diff --git a/tests/electron/modules/calendar_spec.js b/tests/electron/modules/calendar_spec.js index 8731c3cd1a..f73d673489 100644 --- a/tests/electron/modules/calendar_spec.js +++ b/tests/electron/modules/calendar_spec.js @@ -22,6 +22,21 @@ describe("Calendar module", () => { return await loc.count(); }; + const first = 0; + const second = 1; + const third = 2; + const last = -1; + + // get results of table row and column, can select specific row of results, + // row is 0 based index -1 is last, 0 is first... need 10th(human count), use 9 as row + // uses playwright nth locator syntax + const doTestTableContent = async (table_row, table_column, content, row = first) => { + const elem = await global.page.locator(table_row); + const date = await global.page.locator(table_column).locator(`nth=${row}`); + await expect(date.textContent()).resolves.toContain(content); + return true; + }; + afterEach(async () => { await helpers.stopApplication(); }); @@ -147,4 +162,121 @@ describe("Calendar module", () => { }); }); + describe("sliceMultiDayEvents direct count", () => { + it("Issue #3452 split multiday in Europe", async () => { + await helpers.startApplication("tests/configs/modules/calendar/sliceMultiDayEvents.js", "01 Sept 2024 10:38:00 GMT+02:00", ["js/electron.js"], "Europe/Berlin"); + await expect(doTestCount()).resolves.toBe(6); + }); + }); + + describe("germany timezone", () => { + it("Issue #unknown fullday timezone East of UTC edge", async () => { + await helpers.startApplication("tests/configs/modules/calendar/germany_at_end_of_day_repeating.js", "01 Oct 2024 10:38:00 GMT+02:00", ["js/electron.js"], "Europe/Berlin"); + await expect(doTestTableContent(".calendar .event", ".time", "Oct 22nd, 23:00", first)).resolves.toBe(true); + }); + }); + + describe("germany all day repeating moved (recurrence and exdate)", () => { + it("Issue #unknown fullday timezone East of UTC event moved", async () => { + await helpers.startApplication("tests/configs/modules/calendar/3_move_first_allday_repeating_event.js", "01 Oct 2024 10:38:00 GMT+02:00", ["js/electron.js"], "Europe/Berlin"); + await expect(doTestTableContent(".calendar .event", ".time", "12th.Oct")).resolves.toBe(true); + }); + }); + + describe("chicago late in timezone", () => { + it("Issue #unknown rrule US close to timezone edge", async () => { + await helpers.startApplication("tests/configs/modules/calendar/chicago_late_in_timezone.js", "01 Sept 2024 10:38:00 GMT-5:00", ["js/electron.js"], "America/Chicago"); + await expect(doTestTableContent(".calendar .event", ".time", "10th.Sep, 20:15")).resolves.toBe(true); + }); + }); + + describe("berlin late in day event moved, viewed from berlin", () => { + it("Issue #unknown rrule ETC+2 close to timezone edge", async () => { + await helpers.startApplication("tests/configs/modules/calendar/end_of_day_berlin_moved.js", "08 Oct 2024 12:30:00 GMT+02:00", ["js/electron.js"], "Europe/Berlin"); + await expect(doTestTableContent(".calendar .event", ".time", "24th.Oct, 23:00-00:00", last)).resolves.toBe(true); + }); + }); + + describe("berlin late in day event moved, viewed from sydney", () => { + it("Issue #unknown rrule ETC+2 close to timezone edge", async () => { + await helpers.startApplication("tests/configs/modules/calendar/end_of_day_berlin_moved.js", "08 Oct 2024 12:30:00 GMT+02:00", ["js/electron.js"], "Australia/Sydney"); + await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 01:00-02:00", last)).resolves.toBe(true); + }); + }); + + describe("berlin late in day event moved, viewed from chicago", () => { + it("Issue #unknown rrule ETC+2 close to timezone edge", async () => { + await helpers.startApplication("tests/configs/modules/calendar/end_of_day_berlin_moved.js", "08 Oct 2024 12:30:00 GMT+02:00", ["js/electron.js"], "America/Chicago"); + await expect(doTestTableContent(".calendar .event", ".time", "24th.Oct, 16:00-17:00", last)).resolves.toBe(true); + }); + }); + + describe("berlin multi-events inside offset", () => { + it("some events before DST. some after midnight", async () => { + await helpers.startApplication("tests/configs/modules/calendar/berlin_multi.js", "08 Oct 2024 12:30:00 GMT+02:00", ["js/electron.js"], "Europe/Berlin"); + await expect(doTestTableContent(".calendar .event", ".time", "30th.Oct, 00:00-01:00", last)).resolves.toBe(true); + await expect(doTestTableContent(".calendar .event", ".time", "21st.Oct, 00:00-01:00", first)).resolves.toBe(true); + }); + }); + + describe("berlin whole day repeating, start moved after end", () => { + it("some events before DST. some after", async () => { + await helpers.startApplication("tests/configs/modules/calendar/berlin_whole_day_event_moved_over_dst_change.js", "08 Oct 2024 12:30:00 GMT+02:00", ["js/electron.js"], "Europe/Berlin"); + await expect(doTestTableContent(".calendar .event", ".time", "30th.Oct", last)).resolves.toBe(true); + await expect(doTestTableContent(".calendar .event", ".time", "27th.Oct", first)).resolves.toBe(true); + }); + }); + + describe("berlin 11pm-midnight", () => { + it("right inside the offset, before midnight", async () => { + await helpers.startApplication("tests/configs/modules/calendar/berlin_end_of_day_repeating.js", "08 Oct 2024 12:30:00 GMT+02:00", ["js/electron.js"], "Europe/Berlin"); + await expect(doTestTableContent(".calendar .event", ".time", "24th.Oct, 23:00-00:00", last)).resolves.toBe(true); + await expect(doTestTableContent(".calendar .event", ".time", "22nd.Oct, 23:00-00:00", first)).resolves.toBe(true); + }); + }); + + describe("both moved and delete events in recurring list", () => { + it("with moved before and after original", async () => { + await helpers.startApplication("tests/configs/modules/calendar/exdate_and_recurrence_together.js", "08 Oct 2024 12:30:00 GMT-07:00", ["js/electron.js"], "America/Los_Angeles"); + // moved after end at oct 26 + await expect(doTestTableContent(".calendar .event", ".time", "27th.Oct, 14:30-15:30", last)).resolves.toBe(true); + // moved before start at oct 23 + await expect(doTestTableContent(".calendar .event", ".time", "22nd.Oct, 14:30-15:30", first)).resolves.toBe(true); + // remaining original 4th, now 3rd + await expect(doTestTableContent(".calendar .event", ".time", "26th.Oct, 14:30-15:30", second)).resolves.toBe(true); + }); + }); + + describe("one event diff tz", () => { + it("start/end in diff timezones", async () => { + await helpers.startApplication("tests/configs/modules/calendar/diff_tz_start_end.js", "08 Oct 2024 12:30:00 GMT-07:00", ["js/electron.js"], "America/Chicago"); + // just + await expect(doTestTableContent(".calendar .event", ".time", "29th.Oct, 05:00-30th.Oct, 18:00", first)).resolves.toBe(true); + }); + }); + + describe("one event non repeating", () => { + it("fullday non-repeating", async () => { + await helpers.startApplication("tests/configs/modules/calendar/fullday_event_over_multiple_days_nonrepeating.js", "08 Oct 2024 12:30:00 GMT-07:00", ["js/electron.js"], "America/Chicago"); + // just + await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct-30th.Oct", first)).resolves.toBe(true); + }); + }); + + describe("one event no end display", () => { + it("don't display end", async () => { + await helpers.startApplication("tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_no_display_end.js", "08 Oct 2024 12:30:00 GMT-07:00", ["js/electron.js"], "America/Chicago"); + // just + await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 20:00", first)).resolves.toBe(true); + }); + }); + + describe("display end display end", () => { + it("display end", async () => { + await helpers.startApplication("tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end.js", "08 Oct 2024 12:30:00 GMT-07:00", ["js/electron.js"], "America/Chicago"); + // just + await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 20:00-26th.Oct, 06:00", first)).resolves.toBe(true); + }); + }); + }); diff --git a/tests/electron/modules/compliments_spec.js b/tests/electron/modules/compliments_spec.js index 8626b503c8..4555ffcbf7 100644 --- a/tests/electron/modules/compliments_spec.js +++ b/tests/electron/modules/compliments_spec.js @@ -88,7 +88,7 @@ describe("Compliments module", () => { }); }); describe("get updated list from remote file", () => { - it("shows 'test in morning' as test time set to 10am", async () => { + it("shows 'test in morning'", async () => { await helpers.startApplication("tests/configs/modules/compliments/compliments_file_change.js", "01 Jan 2022 10:00:00 GMT"); await expect(doTest(["Remote compliment file works!"])).resolves.toBe(true); await new Promise((r) => setTimeout(r, 10000)); diff --git a/tests/mocks/3_move_first_allday_repeating_event.ics b/tests/mocks/3_move_first_allday_repeating_event.ics new file mode 100644 index 0000000000..d0587e35e1 --- /dev/null +++ b/tests/mocks/3_move_first_allday_repeating_event.ics @@ -0,0 +1,35 @@ +BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:TestCal +X-WR-TIMEZONE:Europe/Berlin +X-WR-CALDESC:Calendar for testing purposes +BEGIN:VEVENT +DTSTART;VALUE=DATE:20241011 +DTEND;VALUE=DATE:20241012 +RRULE:FREQ=WEEKLY;WKST=MO;COUNT=5;BYDAY=FR +DTSTAMP:20241009T153220Z +UID:2m6mt1p89l2anl74915ur3hsgm@google.com +CREATED:20241009T153058Z +LAST-MODIFIED:20241009T153205Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:TestCal_AllDayRepeatingEvent +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20241012 +DTEND;VALUE=DATE:20241013 +DTSTAMP:20241009T153220Z +UID:2m6mt1p89l2anl74915ur3hsgm@google.com +RECURRENCE-ID;VALUE=DATE:20241011 +CREATED:20241009T153058Z +LAST-MODIFIED:20241009T153205Z +SEQUENCE:1 +STATUS:CONFIRMED +SUMMARY:TestCal_AllDayRepeatingEvent +TRANSP:TRANSPARENT +END:VEVENT +END:VCALENDAR diff --git a/tests/mocks/RepeatingEvent.Oct21.ics b/tests/mocks/RepeatingEvent.Oct21.ics new file mode 100644 index 0000000000..9eb6130960 --- /dev/null +++ b/tests/mocks/RepeatingEvent.Oct21.ics @@ -0,0 +1,28 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTART;TZID=Europe/Berlin:20241028T000000 +DTEND;TZID=Europe/Berlin:20241028T010000 +RRULE:FREQ=DAILY;COUNT=3 +DTSTAMP:20241020T093758Z +UID:053fdshnnibo92lu97rsoeqoti@google.com +CREATED:20241020T093230Z +LAST-MODIFIED:20241020T093230Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:RepeatingEventWeekAfterToday +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Europe/Berlin:20241021T000000 +DTEND;TZID=Europe/Berlin:20241021T010000 +RRULE:FREQ=DAILY;COUNT=3 +DTSTAMP:20241020T093758Z +UID:1a6kk47pp61k4td2h9rlf0lv69@google.com +CREATED:20241020T093255Z +LAST-MODIFIED:20241020T093437Z +SEQUENCE:1 +STATUS:CONFIRMED +SUMMARY:RepeatingEventDayAfterToday +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR diff --git a/tests/mocks/chicago_late_in_timezone.ics b/tests/mocks/chicago_late_in_timezone.ics new file mode 100644 index 0000000000..2c9447723c --- /dev/null +++ b/tests/mocks/chicago_late_in_timezone.ics @@ -0,0 +1,15 @@ +BEGIN:VEVENT +CREATED:20240904T053053Z +DTEND;TZID=America/Chicago:20240910T211500 +DTSTAMP:20240925T005517Z +DTSTART;TZID=America/Chicago:20240910T201500 +LAST-MODIFIED:20240925T005515Z +LOCATION:Dance Class +RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:2D48CA37-FCE5-4E16-871 +9-1F47160BDBA3 +RRULE:FREQ=WEEKLY;UNTIL=20250601T011500Z +SEQUENCE:3 +SUMMARY:Wife Barre Class +UID:39669340-7AFD-4685-9BD6-6CE4B715486E +X-APPLE-CREATOR-IDENTITY:com.apple.mobilecal +END:VEVENT \ No newline at end of file diff --git a/tests/mocks/compliments_file.json b/tests/mocks/compliments_file.json index 89171b16ed..03e2e8b109 100644 --- a/tests/mocks/compliments_file.json +++ b/tests/mocks/compliments_file.json @@ -1,5 +1,3 @@ { - "morning": ["test in morning"], - "afternoon": ["test in afternoon"], - "evening": ["test in evening"] + "anytime": ["test in morning"] } diff --git a/tests/mocks/diff_tz_start_end.ics b/tests/mocks/diff_tz_start_end.ics new file mode 100644 index 0000000000..b59ba41ed4 --- /dev/null +++ b/tests/mocks/diff_tz_start_end.ics @@ -0,0 +1,14 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTART:20241029T100000Z +DTEND:20241030T230000Z +DTSTAMP:20241022T203806Z +UID:04ivnntdi20rqsk0iesabsdhmj@google.com +CREATED:20241022T203738Z +LAST-MODIFIED:20241022T203738Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:start/end on diff tz +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR diff --git a/tests/mocks/end_of_day_berlin_moved.ics b/tests/mocks/end_of_day_berlin_moved.ics new file mode 100644 index 0000000000..c5a02d0336 --- /dev/null +++ b/tests/mocks/end_of_day_berlin_moved.ics @@ -0,0 +1,54 @@ +BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:test for mirror +X-WR-TIMEZONE:America/Chicago +X-WR-CALDESC:used to test mirror +BEGIN:VTIMEZONE +TZID:Europe/Berlin +X-LIC-LOCATION:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:GMT+2 +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:GMT+1 +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=Europe/Berlin:20241021T230000 +DTEND;TZID=Europe/Berlin:20241022T000000 +RRULE:FREQ=DAILY;WKST=SU;COUNT=3 +DTSTAMP:20241019T133432Z +UID:0kj3dtvgskhhpli1392n111145@google.com +CREATED:20241018T213040Z +LAST-MODIFIED:20241018T213126Z +SEQUENCE:1 +STATUS:CONFIRMED +SUMMARY:test +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Europe/Berlin:20241024T230000 +DTEND;TZID=Europe/Berlin:20241025T000000 +DTSTAMP:20241019T133432Z +UID:0kj3dtvgskhhpli1392n111145@google.com +RECURRENCE-ID;TZID=Europe/Berlin:20241021T230000 +CREATED:20241018T213040Z +LAST-MODIFIED:20241018T213126Z +SEQUENCE:2 +STATUS:CONFIRMED +SUMMARY:test +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR + diff --git a/tests/mocks/event_with_time_over_multiple_days_non_repeating.ics b/tests/mocks/event_with_time_over_multiple_days_non_repeating.ics new file mode 100644 index 0000000000..a12d58dd1c --- /dev/null +++ b/tests/mocks/event_with_time_over_multiple_days_non_repeating.ics @@ -0,0 +1,14 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTART:20241026T010000Z +DTEND:20241026T110000Z +DTSTAMP:20241024T153358Z +UID:4maud6s79m41a99pj2g7j5km0a@google.com +CREATED:20241024T153313Z +LAST-MODIFIED:20241024T153330Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Sleep over at Bobs +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR diff --git a/tests/mocks/exdate_and_recurrence_together.ics b/tests/mocks/exdate_and_recurrence_together.ics new file mode 100644 index 0000000000..d366cc42df --- /dev/null +++ b/tests/mocks/exdate_and_recurrence_together.ics @@ -0,0 +1,48 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTART;TZID=America/Los_Angeles:20241023T143000 +DTEND;TZID=America/Los_Angeles:20241023T153000 +RRULE:FREQ=DAILY;COUNT=4 +EXDATE;TZID=America/Los_Angeles:20241025T143000 +DTSTAMP:20241021T193426Z +UID:18rd721lfqpue2o08icsqek198@google.com +CREATED:20241021T192450Z +DESCRIPTION:we will move one entry and delete another  ending w 3 of the 4  + start/end\, middle moved after end and 3rd deleted +LAST-MODIFIED:20241021T193419Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:recurrence and exclusion together +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=America/Los_Angeles:20241022T143000 +DTEND;TZID=America/Los_Angeles:20241022T153000 +DTSTAMP:20241021T193426Z +UID:18rd721lfqpue2o08icsqek198@google.com +RECURRENCE-ID;TZID=America/Los_Angeles:20241023T143000 +CREATED:20241021T192450Z +DESCRIPTION:we will move one entry and delete another  ending w 3 of the 4  + start/end\, middle moved after end and 3rd deleted +LAST-MODIFIED:20241021T193419Z +SEQUENCE:1 +STATUS:CONFIRMED +SUMMARY:recurrence and exclusion together +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=America/Los_Angeles:20241027T143000 +DTEND;TZID=America/Los_Angeles:20241027T153000 +DTSTAMP:20241021T193426Z +UID:18rd721lfqpue2o08icsqek198@google.com +RECURRENCE-ID;TZID=America/Los_Angeles:20241024T143000 +CREATED:20241021T192450Z +DESCRIPTION:we will move one entry and delete another  ending w 3 of the 4  + start/end\, middle moved after end and 3rd deleted +LAST-MODIFIED:20241021T193419Z +SEQUENCE:1 +STATUS:CONFIRMED +SUMMARY:recurrence and exclusion together +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR diff --git a/tests/mocks/fullday_event_over_multiple_days_nonrepeating.ics b/tests/mocks/fullday_event_over_multiple_days_nonrepeating.ics new file mode 100644 index 0000000000..8d506e5479 --- /dev/null +++ b/tests/mocks/fullday_event_over_multiple_days_nonrepeating.ics @@ -0,0 +1,15 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTART;VALUE=DATE:20241025 +DTEND;VALUE=DATE:20241031 +DTSTAMP:20241023T141110Z +UID:60nobfcu0ct96jgsh5nhcia24b@google.com +CREATED:20241023T141019Z +DESCRIPTION:test for all day end viewing +LAST-MODIFIED:20241023T141019Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:simple all day event over many days (not repeating) +TRANSP:TRANSPARENT +END:VEVENT +END:VCALENDAR diff --git a/tests/mocks/germany_at_end_of_day_repeating.ics b/tests/mocks/germany_at_end_of_day_repeating.ics new file mode 100644 index 0000000000..9a72df0aed --- /dev/null +++ b/tests/mocks/germany_at_end_of_day_repeating.ics @@ -0,0 +1,15 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTART;TZID=Europe/Berlin:20241022T230000 +DTEND;TZID=Europe/Berlin:20241023T000000 +RRULE:FREQ=DAILY;WKST=MO;COUNT=4 +DTSTAMP:20241009T153220Z +UID:2m6mt1p89l2anl74915ur3hsgm@google.com +CREATED:20241009T153058Z +LAST-MODIFIED:20241009T153205Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:TestCal_AllDayRepeatingEvent +TRANSP:TRANSPARENT +END:VEVENT +END:VCALENDAR \ No newline at end of file diff --git a/tests/mocks/whole_day_moved_over_dst_change_berlin.ics b/tests/mocks/whole_day_moved_over_dst_change_berlin.ics new file mode 100644 index 0000000000..7335ccfdb4 --- /dev/null +++ b/tests/mocks/whole_day_moved_over_dst_change_berlin.ics @@ -0,0 +1,28 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTART;VALUE=DATE:20241027 +DTEND;VALUE=DATE:20241028 +RRULE:FREQ=DAILY;WKST=SU;COUNT=3 +DTSTAMP:20241020T152634Z +UID:14nv8jl8d6dvdbl477lod4fftf@google.com +CREATED:20241020T152434Z +LAST-MODIFIED:20241020T152536Z +SEQUENCE:1 +STATUS:CONFIRMED +SUMMARY:test whole day moved +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20241030 +DTEND;VALUE=DATE:20241031 +DTSTAMP:20241020T152634Z +UID:14nv8jl8d6dvdbl477lod4fftf@google.com +RECURRENCE-ID;VALUE=DATE:20241028 +CREATED:20241020T152434Z +LAST-MODIFIED:20241020T152536Z +SEQUENCE:2 +STATUS:CONFIRMED +SUMMARY:test whole day moved +TRANSP:TRANSPARENT +END:VEVENT +END:VCALENDAR