cadfa70e60
* #708 apply strictier linting rules and fix simple eslint bugs * #708 fix eslint errors relate to promise * #708 fix eslint import/no-extraneous-dependencies * #708 fix eslint errors of react-hook * #708 enable eslint check for typescript --------- Co-authored-by: lethemanh <lethemanh@lethemanhs-MacBook-Pro.local>
1198 lines
36 KiB
TypeScript
1198 lines
36 KiB
TypeScript
import { RootState } from '@/app/store'
|
|
import {
|
|
createEventHandlers,
|
|
EventHandlersProps
|
|
} from '@/components/Calendar/handlers/eventHandlers'
|
|
import { EditModeDialog } from '@/components/Event/EditModeDialog'
|
|
import * as eventThunks from '@/features/Calendars/services'
|
|
import * as EventApi from '@/features/Events/EventApi'
|
|
import EventPreviewModal from '@/features/Events/EventPreview'
|
|
import EventUpdateModal from '@/features/Events/EventUpdateModal'
|
|
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
|
import preview from 'jest-preview'
|
|
import { renderWithProviders } from '../../utils/Renderwithproviders'
|
|
|
|
jest.mock('@/components/Event/utils/eventUtils', () => {
|
|
const actual = jest.requireActual('@/components/Event/utils/eventUtils')
|
|
return {
|
|
...actual,
|
|
refreshCalendars: jest.fn(() => Promise.resolve()),
|
|
refreshSingularCalendar: jest.fn(() => Promise.resolve())
|
|
}
|
|
})
|
|
const mockOnClose = jest.fn()
|
|
const day = new Date('2025-03-15T10:00:00Z')
|
|
|
|
const basePreloadedState = {
|
|
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',
|
|
color: '#FF0000',
|
|
events: {
|
|
'recurring-base/20250315T100000': {
|
|
uid: 'recurring-base/20250315T100000',
|
|
title: 'Recurring Event Instance',
|
|
calId: '667037022b752d0026472254/cal1',
|
|
start: day.toISOString(),
|
|
end: new Date('2025-03-15T11:00:00Z').toISOString(),
|
|
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
|
recurrenceId: '20250315T100000',
|
|
repetition: { freq: 'weekly', occurrences: 4, interval: 1 },
|
|
timezone: 'UTC',
|
|
URL: '/calendars/667037022b752d0026472254/cal1/recurring-base.ics',
|
|
attendee: [
|
|
{
|
|
cn: 'test',
|
|
cal_address: 'test@test.com',
|
|
partstat: 'NEEDS-ACTION',
|
|
rsvp: 'TRUE',
|
|
role: 'REQ-PARTICIPANT',
|
|
cutype: 'INDIVIDUAL'
|
|
},
|
|
{
|
|
cn: 'John',
|
|
cal_address: 'john@test.com',
|
|
partstat: 'NEEDS-ACTION',
|
|
rsvp: 'TRUE',
|
|
role: 'REQ-PARTICIPANT',
|
|
cutype: 'INDIVIDUAL'
|
|
}
|
|
]
|
|
},
|
|
'recurring-base/20250322T100000': {
|
|
uid: 'recurring-base/20250322T100000',
|
|
title: 'Recurring Event Instance',
|
|
calId: '667037022b752d0026472254/cal1',
|
|
start: new Date('2025-03-22T10:00:00Z').toISOString(),
|
|
end: new Date('2025-03-22T11:00:00Z').toISOString(),
|
|
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
|
recurrenceId: '20250322T100000',
|
|
timezone: 'UTC',
|
|
URL: '/calendars/667037022b752d0026472254/cal1/recurring-base.ics',
|
|
attendee: [
|
|
{
|
|
cn: 'test',
|
|
cal_address: 'test@test.com',
|
|
partstat: 'NEEDS-ACTION',
|
|
rsvp: 'TRUE',
|
|
role: 'REQ-PARTICIPANT',
|
|
cutype: 'INDIVIDUAL'
|
|
}
|
|
]
|
|
}
|
|
},
|
|
owner: { emails: ['test@test.com'] }
|
|
}
|
|
},
|
|
pending: false,
|
|
templist: {}
|
|
}
|
|
} as unknown as RootState
|
|
|
|
describe('EditModeDialog Component', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
it("renders dialog when type is 'edit'", async () => {
|
|
const mockSetOpen = jest.fn()
|
|
const mockEventAction = jest.fn()
|
|
await act(async () =>
|
|
renderWithProviders(
|
|
<EditModeDialog
|
|
type="edit"
|
|
setOpen={mockSetOpen}
|
|
event={
|
|
basePreloadedState.calendars.list['667037022b752d0026472254/cal1']
|
|
.events['recurring-base/20250315T100000']
|
|
}
|
|
eventAction={mockEventAction}
|
|
/>,
|
|
basePreloadedState
|
|
)
|
|
)
|
|
|
|
expect(
|
|
screen.getByText('editModeDialog.updateRecurrentEvent')
|
|
).toBeInTheDocument()
|
|
expect(screen.getByText('editModeDialog.thisEvent')).toBeInTheDocument()
|
|
expect(screen.getByText('editModeDialog.allEvents')).toBeInTheDocument()
|
|
})
|
|
|
|
it("renders dialog when type is 'attendance'", async () => {
|
|
const mockSetOpen = jest.fn()
|
|
const mockEventAction = jest.fn()
|
|
await act(async () =>
|
|
renderWithProviders(
|
|
<EditModeDialog
|
|
type="attendance"
|
|
setOpen={mockSetOpen}
|
|
event={
|
|
basePreloadedState.calendars.list['667037022b752d0026472254/cal1']
|
|
.events['recurring-base/20250315T100000']
|
|
}
|
|
eventAction={mockEventAction}
|
|
/>,
|
|
basePreloadedState
|
|
)
|
|
)
|
|
|
|
expect(
|
|
screen.getByText('editModeDialog.updateParticipationStatus')
|
|
).toBeInTheDocument()
|
|
})
|
|
|
|
it("calls eventAction with 'solo' when solo option is selected and Ok clicked", async () => {
|
|
const mockSetOpen = jest.fn()
|
|
const mockEventAction = jest.fn()
|
|
|
|
await act(async () =>
|
|
renderWithProviders(
|
|
<EditModeDialog
|
|
type="edit"
|
|
setOpen={mockSetOpen}
|
|
event={
|
|
basePreloadedState.calendars.list['667037022b752d0026472254/cal1']
|
|
.events['recurring-base/20250315T100000']
|
|
}
|
|
eventAction={mockEventAction}
|
|
/>,
|
|
basePreloadedState
|
|
)
|
|
)
|
|
await act(async () => {
|
|
fireEvent.click(screen.getByLabelText('editModeDialog.thisEvent'))
|
|
fireEvent.click(screen.getByRole('button', { name: 'common.ok' }))
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(mockEventAction).toHaveBeenCalledWith('solo')
|
|
expect(mockSetOpen).toHaveBeenCalledWith(null)
|
|
})
|
|
})
|
|
|
|
it("calls eventAction with 'all' when all option is selected and Ok clicked", async () => {
|
|
const mockSetOpen = jest.fn()
|
|
const mockEventAction = jest.fn()
|
|
|
|
await act(async () =>
|
|
renderWithProviders(
|
|
<EditModeDialog
|
|
type="edit"
|
|
setOpen={mockSetOpen}
|
|
event={
|
|
basePreloadedState.calendars.list['667037022b752d0026472254/cal1']
|
|
.events['recurring-base/20250315T100000']
|
|
}
|
|
eventAction={mockEventAction}
|
|
/>,
|
|
basePreloadedState
|
|
)
|
|
)
|
|
|
|
fireEvent.click(screen.getByLabelText('editModeDialog.allEvents'))
|
|
fireEvent.click(screen.getByRole('button', { name: /Ok/i }))
|
|
|
|
await waitFor(() => {
|
|
expect(mockEventAction).toHaveBeenCalledWith('all')
|
|
expect(mockSetOpen).toHaveBeenCalledWith(null)
|
|
})
|
|
})
|
|
|
|
it('calls setOpen with null when Cancel is clicked', async () => {
|
|
const mockSetOpen = jest.fn()
|
|
const mockEventAction = jest.fn()
|
|
|
|
await act(async () =>
|
|
renderWithProviders(
|
|
<EditModeDialog
|
|
type="edit"
|
|
setOpen={mockSetOpen}
|
|
event={
|
|
basePreloadedState.calendars.list['667037022b752d0026472254/cal1']
|
|
.events['recurring-base/20250315T100000']
|
|
}
|
|
eventAction={mockEventAction}
|
|
/>,
|
|
basePreloadedState
|
|
)
|
|
)
|
|
|
|
fireEvent.click(screen.getByText('common.cancel'))
|
|
|
|
await waitFor(() => {
|
|
expect(mockSetOpen).toHaveBeenCalledWith(null)
|
|
expect(mockEventAction).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('EventPreviewModal - Recurring Event Interactions', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
it('shows EditModeDialog when editing a recurring event', async () => {
|
|
jest.spyOn(eventThunks, 'getEventAsync').mockImplementation(payload => {
|
|
return () =>
|
|
Promise.resolve({
|
|
calId: payload.calId,
|
|
event:
|
|
basePreloadedState.calendars.list['667037022b752d0026472254/cal1']
|
|
.events['recurring-base/20250315T100000']
|
|
}) as any
|
|
})
|
|
await act(async () =>
|
|
renderWithProviders(
|
|
<EventPreviewModal
|
|
open={true}
|
|
onClose={mockOnClose}
|
|
calId={'667037022b752d0026472254/cal1'}
|
|
eventId={'recurring-base/20250315T100000'}
|
|
/>,
|
|
basePreloadedState
|
|
)
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('EditIcon'))
|
|
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByText('editModeDialog.updateRecurrentEvent')
|
|
).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('shows EditModeDialog when deleting a recurring event', async () => {
|
|
await act(async () =>
|
|
renderWithProviders(
|
|
<EventPreviewModal
|
|
open={true}
|
|
onClose={mockOnClose}
|
|
calId={'667037022b752d0026472254/cal1'}
|
|
eventId={'recurring-base/20250315T100000'}
|
|
/>,
|
|
basePreloadedState
|
|
)
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('MoreVertIcon'))
|
|
fireEvent.click(
|
|
screen.getByRole('menuitem', { name: 'eventPreview.deleteEvent' })
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByText('editModeDialog.deleteRecurrentEvent')
|
|
).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('shows EditModeDialog when RSVP to a recurring event', async () => {
|
|
await act(async () =>
|
|
renderWithProviders(
|
|
<EventPreviewModal
|
|
open={true}
|
|
onClose={mockOnClose}
|
|
calId={'667037022b752d0026472254/cal1'}
|
|
eventId={'recurring-base/20250315T100000'}
|
|
/>,
|
|
basePreloadedState
|
|
)
|
|
)
|
|
|
|
fireEvent.click(
|
|
screen.getByRole('button', { name: 'eventPreview.ACCEPTED' })
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByText('editModeDialog.updateParticipationStatus')
|
|
).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('does not show EditModeDialog for non-recurring events', async () => {
|
|
const nonRecurringState = {
|
|
...basePreloadedState,
|
|
calendars: {
|
|
...basePreloadedState.calendars,
|
|
list: {
|
|
'667037022b752d0026472254/cal1': {
|
|
...basePreloadedState.calendars.list[
|
|
'667037022b752d0026472254/cal1'
|
|
],
|
|
events: {
|
|
'single-event': {
|
|
uid: 'single-event',
|
|
title: 'Single Event',
|
|
calId: '667037022b752d0026472254/cal1',
|
|
start: day.toISOString(),
|
|
end: new Date('2025-03-15T11:00:00Z').toISOString(),
|
|
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
|
timezone: 'UTC',
|
|
URL: '/calendars/667037022b752d0026472254/cal1/single-event.ics',
|
|
attendee: []
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const spy = jest
|
|
.spyOn(eventThunks, 'deleteEventAsync')
|
|
.mockImplementation(payload => {
|
|
return () => Promise.resolve(payload) as any
|
|
})
|
|
await act(async () =>
|
|
renderWithProviders(
|
|
<EventPreviewModal
|
|
open={true}
|
|
onClose={mockOnClose}
|
|
calId={'667037022b752d0026472254/cal1'}
|
|
eventId={'single-event'}
|
|
/>,
|
|
nonRecurringState
|
|
)
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('MoreVertIcon'))
|
|
fireEvent.click(
|
|
screen.getByRole('menuitem', { name: 'eventPreview.deleteEvent' })
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(spy).toHaveBeenCalled()
|
|
expect(mockOnClose).toHaveBeenCalled()
|
|
})
|
|
|
|
expect(
|
|
screen.queryByText('editModeDialog.deleteRecurrentEvent')
|
|
).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Delete Recurring Event Instance', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
it('calls deleteEventInstanceAsync when deleting single instance', async () => {
|
|
const spy = jest
|
|
.spyOn(eventThunks, 'deleteEventInstanceAsync')
|
|
.mockImplementation(payload => {
|
|
return () => Promise.resolve(payload) as any
|
|
})
|
|
|
|
await act(async () =>
|
|
renderWithProviders(
|
|
<EventPreviewModal
|
|
open={true}
|
|
onClose={mockOnClose}
|
|
calId={'667037022b752d0026472254/cal1'}
|
|
eventId={'recurring-base/20250315T100000'}
|
|
/>,
|
|
basePreloadedState
|
|
)
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('MoreVertIcon'))
|
|
fireEvent.click(
|
|
screen.getByRole('menuitem', { name: 'eventPreview.deleteEvent' })
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByText('editModeDialog.deleteRecurrentEvent')
|
|
).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByLabelText('editModeDialog.thisEvent'))
|
|
fireEvent.click(screen.getByRole('button', { name: /Ok/i }))
|
|
|
|
await waitFor(() => {
|
|
expect(spy).toHaveBeenCalled()
|
|
})
|
|
|
|
const receivedPayload = spy.mock.calls[0][0]
|
|
expect(receivedPayload.event.uid).toBe('recurring-base/20250315T100000')
|
|
})
|
|
|
|
it('calls deleteEventAsync when deleting all instances', async () => {
|
|
jest.spyOn(EventApi, 'getEvent').mockResolvedValue({
|
|
...basePreloadedState.calendars.list['667037022b752d0026472254/cal1']
|
|
.events['recurring-base/20250315T100000'],
|
|
uid: 'recurring-base'
|
|
} as any)
|
|
|
|
const spy = jest
|
|
.spyOn(eventThunks, 'deleteEventAsync')
|
|
.mockImplementation(payload => {
|
|
return () => Promise.resolve(payload) as any
|
|
})
|
|
|
|
await act(async () =>
|
|
renderWithProviders(
|
|
<EventPreviewModal
|
|
open={true}
|
|
onClose={mockOnClose}
|
|
calId={'667037022b752d0026472254/cal1'}
|
|
eventId={'recurring-base/20250315T100000'}
|
|
/>,
|
|
basePreloadedState
|
|
)
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('MoreVertIcon'))
|
|
fireEvent.click(
|
|
screen.getByRole('menuitem', { name: 'eventPreview.deleteEvent' })
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByText('editModeDialog.deleteRecurrentEvent')
|
|
).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByLabelText('editModeDialog.allEvents'))
|
|
fireEvent.click(screen.getByRole('button', { name: /Ok/i }))
|
|
|
|
await waitFor(() => {
|
|
expect(spy).toHaveBeenCalled()
|
|
})
|
|
|
|
const receivedPayload = spy.mock.calls[0][0]
|
|
expect(receivedPayload.eventId).toBe('recurring-base/20250315T100000')
|
|
})
|
|
})
|
|
|
|
describe('RSVP to Recurring Event', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
it('calls updateEventInstanceAsync when accepting single instance', async () => {
|
|
const spy = jest
|
|
.spyOn(eventThunks, 'updateEventInstanceAsync')
|
|
.mockImplementation(payload => {
|
|
const promise = Promise.resolve(payload)
|
|
;(promise as any).unwrap = () => promise
|
|
return () => promise as any
|
|
})
|
|
|
|
await act(async () =>
|
|
renderWithProviders(
|
|
<EventPreviewModal
|
|
open={true}
|
|
onClose={mockOnClose}
|
|
calId={'667037022b752d0026472254/cal1'}
|
|
eventId={'recurring-base/20250315T100000'}
|
|
/>,
|
|
basePreloadedState
|
|
)
|
|
)
|
|
|
|
fireEvent.click(
|
|
screen.getByRole('button', { name: 'eventPreview.ACCEPTED' })
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByText('editModeDialog.updateParticipationStatus')
|
|
).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByLabelText('editModeDialog.thisEvent'))
|
|
fireEvent.click(screen.getByRole('button', { name: /Ok/i }))
|
|
|
|
await waitFor(() => {
|
|
expect(spy).toHaveBeenCalled()
|
|
})
|
|
|
|
const updatedEvent = spy.mock.calls[0][0].event
|
|
expect(updatedEvent.attendee[0].partstat).toBe('ACCEPTED')
|
|
})
|
|
|
|
it('calls updateSeriesPartstat when accepting all instances', async () => {
|
|
const spy = jest
|
|
.spyOn(EventApi, 'updateSeriesPartstat')
|
|
.mockResolvedValue({} as any)
|
|
|
|
await act(async () =>
|
|
renderWithProviders(
|
|
<EventPreviewModal
|
|
open={true}
|
|
onClose={mockOnClose}
|
|
calId={'667037022b752d0026472254/cal1'}
|
|
eventId={'recurring-base/20250315T100000'}
|
|
/>,
|
|
basePreloadedState
|
|
)
|
|
)
|
|
|
|
fireEvent.click(
|
|
screen.getByRole('button', { name: 'eventPreview.ACCEPTED' })
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByText('editModeDialog.updateParticipationStatus')
|
|
).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByLabelText('editModeDialog.allEvents'))
|
|
fireEvent.click(screen.getByRole('button', { name: 'common.ok' }))
|
|
|
|
await waitFor(() => {
|
|
expect(spy).toHaveBeenCalled()
|
|
})
|
|
|
|
const callArgs = spy.mock.calls[0]
|
|
expect(callArgs[0].uid).toBe('recurring-base/20250315T100000')
|
|
expect(callArgs[1]).toBe('test@test.com')
|
|
expect(callArgs[2]).toBe('ACCEPTED')
|
|
})
|
|
})
|
|
|
|
describe('Edit Recurring Event in Full Display', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
it('renders event with recurrenceId in URL', async () => {
|
|
await act(async () =>
|
|
renderWithProviders(
|
|
<EventUpdateModal
|
|
open={true}
|
|
onClose={mockOnClose}
|
|
calId={'667037022b752d0026472254/cal1'}
|
|
eventId={'recurring-base/20250315T100000'}
|
|
/>,
|
|
basePreloadedState
|
|
)
|
|
)
|
|
|
|
expect(
|
|
screen.getByDisplayValue('Recurring Event Instance')
|
|
).toBeInTheDocument()
|
|
})
|
|
|
|
it("calls updateEventInstanceAsync when saving single instance with typeOfAction='solo'", async () => {
|
|
jest.spyOn(EventApi, 'getEvent').mockResolvedValue({
|
|
...basePreloadedState.calendars.list['667037022b752d0026472254/cal1']
|
|
.events['recurring-base/20250315T100000']
|
|
} as any)
|
|
const spy = jest
|
|
.spyOn(eventThunks, 'updateEventInstanceAsync')
|
|
.mockImplementation(payload => {
|
|
return () => {
|
|
const promise = Promise.resolve()
|
|
;(promise as any).unwrap = () => promise
|
|
return promise as any
|
|
}
|
|
})
|
|
|
|
await act(async () =>
|
|
renderWithProviders(
|
|
<EventUpdateModal
|
|
open={true}
|
|
onClose={mockOnClose}
|
|
calId={'667037022b752d0026472254/cal1'}
|
|
eventId={'recurring-base/20250315T100000'}
|
|
typeOfAction="solo"
|
|
/>,
|
|
basePreloadedState
|
|
)
|
|
)
|
|
|
|
const titleField = screen.getByLabelText('event.form.title')
|
|
fireEvent.change(titleField, {
|
|
target: { value: 'Updated Single Instance' }
|
|
})
|
|
|
|
// Click Save
|
|
fireEvent.click(screen.getByText('actions.save'))
|
|
|
|
await waitFor(() => {
|
|
expect(spy).toHaveBeenCalled()
|
|
})
|
|
|
|
const updatedEvent = spy.mock.calls[0][0].event
|
|
expect(updatedEvent.title).toBe('Updated Single Instance')
|
|
expect(updatedEvent.recurrenceId).toBe('20250315T100000')
|
|
})
|
|
|
|
it("calls updateSeriesAsync when saving all instances with typeOfAction='all'", async () => {
|
|
const getEventSpy = jest.spyOn(EventApi, 'getEvent').mockResolvedValue({
|
|
...basePreloadedState.calendars.list['667037022b752d0026472254/cal1']
|
|
.events['recurring-base/20250315T100000']
|
|
} as any)
|
|
|
|
const spy = jest
|
|
.spyOn(eventThunks, 'updateSeriesAsync')
|
|
.mockImplementation(payload => {
|
|
return () => {
|
|
const promise = Promise.resolve()
|
|
;(promise as any).unwrap = () => promise
|
|
return promise as any
|
|
}
|
|
})
|
|
|
|
await act(async () =>
|
|
renderWithProviders(
|
|
<EventUpdateModal
|
|
open={true}
|
|
onClose={mockOnClose}
|
|
calId={'667037022b752d0026472254/cal1'}
|
|
eventId={'recurring-base/20250315T100000'}
|
|
typeOfAction="all"
|
|
/>,
|
|
basePreloadedState
|
|
)
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(getEventSpy).toHaveBeenCalled()
|
|
})
|
|
|
|
const titleField = screen.getByDisplayValue('Recurring Event Instance')
|
|
fireEvent.change(titleField, {
|
|
target: { value: 'Updated All Instances' }
|
|
})
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'actions.save' }))
|
|
|
|
await waitFor(() => {
|
|
expect(spy).toHaveBeenCalled()
|
|
})
|
|
|
|
const updatedEvent = spy.mock.calls[0][0].event
|
|
expect(updatedEvent.title).toBe('Updated All Instances')
|
|
})
|
|
|
|
it("disables repetition editing when typeOfAction='solo'", async () => {
|
|
await act(async () =>
|
|
renderWithProviders(
|
|
<EventUpdateModal
|
|
open={true}
|
|
onClose={mockOnClose}
|
|
calId={'667037022b752d0026472254/cal1'}
|
|
eventId={'recurring-base/20250315T100000'}
|
|
typeOfAction="solo"
|
|
eventData={
|
|
basePreloadedState.calendars.list['667037022b752d0026472254/cal1']
|
|
.events['recurring-base/20250315T100000']
|
|
}
|
|
/>,
|
|
basePreloadedState
|
|
)
|
|
)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'common.moreOptions' }))
|
|
expect(screen.getByText('event.repeat.every')).toBeInTheDocument()
|
|
expect(screen.getByText('event.repeat.end.label')).toBeInTheDocument()
|
|
|
|
const frequencySelect = screen.getByRole('radio', {
|
|
name: /event.repeat.end.after \d event.repeat.end.occurrences/i
|
|
})
|
|
expect(frequencySelect).toBeDisabled()
|
|
})
|
|
|
|
it("fetches master event data when typeOfAction='all'", async () => {
|
|
const getEventSpy = jest.spyOn(EventApi, 'getEvent').mockResolvedValue({
|
|
...basePreloadedState.calendars.list['667037022b752d0026472254/cal1']
|
|
.events['recurring-base/20250315T100000'],
|
|
uid: 'recurring-base',
|
|
title: 'Master Event Title',
|
|
description: 'Master Description'
|
|
} as any)
|
|
|
|
await act(async () =>
|
|
renderWithProviders(
|
|
<EventUpdateModal
|
|
open={true}
|
|
onClose={mockOnClose}
|
|
calId={'667037022b752d0026472254/cal1'}
|
|
eventId={'recurring-base/20250315T100000'}
|
|
typeOfAction="all"
|
|
/>,
|
|
basePreloadedState
|
|
)
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(getEventSpy).toHaveBeenCalled()
|
|
expect(screen.getByDisplayValue('Master Event Title')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Event Drag and Drop - Recurring Events', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
it('shows EditModeDialog when dragging recurring event', () => {
|
|
const mockDispatch = jest.fn()
|
|
const mockSetSelectedEvent = jest.fn()
|
|
const mockSetOpenEditModePopup = jest.fn()
|
|
const mockSetAfterChoiceFunc = jest.fn()
|
|
|
|
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: basePreloadedState.calendars.list,
|
|
setSelectedEvent: mockSetSelectedEvent,
|
|
setAfterChoiceFunc: mockSetAfterChoiceFunc,
|
|
setOpenEditModePopup: mockSetOpenEditModePopup
|
|
} as unknown as EventHandlersProps)
|
|
|
|
const mockArg = {
|
|
event: {
|
|
_def: {
|
|
extendedProps: {
|
|
uid: 'recurring-base/20250315T100000',
|
|
calId: '667037022b752d0026472254/cal1'
|
|
}
|
|
}
|
|
},
|
|
delta: { years: 0, months: 0, days: 1, milliseconds: 0 }
|
|
}
|
|
|
|
eventHandlers.handleEventDrop(mockArg)
|
|
|
|
expect(mockSetSelectedEvent).toHaveBeenCalled()
|
|
expect(mockSetOpenEditModePopup).toHaveBeenCalledWith('edit')
|
|
expect(mockSetAfterChoiceFunc).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('Event Resize - Recurring Events', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
it('shows EditModeDialog when resizing recurring event', () => {
|
|
const mockDispatch = jest.fn()
|
|
const mockSetSelectedEvent = jest.fn()
|
|
const mockSetOpenEditModePopup = jest.fn()
|
|
const mockSetAfterChoiceFunc = jest.fn()
|
|
|
|
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: basePreloadedState.calendars.list,
|
|
setSelectedEvent: mockSetSelectedEvent,
|
|
setAfterChoiceFunc: mockSetAfterChoiceFunc,
|
|
setOpenEditModePopup: mockSetOpenEditModePopup
|
|
} as unknown as EventHandlersProps)
|
|
|
|
const mockArg = {
|
|
event: {
|
|
_def: {
|
|
extendedProps: {
|
|
uid: 'recurring-base/20250315T100000',
|
|
calId: '667037022b752d0026472254/cal1'
|
|
}
|
|
}
|
|
},
|
|
startDelta: { years: 0, months: 0, days: 0, milliseconds: 0 },
|
|
endDelta: { years: 0, months: 0, days: 0, milliseconds: 3600000 } // 1 hour
|
|
}
|
|
|
|
eventHandlers.handleEventResize(mockArg)
|
|
|
|
expect(mockSetSelectedEvent).toHaveBeenCalled()
|
|
expect(mockSetOpenEditModePopup).toHaveBeenCalledWith('edit')
|
|
expect(mockSetAfterChoiceFunc).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('RepeatEvent Component - Recurrence Editing', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
it('disables repetition fields when isOwn is false', async () => {
|
|
await act(async () =>
|
|
renderWithProviders(
|
|
<EventUpdateModal
|
|
open={true}
|
|
onClose={mockOnClose}
|
|
calId={'otherCal/cal'}
|
|
eventId={'recurring-base/20250315T100000'}
|
|
/>,
|
|
{
|
|
...basePreloadedState,
|
|
calendars: {
|
|
...basePreloadedState.calendars,
|
|
list: {
|
|
'otherCal/cal': {
|
|
id: 'otherCal/cal',
|
|
name: 'Other Calendar',
|
|
color: '#00FF00',
|
|
events: {
|
|
'recurring-base/20250315T100000': {
|
|
...basePreloadedState.calendars.list[
|
|
'667037022b752d0026472254/cal1'
|
|
].events['recurring-base/20250315T100000'],
|
|
calId: 'otherCal/cal',
|
|
organizer: { cn: 'other', cal_address: 'other@test.com' }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
)
|
|
)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'common.moreOptions' }))
|
|
|
|
// Repetition section should not be visible for non-owner
|
|
expect(
|
|
screen.queryByLabelText('event.form.repetition')
|
|
).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('handleRSVP function', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
it('calls putEventAsync for non-recurring events', async () => {
|
|
const mockDispatch = jest.fn()
|
|
const mockOnClose = jest.fn()
|
|
|
|
const {
|
|
handleRSVP
|
|
} = require('@/components/Event/eventHandlers/eventHandlers')
|
|
|
|
jest.spyOn(eventThunks, 'putEventAsync').mockImplementation(payload => {
|
|
const promise = Promise.resolve(payload)
|
|
;(promise as any).unwrap = () => promise
|
|
return () => promise as any
|
|
})
|
|
|
|
const nonRecurringEvent = {
|
|
uid: 'single-event',
|
|
title: 'Single Event',
|
|
calId: '667037022b752d0026472254/cal1',
|
|
start: day.toISOString(),
|
|
end: new Date('2025-03-15T11:00:00Z').toISOString(),
|
|
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
|
attendee: [
|
|
{
|
|
cn: 'test',
|
|
cal_address: 'test@test.com',
|
|
partstat: 'NEEDS-ACTION'
|
|
}
|
|
]
|
|
}
|
|
|
|
await handleRSVP(
|
|
mockDispatch,
|
|
basePreloadedState.calendars.list['667037022b752d0026472254/cal1'],
|
|
basePreloadedState.user,
|
|
nonRecurringEvent,
|
|
'ACCEPTED'
|
|
)
|
|
|
|
expect(mockDispatch).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('handleDelete function', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
it('calls deleteEventAsync for non-recurring events', () => {
|
|
const mockDispatch = jest.fn()
|
|
const mockOnClose = jest.fn()
|
|
|
|
const {
|
|
handleDelete
|
|
} = require('@/components/Event/eventHandlers/eventHandlers')
|
|
|
|
jest.spyOn(eventThunks, 'deleteEventAsync').mockImplementation(payload => {
|
|
return () => Promise.resolve(payload) as any
|
|
})
|
|
|
|
const nonRecurringEvent = {
|
|
uid: 'single-event',
|
|
title: 'Single Event',
|
|
calId: '667037022b752d0026472254/cal1',
|
|
URL: '/calendars/667037022b752d0026472254/cal1/single-event.ics'
|
|
}
|
|
|
|
handleDelete(
|
|
false, // isRecurring
|
|
undefined,
|
|
mockOnClose,
|
|
mockDispatch,
|
|
basePreloadedState.calendars.list['667037022b752d0026472254/cal1'],
|
|
nonRecurringEvent,
|
|
'667037022b752d0026472254/cal1',
|
|
'single-event'
|
|
)
|
|
|
|
expect(mockOnClose).toHaveBeenCalledWith({}, 'backdropClick')
|
|
expect(mockDispatch).toHaveBeenCalled()
|
|
})
|
|
|
|
it('calls deleteEventInstanceAsync when deleting solo recurring event', () => {
|
|
const mockDispatch = jest.fn()
|
|
const mockOnClose = jest.fn()
|
|
|
|
const {
|
|
handleDelete
|
|
} = require('@/components/Event/eventHandlers/eventHandlers')
|
|
|
|
jest
|
|
.spyOn(eventThunks, 'deleteEventInstanceAsync')
|
|
.mockImplementation(payload => {
|
|
return () => Promise.resolve(payload) as any
|
|
})
|
|
|
|
handleDelete(
|
|
true, // isRecurring
|
|
'solo',
|
|
mockOnClose,
|
|
mockDispatch,
|
|
basePreloadedState.calendars.list['667037022b752d0026472254/cal1'],
|
|
basePreloadedState.calendars.list['667037022b752d0026472254/cal1'].events[
|
|
'recurring-base/20250315T100000'
|
|
],
|
|
'667037022b752d0026472254/cal1',
|
|
'recurring-base/20250315T100000'
|
|
)
|
|
|
|
expect(mockOnClose).toHaveBeenCalledWith({}, 'backdropClick')
|
|
expect(mockDispatch).toHaveBeenCalled()
|
|
})
|
|
|
|
it('calls deleteEventAsync when deleting all recurring events', () => {
|
|
const mockDispatch = jest.fn()
|
|
const mockOnClose = jest.fn()
|
|
|
|
const {
|
|
handleDelete
|
|
} = require('@/components/Event/eventHandlers/eventHandlers')
|
|
|
|
jest.spyOn(eventThunks, 'deleteEventAsync').mockImplementation(payload => {
|
|
return () => Promise.resolve(payload) as any
|
|
})
|
|
|
|
handleDelete(
|
|
true, // isRecurring
|
|
'all',
|
|
mockOnClose,
|
|
mockDispatch,
|
|
basePreloadedState.calendars.list['667037022b752d0026472254/cal1'],
|
|
basePreloadedState.calendars.list['667037022b752d0026472254/cal1'].events[
|
|
'recurring-base/20250315T100000'
|
|
],
|
|
'667037022b752d0026472254/cal1',
|
|
'recurring-base/20250315T100000'
|
|
)
|
|
|
|
expect(mockOnClose).toHaveBeenCalledWith({}, 'backdropClick')
|
|
expect(mockDispatch).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('Calendar Integration - EditModeDialog Flow', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
it('passes correct eventId when editing all instances from preview', async () => {
|
|
await act(async () =>
|
|
renderWithProviders(
|
|
<EventPreviewModal
|
|
open={true}
|
|
onClose={mockOnClose}
|
|
calId={'667037022b752d0026472254/cal1'}
|
|
eventId={'recurring-base/20250315T100000'}
|
|
/>,
|
|
basePreloadedState
|
|
)
|
|
)
|
|
|
|
// Click edit
|
|
fireEvent.click(screen.getByTestId('EditIcon'))
|
|
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByText('editModeDialog.updateRecurrentEvent')
|
|
).toBeInTheDocument()
|
|
})
|
|
|
|
// Select "All the events"
|
|
fireEvent.click(screen.getByLabelText('editModeDialog.allEvents'))
|
|
fireEvent.click(screen.getByRole('button', { name: /Ok/i }))
|
|
|
|
await waitFor(() => {
|
|
// Update modal should open
|
|
expect(screen.getByText('event.updateEvent')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Event URL handling for recurring events', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
it('uses base ID for event URL when moving recurring event', async () => {
|
|
const moveEventSpy = jest
|
|
.spyOn(eventThunks, 'moveEventAsync')
|
|
.mockImplementation(payload => {
|
|
const result = { calId: payload.cal.id, events: [] }
|
|
const promise = Promise.resolve(result)
|
|
;(promise as any).unwrap = () => promise
|
|
return () => promise as any
|
|
})
|
|
|
|
const twoCalState = {
|
|
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',
|
|
color: '#FF0000',
|
|
events: {
|
|
'recurring-base/20250315T100000': {
|
|
uid: 'recurring-base/20250315T100000',
|
|
title: 'Recurring Event Instance',
|
|
calId: '667037022b752d0026472254/cal1',
|
|
start: day.toISOString(),
|
|
end: new Date('2025-03-15T11:00:00Z').toISOString(),
|
|
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
|
recurrenceId: '20250315T100000',
|
|
repetition: { freq: 'weekly', occurrences: 4, interval: 1 },
|
|
timezone: 'UTC',
|
|
URL: '/calendars/667037022b752d0026472254/cal1/recurring-base.ics',
|
|
attendee: [
|
|
{
|
|
cn: 'test',
|
|
cal_address: 'test@test.com',
|
|
partstat: 'NEEDS-ACTION',
|
|
rsvp: 'TRUE',
|
|
role: 'REQ-PARTICIPANT',
|
|
cutype: 'INDIVIDUAL'
|
|
},
|
|
{
|
|
cn: 'John',
|
|
cal_address: 'john@test.com',
|
|
partstat: 'NEEDS-ACTION',
|
|
rsvp: 'TRUE',
|
|
role: 'REQ-PARTICIPANT',
|
|
cutype: 'INDIVIDUAL'
|
|
}
|
|
]
|
|
},
|
|
'recurring-base/20250322T100000': {
|
|
uid: 'recurring-base/20250322T100000',
|
|
title: 'Recurring Event Instance',
|
|
calId: '667037022b752d0026472254/cal1',
|
|
start: new Date('2025-03-22T10:00:00Z').toISOString(),
|
|
end: new Date('2025-03-22T11:00:00Z').toISOString(),
|
|
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
|
recurrenceId: '20250322T100000',
|
|
timezone: 'UTC',
|
|
URL: '/calendars/667037022b752d0026472254/cal1/recurring-base.ics',
|
|
attendee: [
|
|
{
|
|
cn: 'test',
|
|
cal_address: 'test@test.com',
|
|
partstat: 'NEEDS-ACTION',
|
|
rsvp: 'TRUE',
|
|
role: 'REQ-PARTICIPANT',
|
|
cutype: 'INDIVIDUAL'
|
|
}
|
|
]
|
|
}
|
|
},
|
|
owner: { emails: ['test@test.com'] }
|
|
},
|
|
'667037022b752d0026472254/cal2': {
|
|
id: '667037022b752d0026472254/cal2',
|
|
name: 'Calendar 2',
|
|
color: '#00FF00',
|
|
events: {}
|
|
}
|
|
},
|
|
pending: false,
|
|
templist: {}
|
|
}
|
|
}
|
|
await act(async () =>
|
|
renderWithProviders(
|
|
<EventUpdateModal
|
|
open={true}
|
|
onClose={mockOnClose}
|
|
calId={'667037022b752d0026472254/cal1'}
|
|
eventId={'recurring-base/20250315T100000'}
|
|
/>,
|
|
twoCalState
|
|
)
|
|
)
|
|
|
|
// Expand to show calendar combobox (normal mode shows SectionPreviewRow)
|
|
fireEvent.click(screen.getByRole('button', { name: 'common.moreOptions' }))
|
|
fireEvent.mouseDown(screen.getByLabelText('event.form.calendar'))
|
|
const option = await screen.findByText('Calendar 2')
|
|
fireEvent.click(option)
|
|
|
|
// Click Save button
|
|
const saveButton = screen.getByRole('button', { name: 'actions.save' })
|
|
|
|
await act(async () => {
|
|
fireEvent.click(saveButton)
|
|
// Wait for the 500ms delay in the code
|
|
await new Promise(resolve => setTimeout(resolve, 600))
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(moveEventSpy).toHaveBeenCalled()
|
|
})
|
|
|
|
const movePayload = moveEventSpy.mock.calls[0][0]
|
|
// Should use base ID, not full uid with recurrence ID
|
|
expect(movePayload.newURL).toContain('recurring-base.ics')
|
|
expect(movePayload.newURL).not.toContain('/20250315T100000')
|
|
})
|
|
})
|