import * as CalendarApi from "@/features/Calendars/CalendarApi"; import * as eventThunks from "@/features/Calendars/services"; import * as EventApi from "@/features/Events/EventApi"; import { CalendarEvent } from "@/features/Events/EventsTypes"; import EventUpdateModal from "@/features/Events/EventUpdateModal"; import { act, fireEvent, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { renderWithProviders } from "../../utils/Renderwithproviders"; jest.mock("@/features/Events/EventApi"); jest.mock("@/features/Calendars/CalendarApi"); describe("EventUpdateModal - Recurring Event 'Edit All' Handling", () => { const mockOnClose = jest.fn(); beforeEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); sessionStorage.clear(); }); 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: {}, ownerEmails: ["test@test.com"], }, }, pending: false, }, }; describe("Master Event Display", () => { it("should fetch and display master event when editing 'all events' of a recurring series", async () => { // Given a recurring series with modified instances const masterEvent = { uid: baseUID, title: "Master Event Title", description: "Master Description", calId, start: "2025-01-15T10:00:00.000Z", end: "2025-01-15T11:00:00.000Z", repetition: { freq: "daily", interval: 1 }, allday: false, timezone: "America/New_York", organizer: { cn: "test", cal_address: "test@test.com" }, attendee: [{ cn: "test", cal_address: "test@test.com" }], URL: `/calendars/${calId}/${baseUID}.ics`, } as CalendarEvent; // Instance that was moved to different time const modifiedInstance = { ...masterEvent, uid: `${baseUID}/20250116`, title: "Modified Instance Title", // Different title start: "2025-01-16T14:00:00.000Z", // Different time (2PM instead of 10AM) end: "2025-01-16T15:00:00.000Z", }; const stateWithRecurringEvent = { ...preloadedState, calendars: { ...preloadedState.calendars, list: { [calId]: { ...preloadedState.calendars.list[calId], events: { [baseUID]: masterEvent, [`${baseUID}/20250116`]: modifiedInstance, }, }, }, }, }; // Mock getEvent to return master event const mockGetEvent = jest .spyOn(EventApi, "getEvent") .mockResolvedValue(masterEvent); renderWithProviders( , stateWithRecurringEvent ); // Wait for master event to be fetched await waitFor(() => { expect(mockGetEvent).toHaveBeenCalledWith( expect.objectContaining({ uid: baseUID, // Should fetch base UID, not instance }), true ); }); // Should display master event data, NOT modified instance data await waitFor(() => { const titleInput = screen.getByDisplayValue("Master Event Title"); expect(titleInput).toBeInTheDocument(); }); // Should NOT show modified instance title expect( screen.queryByDisplayValue("Modified Instance Title") ).not.toBeInTheDocument(); // Expand more options to show timezone (normal mode shows summary first) fireEvent.click( screen.getByRole("button", { name: "common.moreOptions" }) ); // Verify timezone dropdown shows master timezone await waitFor(() => { expect(screen.getByDisplayValue(/New York/i)).toBeDefined(); }); }); it("should use master event directly if clicked event is already the master", async () => { const masterEvent = { uid: baseUID, // No recurrence-id title: "Master Event", calId, start: "2025-01-15T10:00:00.000Z", end: "2025-01-15T11:00:00.000Z", repetition: { freq: "weekly", interval: 1 }, allday: false, organizer: { cn: "test", cal_address: "test@test.com" }, URL: `/calendars/${calId}/${baseUID}.ics`, }; const stateWithMaster = { ...preloadedState, calendars: { ...preloadedState.calendars, list: { [calId]: { ...preloadedState.calendars.list[calId], events: { [baseUID]: masterEvent, }, }, }, }, }; const mockGetEvent = jest.spyOn(EventApi, "getEvent"); renderWithProviders( , stateWithMaster ); await waitFor(() => { expect(screen.getByDisplayValue("Master Event")).toBeInTheDocument(); }); // Should NOT fetch from API since we already have master expect(mockGetEvent).not.toHaveBeenCalled(); }); it("should fallback to instance data if master event fetch fails", async () => { const instance = { uid: `${baseUID}/20250115`, title: "Instance Event", calId, start: "2025-01-15T10:00:00.000Z", end: "2025-01-15T11:00:00.000Z", repetition: { freq: "daily", interval: 1 }, allday: false, organizer: { cn: "test", cal_address: "test@test.com" }, URL: `/calendars/${calId}/${baseUID}.ics`, } as CalendarEvent; const stateWithInstance = { ...preloadedState, calendars: { ...preloadedState.calendars, list: { [calId]: { ...preloadedState.calendars.list[calId], events: { [`${baseUID}/20250115`]: instance, }, }, }, }, }; // Mock getEvent to fail jest .spyOn(EventApi, "getEvent") .mockRejectedValue(new Error("Network error")); const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(); renderWithProviders( , stateWithInstance ); // Should still display the instance data as fallback await waitFor(() => { expect(screen.getByDisplayValue("Instance Event")).toBeInTheDocument(); }); expect(consoleErrorSpy).toHaveBeenCalledWith( "Failed to fetch master event:", expect.any(Error) ); consoleErrorSpy.mockRestore(); }); }); describe("Update All Events - Using Base UID", () => { it("should use base UID (not instance UID) when updating series with property changes", async () => { const masterEvent = { uid: baseUID, title: "Weekly Meeting", description: "Original description", calId, start: "2025-01-15T10:00:00.000Z", end: "2025-01-15T11:00:00.000Z", repetition: { freq: "weekly", interval: 1 }, allday: false, organizer: { cn: "test", cal_address: "test@test.com" }, URL: `/calendars/${calId}/${baseUID}.ics`, } as CalendarEvent; const instance1 = { ...masterEvent, uid: `${baseUID}/20250115`, }; const instance2 = { ...masterEvent, uid: `${baseUID}/20250122`, start: "2025-01-22T10:00:00.000Z", end: "2025-01-22T11:00:00.000Z", }; const stateWithSeries = { ...preloadedState, calendars: { ...preloadedState.calendars, list: { [calId]: { ...preloadedState.calendars.list[calId], events: { [baseUID]: masterEvent, [`${baseUID}/20250115`]: instance1, [`${baseUID}/20250122`]: instance2, }, }, }, }, }; // Mock getEvent to return master jest.spyOn(EventApi, "getEvent").mockResolvedValue(masterEvent); const updateSeriesAsyncSpy = jest.spyOn(eventThunks, "updateSeriesAsync"); renderWithProviders( , stateWithSeries ); await waitFor(() => { expect(screen.getByDisplayValue("Weekly Meeting")).toBeInTheDocument(); }); // Change only the title (property change, not recurrence rules) const titleInput = screen.getByDisplayValue("Weekly Meeting"); fireEvent.change(titleInput, { target: { value: "Updated Meeting" } }); const saveButton = screen.getByRole("button", { name: "actions.save" }); await act(async () => { fireEvent.click(saveButton); }); // Verify updateSeriesAsync was dispatched with base UID await waitFor(() => { expect(updateSeriesAsyncSpy).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ uid: "recurring-event-base", recurrenceId: undefined, }), removeOverrides: false, }) ); }); }); it("should use base UID when updating series with recurrence rule changes", async () => { const masterEvent = { uid: baseUID, title: "Daily Standup", calId, start: "2025-01-15T09:00:00.000Z", end: "2025-01-15T09:30:00.000Z", repetition: { freq: "daily", interval: 1 }, // Daily allday: false, timezone: "America/New_York", organizer: { cn: "test", cal_address: "test@test.com" }, URL: `/calendars/${calId}/${baseUID}.ics`, } as CalendarEvent; const instance1 = { ...masterEvent, uid: `${baseUID}/20250115`, }; const instance2 = { ...masterEvent, uid: `${baseUID}/20250116`, start: "2025-01-16T09:00:00.000Z", end: "2025-01-16T09:30:00.000Z", }; const stateWithSeries = { ...preloadedState, calendars: { ...preloadedState.calendars, list: { [calId]: { ...preloadedState.calendars.list[calId], events: { [baseUID]: masterEvent, [`${baseUID}/20250115`]: instance1, [`${baseUID}/20250116`]: instance2, }, }, }, }, }; jest.spyOn(EventApi, "getEvent").mockResolvedValue(masterEvent); jest.spyOn(CalendarApi, "getCalendar").mockResolvedValue({ _embedded: { "dav:item": [] }, } as any); const { store } = renderWithProviders( , stateWithSeries ); await waitFor(() => { expect(screen.getByDisplayValue("Daily Standup")).toBeInTheDocument(); }); // Click More Options to access repeat settings const moreOptionsButton = screen.getByText("common.moreOptions"); fireEvent.click(moreOptionsButton); await waitFor(() => { const repeatCheckbox = screen.getByLabelText("event.form.repeat"); expect(repeatCheckbox).toBeInTheDocument(); expect(repeatCheckbox).toBeChecked(); }); // Change from daily to weekly const frequencySelect = screen.getByText("event.repeat.frequency.days"); fireEvent.mouseDown(frequencySelect); const weeklyOption = screen.getByRole("option", { name: "event.repeat.frequency.weeks", }); fireEvent.click(weeklyOption); const saveButton = screen.getByRole("button", { name: "actions.save" }); await act(async () => { fireEvent.click(saveButton); await new Promise((resolve) => setTimeout(resolve, 100)); }); // Verify all old instances were removed from store await waitFor(() => { const state = store.getState(); const calendar = state.calendars.list[calId]; expect(calendar.events[baseUID]).toBeUndefined(); expect(calendar.events[`${baseUID}/20250115`]).toBeUndefined(); expect(calendar.events[`${baseUID}/20250116`]).toBeUndefined(); }); }); }); describe("Master Event Date Preservation", () => { it("should preserve master event date when updating time in 'all events' mode", async () => { // Master event starts on Jan 15 at 10:00 AM const masterEvent = { uid: baseUID, title: "Daily Standup", calId, start: "2025-01-15T10:00:00.000Z", end: "2025-01-15T10:30:00.000Z", repetition: { freq: "daily", interval: 1 }, allday: false, timezone: "UTC", organizer: { cn: "test", cal_address: "test@test.com" }, URL: `/calendars/${calId}/${baseUID}.ics`, } as CalendarEvent; // Instance on Jan 17 (different date) const instance = { ...masterEvent, uid: `${baseUID}/20250117`, start: "2025-01-17T10:00:00.000Z", end: "2025-01-17T10:30:00.000Z", }; const stateWithSeries = { ...preloadedState, calendars: { ...preloadedState.calendars, list: { [calId]: { ...preloadedState.calendars.list[calId], events: { [baseUID]: masterEvent, [`${baseUID}/20250117`]: instance, }, }, }, }, }; jest.spyOn(EventApi, "getEvent").mockResolvedValue(masterEvent); jest .spyOn(EventApi, "putEvent") .mockResolvedValue({ status: 201 } as any); const updateSeriesAsyncSpy = jest.spyOn(eventThunks, "updateSeriesAsync"); renderWithProviders( , stateWithSeries ); await waitFor(() => { expect(screen.getByDisplayValue("Daily Standup")).toBeInTheDocument(); }); // Expand more options to show date/time inputs (normal mode shows DateTimeSummary first) fireEvent.click( screen.getByRole("button", { name: "common.moreOptions" }) ); const input = await waitFor(() => screen.getByTestId("start-time-input")); // Change time to 2:00 PM (14:00) await userEvent.click(input); await userEvent.clear(input); await userEvent.type(input, "14:00{enter}"); const saveButton = screen.getByRole("button", { name: "actions.save" }); await act(async () => { fireEvent.click(saveButton); await new Promise((resolve) => setTimeout(resolve, 100)); }); // Verify the API was called with master date (Jan 15) + new time (14:00) await waitFor(() => { expect(updateSeriesAsyncSpy).toHaveBeenCalled(); const callArgs = updateSeriesAsyncSpy.mock.calls[0][0]; const updatedEvent = callArgs.event; // Should preserve Jan 15 date (master), not Jan 17 (clicked instance) expect(updatedEvent.start).toContain("2025-01-15"); // But with new time 14:00 (in UTC, this is 14:00) expect(updatedEvent.start).toContain("14:00"); }); }); }); });