Files
workavia-calendar-front/__test__/features/Events/EventRepetition.test.tsx
T
Camille Moussu 2d8196fc8e [#490 & #489] Removed duplicated api call to get event data (#492)
* [#490 & #489] Removed duplicated api call to get event data

* [#490] removed arbitrary timezone fallback to UTC/ETC & updated exdate formating

* [#490] updated test and fixed eventdiff utils to properly handle dates
2026-02-04 15:01:13 +01:00

1203 lines
36 KiB
TypeScript

import { RootState } from "@/app/store";
import {
createEventHandlers,
EventHandlersProps,
} from "@/components/Calendar/handlers/eventHandlers";
import { EditModeDialog } from "@/components/Event/EditModeDialog";
import * as eventThunks from "@/features/Calendars/services";
import * as EventApi from "@/features/Events/EventApi";
import EventPreviewModal from "@/features/Events/EventDisplayPreview";
import EventUpdateModal from "@/features/Events/EventUpdateModal";
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
import preview from "jest-preview";
import { renderWithProviders } from "../../utils/Renderwithproviders";
jest.mock("@/components/Event/utils/eventUtils", () => {
const actual = jest.requireActual("@/components/Event/utils/eventUtils");
return {
...actual,
refreshCalendars: jest.fn(() => Promise.resolve()),
refreshSingularCalendar: jest.fn(() => Promise.resolve()),
};
});
const mockOnClose = jest.fn();
const day = new Date("2025-03-15T10:00:00Z");
const basePreloadedState = {
user: {
userData: {
sub: "test",
email: "test@test.com",
sid: "aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro",
openpaasId: "667037022b752d0026472254",
},
organiserData: {
cn: "test",
cal_address: "test@test.com",
},
},
calendars: {
list: {
"667037022b752d0026472254/cal1": {
id: "667037022b752d0026472254/cal1",
name: "Calendar",
color: "#FF0000",
events: {
"recurring-base/20250315T100000": {
uid: "recurring-base/20250315T100000",
title: "Recurring Event Instance",
calId: "667037022b752d0026472254/cal1",
start: day.toISOString(),
end: new Date("2025-03-15T11:00:00Z").toISOString(),
organizer: { cn: "test", cal_address: "test@test.com" },
recurrenceId: "20250315T100000",
repetition: { freq: "weekly", occurrences: 4, interval: 1 },
timezone: "UTC",
URL: "/calendars/667037022b752d0026472254/cal1/recurring-base.ics",
attendee: [
{
cn: "test",
cal_address: "test@test.com",
partstat: "NEEDS-ACTION",
rsvp: "TRUE",
role: "REQ-PARTICIPANT",
cutype: "INDIVIDUAL",
},
{
cn: "John",
cal_address: "john@test.com",
partstat: "NEEDS-ACTION",
rsvp: "TRUE",
role: "REQ-PARTICIPANT",
cutype: "INDIVIDUAL",
},
],
},
"recurring-base/20250322T100000": {
uid: "recurring-base/20250322T100000",
title: "Recurring Event Instance",
calId: "667037022b752d0026472254/cal1",
start: new Date("2025-03-22T10:00:00Z").toISOString(),
end: new Date("2025-03-22T11:00:00Z").toISOString(),
organizer: { cn: "test", cal_address: "test@test.com" },
recurrenceId: "20250322T100000",
timezone: "UTC",
URL: "/calendars/667037022b752d0026472254/cal1/recurring-base.ics",
attendee: [
{
cn: "test",
cal_address: "test@test.com",
partstat: "NEEDS-ACTION",
rsvp: "TRUE",
role: "REQ-PARTICIPANT",
cutype: "INDIVIDUAL",
},
],
},
},
ownerEmails: ["test@test.com"],
},
},
pending: false,
templist: {},
},
} as unknown as RootState;
describe("EditModeDialog Component", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("renders dialog when type is 'edit'", async () => {
const mockSetOpen = jest.fn();
const mockEventAction = jest.fn();
await act(async () =>
renderWithProviders(
<EditModeDialog
type="edit"
setOpen={mockSetOpen}
event={
basePreloadedState.calendars.list["667037022b752d0026472254/cal1"]
.events["recurring-base/20250315T100000"]
}
eventAction={mockEventAction}
/>,
basePreloadedState
)
);
expect(
screen.getByText("editModeDialog.updateRecurrentEvent")
).toBeInTheDocument();
expect(screen.getByText("editModeDialog.thisEvent")).toBeInTheDocument();
expect(screen.getByText("editModeDialog.allEvents")).toBeInTheDocument();
});
it("renders dialog when type is 'attendance'", async () => {
const mockSetOpen = jest.fn();
const mockEventAction = jest.fn();
await act(async () =>
renderWithProviders(
<EditModeDialog
type="attendance"
setOpen={mockSetOpen}
event={
basePreloadedState.calendars.list["667037022b752d0026472254/cal1"]
.events["recurring-base/20250315T100000"]
}
eventAction={mockEventAction}
/>,
basePreloadedState
)
);
expect(
screen.getByText("editModeDialog.updateParticipationStatus")
).toBeInTheDocument();
});
it("calls eventAction with 'solo' when solo option is selected and Ok clicked", async () => {
const mockSetOpen = jest.fn();
const mockEventAction = jest.fn();
await act(async () =>
renderWithProviders(
<EditModeDialog
type="edit"
setOpen={mockSetOpen}
event={
basePreloadedState.calendars.list["667037022b752d0026472254/cal1"]
.events["recurring-base/20250315T100000"]
}
eventAction={mockEventAction}
/>,
basePreloadedState
)
);
await act(async () => {
fireEvent.click(screen.getByLabelText("editModeDialog.thisEvent"));
fireEvent.click(screen.getByRole("button", { name: "common.ok" }));
});
await waitFor(() => {
expect(mockEventAction).toHaveBeenCalledWith("solo");
expect(mockSetOpen).toHaveBeenCalledWith(null);
});
});
it("calls eventAction with 'all' when all option is selected and Ok clicked", async () => {
const mockSetOpen = jest.fn();
const mockEventAction = jest.fn();
await act(async () =>
renderWithProviders(
<EditModeDialog
type="edit"
setOpen={mockSetOpen}
event={
basePreloadedState.calendars.list["667037022b752d0026472254/cal1"]
.events["recurring-base/20250315T100000"]
}
eventAction={mockEventAction}
/>,
basePreloadedState
)
);
fireEvent.click(screen.getByLabelText("editModeDialog.allEvents"));
fireEvent.click(screen.getByRole("button", { name: /Ok/i }));
await waitFor(() => {
expect(mockEventAction).toHaveBeenCalledWith("all");
expect(mockSetOpen).toHaveBeenCalledWith(null);
});
});
it("calls setOpen with null when Cancel is clicked", async () => {
const mockSetOpen = jest.fn();
const mockEventAction = jest.fn();
await act(async () =>
renderWithProviders(
<EditModeDialog
type="edit"
setOpen={mockSetOpen}
event={
basePreloadedState.calendars.list["667037022b752d0026472254/cal1"]
.events["recurring-base/20250315T100000"]
}
eventAction={mockEventAction}
/>,
basePreloadedState
)
);
fireEvent.click(screen.getByText("common.cancel"));
await waitFor(() => {
expect(mockSetOpen).toHaveBeenCalledWith(null);
expect(mockEventAction).not.toHaveBeenCalled();
});
});
});
describe("EventPreviewModal - Recurring Event Interactions", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("shows EditModeDialog when editing a recurring event", async () => {
jest.spyOn(eventThunks, "getEventAsync").mockImplementation((payload) => {
return () =>
Promise.resolve({
calId: payload.calId,
event:
basePreloadedState.calendars.list["667037022b752d0026472254/cal1"]
.events["recurring-base/20250315T100000"],
}) as any;
});
await act(async () =>
renderWithProviders(
<EventPreviewModal
open={true}
onClose={mockOnClose}
calId={"667037022b752d0026472254/cal1"}
eventId={"recurring-base/20250315T100000"}
/>,
basePreloadedState
)
);
fireEvent.click(screen.getByTestId("EditIcon"));
await waitFor(() => {
expect(
screen.getByText("editModeDialog.updateRecurrentEvent")
).toBeInTheDocument();
});
});
it("shows EditModeDialog when deleting a recurring event", async () => {
await act(async () =>
renderWithProviders(
<EventPreviewModal
open={true}
onClose={mockOnClose}
calId={"667037022b752d0026472254/cal1"}
eventId={"recurring-base/20250315T100000"}
/>,
basePreloadedState
)
);
fireEvent.click(screen.getByTestId("MoreVertIcon"));
fireEvent.click(
screen.getByRole("menuitem", { name: "eventPreview.deleteEvent" })
);
await waitFor(() => {
expect(
screen.getByText("editModeDialog.deleteRecurrentEvent")
).toBeInTheDocument();
});
});
it("shows EditModeDialog when RSVP to a recurring event", async () => {
await act(async () =>
renderWithProviders(
<EventPreviewModal
open={true}
onClose={mockOnClose}
calId={"667037022b752d0026472254/cal1"}
eventId={"recurring-base/20250315T100000"}
/>,
basePreloadedState
)
);
fireEvent.click(
screen.getByRole("button", { name: "eventPreview.ACCEPTED" })
);
await waitFor(() => {
expect(
screen.getByText("editModeDialog.updateParticipationStatus")
).toBeInTheDocument();
});
});
it("does not show EditModeDialog for non-recurring events", async () => {
const nonRecurringState = {
...basePreloadedState,
calendars: {
...basePreloadedState.calendars,
list: {
"667037022b752d0026472254/cal1": {
...basePreloadedState.calendars.list[
"667037022b752d0026472254/cal1"
],
events: {
"single-event": {
uid: "single-event",
title: "Single Event",
calId: "667037022b752d0026472254/cal1",
start: day.toISOString(),
end: new Date("2025-03-15T11:00:00Z").toISOString(),
organizer: { cn: "test", cal_address: "test@test.com" },
timezone: "UTC",
URL: "/calendars/667037022b752d0026472254/cal1/single-event.ics",
attendee: [],
},
},
},
},
},
};
const spy = jest
.spyOn(eventThunks, "deleteEventAsync")
.mockImplementation((payload) => {
return () => Promise.resolve(payload) as any;
});
await act(async () =>
renderWithProviders(
<EventPreviewModal
open={true}
onClose={mockOnClose}
calId={"667037022b752d0026472254/cal1"}
eventId={"single-event"}
/>,
nonRecurringState
)
);
fireEvent.click(screen.getByTestId("MoreVertIcon"));
fireEvent.click(
screen.getByRole("menuitem", { name: "eventPreview.deleteEvent" })
);
await waitFor(() => {
expect(spy).toHaveBeenCalled();
expect(mockOnClose).toHaveBeenCalled();
});
expect(
screen.queryByText("editModeDialog.deleteRecurrentEvent")
).not.toBeInTheDocument();
});
});
describe("Delete Recurring Event Instance", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("calls deleteEventInstanceAsync when deleting single instance", async () => {
const spy = jest
.spyOn(eventThunks, "deleteEventInstanceAsync")
.mockImplementation((payload) => {
return () => Promise.resolve(payload) as any;
});
await act(async () =>
renderWithProviders(
<EventPreviewModal
open={true}
onClose={mockOnClose}
calId={"667037022b752d0026472254/cal1"}
eventId={"recurring-base/20250315T100000"}
/>,
basePreloadedState
)
);
fireEvent.click(screen.getByTestId("MoreVertIcon"));
fireEvent.click(
screen.getByRole("menuitem", { name: "eventPreview.deleteEvent" })
);
await waitFor(() => {
expect(
screen.getByText("editModeDialog.deleteRecurrentEvent")
).toBeInTheDocument();
});
fireEvent.click(screen.getByLabelText("editModeDialog.thisEvent"));
fireEvent.click(screen.getByRole("button", { name: /Ok/i }));
await waitFor(() => {
expect(spy).toHaveBeenCalled();
});
const receivedPayload = spy.mock.calls[0][0];
expect(receivedPayload.event.uid).toBe("recurring-base/20250315T100000");
});
it("calls deleteEventAsync when deleting all instances", async () => {
jest.spyOn(EventApi, "getEvent").mockResolvedValue({
...basePreloadedState.calendars.list["667037022b752d0026472254/cal1"]
.events["recurring-base/20250315T100000"],
uid: "recurring-base",
} as any);
const spy = jest
.spyOn(eventThunks, "deleteEventAsync")
.mockImplementation((payload) => {
return () => Promise.resolve(payload) as any;
});
await act(async () =>
renderWithProviders(
<EventPreviewModal
open={true}
onClose={mockOnClose}
calId={"667037022b752d0026472254/cal1"}
eventId={"recurring-base/20250315T100000"}
/>,
basePreloadedState
)
);
fireEvent.click(screen.getByTestId("MoreVertIcon"));
fireEvent.click(
screen.getByRole("menuitem", { name: "eventPreview.deleteEvent" })
);
await waitFor(() => {
expect(
screen.getByText("editModeDialog.deleteRecurrentEvent")
).toBeInTheDocument();
});
fireEvent.click(screen.getByLabelText("editModeDialog.allEvents"));
fireEvent.click(screen.getByRole("button", { name: /Ok/i }));
await waitFor(() => {
expect(spy).toHaveBeenCalled();
});
const receivedPayload = spy.mock.calls[0][0];
expect(receivedPayload.eventId).toBe("recurring-base/20250315T100000");
});
});
describe("RSVP to Recurring Event", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("calls updateEventInstanceAsync when accepting single instance", async () => {
const spy = jest
.spyOn(eventThunks, "updateEventInstanceAsync")
.mockImplementation((payload) => {
const promise = Promise.resolve(payload);
(promise as any).unwrap = () => promise;
return () => promise as any;
});
await act(async () =>
renderWithProviders(
<EventPreviewModal
open={true}
onClose={mockOnClose}
calId={"667037022b752d0026472254/cal1"}
eventId={"recurring-base/20250315T100000"}
/>,
basePreloadedState
)
);
fireEvent.click(
screen.getByRole("button", { name: "eventPreview.ACCEPTED" })
);
await waitFor(() => {
expect(
screen.getByText("editModeDialog.updateParticipationStatus")
).toBeInTheDocument();
});
fireEvent.click(screen.getByLabelText("editModeDialog.thisEvent"));
fireEvent.click(screen.getByRole("button", { name: /Ok/i }));
await waitFor(() => {
expect(spy).toHaveBeenCalled();
});
const updatedEvent = spy.mock.calls[0][0].event;
expect(updatedEvent.attendee[0].partstat).toBe("ACCEPTED");
});
it("calls updateSeriesPartstat when accepting all instances", async () => {
const spy = jest
.spyOn(EventApi, "updateSeriesPartstat")
.mockResolvedValue({} as any);
await act(async () =>
renderWithProviders(
<EventPreviewModal
open={true}
onClose={mockOnClose}
calId={"667037022b752d0026472254/cal1"}
eventId={"recurring-base/20250315T100000"}
/>,
basePreloadedState
)
);
fireEvent.click(
screen.getByRole("button", { name: "eventPreview.ACCEPTED" })
);
await waitFor(() => {
expect(
screen.getByText("editModeDialog.updateParticipationStatus")
).toBeInTheDocument();
});
fireEvent.click(screen.getByLabelText("editModeDialog.allEvents"));
fireEvent.click(screen.getByRole("button", { name: "common.ok" }));
await waitFor(() => {
expect(spy).toHaveBeenCalled();
});
const callArgs = spy.mock.calls[0];
expect(callArgs[0].uid).toBe("recurring-base/20250315T100000");
expect(callArgs[1]).toBe("test@test.com");
expect(callArgs[2]).toBe("ACCEPTED");
});
});
describe("Edit Recurring Event in Full Display", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("renders event with recurrenceId in URL", async () => {
await act(async () =>
renderWithProviders(
<EventUpdateModal
open={true}
onClose={mockOnClose}
calId={"667037022b752d0026472254/cal1"}
eventId={"recurring-base/20250315T100000"}
/>,
basePreloadedState
)
);
expect(
screen.getByDisplayValue("Recurring Event Instance")
).toBeInTheDocument();
});
it("calls updateEventInstanceAsync when saving single instance with typeOfAction='solo'", async () => {
jest.spyOn(EventApi, "getEvent").mockResolvedValue({
...basePreloadedState.calendars.list["667037022b752d0026472254/cal1"]
.events["recurring-base/20250315T100000"],
} as any);
const spy = jest
.spyOn(eventThunks, "updateEventInstanceAsync")
.mockImplementation((payload) => {
return () => {
const promise = Promise.resolve();
(promise as any).unwrap = () => promise;
return promise as any;
};
});
await act(async () =>
renderWithProviders(
<EventUpdateModal
open={true}
onClose={mockOnClose}
calId={"667037022b752d0026472254/cal1"}
eventId={"recurring-base/20250315T100000"}
typeOfAction="solo"
/>,
basePreloadedState
)
);
const titleField = screen.getByLabelText("event.form.title");
fireEvent.change(titleField, {
target: { value: "Updated Single Instance" },
});
// Click Save
fireEvent.click(screen.getByText("actions.save"));
await waitFor(() => {
expect(spy).toHaveBeenCalled();
});
const updatedEvent = spy.mock.calls[0][0].event;
expect(updatedEvent.title).toBe("Updated Single Instance");
expect(updatedEvent.recurrenceId).toBe("20250315T100000");
});
it("calls updateSeriesAsync when saving all instances with typeOfAction='all'", async () => {
const getEventSpy = jest.spyOn(EventApi, "getEvent").mockResolvedValue({
...basePreloadedState.calendars.list["667037022b752d0026472254/cal1"]
.events["recurring-base/20250315T100000"],
} as any);
const spy = jest
.spyOn(eventThunks, "updateSeriesAsync")
.mockImplementation((payload) => {
return () => {
const promise = Promise.resolve();
(promise as any).unwrap = () => promise;
return promise as any;
};
});
await act(async () =>
renderWithProviders(
<EventUpdateModal
open={true}
onClose={mockOnClose}
calId={"667037022b752d0026472254/cal1"}
eventId={"recurring-base/20250315T100000"}
typeOfAction="all"
/>,
basePreloadedState
)
);
await waitFor(() => {
expect(getEventSpy).toHaveBeenCalled();
});
const titleField = screen.getByDisplayValue("Recurring Event Instance");
fireEvent.change(titleField, {
target: { value: "Updated All Instances" },
});
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
await waitFor(() => {
expect(spy).toHaveBeenCalled();
});
const updatedEvent = spy.mock.calls[0][0].event;
expect(updatedEvent.title).toBe("Updated All Instances");
});
it("disables repetition editing when typeOfAction='solo'", async () => {
await act(async () =>
renderWithProviders(
<EventUpdateModal
open={true}
onClose={mockOnClose}
calId={"667037022b752d0026472254/cal1"}
eventId={"recurring-base/20250315T100000"}
typeOfAction="solo"
eventData={
basePreloadedState.calendars.list["667037022b752d0026472254/cal1"]
.events["recurring-base/20250315T100000"]
}
/>,
basePreloadedState
)
);
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
expect(screen.getByText("event.repeat.repeatEvery")).toBeInTheDocument();
expect(screen.getByText("event.repeat.end.label")).toBeInTheDocument();
const frequencySelect = screen.getByRole("radio", {
name: /event.repeat.end.after \d event.repeat.end.occurrences/i,
});
expect(frequencySelect).toBeDisabled();
});
it("fetches master event data when typeOfAction='all'", async () => {
const getEventSpy = jest.spyOn(EventApi, "getEvent").mockResolvedValue({
...basePreloadedState.calendars.list["667037022b752d0026472254/cal1"]
.events["recurring-base/20250315T100000"],
uid: "recurring-base",
title: "Master Event Title",
description: "Master Description",
} as any);
await act(async () =>
renderWithProviders(
<EventUpdateModal
open={true}
onClose={mockOnClose}
calId={"667037022b752d0026472254/cal1"}
eventId={"recurring-base/20250315T100000"}
typeOfAction="all"
/>,
basePreloadedState
)
);
await waitFor(() => {
expect(getEventSpy).toHaveBeenCalled();
expect(
screen.getByDisplayValue("Master Event Title")
).toBeInTheDocument();
});
});
});
describe("Event Drag and Drop - Recurring Events", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("shows EditModeDialog when dragging recurring event", () => {
const mockDispatch = jest.fn();
const mockSetSelectedEvent = jest.fn();
const mockSetOpenEditModePopup = jest.fn();
const mockSetAfterChoiceFunc = jest.fn();
const eventHandlers = createEventHandlers({
setSelectedRange: jest.fn(),
setOpenEventModal: jest.fn(),
setTempEvent: jest.fn(),
setOpenEventDisplay: jest.fn(),
dispatch: mockDispatch,
calendarRange: { start: new Date(), end: new Date() },
setEventDisplayedId: jest.fn(),
setEventDisplayedCalId: jest.fn(),
setEventDisplayedTemp: jest.fn(),
calendars: basePreloadedState.calendars.list,
setSelectedEvent: mockSetSelectedEvent,
setAfterChoiceFunc: mockSetAfterChoiceFunc,
setOpenEditModePopup: mockSetOpenEditModePopup,
} as unknown as EventHandlersProps);
const mockArg = {
event: {
_def: {
extendedProps: {
uid: "recurring-base/20250315T100000",
calId: "667037022b752d0026472254/cal1",
},
},
},
delta: { years: 0, months: 0, days: 1, milliseconds: 0 },
};
eventHandlers.handleEventDrop(mockArg);
expect(mockSetSelectedEvent).toHaveBeenCalled();
expect(mockSetOpenEditModePopup).toHaveBeenCalledWith("edit");
expect(mockSetAfterChoiceFunc).toHaveBeenCalled();
});
});
describe("Event Resize - Recurring Events", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("shows EditModeDialog when resizing recurring event", () => {
const mockDispatch = jest.fn();
const mockSetSelectedEvent = jest.fn();
const mockSetOpenEditModePopup = jest.fn();
const mockSetAfterChoiceFunc = jest.fn();
const eventHandlers = createEventHandlers({
setSelectedRange: jest.fn(),
setOpenEventModal: jest.fn(),
setTempEvent: jest.fn(),
setOpenEventDisplay: jest.fn(),
dispatch: mockDispatch,
calendarRange: { start: new Date(), end: new Date() },
setEventDisplayedId: jest.fn(),
setEventDisplayedCalId: jest.fn(),
setEventDisplayedTemp: jest.fn(),
calendars: basePreloadedState.calendars.list,
setSelectedEvent: mockSetSelectedEvent,
setAfterChoiceFunc: mockSetAfterChoiceFunc,
setOpenEditModePopup: mockSetOpenEditModePopup,
} as unknown as EventHandlersProps);
const mockArg = {
event: {
_def: {
extendedProps: {
uid: "recurring-base/20250315T100000",
calId: "667037022b752d0026472254/cal1",
},
},
},
startDelta: { years: 0, months: 0, days: 0, milliseconds: 0 },
endDelta: { years: 0, months: 0, days: 0, milliseconds: 3600000 }, // 1 hour
};
eventHandlers.handleEventResize(mockArg);
expect(mockSetSelectedEvent).toHaveBeenCalled();
expect(mockSetOpenEditModePopup).toHaveBeenCalledWith("edit");
expect(mockSetAfterChoiceFunc).toHaveBeenCalled();
});
});
describe("RepeatEvent Component - Recurrence Editing", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("disables repetition fields when isOwn is false", async () => {
await act(async () =>
renderWithProviders(
<EventUpdateModal
open={true}
onClose={mockOnClose}
calId={"otherCal/cal"}
eventId={"recurring-base/20250315T100000"}
/>,
{
...basePreloadedState,
calendars: {
...basePreloadedState.calendars,
list: {
"otherCal/cal": {
id: "otherCal/cal",
name: "Other Calendar",
color: "#00FF00",
events: {
"recurring-base/20250315T100000": {
...basePreloadedState.calendars.list[
"667037022b752d0026472254/cal1"
].events["recurring-base/20250315T100000"],
calId: "otherCal/cal",
organizer: { cn: "other", cal_address: "other@test.com" },
},
},
},
},
},
}
)
);
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
// Repetition section should not be visible for non-owner
expect(
screen.queryByLabelText("event.form.repetition")
).not.toBeInTheDocument();
});
});
describe("handleRSVP function", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("calls putEventAsync for non-recurring events", async () => {
const mockDispatch = jest.fn();
const mockOnClose = jest.fn();
const {
handleRSVP,
} = require("@/components/Event/eventHandlers/eventHandlers");
jest.spyOn(eventThunks, "putEventAsync").mockImplementation((payload) => {
const promise = Promise.resolve(payload);
(promise as any).unwrap = () => promise;
return () => promise as any;
});
const nonRecurringEvent = {
uid: "single-event",
title: "Single Event",
calId: "667037022b752d0026472254/cal1",
start: day.toISOString(),
end: new Date("2025-03-15T11:00:00Z").toISOString(),
organizer: { cn: "test", cal_address: "test@test.com" },
attendee: [
{
cn: "test",
cal_address: "test@test.com",
partstat: "NEEDS-ACTION",
},
],
};
await handleRSVP(
mockDispatch,
basePreloadedState.calendars.list["667037022b752d0026472254/cal1"],
basePreloadedState.user,
nonRecurringEvent,
"ACCEPTED"
);
expect(mockDispatch).toHaveBeenCalled();
});
});
describe("handleDelete function", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("calls deleteEventAsync for non-recurring events", () => {
const mockDispatch = jest.fn();
const mockOnClose = jest.fn();
const {
handleDelete,
} = require("@/components/Event/eventHandlers/eventHandlers");
jest
.spyOn(eventThunks, "deleteEventAsync")
.mockImplementation((payload) => {
return () => Promise.resolve(payload) as any;
});
const nonRecurringEvent = {
uid: "single-event",
title: "Single Event",
calId: "667037022b752d0026472254/cal1",
URL: "/calendars/667037022b752d0026472254/cal1/single-event.ics",
};
handleDelete(
false, // isRecurring
undefined,
mockOnClose,
mockDispatch,
basePreloadedState.calendars.list["667037022b752d0026472254/cal1"],
nonRecurringEvent,
"667037022b752d0026472254/cal1",
"single-event"
);
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
expect(mockDispatch).toHaveBeenCalled();
});
it("calls deleteEventInstanceAsync when deleting solo recurring event", () => {
const mockDispatch = jest.fn();
const mockOnClose = jest.fn();
const {
handleDelete,
} = require("@/components/Event/eventHandlers/eventHandlers");
jest
.spyOn(eventThunks, "deleteEventInstanceAsync")
.mockImplementation((payload) => {
return () => Promise.resolve(payload) as any;
});
handleDelete(
true, // isRecurring
"solo",
mockOnClose,
mockDispatch,
basePreloadedState.calendars.list["667037022b752d0026472254/cal1"],
basePreloadedState.calendars.list["667037022b752d0026472254/cal1"].events[
"recurring-base/20250315T100000"
],
"667037022b752d0026472254/cal1",
"recurring-base/20250315T100000"
);
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
expect(mockDispatch).toHaveBeenCalled();
});
it("calls deleteEventAsync when deleting all recurring events", () => {
const mockDispatch = jest.fn();
const mockOnClose = jest.fn();
const {
handleDelete,
} = require("@/components/Event/eventHandlers/eventHandlers");
jest
.spyOn(eventThunks, "deleteEventAsync")
.mockImplementation((payload) => {
return () => Promise.resolve(payload) as any;
});
handleDelete(
true, // isRecurring
"all",
mockOnClose,
mockDispatch,
basePreloadedState.calendars.list["667037022b752d0026472254/cal1"],
basePreloadedState.calendars.list["667037022b752d0026472254/cal1"].events[
"recurring-base/20250315T100000"
],
"667037022b752d0026472254/cal1",
"recurring-base/20250315T100000"
);
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
expect(mockDispatch).toHaveBeenCalled();
});
});
describe("Calendar Integration - EditModeDialog Flow", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("passes correct eventId when editing all instances from preview", async () => {
await act(async () =>
renderWithProviders(
<EventPreviewModal
open={true}
onClose={mockOnClose}
calId={"667037022b752d0026472254/cal1"}
eventId={"recurring-base/20250315T100000"}
/>,
basePreloadedState
)
);
// Click edit
fireEvent.click(screen.getByTestId("EditIcon"));
await waitFor(() => {
expect(
screen.getByText("editModeDialog.updateRecurrentEvent")
).toBeInTheDocument();
});
// Select "All the events"
fireEvent.click(screen.getByLabelText("editModeDialog.allEvents"));
fireEvent.click(screen.getByRole("button", { name: /Ok/i }));
await waitFor(() => {
// Update modal should open
expect(screen.getByText("event.updateEvent")).toBeInTheDocument();
});
});
});
describe("Event URL handling for recurring events", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("uses base ID for event URL when moving recurring event", async () => {
const moveEventSpy = jest
.spyOn(eventThunks, "moveEventAsync")
.mockImplementation((payload) => {
const result = { calId: payload.cal.id, events: [] };
const promise = Promise.resolve(result);
(promise as any).unwrap = () => promise;
return () => promise as any;
});
const twoCalState = {
user: {
userData: {
sub: "test",
email: "test@test.com",
sid: "aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro",
openpaasId: "667037022b752d0026472254",
},
organiserData: {
cn: "test",
cal_address: "test@test.com",
},
},
calendars: {
list: {
"667037022b752d0026472254/cal1": {
id: "667037022b752d0026472254/cal1",
name: "Calendar",
color: "#FF0000",
events: {
"recurring-base/20250315T100000": {
uid: "recurring-base/20250315T100000",
title: "Recurring Event Instance",
calId: "667037022b752d0026472254/cal1",
start: day.toISOString(),
end: new Date("2025-03-15T11:00:00Z").toISOString(),
organizer: { cn: "test", cal_address: "test@test.com" },
recurrenceId: "20250315T100000",
repetition: { freq: "weekly", occurrences: 4, interval: 1 },
timezone: "UTC",
URL: "/calendars/667037022b752d0026472254/cal1/recurring-base.ics",
attendee: [
{
cn: "test",
cal_address: "test@test.com",
partstat: "NEEDS-ACTION",
rsvp: "TRUE",
role: "REQ-PARTICIPANT",
cutype: "INDIVIDUAL",
},
{
cn: "John",
cal_address: "john@test.com",
partstat: "NEEDS-ACTION",
rsvp: "TRUE",
role: "REQ-PARTICIPANT",
cutype: "INDIVIDUAL",
},
],
},
"recurring-base/20250322T100000": {
uid: "recurring-base/20250322T100000",
title: "Recurring Event Instance",
calId: "667037022b752d0026472254/cal1",
start: new Date("2025-03-22T10:00:00Z").toISOString(),
end: new Date("2025-03-22T11:00:00Z").toISOString(),
organizer: { cn: "test", cal_address: "test@test.com" },
recurrenceId: "20250322T100000",
timezone: "UTC",
URL: "/calendars/667037022b752d0026472254/cal1/recurring-base.ics",
attendee: [
{
cn: "test",
cal_address: "test@test.com",
partstat: "NEEDS-ACTION",
rsvp: "TRUE",
role: "REQ-PARTICIPANT",
cutype: "INDIVIDUAL",
},
],
},
},
ownerEmails: ["test@test.com"],
},
"667037022b752d0026472254/cal2": {
id: "667037022b752d0026472254/cal2",
name: "Calendar 2",
color: "#00FF00",
events: {},
},
},
pending: false,
templist: {},
},
};
await act(async () =>
renderWithProviders(
<EventUpdateModal
open={true}
onClose={mockOnClose}
calId={"667037022b752d0026472254/cal1"}
eventId={"recurring-base/20250315T100000"}
/>,
twoCalState
)
);
// Change calendar
fireEvent.mouseDown(screen.getByLabelText("event.form.calendar"));
const option = await screen.findByText("Calendar 2");
fireEvent.click(option);
// Click Save button
const saveButton = screen.getByRole("button", { name: "actions.save" });
await act(async () => {
fireEvent.click(saveButton);
// Wait for the 500ms delay in the code
await new Promise((resolve) => setTimeout(resolve, 600));
});
await waitFor(() => {
expect(moveEventSpy).toHaveBeenCalled();
});
const movePayload = moveEventSpy.mock.calls[0][0];
// Should use base ID, not full uid with recurrence ID
expect(movePayload.newURL).toContain("recurring-base.ics");
expect(movePayload.newURL).not.toContain("/20250315T100000");
});
});