import * as appHooks from "@/app/hooks"; import { AppDispatch } from "@/app/store"; import CalendarApp from "@/components/Calendar/Calendar"; import { createEventHandlers, EventHandlersProps, } from "@/components/Calendar/handlers/eventHandlers"; import * as eventThunks from "@/features/Calendars/services"; import EventUpdateModal from "@/features/Events/EventUpdateModal"; import { CalendarApi } from "@fullcalendar/core"; import { jest } from "@jest/globals"; import "@testing-library/jest-dom"; import { act, fireEvent, screen, waitFor, within, } from "@testing-library/react"; import { renderWithProviders } from "../utils/Renderwithproviders"; describe("CalendarApp integration", () => { const today = new Date(); const start = new Date(today); start.setHours(10, 0, 0, 0); const end = new Date(today); end.setHours(11, 0, 0, 0); beforeEach(() => { jest.clearAllMocks(); const dispatch = jest.fn() as AppDispatch; jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(dispatch); }); const renderCalendar = () => { const preloadedState = { user: { userData: { sub: "test", email: "test@test.com", sid: "mockSid", openpaasId: "667037022b752d0026472254", }, tokens: { accessToken: "token" }, // required to avoid redirect }, calendars: { list: { "667037022b752d0026472254/cal1": { name: "Calendar 1", id: "667037022b752d0026472254/cal1", color: { light: "#FFFFFF", dark: "#000000" }, ownerEmails: ["alice@example.com"], events: { event1: { id: "event1", calId: "667037022b752d0026472254/cal1", uid: "event1", title: "Test Event", start: start.toISOString(), end: end.toISOString(), partstat: "ACCEPTED", organizer: { cn: "Alice", cal_address: "alice@example.com", }, attendee: [ { cn: "Alice", partstat: "ACCEPTED", rsvp: "TRUE", role: "REQ-PARTICIPANT", cutype: "INDIVIDUAL", cal_address: "alice@example.com", }, ], }, }, }, }, pending: false, }, }; const mockCalendarRef = { current: null }; renderWithProviders( , preloadedState ); }; it("renders the event on the calendar and calendarRef works", async () => { const dispatch = appHooks.useAppDispatch(); renderCalendar(); const calendarRef: React.RefObject = (window as any) .__calendarRef; const calendarApi = calendarRef.current; // Wait for the FullCalendar DOM to populate const eventEl = await screen.findByText( "Test Event", {}, { timeout: 3000 } ); expect(eventEl).toBeInTheDocument(); act(() => { if (calendarApi) { const fcEvent = calendarApi.getEventById("event1"); expect(fcEvent?.title).toBe("Test Event"); const oldEnd = new Date(today.getTime() + 3600000); // +1 hour const newEnd = new Date(oldEnd.getTime() + 1800000); // +30 min fcEvent?.setEnd(newEnd); waitFor(() => expect(dispatch).toHaveBeenCalled()); } }); }); const createPreloadedState = (eventProps = {}) => ({ user: { userData: { sub: "test", email: "test@test.com", sid: "mockSid", openpaasId: "667037022b752d0026472254", }, tokens: { accessToken: "token", }, }, calendars: { list: { "667037022b752d0026472254/cal1": { name: "Calendar 1", id: "667037022b752d0026472254/cal1", color: { light: "#FFFFFF", dark: "#000000" }, ownerEmails: ["alice@example.com"], events: { event1: { id: "event1", calId: "667037022b752d0026472254/cal1", uid: "event1", start: new Date().toISOString(), end: new Date(Date.now() + 3600000).toISOString(), partstat: "ACCEPTED", organizer: { cn: "Alice", cal_address: "alice@example.com", }, attendee: [ { cn: "Alice", partstat: "ACCEPTED", rsvp: "TRUE", role: "REQ-PARTICIPANT", cutype: "INDIVIDUAL", cal_address: "alice@example.com", }, ], ...eventProps, }, }, }, }, pending: false, }, }); it("renders lock icon for private events on the calendar", async () => { Object.defineProperty(HTMLElement.prototype, "offsetWidth", { configurable: true, value: 120, }); const preloadedState = createPreloadedState({ class: "PRIVATE", title: "Private Event", }); const mockCalendarRef = { current: null }; renderWithProviders( , preloadedState ); const card = screen.getByTestId("event-card-event1"); const lockIcon = within(card).getByTestId("LockOutlineIcon"); expect(lockIcon).toBeInTheDocument(); }); it("renders lock icon for confidential events on the calendar", async () => { Object.defineProperty(HTMLElement.prototype, "offsetWidth", { configurable: true, value: 120, }); const preloadedState = createPreloadedState({ class: "CONFIDENTIAL", title: "Confidential Event", }); const mockCalendarRef = { current: null }; renderWithProviders( , preloadedState ); const card = screen.getByTestId("event-card-event1"); const lockIcon = within(card).getByTestId("LockOutlineIcon"); expect(lockIcon).toBeInTheDocument(); }); it("does NOT render a lock icon for public events on the calendar", async () => { Object.defineProperty(HTMLElement.prototype, "offsetWidth", { configurable: true, value: 120, }); const preloadedState = createPreloadedState({ class: "PUBLIC", title: "Public Event", }); const mockCalendarRef = { current: null }; renderWithProviders( , preloadedState ); const card = screen.getByTestId("event-card-event1"); const lockIcon = within(card).queryByTestId("LockOutlineIcon"); expect(lockIcon).not.toBeInTheDocument(); }); it("does render a title for events without any attendees or user as organizer", async () => { const mockCalendarRef = { current: null }; renderWithProviders(, { user: { userData: { sub: "test", email: "test@test.com", sid: "mockSid", openpaasId: "667037022b752d0026472254", }, tokens: { accessToken: "token", }, }, calendars: { list: { "667037022b752d0026472254/cal1": { name: "Calendar 1", id: "667037022b752d0026472254/cal1", color: { light: "#FF0000", dark: "#000" }, ownerEmails: ["alice@example.com"], events: { event1: { id: "event1", calId: "667037022b752d0026472254/cal1", uid: "event1", start: new Date().toISOString(), end: new Date(Date.now() + 3600000).toISOString(), partstat: "ACCEPTED", organizer: { cn: "Alice", cal_address: "alice@example.com", }, class: "PUBLIC", title: "Public Event", }, }, }, }, pending: false, }, }); expect(screen.getByText("Public Event")).toBeInTheDocument(); }); describe("BUGFIX", () => { const preloadedState = { user: { userData: { sub: "test", email: "test@test.com", sid: "mockSid", openpaasId: "667037022b752d0026472254", }, tokens: { accessToken: "token", }, }, calendars: { list: { "667037022b752d0026472254/cal1": { name: "Calendar 1", id: "667037022b752d0026472254/cal1", color: { light: "#FFFFFF", dark: "#000000" }, ownerEmails: ["alice@example.com"], events: { event1: { id: "event1", calId: "667037022b752d0026472254/cal1", uid: "event1", title: "Original Event", start: new Date("2025-11-14T10:31:00.000Z").toISOString(), end: new Date("2025-11-14T11:31:00.000Z").toISOString(), class: "PUBLIC", partstat: "ACCEPTED", sequence: 2, organizer: { cn: "Alice", cal_address: "alice@example.com", }, attendee: [ { cn: "Alice", partstat: "ACCEPTED", rsvp: "TRUE", role: "CHAIR", cutype: "INDIVIDUAL", cal_address: "alice@example.com", }, { cn: "Bob", partstat: "ACCEPTED", rsvp: "TRUE", role: "REQ-PARTICIPANT", cutype: "INDIVIDUAL", cal_address: "bob@example.com", }, ], }, }, }, }, pending: false, }, }; it("keeps all attendees event participation on title update", async () => { const updateSpy = jest.spyOn(eventThunks, "putEventAsync"); const onClose = jest.fn(); renderWithProviders( , preloadedState ); const titleInput = await screen.findByDisplayValue("Original Event"); await act(async () => { fireEvent.change(titleInput, "Updated Event"); }); const saveButton = screen.getByRole("button", { name: /save/i }); await act(async () => { saveButton.click(); }); await waitFor(() => expect(updateSpy).toHaveBeenCalled()); const dispatchedCalls = updateSpy.mock.calls; expect(dispatchedCalls.length).toBeGreaterThan(0); const updatedEvent = dispatchedCalls[0][0].newEvent; // Ensure organizer attendee info is preserved const organizerAttendee = updatedEvent?.attendee?.find( (a: any) => a.cal_address === "alice@example.com" ); expect(organizerAttendee).toBeTruthy(); expect(organizerAttendee?.partstat).toBe("ACCEPTED"); expect(organizerAttendee?.role).toBe("CHAIR"); // Ensure normal attendee info is preserved too const normalAttendee = updatedEvent?.attendee?.find( (a: any) => a.cal_address === "bob@example.com" ); expect(normalAttendee).toBeTruthy(); expect(normalAttendee?.partstat).toBe("ACCEPTED"); expect(normalAttendee?.role).toBe("REQ-PARTICIPANT"); // Verify SEQUENCE is incremented expect(updatedEvent?.sequence).toBe(3); // 2 + 1 }); it("changes normal attendee to need action on time update and no organizer changes", async () => { const updateSpy = jest.spyOn(eventThunks, "putEventAsync"); const onClose = jest.fn(); renderWithProviders( , preloadedState ); const startDateInput = await screen.findByTestId("start-time-input"); await act(async () => { fireEvent.change(startDateInput, { target: { value: "08:00" }, }); fireEvent.blur(startDateInput); }); // Wait for blur handler to complete await act(async () => { await new Promise((resolve) => setTimeout(resolve, 150)); }); const saveButton = screen.getByRole("button", { name: /save/i }); await act(async () => { saveButton.click(); }); await waitFor(() => expect(updateSpy).toHaveBeenCalled()); const dispatchedCalls = updateSpy.mock.calls; expect(dispatchedCalls.length).toBeGreaterThan(0); const updatedEvent = dispatchedCalls[0][0].newEvent; // Ensure organizer attendee info is preserved const organizerAttendee = updatedEvent?.attendee?.find( (a: any) => a.cal_address === "alice@example.com" ); expect(organizerAttendee).toBeTruthy(); expect(organizerAttendee?.partstat).toBe("ACCEPTED"); expect(organizerAttendee?.role).toBe("CHAIR"); // Ensure normal attendee partstat is updated const normalAttendee = updatedEvent?.attendee?.find( (a: any) => a.cal_address === "bob@example.com" ); expect(normalAttendee).toBeTruthy(); expect(normalAttendee?.partstat).toBe("NEEDS-ACTION"); expect(normalAttendee?.role).toBe("REQ-PARTICIPANT"); // Verify SEQUENCE is incremented expect(updatedEvent?.sequence).toBe(3); // 2 + 1 }); it("update event attendees on drag", async () => { const mockDispatch = jest.fn(); const updateSpy = jest.spyOn(eventThunks, "putEventAsync"); const eventHandlers = createEventHandlers({ setSelectedRange: jest.fn(), setOpenEventModal: jest.fn(), setTempEvent: jest.fn(), setOpenEventDisplay: jest.fn(), dispatch: mockDispatch, calendarRange: { start: new Date(), end: new Date() }, setEventDisplayedId: jest.fn(), setEventDisplayedCalId: jest.fn(), setEventDisplayedTemp: jest.fn(), calendars: preloadedState.calendars.list, setSelectedEvent: jest.fn(), setAfterChoiceFunc: jest.fn(), setOpenEditModePopup: jest.fn(), } as unknown as EventHandlersProps); const mockArg = { event: { _def: { extendedProps: { uid: "event1", calId: "667037022b752d0026472254/cal1", }, }, }, // drag event → move by 1 day delta: { years: 0, months: 0, days: 1, milliseconds: 0 }, }; renderCalendar(); eventHandlers.handleEventDrop(mockArg); expect(updateSpy).toHaveBeenCalled(); // Extract the dispatched update event const dispatched = updateSpy.mock.calls[0]; expect(dispatched).toBeTruthy(); const updatePayload = dispatched[0]; const updatedEvent = updatePayload.newEvent; // Organizer should remain unchanged const organizer = updatedEvent.attendee.find( (a: any) => a.cal_address === "alice@example.com" ); expect(organizer?.partstat).toBe("ACCEPTED"); // Normal attendee must become NEEDS-ACTION const normal = updatedEvent.attendee.find( (a: any) => a.cal_address === "bob@example.com" ); expect(normal?.partstat).toBe("NEEDS-ACTION"); // Verify SEQUENCE is incremented expect(updatedEvent?.sequence).toBe(3); // 2 + 1 }); it("update event attendees on resize", async () => { const mockDispatch = jest.fn(); const updateSpy = jest.spyOn(eventThunks, "putEventAsync"); const eventHandlers = createEventHandlers({ setSelectedRange: jest.fn(), setOpenEventModal: jest.fn(), setTempEvent: jest.fn(), setOpenEventDisplay: jest.fn(), dispatch: mockDispatch, calendarRange: { start: new Date(), end: new Date() }, setEventDisplayedId: jest.fn(), setEventDisplayedCalId: jest.fn(), setEventDisplayedTemp: jest.fn(), calendars: preloadedState.calendars.list, setSelectedEvent: jest.fn(), setAfterChoiceFunc: jest.fn(), setOpenEditModePopup: jest.fn(), } as unknown as EventHandlersProps); const mockArg = { event: { _def: { extendedProps: { uid: "event1", calId: "667037022b752d0026472254/cal1", }, }, }, startDelta: { years: 0, months: 0, days: 0, milliseconds: 0 }, endDelta: { years: 0, months: 0, days: 0, milliseconds: 3600000 }, // 1 hour }; renderCalendar(); eventHandlers.handleEventResize(mockArg); expect(updateSpy).toHaveBeenCalled(); // Extract the dispatched update event const dispatched = updateSpy.mock.calls[0]; expect(dispatched).toBeTruthy(); const updatePayload = dispatched[0]; const updatedEvent = updatePayload.newEvent; // Organizer should remain unchanged const organizer = updatedEvent.attendee.find( (a: any) => a.cal_address === "alice@example.com" ); expect(organizer?.partstat).toBe("ACCEPTED"); // Normal attendee must become NEEDS-ACTION const normal = updatedEvent.attendee.find( (a: any) => a.cal_address === "bob@example.com" ); expect(normal?.partstat).toBe("NEEDS-ACTION"); // Verify SEQUENCE is incremented expect(updatedEvent?.sequence).toBe(3); // 2 + 1 }); }); });