[#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
This commit is contained in:
Camille Moussu
2026-02-04 15:01:13 +01:00
committed by GitHub
parent 7d8d6c7a45
commit 2d8196fc8e
9 changed files with 929 additions and 242 deletions
@@ -0,0 +1,522 @@
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(
<EventUpdateModal
open={true}
onClose={mockOnClose}
calId={calId}
eventId={`${baseUID}/20250116`} // User clicked on modified instance
typeOfAction="all"
/>,
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();
// 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(
<EventUpdateModal
open={true}
onClose={mockOnClose}
calId={calId}
eventId={baseUID} // Already master event
typeOfAction="all"
/>,
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(
<EventUpdateModal
open={true}
onClose={mockOnClose}
calId={calId}
eventId={`${baseUID}/20250115`}
typeOfAction="all"
/>,
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(
<EventUpdateModal
open={true}
onClose={mockOnClose}
calId={calId}
eventId={`${baseUID}/20250115`}
typeOfAction="all"
/>,
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(
<EventUpdateModal
open={true}
onClose={mockOnClose}
calId={calId}
eventId={`${baseUID}/20250115`}
typeOfAction="all"
/>,
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(
<EventUpdateModal
open={true}
onClose={mockOnClose}
calId={calId}
eventId={`${baseUID}/20250117`} // User clicked on Jan 17 instance
typeOfAction="all"
/>,
stateWithSeries
);
await waitFor(() => {
expect(screen.getByDisplayValue("Daily Standup")).toBeInTheDocument();
});
// Change time to 2:00 PM (14:00)
const input = screen.getByTestId("start-time-input");
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");
});
});
});
});