import { jest } from "@jest/globals"; import { ThunkDispatch } from "@reduxjs/toolkit"; import "@testing-library/jest-dom"; import { screen, waitFor, fireEvent } from "@testing-library/react"; import * as appHooks from "../../../src/app/hooks"; import * as eventUtils from "../../../src/components/Event/utils/eventUtils"; import * as userApi from "../../../src/features/User/userAPI"; import * as calendarUtils from "../../../src/components/Calendar/utils/calendarUtils"; import * as eventThunks from "../../../src/features/Calendars/CalendarSlice"; import EventPreviewModal from "../../../src/features/Events/EventDisplayPreview"; import EventPopover from "../../../src/features/Events/EventModal"; import EventUpdateModal from "../../../src/features/Events/EventUpdateModal"; import CalendarLayout from "../../../src/components/Calendar/CalendarLayout"; import { renderWithProviders } from "../../utils/Renderwithproviders"; import { SpiedFunction } from "jest-mock"; import { Calendars } from "../../../src/features/Calendars/CalendarTypes"; import { CalendarEvent } from "../../../src/features/Events/EventsTypes"; import { DateSelectArg } from "@fullcalendar/core"; describe("Update tempcalendars called with correct params", () => { 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); let refreshCalendarsSpy: SpiedFunction< ( dispatch: ThunkDispatch, calendars: Calendars[], calendarRange: { start: Date; end: Date }, calType?: "temp" ) => Promise >; let refreshSingularCalendarSpy: SpiedFunction< ( dispatch: ThunkDispatch, calendar: Calendars, calendarRange: { start: Date; end: Date }, calType?: "temp" ) => Promise >; let updateTempCalendarSpy: SpiedFunction< ( tempcalendars: Record, event: CalendarEvent, dispatch: ThunkDispatch, calendarRange: { start: Date; end: Date } ) => Promise >; beforeEach(() => { jest.clearAllMocks(); const dispatch = jest.fn() as ThunkDispatch; jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(dispatch); refreshCalendarsSpy = jest.spyOn(eventUtils, "refreshCalendars"); updateTempCalendarSpy = jest.spyOn(calendarUtils, "updateTempCalendar"); refreshSingularCalendarSpy = jest.spyOn( eventUtils, "refreshSingularCalendar" ); }); afterEach(() => { refreshCalendarsSpy.mockRestore(); updateTempCalendarSpy.mockRestore(); refreshSingularCalendarSpy.mockRestore(); }); const createPreloadedState = (withTempList = true) => ({ user: { userData: { sub: "test", email: "owner@test.com", sid: "mockSid", openpaasId: "667037022b752d0026472254", }, organiserData: { cn: "Owner", cal_address: "owner@test.com", }, tokens: { accessToken: "token", }, }, calendars: { list: { "667037022b752d0026472254/cal1": { name: "Calendar 1", id: "667037022b752d0026472254/cal1", color: "#FF0000", ownerEmails: ["owner@test.com"], events: { event1: { id: "event1", calId: "667037022b752d0026472254/cal1", uid: "event1", title: "Test Event", start: start.toISOString(), end: end.toISOString(), attendee: [{ cal_address: "attendee@test.com" }], organizer: { cn: "Owner", cal_address: "owner@test.com", }, }, }, }, }, templist: withTempList ? { temp1: { id: "temp1", name: "Temp Calendar", color: "#00FF00", events: {}, ownerEmails: ["attendee@test.com"], }, } : undefined, pending: false, }, }); it("should call updateTempCalendar with correct params after event deletion", async () => { const preloadedState = createPreloadedState(true); const mockOnClose = jest.fn(); renderWithProviders( , preloadedState ); await waitFor(() => { fireEvent.click(screen.getByTestId("MoreVertIcon")); expect(screen.getByText("eventPreview.deleteEvent")).toBeInTheDocument(); }); const deleteMenuItem = screen.getByText("eventPreview.deleteEvent"); fireEvent.click(deleteMenuItem); await waitFor(() => expect(updateTempCalendarSpy).toHaveBeenCalledWith( expect.objectContaining({ temp1: expect.objectContaining({ id: "temp1" }), }), expect.objectContaining({ attendee: [ expect.objectContaining({ cal_address: "attendee@test.com" }), ], }), expect.any(Function), expect.objectContaining({ start: expect.any(Date), end: expect.any(Date), }) ) ); await waitFor(() => expect(refreshSingularCalendarSpy).toHaveBeenCalledWith( expect.any(Function), expect.objectContaining({ id: "temp1" }), expect.objectContaining({ start: expect.any(Date), end: expect.any(Date), }), "temp" ) ); }); it("should NOT call updateTempCalendar when templist is undefined", async () => { const preloadedState = createPreloadedState(false); const mockOnClose = jest.fn(); renderWithProviders( , preloadedState ); await waitFor(() => { fireEvent.click(screen.getByTestId("MoreVertIcon")); expect(screen.getByText("eventPreview.deleteEvent")).toBeInTheDocument(); }); const deleteMenuItem = screen.getByText("eventPreview.deleteEvent"); fireEvent.click(deleteMenuItem); await waitFor(() => { expect(updateTempCalendarSpy).not.toHaveBeenCalled(); expect(refreshSingularCalendarSpy).not.toHaveBeenCalled(); }); }); it("should call updateTempCalendar with correct params after event creation", async () => { const preloadedState = createPreloadedState(true); const defaultSelectedRange = { startStr: "2025-07-18T09:00Z", endStr: "2025-07-18T10:00Z", start: new Date("2025-07-18T09:00Z"), end: new Date("2025-07-18T10:00Z"), allDay: false, resource: undefined, } as unknown as DateSelectArg; jest.spyOn(userApi, "searchUsers"); renderWithProviders( , preloadedState ); await waitFor(() => { expect(screen.getByRole("dialog")).toBeInTheDocument(); }); const titleInput = screen.getByLabelText(/title/i); fireEvent.change(titleInput, { target: { value: "New Event" } }); const attendeeInput = screen.getByLabelText(/peopleSearch.label/i); fireEvent.change(attendeeInput, { target: { value: "attendee@test.com" } }); fireEvent.keyDown(attendeeInput, { key: "Enter", code: "Enter" }); const saveButton = screen.getByText(/save/i); fireEvent.click(saveButton); await waitFor(() => expect(updateTempCalendarSpy).toHaveBeenCalledWith( expect.objectContaining({ temp1: expect.objectContaining({ id: "temp1" }), }), expect.objectContaining({ title: "New Event", }), expect.any(Function), expect.objectContaining({ start: expect.any(Date), end: expect.any(Date), }) ) ); await waitFor(() => expect(refreshSingularCalendarSpy).toHaveBeenCalledWith( expect.any(Function), expect.objectContaining({ id: "temp1" }), expect.objectContaining({ start: expect.any(Date), end: expect.any(Date), }), "temp" ) ); }); it("should call updateTempCalendar with correct params after event update", async () => { const preloadedState = createPreloadedState(true); const mockOnClose = jest.fn(); renderWithProviders( , preloadedState ); await waitFor(() => { expect(screen.getByRole("dialog")).toBeInTheDocument(); }); const saveButton = screen.getByText(/save/i); fireEvent.click(saveButton); await waitFor(() => expect(updateTempCalendarSpy).toHaveBeenCalledWith( expect.objectContaining({ temp1: expect.objectContaining({ id: "temp1" }), }), expect.objectContaining({ attendee: [ expect.objectContaining({ cal_address: "attendee@test.com" }), ], }), expect.any(Function), expect.objectContaining({ start: expect.any(Date), end: expect.any(Date), }) ) ); await waitFor(() => expect(refreshSingularCalendarSpy).toHaveBeenCalledWith( expect.any(Function), expect.objectContaining({ id: "temp1" }), expect.objectContaining({ start: expect.any(Date), end: expect.any(Date), }), "temp" ) ); }); it("should call updateTempCalendar with correct params after recurring event instance update", async () => { const preloadedStateWithRecurring = { ...createPreloadedState(true), calendars: { ...createPreloadedState(true).calendars, list: { "667037022b752d0026472254/cal1": { name: "Calendar 1", id: "667037022b752d0026472254/cal1", color: "#FF0000", ownerEmails: ["owner@test.com"], events: { event1: { id: "event1", calId: "667037022b752d0026472254/cal1", uid: "event1", title: "Recurring Event", start: start.toISOString(), end: end.toISOString(), attendee: [{ cal_address: "attendee@test.com" }], rrule: { freq: "WEEKLY", }, organizer: { cn: "Owner", cal_address: "owner@test.com", }, }, }, }, }, }, }; const mockOnClose = jest.fn(); renderWithProviders( , preloadedStateWithRecurring ); await waitFor(() => { expect(screen.getByRole("dialog")).toBeInTheDocument(); }); const saveButton = screen.getByText(/save/i); fireEvent.click(saveButton); await waitFor(() => expect(updateTempCalendarSpy).toHaveBeenCalledWith( expect.objectContaining({ temp1: expect.objectContaining({ id: "temp1" }), }), expect.objectContaining({ attendee: [ expect.objectContaining({ cal_address: "attendee@test.com" }), ], }), expect.any(Function), expect.objectContaining({ start: expect.any(Date), end: expect.any(Date), }) ) ); await waitFor(() => expect(refreshSingularCalendarSpy).toHaveBeenCalledWith( expect.any(Function), expect.objectContaining({ id: "temp1" }), expect.objectContaining({ start: expect.any(Date), end: expect.any(Date), }), "temp" ) ); }); it("should call updateTempCalendar with correct params after recurring series update", async () => { const preloadedStateWithRecurring = { ...createPreloadedState(true), calendars: { ...createPreloadedState(true).calendars, list: { "667037022b752d0026472254/cal1": { name: "Calendar 1", id: "667037022b752d0026472254/cal1", color: "#FF0000", ownerEmails: ["owner@test.com"], events: { event1: { id: "event1", calId: "667037022b752d0026472254/cal1", uid: "event1", title: "Recurring Event", start: start.toISOString(), end: end.toISOString(), attendee: [{ cal_address: "attendee@test.com" }], rrule: { freq: "DAILY", }, organizer: { cn: "Owner", cal_address: "owner@test.com", }, }, }, }, }, }, }; const mockOnClose = jest.fn(); renderWithProviders( , preloadedStateWithRecurring ); await waitFor(() => { expect(screen.getByRole("dialog")).toBeInTheDocument(); }); const saveButton = screen.getByText(/save/i); fireEvent.click(saveButton); await waitFor(() => expect(updateTempCalendarSpy).toHaveBeenCalledWith( expect.objectContaining({ temp1: expect.objectContaining({ id: "temp1" }), }), expect.objectContaining({ attendee: [ expect.objectContaining({ cal_address: "attendee@test.com" }), ], }), expect.any(Function), expect.objectContaining({ start: expect.any(Date), end: expect.any(Date), }) ) ); await waitFor(() => expect(refreshSingularCalendarSpy).toHaveBeenCalledWith( expect.any(Function), expect.objectContaining({ id: "temp1" }), expect.objectContaining({ start: expect.any(Date), end: expect.any(Date), }), "temp" ) ); }); it("should call refreshCalendars with 'temp' param in CalendarLayout handleRefresh", async () => { const preloadedState = createPreloadedState(true); renderWithProviders(, preloadedState); fireEvent.click(screen.getByTestId("RefreshIcon")); await waitFor(() => { // Should call refreshCalendars for regular calendars expect(refreshCalendarsSpy).toHaveBeenCalledWith( expect.any(Function), expect.any(Array), expect.objectContaining({ start: expect.any(Date), end: expect.any(Date), }) ); // Should also call refreshCalendars with 'temp' for templist expect(refreshCalendarsSpy).toHaveBeenCalledWith( expect.any(Function), expect.arrayContaining([expect.objectContaining({ id: "temp1" })]), expect.objectContaining({ start: expect.any(Date), end: expect.any(Date), }), "temp" ); }); }); it("should only refresh temp calendars where attendees match ownerEmails", async () => { const preloadedStateMultipleTempCals = { ...createPreloadedState(true), calendars: { ...createPreloadedState(true).calendars, templist: { temp1: { id: "temp1", name: "Temp Calendar 1", color: "#00FF00", events: {}, ownerEmails: ["attendee@test.com"], // matches event attendee }, temp2: { id: "temp2", name: "Temp Calendar 2", color: "#0000FF", events: {}, ownerEmails: ["other@test.com"], // does NOT match event attendee }, }, }, }; const mockOnClose = jest.fn(); renderWithProviders( , preloadedStateMultipleTempCals ); await waitFor(() => { fireEvent.click(screen.getByTestId("MoreVertIcon")); expect(screen.getByText("eventPreview.deleteEvent")).toBeInTheDocument(); }); const deleteMenuItem = screen.getByText("eventPreview.deleteEvent"); fireEvent.click(deleteMenuItem); await waitFor(() => { // Should only refresh temp1, not temp2 const temp1Calls = refreshSingularCalendarSpy.mock.calls.filter( (call) => call[1]?.id === "temp1" ); const temp2Calls = refreshSingularCalendarSpy.mock.calls.filter( (call) => call[1]?.id === "temp2" ); expect(temp1Calls.length).toBeGreaterThan(0); expect(temp2Calls.length).toBe(0); }); }); it("should call emptyEventsCal with correct params before refreshing", async () => { const emptyEventsCalSpy = jest.spyOn(eventThunks, "emptyEventsCal"); const preloadedState = createPreloadedState(true); const mockOnClose = jest.fn(); renderWithProviders( , preloadedState ); await waitFor(() => { fireEvent.click(screen.getByTestId("MoreVertIcon")); expect(screen.getByText("eventPreview.deleteEvent")).toBeInTheDocument(); }); const deleteMenuItem = screen.getByText("eventPreview.deleteEvent"); fireEvent.click(deleteMenuItem); await waitFor(() => { // emptyEventsCal should be called with calType: "temp" for singular calendar refresh expect(emptyEventsCalSpy).toHaveBeenCalledWith( expect.objectContaining({ calId: "temp1", calType: "temp", }) ); }); emptyEventsCalSpy.mockRestore(); }); it("should handle temp calendar with multiple ownerEmails where one matches", async () => { const preloadedStateMultipleOwners = { ...createPreloadedState(true), calendars: { ...createPreloadedState(true).calendars, list: { "667037022b752d0026472254/cal1": { name: "Calendar 1", id: "667037022b752d0026472254/cal1", color: "#FF0000", ownerEmails: ["owner@test.com"], events: { event1: { id: "event1", calId: "667037022b752d0026472254/cal1", uid: "event1", title: "Test Event", start: start.toISOString(), end: end.toISOString(), attendee: [{ cal_address: "attendee@test.com" }], organizer: { cn: "Owner", cal_address: "owner@test.com", }, }, }, }, }, templist: { temp1: { id: "temp1", name: "Shared Calendar", color: "#00FF00", events: {}, ownerEmails: [ "someoneelse@test.com", "attendee@test.com", // matches "anotherperson@test.com", ], }, }, }, }; const mockOnClose = jest.fn(); renderWithProviders( , preloadedStateMultipleOwners ); await waitFor(() => { fireEvent.click(screen.getByTestId("MoreVertIcon")); expect(screen.getByText("eventPreview.deleteEvent")).toBeInTheDocument(); }); const deleteMenuItem = screen.getByText("eventPreview.deleteEvent"); fireEvent.click(deleteMenuItem); await waitFor(() => { // Should still refresh temp1 because one ownerEmail matches const temp1Calls = refreshSingularCalendarSpy.mock.calls.filter( (call) => call[1]?.id === "temp1" ); expect(temp1Calls.length).toBeGreaterThan(0); expect(temp1Calls[0][3]).toBe("temp"); }); }); it("should update multiple temp calendars when event has multiple attendees matching different ownerEmails", async () => { const preloadedStateMultipleAttendees = { ...createPreloadedState(true), calendars: { ...createPreloadedState(true).calendars, list: { "667037022b752d0026472254/cal1": { name: "Calendar 1", id: "667037022b752d0026472254/cal1", color: "#FF0000", ownerEmails: ["owner@test.com"], events: { event1: { id: "event1", calId: "667037022b752d0026472254/cal1", uid: "event1", title: "Test Event", start: start.toISOString(), end: end.toISOString(), attendee: [ { cal_address: "attendee1@test.com" }, { cal_address: "attendee2@test.com" }, { cal_address: "attendee3@test.com" }, ], organizer: { cn: "Owner", cal_address: "owner@test.com", }, }, }, }, }, templist: { temp1: { id: "temp1", name: "Temp Calendar 1", color: "#00FF00", events: {}, ownerEmails: ["attendee1@test.com"], }, temp2: { id: "temp2", name: "Temp Calendar 2", color: "#0000FF", events: {}, ownerEmails: ["attendee2@test.com"], }, temp3: { id: "temp3", name: "Temp Calendar 3", color: "#FF00FF", events: {}, ownerEmails: ["attendee3@test.com"], }, temp4: { id: "temp4", name: "Temp Calendar 4", color: "#FFFF00", events: {}, ownerEmails: ["nonmatching@test.com"], // should NOT be refreshed }, }, }, }; const mockOnClose = jest.fn(); renderWithProviders( , preloadedStateMultipleAttendees ); await waitFor(() => { fireEvent.click(screen.getByTestId("MoreVertIcon")); expect(screen.getByText("eventPreview.deleteEvent")).toBeInTheDocument(); }); const deleteMenuItem = screen.getByText("eventPreview.deleteEvent"); fireEvent.click(deleteMenuItem); await waitFor(() => { // Should be called once with all templist expect(updateTempCalendarSpy).toHaveBeenCalledWith( expect.objectContaining({ temp1: expect.objectContaining({ id: "temp1" }), temp2: expect.objectContaining({ id: "temp2" }), temp3: expect.objectContaining({ id: "temp3" }), temp4: expect.objectContaining({ id: "temp4" }), }), expect.objectContaining({ attendee: expect.arrayContaining([ expect.objectContaining({ cal_address: "attendee1@test.com" }), expect.objectContaining({ cal_address: "attendee2@test.com" }), expect.objectContaining({ cal_address: "attendee3@test.com" }), ]), }), expect.any(Function), expect.objectContaining({ start: expect.any(Date), end: expect.any(Date), }) ); }); await waitFor(() => { // Should refresh temp1, temp2, and temp3 (matching attendees) const temp1Calls = refreshSingularCalendarSpy.mock.calls.filter( (call) => call[1]?.id === "temp1" ); const temp2Calls = refreshSingularCalendarSpy.mock.calls.filter( (call) => call[1]?.id === "temp2" ); const temp3Calls = refreshSingularCalendarSpy.mock.calls.filter( (call) => call[1]?.id === "temp3" ); const temp4Calls = refreshSingularCalendarSpy.mock.calls.filter( (call) => call[1]?.id === "temp4" ); expect(temp1Calls.length).toBeGreaterThan(0); expect(temp2Calls.length).toBeGreaterThan(0); expect(temp3Calls.length).toBeGreaterThan(0); expect(temp4Calls.length).toBe(0); // Should NOT be refreshed // Verify all matching calendars were refreshed with 'temp' param expect(temp1Calls[0][3]).toBe("temp"); expect(temp2Calls[0][3]).toBe("temp"); expect(temp3Calls[0][3]).toBe("temp"); }); }); });