import * as calendarsApi from '@/features/Calendars/CalendarApi' import * as eventThunks from '@/features/Calendars/services' import EventPopover from '@/features/Events/EventModal' import { api } from '@/utils/apiUtils' import { DateSelectArg } from '@fullcalendar/core' import { act, fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { renderWithProviders } from '../../utils/Renderwithproviders' jest.mock('@/utils/apiUtils') describe('EventPopover', () => { beforeEach(() => { sessionStorage.clear() }) const mockOnClose = jest.fn() const mockSetSelectedRange = jest.fn() const mockCalendarRef = { current: { select: jest.fn() } } as any const preloadedState = { user: { userData: { sub: 'test', email: 'test@test.com', sid: 'aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro', openpaasId: '667037022b752d0026472254' }, organiserData: { cn: 'test', cal_address: 'test@test.com' } }, calendars: { list: { '667037022b752d0026472254/cal1': { id: '667037022b752d0026472254/cal1', name: 'Calendar 1', color: '#FF0000' }, '667037022b752d0026472254/cal2': { id: '667037022b752d0026472254/cal2', name: 'Calendar 2', color: '#00FF00' } }, pending: false } } const mockUsers = [ { id: 'john@example.com', objectType: 'user', emailAddresses: [ { value: 'john@example.com', type: 'default' } ], names: [ { displayName: 'John Doe', type: 'default' } ] }, { id: 'jane@example.com', objectType: 'user', emailAddresses: [ { value: 'jane@example.com', type: 'default' } ], names: [ { displayName: 'Jane Smith', type: 'default' } ] }, { 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) }) const defaultSelectedRange = { startStr: '2025-07-18T09:00', endStr: '2025-07-18T10:00', start: new Date('2025-07-18T09:00'), end: new Date('2025-07-18T10:00'), allDay: false, resource: undefined } as unknown as DateSelectArg const renderPopover = (selectedRange = defaultSelectedRange) => { renderWithProviders( , preloadedState ) } it('renders correctly with inputs and calendar options', () => { renderPopover() // Check dialog title expect(screen.getByText('event.createEvent')).toBeInTheDocument() // Check inputs exist by their roles const titleInput = screen.getByRole('textbox', { name: /title/i }) expect(titleInput).toBeInTheDocument() // Description input is only visible after clicking "Add description" button const addDescriptionButton = screen.getByRole('button', { name: 'event.form.addDescription' }) expect(addDescriptionButton).toBeInTheDocument() // In normal mode calendar is a SectionPreviewRow (button), not combobox expect( screen.getByRole('button', { name: 'Calendar 1' }) ).toBeInTheDocument() // Check button expect( screen.getByRole('button', { name: 'common.moreOptions' }) ).toBeInTheDocument() // Extended mode fireEvent.click(screen.getByRole('button', { name: 'common.moreOptions' })) // Back button appears expect(screen.getByLabelText('show less')).toBeInTheDocument() // Extended labels appear expect(screen.getAllByText('event.form.repeat')).toHaveLength(1) expect(screen.getAllByText('event.form.notification')).toHaveLength(1) expect(screen.getAllByText('event.form.visibleTo')).toHaveLength(1) expect(screen.getAllByText('event.form.showMeAs')).toHaveLength(1) }) it('fills start from selectedRange', () => { renderPopover({ startStr: '2026-07-20T10:00', endStr: '2026-07-20T12:00', start: new Date('2026-07-20T10:00'), end: new Date('2026-07-20T12:00'), allDay: false } as unknown as DateSelectArg) // Expand to show date/time inputs (normal mode shows DateTimeSummary) fireEvent.click(screen.getByRole('button', { name: 'common.moreOptions' })) expect(screen.getByTestId('start-date-input')).toBeInTheDocument() expect(screen.getByTestId('start-time-input')).toBeInTheDocument() }) it('updates inputs on change', () => { renderPopover() fireEvent.change(screen.getByLabelText('event.form.title'), { target: { value: 'My Event' } }) expect(screen.getByLabelText('event.form.title')).toHaveValue('My Event') // Click "Add description" button first fireEvent.click( screen.getByRole('button', { name: 'event.form.addDescription' }) ) fireEvent.change(screen.getByLabelText('event.form.description'), { target: { value: 'Event Description' } }) expect(screen.getByLabelText('event.form.description')).toHaveValue( 'Event Description' ) // Expand location section (normal mode shows SectionPreviewRow) fireEvent.click( screen.getByRole('button', { name: 'event.form.locationPlaceholder' }) ) fireEvent.change(screen.getByLabelText('event.form.location'), { target: { value: 'Conference Room' } }) expect(screen.getByLabelText('event.form.location')).toHaveValue( 'Conference Room' ) }) it('changes selected calendar', async () => { renderPopover() // Expand to show calendar combobox (normal mode shows SectionPreviewRow) fireEvent.click(screen.getByRole('button', { name: 'common.moreOptions' })) const select = screen.getByLabelText('event.form.calendar') fireEvent.mouseDown(select) // Open menu const option = await screen.findByText('Calendar 2') fireEvent.click(option) const calendarSelect = screen.getByRole('combobox', { name: /Calendar/i }) expect(calendarSelect).toHaveTextContent('Calendar 2') }) it('adds a attendee', async () => { jest.useFakeTimers() 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)) 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 }) fireEvent.click(screen.getByRole('button', { name: 'actions.save' })) await waitFor(() => { expect(spy).toHaveBeenCalled() }) const receivedPayload = spy.mock.calls[0][0] 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: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'RESOURCE' }) } finally { jest.useRealTimers() } }) it('dispatches putEventAsync and calls onClose when Save is clicked', async () => { renderPopover() const newEvent = { title: 'Meeting', allday: false, uid: '6045c603-11ab-43c5-bc30-0641420bb3a8', description: 'Discuss project', location: 'Zoom', organizer: { cn: 'test', cal_address: 'test@test.com' }, timezone: 'Europe/Paris', transp: 'TRANSPARENT' } // Fill inputs fireEvent.change(screen.getByLabelText('event.form.title'), { target: { value: newEvent.title } }) // Click "Add description" button first fireEvent.click( screen.getByRole('button', { name: 'event.form.addDescription' }) ) fireEvent.change(screen.getByLabelText('event.form.description'), { target: { value: newEvent.description } }) fireEvent.click( screen.getByRole('button', { name: 'event.form.locationPlaceholder' }) ) fireEvent.change(screen.getByLabelText('event.form.location'), { target: { value: newEvent.location } }) 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.cal).toEqual( preloadedState.calendars.list['667037022b752d0026472254/cal1'] ) expect(receivedPayload.newEvent.title).toBe(newEvent.title) expect(receivedPayload.newEvent.description).toBe(newEvent.description) expect(receivedPayload.newEvent.location).toBe(newEvent.location) expect(receivedPayload.newEvent.organizer).toEqual(newEvent.organizer) expect(receivedPayload.newEvent.color).toEqual( preloadedState.calendars.list['667037022b752d0026472254/cal1'].color ) // onClose should be called expect(mockOnClose).toHaveBeenCalledWith(true) }) it('calls onClose with refresh = false when Cancel clicked', () => { renderPopover() // Cancel button only appears in expanded mode fireEvent.click(screen.getByRole('button', { name: 'common.moreOptions' })) fireEvent.click(screen.getByRole('button', { name: /Cancel/i })) expect(mockOnClose).toHaveBeenCalledWith(false) }) it('BUGFIX: Prefill Calendar field', async () => { renderPopover() // In normal mode calendar is a SectionPreviewRow (button) with calendar name await waitFor(() => expect( screen.getByRole('button', { name: 'Calendar 1' }) ).toHaveTextContent('Calendar 1') ) }) describe('EventModal - Drag and Drop Scenarios', () => { // Test 2.1: Drag from allday slot (single day) it('sets allday=true when drag from allday slot (single day)', async () => { const selectedRange = { startStr: '2025-07-18', endStr: '2025-07-19', start: new Date('2025-07-18'), end: new Date('2025-07-19'), allDay: true } as unknown as DateSelectArg renderPopover(selectedRange) fireEvent.click( screen.getByRole('button', { name: 'common.moreOptions' }) ) const allDayCheckbox = screen.getByLabelText('event.form.allDay') await waitFor(() => { expect(allDayCheckbox).toBeChecked() }) }) // Test 2.2: Drag from allday slot (multiple days) it('sets allday=true and shows 2 date fields when drag from allday slot (multiple days)', async () => { const selectedRange = { startStr: '2025-07-18', endStr: '2025-07-21', start: new Date('2025-07-18'), end: new Date('2025-07-21'), allDay: true } as unknown as DateSelectArg renderPopover(selectedRange) fireEvent.click( screen.getByRole('button', { name: 'common.moreOptions' }) ) const allDayCheckbox = screen.getByLabelText('event.form.allDay') await waitFor(() => { expect(allDayCheckbox).toBeChecked() }) // Verify only date fields are shown await waitFor(() => { expect(screen.getByTestId('start-date-input')).toBeInTheDocument() expect(screen.getByTestId('end-date-input')).toBeInTheDocument() expect(screen.queryByTestId('start-time-input')).not.toBeInTheDocument() expect(screen.queryByTestId('end-time-input')).not.toBeInTheDocument() }) }) // Test 2.3: Drag from week view (single day) it('keeps allday=false when drag from week view (single day)', async () => { const selectedRange = { startStr: '2025-07-18T09:00', endStr: '2025-07-18T10:00', start: new Date('2025-07-18T09:00'), end: new Date('2025-07-18T10:00'), allDay: false } as unknown as DateSelectArg renderPopover(selectedRange) fireEvent.click( screen.getByRole('button', { name: 'common.moreOptions' }) ) const allDayCheckbox = screen.getByLabelText('event.form.allDay') await waitFor(() => { expect(allDayCheckbox).not.toBeChecked() }) // Verify time fields are shown await waitFor(() => { expect(screen.getByTestId('start-time-input')).toBeInTheDocument() expect(screen.getByTestId('end-time-input')).toBeInTheDocument() }) }) // Test 2.4: Drag from week view (multiple days) - CRITICAL TEST it('keeps allday=false and shows 4 fields when drag from week view (multiple days)', async () => { const selectedRange = { startStr: '2025-07-18T09:00', endStr: '2025-07-20T10:00', start: new Date('2025-07-18T09:00'), end: new Date('2025-07-20T10:00'), allDay: false } as unknown as DateSelectArg renderPopover(selectedRange) fireEvent.click( screen.getByRole('button', { name: 'common.moreOptions' }) ) const allDayCheckbox = screen.getByLabelText('event.form.allDay') await waitFor(() => { expect(allDayCheckbox).not.toBeChecked() // CRITICAL: should NOT be checked }) // Verify all 4 fields are shown await waitFor(() => { expect(screen.getByTestId('start-date-input')).toBeInTheDocument() expect(screen.getByTestId('start-time-input')).toBeInTheDocument() expect(screen.getByTestId('end-date-input')).toBeInTheDocument() expect(screen.getByTestId('end-time-input')).toBeInTheDocument() }) }) // Test 2.5: Drag from week view (multiple days) - fallback path it('handles drag from week view (multiple days) with Date objects only', async () => { const selectedRange = { start: new Date('2025-07-18T09:00'), end: new Date('2025-07-20T10:00'), allDay: false } as unknown as DateSelectArg renderPopover(selectedRange) fireEvent.click( screen.getByRole('button', { name: 'common.moreOptions' }) ) const allDayCheckbox = screen.getByLabelText('event.form.allDay') await waitFor(() => { expect(allDayCheckbox).not.toBeChecked() }) // Verify all 4 fields are shown await waitFor(() => { expect(screen.getByTestId('start-date-input')).toBeInTheDocument() expect(screen.getByTestId('start-time-input')).toBeInTheDocument() expect(screen.getByTestId('end-date-input')).toBeInTheDocument() expect(screen.getByTestId('end-time-input')).toBeInTheDocument() }) }) }) })