From 42f8239eeab45c820330b27700abd50d10302fad Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Thu, 12 Mar 2026 23:30:44 +0100 Subject: [PATCH 01/13] fix(calendar): show end date for multi-day full-day events in all timeFormats showEnd: true now works for full-day events spanning multiple days in 'relative' and 'dateheaders' mode, not just 'absolute'. Also, when nextDaysRelative replaces the start with 'Tomorrow' etc., the end date is appended (e.g. 'Tomorrow - 17th Mar'). showEndsOnlyWithDuration suppresses the end date as expected. Closes #4053 --- defaultmodules/calendar/calendar.js | 36 +++++++++++++++++-- .../fullday_multiday_showend_dateheaders.js | 31 ++++++++++++++++ ...llday_multiday_showend_nextdaysrelative.js | 32 +++++++++++++++++ .../fullday_multiday_showend_relative.js | 31 ++++++++++++++++ tests/electron/modules/calendar_spec.js | 17 +++++++++ 5 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 tests/configs/modules/calendar/fullday_multiday_showend_dateheaders.js create mode 100644 tests/configs/modules/calendar/fullday_multiday_showend_nextdaysrelative.js create mode 100644 tests/configs/modules/calendar/fullday_multiday_showend_relative.js diff --git a/defaultmodules/calendar/calendar.js b/defaultmodules/calendar/calendar.js index 0d8575c810..755b0dfa70 100644 --- a/defaultmodules/calendar/calendar.js +++ b/defaultmodules/calendar/calendar.js @@ -387,8 +387,24 @@ Module.register("calendar", { if (this.config.flipDateHeaderTitle) eventWrapper.appendChild(titleWrapper); if (event.fullDayEvent) { - titleWrapper.colSpan = "2"; - titleWrapper.classList.add("align-left"); + if (this.config.showEnd && !this.config.showEndsOnlyWithDuration) { + const endMomentAdjusted = eventEndDateMoment.clone().subtract(1, "second"); + if (!eventStartDateMoment.isSame(endMomentAdjusted, "d")) { + const timeWrapper = document.createElement("td"); + timeWrapper.className = `time light ${this.config.flipDateHeaderTitle ? "align-right " : "align-left "}${this.timeClassForUrl(event.url)}`; + timeWrapper.style.paddingLeft = "2px"; + timeWrapper.style.textAlign = this.config.flipDateHeaderTitle ? "right" : "left"; + timeWrapper.innerHTML = `-${CalendarUtils.capFirst(endMomentAdjusted.format(this.config.fullDayEventDateFormat))}`; + eventWrapper.appendChild(timeWrapper); + if (!this.config.flipDateHeaderTitle) titleWrapper.classList.add("align-right"); + } else { + titleWrapper.colSpan = "2"; + titleWrapper.classList.add("align-left"); + } + } else { + titleWrapper.colSpan = "2"; + titleWrapper.classList.add("align-left"); + } } else { const timeWrapper = document.createElement("td"); timeWrapper.className = `time light ${this.config.flipDateHeaderTitle ? "align-right " : "align-left "}${this.timeClassForUrl(event.url)}`; @@ -454,17 +470,26 @@ Module.register("calendar", { } if (event.fullDayEvent && this.config.nextDaysRelative) { // Full days events within the next two days + let relativeLabel = false; if (event.today) { timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY")); + relativeLabel = true; } else if (event.yesterday) { timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY")); + relativeLabel = true; } else if (event.tomorrow) { timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW")); + relativeLabel = true; } else if (event.dayAfterTomorrow) { if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") { timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW")); + relativeLabel = true; } } + // Append end date only if a relative label replaced the start date + if (relativeLabel && this.config.showEnd && !this.config.showEndsOnlyWithDuration && !eventStartDateMoment.isSame(eventEndDateMoment, "d")) { + timeWrapper.innerHTML += `-${CalendarUtils.capFirst(eventEndDateMoment.format(this.config.fullDayEventDateFormat))}`; + } } } else { // Show relative times @@ -501,6 +526,13 @@ Module.register("calendar", { timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW")); } } + // Show end date for multi-day full-day events if showEnd is configured + if (this.config.showEnd && !this.config.showEndsOnlyWithDuration) { + const endMomentAdjusted = eventEndDateMoment.clone().subtract(1, "second"); + if (!eventStartDateMoment.isSame(endMomentAdjusted, "d")) { + timeWrapper.innerHTML += `-${CalendarUtils.capFirst(endMomentAdjusted.format(this.config.fullDayEventDateFormat))}`; + } + } Log.info("[calendar] event fullday"); } else if (eventStartDateMoment.diff(now, "h") < this.config.getRelative) { Log.info("[calendar] not full day but within getRelative size"); diff --git a/tests/configs/modules/calendar/fullday_multiday_showend_dateheaders.js b/tests/configs/modules/calendar/fullday_multiday_showend_dateheaders.js new file mode 100644 index 0000000000..2033d55d60 --- /dev/null +++ b/tests/configs/modules/calendar/fullday_multiday_showend_dateheaders.js @@ -0,0 +1,31 @@ +let config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + fullDayEventDateFormat: "Do.MMM", + timeFormat: "dateheaders", + 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/fullday_multiday_showend_nextdaysrelative.js b/tests/configs/modules/calendar/fullday_multiday_showend_nextdaysrelative.js new file mode 100644 index 0000000000..32ca4ec2b1 --- /dev/null +++ b/tests/configs/modules/calendar/fullday_multiday_showend_nextdaysrelative.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, + fullDayEventDateFormat: "Do.MMM", + timeFormat: "absolute", + getRelative: 0, + showEnd: true, + nextDaysRelative: 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/fullday_multiday_showend_relative.js b/tests/configs/modules/calendar/fullday_multiday_showend_relative.js new file mode 100644 index 0000000000..c4dc08cc2d --- /dev/null +++ b/tests/configs/modules/calendar/fullday_multiday_showend_relative.js @@ -0,0 +1,31 @@ +let config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + fullDayEventDateFormat: "Do.MMM", + timeFormat: "relative", + 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/electron/modules/calendar_spec.js b/tests/electron/modules/calendar_spec.js index 2a9845ae18..0defb5a477 100644 --- a/tests/electron/modules/calendar_spec.js +++ b/tests/electron/modules/calendar_spec.js @@ -314,4 +314,21 @@ describe("Calendar module", () => { await expect(doTestTableContent(".testNotification", ".elementCount", "12", first)).resolves.toBe(true); }); }); + + describe("showEnd for multi-day full-day events", () => { + it("relative timeFormat shows start and end date", async () => { + await helpers.startApplication("tests/configs/modules/calendar/fullday_multiday_showend_relative.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); + await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct-30th.Oct", first)).resolves.toBe(true); + }); + + it("dateheaders timeFormat shows end date in time cell", async () => { + await helpers.startApplication("tests/configs/modules/calendar/fullday_multiday_showend_dateheaders.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); + await expect(doTestTableContent(".calendar .event", ".time", "-30th.Oct", first)).resolves.toBe(true); + }); + + it("absolute timeFormat with nextDaysRelative shows relative label and end date", async () => { + await helpers.startApplication("tests/configs/modules/calendar/fullday_multiday_showend_nextdaysrelative.js", "24 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); + await expect(doTestTableContent(".calendar .event", ".time", "Tomorrow-30th.Oct", first)).resolves.toBe(true); + }); + }); }); From 7c0cd384c690ccd930711efe757e9d5bb464c986 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sun, 15 Mar 2026 13:39:03 +0100 Subject: [PATCH 02/13] fix(calendar): apply showEnd to timed events in relative/dateheaders --- defaultmodules/calendar/calendar.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/defaultmodules/calendar/calendar.js b/defaultmodules/calendar/calendar.js index 755b0dfa70..cb70308d36 100644 --- a/defaultmodules/calendar/calendar.js +++ b/defaultmodules/calendar/calendar.js @@ -412,12 +412,12 @@ Module.register("calendar", { timeWrapper.style.textAlign = this.config.flipDateHeaderTitle ? "right" : "left"; timeWrapper.innerHTML = eventStartDateMoment.format("LT"); - // Add endDate to dataheaders if showEnd is enabled + // Add endDate to dateheaders if showEnd is enabled if (this.config.showEnd) { if (this.config.showEndsOnlyWithDuration && event.startDate === event.endDate) { // no duration here, don't display end } else { - timeWrapper.innerHTML += ` - ${CalendarUtils.capFirst(eventEndDateMoment.format("LT"))}`; + timeWrapper.innerHTML += `-${CalendarUtils.capFirst(eventEndDateMoment.format(this.config.dateEndFormat))}`; } } @@ -538,6 +538,9 @@ Module.register("calendar", { Log.info("[calendar] 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(eventStartDateMoment.fromNow())}`; + } else if (this.config.showEnd && (!this.config.showEndsOnlyWithDuration || event.startDate !== event.endDate)) { + // Show end time for timed events + timeWrapper.innerHTML += `-${CalendarUtils.capFirst(eventEndDateMoment.format(this.config.dateEndFormat))}`; } } else { // Ongoing event From 32033971f6be2ea7c3eb7465789b4fec35a0d58c Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:22:28 +0100 Subject: [PATCH 03/13] test(calendar): cover timed multi-day showEnd in relative/dateheaders Add electron test coverage for timed (non-full-day) multi-day events with showEnd enabled in `relative` and `dateheaders` modes. Use a yearly recurring mock ICS so tests stay stable over time and avoid "no upcoming events" failures as dates move on. --- ...s_non_repeating_display_end_dateheaders.js | 33 +++++++++++++++++++ ...days_non_repeating_display_end_relative.js | 33 +++++++++++++++++++ tests/electron/modules/calendar_spec.js | 12 +++++++ ...nt_with_time_over_multiple_days_yearly.ics | 18 ++++++++++ 4 files changed, 96 insertions(+) create mode 100644 tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_dateheaders.js create mode 100644 tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_relative.js create mode 100644 tests/mocks/event_with_time_over_multiple_days_yearly.ics diff --git a/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_dateheaders.js b/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_dateheaders.js new file mode 100644 index 0000000000..b7dd24218c --- /dev/null +++ b/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_dateheaders.js @@ -0,0 +1,33 @@ +const 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: "dateheaders", + getRelative: 0, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/event_with_time_over_multiple_days_yearly.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_relative.js b/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_relative.js new file mode 100644 index 0000000000..92afb5f184 --- /dev/null +++ b/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_relative.js @@ -0,0 +1,33 @@ +const 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: "relative", + getRelative: 0, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/event_with_time_over_multiple_days_yearly.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/electron/modules/calendar_spec.js b/tests/electron/modules/calendar_spec.js index 0defb5a477..6436940d77 100644 --- a/tests/electron/modules/calendar_spec.js +++ b/tests/electron/modules/calendar_spec.js @@ -297,6 +297,18 @@ describe("Calendar module", () => { }); }); + describe("showEnd for timed multi-day events", () => { + it("relative timeFormat shows start and end for timed multi-day events", async () => { + await helpers.startApplication("tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_relative.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); + await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 20:00-26th.Oct, 06:00", first)).resolves.toBe(true); + }); + + it("dateheaders timeFormat shows end for timed multi-day events", async () => { + await helpers.startApplication("tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_dateheaders.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); + await expect(doTestTableContent(".calendar .event", ".time", "-26th.Oct, 06:00", first)).resolves.toBe(true); + }); + }); + describe("count and check symbols", () => { it("in array", async () => { await helpers.startApplication("tests/configs/modules/calendar/symboltest.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); diff --git a/tests/mocks/event_with_time_over_multiple_days_yearly.ics b/tests/mocks/event_with_time_over_multiple_days_yearly.ics new file mode 100644 index 0000000000..00f65b2f1d --- /dev/null +++ b/tests/mocks/event_with_time_over_multiple_days_yearly.ics @@ -0,0 +1,18 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//MagicMirror Test//timed-multiday-yearly//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +DTSTART:20241026T010000Z +DTEND:20241026T110000Z +DTSTAMP:20241024T153358Z +UID:4maud6s79m41a99pj2g7j5km0a@google.com +CREATED:20241024T153313Z +LAST-MODIFIED:20241024T153330Z +SEQUENCE:0 +STATUS:CONFIRMED +RRULE:FREQ=YEARLY +SUMMARY:Sleep over at Bobs +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR From d1d43c2c3af11f79efbb55398f3e988ed1ca347d Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sun, 15 Mar 2026 20:59:46 +0100 Subject: [PATCH 04/13] fix(calendar): use time-only format for same-day timed event ends --- defaultmodules/calendar/calendar.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/defaultmodules/calendar/calendar.js b/defaultmodules/calendar/calendar.js index cb70308d36..89a29cf14c 100644 --- a/defaultmodules/calendar/calendar.js +++ b/defaultmodules/calendar/calendar.js @@ -417,7 +417,8 @@ Module.register("calendar", { if (this.config.showEndsOnlyWithDuration && event.startDate === event.endDate) { // no duration here, don't display end } else { - timeWrapper.innerHTML += `-${CalendarUtils.capFirst(eventEndDateMoment.format(this.config.dateEndFormat))}`; + const endFormat = eventStartDateMoment.isSame(eventEndDateMoment, "d") ? "LT" : this.config.dateEndFormat; + timeWrapper.innerHTML += `-${CalendarUtils.capFirst(eventEndDateMoment.format(endFormat))}`; } } @@ -439,8 +440,9 @@ Module.register("calendar", { if (this.config.showEnd) { // and has a duration if (event.startDate !== event.endDate) { + const endFormat = eventStartDateMoment.isSame(eventEndDateMoment, "d") ? "LT" : this.config.dateEndFormat; timeWrapper.innerHTML += "-"; - timeWrapper.innerHTML += CalendarUtils.capFirst(eventEndDateMoment.format(this.config.dateEndFormat)); + timeWrapper.innerHTML += CalendarUtils.capFirst(eventEndDateMoment.format(endFormat)); } } @@ -540,7 +542,8 @@ Module.register("calendar", { timeWrapper.innerHTML = `${CalendarUtils.capFirst(eventStartDateMoment.fromNow())}`; } else if (this.config.showEnd && (!this.config.showEndsOnlyWithDuration || event.startDate !== event.endDate)) { // Show end time for timed events - timeWrapper.innerHTML += `-${CalendarUtils.capFirst(eventEndDateMoment.format(this.config.dateEndFormat))}`; + const endFormat = eventStartDateMoment.isSame(eventEndDateMoment, "d") ? "LT" : this.config.dateEndFormat; + timeWrapper.innerHTML += `-${CalendarUtils.capFirst(eventEndDateMoment.format(endFormat))}`; } } else { // Ongoing event From c5cb8480c47e7c34f1fbb50dd90c20ccce698b25 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sun, 15 Mar 2026 22:27:08 +0100 Subject: [PATCH 05/13] fix(calendar): improve timed showEnd formatting across modes --- defaultmodules/calendar/calendar.js | 59 +++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/defaultmodules/calendar/calendar.js b/defaultmodules/calendar/calendar.js index 89a29cf14c..135ee96360 100644 --- a/defaultmodules/calendar/calendar.js +++ b/defaultmodules/calendar/calendar.js @@ -417,8 +417,8 @@ Module.register("calendar", { if (this.config.showEndsOnlyWithDuration && event.startDate === event.endDate) { // no duration here, don't display end } else { - const endFormat = eventStartDateMoment.isSame(eventEndDateMoment, "d") ? "LT" : this.config.dateEndFormat; - timeWrapper.innerHTML += `-${CalendarUtils.capFirst(eventEndDateMoment.format(endFormat))}`; + // In dateheaders mode, keep the end as time-only to avoid redundant date info under a date header. + timeWrapper.innerHTML += `-${CalendarUtils.capFirst(eventEndDateMoment.format("LT"))}`; } } @@ -436,14 +436,14 @@ Module.register("calendar", { if (this.config.timeFormat === "absolute") { // Use dateFormat timeWrapper.innerHTML = CalendarUtils.capFirst(eventStartDateMoment.format(this.config.dateFormat)); - // Add end time if showEnd - if (this.config.showEnd) { - // and has a duration - if (event.startDate !== event.endDate) { - const endFormat = eventStartDateMoment.isSame(eventEndDateMoment, "d") ? "LT" : this.config.dateEndFormat; - timeWrapper.innerHTML += "-"; - timeWrapper.innerHTML += CalendarUtils.capFirst(eventEndDateMoment.format(endFormat)); + // Add end time if showEnd and event has duration + if (this.config.showEnd && event.startDate !== event.endDate) { + const sameDay = this.isSameDay(eventStartDateMoment, eventEndDateMoment); + if (sameDay && !this.dateFormatIncludesTime()) { + // Include start time so result is e.g. "Mar 25th, 10:00-11:00" instead of "Mar 25th-11:00" + timeWrapper.innerHTML += `, ${eventStartDateMoment.format("LT")}`; } + timeWrapper.innerHTML += `-${this.formatTimedEventEnd(eventStartDateMoment, eventEndDateMoment)}`; } // For full day events we use the fullDayEventDateFormat @@ -542,8 +542,14 @@ Module.register("calendar", { timeWrapper.innerHTML = `${CalendarUtils.capFirst(eventStartDateMoment.fromNow())}`; } else if (this.config.showEnd && (!this.config.showEndsOnlyWithDuration || event.startDate !== event.endDate)) { // Show end time for timed events - const endFormat = eventStartDateMoment.isSame(eventEndDateMoment, "d") ? "LT" : this.config.dateEndFormat; - timeWrapper.innerHTML += `-${CalendarUtils.capFirst(eventEndDateMoment.format(endFormat))}`; + if (this.isSameDay(eventStartDateMoment, eventEndDateMoment)) { + // Re-format start to include time (sameElse may not have included it) + const sameElseFormat = this.dateFormatIncludesTime() ? this.config.dateFormat : `${this.config.dateFormat}, LT`; + timeWrapper.innerHTML = CalendarUtils.capFirst( + eventStartDateMoment.calendar(null, { sameElse: sameElseFormat }) + ); + } + timeWrapper.innerHTML += `-${this.formatTimedEventEnd(eventStartDateMoment, eventEndDateMoment)}`; } } else { // Ongoing event @@ -831,6 +837,37 @@ Module.register("calendar", { ); }, + /** + * Determines whether two moments are on the same day. + * @param {moment.Moment} startMoment The start moment. + * @param {moment.Moment} endMoment The end moment. + * @returns {boolean} True when both moments share the same calendar day. + */ + isSameDay (startMoment, endMoment) { + return startMoment.isSame(endMoment, "d"); + }, + + /** + * Checks whether the configured dateFormat already contains time components. + * @returns {boolean} True when dateFormat includes time tokens. + */ + dateFormatIncludesTime () { + const dateFormatWithoutLiterals = this.config.dateFormat.replace(/\[[^\]]*\]/g, ""); + return (/(LTS|LT|H{1,2}|h{1,2}|k{1,2}|m{1,2}|s{1,2}|a|A)/).test(dateFormatWithoutLiterals); + }, + + /** + * Formats a timed event end value. + * Uses time-only for same-day events and dateEndFormat for multi-day events. + * @param {moment.Moment} startMoment The event start moment. + * @param {moment.Moment} endMoment The event end moment. + * @returns {string} The formatted end value. + */ + formatTimedEventEnd (startMoment, endMoment) { + const endFormat = this.isSameDay(startMoment, endMoment) ? "LT" : this.config.dateEndFormat; + return CalendarUtils.capFirst(endMoment.format(endFormat)); + }, + /** * Retrieves the symbolClass for a specific calendar url. * @param {string} url The calendar url From 27e410ef7e47606fc32c098b942f837e0a9a443e Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sun, 15 Mar 2026 22:27:20 +0100 Subject: [PATCH 06/13] test(calendar): add timed showEnd regression coverage --- ...me_same_day_yearly_display_end_absolute.js | 33 +++++++++++++++++++ ...splay_end_absolute_dateformat_with_time.js | 33 +++++++++++++++++++ ...same_day_yearly_display_end_dateheaders.js | 33 +++++++++++++++++++ ...me_same_day_yearly_display_end_relative.js | 33 +++++++++++++++++++ tests/electron/modules/calendar_spec.js | 24 +++++++++++++- .../mocks/event_with_time_same_day_yearly.ics | 18 ++++++++++ 6 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute.js create mode 100644 tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_with_time.js create mode 100644 tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_dateheaders.js create mode 100644 tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative.js create mode 100644 tests/mocks/event_with_time_same_day_yearly.ics diff --git a/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute.js b/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute.js new file mode 100644 index 0000000000..5fefe78e73 --- /dev/null +++ b/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute.js @@ -0,0 +1,33 @@ +const config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "Do.MMM", + 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_same_day_yearly.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_with_time.js b/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_with_time.js new file mode 100644 index 0000000000..4af06d4158 --- /dev/null +++ b/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_with_time.js @@ -0,0 +1,33 @@ +const 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_same_day_yearly.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_dateheaders.js b/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_dateheaders.js new file mode 100644 index 0000000000..3d1b94d7f0 --- /dev/null +++ b/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_dateheaders.js @@ -0,0 +1,33 @@ +const config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "Do.MMM", + dateEndFormat: "Do.MMM, HH:mm", + fullDayEventDateFormat: "Do.MMM", + timeFormat: "dateheaders", + getRelative: 0, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/event_with_time_same_day_yearly.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative.js b/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative.js new file mode 100644 index 0000000000..e75e506a77 --- /dev/null +++ b/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative.js @@ -0,0 +1,33 @@ +const config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "Do.MMM", + dateEndFormat: "Do.MMM, HH:mm", + fullDayEventDateFormat: "Do.MMM", + timeFormat: "relative", + getRelative: 0, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/event_with_time_same_day_yearly.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/electron/modules/calendar_spec.js b/tests/electron/modules/calendar_spec.js index 6436940d77..97785472ef 100644 --- a/tests/electron/modules/calendar_spec.js +++ b/tests/electron/modules/calendar_spec.js @@ -305,7 +305,29 @@ describe("Calendar module", () => { it("dateheaders timeFormat shows end for timed multi-day events", async () => { await helpers.startApplication("tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_dateheaders.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); - await expect(doTestTableContent(".calendar .event", ".time", "-26th.Oct, 06:00", first)).resolves.toBe(true); + await expect(doTestTableContent(".calendar .event", ".time", "20:00-06:00", first)).resolves.toBe(true); + }); + }); + + describe("showEnd for timed same-day events", () => { + it("absolute timeFormat shows start and end time without repeating date", async () => { + await helpers.startApplication("tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); + await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 20:00-22:00", first)).resolves.toBe(true); + }); + + it("absolute timeFormat with time in dateFormat does not duplicate start time", async () => { + await helpers.startApplication("tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_with_time.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); + await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 20:00-22:00", first)).resolves.toBe(true); + }); + + it("relative timeFormat shows start and end time without repeating date", async () => { + await helpers.startApplication("tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); + await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 20:00-22:00", first)).resolves.toBe(true); + }); + + it("dateheaders timeFormat shows start and end time only", async () => { + await helpers.startApplication("tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_dateheaders.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); + await expect(doTestTableContent(".calendar .event", ".time", "20:00-22:00", first)).resolves.toBe(true); }); }); diff --git a/tests/mocks/event_with_time_same_day_yearly.ics b/tests/mocks/event_with_time_same_day_yearly.ics new file mode 100644 index 0000000000..4e0bb85185 --- /dev/null +++ b/tests/mocks/event_with_time_same_day_yearly.ics @@ -0,0 +1,18 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//MagicMirror Test//timed-same-day-yearly//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +DTSTART:20241025T200000 +DTEND:20241025T220000 +DTSTAMP:20241024T153358Z +UID:timed-same-day-yearly@magicmirror +CREATED:20241024T153313Z +LAST-MODIFIED:20241024T153330Z +SEQUENCE:0 +STATUS:CONFIRMED +RRULE:FREQ=YEARLY +SUMMARY:Same day timed event +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR From c30813a5d5c41884a6bb090235fca13afd51c6af Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sun, 15 Mar 2026 22:44:42 +0100 Subject: [PATCH 07/13] fix(calendar): prevent duplicate start time with localized dateFormat tokens Expand `dateFormatIncludesTime()` to resolve localized Moment tokens (`L`, `LL`, `LLL`, `LLLL` and lowercase variants) before checking for time parts. This prevents duplicated start times in same-day timed `showEnd` output (e.g. with `dateFormat: "LLL"`). Add an electron regression test and config fixture covering the `LLL` case. --- defaultmodules/calendar/calendar.js | 8 ++++- ...rly_display_end_absolute_dateformat_lll.js | 34 +++++++++++++++++++ tests/electron/modules/calendar_spec.js | 7 ++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_lll.js diff --git a/defaultmodules/calendar/calendar.js b/defaultmodules/calendar/calendar.js index 135ee96360..d4331a018c 100644 --- a/defaultmodules/calendar/calendar.js +++ b/defaultmodules/calendar/calendar.js @@ -853,7 +853,13 @@ Module.register("calendar", { */ dateFormatIncludesTime () { const dateFormatWithoutLiterals = this.config.dateFormat.replace(/\[[^\]]*\]/g, ""); - return (/(LTS|LT|H{1,2}|h{1,2}|k{1,2}|m{1,2}|s{1,2}|a|A)/).test(dateFormatWithoutLiterals); + const localeDateFormat = moment.localeData(); + const expandedDateFormat = dateFormatWithoutLiterals.replace( + /LTS|LT|LLLL|LLL|LL|L|llll|lll|ll|l/g, + (token) => localeDateFormat.longDateFormat(token) || token + ); + const expandedDateFormatWithoutLiterals = expandedDateFormat.replace(/\[[^\]]*\]/g, ""); + return (/(H{1,2}|h{1,2}|k{1,2}|m{1,2}|s{1,2}|a|A)/).test(expandedDateFormatWithoutLiterals); }, /** diff --git a/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_lll.js b/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_lll.js new file mode 100644 index 0000000000..93ffb8788c --- /dev/null +++ b/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_lll.js @@ -0,0 +1,34 @@ +const config = { + address: "0.0.0.0", + ipWhitelist: [], + + language: "en", + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "LLL", + 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_same_day_yearly.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/electron/modules/calendar_spec.js b/tests/electron/modules/calendar_spec.js index 97785472ef..6d74074974 100644 --- a/tests/electron/modules/calendar_spec.js +++ b/tests/electron/modules/calendar_spec.js @@ -320,6 +320,13 @@ describe("Calendar module", () => { await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 20:00-22:00", first)).resolves.toBe(true); }); + it("absolute timeFormat with dateFormat LLL does not duplicate start time", async () => { + await helpers.startApplication("tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_lll.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); + const timeText = (await global.page.locator(".calendar .event .time").locator(`nth=${first}`).textContent()) || ""; + expect((timeText.match(/20:00/g) || [])).toHaveLength(1); + await expect(doTestTableContent(".calendar .event", ".time", "-22:00", first)).resolves.toBe(true); + }); + it("relative timeFormat shows start and end time without repeating date", async () => { await helpers.startApplication("tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 20:00-22:00", first)).resolves.toBe(true); From c6fb2d9496fe70d12d89eedc1ea32580fda76812 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:18:38 +0100 Subject: [PATCH 08/13] fix(calendar): respect hideTime for relative showEnd output Prevent appending timed event end/start times in relative mode when hideTime=true. Make LLL same-day test format-agnostic and add hideTime+showEnd regression coverage. --- defaultmodules/calendar/calendar.js | 2 +- ...y_yearly_display_end_relative_hide_time.js | 34 +++++++++++++++++++ tests/electron/modules/calendar_spec.js | 12 +++++-- 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative_hide_time.js diff --git a/defaultmodules/calendar/calendar.js b/defaultmodules/calendar/calendar.js index d4331a018c..1bf5be5458 100644 --- a/defaultmodules/calendar/calendar.js +++ b/defaultmodules/calendar/calendar.js @@ -540,7 +540,7 @@ Module.register("calendar", { Log.info("[calendar] 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(eventStartDateMoment.fromNow())}`; - } else if (this.config.showEnd && (!this.config.showEndsOnlyWithDuration || event.startDate !== event.endDate)) { + } else if (!this.config.hideTime && this.config.showEnd && (!this.config.showEndsOnlyWithDuration || event.startDate !== event.endDate)) { // Show end time for timed events if (this.isSameDay(eventStartDateMoment, eventEndDateMoment)) { // Re-format start to include time (sameElse may not have included it) diff --git a/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative_hide_time.js b/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative_hide_time.js new file mode 100644 index 0000000000..57274eae16 --- /dev/null +++ b/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative_hide_time.js @@ -0,0 +1,34 @@ +const config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "Do.MMM", + dateEndFormat: "Do.MMM, HH:mm", + fullDayEventDateFormat: "Do.MMM", + timeFormat: "relative", + getRelative: 0, + hideTime: true, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/event_with_time_same_day_yearly.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/electron/modules/calendar_spec.js b/tests/electron/modules/calendar_spec.js index 6d74074974..cd7dbbf752 100644 --- a/tests/electron/modules/calendar_spec.js +++ b/tests/electron/modules/calendar_spec.js @@ -323,8 +323,9 @@ describe("Calendar module", () => { it("absolute timeFormat with dateFormat LLL does not duplicate start time", async () => { await helpers.startApplication("tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_lll.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); const timeText = (await global.page.locator(".calendar .event .time").locator(`nth=${first}`).textContent()) || ""; - expect((timeText.match(/20:00/g) || [])).toHaveLength(1); - await expect(doTestTableContent(".calendar .event", ".time", "-22:00", first)).resolves.toBe(true); + const timeTokens = timeText.match(/\d{1,2}:\d{2}(?:\s?[AP]M)?/gi) || []; + expect(timeTokens).toHaveLength(2); + expect(timeText).toContain("-"); }); it("relative timeFormat shows start and end time without repeating date", async () => { @@ -332,6 +333,13 @@ describe("Calendar module", () => { await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 20:00-22:00", first)).resolves.toBe(true); }); + it("relative timeFormat with hideTime does not show start or end times", async () => { + await helpers.startApplication("tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative_hide_time.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); + const timeText = (await global.page.locator(".calendar .event .time").locator(`nth=${first}`).textContent()) || ""; + expect(timeText).toContain("25th.Oct"); + expect(timeText.match(/\d{1,2}:\d{2}(?:\s?[AP]M)?/gi) || []).toHaveLength(0); + }); + it("dateheaders timeFormat shows start and end time only", async () => { await helpers.startApplication("tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_dateheaders.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); await expect(doTestTableContent(".calendar .event", ".time", "20:00-22:00", first)).resolves.toBe(true); From 8c265e1be7a54130656e64a081ecacfa8e9591c9 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:32:09 +0100 Subject: [PATCH 09/13] test(calendar): refactor showEnd specs to table-driven cases Replace repetitive showEnd permutations with shared helpers and case tables. Keep special assertions for LLL and hideTime while reducing duplication. --- tests/electron/modules/calendar_spec.js | 123 +++++++++++++++--------- 1 file changed, 80 insertions(+), 43 deletions(-) diff --git a/tests/electron/modules/calendar_spec.js b/tests/electron/modules/calendar_spec.js index cd7dbbf752..29a7e42cd8 100644 --- a/tests/electron/modules/calendar_spec.js +++ b/tests/electron/modules/calendar_spec.js @@ -50,6 +50,18 @@ describe("Calendar module", () => { return true; }; + const defaultCalendarNow = "08 Oct 2024 12:30:00 GMT-07:00"; + const defaultCalendarTimeZone = "America/Chicago"; + + const expectFirstEventTimeCell = async ({ configPath, expectedTime, now = defaultCalendarNow, timeZone = defaultCalendarTimeZone }) => { + await helpers.startApplication(configPath, now, [], timeZone); + await expect(doTestTableContent(".calendar .event", ".time", expectedTime, first)).resolves.toBe(true); + }; + + const getFirstEventTimeText = async () => { + return (await global.page.locator(".calendar .event .time").locator(`nth=${first}`).textContent()) || ""; + }; + afterEach(async () => { await helpers.stopApplication(); }); @@ -298,52 +310,68 @@ describe("Calendar module", () => { }); describe("showEnd for timed multi-day events", () => { - it("relative timeFormat shows start and end for timed multi-day events", async () => { - await helpers.startApplication("tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_relative.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); - await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 20:00-26th.Oct, 06:00", first)).resolves.toBe(true); - }); - - it("dateheaders timeFormat shows end for timed multi-day events", async () => { - await helpers.startApplication("tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_dateheaders.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); - await expect(doTestTableContent(".calendar .event", ".time", "20:00-06:00", first)).resolves.toBe(true); + const timedMultiDayCases = [ + { + name: "relative timeFormat shows start and end for timed multi-day events", + configPath: "tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_relative.js", + expectedTime: "25th.Oct, 20:00-26th.Oct, 06:00" + }, + { + name: "dateheaders timeFormat shows end for timed multi-day events", + configPath: "tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_dateheaders.js", + expectedTime: "20:00-06:00" + } + ]; + + it.each(timedMultiDayCases)("$name", async (testCase) => { + expect.hasAssertions(); + await expectFirstEventTimeCell(testCase); }); }); describe("showEnd for timed same-day events", () => { - it("absolute timeFormat shows start and end time without repeating date", async () => { - await helpers.startApplication("tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); - await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 20:00-22:00", first)).resolves.toBe(true); - }); - - it("absolute timeFormat with time in dateFormat does not duplicate start time", async () => { - await helpers.startApplication("tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_with_time.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); - await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 20:00-22:00", first)).resolves.toBe(true); + const timedSameDaySimpleCases = [ + { + name: "absolute timeFormat shows start and end time without repeating date", + configPath: "tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute.js", + expectedTime: "25th.Oct, 20:00-22:00" + }, + { + name: "absolute timeFormat with time in dateFormat does not duplicate start time", + configPath: "tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_with_time.js", + expectedTime: "25th.Oct, 20:00-22:00" + }, + { + name: "relative timeFormat shows start and end time without repeating date", + configPath: "tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative.js", + expectedTime: "25th.Oct, 20:00-22:00" + }, + { + name: "dateheaders timeFormat shows start and end time only", + configPath: "tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_dateheaders.js", + expectedTime: "20:00-22:00" + } + ]; + + it.each(timedSameDaySimpleCases)("$name", async (testCase) => { + expect.hasAssertions(); + await expectFirstEventTimeCell(testCase); }); it("absolute timeFormat with dateFormat LLL does not duplicate start time", async () => { - await helpers.startApplication("tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_lll.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); - const timeText = (await global.page.locator(".calendar .event .time").locator(`nth=${first}`).textContent()) || ""; + await helpers.startApplication("tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_lll.js", defaultCalendarNow, [], defaultCalendarTimeZone); + const timeText = await getFirstEventTimeText(); const timeTokens = timeText.match(/\d{1,2}:\d{2}(?:\s?[AP]M)?/gi) || []; expect(timeTokens).toHaveLength(2); expect(timeText).toContain("-"); }); - it("relative timeFormat shows start and end time without repeating date", async () => { - await helpers.startApplication("tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); - await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 20:00-22:00", first)).resolves.toBe(true); - }); - it("relative timeFormat with hideTime does not show start or end times", async () => { - await helpers.startApplication("tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative_hide_time.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); - const timeText = (await global.page.locator(".calendar .event .time").locator(`nth=${first}`).textContent()) || ""; + await helpers.startApplication("tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative_hide_time.js", defaultCalendarNow, [], defaultCalendarTimeZone); + const timeText = await getFirstEventTimeText(); expect(timeText).toContain("25th.Oct"); expect(timeText.match(/\d{1,2}:\d{2}(?:\s?[AP]M)?/gi) || []).toHaveLength(0); }); - - it("dateheaders timeFormat shows start and end time only", async () => { - await helpers.startApplication("tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_dateheaders.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); - await expect(doTestTableContent(".calendar .event", ".time", "20:00-22:00", first)).resolves.toBe(true); - }); }); describe("count and check symbols", () => { @@ -365,19 +393,28 @@ describe("Calendar module", () => { }); describe("showEnd for multi-day full-day events", () => { - it("relative timeFormat shows start and end date", async () => { - await helpers.startApplication("tests/configs/modules/calendar/fullday_multiday_showend_relative.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); - await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct-30th.Oct", first)).resolves.toBe(true); - }); - - it("dateheaders timeFormat shows end date in time cell", async () => { - await helpers.startApplication("tests/configs/modules/calendar/fullday_multiday_showend_dateheaders.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); - await expect(doTestTableContent(".calendar .event", ".time", "-30th.Oct", first)).resolves.toBe(true); - }); - - it("absolute timeFormat with nextDaysRelative shows relative label and end date", async () => { - await helpers.startApplication("tests/configs/modules/calendar/fullday_multiday_showend_nextdaysrelative.js", "24 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); - await expect(doTestTableContent(".calendar .event", ".time", "Tomorrow-30th.Oct", first)).resolves.toBe(true); + const fullDayShowEndCases = [ + { + name: "relative timeFormat shows start and end date", + configPath: "tests/configs/modules/calendar/fullday_multiday_showend_relative.js", + expectedTime: "25th.Oct-30th.Oct" + }, + { + name: "dateheaders timeFormat shows end date in time cell", + configPath: "tests/configs/modules/calendar/fullday_multiday_showend_dateheaders.js", + expectedTime: "-30th.Oct" + }, + { + name: "absolute timeFormat with nextDaysRelative shows relative label and end date", + configPath: "tests/configs/modules/calendar/fullday_multiday_showend_nextdaysrelative.js", + expectedTime: "Tomorrow-30th.Oct", + now: "24 Oct 2024 12:30:00 GMT-07:00" + } + ]; + + it.each(fullDayShowEndCases)("$name", async (testCase) => { + expect.hasAssertions(); + await expectFirstEventTimeCell(testCase); }); }); }); From a1e2d3418daab7f6c43dd33b31a5dd5bb7666e8d Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:33:20 +0100 Subject: [PATCH 10/13] refactor(calendar): extract time rendering helpers Move dateheaders, absolute, and relative time text generation into dedicated helper methods. Reduce branching in getDom while preserving behavior and existing output semantics. --- defaultmodules/calendar/calendar.js | 355 +++++++++++++++------------- 1 file changed, 187 insertions(+), 168 deletions(-) diff --git a/defaultmodules/calendar/calendar.js b/defaultmodules/calendar/calendar.js index 1bf5be5458..e434c4c765 100644 --- a/defaultmodules/calendar/calendar.js +++ b/defaultmodules/calendar/calendar.js @@ -384,49 +384,7 @@ Module.register("calendar", { } if (this.config.timeFormat === "dateheaders") { - if (this.config.flipDateHeaderTitle) eventWrapper.appendChild(titleWrapper); - - if (event.fullDayEvent) { - if (this.config.showEnd && !this.config.showEndsOnlyWithDuration) { - const endMomentAdjusted = eventEndDateMoment.clone().subtract(1, "second"); - if (!eventStartDateMoment.isSame(endMomentAdjusted, "d")) { - const timeWrapper = document.createElement("td"); - timeWrapper.className = `time light ${this.config.flipDateHeaderTitle ? "align-right " : "align-left "}${this.timeClassForUrl(event.url)}`; - timeWrapper.style.paddingLeft = "2px"; - timeWrapper.style.textAlign = this.config.flipDateHeaderTitle ? "right" : "left"; - timeWrapper.innerHTML = `-${CalendarUtils.capFirst(endMomentAdjusted.format(this.config.fullDayEventDateFormat))}`; - eventWrapper.appendChild(timeWrapper); - if (!this.config.flipDateHeaderTitle) titleWrapper.classList.add("align-right"); - } else { - titleWrapper.colSpan = "2"; - titleWrapper.classList.add("align-left"); - } - } else { - titleWrapper.colSpan = "2"; - titleWrapper.classList.add("align-left"); - } - } else { - const timeWrapper = document.createElement("td"); - timeWrapper.className = `time light ${this.config.flipDateHeaderTitle ? "align-right " : "align-left "}${this.timeClassForUrl(event.url)}`; - timeWrapper.style.paddingLeft = "2px"; - timeWrapper.style.textAlign = this.config.flipDateHeaderTitle ? "right" : "left"; - timeWrapper.innerHTML = eventStartDateMoment.format("LT"); - - // Add endDate to dateheaders if showEnd is enabled - if (this.config.showEnd) { - if (this.config.showEndsOnlyWithDuration && event.startDate === event.endDate) { - // no duration here, don't display end - } else { - // In dateheaders mode, keep the end as time-only to avoid redundant date info under a date header. - timeWrapper.innerHTML += `-${CalendarUtils.capFirst(eventEndDateMoment.format("LT"))}`; - } - } - - eventWrapper.appendChild(timeWrapper); - - if (!this.config.flipDateHeaderTitle) titleWrapper.classList.add("align-right"); - } - if (!this.config.flipDateHeaderTitle) eventWrapper.appendChild(titleWrapper); + this.renderDateHeadersEventTime(eventWrapper, titleWrapper, event, eventStartDateMoment, eventEndDateMoment); } else { const timeWrapper = document.createElement("td"); @@ -434,133 +392,11 @@ Module.register("calendar", { const now = moment(); if (this.config.timeFormat === "absolute") { - // Use dateFormat - timeWrapper.innerHTML = CalendarUtils.capFirst(eventStartDateMoment.format(this.config.dateFormat)); - // Add end time if showEnd and event has duration - if (this.config.showEnd && event.startDate !== event.endDate) { - const sameDay = this.isSameDay(eventStartDateMoment, eventEndDateMoment); - if (sameDay && !this.dateFormatIncludesTime()) { - // Include start time so result is e.g. "Mar 25th, 10:00-11:00" instead of "Mar 25th-11:00" - timeWrapper.innerHTML += `, ${eventStartDateMoment.format("LT")}`; - } - timeWrapper.innerHTML += `-${this.formatTimedEventEnd(eventStartDateMoment, eventEndDateMoment)}`; - } - - // 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 - eventEndDateMoment.subtract(1, "second"); - timeWrapper.innerHTML = CalendarUtils.capFirst(eventStartDateMoment.format(this.config.fullDayEventDateFormat)); - // only show end if requested and allowed and the dates are different - if (this.config.showEnd && !this.config.showEndsOnlyWithDuration && !eventStartDateMoment.isSame(eventEndDateMoment, "d")) { - timeWrapper.innerHTML += "-"; - timeWrapper.innerHTML += CalendarUtils.capFirst(eventEndDateMoment.format(this.config.fullDayEventDateFormat)); - } else if (!eventStartDateMoment.isSame(eventEndDateMoment, "d") && eventStartDateMoment.isBefore(now)) { - timeWrapper.innerHTML = CalendarUtils.capFirst(now.format(this.config.fullDayEventDateFormat)); - } - } else if (this.config.getRelative > 0 && eventStartDateMoment.isBefore(now)) { - // Ongoing and getRelative is set - timeWrapper.innerHTML = CalendarUtils.capFirst( - this.translate("RUNNING", { - fallback: `${this.translate("RUNNING")} {timeUntilEnd}`, - timeUntilEnd: eventEndDateMoment.fromNow(true) - }) - ); - } else if (this.config.urgency > 0 && eventStartDateMoment.diff(now, "d") < this.config.urgency) { - // Within urgency days - timeWrapper.innerHTML = CalendarUtils.capFirst(eventStartDateMoment.fromNow()); - } - if (event.fullDayEvent && this.config.nextDaysRelative) { - // Full days events within the next two days - let relativeLabel = false; - if (event.today) { - timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY")); - relativeLabel = true; - } else if (event.yesterday) { - timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY")); - relativeLabel = true; - } else if (event.tomorrow) { - timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW")); - relativeLabel = true; - } else if (event.dayAfterTomorrow) { - if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") { - timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW")); - relativeLabel = true; - } - } - // Append end date only if a relative label replaced the start date - if (relativeLabel && this.config.showEnd && !this.config.showEndsOnlyWithDuration && !eventStartDateMoment.isSame(eventEndDateMoment, "d")) { - timeWrapper.innerHTML += `-${CalendarUtils.capFirst(eventEndDateMoment.format(this.config.fullDayEventDateFormat))}`; - } - } + timeWrapper.innerHTML = this.buildAbsoluteTimeText(event, eventStartDateMoment, eventEndDateMoment, now); } else { - // Show relative times - if (eventStartDateMoment.isSameOrAfter(now) || (event.fullDayEvent && eventEndDateMoment.diff(now, "days") === 0)) { - // Use relative time - if (!this.config.hideTime && !event.fullDayEvent) { - Log.debug("[calendar] event not hidden and not fullday"); - timeWrapper.innerHTML = `${CalendarUtils.capFirst(eventStartDateMoment.calendar(null, { sameElse: this.config.dateFormat }))}`; - } else { - Log.debug("[calendar] event full day or hidden"); - timeWrapper.innerHTML = `${CalendarUtils.capFirst( - eventStartDateMoment.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 - if (event.today || (event.fullDayEvent && eventEndDateMoment.diff(now, "days") === 0)) { - timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY")); - } else if (event.dayBeforeYesterday) { - if (this.translate("DAYBEFOREYESTERDAY") !== "DAYBEFOREYESTERDAY") { - timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYBEFOREYESTERDAY")); - } - } else if (event.yesterday) { - timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY")); - } else if (event.tomorrow) { - timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW")); - } else if (event.dayAfterTomorrow) { - if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") { - timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW")); - } - } - // Show end date for multi-day full-day events if showEnd is configured - if (this.config.showEnd && !this.config.showEndsOnlyWithDuration) { - const endMomentAdjusted = eventEndDateMoment.clone().subtract(1, "second"); - if (!eventStartDateMoment.isSame(endMomentAdjusted, "d")) { - timeWrapper.innerHTML += `-${CalendarUtils.capFirst(endMomentAdjusted.format(this.config.fullDayEventDateFormat))}`; - } - } - Log.info("[calendar] event fullday"); - } else if (eventStartDateMoment.diff(now, "h") < this.config.getRelative) { - Log.info("[calendar] 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(eventStartDateMoment.fromNow())}`; - } else if (!this.config.hideTime && this.config.showEnd && (!this.config.showEndsOnlyWithDuration || event.startDate !== event.endDate)) { - // Show end time for timed events - if (this.isSameDay(eventStartDateMoment, eventEndDateMoment)) { - // Re-format start to include time (sameElse may not have included it) - const sameElseFormat = this.dateFormatIncludesTime() ? this.config.dateFormat : `${this.config.dateFormat}, LT`; - timeWrapper.innerHTML = CalendarUtils.capFirst( - eventStartDateMoment.calendar(null, { sameElse: sameElseFormat }) - ); - } - timeWrapper.innerHTML += `-${this.formatTimedEventEnd(eventStartDateMoment, eventEndDateMoment)}`; - } - } else { - // Ongoing event - timeWrapper.innerHTML = CalendarUtils.capFirst( - this.translate("RUNNING", { - fallback: `${this.translate("RUNNING")} {timeUntilEnd}`, - timeUntilEnd: eventEndDateMoment.fromNow(true) - }) - ); - } + timeWrapper.innerHTML = this.buildRelativeTimeText(event, eventStartDateMoment, eventEndDateMoment, now); } + timeWrapper.className = `time light ${this.timeClassForUrl(event.url)}`; eventWrapper.appendChild(timeWrapper); } @@ -837,6 +673,189 @@ Module.register("calendar", { ); }, + createDateHeadersTimeWrapper (url) { + const timeWrapper = document.createElement("td"); + timeWrapper.className = `time light ${this.config.flipDateHeaderTitle ? "align-right " : "align-left "}${this.timeClassForUrl(url)}`; + timeWrapper.style.paddingLeft = "2px"; + timeWrapper.style.textAlign = this.config.flipDateHeaderTitle ? "right" : "left"; + return timeWrapper; + }, + + hasEventDuration (event) { + return event.startDate !== event.endDate; + }, + + shouldShowDateHeadersTimedEnd (event) { + return this.config.showEnd && (!this.config.showEndsOnlyWithDuration || this.hasEventDuration(event)); + }, + + shouldShowRelativeTimedEnd (event) { + return !this.config.hideTime && this.config.showEnd && (!this.config.showEndsOnlyWithDuration || this.hasEventDuration(event)); + }, + + getAdjustedFullDayEndMoment (endMoment) { + return endMoment.clone().subtract(1, "second"); + }, + + renderDateHeadersEventTime (eventWrapper, titleWrapper, event, eventStartDateMoment, eventEndDateMoment) { + if (this.config.flipDateHeaderTitle) eventWrapper.appendChild(titleWrapper); + + if (event.fullDayEvent) { + const adjustedEndMoment = this.getAdjustedFullDayEndMoment(eventEndDateMoment); + if (this.config.showEnd && !this.config.showEndsOnlyWithDuration && !eventStartDateMoment.isSame(adjustedEndMoment, "d")) { + const timeWrapper = this.createDateHeadersTimeWrapper(event.url); + timeWrapper.innerHTML = `-${CalendarUtils.capFirst(adjustedEndMoment.format(this.config.fullDayEventDateFormat))}`; + eventWrapper.appendChild(timeWrapper); + if (!this.config.flipDateHeaderTitle) titleWrapper.classList.add("align-right"); + } else { + titleWrapper.colSpan = "2"; + titleWrapper.classList.add("align-left"); + } + } else { + const timeWrapper = this.createDateHeadersTimeWrapper(event.url); + timeWrapper.innerHTML = eventStartDateMoment.format("LT"); + + // In dateheaders mode, keep the end as time-only to avoid redundant date info under a date header. + if (this.shouldShowDateHeadersTimedEnd(event)) { + timeWrapper.innerHTML += `-${CalendarUtils.capFirst(eventEndDateMoment.format("LT"))}`; + } + + eventWrapper.appendChild(timeWrapper); + if (!this.config.flipDateHeaderTitle) titleWrapper.classList.add("align-right"); + } + + if (!this.config.flipDateHeaderTitle) eventWrapper.appendChild(titleWrapper); + }, + + buildAbsoluteTimeText (event, eventStartDateMoment, eventEndDateMoment, now) { + let timeText = CalendarUtils.capFirst(eventStartDateMoment.format(this.config.dateFormat)); + + if (this.config.showEnd && this.hasEventDuration(event)) { + const sameDay = this.isSameDay(eventStartDateMoment, eventEndDateMoment); + if (sameDay && !this.dateFormatIncludesTime()) { + timeText += `, ${eventStartDateMoment.format("LT")}`; + } + timeText += `-${this.formatTimedEventEnd(eventStartDateMoment, eventEndDateMoment)}`; + } + + if (event.fullDayEvent) { + const adjustedEndMoment = this.getAdjustedFullDayEndMoment(eventEndDateMoment); + timeText = CalendarUtils.capFirst(eventStartDateMoment.format(this.config.fullDayEventDateFormat)); + + if (this.config.showEnd && !this.config.showEndsOnlyWithDuration && !eventStartDateMoment.isSame(adjustedEndMoment, "d")) { + timeText += `-${CalendarUtils.capFirst(adjustedEndMoment.format(this.config.fullDayEventDateFormat))}`; + } else if (!eventStartDateMoment.isSame(adjustedEndMoment, "d") && eventStartDateMoment.isBefore(now)) { + timeText = CalendarUtils.capFirst(now.format(this.config.fullDayEventDateFormat)); + } + + if (this.config.nextDaysRelative) { + let relativeLabel = false; + if (event.today) { + timeText = CalendarUtils.capFirst(this.translate("TODAY")); + relativeLabel = true; + } else if (event.yesterday) { + timeText = CalendarUtils.capFirst(this.translate("YESTERDAY")); + relativeLabel = true; + } else if (event.tomorrow) { + timeText = CalendarUtils.capFirst(this.translate("TOMORROW")); + relativeLabel = true; + } else if (event.dayAfterTomorrow && this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") { + timeText = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW")); + relativeLabel = true; + } + + if (relativeLabel && this.config.showEnd && !this.config.showEndsOnlyWithDuration && !eventStartDateMoment.isSame(adjustedEndMoment, "d")) { + timeText += `-${CalendarUtils.capFirst(adjustedEndMoment.format(this.config.fullDayEventDateFormat))}`; + } + } + + return timeText; + } + + if (this.config.getRelative > 0 && eventStartDateMoment.isBefore(now)) { + return CalendarUtils.capFirst( + this.translate("RUNNING", { + fallback: `${this.translate("RUNNING")} {timeUntilEnd}`, + timeUntilEnd: eventEndDateMoment.fromNow(true) + }) + ); + } + + if (this.config.urgency > 0 && eventStartDateMoment.diff(now, "d") < this.config.urgency) { + return CalendarUtils.capFirst(eventStartDateMoment.fromNow()); + } + + return timeText; + }, + + buildRelativeTimeText (event, eventStartDateMoment, eventEndDateMoment, now) { + if (eventStartDateMoment.isSameOrAfter(now) || (event.fullDayEvent && eventEndDateMoment.diff(now, "days") === 0)) { + let timeText; + + if (!this.config.hideTime && !event.fullDayEvent) { + Log.debug("[calendar] event not hidden and not fullday"); + timeText = `${CalendarUtils.capFirst(eventStartDateMoment.calendar(null, { sameElse: this.config.dateFormat }))}`; + } else { + Log.debug("[calendar] event full day or hidden"); + timeText = `${CalendarUtils.capFirst( + eventStartDateMoment.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) { + if (event.today || (event.fullDayEvent && eventEndDateMoment.diff(now, "days") === 0)) { + timeText = CalendarUtils.capFirst(this.translate("TODAY")); + } else if (event.dayBeforeYesterday) { + if (this.translate("DAYBEFOREYESTERDAY") !== "DAYBEFOREYESTERDAY") { + timeText = CalendarUtils.capFirst(this.translate("DAYBEFOREYESTERDAY")); + } + } else if (event.yesterday) { + timeText = CalendarUtils.capFirst(this.translate("YESTERDAY")); + } else if (event.tomorrow) { + timeText = CalendarUtils.capFirst(this.translate("TOMORROW")); + } else if (event.dayAfterTomorrow) { + if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") { + timeText = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW")); + } + } + + if (this.config.showEnd && !this.config.showEndsOnlyWithDuration) { + const adjustedEndMoment = this.getAdjustedFullDayEndMoment(eventEndDateMoment); + if (!eventStartDateMoment.isSame(adjustedEndMoment, "d")) { + timeText += `-${CalendarUtils.capFirst(adjustedEndMoment.format(this.config.fullDayEventDateFormat))}`; + } + } + + Log.info("[calendar] event fullday"); + } else if (eventStartDateMoment.diff(now, "h") < this.config.getRelative) { + Log.info("[calendar] not full day but within getRelative size"); + timeText = `${CalendarUtils.capFirst(eventStartDateMoment.fromNow())}`; + } else if (this.shouldShowRelativeTimedEnd(event)) { + if (this.isSameDay(eventStartDateMoment, eventEndDateMoment)) { + const sameElseFormat = this.dateFormatIncludesTime() ? this.config.dateFormat : `${this.config.dateFormat}, LT`; + timeText = CalendarUtils.capFirst( + eventStartDateMoment.calendar(null, { sameElse: sameElseFormat }) + ); + } + timeText += `-${this.formatTimedEventEnd(eventStartDateMoment, eventEndDateMoment)}`; + } + + return timeText; + } + + return CalendarUtils.capFirst( + this.translate("RUNNING", { + fallback: `${this.translate("RUNNING")} {timeUntilEnd}`, + timeUntilEnd: eventEndDateMoment.fromNow(true) + }) + ); + }, + /** * Determines whether two moments are on the same day. * @param {moment.Moment} startMoment The start moment. From 4e1683fe590fb68e2b29bccb213c11413ad04813 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:22:31 +0100 Subject: [PATCH 11/13] test(calendar): centralize showEnd fixtures without wrapper files Move showEnd fixture variants into one config file selected by MM_CALENDAR_SHOWEND_SCENARIO. Update calendar_spec to choose scenarios by name and remove per-scenario wrapper config files. --- .../calendar/calendarShowEndConfigs.js | 361 ++++++++++++++++++ ...multiple_days_non_repeating_display_end.js | 33 -- ...s_non_repeating_display_end_dateheaders.js | 33 -- ...days_non_repeating_display_end_relative.js | 33 -- ...tiple_days_non_repeating_no_display_end.js | 34 -- ...me_same_day_yearly_display_end_absolute.js | 33 -- ...rly_display_end_absolute_dateformat_lll.js | 34 -- ...splay_end_absolute_dateformat_with_time.js | 33 -- ...same_day_yearly_display_end_dateheaders.js | 33 -- ...me_same_day_yearly_display_end_relative.js | 33 -- ...y_yearly_display_end_relative_hide_time.js | 34 -- .../fullday_multiday_showend_dateheaders.js | 31 -- ...llday_multiday_showend_nextdaysrelative.js | 32 -- .../fullday_multiday_showend_relative.js | 31 -- tests/electron/modules/calendar_spec.js | 37 +- 15 files changed, 383 insertions(+), 442 deletions(-) create mode 100644 tests/configs/modules/calendar/calendarShowEndConfigs.js delete mode 100644 tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end.js delete mode 100644 tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_dateheaders.js delete mode 100644 tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_relative.js delete mode 100644 tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_no_display_end.js delete mode 100644 tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute.js delete mode 100644 tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_lll.js delete mode 100644 tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_with_time.js delete mode 100644 tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_dateheaders.js delete mode 100644 tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative.js delete mode 100644 tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative_hide_time.js delete mode 100644 tests/configs/modules/calendar/fullday_multiday_showend_dateheaders.js delete mode 100644 tests/configs/modules/calendar/fullday_multiday_showend_nextdaysrelative.js delete mode 100644 tests/configs/modules/calendar/fullday_multiday_showend_relative.js diff --git a/tests/configs/modules/calendar/calendarShowEndConfigs.js b/tests/configs/modules/calendar/calendarShowEndConfigs.js new file mode 100644 index 0000000000..129ad97388 --- /dev/null +++ b/tests/configs/modules/calendar/calendarShowEndConfigs.js @@ -0,0 +1,361 @@ +const calendarShowEndConfigs = { + event_with_time_over_multiple_days_non_repeating_display_end: { + 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" + } + ] + } + } + ] + }, + event_with_time_over_multiple_days_non_repeating_display_end_dateheaders: { + 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: "dateheaders", + getRelative: 0, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/event_with_time_over_multiple_days_yearly.ics" + } + ] + } + } + ] + }, + event_with_time_over_multiple_days_non_repeating_display_end_relative: { + 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: "relative", + getRelative: 0, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/event_with_time_over_multiple_days_yearly.ics" + } + ] + } + } + ] + }, + event_with_time_over_multiple_days_non_repeating_no_display_end: { + 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" + } + ] + } + } + ] + }, + event_with_time_same_day_yearly_display_end_absolute: { + address: "0.0.0.0", + ipWhitelist: [], + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "Do.MMM", + 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_same_day_yearly.ics" + } + ] + } + } + ] + }, + event_with_time_same_day_yearly_display_end_absolute_dateformat_lll: { + address: "0.0.0.0", + ipWhitelist: [], + language: "en", + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "LLL", + 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_same_day_yearly.ics" + } + ] + } + } + ] + }, + event_with_time_same_day_yearly_display_end_absolute_dateformat_with_time: { + 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_same_day_yearly.ics" + } + ] + } + } + ] + }, + event_with_time_same_day_yearly_display_end_dateheaders: { + address: "0.0.0.0", + ipWhitelist: [], + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "Do.MMM", + dateEndFormat: "Do.MMM, HH:mm", + fullDayEventDateFormat: "Do.MMM", + timeFormat: "dateheaders", + getRelative: 0, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/event_with_time_same_day_yearly.ics" + } + ] + } + } + ] + }, + event_with_time_same_day_yearly_display_end_relative: { + address: "0.0.0.0", + ipWhitelist: [], + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "Do.MMM", + dateEndFormat: "Do.MMM, HH:mm", + fullDayEventDateFormat: "Do.MMM", + timeFormat: "relative", + getRelative: 0, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/event_with_time_same_day_yearly.ics" + } + ] + } + } + ] + }, + event_with_time_same_day_yearly_display_end_relative_hide_time: { + address: "0.0.0.0", + ipWhitelist: [], + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "Do.MMM", + dateEndFormat: "Do.MMM, HH:mm", + fullDayEventDateFormat: "Do.MMM", + timeFormat: "relative", + getRelative: 0, + hideTime: true, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/event_with_time_same_day_yearly.ics" + } + ] + } + } + ] + }, + fullday_multiday_showend_dateheaders: { + address: "0.0.0.0", + ipWhitelist: [], + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + fullDayEventDateFormat: "Do.MMM", + timeFormat: "dateheaders", + getRelative: 0, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/fullday_event_over_multiple_days_nonrepeating.ics" + } + ] + } + } + ] + }, + fullday_multiday_showend_nextdaysrelative: { + address: "0.0.0.0", + ipWhitelist: [], + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + fullDayEventDateFormat: "Do.MMM", + timeFormat: "absolute", + getRelative: 0, + showEnd: true, + nextDaysRelative: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/fullday_event_over_multiple_days_nonrepeating.ics" + } + ] + } + } + ] + }, + fullday_multiday_showend_relative: { + address: "0.0.0.0", + ipWhitelist: [], + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + fullDayEventDateFormat: "Do.MMM", + timeFormat: "relative", + getRelative: 0, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/fullday_event_over_multiple_days_nonrepeating.ics" + } + ] + } + } + ] + } +}; + +const defaultScenario = "event_with_time_over_multiple_days_non_repeating_display_end"; +const selectedScenario = process.env.MM_CALENDAR_SHOWEND_SCENARIO || defaultScenario; +const config = calendarShowEndConfigs[selectedScenario]; + +if (!config) { + throw new Error(`Unknown MM_CALENDAR_SHOWEND_SCENARIO: ${selectedScenario}`); +} + +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 deleted file mode 100644 index 95989648ca..0000000000 --- a/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end.js +++ /dev/null @@ -1,33 +0,0 @@ -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_display_end_dateheaders.js b/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_dateheaders.js deleted file mode 100644 index b7dd24218c..0000000000 --- a/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_dateheaders.js +++ /dev/null @@ -1,33 +0,0 @@ -const 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: "dateheaders", - getRelative: 0, - showEnd: true, - calendars: [ - { - maximumEntries: 100, - url: "http://localhost:8080/tests/mocks/event_with_time_over_multiple_days_yearly.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_relative.js b/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_relative.js deleted file mode 100644 index 92afb5f184..0000000000 --- a/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_relative.js +++ /dev/null @@ -1,33 +0,0 @@ -const 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: "relative", - getRelative: 0, - showEnd: true, - calendars: [ - { - maximumEntries: 100, - url: "http://localhost:8080/tests/mocks/event_with_time_over_multiple_days_yearly.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 deleted file mode 100644 index ef60df4c94..0000000000 --- a/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_no_display_end.js +++ /dev/null @@ -1,34 +0,0 @@ -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/event_with_time_same_day_yearly_display_end_absolute.js b/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute.js deleted file mode 100644 index 5fefe78e73..0000000000 --- a/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute.js +++ /dev/null @@ -1,33 +0,0 @@ -const config = { - address: "0.0.0.0", - ipWhitelist: [], - - timeFormat: 24, - modules: [ - { - module: "calendar", - position: "bottom_bar", - config: { - fade: false, - urgency: 0, - dateFormat: "Do.MMM", - 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_same_day_yearly.ics" - } - ] - } - } - ] -}; - -/*************** DO NOT EDIT THE LINE BELOW ***************/ -if (typeof module !== "undefined") { - module.exports = config; -} diff --git a/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_lll.js b/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_lll.js deleted file mode 100644 index 93ffb8788c..0000000000 --- a/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_lll.js +++ /dev/null @@ -1,34 +0,0 @@ -const config = { - address: "0.0.0.0", - ipWhitelist: [], - - language: "en", - timeFormat: 24, - modules: [ - { - module: "calendar", - position: "bottom_bar", - config: { - fade: false, - urgency: 0, - dateFormat: "LLL", - 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_same_day_yearly.ics" - } - ] - } - } - ] -}; - -/*************** DO NOT EDIT THE LINE BELOW ***************/ -if (typeof module !== "undefined") { - module.exports = config; -} diff --git a/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_with_time.js b/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_with_time.js deleted file mode 100644 index 4af06d4158..0000000000 --- a/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_with_time.js +++ /dev/null @@ -1,33 +0,0 @@ -const 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_same_day_yearly.ics" - } - ] - } - } - ] -}; - -/*************** DO NOT EDIT THE LINE BELOW ***************/ -if (typeof module !== "undefined") { - module.exports = config; -} diff --git a/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_dateheaders.js b/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_dateheaders.js deleted file mode 100644 index 3d1b94d7f0..0000000000 --- a/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_dateheaders.js +++ /dev/null @@ -1,33 +0,0 @@ -const config = { - address: "0.0.0.0", - ipWhitelist: [], - - timeFormat: 24, - modules: [ - { - module: "calendar", - position: "bottom_bar", - config: { - fade: false, - urgency: 0, - dateFormat: "Do.MMM", - dateEndFormat: "Do.MMM, HH:mm", - fullDayEventDateFormat: "Do.MMM", - timeFormat: "dateheaders", - getRelative: 0, - showEnd: true, - calendars: [ - { - maximumEntries: 100, - url: "http://localhost:8080/tests/mocks/event_with_time_same_day_yearly.ics" - } - ] - } - } - ] -}; - -/*************** DO NOT EDIT THE LINE BELOW ***************/ -if (typeof module !== "undefined") { - module.exports = config; -} diff --git a/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative.js b/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative.js deleted file mode 100644 index e75e506a77..0000000000 --- a/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative.js +++ /dev/null @@ -1,33 +0,0 @@ -const config = { - address: "0.0.0.0", - ipWhitelist: [], - - timeFormat: 24, - modules: [ - { - module: "calendar", - position: "bottom_bar", - config: { - fade: false, - urgency: 0, - dateFormat: "Do.MMM", - dateEndFormat: "Do.MMM, HH:mm", - fullDayEventDateFormat: "Do.MMM", - timeFormat: "relative", - getRelative: 0, - showEnd: true, - calendars: [ - { - maximumEntries: 100, - url: "http://localhost:8080/tests/mocks/event_with_time_same_day_yearly.ics" - } - ] - } - } - ] -}; - -/*************** DO NOT EDIT THE LINE BELOW ***************/ -if (typeof module !== "undefined") { - module.exports = config; -} diff --git a/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative_hide_time.js b/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative_hide_time.js deleted file mode 100644 index 57274eae16..0000000000 --- a/tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative_hide_time.js +++ /dev/null @@ -1,34 +0,0 @@ -const config = { - address: "0.0.0.0", - ipWhitelist: [], - - timeFormat: 24, - modules: [ - { - module: "calendar", - position: "bottom_bar", - config: { - fade: false, - urgency: 0, - dateFormat: "Do.MMM", - dateEndFormat: "Do.MMM, HH:mm", - fullDayEventDateFormat: "Do.MMM", - timeFormat: "relative", - getRelative: 0, - hideTime: true, - showEnd: true, - calendars: [ - { - maximumEntries: 100, - url: "http://localhost:8080/tests/mocks/event_with_time_same_day_yearly.ics" - } - ] - } - } - ] -}; - -/*************** DO NOT EDIT THE LINE BELOW ***************/ -if (typeof module !== "undefined") { - module.exports = config; -} diff --git a/tests/configs/modules/calendar/fullday_multiday_showend_dateheaders.js b/tests/configs/modules/calendar/fullday_multiday_showend_dateheaders.js deleted file mode 100644 index 2033d55d60..0000000000 --- a/tests/configs/modules/calendar/fullday_multiday_showend_dateheaders.js +++ /dev/null @@ -1,31 +0,0 @@ -let config = { - address: "0.0.0.0", - ipWhitelist: [], - - timeFormat: 24, - modules: [ - { - module: "calendar", - position: "bottom_bar", - config: { - fade: false, - urgency: 0, - fullDayEventDateFormat: "Do.MMM", - timeFormat: "dateheaders", - 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/fullday_multiday_showend_nextdaysrelative.js b/tests/configs/modules/calendar/fullday_multiday_showend_nextdaysrelative.js deleted file mode 100644 index 32ca4ec2b1..0000000000 --- a/tests/configs/modules/calendar/fullday_multiday_showend_nextdaysrelative.js +++ /dev/null @@ -1,32 +0,0 @@ -let config = { - address: "0.0.0.0", - ipWhitelist: [], - - timeFormat: 24, - modules: [ - { - module: "calendar", - position: "bottom_bar", - config: { - fade: false, - urgency: 0, - fullDayEventDateFormat: "Do.MMM", - timeFormat: "absolute", - getRelative: 0, - showEnd: true, - nextDaysRelative: 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/fullday_multiday_showend_relative.js b/tests/configs/modules/calendar/fullday_multiday_showend_relative.js deleted file mode 100644 index c4dc08cc2d..0000000000 --- a/tests/configs/modules/calendar/fullday_multiday_showend_relative.js +++ /dev/null @@ -1,31 +0,0 @@ -let config = { - address: "0.0.0.0", - ipWhitelist: [], - - timeFormat: 24, - modules: [ - { - module: "calendar", - position: "bottom_bar", - config: { - fade: false, - urgency: 0, - fullDayEventDateFormat: "Do.MMM", - timeFormat: "relative", - 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/electron/modules/calendar_spec.js b/tests/electron/modules/calendar_spec.js index 29a7e42cd8..afaa071e43 100644 --- a/tests/electron/modules/calendar_spec.js +++ b/tests/electron/modules/calendar_spec.js @@ -52,9 +52,15 @@ describe("Calendar module", () => { const defaultCalendarNow = "08 Oct 2024 12:30:00 GMT-07:00"; const defaultCalendarTimeZone = "America/Chicago"; + const showEndConfigPath = "tests/configs/modules/calendar/calendarShowEndConfigs.js"; - const expectFirstEventTimeCell = async ({ configPath, expectedTime, now = defaultCalendarNow, timeZone = defaultCalendarTimeZone }) => { - await helpers.startApplication(configPath, now, [], timeZone); + const startCalendarShowEndScenario = async (scenario, now = defaultCalendarNow, timeZone = defaultCalendarTimeZone) => { + process.env.MM_CALENDAR_SHOWEND_SCENARIO = scenario; + await helpers.startApplication(showEndConfigPath, now, [], timeZone); + }; + + const expectFirstEventTimeCell = async ({ scenario, expectedTime, now = defaultCalendarNow, timeZone = defaultCalendarTimeZone }) => { + await startCalendarShowEndScenario(scenario, now, timeZone); await expect(doTestTableContent(".calendar .event", ".time", expectedTime, first)).resolves.toBe(true); }; @@ -64,6 +70,7 @@ describe("Calendar module", () => { afterEach(async () => { await helpers.stopApplication(); + delete process.env.MM_CALENDAR_SHOWEND_SCENARIO; }); describe("Test css classes", () => { @@ -295,7 +302,7 @@ describe("Calendar module", () => { 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", [], "America/Chicago"); + await startCalendarShowEndScenario("event_with_time_over_multiple_days_non_repeating_no_display_end"); // just await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 20:00", first)).resolves.toBe(true); }); @@ -303,7 +310,7 @@ describe("Calendar module", () => { 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", [], "America/Chicago"); + await startCalendarShowEndScenario("event_with_time_over_multiple_days_non_repeating_display_end"); // just await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 20:00-26th.Oct, 06:00", first)).resolves.toBe(true); }); @@ -313,12 +320,12 @@ describe("Calendar module", () => { const timedMultiDayCases = [ { name: "relative timeFormat shows start and end for timed multi-day events", - configPath: "tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_relative.js", + scenario: "event_with_time_over_multiple_days_non_repeating_display_end_relative", expectedTime: "25th.Oct, 20:00-26th.Oct, 06:00" }, { name: "dateheaders timeFormat shows end for timed multi-day events", - configPath: "tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_dateheaders.js", + scenario: "event_with_time_over_multiple_days_non_repeating_display_end_dateheaders", expectedTime: "20:00-06:00" } ]; @@ -333,22 +340,22 @@ describe("Calendar module", () => { const timedSameDaySimpleCases = [ { name: "absolute timeFormat shows start and end time without repeating date", - configPath: "tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute.js", + scenario: "event_with_time_same_day_yearly_display_end_absolute", expectedTime: "25th.Oct, 20:00-22:00" }, { name: "absolute timeFormat with time in dateFormat does not duplicate start time", - configPath: "tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_with_time.js", + scenario: "event_with_time_same_day_yearly_display_end_absolute_dateformat_with_time", expectedTime: "25th.Oct, 20:00-22:00" }, { name: "relative timeFormat shows start and end time without repeating date", - configPath: "tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative.js", + scenario: "event_with_time_same_day_yearly_display_end_relative", expectedTime: "25th.Oct, 20:00-22:00" }, { name: "dateheaders timeFormat shows start and end time only", - configPath: "tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_dateheaders.js", + scenario: "event_with_time_same_day_yearly_display_end_dateheaders", expectedTime: "20:00-22:00" } ]; @@ -359,7 +366,7 @@ describe("Calendar module", () => { }); it("absolute timeFormat with dateFormat LLL does not duplicate start time", async () => { - await helpers.startApplication("tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_absolute_dateformat_lll.js", defaultCalendarNow, [], defaultCalendarTimeZone); + await startCalendarShowEndScenario("event_with_time_same_day_yearly_display_end_absolute_dateformat_lll"); const timeText = await getFirstEventTimeText(); const timeTokens = timeText.match(/\d{1,2}:\d{2}(?:\s?[AP]M)?/gi) || []; expect(timeTokens).toHaveLength(2); @@ -367,7 +374,7 @@ describe("Calendar module", () => { }); it("relative timeFormat with hideTime does not show start or end times", async () => { - await helpers.startApplication("tests/configs/modules/calendar/event_with_time_same_day_yearly_display_end_relative_hide_time.js", defaultCalendarNow, [], defaultCalendarTimeZone); + await startCalendarShowEndScenario("event_with_time_same_day_yearly_display_end_relative_hide_time"); const timeText = await getFirstEventTimeText(); expect(timeText).toContain("25th.Oct"); expect(timeText.match(/\d{1,2}:\d{2}(?:\s?[AP]M)?/gi) || []).toHaveLength(0); @@ -396,17 +403,17 @@ describe("Calendar module", () => { const fullDayShowEndCases = [ { name: "relative timeFormat shows start and end date", - configPath: "tests/configs/modules/calendar/fullday_multiday_showend_relative.js", + scenario: "fullday_multiday_showend_relative", expectedTime: "25th.Oct-30th.Oct" }, { name: "dateheaders timeFormat shows end date in time cell", - configPath: "tests/configs/modules/calendar/fullday_multiday_showend_dateheaders.js", + scenario: "fullday_multiday_showend_dateheaders", expectedTime: "-30th.Oct" }, { name: "absolute timeFormat with nextDaysRelative shows relative label and end date", - configPath: "tests/configs/modules/calendar/fullday_multiday_showend_nextdaysrelative.js", + scenario: "fullday_multiday_showend_nextdaysrelative", expectedTime: "Tomorrow-30th.Oct", now: "24 Oct 2024 12:30:00 GMT-07:00" } From e4acbc04de56b000efc17d411470133441747bf9 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:59:23 +0100 Subject: [PATCH 12/13] test(calendar): wait for first event time cell before reading text --- tests/electron/modules/calendar_spec.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/electron/modules/calendar_spec.js b/tests/electron/modules/calendar_spec.js index afaa071e43..1be60c4d8f 100644 --- a/tests/electron/modules/calendar_spec.js +++ b/tests/electron/modules/calendar_spec.js @@ -65,7 +65,9 @@ describe("Calendar module", () => { }; const getFirstEventTimeText = async () => { - return (await global.page.locator(".calendar .event .time").locator(`nth=${first}`).textContent()) || ""; + const timeCell = global.page.locator(".calendar .event .time").locator(`nth=${first}`); + await timeCell.waitFor({ state: "visible" }); + return (await timeCell.textContent()) || ""; }; afterEach(async () => { From 0905eb43571bf2c45113891229dbd5815a96d470 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:59:44 +0100 Subject: [PATCH 13/13] fix(calendar): respect showEndsOnlyWithDuration in absolute timed end --- defaultmodules/calendar/calendar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/defaultmodules/calendar/calendar.js b/defaultmodules/calendar/calendar.js index e434c4c765..cfc625431a 100644 --- a/defaultmodules/calendar/calendar.js +++ b/defaultmodules/calendar/calendar.js @@ -730,7 +730,7 @@ Module.register("calendar", { buildAbsoluteTimeText (event, eventStartDateMoment, eventEndDateMoment, now) { let timeText = CalendarUtils.capFirst(eventStartDateMoment.format(this.config.dateFormat)); - if (this.config.showEnd && this.hasEventDuration(event)) { + if (this.config.showEnd && (!this.config.showEndsOnlyWithDuration || this.hasEventDuration(event))) { const sameDay = this.isSameDay(eventStartDateMoment, eventEndDateMoment); if (sameDay && !this.dateFormatIncludesTime()) { timeText += `, ${eventStartDateMoment.format("LT")}`;