Files
workavia-calendar-front/__test__/features/Events/EventUpdateModal.recurring.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

530 lines
16 KiB
TypeScript

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(
<EventUpdateModal
open={true}
onClose={mockOnClose}
calId={calId}
eventId={`${baseUID}/20250116`} // User clicked on modified instance
typeOfAction="all"
/>,
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(
<EventUpdateModal
open={true}
onClose={mockOnClose}
calId={calId}
eventId={baseUID} // Already master event
typeOfAction="all"
/>,
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(
<EventUpdateModal
open={true}
onClose={mockOnClose}
calId={calId}
eventId={`${baseUID}/20250115`}
typeOfAction="all"
/>,
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(
<EventUpdateModal
open={true}
onClose={mockOnClose}
calId={calId}
eventId={`${baseUID}/20250115`}
typeOfAction="all"
/>,
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(
<EventUpdateModal
open={true}
onClose={mockOnClose}
calId={calId}
eventId={`${baseUID}/20250115`}
typeOfAction="all"
/>,
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(
<EventUpdateModal
open={true}
onClose={mockOnClose}
calId={calId}
eventId={`${baseUID}/20250117`} // User clicked on Jan 17 instance
typeOfAction="all"
/>,
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')
})
})
})
})