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>
581 lines
18 KiB
TypeScript
581 lines
18 KiB
TypeScript
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(
|
|
<EventPopover
|
|
anchorEl={document.body}
|
|
open={true}
|
|
onClose={mockOnClose}
|
|
selectedRange={selectedRange}
|
|
setSelectedRange={mockSetSelectedRange}
|
|
calendarRef={mockCalendarRef}
|
|
/>,
|
|
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()
|
|
})
|
|
})
|
|
})
|
|
})
|