Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/automated-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,14 @@ jobs:
run: |
node --run install-mm:dev
test:
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 30
strategy:
matrix:
node-version: [22.x, 24.x, 26.x]
steps:
- name: Install electron dependencies and labwc
timeout-minutes: 10
run: |
sudo apt-get update
sudo apt-get install -y libnss3 libasound2t64 labwc
Expand Down
2 changes: 1 addition & 1 deletion defaultmodules/calendar/calendar.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Module.register("calendar", {

// Define required scripts.
getScripts () {
return ["calendarutils.js", "moment.js", "moment-timezone.js"];
return ["calendarutils.mjs", "moment.js", "moment-timezone.js"];
},

// Define required translations.
Expand Down
12 changes: 6 additions & 6 deletions defaultmodules/calendar/calendarfetcher.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const ical = require("node-ical");
const Log = require("logger");
const CalendarFetcherUtils = require("./calendarfetcherutils");
const HTTPFetcher = require("#http_fetcher");
import { async as icalAsync } from "node-ical";
import Log from "logger";
import HTTPFetcher from "../../js/http_fetcher.mjs";
import { CalendarFetcherUtils } from "./calendarfetcherutils.js";

/**
* CalendarFetcher - Fetches and parses iCal calendar data
Expand Down Expand Up @@ -66,7 +66,7 @@ class CalendarFetcher {
maximumNumberOfDays: this.maximumNumberOfDays
});

const parsed = await ical.async.parseICS(filteredData);
const parsed = await icalAsync.parseICS(filteredData);

Log.debug(`Parsed iCal data from ${this.url} with ${Object.keys(parsed).length} entries.`);

Expand Down Expand Up @@ -139,4 +139,4 @@ class CalendarFetcher {
}
}

module.exports = CalendarFetcher;
export { CalendarFetcher };
10 changes: 4 additions & 6 deletions defaultmodules/calendar/calendarfetcherutils.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/**
* @external Moment
*/
const moment = require("moment-timezone");
const ical = require("node-ical");
import moment from "moment-timezone";
import ical from "node-ical";

const Log = require("logger");
import Log from "logger";

const CalendarFetcherUtils = {

Expand Down Expand Up @@ -341,6 +341,4 @@ const CalendarFetcherUtils = {
}
};

if (typeof module !== "undefined") {
module.exports = CalendarFetcherUtils;
}
export { CalendarFetcherUtils };
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const CalendarUtils = {
return { longDateFormat: { LT: "HH:mm" } };
}
default: {
return { longDateFormat: { LT: moment.localeData().longDateFormat("LT") } };
return { longDateFormat: { LT: globalThis.moment.localeData().longDateFormat("LT") } };
}
}
},
Expand Down Expand Up @@ -131,6 +131,6 @@ const CalendarUtils = {
}
};

if (typeof module !== "undefined") {
module.exports = CalendarUtils;
}
globalThis.CalendarUtils = CalendarUtils;

export default CalendarUtils;
12 changes: 7 additions & 5 deletions defaultmodules/calendar/debug.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
* use this script with `node debug.js` to test the fetcher without the need
* of starting the MagicMirror² core. Adjust the values below to your desire.
*/
// Load internal alias resolver
require("../../js/alias-resolver");
const Log = require("logger");
import { createRequire } from "node:module";

const CalendarFetcher = require("./calendarfetcher");
const require = createRequire(import.meta.url);
require("../../js/alias-resolver.js");

const { default: Log } = await import("logger");
const { CalendarFetcher } = await import("./calendarfetcher.js");

const url = "https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics"; // Standard test URL
//const url = "https://www.googleapis.com/calendar/v3/calendars/primary/events/"; // URL for Bearer auth (must be configured in Google OAuth2 first)
Expand Down Expand Up @@ -35,6 +37,6 @@ fetcher.onError((fetcher, error) => {
process.exit(1);
});

fetcher.startFetch();
fetcher.fetchCalendar();

Log.log("Create fetcher done! ");
10 changes: 5 additions & 5 deletions defaultmodules/calendar/node_helper.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
const zlib = require("node:zlib");
const NodeHelper = require("node_helper");
const Log = require("logger");
const CalendarFetcher = require("./calendarfetcher");
import zlib from "node:zlib";
import NodeHelper from "node_helper";
import Log from "logger";
import { CalendarFetcher } from "./calendarfetcher.js";

