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 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' }, owner: { emails: ['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 = jest.fn().mockReturnValue( Object.assign(Promise.resolve({}), { unwrap: () => Promise.resolve({}) }) ) as unknown as AppDispatch jest.spyOn(appHooks, 'useAppDispatch').mockReturnValue(dispatch) 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' }, owner: { emails: ['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' }, owner: { emails: ['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' }, owner: { emails: ['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') .mockImplementation(payload => { const promise = Promise.resolve(payload) ;(promise as any).unwrap = () => promise return () => promise as any }) 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') .mockImplementation(payload => { const promise = Promise.resolve(payload) ;(promise as any).unwrap = () => promise return () => promise as any }) const onClose = jest.fn() renderWithProviders( , preloadedState ) // Expand to show date/time inputs (normal mode shows DateTimeSummary) fireEvent.click( screen.getByRole('button', { name: 'common.moreOptions' }) ) 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 () => { // Mock dispatch locally — this test calls createEventHandlers directly // and does not go through the Redux store or useAppDispatch. const mockDispatch = jest.fn().mockReturnValue( Object.assign(Promise.resolve({}), { unwrap: () => Promise.resolve({}) }) ) as unknown as AppDispatch 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({ 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 () => { // Mock dispatch locally — this test calls createEventHandlers directly // and does not go through the Redux store or useAppDispatch. const mockDispatch = jest.fn().mockReturnValue( Object.assign(Promise.resolve({}), { unwrap: () => Promise.resolve({}) }) ) as unknown as AppDispatch 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({ 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 }) }) })