Files
workavia-calendar-front/__test__/components/EventModifications.test.tsx
T
lethemanh cadfa70e60 #708 apply strictier linting rules (#717)
* #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>
2026-04-01 17:15:10 +02:00

616 lines
19 KiB
TypeScript

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(
<CalendarApp calendarRef={mockCalendarRef} />,
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<CalendarApi | null> = (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(
<CalendarApp calendarRef={mockCalendarRef} />,
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(
<CalendarApp calendarRef={mockCalendarRef} />,
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(
<CalendarApp calendarRef={mockCalendarRef} />,
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(<CalendarApp calendarRef={mockCalendarRef} />, {
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(
<EventUpdateModal
eventId="event1"
calId="667037022b752d0026472254/cal1"
open={true}
onClose={onClose}
typeOfAction="solo"
/>,
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(
<EventUpdateModal
eventId="event1"
calId="667037022b752d0026472254/cal1"
open={true}
onClose={onClose}
typeOfAction="solo"
/>,
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
})
})
})