diff --git a/__test__/features/Events/EventUpdateModal.test.tsx b/__test__/features/Events/EventUpdateModal.test.tsx
index 3e7ebca..10add7e 100644
--- a/__test__/features/Events/EventUpdateModal.test.tsx
+++ b/__test__/features/Events/EventUpdateModal.test.tsx
@@ -1,7 +1,8 @@
-import { screen, fireEvent, waitFor } from "@testing-library/react";
+import { screen, fireEvent, waitFor, act } from "@testing-library/react";
import { renderWithProviders } from "../../utils/Renderwithproviders";
import EventUpdateModal from "../../../src/features/Events/EventUpdateModal";
import * as EventApi from "../../../src/features/Events/EventApi";
+import * as eventUtils from "../../../src/components/Event/utils/eventUtils";
jest.mock("../../../src/features/Events/EventApi");
@@ -151,3 +152,357 @@ describe("EventUpdateModal Timezone Handling", () => {
expect(titleInput).toHaveValue("Updated Event");
});
});
+
+describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
+ const mockOnClose = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.restoreAllMocks();
+ });
+
+ const baseUID = "recurring-event-base";
+ const calId = "667037022b752d0026472254/cal1";
+
+ const preloadedState = {
+ user: {
+ userData: {
+ sub: "test",
+ email: "test@test.com",
+ sid: "test-sid",
+ openpaasId: "667037022b752d0026472254",
+ },
+ organiserData: {
+ cn: "test",
+ cal_address: "test@test.com",
+ },
+ },
+ calendars: {
+ list: {
+ [calId]: {
+ id: calId,
+ name: "Test Calendar",
+ color: "#FF0000",
+ events: {},
+ },
+ },
+ pending: false,
+ },
+ };
+
+ it("converts recurring event to non-recurring when repeat is disabled in edit all mode", async () => {
+ // Create recurring event with multiple instances
+ const eventDate = new Date("2025-01-15T10:00:00.000Z");
+
+ const masterEvent = {
+ uid: baseUID,
+ title: "Recurring Meeting",
+ calId,
+ start: eventDate.toISOString(),
+ end: new Date(eventDate.getTime() + 3600000).toISOString(),
+ repetition: { freq: "daily", interval: 1 },
+ allday: false,
+ organizer: { cn: "test", cal_address: "test@test.com" },
+ attendee: [{ cn: "test", cal_address: "test@test.com" }],
+ URL: `/calendars/${calId}/${baseUID}.ics`,
+ };
+
+ const instance1 = {
+ ...masterEvent,
+ uid: `${baseUID}/20250115`,
+ start: "2025-01-15T10:00:00.000Z",
+ end: "2025-01-15T11:00:00.000Z",
+ };
+
+ const instance2 = {
+ ...masterEvent,
+ uid: `${baseUID}/20250116`,
+ start: "2025-01-16T10:00:00.000Z",
+ end: "2025-01-16T11:00:00.000Z",
+ };
+
+ const instance3 = {
+ ...masterEvent,
+ uid: `${baseUID}/20250117`,
+ start: "2025-01-17T10:00:00.000Z",
+ end: "2025-01-17T11:00:00.000Z",
+ };
+
+ const stateWithRecurringEvent = {
+ ...preloadedState,
+ calendars: {
+ ...preloadedState.calendars,
+ list: {
+ [calId]: {
+ ...preloadedState.calendars.list[calId],
+ events: {
+ [baseUID]: masterEvent,
+ [`${baseUID}/20250115`]: instance1,
+ [`${baseUID}/20250116`]: instance2,
+ [`${baseUID}/20250117`]: instance3,
+ },
+ },
+ },
+ },
+ };
+
+ // Mock API calls
+ const mockDeleteEvent = jest.spyOn(EventApi, 'deleteEvent').mockResolvedValue({} as any);
+ const mockPutEvent = jest.spyOn(EventApi, 'putEvent').mockResolvedValue({ status: 201, url: `/calendars/${calId}/new-event.ics` } as any);
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
+
+ // Mock refreshCalendars
+ const mockRefreshCalendars = jest.spyOn(eventUtils, 'refreshCalendars').mockResolvedValue(undefined);
+
+ const { store } = renderWithProviders(
+ ,
+ stateWithRecurringEvent
+ );
+
+ // Wait for component to load
+ await waitFor(() => {
+ expect(screen.getByDisplayValue("Recurring Meeting")).toBeInTheDocument();
+ });
+
+ // Uncheck repeat checkbox
+ const repeatCheckbox = screen.getByLabelText("Repeat");
+ expect(repeatCheckbox).toBeChecked();
+
+ await act(async () => {
+ fireEvent.click(repeatCheckbox);
+ });
+
+ expect(repeatCheckbox).not.toBeChecked();
+
+ // Click Save button
+ const saveButton = screen.getByText("Save");
+
+ await act(async () => {
+ fireEvent.click(saveButton);
+ // Wait for the 500ms delay in the code
+ await new Promise(resolve => setTimeout(resolve, 600));
+ });
+
+ // Verify API calls
+ await waitFor(() => {
+ expect(mockDeleteEvent).toHaveBeenCalledWith(`/calendars/${calId}/${baseUID}.ics`);
+ }, { timeout: 3000 });
+
+ expect(mockPutEvent).toHaveBeenCalled();
+ const putEventCall = mockPutEvent.mock.calls[0][0];
+ expect(putEventCall.title).toBe("Recurring Meeting");
+ expect(putEventCall.repetition?.freq).toBeFalsy();
+ expect(putEventCall.uid).not.toContain(baseUID);
+
+ // Verify console.log for successful deletion
+ expect(consoleLogSpy).toHaveBeenCalledWith(
+ "Deleted master event via direct API call:",
+ expect.stringContaining(baseUID)
+ );
+
+ // Verify Redux store state changes
+ const finalState = store.getState();
+ const calendar = finalState.calendars.list[calId];
+
+ // Old recurring instances should be removed from store
+ expect(calendar.events[baseUID]).toBeUndefined();
+ expect(calendar.events[`${baseUID}/20250115`]).toBeUndefined();
+ expect(calendar.events[`${baseUID}/20250116`]).toBeUndefined();
+ expect(calendar.events[`${baseUID}/20250117`]).toBeUndefined();
+
+ // Verify refreshCalendars was called
+ expect(mockRefreshCalendars).toHaveBeenCalled();
+
+ consoleLogSpy.mockRestore();
+ });
+
+ it("continues creating new event even if deletion of old series fails", async () => {
+ const eventDate = new Date("2025-01-15T10:00:00.000Z");
+
+ const masterEvent = {
+ uid: baseUID,
+ title: "Recurring Meeting",
+ calId,
+ start: eventDate.toISOString(),
+ end: new Date(eventDate.getTime() + 3600000).toISOString(),
+ repetition: { freq: "weekly", interval: 1 },
+ allday: false,
+ organizer: { cn: "test", cal_address: "test@test.com" },
+ attendee: [{ cn: "test", cal_address: "test@test.com" }],
+ URL: `/calendars/${calId}/${baseUID}.ics`,
+ };
+
+ const instance1 = {
+ ...masterEvent,
+ uid: `${baseUID}/20250115`,
+ };
+
+ const stateWithRecurringEvent = {
+ ...preloadedState,
+ calendars: {
+ ...preloadedState.calendars,
+ list: {
+ [calId]: {
+ ...preloadedState.calendars.list[calId],
+ events: {
+ [baseUID]: masterEvent,
+ [`${baseUID}/20250115`]: instance1,
+ },
+ },
+ },
+ },
+ };
+
+ // Mock deleteEvent to fail
+ const mockDeleteEvent = jest.spyOn(EventApi, 'deleteEvent')
+ .mockRejectedValue(new Error("Network error"));
+ const mockPutEvent = jest.spyOn(EventApi, 'putEvent')
+ .mockResolvedValue({ status: 201 } as any);
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
+
+ // Mock refreshCalendars
+ jest.spyOn(eventUtils, 'refreshCalendars').mockResolvedValue(undefined);
+
+ const { store } = renderWithProviders(
+ ,
+ stateWithRecurringEvent
+ );
+
+ // Wait for component to load
+ await waitFor(() => {
+ expect(screen.getByDisplayValue("Recurring Meeting")).toBeInTheDocument();
+ });
+
+ // Uncheck repeat checkbox and save
+ const repeatCheckbox = screen.getByLabelText("Repeat");
+
+ await act(async () => {
+ fireEvent.click(repeatCheckbox);
+ });
+
+ const saveButton = screen.getByText("Save");
+
+ await act(async () => {
+ fireEvent.click(saveButton);
+ await new Promise(resolve => setTimeout(resolve, 600));
+ });
+
+ // Verify error was logged
+ await waitFor(() => {
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
+ "Failed to delete recurring event:",
+ expect.any(Error)
+ );
+ }, { timeout: 3000 });
+
+ // Verify new event was still created despite deletion failure
+ expect(mockPutEvent).toHaveBeenCalled();
+ const putEventCall = mockPutEvent.mock.calls[0][0];
+ expect(putEventCall.title).toBe("Recurring Meeting");
+ expect(putEventCall.repetition?.freq).toBeFalsy();
+ expect(putEventCall.uid).not.toContain(baseUID);
+
+ // When API deletion fails, the error is caught and logged
+ // The code continues to create the new event (graceful degradation)
+ // Old events remain in store since API deletion failed
+ // They will be cleaned up when calendar refreshes from server
+
+ consoleErrorSpy.mockRestore();
+ });
+
+ it("refreshes calendar after converting recurring to non-recurring", async () => {
+ const eventDate = new Date("2025-01-15T10:00:00.000Z");
+
+ const masterEvent = {
+ uid: baseUID,
+ title: "Recurring Meeting",
+ calId,
+ start: eventDate.toISOString(),
+ end: new Date(eventDate.getTime() + 3600000).toISOString(),
+ repetition: { freq: "monthly", interval: 1 },
+ allday: false,
+ organizer: { cn: "test", cal_address: "test@test.com" },
+ attendee: [{ cn: "test", cal_address: "test@test.com" }],
+ URL: `/calendars/${calId}/${baseUID}.ics`,
+ };
+
+ const instance1 = {
+ ...masterEvent,
+ uid: `${baseUID}/20250115`,
+ };
+
+ const stateWithRecurringEvent = {
+ ...preloadedState,
+ calendars: {
+ ...preloadedState.calendars,
+ list: {
+ [calId]: {
+ ...preloadedState.calendars.list[calId],
+ events: {
+ [baseUID]: masterEvent,
+ [`${baseUID}/20250115`]: instance1,
+ },
+ },
+ },
+ },
+ };
+
+ // Mock API calls
+ jest.spyOn(EventApi, 'deleteEvent').mockResolvedValue({} as any);
+ jest.spyOn(EventApi, 'putEvent').mockResolvedValue({ status: 201 } as any);
+
+ // Mock refreshCalendars to track calls
+ const mockRefreshCalendars = jest.spyOn(eventUtils, 'refreshCalendars').mockResolvedValue(undefined);
+
+ renderWithProviders(
+ ,
+ stateWithRecurringEvent
+ );
+
+ // Wait for component to load
+ await waitFor(() => {
+ expect(screen.getByDisplayValue("Recurring Meeting")).toBeInTheDocument();
+ });
+
+ // Uncheck repeat and save
+ const repeatCheckbox = screen.getByLabelText("Repeat");
+
+ await act(async () => {
+ fireEvent.click(repeatCheckbox);
+ });
+
+ const saveButton = screen.getByText("Save");
+
+ await act(async () => {
+ fireEvent.click(saveButton);
+ await new Promise(resolve => setTimeout(resolve, 600));
+ });
+
+ // Verify refreshCalendars was called
+ await waitFor(() => {
+ expect(mockRefreshCalendars).toHaveBeenCalledWith(
+ expect.any(Function), // dispatch
+ expect.any(Array), // calendars list
+ expect.any(Object) // calendar range
+ );
+ }, { timeout: 3000 });
+ });
+});