From 291ae8546cb5ec54162135bd8bcaad2cef1a06f9 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sat, 7 Dec 2024 08:17:04 +0100 Subject: [PATCH 1/2] Handle "module is not defined" in e2e tests (#3647) Even in successful tests, there are many module not defined` error entries. Like this: https://github.com/MagicMirrorOrg/MagicMirror/actions/runs/12199106844/job/34032254241#step:5:353 I think we can suppress them. --- CHANGELOG.md | 5 +++-- tests/e2e/helpers/mock-console.js | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cdc31ec37..40e3802fa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ _This release is scheduled to be released on 2025-01-01._ ### 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 @@ -43,7 +43,8 @@ _This release is scheduled to be released on 2025-01-01._ - [tests] Fix test cases 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) +- [tests] Suppress "module is not defined" in e2e tests ## [2.29.0] - 2024-10-01 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") From 19bd76ab932866baf49404582398b7dbce83dd15 Mon Sep 17 00:00:00 2001 From: sam detweiler Date: Sat, 7 Dec 2024 02:51:11 -0600 Subject: [PATCH 2/2] Fixcaldates2 fix calendar module date processing, using node-ical@0.20.1 (#3587) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit here is an updated test version of the fixes for all kinds of calendar date problems. NOTE: the changed branch name NOTE: this used the node-cal@0.19.0 library UNCHANGED best to make a new folder and git clone there git clone https://github.com/sdetweil/MagicMirror cd MagicMirror git checkout fixcaldates2 // <------ note this is a changed branch name npm run install-mm copy your config.js and custom.css from the prior folder and the non-default modules you have installed… this ONLY changes the default calendar but DOES ship an updated node-ical library too if you need to fall back, just rename the folders around again so that your original is called MagicMirror all the testcases for node-ical and MagicMirror execute successfully. the ‘BIG’ change here is to get the local NON-TZ dates for the rrule.between() all the checking and conversion code is commented out or not used the node-ical fixes are for excluded dates (exdate) values being adjusted for DST/STD time… waiting to submit that PR one fix in calendar.js for checking if a past date was too far back, but it never checked to see IF the event date was in the past… (before today) so it chopped off too many and one change in calendarfetcher.js to put out a better diagnostic message of the parsed data… (exdate was excluded cause JSON stringify couldn’t convert the complex structure) I added the tests you all have documented please re-pull and checkout the new branch (I deleted the old branch) and npm run install-mm again --------- Co-authored-by: Veeck --- CHANGELOG.md | 11 +- modules/default/calendar/calendar.js | 55 ++- modules/default/calendar/calendarfetcher.js | 2 +- .../default/calendar/calendarfetcherutils.js | 343 ++++++++++++++---- modules/default/compliments/compliments.js | 17 +- package-lock.json | 29 +- package.json | 2 +- .../3_move_first_allday_repeating_event.js | 35 ++ .../calendar/berlin_end_of_day_repeating.js | 33 ++ .../configs/modules/calendar/berlin_multi.js | 33 ++ ...n_whole_day_event_moved_over_dst_change.js | 33 ++ .../calendar/chicago_late_in_timezone.js | 33 ++ .../modules/calendar/diff_tz_start_end.js | 34 ++ .../calendar/end_of_day_berlin_moved.js | 33 ++ ...multiple_days_non_repeating_display_end.js | 33 ++ ...tiple_days_non_repeating_no_display_end.js | 34 ++ .../exdate_and_recurrence_together.js | 33 ++ ...y_event_over_multiple_days_nonrepeating.js | 32 ++ .../germany_at_end_of_day_repeating.js | 33 ++ tests/e2e/modules/compliments_spec.js | 33 ++ tests/electron/modules/calendar_spec.js | 132 +++++++ tests/electron/modules/compliments_spec.js | 2 +- .../3_move_first_allday_repeating_event.ics | 35 ++ tests/mocks/RepeatingEvent.Oct21.ics | 28 ++ tests/mocks/chicago_late_in_timezone.ics | 15 + tests/mocks/compliments_file.json | 4 +- tests/mocks/diff_tz_start_end.ics | 14 + tests/mocks/end_of_day_berlin_moved.ics | 54 +++ ..._time_over_multiple_days_non_repeating.ics | 14 + .../mocks/exdate_and_recurrence_together.ics | 48 +++ ..._event_over_multiple_days_nonrepeating.ics | 15 + .../mocks/germany_at_end_of_day_repeating.ics | 15 + ...whole_day_moved_over_dst_change_berlin.ics | 28 ++ 33 files changed, 1182 insertions(+), 113 deletions(-) create mode 100644 tests/configs/modules/calendar/3_move_first_allday_repeating_event.js create mode 100644 tests/configs/modules/calendar/berlin_end_of_day_repeating.js create mode 100644 tests/configs/modules/calendar/berlin_multi.js create mode 100644 tests/configs/modules/calendar/berlin_whole_day_event_moved_over_dst_change.js create mode 100644 tests/configs/modules/calendar/chicago_late_in_timezone.js create mode 100644 tests/configs/modules/calendar/diff_tz_start_end.js create mode 100644 tests/configs/modules/calendar/end_of_day_berlin_moved.js create mode 100644 tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end.js create mode 100644 tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_no_display_end.js create mode 100644 tests/configs/modules/calendar/exdate_and_recurrence_together.js create mode 100644 tests/configs/modules/calendar/fullday_event_over_multiple_days_nonrepeating.js create mode 100644 tests/configs/modules/calendar/germany_at_end_of_day_repeating.js create mode 100644 tests/mocks/3_move_first_allday_repeating_event.ics create mode 100644 tests/mocks/RepeatingEvent.Oct21.ics create mode 100644 tests/mocks/chicago_late_in_timezone.ics create mode 100644 tests/mocks/diff_tz_start_end.ics create mode 100644 tests/mocks/end_of_day_berlin_moved.ics create mode 100644 tests/mocks/event_with_time_over_multiple_days_non_repeating.ics create mode 100644 tests/mocks/exdate_and_recurrence_together.ics create mode 100644 tests/mocks/fullday_event_over_multiple_days_nonrepeating.ics create mode 100644 tests/mocks/germany_at_end_of_day_repeating.ics create mode 100644 tests/mocks/whole_day_moved_over_dst_change_berlin.ics diff --git a/CHANGELOG.md b/CHANGELOG.md index 40e3802fa1..e3cef7e281 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ _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` - [core] Add export on animation names +- [calendar] - added ability to display end date for full date events, where end is not same day (showEnd=true) ### Removed @@ -38,12 +39,16 @@ _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) +- [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 5dbf244707..4c2a3adb81 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", @@ -3285,12 +3285,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" } @@ -9370,15 +9370,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": { @@ -12623,9 +12622,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 372b3196a4..8e6f4108a7 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,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/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