module.exports = NodeHelper.create({
export default NodeHelper.create({
// Override start method.
start () {
Log.log(`Starting node helper for: ${this.name}`);
Expand Down
3 changes: 3 additions & 0 deletions defaultmodules/calendar/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "module"
}
8 changes: 4 additions & 4 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default defineConfig([
}
},
{
files: ["**/*.js", "defaultmodules/defaultmodules.mjs", "js/app.mjs", "js/deprecated.mjs", "js/electron_helper.mjs", "js/http_fetcher.mjs", "js/ip_access_control.mjs", "js/releasenotes.mjs", "js/server.mjs", "js/server_functions.mjs", "js/vendor.mjs", "tests/**/*.mjs"],
files: ["**/*.js", "defaultmodules/defaultmodules.mjs", "defaultmodules/calendar/calendarutils.mjs", "js/app.mjs", "js/deprecated.mjs", "js/electron_helper.mjs", "js/http_fetcher.mjs", "js/ip_access_control.mjs", "js/releasenotes.mjs", "js/server.mjs", "js/server_functions.mjs", "js/vendor.mjs", "tests/**/*.mjs"],
languageOptions: {
ecmaVersion: "latest",
globals: {
Expand Down Expand Up @@ -104,7 +104,7 @@ export default defineConfig([
}
},
{
files: ["**/*.js", "defaultmodules/defaultmodules.mjs", "js/app.mjs", "js/deprecated.mjs", "js/electron_helper.mjs", "js/http_fetcher.mjs", "js/ip_access_control.mjs", "js/releasenotes.mjs", "js/server.mjs", "js/server_functions.mjs", "js/vendor.mjs", "tests/**/*.mjs"],
files: ["**/*.js", "defaultmodules/defaultmodules.mjs", "defaultmodules/calendar/calendarutils.mjs", "js/app.mjs", "js/deprecated.mjs", "js/electron_helper.mjs", "js/http_fetcher.mjs", "js/ip_access_control.mjs", "js/releasenotes.mjs", "js/server.mjs", "js/server_functions.mjs", "js/vendor.mjs", "tests/**/*.mjs"],
ignores: [
"clientonly/index.js",
"js/logger.js",
Expand All @@ -119,7 +119,7 @@ export default defineConfig([
},
{
files: ["**/*.mjs"],
ignores: ["defaultmodules/defaultmodules.mjs", "js/app.mjs", "js/deprecated.mjs", "js/electron_helper.mjs", "js/http_fetcher.mjs", "js/ip_access_control.mjs", "js/releasenotes.mjs", "js/server.mjs", "js/server_functions.mjs", "js/vendor.mjs", "tests/**/*.mjs"],
ignores: ["defaultmodules/defaultmodules.mjs", "defaultmodules/calendar/calendarutils.mjs", "js/app.mjs", "js/deprecated.mjs", "js/electron_helper.mjs", "js/http_fetcher.mjs", "js/ip_access_control.mjs", "js/releasenotes.mjs", "js/server.mjs", "js/server_functions.mjs", "js/vendor.mjs", "tests/**/*.mjs"],
languageOptions: {
ecmaVersion: "latest",
globals: {
Expand Down Expand Up @@ -181,7 +181,7 @@ export default defineConfig([
}
},
{
files: ["tests/mocks/**/node_helper.js", "defaultmodules/*/node_helper.js", "defaultmodules/*/git_helper.js", "defaultmodules/newsfeed/feeditem.js", "defaultmodules/newsfeed/newsfeedfetcher.js"],
files: ["tests/mocks/**/node_helper.js", "defaultmodules/*/node_helper.js", "defaultmodules/*/git_helper.js", "defaultmodules/newsfeed/feeditem.js", "defaultmodules/newsfeed/newsfeedfetcher.js", "defaultmodules/calendar/calendarfetcher.js", "defaultmodules/calendar/calendarfetcherutils.js", "defaultmodules/calendar/debug.js"],
rules: {
// MagicMirror aliases are resolved at runtime by js/alias-resolver.js.
"import-x/no-unresolved": ["error", {ignore: ["^node_helper$", "^logger$"]}]
Expand Down
16 changes: 7 additions & 9 deletions tests/unit/modules/default/calendar/calendar_fetcher_spec.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
global.moment = require("moment-timezone");
import { afterEach, describe, expect, it, vi } from "vitest";
import { async as icalAsync } from "node-ical";
import moment from "moment-timezone";

const ical = require("node-ical");
const moment = require("moment-timezone");
const defaults = require("../../../../../js/defaults");

const CalendarFetcherUtils = require(`../../../../../${defaults.defaultModulesDir}/calendar/calendarfetcherutils`);

const CalendarFetcher = require(`../../../../../${defaults.defaultModulesDir}/calendar/calendarfetcher`);
globalThis.moment = moment;
const { CalendarFetcherUtils } = await import("../../../../../defaultmodules/calendar/calendarfetcherutils.js");
const { CalendarFetcher } = await import("../../../../../defaultmodules/calendar/calendarfetcher.js");

const makeFetcher = (options = {}) => new CalendarFetcher(
options.url ?? "http://test.example.com/cal.ics",
Expand Down Expand Up @@ -78,7 +76,7 @@ describe("CalendarFetcher", () => {
await emitResponse(fetcher, new Response(futureEventICS(), { status: 200 }));
expect(fetcher.events).toHaveLength(1);

vi.spyOn(ical.async, "parseICS").mockRejectedValueOnce(new Error("invalid ics"));
vi.spyOn(icalAsync, "parseICS").mockRejectedValueOnce(new Error("invalid ics"));
const error = await emitResponse(fetcher, new Response("BROKEN", { status: 200 }));

expect(fetcher.events).toHaveLength(1);
Expand Down
31 changes: 14 additions & 17 deletions tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
global.moment = require("moment-timezone");

const ical = require("node-ical");
const moment = require("moment-timezone");
const defaults = require("../../../../../js/defaults");

const CalendarFetcherUtils = require(`../../../../../${defaults.defaultModulesDir}/calendar/calendarfetcherutils`);
import { describe, expect, it } from "vitest";
import { parseICS } from "node-ical";
import moment from "moment-timezone";
import { CalendarFetcherUtils } from "../../../../../defaultmodules/calendar/calendarfetcherutils.js";

describe("Calendar fetcher utils test", () => {
const defaultConfig = {
Expand Down Expand Up @@ -114,7 +111,7 @@ describe("Calendar fetcher utils test", () => {
});

it("should return the correct times when recurring events pass through daylight saving time", () => {
const data = ical.parseICS(`BEGIN:VEVENT
const data = parseICS(`BEGIN:VEVENT
DTSTART;TZID=Europe/Amsterdam:20250311T090000
DTEND;TZID=Europe/Amsterdam:20250311T091500
RRULE:FREQ=WEEKLY;BYDAY=FR,MO,TH,TU,WE,SA,SU
Expand Down Expand Up @@ -148,7 +145,7 @@ END:VEVENT`);
});

it("should return the correct moments based on the timezone given", () => {
const data = ical.parseICS(`BEGIN:VEVENT
const data = parseICS(`BEGIN:VEVENT
DTSTART;TZID=Europe/Amsterdam:20250311T090000
DTEND;TZID=Europe/Amsterdam:20250311T091500
RRULE:FREQ=WEEKLY;BYDAY=FR,MO,TH,TU,WE,SA,SU
Expand All @@ -170,7 +167,7 @@ END:VEVENT`);
it("should return correct day-of-week for full-day recurring events across DST transitions", () => {
// Test case for GitHub issue #3976: recurring full-day events showing on wrong day
// This happens when DST transitions change the UTC offset between occurrences
const data = ical.parseICS(`BEGIN:VCALENDAR
const data = parseICS(`BEGIN:VCALENDAR
BEGIN:VEVENT
DTSTART;VALUE=DATE:20251027
DTEND;VALUE=DATE:20251028
Expand Down Expand Up @@ -210,7 +207,7 @@ END:VCALENDAR`);
// Facebook birthday calendars use DTSTART with the actual birth year (e.g. 1990),
// which previously caused rrule.js to return the wrong year occurrence.
// With rrule-temporal this works correctly without any special-casing.
const data = ical.parseICS(`BEGIN:VCALENDAR
const data = parseICS(`BEGIN:VCALENDAR
BEGIN:VEVENT
DTSTART;VALUE=DATE:19900215
RRULE:FREQ=YEARLY
Expand Down Expand Up @@ -277,7 +274,7 @@ END:VCALENDAR`);
// node-ical creates DATE-only events with the local Date constructor: new Date(year, month, day).
// getFullYear() on a locally-constructed date always returns the correct calendar year
// regardless of the server's UTC offset — guard against regressions that switch to getUTCFullYear().
const data = ical.parseICS(`BEGIN:VCALENDAR
const data = parseICS(`BEGIN:VCALENDAR
BEGIN:VEVENT
DTSTART;VALUE=DATE:19900101
DTEND;VALUE=DATE:19900102
Expand Down Expand Up @@ -324,7 +321,7 @@ END:VCALENDAR`);
describe("expandRecurringEvent", () => {
it("should extend end to end-of-day when event has no DTEND", () => {
// node-ical sets end === start when DTEND is absent; our code extends to endOf("day")
const data = ical.parseICS(`BEGIN:VCALENDAR
const data = parseICS(`BEGIN:VCALENDAR
BEGIN:VEVENT
DTSTART:20260222T100000Z
UID:no-end-test@test
Expand All @@ -340,7 +337,7 @@ END:VCALENDAR`);

it("should apply RECURRENCE-ID overrides (moved single occurrence)", () => {
// A weekly event on Mondays at 10:00, but the second occurrence is moved to Tuesday 14:00
const data = ical.parseICS(`BEGIN:VCALENDAR
const data = parseICS(`BEGIN:VCALENDAR
BEGIN:VEVENT
DTSTART;TZID=Europe/Berlin:20260302T100000
DTEND;TZID=Europe/Berlin:20260302T110000
Expand Down Expand Up @@ -379,7 +376,7 @@ END:VCALENDAR`);

it("should handle events with DURATION instead of DTEND", () => {
// RFC 5545 allows DURATION as alternative to DTEND
const data = ical.parseICS(`BEGIN:VCALENDAR
const data = parseICS(`BEGIN:VCALENDAR
BEGIN:VEVENT
DTSTART:20260315T090000Z
DURATION:PT1H30M
Expand All @@ -401,7 +398,7 @@ END:VCALENDAR`);
});

it("should handle recurring events with DURATION instead of DTEND", () => {
const data = ical.parseICS(`BEGIN:VCALENDAR
const data = parseICS(`BEGIN:VCALENDAR
BEGIN:VEVENT
DTSTART;TZID=Europe/Berlin:20260301T080000
DURATION:PT45M
Expand Down Expand Up @@ -528,7 +525,7 @@ END:VCALENDAR`);
const yearConfig = { ...defaultConfig, maximumNumberOfDays: 365 };

const buildEvent = (rrule, dtstart = "20231002", dtend = "20231003") => {
return ical.parseICS(`BEGIN:VCALENDAR
return parseICS(`BEGIN:VCALENDAR
BEGIN:VEVENT
DTSTART;VALUE=DATE:${dtstart}
DTEND;VALUE=DATE:${dtend}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
global.moment = require("moment-timezone");
import moment from "moment-timezone";
import { CalendarFetcherUtils } from "../../../../../defaultmodules/calendar/calendarfetcherutils.js";

const defaults = require("../../../js/defaults");

const CalendarFetcherUtils = require(`../../../../../${defaults.defaultModulesDir}/calendar/calendarfetcherutils`);
globalThis.moment = moment;

describe("Calendar fetcher utils test", () => {
const defaultConfig = {
Expand Down
7 changes: 3 additions & 4 deletions tests/unit/modules/default/calendar/calendar_utils_spec.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
global.moment = require("moment");
import moment from "moment";
import CalendarUtils from "../../../../../defaultmodules/calendar/calendarutils.mjs";

const defaults = require("../../../../../js/defaults");

const CalendarUtils = require(`../../../../../${defaults.defaultModulesDir}/calendar/calendarutils`);
globalThis.moment = moment;

describe("Calendar utils tests", () => {
describe("capFirst", () => {
Expand Down
5 changes: 1 addition & 4 deletions vitest.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,7 @@ export default defineConfig({
globals: true,
environment: "node",
setupFiles: ["./tests/utils/vitest-setup.js"],
include: [
"tests/unit/**/*_spec.{js,mjs}",
"tests/unit/defaultmodules/calendar/calendar_fetcher_utils_bad_rrule.js"
],
include: ["tests/unit/**/*_spec.{js,mjs}"],
testTimeout: 20000,
hookTimeout: 10000
}
Expand Down
Loading