import * as CalendarApi from '@/features/Calendars/CalendarApi' import * as eventThunks from '@/features/Calendars/services' import * as EventApi from '@/features/Events/EventApi' import { CalendarEvent } from '@/features/Events/EventsTypes' import EventUpdateModal from '@/features/Events/EventUpdateModal' import { act, fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { renderWithProviders } from '../../utils/Renderwithproviders' jest.mock('@/features/Events/EventApi') jest.mock('@/features/Calendars/CalendarApi') jest.mock('@/features/Events/api/updateSeries') describe("EventUpdateModal - Recurring Event 'Edit All' Handling", () => { const mockOnClose = jest.fn() beforeEach(() => { jest.clearAllMocks() jest.restoreAllMocks() sessionStorage.clear() }) const baseUID = 'recurring-event-base' const calId = '667037022b752d0026472254/cal1' const preloadedState = { user: { userData: { sub: 'test', email: 'test@test.com', sid: 'test-sid', openpaasId: '667037022b752d0026472254' }, organiserData: { cn: 'test', cal_address: 'test@test.com' } }, calendars: { list: { [calId]: { id: calId, name: 'Test Calendar', color: '#FF0000', events: {}, owner: { emails: ['test@test.com'] } } }, pending: false } } describe('Master Event Display', () => { it("should fetch and display master event when editing 'all events' of a recurring series", async () => { // Given a recurring series with modified instances const masterEvent = { uid: baseUID, title: 'Master Event Title', description: 'Master Description', calId, start: '2025-01-15T10:00:00.000Z', end: '2025-01-15T11:00:00.000Z', repetition: { freq: 'daily', interval: 1 }, allday: false, timezone: 'America/New_York', organizer: { cn: 'test', cal_address: 'test@test.com' }, attendee: [{ cn: 'test', cal_address: 'test@test.com' }], URL: `/calendars/${calId}/${baseUID}.ics` } as CalendarEvent // Instance that was moved to different time const modifiedInstance = { ...masterEvent, uid: `${baseUID}/20250116`, title: 'Modified Instance Title', // Different title start: '2025-01-16T14:00:00.000Z', // Different time (2PM instead of 10AM) end: '2025-01-16T15:00:00.000Z' } const stateWithRecurringEvent = { ...preloadedState, calendars: { ...preloadedState.calendars, list: { [calId]: { ...preloadedState.calendars.list[calId], events: { [baseUID]: masterEvent, [`${baseUID}/20250116`]: modifiedInstance } } } } } // Mock getEvent to return master event const mockGetEvent = jest .spyOn(EventApi, 'getEvent') .mockResolvedValue(masterEvent) renderWithProviders( , stateWithRecurringEvent ) // Wait for master event to be fetched await waitFor(() => { expect(mockGetEvent).toHaveBeenCalledWith( expect.objectContaining({ uid: baseUID // Should fetch base UID, not instance }), true ) }) // Should display master event data, NOT modified instance data await waitFor(() => { const titleInput = screen.getByDisplayValue('Master Event Title') expect(titleInput).toBeInTheDocument() }) // Should NOT show modified instance title expect( screen.queryByDisplayValue('Modified Instance Title') ).not.toBeInTheDocument() // Expand more options to show timezone (normal mode shows summary first) fireEvent.click( screen.getByRole('button', { name: 'common.moreOptions' }) ) // Verify timezone dropdown shows master timezone await waitFor(() => { expect(screen.getByDisplayValue(/New York/i)).toBeDefined() }) }) it('should use master event directly if clicked event is already the master', async () => { const masterEvent = { uid: baseUID, // No recurrence-id title: 'Master Event', calId, start: '2025-01-15T10:00:00.000Z', end: '2025-01-15T11:00:00.000Z', repetition: { freq: 'weekly', interval: 1 }, allday: false, organizer: { cn: 'test', cal_address: 'test@test.com' }, URL: `/calendars/${calId}/${baseUID}.ics` } const stateWithMaster = { ...preloadedState, calendars: { ...preloadedState.calendars, list: { [calId]: { ...preloadedState.calendars.list[calId], events: { [baseUID]: masterEvent } } } } } const mockGetEvent = jest.spyOn(EventApi, 'getEvent') renderWithProviders( , stateWithMaster ) await waitFor(() => { expect(screen.getByDisplayValue('Master Event')).toBeInTheDocument() }) // Should NOT fetch from API since we already have master expect(mockGetEvent).not.toHaveBeenCalled() }) it('should fallback to instance data if master event fetch fails', async () => { const instance = { uid: `${baseUID}/20250115`, title: 'Instance Event', calId, start: '2025-01-15T10:00:00.000Z', end: '2025-01-15T11:00:00.000Z', repetition: { freq: 'daily', interval: 1 }, allday: false, organizer: { cn: 'test', cal_address: 'test@test.com' }, URL: `/calendars/${calId}/${baseUID}.ics` } as CalendarEvent const stateWithInstance = { ...preloadedState, calendars: { ...preloadedState.calendars, list: { [calId]: { ...preloadedState.calendars.list[calId], events: { [`${baseUID}/20250115`]: instance } } } } } // Mock getEvent to fail jest .spyOn(EventApi, 'getEvent') .mockRejectedValue(new Error('Network error')) const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation() renderWithProviders( , stateWithInstance ) // Should still display the instance data as fallback await waitFor(() => { expect(screen.getByDisplayValue('Instance Event')).toBeInTheDocument() }) expect(consoleErrorSpy).toHaveBeenCalledWith( 'Failed to fetch master event:', expect.any(Error) ) consoleErrorSpy.mockRestore() }) }) describe('Update All Events - Using Base UID', () => { it('should use base UID (not instance UID) when updating series with property changes', async () => { const masterEvent = { uid: baseUID, title: 'Weekly Meeting', description: 'Original description', calId, start: '2025-01-15T10:00:00.000Z', end: '2025-01-15T11:00:00.000Z', repetition: { freq: 'weekly', interval: 1 }, allday: false, organizer: { cn: 'test', cal_address: 'test@test.com' }, URL: `/calendars/${calId}/${baseUID}.ics` } as CalendarEvent const instance1 = { ...masterEvent, uid: `${baseUID}/20250115` } const instance2 = { ...masterEvent, uid: `${baseUID}/20250122`, start: '2025-01-22T10:00:00.000Z', end: '2025-01-22T11:00:00.000Z' } const stateWithSeries = { ...preloadedState, calendars: { ...preloadedState.calendars, list: { [calId]: { ...preloadedState.calendars.list[calId], events: { [baseUID]: masterEvent, [`${baseUID}/20250115`]: instance1, [`${baseUID}/20250122`]: instance2 } } } } } // Mock getEvent to return master jest.spyOn(EventApi, 'getEvent').mockResolvedValue(masterEvent) const updateSeriesAsyncSpy = jest.spyOn(eventThunks, 'updateSeriesAsync') renderWithProviders( , stateWithSeries ) await waitFor(() => { expect(screen.getByDisplayValue('Weekly Meeting')).toBeInTheDocument() }) // Change only the title (property change, not recurrence rules) const titleInput = screen.getByDisplayValue('Weekly Meeting') fireEvent.change(titleInput, { target: { value: 'Updated Meeting' } }) const saveButton = screen.getByRole('button', { name: 'actions.save' }) await act(async () => { fireEvent.click(saveButton) }) // Verify updateSeriesAsync was dispatched with base UID await waitFor(() => { expect(updateSeriesAsyncSpy).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ uid: 'recurring-event-base', recurrenceId: undefined }), removeOverrides: false }) ) }) }) it('should use base UID when updating series with recurrence rule changes', async () => { const masterEvent = { uid: baseUID, title: 'Daily Standup', calId, start: '2025-01-15T09:00:00.000Z', end: '2025-01-15T09:30:00.000Z', repetition: { freq: 'daily', interval: 1 }, // Daily allday: false, timezone: 'America/New_York', organizer: { cn: 'test', cal_address: 'test@test.com' }, URL: `/calendars/${calId}/${baseUID}.ics` } as CalendarEvent const instance1 = { ...masterEvent, uid: `${baseUID}/20250115` } const instance2 = { ...masterEvent, uid: `${baseUID}/20250116`, start: '2025-01-16T09:00:00.000Z', end: '2025-01-16T09:30:00.000Z' } const stateWithSeries = { ...preloadedState, calendars: { ...preloadedState.calendars, list: { [calId]: { ...preloadedState.calendars.list[calId], events: { [baseUID]: masterEvent, [`${baseUID}/20250115`]: instance1, [`${baseUID}/20250116`]: instance2 } } } } } jest.spyOn(EventApi, 'getEvent').mockResolvedValue(masterEvent) jest.spyOn(CalendarApi, 'getCalendar').mockResolvedValue({ _embedded: { 'dav:item': [] } } as any) const { store } = renderWithProviders( , stateWithSeries ) await waitFor(() => { expect(screen.getByDisplayValue('Daily Standup')).toBeInTheDocument() }) // Click More Options to access repeat settings const moreOptionsButton = screen.getByText('common.moreOptions') fireEvent.click(moreOptionsButton) await waitFor(() => { const repeatCheckbox = screen.getByLabelText('event.form.repeat') expect(repeatCheckbox).toBeInTheDocument() expect(repeatCheckbox).toBeChecked() }) // Change from daily to weekly const frequencySelect = screen.getByText('event.repeat.frequency.days') fireEvent.mouseDown(frequencySelect) const weeklyOption = screen.getByRole('option', { name: 'event.repeat.frequency.weeks' }) fireEvent.click(weeklyOption) const saveButton = screen.getByRole('button', { name: 'actions.save' }) await act(async () => { fireEvent.click(saveButton) await new Promise(resolve => setTimeout(resolve, 100)) }) // Verify all old instances were removed from store await waitFor(() => { const state = store.getState() const calendar = state.calendars.list[calId] expect(calendar.events[baseUID]).toBeUndefined() expect(calendar.events[`${baseUID}/20250115`]).toBeUndefined() expect(calendar.events[`${baseUID}/20250116`]).toBeUndefined() }) }) }) describe('Master Event Date Preservation', () => { it("should preserve master event date when updating time in 'all events' mode", async () => { // Master event starts on Jan 15 at 10:00 AM const masterEvent = { uid: baseUID, title: 'Daily Standup', calId, start: '2025-01-15T10:00:00.000Z', end: '2025-01-15T10:30:00.000Z', repetition: { freq: 'daily', interval: 1 }, allday: false, timezone: 'UTC', organizer: { cn: 'test', cal_address: 'test@test.com' }, URL: `/calendars/${calId}/${baseUID}.ics` } as CalendarEvent // Instance on Jan 17 (different date) const instance = { ...masterEvent, uid: `${baseUID}/20250117`, start: '2025-01-17T10:00:00.000Z', end: '2025-01-17T10:30:00.000Z' } const stateWithSeries = { ...preloadedState, calendars: { ...preloadedState.calendars, list: { [calId]: { ...preloadedState.calendars.list[calId], events: { [baseUID]: masterEvent, [`${baseUID}/20250117`]: instance } } } } } jest.spyOn(EventApi, 'getEvent').mockResolvedValue(masterEvent) jest.spyOn(EventApi, 'putEvent').mockResolvedValue({ status: 201 } as any) const updateSeriesAsyncSpy = jest.spyOn(eventThunks, 'updateSeriesAsync') renderWithProviders( , stateWithSeries ) await waitFor(() => { expect(screen.getByDisplayValue('Daily Standup')).toBeInTheDocument() }) // Expand more options to show date/time inputs (normal mode shows DateTimeSummary first) fireEvent.click( screen.getByRole('button', { name: 'common.moreOptions' }) ) const input = await waitFor(() => screen.getByTestId('start-time-input')) // Change time to 2:00 PM (14:00) await userEvent.click(input) await userEvent.clear(input) await userEvent.type(input, '14:00{enter}') const saveButton = screen.getByRole('button', { name: 'actions.save' }) await act(async () => { fireEvent.click(saveButton) await new Promise(resolve => setTimeout(resolve, 100)) }) // Verify the API was called with master date (Jan 15) + new time (14:00) await waitFor(() => { expect(updateSeriesAsyncSpy).toHaveBeenCalled() const callArgs = updateSeriesAsyncSpy.mock.calls[0][0] const updatedEvent = callArgs.event // Should preserve Jan 15 date (master), not Jan 17 (clicked instance) expect(updatedEvent.start).toContain('2025-01-15') // But with new time 14:00 (in UTC, this is 14:00) expect(updatedEvent.start).toContain('14:00') }) }) }) })