Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -28,8 +28,6 @@ describe("CalendarApp integration", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
const dispatch = jest.fn() as AppDispatch;
|
|
||||||
jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(dispatch);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderCalendar = () => {
|
const renderCalendar = () => {
|
||||||
@@ -89,7 +87,8 @@ describe("CalendarApp integration", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it("renders the event on the calendar and calendarRef works", async () => {
|
it("renders the event on the calendar and calendarRef works", async () => {
|
||||||
const dispatch = appHooks.useAppDispatch();
|
const dispatch = jest.fn() as AppDispatch;
|
||||||
|
jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(dispatch);
|
||||||
|
|
||||||
renderCalendar();
|
renderCalendar();
|
||||||
const calendarRef: React.RefObject<CalendarApi | null> = (window as any)
|
const calendarRef: React.RefObject<CalendarApi | null> = (window as any)
|
||||||
@@ -334,7 +333,13 @@ describe("CalendarApp integration", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it("keeps all attendees event participation on title update", async () => {
|
it("keeps all attendees event participation on title update", async () => {
|
||||||
const updateSpy = jest.spyOn(eventThunks, "putEventAsync");
|
const updateSpy = jest
|
||||||
|
.spyOn(eventThunks, "putEventAsync")
|
||||||
|
.mockImplementation((payload) => {
|
||||||
|
const promise = Promise.resolve(payload);
|
||||||
|
(promise as any).unwrap = () => promise;
|
||||||
|
return () => promise as any;
|
||||||
|
});
|
||||||
const onClose = jest.fn();
|
const onClose = jest.fn();
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventUpdateModal
|
<EventUpdateModal
|
||||||
@@ -387,7 +392,13 @@ describe("CalendarApp integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("changes normal attendee to need action on time update and no organizer changes", async () => {
|
it("changes normal attendee to need action on time update and no organizer changes", async () => {
|
||||||
const updateSpy = jest.spyOn(eventThunks, "putEventAsync");
|
const updateSpy = jest
|
||||||
|
.spyOn(eventThunks, "putEventAsync")
|
||||||
|
.mockImplementation((payload) => {
|
||||||
|
const promise = Promise.resolve(payload);
|
||||||
|
(promise as any).unwrap = () => promise;
|
||||||
|
return () => promise as any;
|
||||||
|
});
|
||||||
const onClose = jest.fn();
|
const onClose = jest.fn();
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventUpdateModal
|
<EventUpdateModal
|
||||||
@@ -450,9 +461,22 @@ describe("CalendarApp integration", () => {
|
|||||||
// Verify SEQUENCE is incremented
|
// Verify SEQUENCE is incremented
|
||||||
expect(updatedEvent?.sequence).toBe(3); // 2 + 1
|
expect(updatedEvent?.sequence).toBe(3); // 2 + 1
|
||||||
});
|
});
|
||||||
|
|
||||||
it("update event attendees on drag", async () => {
|
it("update event attendees on drag", async () => {
|
||||||
|
// Mock dispatch locally — this test calls createEventHandlers directly
|
||||||
|
// and does not go through the Redux store or useAppDispatch.
|
||||||
const mockDispatch = jest.fn();
|
const mockDispatch = jest.fn();
|
||||||
const updateSpy = jest.spyOn(eventThunks, "putEventAsync");
|
jest
|
||||||
|
.spyOn(appHooks, "useAppDispatch")
|
||||||
|
.mockReturnValue(mockDispatch as unknown as AppDispatch);
|
||||||
|
|
||||||
|
const updateSpy = jest
|
||||||
|
.spyOn(eventThunks, "putEventAsync")
|
||||||
|
.mockImplementation((payload) => {
|
||||||
|
const promise = Promise.resolve(payload);
|
||||||
|
(promise as any).unwrap = () => promise;
|
||||||
|
return () => promise as any;
|
||||||
|
});
|
||||||
|
|
||||||
const eventHandlers = createEventHandlers({
|
const eventHandlers = createEventHandlers({
|
||||||
setSelectedRange: jest.fn(),
|
setSelectedRange: jest.fn(),
|
||||||
@@ -513,8 +537,20 @@ describe("CalendarApp integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("update event attendees on resize", async () => {
|
it("update event attendees on resize", async () => {
|
||||||
|
// Mock dispatch locally — this test calls createEventHandlers directly
|
||||||
|
// and does not go through the Redux store or useAppDispatch.
|
||||||
const mockDispatch = jest.fn();
|
const mockDispatch = jest.fn();
|
||||||
const updateSpy = jest.spyOn(eventThunks, "putEventAsync");
|
jest
|
||||||
|
.spyOn(appHooks, "useAppDispatch")
|
||||||
|
.mockReturnValue(mockDispatch as unknown as AppDispatch);
|
||||||
|
|
||||||
|
const updateSpy = jest
|
||||||
|
.spyOn(eventThunks, "putEventAsync")
|
||||||
|
.mockImplementation((payload) => {
|
||||||
|
const promise = Promise.resolve(payload);
|
||||||
|
(promise as any).unwrap = () => promise;
|
||||||
|
return () => promise as any;
|
||||||
|
});
|
||||||
|
|
||||||
const eventHandlers = createEventHandlers({
|
const eventHandlers = createEventHandlers({
|
||||||
setSelectedRange: jest.fn(),
|
setSelectedRange: jest.fn(),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import * as appHooks from "@/app/hooks";
|
import * as appHooks from "@/app/hooks";
|
||||||
import { AppDispatch } from "@/app/store";
|
import { AppDispatch } from "@/app/store";
|
||||||
import { InfoRow } from "@/components/Event/InfoRow";
|
import { InfoRow } from "@/components/Event/InfoRow";
|
||||||
import { LONG_DATE_FORMAT } from "@/components/Event/utils/dateTimeFormatters";
|
|
||||||
import {
|
import {
|
||||||
stringAvatar,
|
stringAvatar,
|
||||||
stringToColor,
|
stringToColor,
|
||||||
@@ -9,15 +8,7 @@ import {
|
|||||||
import { DelegationAccess } from "@/features/Calendars/CalendarTypes";
|
import { DelegationAccess } from "@/features/Calendars/CalendarTypes";
|
||||||
import * as eventThunks from "@/features/Calendars/services";
|
import * as eventThunks from "@/features/Calendars/services";
|
||||||
import EventPreviewModal from "@/features/Events/EventDisplayPreview";
|
import EventPreviewModal from "@/features/Events/EventDisplayPreview";
|
||||||
import EventUpdateModal from "@/features/Events/EventUpdateModal";
|
import { cleanup, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||||
import {
|
|
||||||
act,
|
|
||||||
cleanup,
|
|
||||||
fireEvent,
|
|
||||||
screen,
|
|
||||||
waitFor,
|
|
||||||
} from "@testing-library/react";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||||
|
|
||||||
describe("Event Preview Display", () => {
|
describe("Event Preview Display", () => {
|
||||||
@@ -1752,422 +1743,6 @@ describe("Event Preview Display", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Event Full Display", () => {
|
|
||||||
const mockOnClose = jest.fn();
|
|
||||||
const day = new Date("2025-01-15T10:00:00.000Z"); // Fixed date in UTC: Jan 15, 2025 10:00 AM UTC
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
const preloadedState = {
|
|
||||||
user: {
|
|
||||||
userData: {
|
|
||||||
sub: "test",
|
|
||||||
email: "test@test.com",
|
|
||||||
sid: "aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro",
|
|
||||||
openpaasId: "667037022b752d0026472254",
|
|
||||||
},
|
|
||||||
organiserData: {
|
|
||||||
cn: "test",
|
|
||||||
cal_address: "mailto:test@test.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
calendars: {
|
|
||||||
list: {
|
|
||||||
"667037022b752d0026472254/cal1": {
|
|
||||||
id: "667037022b752d0026472254/cal1",
|
|
||||||
name: "First Calendar",
|
|
||||||
color: "#FF0000",
|
|
||||||
events: {
|
|
||||||
event1: {
|
|
||||||
uid: "event1",
|
|
||||||
title: "Test Event",
|
|
||||||
calId: "667037022b752d0026472254/cal1",
|
|
||||||
start: day.toISOString(),
|
|
||||||
end: day.toISOString(),
|
|
||||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
|
||||||
attendee: [
|
|
||||||
{
|
|
||||||
cn: "test",
|
|
||||||
cal_address: "test@test.com",
|
|
||||||
partstat: "NEEDS-ACTION",
|
|
||||||
rsvp: "TRUE",
|
|
||||||
role: "REQ-PARTICIPANT",
|
|
||||||
cutype: "INDIVIDUAL",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cn: "John",
|
|
||||||
cal_address: "john@test.com",
|
|
||||||
partstat: "NEEDS-ACTION",
|
|
||||||
rsvp: "TRUE",
|
|
||||||
role: "REQ-PARTICIPANT",
|
|
||||||
cutype: "INDIVIDUAL",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"otherCal/cal": {
|
|
||||||
id: "otherCal/cal",
|
|
||||||
name: "Calendar 1",
|
|
||||||
color: "#FF0000",
|
|
||||||
events: {
|
|
||||||
event1: {
|
|
||||||
uid: "event1",
|
|
||||||
calId: "otherCal/cal",
|
|
||||||
title: "Test Event Other cal",
|
|
||||||
start: day.toISOString(),
|
|
||||||
end: day.toISOString(),
|
|
||||||
organizer: { cn: "john", cal_address: "john@test.com" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pending: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
it("renders correctly event data with fixed timezone", () => {
|
|
||||||
// Use fixed timezone UTC for consistent test results across all environments
|
|
||||||
const fixedDate = new Date("2025-01-15T10:00:00.000Z"); // 10AM UTC
|
|
||||||
const endDate = new Date(fixedDate.getTime() + 3600000); // 11AM UTC
|
|
||||||
|
|
||||||
// With UTC timezone set, formatLocalDateTime produces predictable values
|
|
||||||
const expectedStart = "2025-01-15T10:00"; // 10:00 AM UTC
|
|
||||||
const expectedEnd = "2025-01-15T11:00"; // 11:00 AM UTC
|
|
||||||
|
|
||||||
const stateWithFixedDate = {
|
|
||||||
...preloadedState,
|
|
||||||
calendars: {
|
|
||||||
list: {
|
|
||||||
"667037022b752d0026472254/cal1": {
|
|
||||||
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
|
|
||||||
events: {
|
|
||||||
event1: {
|
|
||||||
...preloadedState.calendars.list[
|
|
||||||
"667037022b752d0026472254/cal1"
|
|
||||||
].events.event1,
|
|
||||||
start: fixedDate.toISOString(),
|
|
||||||
end: endDate.toISOString(),
|
|
||||||
timezone: "UTC",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
renderWithProviders(
|
|
||||||
<EventUpdateModal
|
|
||||||
open={true}
|
|
||||||
onClose={mockOnClose}
|
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
|
||||||
eventId={"event1"}
|
|
||||||
/>,
|
|
||||||
stateWithFixedDate
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByDisplayValue("Test Event")).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Expand to show date/time inputs (normal mode shows DateTimeSummary)
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
|
||||||
|
|
||||||
const startDateInput = screen.getByTestId("start-date-input");
|
|
||||||
const startTimeInput = screen.getByTestId("start-time-input");
|
|
||||||
const endTimeInput = screen.getByTestId("end-time-input");
|
|
||||||
|
|
||||||
expect(startDateInput).toBeInTheDocument();
|
|
||||||
expect(startTimeInput).toBeInTheDocument();
|
|
||||||
expect(endTimeInput).toBeInTheDocument();
|
|
||||||
|
|
||||||
expect(startDateInput).toHaveValue(
|
|
||||||
dayjs(expectedStart).format(LONG_DATE_FORMAT)
|
|
||||||
);
|
|
||||||
expect(startTimeInput).toHaveValue(dayjs(expectedStart).format("HH:mm"));
|
|
||||||
|
|
||||||
expect(endTimeInput).toHaveValue(dayjs(expectedEnd).format("HH:mm"));
|
|
||||||
|
|
||||||
expect(screen.getByText("First Calendar")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
it("calls onClose when Cancel clicked", () => {
|
|
||||||
renderWithProviders(
|
|
||||||
<EventUpdateModal
|
|
||||||
open={true}
|
|
||||||
onClose={mockOnClose}
|
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
|
||||||
eventId={"event1"}
|
|
||||||
/>,
|
|
||||||
preloadedState
|
|
||||||
);
|
|
||||||
fireEvent.click(screen.getAllByTestId("CloseIcon")[0]);
|
|
||||||
|
|
||||||
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
|
|
||||||
});
|
|
||||||
it("toggle Show More reveals extra fields", async () => {
|
|
||||||
renderWithProviders(
|
|
||||||
<EventUpdateModal
|
|
||||||
open={true}
|
|
||||||
onClose={mockOnClose}
|
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
|
||||||
eventId={"event1"}
|
|
||||||
/>,
|
|
||||||
preloadedState
|
|
||||||
);
|
|
||||||
act(() => {
|
|
||||||
fireEvent.click(
|
|
||||||
screen.getByRole("button", { name: "common.moreOptions" })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText("event.form.notification")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Debug: Print DOM to see what's rendered
|
|
||||||
console.log("DOM after Show More clicked:", document.body.innerHTML);
|
|
||||||
|
|
||||||
// EventDisplay modal doesn't have Repeat checkbox, only RepeatEvent component
|
|
||||||
// which shows repetition settings when repetition data exists
|
|
||||||
// Since test event has no repetition data, RepeatEvent component won't show Repeat checkbox
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: /Show Less/i }));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can edit title when user is organizer", () => {
|
|
||||||
renderWithProviders(
|
|
||||||
<EventUpdateModal
|
|
||||||
open={true}
|
|
||||||
onClose={mockOnClose}
|
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
|
||||||
eventId={"event1"}
|
|
||||||
/>,
|
|
||||||
preloadedState
|
|
||||||
);
|
|
||||||
const titleField = screen.getByLabelText("event.form.title");
|
|
||||||
fireEvent.change(titleField, { target: { value: "New Title" } });
|
|
||||||
expect(screen.getByDisplayValue("New Title")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
it("toggle all-day updates end date correctly", () => {
|
|
||||||
renderWithProviders(
|
|
||||||
<EventUpdateModal
|
|
||||||
open={true}
|
|
||||||
onClose={mockOnClose}
|
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
|
||||||
eventId={"event1"}
|
|
||||||
/>,
|
|
||||||
preloadedState
|
|
||||||
);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
|
||||||
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
|
|
||||||
fireEvent.click(allDayCheckbox);
|
|
||||||
expect(allDayCheckbox).toBeChecked();
|
|
||||||
|
|
||||||
const expectedDate = dayjs(day).format(LONG_DATE_FORMAT);
|
|
||||||
|
|
||||||
const startDateInput = screen.getByTestId("start-date-input");
|
|
||||||
const endDateInput = screen.getByTestId("end-date-input");
|
|
||||||
|
|
||||||
expect(startDateInput).toHaveValue(expectedDate);
|
|
||||||
expect(endDateInput).toHaveValue(expectedDate);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("saves event and moves it when calendar is changed", async () => {
|
|
||||||
const spyPut = jest
|
|
||||||
.spyOn(eventThunks, "putEventAsync")
|
|
||||||
.mockImplementation((payload) => {
|
|
||||||
const promise = Promise.resolve(payload);
|
|
||||||
(promise as any).unwrap = () => promise;
|
|
||||||
return () => promise as any;
|
|
||||||
});
|
|
||||||
const spyMove = jest
|
|
||||||
.spyOn(eventThunks, "moveEventAsync")
|
|
||||||
.mockImplementation((payload) => {
|
|
||||||
const promise = Promise.resolve(payload);
|
|
||||||
(promise as any).unwrap = () => promise;
|
|
||||||
return () => promise as any;
|
|
||||||
});
|
|
||||||
|
|
||||||
const testDate = new Date("2025-01-15T10:00:00.000Z");
|
|
||||||
const testEndDate = new Date("2025-01-15T11:00:00.000Z");
|
|
||||||
const preloadedTwoCals = {
|
|
||||||
...preloadedState,
|
|
||||||
calendars: {
|
|
||||||
list: {
|
|
||||||
"667037022b752d0026472254/cal1": {
|
|
||||||
id: "667037022b752d0026472254/cal1",
|
|
||||||
name: "Calendar One",
|
|
||||||
color: "#FF0000",
|
|
||||||
events: {
|
|
||||||
event1: {
|
|
||||||
uid: "event1",
|
|
||||||
title: "Test Event",
|
|
||||||
calId: "667037022b752d0026472254/cal1",
|
|
||||||
start: testDate.toISOString(),
|
|
||||||
end: testEndDate.toISOString(),
|
|
||||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
|
||||||
attendee: [{ cn: "test", cal_address: "test@test.com" }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"667037022b752d0026472254/cal2": {
|
|
||||||
id: "667037022b752d0026472254/cal2",
|
|
||||||
name: "Calendar Two",
|
|
||||||
color: "#00FF00",
|
|
||||||
events: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pending: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await act(async () =>
|
|
||||||
renderWithProviders(
|
|
||||||
<EventUpdateModal
|
|
||||||
open={true}
|
|
||||||
onClose={mockOnClose}
|
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
|
||||||
eventId={"event1"}
|
|
||||||
/>,
|
|
||||||
preloadedTwoCals
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
|
||||||
const calendarSelect = screen.getByLabelText("event.form.calendar");
|
|
||||||
await act(async () => fireEvent.mouseDown(calendarSelect));
|
|
||||||
|
|
||||||
const option = await screen.findByText("Calendar Two");
|
|
||||||
fireEvent.click(option);
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(
|
|
||||||
() => {
|
|
||||||
expect(spyPut).toHaveBeenCalled();
|
|
||||||
},
|
|
||||||
{ timeout: 3000 }
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitFor(
|
|
||||||
() => {
|
|
||||||
expect(spyMove).toHaveBeenCalled();
|
|
||||||
},
|
|
||||||
{ timeout: 3000 }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("edit modal displays event time in original event timezone", () => {
|
|
||||||
// GIVEN user timezone is UTC+2
|
|
||||||
// WHEN the user edits an event at 2PM UTC+7 (Asia/Bangkok)
|
|
||||||
// THEN the update modal displays the time as 2PM in Asia/Bangkok timezone
|
|
||||||
|
|
||||||
const eventDateUTC7 = new Date("2025-01-15T07:00:00.000Z"); // 7AM UTC = 2PM UTC+7
|
|
||||||
|
|
||||||
const stateWithTimezone = {
|
|
||||||
...preloadedState,
|
|
||||||
calendars: {
|
|
||||||
list: {
|
|
||||||
"667037022b752d0026472254/cal1": {
|
|
||||||
id: "667037022b752d0026472254/cal1",
|
|
||||||
name: "Test Calendar",
|
|
||||||
color: "#FF0000",
|
|
||||||
events: {
|
|
||||||
event1: {
|
|
||||||
uid: "event1",
|
|
||||||
title: "Timezone Edit Test",
|
|
||||||
calId: "667037022b752d0026472254/cal1",
|
|
||||||
start: eventDateUTC7.toISOString(),
|
|
||||||
end: new Date(eventDateUTC7.getTime() + 3600000).toISOString(),
|
|
||||||
timezone: "Asia/Bangkok",
|
|
||||||
allday: false,
|
|
||||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
|
||||||
attendee: [{ cn: "test", cal_address: "test@test.com" }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pending: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
renderWithProviders(
|
|
||||||
<EventUpdateModal
|
|
||||||
open={true}
|
|
||||||
onClose={mockOnClose}
|
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
|
||||||
eventId={"event1"}
|
|
||||||
/>,
|
|
||||||
stateWithTimezone
|
|
||||||
);
|
|
||||||
|
|
||||||
// Expand to show timezone selector (normal mode hides it)
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
|
||||||
// The timezone select should have Asia/Bangkok selected
|
|
||||||
const timeZone = screen.getByDisplayValue(/Asia\/Bangkok/i);
|
|
||||||
expect(timeZone).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("InfoRow renders error style when error prop is true", () => {
|
|
||||||
renderWithProviders(<InfoRow icon={<span>i</span>} text="Bad" error />);
|
|
||||||
expect(screen.getByText("Bad")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can remove an attendee with the close button", () => {
|
|
||||||
renderWithProviders(
|
|
||||||
<EventUpdateModal
|
|
||||||
open={true}
|
|
||||||
onClose={mockOnClose}
|
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
|
||||||
eventId={"event1"}
|
|
||||||
/>,
|
|
||||||
preloadedState
|
|
||||||
);
|
|
||||||
|
|
||||||
const removeBtn = screen.getAllByTestId("CloseIcon").pop()!;
|
|
||||||
fireEvent.click(removeBtn);
|
|
||||||
|
|
||||||
expect(screen.queryByText(/John/)).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders video conference info when x_openpass_videoconference exists", () => {
|
|
||||||
const videoState = {
|
|
||||||
...preloadedState,
|
|
||||||
calendars: {
|
|
||||||
list: {
|
|
||||||
"667037022b752d0026472254/cal1": {
|
|
||||||
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
|
|
||||||
events: {
|
|
||||||
event1: {
|
|
||||||
...preloadedState.calendars.list[
|
|
||||||
"667037022b752d0026472254/cal1"
|
|
||||||
].events.event1,
|
|
||||||
x_openpass_videoconference: "https://meet.test/video",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
renderWithProviders(
|
|
||||||
<EventUpdateModal
|
|
||||||
open={true}
|
|
||||||
onClose={mockOnClose}
|
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
|
||||||
eventId={"event1"}
|
|
||||||
/>,
|
|
||||||
videoState
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
screen.getByText("event.form.joinVisioConference")
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Helper functions", () => {
|
describe("Helper functions", () => {
|
||||||
it("stringToColor generates consistent color", () => {
|
it("stringToColor generates consistent color", () => {
|
||||||
expect(stringToColor("Alice")).toMatch(/^#[0-9a-f]{6}$/);
|
expect(stringToColor("Alice")).toMatch(/^#[0-9a-f]{6}$/);
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import * as eventThunks from "@/features/Calendars/services";
|
||||||
|
import EventUpdateModal from "@/features/Events/EventUpdateModal";
|
||||||
|
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||||
|
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||||
|
|
||||||
|
describe("Event Full Display — delegated calendar move", () => {
|
||||||
|
const mockOnClose = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const testDate = new Date("2025-01-15T10:00:00.000Z");
|
||||||
|
const testEndDate = new Date("2025-01-15T11:00:00.000Z");
|
||||||
|
|
||||||
|
const baseUserState = {
|
||||||
|
user: {
|
||||||
|
userData: {
|
||||||
|
sub: "test",
|
||||||
|
email: "test@test.com",
|
||||||
|
sid: "aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro",
|
||||||
|
openpaasId: "user1",
|
||||||
|
},
|
||||||
|
organiserData: {
|
||||||
|
cn: "test",
|
||||||
|
cal_address: "test@test.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const preloadedDelegatedCals = {
|
||||||
|
...baseUserState,
|
||||||
|
calendars: {
|
||||||
|
pending: false,
|
||||||
|
list: {
|
||||||
|
"user1/cal1": {
|
||||||
|
id: "user1/cal1",
|
||||||
|
name: "Calendar One",
|
||||||
|
color: "#FF0000",
|
||||||
|
events: {
|
||||||
|
event1: {
|
||||||
|
uid: "event1",
|
||||||
|
title: "Test Event",
|
||||||
|
calId: "user1/cal1",
|
||||||
|
start: testDate.toISOString(),
|
||||||
|
end: testEndDate.toISOString(),
|
||||||
|
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||||
|
attendee: [{ cn: "test", cal_address: "test@test.com" }],
|
||||||
|
URL: "/calendar/user1/cal1/event1.ics",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
link: "/calendar/user1/cal1.json",
|
||||||
|
},
|
||||||
|
"user2/cal2": {
|
||||||
|
id: "user2/cal2",
|
||||||
|
name: "Delegated Calendar",
|
||||||
|
color: "#00FF00",
|
||||||
|
delegated: true,
|
||||||
|
owner: {
|
||||||
|
emails: ["delegate@test.com"],
|
||||||
|
name: "Delegate User",
|
||||||
|
},
|
||||||
|
events: {},
|
||||||
|
link: "/calendar/user2/cal2.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it("calls deleteEventAsync and putEventAsync — and NOT moveEventAsync — when moving to a delegated calendar", async () => {
|
||||||
|
const spyPut = jest
|
||||||
|
.spyOn(eventThunks, "putEventAsync")
|
||||||
|
.mockImplementation((payload) => {
|
||||||
|
const promise = Promise.resolve(payload);
|
||||||
|
(promise as any).unwrap = () => promise;
|
||||||
|
return () => promise as any;
|
||||||
|
});
|
||||||
|
const spyDelete = jest
|
||||||
|
.spyOn(eventThunks, "deleteEventAsync")
|
||||||
|
.mockImplementation((payload) => {
|
||||||
|
const promise = Promise.resolve(payload);
|
||||||
|
(promise as any).unwrap = () => promise;
|
||||||
|
return () => promise as any;
|
||||||
|
});
|
||||||
|
const spyMove = jest
|
||||||
|
.spyOn(eventThunks, "moveEventAsync")
|
||||||
|
.mockImplementation((payload) => {
|
||||||
|
const promise = Promise.resolve(payload);
|
||||||
|
(promise as any).unwrap = () => promise;
|
||||||
|
return () => promise as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () =>
|
||||||
|
renderWithProviders(
|
||||||
|
<EventUpdateModal
|
||||||
|
open={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
calId={"user1/cal1"}
|
||||||
|
eventId={"event1"}
|
||||||
|
/>,
|
||||||
|
preloadedDelegatedCals
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||||
|
|
||||||
|
const calendarSelect = screen.getByLabelText("event.form.calendar");
|
||||||
|
await act(async () => fireEvent.mouseDown(calendarSelect));
|
||||||
|
const option = await screen.findByText("Delegated Calendar");
|
||||||
|
fireEvent.click(option);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
||||||
|
});
|
||||||
|
await waitFor(
|
||||||
|
() => {
|
||||||
|
expect(spyPut).toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
|
await waitFor(
|
||||||
|
() => {
|
||||||
|
expect(spyDelete).toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(spyMove).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,422 @@
|
|||||||
|
import { InfoRow } from "@/components/Event/InfoRow";
|
||||||
|
import { LONG_DATE_FORMAT } from "@/components/Event/utils/dateTimeFormatters";
|
||||||
|
import * as eventThunks from "@/features/Calendars/services";
|
||||||
|
import EventUpdateModal from "@/features/Events/EventUpdateModal";
|
||||||
|
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||||
|
|
||||||
|
describe("Event Full Display", () => {
|
||||||
|
const mockOnClose = jest.fn();
|
||||||
|
const day = new Date("2025-01-15T10:00:00.000Z"); // Fixed date in UTC: Jan 15, 2025 10:00 AM UTC
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const preloadedState = {
|
||||||
|
user: {
|
||||||
|
userData: {
|
||||||
|
sub: "test",
|
||||||
|
email: "test@test.com",
|
||||||
|
sid: "aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro",
|
||||||
|
openpaasId: "667037022b752d0026472254",
|
||||||
|
},
|
||||||
|
organiserData: {
|
||||||
|
cn: "test",
|
||||||
|
cal_address: "test@test.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
calendars: {
|
||||||
|
list: {
|
||||||
|
"667037022b752d0026472254/cal1": {
|
||||||
|
id: "667037022b752d0026472254/cal1",
|
||||||
|
name: "First Calendar",
|
||||||
|
color: "#FF0000",
|
||||||
|
events: {
|
||||||
|
event1: {
|
||||||
|
uid: "event1",
|
||||||
|
title: "Test Event",
|
||||||
|
calId: "667037022b752d0026472254/cal1",
|
||||||
|
start: day.toISOString(),
|
||||||
|
end: day.toISOString(),
|
||||||
|
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||||
|
attendee: [
|
||||||
|
{
|
||||||
|
cn: "test",
|
||||||
|
cal_address: "test@test.com",
|
||||||
|
partstat: "NEEDS-ACTION",
|
||||||
|
rsvp: "TRUE",
|
||||||
|
role: "REQ-PARTICIPANT",
|
||||||
|
cutype: "INDIVIDUAL",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cn: "John",
|
||||||
|
cal_address: "john@test.com",
|
||||||
|
partstat: "NEEDS-ACTION",
|
||||||
|
rsvp: "TRUE",
|
||||||
|
role: "REQ-PARTICIPANT",
|
||||||
|
cutype: "INDIVIDUAL",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"otherCal/cal": {
|
||||||
|
id: "otherCal/cal",
|
||||||
|
name: "Calendar 1",
|
||||||
|
color: "#FF0000",
|
||||||
|
events: {
|
||||||
|
event1: {
|
||||||
|
uid: "event1",
|
||||||
|
calId: "otherCal/cal",
|
||||||
|
title: "Test Event Other cal",
|
||||||
|
start: day.toISOString(),
|
||||||
|
end: day.toISOString(),
|
||||||
|
organizer: { cn: "john", cal_address: "john@test.com" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it("renders correctly event data with fixed timezone", () => {
|
||||||
|
// Use fixed timezone UTC for consistent test results across all environments
|
||||||
|
const fixedDate = new Date("2025-01-15T10:00:00.000Z"); // 10AM UTC
|
||||||
|
const endDate = new Date(fixedDate.getTime() + 3600000); // 11AM UTC
|
||||||
|
|
||||||
|
// With UTC timezone set, formatLocalDateTime produces predictable values
|
||||||
|
const expectedStart = "2025-01-15T10:00"; // 10:00 AM UTC
|
||||||
|
const expectedEnd = "2025-01-15T11:00"; // 11:00 AM UTC
|
||||||
|
|
||||||
|
const stateWithFixedDate = {
|
||||||
|
...preloadedState,
|
||||||
|
calendars: {
|
||||||
|
pending: false,
|
||||||
|
list: {
|
||||||
|
"667037022b752d0026472254/cal1": {
|
||||||
|
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
|
||||||
|
events: {
|
||||||
|
event1: {
|
||||||
|
...preloadedState.calendars.list[
|
||||||
|
"667037022b752d0026472254/cal1"
|
||||||
|
].events.event1,
|
||||||
|
start: fixedDate.toISOString(),
|
||||||
|
end: endDate.toISOString(),
|
||||||
|
timezone: "UTC",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithProviders(
|
||||||
|
<EventUpdateModal
|
||||||
|
open={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
|
eventId={"event1"}
|
||||||
|
/>,
|
||||||
|
stateWithFixedDate
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByDisplayValue("Test Event")).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Expand to show date/time inputs (normal mode shows DateTimeSummary)
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||||
|
|
||||||
|
const startDateInput = screen.getByTestId("start-date-input");
|
||||||
|
const startTimeInput = screen.getByTestId("start-time-input");
|
||||||
|
const endTimeInput = screen.getByTestId("end-time-input");
|
||||||
|
|
||||||
|
expect(startDateInput).toBeInTheDocument();
|
||||||
|
expect(startTimeInput).toBeInTheDocument();
|
||||||
|
expect(endTimeInput).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(startDateInput).toHaveValue(
|
||||||
|
dayjs(expectedStart).format(LONG_DATE_FORMAT)
|
||||||
|
);
|
||||||
|
expect(startTimeInput).toHaveValue(dayjs(expectedStart).format("HH:mm"));
|
||||||
|
|
||||||
|
expect(endTimeInput).toHaveValue(dayjs(expectedEnd).format("HH:mm"));
|
||||||
|
|
||||||
|
expect(screen.getByText("First Calendar")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it("calls onClose when Cancel clicked", () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<EventUpdateModal
|
||||||
|
open={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
|
eventId={"event1"}
|
||||||
|
/>,
|
||||||
|
preloadedState
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getAllByTestId("CloseIcon")[0]);
|
||||||
|
|
||||||
|
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
|
||||||
|
});
|
||||||
|
it("toggle Show More reveals extra fields", async () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<EventUpdateModal
|
||||||
|
open={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
|
eventId={"event1"}
|
||||||
|
/>,
|
||||||
|
preloadedState
|
||||||
|
);
|
||||||
|
act(() => {
|
||||||
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: "common.moreOptions" })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("event.form.notification")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// EventDisplay modal doesn't have Repeat checkbox, only RepeatEvent component
|
||||||
|
// which shows repetition settings when repetition data exists
|
||||||
|
// Since test event has no repetition data, RepeatEvent component won't show Repeat checkbox
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /Show Less/i }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can edit title when user is organizer", () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<EventUpdateModal
|
||||||
|
open={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
|
eventId={"event1"}
|
||||||
|
/>,
|
||||||
|
preloadedState
|
||||||
|
);
|
||||||
|
const titleField = screen.getByLabelText("event.form.title");
|
||||||
|
fireEvent.change(titleField, { target: { value: "New Title" } });
|
||||||
|
expect(screen.getByDisplayValue("New Title")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it("toggle all-day updates end date correctly", () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<EventUpdateModal
|
||||||
|
open={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
|
eventId={"event1"}
|
||||||
|
/>,
|
||||||
|
preloadedState
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||||
|
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
|
||||||
|
fireEvent.click(allDayCheckbox);
|
||||||
|
expect(allDayCheckbox).toBeChecked();
|
||||||
|
|
||||||
|
const expectedDate = dayjs(day).format(LONG_DATE_FORMAT);
|
||||||
|
|
||||||
|
const startDateInput = screen.getByTestId("start-date-input");
|
||||||
|
const endDateInput = screen.getByTestId("end-date-input");
|
||||||
|
|
||||||
|
expect(startDateInput).toHaveValue(expectedDate);
|
||||||
|
expect(endDateInput).toHaveValue(expectedDate);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("saves event and moves it when calendar is changed", async () => {
|
||||||
|
const spyPut = jest
|
||||||
|
.spyOn(eventThunks, "putEventAsync")
|
||||||
|
.mockImplementation((payload) => {
|
||||||
|
const promise = Promise.resolve(payload);
|
||||||
|
(promise as any).unwrap = () => promise;
|
||||||
|
return () => promise as any;
|
||||||
|
});
|
||||||
|
const spyMove = jest
|
||||||
|
.spyOn(eventThunks, "moveEventAsync")
|
||||||
|
.mockImplementation((payload) => {
|
||||||
|
const promise = Promise.resolve(payload);
|
||||||
|
(promise as any).unwrap = () => promise;
|
||||||
|
return () => promise as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
const testDate = new Date("2025-01-15T10:00:00.000Z");
|
||||||
|
const testEndDate = new Date("2025-01-15T11:00:00.000Z");
|
||||||
|
const preloadedTwoCals = {
|
||||||
|
...preloadedState,
|
||||||
|
calendars: {
|
||||||
|
pending: false,
|
||||||
|
list: {
|
||||||
|
"667037022b752d0026472254/cal1": {
|
||||||
|
id: "667037022b752d0026472254/cal1",
|
||||||
|
name: "Calendar One",
|
||||||
|
color: "#FF0000",
|
||||||
|
events: {
|
||||||
|
event1: {
|
||||||
|
uid: "event1",
|
||||||
|
title: "Test Event",
|
||||||
|
calId: "667037022b752d0026472254/cal1",
|
||||||
|
start: testDate.toISOString(),
|
||||||
|
end: testEndDate.toISOString(),
|
||||||
|
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||||
|
attendee: [{ cn: "test", cal_address: "test@test.com" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"667037022b752d0026472254/cal2": {
|
||||||
|
id: "667037022b752d0026472254/cal2",
|
||||||
|
name: "Calendar Two",
|
||||||
|
color: "#00FF00",
|
||||||
|
events: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pending: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () =>
|
||||||
|
renderWithProviders(
|
||||||
|
<EventUpdateModal
|
||||||
|
open={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
|
eventId={"event1"}
|
||||||
|
/>,
|
||||||
|
preloadedTwoCals
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||||
|
const calendarSelect = screen.getByLabelText("event.form.calendar");
|
||||||
|
await act(async () => fireEvent.mouseDown(calendarSelect));
|
||||||
|
|
||||||
|
const option = await screen.findByText("Calendar Two");
|
||||||
|
fireEvent.click(option);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(
|
||||||
|
() => {
|
||||||
|
expect(spyPut).toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(
|
||||||
|
() => {
|
||||||
|
expect(spyMove).toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("edit modal displays event time in original event timezone", () => {
|
||||||
|
// GIVEN user timezone is UTC+2
|
||||||
|
// WHEN the user edits an event at 2PM UTC+7 (Asia/Bangkok)
|
||||||
|
// THEN the update modal displays the time as 2PM in Asia/Bangkok timezone
|
||||||
|
const eventDateUTC7 = new Date("2025-01-15T07:00:00.000Z"); // 7AM UTC = 2PM UTC+7
|
||||||
|
|
||||||
|
const stateWithTimezone = {
|
||||||
|
...preloadedState,
|
||||||
|
calendars: {
|
||||||
|
pending: false,
|
||||||
|
list: {
|
||||||
|
"667037022b752d0026472254/cal1": {
|
||||||
|
id: "667037022b752d0026472254/cal1",
|
||||||
|
name: "Test Calendar",
|
||||||
|
color: "#FF0000",
|
||||||
|
events: {
|
||||||
|
event1: {
|
||||||
|
uid: "event1",
|
||||||
|
title: "Timezone Edit Test",
|
||||||
|
calId: "667037022b752d0026472254/cal1",
|
||||||
|
start: eventDateUTC7.toISOString(),
|
||||||
|
end: new Date(eventDateUTC7.getTime() + 3600000).toISOString(),
|
||||||
|
timezone: "Asia/Bangkok",
|
||||||
|
allday: false,
|
||||||
|
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||||
|
attendee: [{ cn: "test", cal_address: "test@test.com" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pending: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithProviders(
|
||||||
|
<EventUpdateModal
|
||||||
|
open={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
|
eventId={"event1"}
|
||||||
|
/>,
|
||||||
|
stateWithTimezone
|
||||||
|
);
|
||||||
|
|
||||||
|
// Expand to show timezone selector (normal mode hides it)
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||||
|
// The timezone select should have Asia/Bangkok selected
|
||||||
|
const timeZone = screen.getByDisplayValue(/Asia\/Bangkok/i);
|
||||||
|
expect(timeZone).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("InfoRow renders error style when error prop is true", () => {
|
||||||
|
renderWithProviders(<InfoRow icon={<span>i</span>} text="Bad" error />);
|
||||||
|
expect(screen.getByText("Bad")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can remove an attendee with the close button", () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<EventUpdateModal
|
||||||
|
open={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
|
eventId={"event1"}
|
||||||
|
/>,
|
||||||
|
preloadedState
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeBtn = screen.getAllByTestId("CloseIcon").pop()!;
|
||||||
|
fireEvent.click(removeBtn);
|
||||||
|
|
||||||
|
expect(screen.queryByText(/John/)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders video conference info when x_openpass_videoconference exists", () => {
|
||||||
|
const videoState = {
|
||||||
|
...preloadedState,
|
||||||
|
calendars: {
|
||||||
|
pending: false,
|
||||||
|
list: {
|
||||||
|
"667037022b752d0026472254/cal1": {
|
||||||
|
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
|
||||||
|
events: {
|
||||||
|
event1: {
|
||||||
|
...preloadedState.calendars.list[
|
||||||
|
"667037022b752d0026472254/cal1"
|
||||||
|
].events.event1,
|
||||||
|
x_openpass_videoconference: "https://meet.test/video",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithProviders(
|
||||||
|
<EventUpdateModal
|
||||||
|
open={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
|
eventId={"event1"}
|
||||||
|
/>,
|
||||||
|
videoState
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText("event.form.joinVisioConference")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,7 +6,6 @@ import { addDays } from "@/components/Event/utils/dateRules";
|
|||||||
import { formatDateTimeInTimezone } from "@/components/Event/utils/dateTimeFormatters";
|
import { formatDateTimeInTimezone } from "@/components/Event/utils/dateTimeFormatters";
|
||||||
import { convertFormDateTimeToISO } from "@/components/Event/utils/dateTimeHelpers";
|
import { convertFormDateTimeToISO } from "@/components/Event/utils/dateTimeHelpers";
|
||||||
import {
|
import {
|
||||||
moveEventAsync,
|
|
||||||
putEventAsync,
|
putEventAsync,
|
||||||
updateEventInstanceAsync,
|
updateEventInstanceAsync,
|
||||||
updateSeriesAsync,
|
updateSeriesAsync,
|
||||||
@@ -45,6 +44,7 @@ import { userAttendee } from "../User/models/attendee";
|
|||||||
import { deleteEvent, getEvent, putEvent } from "./EventApi";
|
import { deleteEvent, getEvent, putEvent } from "./EventApi";
|
||||||
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
||||||
import { detectRecurringEventChanges } from "./eventUtils";
|
import { detectRecurringEventChanges } from "./eventUtils";
|
||||||
|
import { moveEventBetweenCalendars } from "./updateEventHelpers/moveEventBetweenCalendars";
|
||||||
|
|
||||||
function EventUpdateModal({
|
function EventUpdateModal({
|
||||||
eventId,
|
eventId,
|
||||||
@@ -79,14 +79,12 @@ function EventUpdateModal({
|
|||||||
// if the event's calendar is delegated then it shall be the only calendar accessible from the event update modal
|
// if the event's calendar is delegated then it shall be the only calendar accessible from the event update modal
|
||||||
const userPersonalCalendars: Calendar[] = useMemo(() => {
|
const userPersonalCalendars: Calendar[] = useMemo(() => {
|
||||||
const allCalendars = Object.values(calList) as Calendar[];
|
const allCalendars = Object.values(calList) as Calendar[];
|
||||||
if (calList[calId]?.delegated) {
|
|
||||||
return [calList[calId]];
|
|
||||||
}
|
|
||||||
return allCalendars.filter(
|
return allCalendars.filter(
|
||||||
(calendar: Calendar) =>
|
(calendar: Calendar) =>
|
||||||
calendar.id?.split("/")[0] === user.userData?.openpaasId
|
calendar.id?.split("/")[0] === user.userData?.openpaasId ||
|
||||||
|
calendar.delegated
|
||||||
);
|
);
|
||||||
}, [calList, calId, user.userData?.openpaasId]);
|
}, [calList, user.userData?.openpaasId]);
|
||||||
|
|
||||||
const timezoneList = useMemo(() => {
|
const timezoneList = useMemo(() => {
|
||||||
const zones = Object.keys(TIMEZONES.zones).sort();
|
const zones = Object.keys(TIMEZONES.zones).sort();
|
||||||
@@ -947,7 +945,7 @@ function EventUpdateModal({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Handle result of updateSeriesAsync
|
// Handle result of updateSeriesAsync
|
||||||
assertThunkSuccess(result);
|
await assertThunkSuccess(result);
|
||||||
|
|
||||||
// Clear cache to ensure navigation shows updated data
|
// Clear cache to ensure navigation shows updated data
|
||||||
dispatch(clearFetchCache(calId));
|
dispatch(clearFetchCache(calId));
|
||||||
@@ -969,7 +967,7 @@ function EventUpdateModal({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Handle result of putEventAsync - check if rejected first
|
// Handle result of putEventAsync - check if rejected first
|
||||||
assertThunkSuccess(result);
|
await assertThunkSuccess(result);
|
||||||
|
|
||||||
// Remove old single event AFTER new recurring instances are added to store
|
// Remove old single event AFTER new recurring instances are added to store
|
||||||
// This prevents empty grid during the transition
|
// This prevents empty grid during the transition
|
||||||
@@ -980,68 +978,30 @@ function EventUpdateModal({
|
|||||||
|
|
||||||
// Clear temp data on successful save
|
// Clear temp data on successful save
|
||||||
clearEventFormTempData("update");
|
clearEventFormTempData("update");
|
||||||
} else {
|
} else if (newCalId === calId) {
|
||||||
// Normal non-recurring event update
|
// Normal non-recurring event update (same calendar)
|
||||||
// If calendar is changing, we'll handle it separately with moveEventAsync
|
const result = await dispatch(
|
||||||
// So only call putEventAsync if calendar is NOT changing
|
putEventAsync({ cal: targetCalendar, newEvent })
|
||||||
if (newCalId === calId) {
|
);
|
||||||
const result = await dispatch(
|
|
||||||
putEventAsync({ cal: targetCalendar, newEvent })
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle result of putEventAsync - check if rejected first
|
await assertThunkSuccess(result);
|
||||||
const typedResult = result as AsyncThunkResult;
|
|
||||||
if (typedResult.type && typedResult.type.endsWith("/rejected")) {
|
|
||||||
throw new Error(
|
|
||||||
typedResult.error?.message ||
|
|
||||||
typedResult.payload?.message ||
|
|
||||||
"API call failed"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (typedResult && typeof typedResult.unwrap === "function") {
|
|
||||||
await typedResult.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear temp data on successful save
|
// Clear temp data on successful save
|
||||||
clearEventFormTempData("update");
|
clearEventFormTempData("update");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// Note: when newCalId !== calId, the move is handled below in the
|
||||||
|
// "Handle calendar change" block, so we intentionally skip putEventAsync here.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle calendar change
|
// Handle calendar change (move to a different calendar)
|
||||||
if (newCalId !== calId) {
|
if (newCalId !== calId) {
|
||||||
// Get the old calendar for updating
|
await moveEventBetweenCalendars({
|
||||||
const oldCalendar = calList[calId];
|
dispatch,
|
||||||
if (!oldCalendar) {
|
calList,
|
||||||
console.error("Old calendar not found");
|
newEvent,
|
||||||
return;
|
oldCalId: calId,
|
||||||
}
|
newCalId,
|
||||||
|
});
|
||||||
// First update the event in the old calendar, then move it
|
|
||||||
const putResult = await dispatch(
|
|
||||||
putEventAsync({ cal: oldCalendar, newEvent: { ...newEvent, calId } })
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle result of putEventAsync
|
|
||||||
const typedPutResult = putResult as AsyncThunkResult;
|
|
||||||
if (typedPutResult && typeof typedPutResult.unwrap === "function") {
|
|
||||||
await typedPutResult.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then move it to the new calendar
|
|
||||||
const moveResult = await dispatch(
|
|
||||||
moveEventAsync({
|
|
||||||
cal: targetCalendar,
|
|
||||||
newEvent,
|
|
||||||
newURL: `/calendars/${newCalId}/${extractEventBaseUuid(event.uid)}.ics`,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle result of moveEventAsync
|
|
||||||
const typedMoveResult = moveResult as AsyncThunkResult;
|
|
||||||
if (typedMoveResult && typeof typedMoveResult.unwrap === "function") {
|
|
||||||
await typedMoveResult.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear temp data on successful move
|
// Clear temp data on successful move
|
||||||
clearEventFormTempData("update");
|
clearEventFormTempData("update");
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import { AppDispatch } from "@/app/store";
|
||||||
|
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||||
|
import {
|
||||||
|
deleteEventAsync,
|
||||||
|
moveEventAsync,
|
||||||
|
putEventAsync,
|
||||||
|
} from "@/features/Calendars/services";
|
||||||
|
import { AsyncThunkResult } from "@/features/Calendars/types/AsyncThunkResult";
|
||||||
|
import { userAttendee } from "@/features/User/models/attendee";
|
||||||
|
import { userOrganiser } from "@/features/User/userDataTypes";
|
||||||
|
import { assertThunkSuccess } from "@/utils/assertThunkSuccess";
|
||||||
|
import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid";
|
||||||
|
import { makeDisplayName } from "@/utils/makeDisplayName";
|
||||||
|
import { CalendarEvent } from "../EventsTypes";
|
||||||
|
import { buildDelegatedEventURL } from "../eventUtils";
|
||||||
|
|
||||||
|
export interface MoveEventBetweenCalendarsParams {
|
||||||
|
dispatch: AppDispatch;
|
||||||
|
calList: Record<string, Calendar>;
|
||||||
|
newEvent: CalendarEvent;
|
||||||
|
oldCalId: string;
|
||||||
|
newCalId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveOrganizerForCalendar(
|
||||||
|
calendar: Calendar,
|
||||||
|
originalOrganizer: CalendarEvent["organizer"]
|
||||||
|
): CalendarEvent["organizer"] {
|
||||||
|
const ownerEmail = calendar.owner?.emails?.[0];
|
||||||
|
if (!ownerEmail) {
|
||||||
|
return originalOrganizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...originalOrganizer,
|
||||||
|
cal_address: ownerEmail,
|
||||||
|
cn: makeDisplayName(calendar) ?? originalOrganizer?.cn ?? "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewriteAttendeesForOrganizerChange(
|
||||||
|
attendees: userAttendee[],
|
||||||
|
oldOrganizer?: userOrganiser,
|
||||||
|
newOrganizer?: userOrganiser
|
||||||
|
): userAttendee[] {
|
||||||
|
if (!newOrganizer) {
|
||||||
|
return attendees;
|
||||||
|
}
|
||||||
|
const normalise = (addr: string | undefined) => (addr ?? "").toLowerCase();
|
||||||
|
const oldAddr = normalise(oldOrganizer?.cal_address);
|
||||||
|
const newAddr = normalise(newOrganizer.cal_address);
|
||||||
|
|
||||||
|
// Remove the old organizer from the attendee list if there is one
|
||||||
|
const filtered = attendees.filter(
|
||||||
|
(a) => normalise(a.cal_address) !== oldAddr
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add the new organizer as CHAIR if they are not already listed
|
||||||
|
const alreadyPresent = filtered.some(
|
||||||
|
(a) => normalise(a.cal_address) === newAddr
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!alreadyPresent && newAddr) {
|
||||||
|
filtered.push({
|
||||||
|
cal_address: newOrganizer.cal_address,
|
||||||
|
partstat: "ACCEPTED",
|
||||||
|
role: "CHAIR",
|
||||||
|
rsvp: "FALSE",
|
||||||
|
cn: newOrganizer.cn || newOrganizer.cal_address,
|
||||||
|
cutype: "INDIVIDUAL",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function moveEventBetweenCalendars({
|
||||||
|
dispatch,
|
||||||
|
calList,
|
||||||
|
newEvent,
|
||||||
|
oldCalId,
|
||||||
|
newCalId,
|
||||||
|
}: MoveEventBetweenCalendarsParams): Promise<void> {
|
||||||
|
const oldCalendar = calList[oldCalId];
|
||||||
|
if (!oldCalendar) {
|
||||||
|
throw new Error(`Old calendar not found: ${oldCalId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetCalendar = calList[newCalId];
|
||||||
|
if (!targetCalendar) {
|
||||||
|
throw new Error(`Target calendar not found: ${newCalId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDelegatedMove = oldCalendar.delegated || targetCalendar.delegated;
|
||||||
|
|
||||||
|
if (isDelegatedMove) {
|
||||||
|
await moveDelegatedEvent({
|
||||||
|
dispatch,
|
||||||
|
newEvent,
|
||||||
|
oldCalendar,
|
||||||
|
targetCalendar,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await moveStandardEvent({
|
||||||
|
dispatch,
|
||||||
|
newEvent,
|
||||||
|
targetCalendar,
|
||||||
|
oldCalendar,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StandardMoveParams {
|
||||||
|
dispatch: AppDispatch;
|
||||||
|
newEvent: CalendarEvent;
|
||||||
|
targetCalendar: Calendar;
|
||||||
|
oldCalendar: Calendar;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function moveStandardEvent({
|
||||||
|
dispatch,
|
||||||
|
newEvent,
|
||||||
|
targetCalendar,
|
||||||
|
oldCalendar,
|
||||||
|
}: StandardMoveParams): Promise<void> {
|
||||||
|
const newCalId = targetCalendar.id;
|
||||||
|
|
||||||
|
const putResult = await dispatch(
|
||||||
|
putEventAsync({
|
||||||
|
cal: oldCalendar,
|
||||||
|
newEvent: { ...newEvent, calId: oldCalendar.id },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await assertThunkSuccess(putResult);
|
||||||
|
const newURL = `/calendars/${newCalId}/${extractEventBaseUuid(newEvent.uid)}.ics`;
|
||||||
|
|
||||||
|
const moveResult = await dispatch(
|
||||||
|
moveEventAsync({
|
||||||
|
cal: targetCalendar,
|
||||||
|
newEvent,
|
||||||
|
newURL,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await assertThunkSuccess(moveResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DelegatedMoveParams {
|
||||||
|
dispatch: AppDispatch;
|
||||||
|
newEvent: CalendarEvent;
|
||||||
|
oldCalendar: Calendar;
|
||||||
|
targetCalendar: Calendar;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function moveDelegatedEvent({
|
||||||
|
dispatch,
|
||||||
|
newEvent,
|
||||||
|
oldCalendar,
|
||||||
|
targetCalendar,
|
||||||
|
}: DelegatedMoveParams): Promise<void> {
|
||||||
|
const newCalId = targetCalendar.id;
|
||||||
|
|
||||||
|
const newOrganizer = resolveOrganizerForCalendar(
|
||||||
|
targetCalendar,
|
||||||
|
newEvent.organizer
|
||||||
|
);
|
||||||
|
|
||||||
|
const newAttendees = rewriteAttendeesForOrganizerChange(
|
||||||
|
newEvent.attendee ?? [],
|
||||||
|
newEvent.organizer,
|
||||||
|
newOrganizer
|
||||||
|
);
|
||||||
|
|
||||||
|
const newURL = `/calendars/${newCalId}/${extractEventBaseUuid(newEvent.uid)}.ics`;
|
||||||
|
|
||||||
|
const eventForTargetCalendar: CalendarEvent = {
|
||||||
|
...newEvent,
|
||||||
|
calId: newCalId,
|
||||||
|
URL: targetCalendar.delegated
|
||||||
|
? buildDelegatedEventURL(targetCalendar, newURL)
|
||||||
|
: newURL,
|
||||||
|
organizer: newOrganizer,
|
||||||
|
attendee: newAttendees,
|
||||||
|
};
|
||||||
|
|
||||||
|
const putResult = await dispatch(
|
||||||
|
putEventAsync({ cal: targetCalendar, newEvent: eventForTargetCalendar })
|
||||||
|
);
|
||||||
|
|
||||||
|
const typedPutResult = putResult as AsyncThunkResult;
|
||||||
|
if (typedPutResult && typeof typedPutResult.unwrap === "function") {
|
||||||
|
await typedPutResult.unwrap();
|
||||||
|
} else {
|
||||||
|
await assertThunkSuccess(putResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteResult = await dispatch(
|
||||||
|
deleteEventAsync({
|
||||||
|
calId: oldCalendar.id,
|
||||||
|
eventId: newEvent.uid,
|
||||||
|
eventURL: newEvent.URL,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const typedDeleteResult = deleteResult as AsyncThunkResult;
|
||||||
|
if (typedDeleteResult && typeof typedDeleteResult.unwrap === "function") {
|
||||||
|
await typedDeleteResult.unwrap();
|
||||||
|
} else {
|
||||||
|
await assertThunkSuccess(deleteResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import { AsyncThunkResult } from "@/features/Calendars/types/AsyncThunkResult";
|
import { AsyncThunkResult } from "@/features/Calendars/types/AsyncThunkResult";
|
||||||
|
|
||||||
export async function assertThunkSuccess(result: unknown): Promise<void> {
|
export async function assertThunkSuccess(result: unknown): Promise<void> {
|
||||||
|
if (result === undefined || result === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const typed = result as AsyncThunkResult;
|
const typed = result as AsyncThunkResult;
|
||||||
if (typed?.type?.endsWith("/rejected")) {
|
if (typed.type && typed.type.endsWith("/rejected")) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
typed.error?.message || typed.payload?.message || "API call failed"
|
typed.error?.message ?? typed.payload?.message ?? "Thunk was rejected"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (typeof typed.unwrap === "function") {
|
if (typeof typed.unwrap === "function") {
|
||||||
|
|||||||
Reference in New Issue
Block a user