import CalendarApp from "@/components/Calendar/Calendar"; import CalendarLayout from "@/components/Calendar/CalendarLayout"; import * as calendarDetailThunks from "@/features/Calendars/services"; import * as servicesModule from "@/features/Calendars/services"; import { searchUsers } from "@/features/User/userAPI"; import { act, fireEvent, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { useRef } from "react"; import { renderWithProviders } from "../utils/Renderwithproviders"; jest.mock("@/features/User/userAPI"); const mockedSearchUsers = searchUsers as jest.MockedFunction< typeof searchUsers >; // Test wrapper component to provide calendarRef function CalendarTestWrapper() { const calendarRef = useRef(null); return ; } describe("CalendarSelection", () => { beforeEach(() => { localStorage.clear(); }); afterEach(() => { jest.useRealTimers(); }); 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); const preloadedState = { user: { userData: { sub: "test", email: "test@test.com", sid: "mockSid", openpaasId: "user1", }, tokens: { accessToken: "token" }, // required to avoid redirect }, calendars: { list: { "user1/cal1": { name: "Calendar personal", id: "user1/cal1", color: { light: "#FF0000", dark: "#000" }, owner: { emails: ["alice@example.com"] }, events: { event1: { id: "event1", calId: "user1/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", }, ], }, }, }, "user2/cal1": { name: "Calendar delegated", delegated: true, id: "user2/cal1", color: { light: "#FF0000", dark: "#000" }, owner: { emails: ["alice@example.com"], lastname: "alice" }, events: { event1: { id: "event1", calId: "user2/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", }, ], }, }, }, "user3/cal1": { name: "Calendar shared", id: "user3/cal1", color: { light: "#FF0000", dark: "#000" }, owner: { emails: ["alice@example.com"], lastname: "alice" }, events: { event1: { id: "event1", calId: "user3/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: true, }, }; it("renders calendars", async () => { const mockCalendarRef = { current: null }; await act(async () => { renderWithProviders( , preloadedState ); }); expect(screen.getByText("calendar.personal")).toBeInTheDocument(); expect(screen.getByText("calendar.delegated")).toBeInTheDocument(); expect(screen.getByText("calendar.other")).toBeInTheDocument(); expect(screen.getByLabelText("Calendar personal")).toBeInTheDocument(); expect(screen.getByLabelText("Calendar delegated")).toBeInTheDocument(); expect(screen.getByLabelText("Calendar shared")).toBeInTheDocument(); }); it("open accordeon when clicking on button only", async () => { const mockCalendarRef = { current: null }; await act(async () => { renderWithProviders( , preloadedState ); }); expect(screen.getByText("calendar.personal")).toBeInTheDocument(); expect(screen.getByText("calendar.delegated")).toBeInTheDocument(); expect(screen.getByText("calendar.other")).toBeInTheDocument(); expect(screen.getByLabelText("Calendar personal")).toBeInTheDocument(); expect(screen.getByLabelText("Calendar delegated")).toBeInTheDocument(); expect(screen.getByLabelText("Calendar shared")).toBeInTheDocument(); const sharedAccordionSummary = screen .getByText("calendar.other") .closest(".MuiAccordionSummary-root"); const addButton = screen.getAllByTestId("AddIcon")[2]; await act(async () => { fireEvent.click(addButton); }); expect(sharedAccordionSummary).toHaveAttribute("aria-expanded", "true"); await act(async () => { fireEvent.click(addButton); }); expect(sharedAccordionSummary).toHaveAttribute("aria-expanded", "true"); }); it("renders calendars with local storage", async () => { const mockCalendarRef = { current: null }; localStorage.setItem( "selectedCalendars", JSON.stringify(Object.keys(preloadedState.calendars.list)) ); await act(async () => { renderWithProviders( , preloadedState ); }); expect(screen.getByLabelText("Calendar personal")).toBeChecked(); expect(screen.getByLabelText("Calendar delegated")).toBeChecked(); expect(screen.getByLabelText("Calendar shared")).toBeChecked(); }); it("persist selected calendars in local storage", async () => { const mockCalendarRef = { current: null }; await act(async () => { renderWithProviders( , preloadedState ); }); expect(screen.getByLabelText("Calendar personal")).toBeChecked(); expect(screen.getByLabelText("Calendar delegated")).not.toBeChecked(); expect(screen.getByLabelText("Calendar shared")).not.toBeChecked(); await act(async () => { fireEvent.click(screen.getByLabelText("Calendar personal")); fireEvent.click(screen.getByLabelText("Calendar shared")); }); expect(localStorage.getItem("selectedCalendars")).toBe('["user3/cal1"]'); }); }); describe("calendar Availability search", () => { beforeEach(() => { localStorage.clear(); }); afterEach(() => { jest.useRealTimers(); }); const preloadedState = { user: { userData: { sub: "test", email: "test@test.com", sid: "mockSid", openpaasId: "user1", }, organiserData: { cn: "test", cal_address: "test@test.com", }, tokens: { accessToken: "token" }, }, calendars: { list: { "user1/cal1": { name: "Calendar personal", id: "user1/cal1", color: { light: "#FF0000", dark: "#000" }, owner: { emails: ["alice@example.com"] }, events: {}, }, }, pending: true, templist: {}, }, }; it("imports temporary calendars when selecting new users", async () => { const spy = jest .spyOn(servicesModule, "getTempCalendarsListAsync") .mockImplementation((payload) => { return () => Promise.resolve(payload) as any; }); mockedSearchUsers.mockResolvedValueOnce([ { email: "newuser@example.com", displayName: "New User", avatarUrl: "image.png", openpaasId: "1234567890", }, ]); await act(async () => renderWithProviders(, preloadedState) ); const input = screen.getByPlaceholderText( "peopleSearch.availabilityPlaceholder" ); act(() => { userEvent.type(input, "New"); }); const option = await screen.findByText("New User"); await act(async () => { fireEvent.click(option); }); expect(spy).toHaveBeenCalled(); }); it("open window with attendees filled after temp search on create event button click", async () => { const spy = jest .spyOn(servicesModule, "getTempCalendarsListAsync") .mockImplementation((payload) => { return () => Promise.resolve(payload) as any; }); mockedSearchUsers.mockResolvedValue([ { email: "newuser@example.com", displayName: "New User", avatarUrl: "image.png", openpaasId: "1234567890", }, ]); await act(async () => renderWithProviders(, preloadedState) ); const input = screen.getByPlaceholderText( "peopleSearch.availabilityPlaceholder" ); await act(async () => userEvent.type(input, "New")); const option = await screen.findByText("New User"); await act(async () => { fireEvent.click(option); }); expect(spy).toHaveBeenCalled(); await act(async () => { fireEvent.click( screen.getByRole("button", { name: "event.createEvent" }) ); }); await waitFor( () => { expect(screen.getAllByText("event.createEvent")).toHaveLength(2); expect(screen.getAllByText(/New User/i)).toHaveLength(2); }, { timeout: 10000 } ); }, 15000); it("open window with attendees filled after temp search on enter in temp input", async () => { const spy = jest .spyOn(servicesModule, "getTempCalendarsListAsync") .mockImplementation((payload) => { return () => Promise.resolve(payload) as any; }); mockedSearchUsers.mockResolvedValue([ { email: "newuser@example.com", displayName: "New User", avatarUrl: "image.png", openpaasId: "1234567890", }, ]); await act(async () => renderWithProviders(, preloadedState) ); const input = screen.getByPlaceholderText( "peopleSearch.availabilityPlaceholder" ); await act(async () => userEvent.type(input, "New")); const option = await screen.findByText("New User"); await act(async () => { fireEvent.click(option); }); expect(spy).toHaveBeenCalled(); await act(async () => { fireEvent.keyDown(input, { key: "Enter" }); }); await waitFor( () => { expect(screen.getAllByText("event.createEvent")).toHaveLength(2); expect(screen.getAllByText(/New User/i)).toHaveLength(2); }, { timeout: 10000 } ); }, 15000); it("BUGFIX: can untoggle all calendar.personal", async () => { jest .spyOn(calendarDetailThunks, "getCalendarDetailAsync") .mockImplementation( () => ({ type: "getCalendarDetailAsync", unwrap: () => Promise.resolve({}), }) as any ); await act(async () => renderWithProviders(, { user: preloadedState.user, calendars: { list: { "user1/cal1": preloadedState.calendars.list["user1/cal1"] }, }, }) ); const checkbox = screen.getByLabelText("Calendar personal"); expect(checkbox).toBeChecked(); fireEvent.click(checkbox); // toggle off expect(checkbox).not.toBeChecked(); }); it("BUGFIX: monthview doesn't show days numbers in banner", async () => { await act(async () => renderWithProviders(, { user: preloadedState.user, calendars: { list: { "user1/cal1": preloadedState.calendars.list["user1/cal1"] }, pending: false, }, }) ); const calendarRef = window.__calendarRef; await waitFor(() => { expect(calendarRef.current).not.toBeNull(); }); const calendarApi = calendarRef.current; await act(async () => { calendarApi.changeView("dayGridMonth"); }); await waitFor(() => { expect(screen.queryAllByRole("columnheader").length).toBe(14); }); const dayNumbers = screen.getAllByRole("columnheader"); expect(dayNumbers.length).toBeGreaterThan(0); const dayNumbersWithContent = dayNumbers.filter((cell) => { const dayNum = cell.querySelector( ".fc-daygrid-day-number, .fc-daygrid-day-top" ); return dayNum && dayNum.textContent && /\d/.test(dayNum.textContent); }); expect(dayNumbersWithContent.length).toBe(0); }); it("should fetch calendar details for February after navigating to month view and clicking next", async () => { const spy = jest .spyOn(calendarDetailThunks, "getCalendarDetailAsync") .mockImplementation((payload) => { const promise = Promise.resolve(payload); const thunk = () => Object.assign(promise, { unwrap: () => promise }); return thunk as any; }); jest.useFakeTimers().setSystemTime(new Date("2025-01-01")); await act(async () => renderWithProviders(, { ...preloadedState, calendars: { ...preloadedState.calendars, pending: false }, }) ); // Advance past debounce so the initial load fires await act(async () => { jest.advanceTimersByTime(300); }); await waitFor(() => { expect(spy).toHaveBeenCalled(); }); spy.mockClear(); const calendarRef = window.__calendarRef; const calendarApi = calendarRef.current; await act(async () => { calendarApi.changeView("dayGridMonth"); fireEvent.click(screen.getByTestId("ChevronRightIcon")); }); // Advance past debounce so the navigation fetch fires await act(async () => { jest.advanceTimersByTime(300); }); await waitFor(() => { expect(spy).toHaveBeenCalled(); }); // Find the call that covers February 2025 (the navigated-to month) const februaryCall = spy.mock.calls.find((call) => { const start = call[0].match.start as string; // start should be in January or February 2025 (month view includes padding days) return start.startsWith("2025"); }); expect(februaryCall).toBeDefined(); const callArgs = februaryCall![0]; expect(callArgs.calId).toBe("user1/cal1"); const parseDate = (s: string) => new Date( s.replace( /(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/, "$1-$2-$3T$4:$5:$6" ) ); const startDate = parseDate(callArgs.match.start); const endDate = parseDate(callArgs.match.end); // February 2025 month view: range must cover Feb 1 through Mar 1 expect(startDate.getTime()).toBeLessThanOrEqual( new Date("2025-02-01").getTime() ); expect(endDate.getTime()).toBeGreaterThanOrEqual( new Date("2025-03-01").getTime() ); }); describe("Batch loading and prefetching", () => { beforeEach(() => { jest.clearAllMocks(); }); it("loads active calendars in batches of 5", async () => { const manyCalendars = Array.from({ length: 12 }, (_, i) => ({ [`user1/cal${i + 1}`]: { name: `Calendar ${i + 1}`, id: `user1/cal${i + 1}`, color: { light: "#FF0000", dark: "#000" }, events: {}, }, })).reduce((acc, cal) => ({ ...acc, ...cal }), {}); const stateWithManyCalendars = { ...preloadedState, calendars: { ...preloadedState.calendars, list: manyCalendars, pending: false, }, }; const spy = jest .spyOn(calendarDetailThunks, "getCalendarDetailAsync") .mockImplementation( () => ({ type: "getCalendarDetailAsync", unwrap: () => Promise.resolve({}), }) as any ); await act(async () => { renderWithProviders( , stateWithManyCalendars ); }); await waitFor( () => { expect(spy).toHaveBeenCalled(); }, { timeout: 3000 } ); const callCount = spy.mock.calls.length; expect(callCount).toBeGreaterThan(0); expect(callCount).toBeLessThanOrEqual(24); }); it("prefetches hidden calendars only after active load completes", async () => { const calendarsWithSelected = { ...preloadedState, calendars: { ...preloadedState.calendars, list: { "user1/cal1": { name: "Selected Calendar", id: "user1/cal1", color: { light: "#FF0000", dark: "#000" }, events: {}, }, "user1/cal2": { name: "Hidden Calendar 1", id: "user1/cal2", color: { light: "#00FF00", dark: "#000" }, events: {}, }, "user1/cal3": { name: "Hidden Calendar 2", id: "user1/cal3", color: { light: "#0000FF", dark: "#000" }, events: {}, }, }, pending: false, }, }; const spy = jest .spyOn(calendarDetailThunks, "getCalendarDetailAsync") .mockImplementation( () => ({ type: "getCalendarDetailAsync", unwrap: () => Promise.resolve({}), }) as any ); await act(async () => { renderWithProviders( , calendarsWithSelected ); }); await waitFor( () => { expect(spy).toHaveBeenCalled(); }, { timeout: 3000 } ); const selectedCalls = spy.mock.calls.filter( (call) => call[0].calId === "user1/cal1" ); spy.mock.calls.filter( (call) => call[0].calId === "user1/cal2" || call[0].calId === "user1/cal3" ); expect(selectedCalls.length).toBeGreaterThan(0); }); it("does not make duplicate API calls for same calendar and range", async () => { const spy = jest .spyOn(calendarDetailThunks, "getCalendarDetailAsync") .mockImplementation( () => ({ type: "getCalendarDetailAsync", unwrap: () => Promise.resolve({}), }) as any ); await act(async () => { renderWithProviders(, { ...preloadedState, calendars: { ...preloadedState.calendars, pending: false }, }); }); await waitFor( () => { expect(spy).toHaveBeenCalled(); }, { timeout: 3000 } ); const callsForCal1 = spy.mock.calls.filter( (call) => call[0].calId === "user1/cal1" ); const uniqueRanges = new Set( callsForCal1.map( (call) => `${call[0].match.start}_${call[0].match.end}` ) ); expect(uniqueRanges.size).toBeLessThanOrEqual(callsForCal1.length); }); it("handles undefined calendars gracefully", async () => { const stateWithUndefinedCalendars = { ...preloadedState, calendars: { list: undefined, templist: undefined, pending: false, }, }; await act(async () => { renderWithProviders( , stateWithUndefinedCalendars ); }); expect(screen.getByText("calendar.personal")).toBeInTheDocument(); }); it("handles undefined tempcalendars gracefully", async () => { const stateWithUndefinedTemp = { ...preloadedState, calendars: { ...preloadedState.calendars, templist: undefined, }, }; await act(async () => { renderWithProviders( , stateWithUndefinedTemp ); }); expect(screen.getByText("calendar.personal")).toBeInTheDocument(); }); }); });