* [#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:
@@ -512,5 +512,72 @@ describe("CalendarSlice", () => {
|
||||
);
|
||||
expect(state.list.c1.events.e1.uid).toBe("e1");
|
||||
});
|
||||
|
||||
it("getEventAsync.fulfilled doesnt create new events when there are already event with base UID", () => {
|
||||
const baseUid = "recurring-event-base";
|
||||
const existingEvent = {
|
||||
uid: `${baseUid}/20240115`,
|
||||
title: "Existing Recurring Event",
|
||||
recurrenceId: null,
|
||||
} as unknown as CalendarEvent;
|
||||
|
||||
const newEventInstance = {
|
||||
uid: baseUid,
|
||||
title: "Fetched Master Event",
|
||||
recurrenceId: "20240115",
|
||||
} as CalendarEvent;
|
||||
|
||||
const stateWithEvent = {
|
||||
...initialState,
|
||||
list: {
|
||||
c1: {
|
||||
id: "c1",
|
||||
events: { [`${baseUid}/20240115`]: existingEvent },
|
||||
} as unknown as Calendar,
|
||||
},
|
||||
};
|
||||
|
||||
const payload = { calId: "c1", event: newEventInstance };
|
||||
const state = reducer(
|
||||
stateWithEvent,
|
||||
getEventAsync.fulfilled(payload, "req", newEventInstance)
|
||||
);
|
||||
|
||||
// Should still only have the base event, not the instance
|
||||
expect(Object.keys(state.list.c1.events)).toHaveLength(1);
|
||||
expect(state.list.c1.events[`${baseUid}/20240115`]).toBeDefined();
|
||||
expect(state.list.c1.events[baseUid]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("getEventAsync.fulfilled create new event when there isn't any event with base UID", () => {
|
||||
const eventUid = "new-event-uid";
|
||||
const newEvent = {
|
||||
uid: eventUid,
|
||||
title: "New Event",
|
||||
recurrenceId: null,
|
||||
} as unknown as CalendarEvent;
|
||||
|
||||
const stateWithoutEvent = {
|
||||
...initialState,
|
||||
list: {
|
||||
c1: {
|
||||
id: "c1",
|
||||
events: {},
|
||||
} as unknown as Calendar,
|
||||
},
|
||||
};
|
||||
|
||||
const payload = { calId: "c1", event: newEvent };
|
||||
const state = reducer(
|
||||
stateWithoutEvent,
|
||||
getEventAsync.fulfilled(payload, "req", newEvent)
|
||||
);
|
||||
|
||||
// Should create the new event
|
||||
expect(Object.keys(state.list.c1.events)).toHaveLength(1);
|
||||
expect(state.list.c1.events[eventUid]).toBeDefined();
|
||||
expect(state.list.c1.events[eventUid].uid).toBe(eventUid);
|
||||
expect(state.list.c1.events[eventUid].title).toBe("New Event");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -699,7 +699,6 @@ describe("Edit Recurring Event in Full Display", () => {
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
preview.debug();
|
||||
expect(screen.getByText("event.repeat.repeatEvery")).toBeInTheDocument();
|
||||
expect(screen.getByText("event.repeat.end.label")).toBeInTheDocument();
|
||||
|
||||
@@ -1191,7 +1190,6 @@ describe("Event URL handling for recurring events", () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 600));
|
||||
});
|
||||
|
||||
preview.debug();
|
||||
await waitFor(() => {
|
||||
expect(moveEventSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as eventUtils from "@/components/Event/utils/eventUtils";
|
||||
import * as CalendarApi from "@/features/Calendars/CalendarApi";
|
||||
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 { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
@@ -257,6 +257,9 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
||||
status: 201,
|
||||
url: `/calendars/${calId}/new-event.ics`,
|
||||
} as any);
|
||||
const mockGetMasterEvent = jest
|
||||
.spyOn(EventApi, "getEvent")
|
||||
.mockResolvedValue(masterEvent);
|
||||
jest.spyOn(CalendarApi, "getCalendar").mockResolvedValue({
|
||||
_embedded: {
|
||||
"dav:item": [],
|
||||
@@ -274,28 +277,37 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
||||
stateWithRecurringEvent
|
||||
);
|
||||
|
||||
// Wait for component to load
|
||||
// Wait for master event to be fetched AND form to be initialized
|
||||
await waitFor(() => {
|
||||
expect(screen.getByDisplayValue("Recurring Meeting")).toBeInTheDocument();
|
||||
expect(mockGetMasterEvent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Uncheck repeat checkbox
|
||||
const repeatCheckbox = screen.getByLabelText("event.form.repeat");
|
||||
expect(repeatCheckbox).toBeChecked();
|
||||
// Wait for the repeat checkbox to be checked (indicating form is initialized)
|
||||
const repeatCheckbox = await waitFor(() => {
|
||||
const checkbox = screen.getByLabelText("event.form.repeat");
|
||||
expect(checkbox).toBeChecked();
|
||||
return checkbox;
|
||||
});
|
||||
|
||||
// Now uncheck repeat checkbox
|
||||
await act(async () => {
|
||||
fireEvent.click(repeatCheckbox);
|
||||
});
|
||||
|
||||
expect(repeatCheckbox).not.toBeChecked();
|
||||
// Wait for checkbox to be unchecked
|
||||
await waitFor(() => {
|
||||
expect(repeatCheckbox).not.toBeChecked();
|
||||
});
|
||||
|
||||
// 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(mockPutEvent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Verify API calls - should delete all instances
|
||||
@@ -346,7 +358,7 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
attendee: [{ cn: "test", cal_address: "test@test.com" }],
|
||||
URL: `/calendars/${calId}/${baseUID}.ics`,
|
||||
};
|
||||
} as CalendarEvent;
|
||||
|
||||
const instance1 = {
|
||||
...masterEvent,
|
||||
@@ -369,17 +381,17 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
||||
},
|
||||
};
|
||||
|
||||
// Mock deleteEvent to fail
|
||||
jest
|
||||
// Mock deleteEvent to fail with a non-404 error
|
||||
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 mockGetMasterEvent = jest
|
||||
.spyOn(EventApi, "getEvent")
|
||||
.mockResolvedValue(masterEvent);
|
||||
|
||||
const { store } = renderWithProviders(
|
||||
<EventUpdateModal
|
||||
@@ -392,25 +404,47 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
||||
stateWithRecurringEvent
|
||||
);
|
||||
|
||||
// Wait for master event to be fetched
|
||||
await waitFor(() => {
|
||||
expect(mockGetMasterEvent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Wait for component to load
|
||||
await waitFor(() => {
|
||||
expect(screen.getByDisplayValue("Recurring Meeting")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Uncheck repeat checkbox and save
|
||||
const repeatCheckbox = screen.getByLabelText("event.form.repeat");
|
||||
// Wait for repeat checkbox to be checked
|
||||
const repeatCheckbox = await waitFor(() => {
|
||||
const checkbox = screen.getByLabelText("event.form.repeat");
|
||||
expect(checkbox).toBeChecked();
|
||||
return checkbox;
|
||||
});
|
||||
|
||||
// Uncheck repeat checkbox and save
|
||||
await act(async () => {
|
||||
fireEvent.click(repeatCheckbox);
|
||||
});
|
||||
|
||||
// Wait for checkbox to be unchecked
|
||||
await waitFor(() => {
|
||||
expect(repeatCheckbox).not.toBeChecked();
|
||||
});
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "actions.save" });
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(saveButton);
|
||||
await new Promise((resolve) => setTimeout(resolve, 600));
|
||||
});
|
||||
|
||||
// Wait for delete to be attempted
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(mockDeleteEvent).toHaveBeenCalled();
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
|
||||
// Verify error was logged for failed deletion
|
||||
await waitFor(
|
||||
() => {
|
||||
@@ -523,7 +557,6 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(saveButton);
|
||||
await new Promise((resolve) => setTimeout(resolve, 600));
|
||||
});
|
||||
|
||||
// Verify onCloseAll was called to close both preview and update modals
|
||||
|
||||
@@ -55,7 +55,7 @@ describe("parseCalendarEvent", () => {
|
||||
expect(result.transp).toBe("OPAQUE");
|
||||
expect(result.class).toBe("PUBLIC");
|
||||
expect(result.x_openpass_videoconference).toBe("https://meet.link");
|
||||
expect(result.timezone).toBe("Etc/UTC");
|
||||
expect(result.timezone).not.toBeDefined();
|
||||
|
||||
expect(result.organizer).toEqual({
|
||||
cn: "Alice",
|
||||
@@ -117,7 +117,7 @@ describe("parseCalendarEvent", () => {
|
||||
expect(result.transp).toBe("OPAQUE");
|
||||
expect(result.class).toBe("PUBLIC");
|
||||
expect(result.x_openpass_videoconference).toBe("https://meet.link");
|
||||
expect(result.timezone).toBe("Etc/UTC");
|
||||
expect(result.timezone).not.toBeDefined();
|
||||
|
||||
expect(result.organizer).toEqual({
|
||||
cn: "Alice",
|
||||
@@ -210,7 +210,6 @@ describe("parseCalendarEvent", () => {
|
||||
"/calendars/test.ics"
|
||||
);
|
||||
expect(result.end).toBeDefined();
|
||||
expect(result.timezone).toBeDefined();
|
||||
const endDate = new Date(result.end ?? "");
|
||||
const startDate = new Date(result.start);
|
||||
expect(endDate.getTime() - startDate.getTime()).toBe(60 * 60 * 1000);
|
||||
@@ -229,7 +228,6 @@ describe("parseCalendarEvent", () => {
|
||||
"/calendars/test.ics"
|
||||
);
|
||||
expect(result.end).toBeDefined();
|
||||
expect(result.timezone).toBeDefined();
|
||||
const endDate = new Date(result.end ?? "");
|
||||
const startDate = new Date(result.start);
|
||||
expect(endDate.getTime() - startDate.getTime()).toBe(30 * 60 * 1000);
|
||||
@@ -332,7 +330,6 @@ describe("parseCalendarEvent", () => {
|
||||
|
||||
expect(result.start).toBe("2025-07-18T09:00:00Z");
|
||||
expect(result.end).toBe("2025-07-18T10:00:00Z");
|
||||
expect(result.timezone).toBe("Etc/UTC");
|
||||
});
|
||||
|
||||
it("preserves all-day event dates without conversion", () => {
|
||||
@@ -356,15 +353,6 @@ describe("parseCalendarEvent", () => {
|
||||
});
|
||||
|
||||
describe("calendarEventToJCal", () => {
|
||||
beforeAll(() => {
|
||||
jest.mock("ical.js", () => ({
|
||||
Timezone: jest.fn().mockImplementation(({ component, tzid }) => ({
|
||||
component: {
|
||||
jCal: [`vtimezone`, [], [["tzid", {}, "text", tzid]]],
|
||||
},
|
||||
})),
|
||||
}));
|
||||
});
|
||||
it("should convert a CalendarEvent to JCal format", () => {
|
||||
const mockEvent = {
|
||||
uid: "event-123",
|
||||
@@ -1165,8 +1153,7 @@ describe("detectRecurringEventChanges", () => {
|
||||
end: "2025-10-15T10:00",
|
||||
},
|
||||
null,
|
||||
mockResolveTimezone,
|
||||
mockFormatDateTime
|
||||
mockResolveTimezone
|
||||
);
|
||||
|
||||
expect(result.timeChanged).toBe(true);
|
||||
@@ -1192,8 +1179,7 @@ describe("detectRecurringEventChanges", () => {
|
||||
end: "2025-10-15T08:00",
|
||||
},
|
||||
null,
|
||||
mockResolveTimezone,
|
||||
mockFormatDateTime
|
||||
mockResolveTimezone
|
||||
);
|
||||
|
||||
expect(result.timeChanged).toBe(false);
|
||||
@@ -1218,8 +1204,7 @@ describe("detectRecurringEventChanges", () => {
|
||||
end: "2025-10-15T08:00",
|
||||
},
|
||||
null,
|
||||
mockResolveTimezone,
|
||||
mockFormatDateTime
|
||||
mockResolveTimezone
|
||||
);
|
||||
|
||||
expect(result.timezoneChanged).toBe(true);
|
||||
@@ -1249,8 +1234,7 @@ describe("detectRecurringEventChanges", () => {
|
||||
end: "2025-10-15T08:00",
|
||||
},
|
||||
null,
|
||||
resolveWithAlias,
|
||||
mockFormatDateTime
|
||||
resolveWithAlias
|
||||
);
|
||||
|
||||
expect(result.timezoneChanged).toBe(false);
|
||||
@@ -1274,8 +1258,7 @@ describe("detectRecurringEventChanges", () => {
|
||||
end: "2025-10-15T08:00",
|
||||
},
|
||||
null,
|
||||
mockResolveTimezone,
|
||||
mockFormatDateTime
|
||||
mockResolveTimezone
|
||||
);
|
||||
|
||||
expect(result.repetitionRulesChanged).toBe(true);
|
||||
@@ -1299,8 +1282,7 @@ describe("detectRecurringEventChanges", () => {
|
||||
end: "2025-10-15T08:00",
|
||||
},
|
||||
null,
|
||||
mockResolveTimezone,
|
||||
mockFormatDateTime
|
||||
mockResolveTimezone
|
||||
);
|
||||
|
||||
expect(result.repetitionRulesChanged).toBe(true);
|
||||
@@ -1332,8 +1314,7 @@ describe("detectRecurringEventChanges", () => {
|
||||
end: "2025-10-15T08:00",
|
||||
},
|
||||
null,
|
||||
mockResolveTimezone,
|
||||
mockFormatDateTime
|
||||
mockResolveTimezone
|
||||
);
|
||||
|
||||
expect(result.repetitionRulesChanged).toBe(true);
|
||||
@@ -1357,8 +1338,7 @@ describe("detectRecurringEventChanges", () => {
|
||||
end: "2025-10-15T10:00",
|
||||
},
|
||||
null,
|
||||
mockResolveTimezone,
|
||||
mockFormatDateTime
|
||||
mockResolveTimezone
|
||||
);
|
||||
|
||||
expect(result.timeChanged).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user