Files
workavia-calendar-front/__test__/features/Events/eventUtils.test.ts
T
lenhanphung 7d46aef1fa test: add edge case tests for Menubar and eventUtils
- Add comprehensive edge case tests for Menubar avatar display logic
- Add timezone fallback edge case tests for eventUtils
- Cover null/undefined/empty string scenarios for user data
- Cover invalid/missing timezone scenarios with UTC fallback
- Ensure robust error handling and graceful degradation
2025-09-19 13:40:02 +07:00

633 lines
18 KiB
TypeScript

import { CalendarEvent } from "../../../src/features/Events/EventsTypes";
import {
calendarEventToJCal,
parseCalendarEvent,
} from "../../../src/features/Events/eventUtils";
import { TIMEZONES } from "../../../src/utils/timezone-data";
describe("parseCalendarEvent", () => {
const baseColor = "#00FF00";
const calendarId = "calendar-123";
it("parses a full event correctly", () => {
const rawData = [
["UID", {}, "text", "event-1"],
["DTSTART", {}, "date-time", "2025-07-18T09:00:00Z"],
["DTEND", {}, "date-time", "2025-07-18T10:00:00Z"],
["SUMMARY", {}, "text", "Team Meeting"],
["DESCRIPTION", {}, "text", "Discuss roadmap"],
["LOCATION", {}, "text", "Zoom"],
["ORGANIZER", { cn: "Alice" }, "cal-address", "alice@example.com"],
[
"ATTENDEE",
{ cn: "Bob", partstat: "ACCEPTED" },
"cal-address",
"bob@example.com",
],
["X-OPENPAAS-VIDEOCONFERENCE", {}, "text", "https://meet.link"],
["STATUS", {}, "text", "CONFIRMED"],
["SEQUENCE", {}, "integer", "2"],
["TRANSP", {}, "text", "OPAQUE"],
["CLASS", {}, "text", "PUBLIC"],
["DTSTAMP", {}, "date-time", "2025-07-18T08:00:00Z"],
] as unknown as [string, Record<string, string>, string, any];
const result = parseCalendarEvent(
rawData,
baseColor,
calendarId,
"/calendars/test.ics"
);
expect(result.uid).toBe("event-1");
expect(result.title).toBe("Team Meeting");
expect(result.description).toBe("Discuss roadmap");
expect(result.location).toBe("Zoom");
expect(result.start).toBe("2025-07-18T09:00:00Z");
expect(result.end).toBe("2025-07-18T10:00:00Z");
expect(result.stamp).toBe("2025-07-18T08:00:00Z");
expect(result.sequence).toBe(2);
expect(result.color).toBe(baseColor);
expect(result.status).toBe("CONFIRMED");
expect(result.transp).toBe("OPAQUE");
expect(result.class).toBe("PUBLIC");
expect(result.x_openpass_videoconference).toBe("https://meet.link");
expect(result.organizer).toEqual({
cn: "Alice",
cal_address: "alice@example.com",
});
expect(result.attendee).toEqual([
{
cn: "Bob",
cal_address: "bob@example.com",
partstat: "ACCEPTED",
rsvp: "",
role: "",
cutype: "",
},
]);
});
it("marks allday true for DATE format", () => {
const rawData = [
["UID", {}, "text", "event-2"],
["DTSTART", {}, "date", "2025-07-20"],
["DTEND", {}, "date", "2025-07-21"],
] as any;
const result = parseCalendarEvent(
rawData,
baseColor,
calendarId,
"/calendars/test.ics"
);
expect(result.allday).toBe(true);
});
it("appends recurrence-id to UID if present", () => {
const rawData: any = [
["UID", {}, "text", "event-2"],
["RECURRENCE-ID", {}, "date-time", "2025-07-18T09:00:00Z"],
["DTSTART", {}, "date-time", "2025-07-18T09:00:00Z"],
];
const result = parseCalendarEvent(
rawData,
baseColor,
calendarId,
"/calendars/test.ics"
);
expect(result.uid).toBe("event-2/2025-07-18T09:00:00Z");
});
it("returns error if UID or start is missing", () => {
const rawDataMissingUid: any = [
["DTSTART", {}, "date-time", "2025-07-18T09:00:00Z"],
];
const result = parseCalendarEvent(
rawDataMissingUid,
baseColor,
calendarId,
"/calendars/test.ics"
);
expect(result.error).toMatch(/missing crucial event param/);
const rawDataMissingStart: any = [["UID", {}, "text", "event-3"]];
const result2 = parseCalendarEvent(
rawDataMissingStart,
baseColor,
calendarId,
"/calendars/test.ics"
);
expect(result2.error).toMatch(/missing crucial event param/);
});
it("parses alarm block correctly", () => {
const rawData = [
["UID", {}, "text", "event-5"],
["DTSTART", {}, "date-time", "2025-07-18T09:00:00Z"],
] as any;
const valarm: any = [
"VALARM",
[
["ACTION", {}, "text", "DISPLAY"],
["TRIGGER", {}, "duration", "-PT15M"],
],
];
const result = parseCalendarEvent(
rawData,
baseColor,
calendarId,
"/calendars/test.ics",
valarm
);
expect(result.alarm?.action).toBe("DISPLAY");
expect(result.alarm?.trigger).toBe("-PT15M");
});
it("handles optional organizer and attendee fields gracefully", () => {
const rawData = [
["UID", {}, "text", "event-4"],
["DTSTART", {}, "date-time", "2025-07-18T09:00:00Z"],
["ATTENDEE", {}, "cal-address", "john@example.com"],
["ORGANIZER", {}, "cal-address", "jane@example.com"],
] as unknown as [string, Record<string, string>, string, any];
const result = parseCalendarEvent(
rawData,
baseColor,
calendarId,
"/calendars/test.ics"
);
expect(result.attendee).toEqual([
{
cn: "",
cal_address: "john@example.com",
partstat: "",
rsvp: "",
role: "",
cutype: "",
},
]);
expect(result.organizer).toEqual({
cn: "",
cal_address: "jane@example.com",
});
});
});
describe("calendarEventToJCal", () => {
beforeAll(() => {
jest.mock("ical.js", () => ({
Timezone: jest.fn().mockImplementation(({ component, tzid }) => ({
component: {
jCal: [`vtimezone`, [], [["tzid", {}, "text", tzid]]],
},
})),
}));
});
it("should convert a CalendarEvent to JCal format", () => {
const mockEvent = {
uid: "event-123",
URL: "/calendars/test.ics",
calId: "test/test",
title: "Team Meeting",
start: new Date("2025-07-23T10:00:00"),
end: new Date("2025-07-23T11:00:00"),
timezone: "Europe/Paris",
transp: "OPAQUE",
class: "PUBLIC",
allday: false,
location: "Room 101",
description: "Discuss project roadmap.",
repetition: { freq: "WEEKLY" },
organizer: {
cn: "Alice",
cal_address: "alice@example.com",
},
attendee: [
{
cn: "Bob",
partstat: "ACCEPTED",
rsvp: "TRUE",
role: "REQ-PARTICIPANT",
cutype: "INDIVIDUAL",
cal_address: "bob@example.com",
},
],
};
const result = calendarEventToJCal(mockEvent as CalendarEvent);
expect(result[0]).toBe("vcalendar");
const [vevent, vtimezone] = result[2];
expect(vevent[0]).toBe("vevent");
const props = vevent[1];
expect(props).toEqual(
expect.arrayContaining([
["uid", {}, "text", "event-123"],
["summary", {}, "text", "Team Meeting"],
["transp", {}, "text", "OPAQUE"],
[
"dtstart",
{ tzid: "Europe/Paris" },
"date-time",
"2025-07-23T10:00:00",
],
["dtend", { tzid: "Europe/Paris" }, "date-time", "2025-07-23T11:00:00"],
["class", {}, "text", "PUBLIC"],
["location", {}, "text", "Room 101"],
["description", {}, "text", "Discuss project roadmap."],
["x-openpaas-videoconference", {}, "unknown", null],
["rrule", {}, "recur", { freq: "WEEKLY" }],
[
"organizer",
{ cn: "Alice" },
"cal-address",
"mailto:alice@example.com",
],
[
"attendee",
{
cn: "Bob",
partstat: "ACCEPTED",
rsvp: "TRUE",
role: "REQ-PARTICIPANT",
cutype: "INDIVIDUAL",
},
"cal-address",
"mailto:bob@example.com",
],
])
);
expect(vtimezone[0]).toBe("vtimezone");
expect(vtimezone[2]).toEqual(
expect.arrayContaining([
[
"daylight",
[
["tzoffsetfrom", {}, "utc-offset", "+01:00"],
["tzoffsetto", {}, "utc-offset", "+02:00"],
["tzname", {}, "text", "CEST"],
["dtstart", {}, "date-time", "1970-03-29T02:00:00"],
[
"rrule",
{},
"recur",
{ byday: "-1SU", bymonth: 3, freq: "YEARLY" },
],
],
[],
],
[
"standard",
[
["tzoffsetfrom", {}, "utc-offset", "+02:00"],
["tzoffsetto", {}, "utc-offset", "+01:00"],
["tzname", {}, "text", "CET"],
["dtstart", {}, "date-time", "1970-10-25T03:00:00"],
[
"rrule",
{},
"recur",
{ byday: "-1SU", bymonth: 10, freq: "YEARLY" },
],
],
[],
],
])
);
});
it("converts with alarm included", () => {
const mockEvent: any = {
uid: "event-10",
title: "Alarm Event",
start: new Date("2025-07-20T09:00:00"),
end: new Date("2025-07-20T10:00:00"),
timezone: "Europe/Paris",
allday: false,
alarm: { trigger: "-PT10M", action: "DISPLAY" },
attendee: [],
};
const result = calendarEventToJCal(mockEvent, "owner@example.com");
const vevent = result[2][0];
expect(vevent[2][0][0]).toBe("valarm");
expect(vevent[2][0][1]).toEqual(
expect.arrayContaining([
["trigger", {}, "duration", "-PT10M"],
["action", {}, "text", "DISPLAY"],
])
);
});
it("converts all-day events adjusting dtend", () => {
const mockEvent: any = {
uid: "event-11",
title: "All Day",
start: new Date("2025-07-21"),
end: new Date("2025-07-21"),
timezone: "Europe/Paris",
allday: true,
attendee: [],
};
const result = calendarEventToJCal(mockEvent);
const veventProps = result[2][0][1];
expect(veventProps).toEqual(
expect.arrayContaining([
["dtstart", { tzid: "Europe/Paris" }, "date", "2025-07-21"],
["dtend", { tzid: "Europe/Paris" }, "date", "2025-07-22"],
])
);
});
it("should convert a CalendarEvent to JCal format, with all day activated", () => {
const mockEvent = {
uid: "event-123",
URL: "/calendars/test.ics",
calId: "test/test",
title: "Team Meeting",
start: new Date("2025-07-23"),
end: new Date("2025-07-23"),
timezone: "Europe/Paris",
transp: "OPAQUE",
class: "PUBLIC",
allday: true,
location: "Room 101",
description: "Discuss project roadmap.",
repetition: { freq: "WEEKLY", interval: 2 },
organizer: {
cn: "Alice",
cal_address: "alice@example.com",
},
attendee: [
{
cn: "Bob",
partstat: "ACCEPTED",
rsvp: "TRUE",
role: "REQ-PARTICIPANT",
cutype: "INDIVIDUAL",
cal_address: "bob@example.com",
},
],
};
const result = calendarEventToJCal(mockEvent as CalendarEvent);
expect(result[0]).toBe("vcalendar");
const [vevent, vtimezone] = result[2];
expect(vevent[0]).toBe("vevent");
const props = vevent[1];
expect(props).toEqual(
expect.arrayContaining([
["uid", {}, "text", "event-123"],
["summary", {}, "text", "Team Meeting"],
["transp", {}, "text", "OPAQUE"],
["dtstart", { tzid: "Europe/Paris" }, "date", "2025-07-23"],
["dtend", { tzid: "Europe/Paris" }, "date", "2025-07-24"],
["class", {}, "text", "PUBLIC"],
["location", {}, "text", "Room 101"],
["description", {}, "text", "Discuss project roadmap."],
["x-openpaas-videoconference", {}, "unknown", null],
["rrule", {}, "recur", { freq: "WEEKLY", interval: 2 }],
[
"organizer",
{ cn: "Alice" },
"cal-address",
"mailto:alice@example.com",
],
[
"attendee",
{
cn: "Bob",
partstat: "ACCEPTED",
rsvp: "TRUE",
role: "REQ-PARTICIPANT",
cutype: "INDIVIDUAL",
},
"cal-address",
"mailto:bob@example.com",
],
])
);
expect(vtimezone[0]).toBe("vtimezone");
expect(vtimezone[2]).toEqual(
expect.arrayContaining([
[
"daylight",
[
["tzoffsetfrom", {}, "utc-offset", "+01:00"],
["tzoffsetto", {}, "utc-offset", "+02:00"],
["tzname", {}, "text", "CEST"],
["dtstart", {}, "date-time", "1970-03-29T02:00:00"],
[
"rrule",
{},
"recur",
{ byday: "-1SU", bymonth: 3, freq: "YEARLY" },
],
],
[],
],
[
"standard",
[
["tzoffsetfrom", {}, "utc-offset", "+02:00"],
["tzoffsetto", {}, "utc-offset", "+01:00"],
["tzname", {}, "text", "CET"],
["dtstart", {}, "date-time", "1970-10-25T03:00:00"],
[
"rrule",
{},
"recur",
{ byday: "-1SU", bymonth: 10, freq: "YEARLY" },
],
],
[],
],
])
);
});
// Edge cases for timezone fallback
it("handles invalid timezone with UTC fallback", () => {
const mockEvent = {
uid: "event-invalid-tz",
title: "Invalid Timezone Event",
start: new Date("2025-07-23T10:00:00"),
end: new Date("2025-07-23T11:00:00"),
timezone: "Invalid/Timezone",
allday: false,
attendee: [],
};
const result = calendarEventToJCal(mockEvent as CalendarEvent);
expect(result[0]).toBe("vcalendar");
const [vevent, vtimezone] = result[2];
expect(vevent[0]).toBe("vevent");
expect(vtimezone[0]).toBe("vtimezone");
// Should use UTC timezone as fallback - check for UTC standard timezone
expect(vtimezone[2]).toEqual(
expect.arrayContaining([
["standard", [
["tzoffsetfrom", {}, "utc-offset", "+00:00"],
["tzoffsetto", {}, "utc-offset", "+00:00"],
["tzname", {}, "text", "UTC"],
["dtstart", {}, "date-time", "1970-01-01T00:00:00"]
], []]
])
);
});
it("handles null timezone with UTC fallback", () => {
const mockEvent = {
uid: "event-null-tz",
title: "Null Timezone Event",
start: new Date("2025-07-23T10:00:00"),
end: new Date("2025-07-23T11:00:00"),
timezone: null,
allday: false,
attendee: [],
};
const result = calendarEventToJCal(mockEvent as CalendarEvent);
expect(result[0]).toBe("vcalendar");
const [vevent, vtimezone] = result[2];
expect(vevent[0]).toBe("vevent");
expect(vtimezone[0]).toBe("vtimezone");
// Should use UTC timezone as fallback
expect(vtimezone[2]).toEqual(
expect.arrayContaining([
["standard", [
["tzoffsetfrom", {}, "utc-offset", "+00:00"],
["tzoffsetto", {}, "utc-offset", "+00:00"],
["tzname", {}, "text", "UTC"],
["dtstart", {}, "date-time", "1970-01-01T00:00:00"]
], []]
])
);
});
it("handles undefined timezone with UTC fallback", () => {
const mockEvent = {
uid: "event-undefined-tz",
title: "Undefined Timezone Event",
start: new Date("2025-07-23T10:00:00"),
end: new Date("2025-07-23T11:00:00"),
timezone: undefined,
allday: false,
attendee: [],
};
const result = calendarEventToJCal(mockEvent as CalendarEvent);
expect(result[0]).toBe("vcalendar");
const [vevent, vtimezone] = result[2];
expect(vevent[0]).toBe("vevent");
expect(vtimezone[0]).toBe("vtimezone");
// Should use UTC timezone as fallback
expect(vtimezone[2]).toEqual(
expect.arrayContaining([
["standard", [
["tzoffsetfrom", {}, "utc-offset", "+00:00"],
["tzoffsetto", {}, "utc-offset", "+00:00"],
["tzname", {}, "text", "UTC"],
["dtstart", {}, "date-time", "1970-01-01T00:00:00"]
], []]
])
);
});
it("handles empty string timezone with UTC fallback", () => {
const mockEvent = {
uid: "event-empty-tz",
title: "Empty Timezone Event",
start: new Date("2025-07-23T10:00:00"),
end: new Date("2025-07-23T11:00:00"),
timezone: "",
allday: false,
attendee: [],
};
const result = calendarEventToJCal(mockEvent as CalendarEvent);
expect(result[0]).toBe("vcalendar");
const [vevent, vtimezone] = result[2];
expect(vevent[0]).toBe("vevent");
expect(vtimezone[0]).toBe("vtimezone");
// Should use UTC timezone as fallback
expect(vtimezone[2]).toEqual(
expect.arrayContaining([
["standard", [
["tzoffsetfrom", {}, "utc-offset", "+00:00"],
["tzoffsetto", {}, "utc-offset", "+00:00"],
["tzname", {}, "text", "UTC"],
["dtstart", {}, "date-time", "1970-01-01T00:00:00"]
], []]
])
);
});
it("handles valid timezone correctly", () => {
const mockEvent = {
uid: "event-valid-tz",
title: "Valid Timezone Event",
start: new Date("2025-07-23T10:00:00"),
end: new Date("2025-07-23T11:00:00"),
timezone: "Europe/Paris",
allday: false,
attendee: [],
};
const result = calendarEventToJCal(mockEvent as CalendarEvent);
expect(result[0]).toBe("vcalendar");
const [vevent, vtimezone] = result[2];
expect(vevent[0]).toBe("vevent");
expect(vtimezone[0]).toBe("vtimezone");
// Should use the specified timezone - check for Europe/Paris timezone components
expect(vtimezone[2]).toEqual(
expect.arrayContaining([
["daylight", [
["tzoffsetfrom", {}, "utc-offset", "+01:00"],
["tzoffsetto", {}, "utc-offset", "+02:00"],
["tzname", {}, "text", "CEST"],
["dtstart", {}, "date-time", "1970-03-29T02:00:00"],
["rrule", {}, "recur", { byday: "-1SU", bymonth: 3, freq: "YEARLY" }]
], []],
["standard", [
["tzoffsetfrom", {}, "utc-offset", "+02:00"],
["tzoffsetto", {}, "utc-offset", "+01:00"],
["tzname", {}, "text", "CET"],
["dtstart", {}, "date-time", "1970-10-25T03:00:00"],
["rrule", {}, "recur", { byday: "-1SU", bymonth: 10, freq: "YEARLY" }]
], []]
])
);
});
});