Co-authored-by: lethemanh <lethemanh@lethemanhs-MacBook-Pro.local>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { addCalendarResourceAsync } from "@/features/Calendars/api/addCalendarResourceAsync";
|
||||
import { addSharedCalendar } from "@/features/Calendars/CalendarApi";
|
||||
import { getResourceDetails } from "@/features/User/userAPI";
|
||||
import { getResourceDetails, getUserDetails } from "@/features/User/userAPI";
|
||||
import { toRejectedError } from "@/utils/errorUtils";
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
|
||||
@@ -10,6 +10,7 @@ jest.mock("@/utils/errorUtils");
|
||||
|
||||
const mockedAddSharedCalendar = addSharedCalendar as jest.Mock;
|
||||
const mockedGetResourceDetails = getResourceDetails as jest.Mock;
|
||||
const mockedGetUserDetails = getUserDetails as jest.Mock;
|
||||
const mockedToRejectedError = toRejectedError as jest.Mock;
|
||||
|
||||
describe("addCalendarResourceAsync thunk", () => {
|
||||
@@ -55,6 +56,11 @@ describe("addCalendarResourceAsync thunk", () => {
|
||||
|
||||
it("should add shared calendar, fetch resource details, map userData", async () => {
|
||||
mockedGetResourceDetails.mockResolvedValueOnce(mockResolvedResourceData);
|
||||
mockedGetUserDetails.mockResolvedValueOnce({
|
||||
firstname: "Creator",
|
||||
lastname: "User",
|
||||
emails: ["creator@example.com"],
|
||||
});
|
||||
mockedAddSharedCalendar.mockResolvedValueOnce({});
|
||||
|
||||
const result = await addCalendarResourceAsync(
|
||||
@@ -67,6 +73,7 @@ describe("addCalendarResourceAsync thunk", () => {
|
||||
mockPayload.cal
|
||||
);
|
||||
expect(mockedGetResourceDetails).toHaveBeenCalledWith("res-456");
|
||||
expect(mockedGetUserDetails).toHaveBeenCalledWith("user-789");
|
||||
|
||||
expect(result.type).toBe("calendars/addCalendarResource/fulfilled");
|
||||
expect(result.payload).toEqual({
|
||||
@@ -75,7 +82,12 @@ describe("addCalendarResourceAsync thunk", () => {
|
||||
desc: "A meeting room",
|
||||
link: "/calendars/user-123/cal-123.json",
|
||||
name: "Resource Room A",
|
||||
owner: mockResolvedResourceData,
|
||||
owner: {
|
||||
firstname: "Creator",
|
||||
lastname: "User",
|
||||
emails: ["creator@example.com"],
|
||||
resource: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
getUserDetails,
|
||||
} from "@/features/User/userAPI";
|
||||
import { getCalendars } from "@/features/Calendars/CalendarApi";
|
||||
import { formatReduxError, toRejectedError } from "@/utils/errorUtils";
|
||||
import { formatReduxError } from "@/utils/errorUtils";
|
||||
import { normalizeCalendar } from "@/features/Calendars/utils/normalizeCalendar";
|
||||
|
||||
jest.mock("@/features/User/userAPI");
|
||||
@@ -176,16 +176,20 @@ describe("getCalendarsListAsync", () => {
|
||||
ownerId: "resource-123",
|
||||
});
|
||||
|
||||
// getUserDetails fails with 404 for the resource ID
|
||||
mockedGetUserDetails.mockRejectedValueOnce({ response: { status: 404 } });
|
||||
// getUserDetails fails with 404 for the resource ID, succeeds for the creator
|
||||
mockedGetUserDetails.mockImplementation((id: string) => {
|
||||
if (id === "resource-123")
|
||||
return Promise.reject({ response: { status: 404 } });
|
||||
if (id === "creator-456")
|
||||
return Promise.resolve({
|
||||
firstname: "Creator",
|
||||
lastname: "User",
|
||||
emails: [],
|
||||
});
|
||||
return Promise.resolve({ firstname: "", lastname: "", emails: [] });
|
||||
});
|
||||
// Then getResourceDetails is called and succeeds
|
||||
mockedGetResourceDetails.mockResolvedValueOnce({ creator: "creator-456" });
|
||||
// Then getUserDetails is called for the creator and succeeds
|
||||
mockedGetUserDetails.mockResolvedValueOnce({
|
||||
firstname: "Creator",
|
||||
lastname: "User",
|
||||
emails: [],
|
||||
});
|
||||
|
||||
const thunk = getCalendarsListAsync();
|
||||
const result = await thunk(dispatch, getState, undefined);
|
||||
|
||||
@@ -65,6 +65,14 @@ describe("Event Preview Display", () => {
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
},
|
||||
{
|
||||
cn: "Projector Room",
|
||||
cal_address: "room1@test.com",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "RESOURCE",
|
||||
},
|
||||
],
|
||||
},
|
||||
event2: {
|
||||
@@ -163,6 +171,8 @@ describe("Event Preview Display", () => {
|
||||
expect(screen.getByText(/– 10:00/)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Calendar")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Projector Room")).toBeInTheDocument();
|
||||
});
|
||||
it("calls onClose when Cancel clicked", () => {
|
||||
renderWithProviders(
|
||||
|
||||
@@ -81,6 +81,22 @@ describe("EventPopover", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "room1@example.com",
|
||||
objectType: "resource",
|
||||
emailAddresses: [
|
||||
{
|
||||
value: "room1@example.com",
|
||||
type: "default",
|
||||
},
|
||||
],
|
||||
names: [
|
||||
{
|
||||
displayName: "Room 1",
|
||||
type: "default",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
(api.post as jest.Mock).mockReturnValue({
|
||||
json: jest.fn().mockResolvedValue(mockUsers),
|
||||
@@ -211,71 +227,137 @@ describe("EventPopover", () => {
|
||||
});
|
||||
it("adds a attendee", async () => {
|
||||
jest.useFakeTimers();
|
||||
jest
|
||||
.spyOn(calendarsApi, "getCalendars")
|
||||
.mockReturnValue({ json: jest.fn() });
|
||||
renderPopover();
|
||||
fireEvent.change(screen.getByLabelText("event.form.title"), {
|
||||
target: { value: "newEvent" },
|
||||
});
|
||||
const select = screen.getByLabelText("peopleSearch.label");
|
||||
try {
|
||||
jest
|
||||
.spyOn(calendarsApi, "getCalendars")
|
||||
.mockReturnValue({ json: jest.fn() });
|
||||
renderPopover();
|
||||
fireEvent.change(screen.getByLabelText("event.form.title"), {
|
||||
target: { value: "newEvent" },
|
||||
});
|
||||
const select = screen.getByLabelText("peopleSearch.label");
|
||||
|
||||
act(() => {
|
||||
select.focus();
|
||||
fireEvent.mouseDown(select);
|
||||
userEvent.type(select, "john");
|
||||
});
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(400);
|
||||
});
|
||||
await waitFor(() => expect(api.post).toHaveBeenCalledTimes(1));
|
||||
act(() => {
|
||||
select.focus();
|
||||
fireEvent.mouseDown(select);
|
||||
userEvent.type(select, "john");
|
||||
});
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(400);
|
||||
});
|
||||
await waitFor(() => expect(api.post).toHaveBeenCalledTimes(1));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("John Doe")).toBeInTheDocument();
|
||||
});
|
||||
await act(async () => {
|
||||
userEvent.click(screen.getByText("John Doe"));
|
||||
});
|
||||
|
||||
const spy = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("John Doe")).toBeInTheDocument();
|
||||
});
|
||||
await act(async () => {
|
||||
userEvent.click(screen.getByText("John Doe"));
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
||||
const spy = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
||||
|
||||
const receivedPayload = spy.mock.calls[0][0];
|
||||
await waitFor(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(receivedPayload.cal).toEqual(
|
||||
preloadedState.calendars.list["667037022b752d0026472254/cal1"]
|
||||
);
|
||||
const receivedPayload = spy.mock.calls[0][0];
|
||||
|
||||
expect(receivedPayload.newEvent.attendee).toHaveLength(2);
|
||||
expect(receivedPayload.newEvent.attendee).toStrictEqual([
|
||||
{
|
||||
cn: "test",
|
||||
cal_address: "test@test.com",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "FALSE",
|
||||
role: "CHAIR",
|
||||
cutype: "INDIVIDUAL",
|
||||
},
|
||||
{
|
||||
cn: "John Doe",
|
||||
cal_address: "john@example.com",
|
||||
expect(receivedPayload.cal).toEqual(
|
||||
preloadedState.calendars.list["667037022b752d0026472254/cal1"]
|
||||
);
|
||||
|
||||
expect(receivedPayload.newEvent.attendee).toHaveLength(2);
|
||||
expect(receivedPayload.newEvent.attendee).toStrictEqual([
|
||||
{
|
||||
cn: "test",
|
||||
cal_address: "test@test.com",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "FALSE",
|
||||
role: "CHAIR",
|
||||
cutype: "INDIVIDUAL",
|
||||
},
|
||||
{
|
||||
cn: "John Doe",
|
||||
cal_address: "john@example.com",
|
||||
partstat: "NEEDS-ACTION",
|
||||
rsvp: "FALSE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("adds a resource", async () => {
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
renderPopover();
|
||||
fireEvent.change(screen.getByLabelText("event.form.title"), {
|
||||
target: { value: "newEventWithResource" },
|
||||
});
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "common.moreOptions" })
|
||||
);
|
||||
|
||||
const resourceCombobox = screen.getByPlaceholderText(
|
||||
"resourceSearch.placeholder"
|
||||
);
|
||||
|
||||
act(() => {
|
||||
resourceCombobox.focus();
|
||||
fireEvent.mouseDown(resourceCombobox);
|
||||
});
|
||||
await userEvent.type(resourceCombobox, "room");
|
||||
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(400);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Room 1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByText("Room 1"));
|
||||
|
||||
const spy = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const receivedPayload = spy.mock.calls[0][0];
|
||||
|
||||
expect(receivedPayload.newEvent.attendee).toHaveLength(2); // Organizer + 1 resource
|
||||
expect(receivedPayload.newEvent.attendee[1]).toStrictEqual({
|
||||
cn: "Room 1",
|
||||
cal_address: "room1@example.com",
|
||||
partstat: "NEEDS-ACTION",
|
||||
rsvp: "FALSE",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
},
|
||||
]);
|
||||
cutype: "RESOURCE",
|
||||
});
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("dispatches putEventAsync and calls onClose when Save is clicked", async () => {
|
||||
|
||||
@@ -157,6 +157,89 @@ describe("EventUpdateModal Timezone Handling", () => {
|
||||
// Verify the timezone is still preserved (should be Asia/Bangkok)
|
||||
expect(titleInput).toHaveValue("Updated Event");
|
||||
});
|
||||
|
||||
it("preserves resources when editing an event", async () => {
|
||||
const eventDateUTC = new Date("2025-01-15T07:00:00.000Z");
|
||||
|
||||
const eventData = {
|
||||
uid: "test-event-resource",
|
||||
title: "Resource Event",
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
start: eventDateUTC.toISOString(),
|
||||
end: new Date(eventDateUTC.getTime() + 3600000).toISOString(),
|
||||
timezone: "Asia/Bangkok",
|
||||
allday: false,
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
attendee: [
|
||||
{ cn: "test", cal_address: "test@test.com" },
|
||||
{
|
||||
cn: "Conference Room",
|
||||
cal_address: "room@test.com",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "RESOURCE",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const stateWithEvent = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
...preloadedState.calendars,
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
|
||||
events: {
|
||||
"test-event-resource": eventData,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockPutEvent = jest.spyOn(EventApi, "putEvent").mockResolvedValue({
|
||||
status: 201,
|
||||
url: `/calendars/667037022b752d0026472254/cal1/test-event-resource.ics`,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"test-event-resource"}
|
||||
eventData={eventData}
|
||||
/>,
|
||||
stateWithEvent
|
||||
);
|
||||
|
||||
// Edit the title
|
||||
const titleInput = screen.getByDisplayValue("Resource Event");
|
||||
fireEvent.change(titleInput, {
|
||||
target: { value: "Updated Resource Event" },
|
||||
});
|
||||
|
||||
// Click Save
|
||||
const saveButton = screen.getByRole("button", { name: "actions.save" });
|
||||
await act(async () => {
|
||||
fireEvent.click(saveButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPutEvent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const putEventCall = mockPutEvent.mock.calls[0][0];
|
||||
expect(putEventCall.title).toBe("Updated Resource Event");
|
||||
|
||||
// Check that the resource is still in the attendee list!
|
||||
const attendees = putEventCall.attendee;
|
||||
const resource = attendees.find((a: any) => a.cutype === "RESOURCE");
|
||||
expect(resource).toBeDefined();
|
||||
expect(resource!.cn).toBe("Conference Room");
|
||||
expect(resource!.cal_address).toBe("room@test.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
||||
|
||||
Reference in New Issue
Block a user