test: add non-regression tests for recurring to non-recurring conversion
- Add comprehensive integration test for happy path conversion flow - Test verifies deleteEvent API called with correct URL - Test verifies new event created without repetition and with new UID - Test verifies store cleanup removes all old instances - Test verifies refreshCalendars called after conversion - Add test for error handling when API deletion fails - Add test to verify calendar refresh mechanism
This commit is contained in:
committed by
Benoit TELLIER
parent
2932607afb
commit
757060946f
@@ -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 { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||||
import EventUpdateModal from "../../../src/features/Events/EventUpdateModal";
|
import EventUpdateModal from "../../../src/features/Events/EventUpdateModal";
|
||||||
import * as EventApi from "../../../src/features/Events/EventApi";
|
import * as EventApi from "../../../src/features/Events/EventApi";
|
||||||
|
import * as eventUtils from "../../../src/components/Event/utils/eventUtils";
|
||||||
|
|
||||||
jest.mock("../../../src/features/Events/EventApi");
|
jest.mock("../../../src/features/Events/EventApi");
|
||||||
|
|
||||||
@@ -151,3 +152,357 @@ describe("EventUpdateModal Timezone Handling", () => {
|
|||||||
expect(titleInput).toHaveValue("Updated Event");
|
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(
|
||||||
|
<EventUpdateModal
|
||||||
|
open={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
calId={calId}
|
||||||
|
eventId={`${baseUID}/20250115`}
|
||||||
|
typeOfAction="all"
|
||||||
|
/>,
|
||||||
|
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(
|
||||||
|
<EventUpdateModal
|
||||||
|
open={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
calId={calId}
|
||||||
|
eventId={`${baseUID}/20250115`}
|
||||||
|
typeOfAction="all"
|
||||||
|
/>,
|
||||||
|
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(
|
||||||
|
<EventUpdateModal
|
||||||
|
open={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
calId={calId}
|
||||||
|
eventId={`${baseUID}/20250115`}
|
||||||
|
typeOfAction="all"
|
||||||
|
/>,
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user