Files
workavia-calendar-front/__test__/features/Calendars/CalendarSlice.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

537 lines
16 KiB
TypeScript

import * as calAPI from '@/features/Calendars/CalendarApi'
import reducer, {
addEvent,
createCalendar,
removeEvent,
removeTempCal,
updateEventLocal
} from '@/features/Calendars/CalendarSlice'
import { Calendar } from '@/features/Calendars/CalendarTypes'
import {
addSharedCalendarAsync,
createCalendarAsync,
getCalendarDetailAsync,
getCalendarsListAsync,
getEventAsync,
getTempCalendarsListAsync,
patchACLCalendarAsync
} from '@/features/Calendars/services'
import { CalendarEvent } from '@/features/Events/EventsTypes'
import * as userAPI from '@/features/User/userAPI'
import userReducer, { setUserData } from '@/features/User/userSlice'
import { configureStore } from '@reduxjs/toolkit'
jest.mock('@/features/Calendars/CalendarApi')
jest.mock('@/features/User/userAPI')
jest.mock('@/features/Events/EventApi')
jest.mock('@/features/Events/utils')
jest.mock('@/utils/apiUtils')
describe('CalendarSlice', () => {
const initialState = {
list: {},
templist: {},
pending: false,
error: null
}
beforeEach(() => {
jest.resetAllMocks()
})
describe('reducers', () => {
it('createCalendar adds new calendar', () => {
const action = createCalendar({
name: 'Test Cal',
color: '#ff0000',
description: 'desc'
})
const state = reducer(initialState, action)
const values = Object.values(state.list)
expect(values[0].name).toBe('Test Cal')
expect(values[0].color).toBe('#ff0000')
})
it('addEvent adds event to calendar', () => {
const calId = 'user/cal'
const event = { uid: 'event1', title: 'My Event' } as any
const stateWithCal = {
...initialState,
list: { [calId]: { id: calId, events: {} } as any }
}
const state = reducer(
stateWithCal,
addEvent({ calendarUid: calId, event })
)
expect(state.list[calId].events['event1']).toEqual(
expect.objectContaining({ uid: 'event1' })
)
})
it('removeEvent deletes event', () => {
const calId = 'user/cal'
const stateWithEvent = {
...initialState,
list: {
[calId]: {
id: calId,
events: { e1: { uid: 'e1' } }
} as unknown as Calendar
}
}
const state = reducer(
stateWithEvent,
removeEvent({ calendarUid: calId, eventUid: 'e1' })
)
expect(state.list[calId].events).toEqual({})
})
it('updateEventLocal updates an event', () => {
const calId = 'user/cal'
const stateWithEvent = {
...initialState,
list: {
[calId]: {
id: calId,
events: { e1: { uid: 'e1', title: 'Old' } }
} as unknown as Calendar
}
}
const state = reducer(
stateWithEvent,
updateEventLocal({ calId, event: { uid: 'e1', title: 'New' } as any })
)
expect(state.list[calId].events.e1.title).toBe('New')
})
it('removeTempCal deletes temp calendar', () => {
const stateWithTemp = {
...initialState,
templist: { temp1: { id: 'temp1' } as any }
}
const state = reducer(stateWithTemp, removeTempCal('temp1'))
expect(state.templist).toEqual({})
})
})
describe('extraReducers (thunks)', () => {
const storeFactory = () =>
configureStore({
reducer: { calendars: reducer, user: userReducer }
})
beforeEach(() => {
jest.resetAllMocks()
})
it('getCalendarsListAsync.fulfilled replaces list', async () => {
;(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: 'u1' })
;(calAPI.getCalendars as jest.Mock).mockResolvedValue({
_embedded: { 'dav:calendar': [] }
})
;(userAPI.getUserDetails as jest.Mock).mockResolvedValue({
firstname: 'Alice',
lastname: 'Smith',
emails: ['a@b.com']
})
const store = storeFactory()
await store.dispatch(getCalendarsListAsync() as any)
const state = store.getState().calendars
expect(state.list).toEqual({})
})
it('getCalendarsListAsync loads user details in parallel for multiple owners', async () => {
const mockCalendars = [
{
_links: { self: { href: '/calendars/u1/cal1.json' } },
'dav:name': 'Calendar 1',
'apple:color': '#FF0000',
acl: []
},
{
_links: { self: { href: '/calendars/u2/cal2.json' } },
'dav:name': 'Calendar 2',
'apple:color': '#00FF00',
acl: []
},
{
_links: { self: { href: '/calendars/u3/cal3.json' } },
'dav:name': 'Calendar 3',
'apple:color': '#0000FF',
acl: []
}
]
;(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: 'u1' })
;(calAPI.getCalendars as jest.Mock).mockResolvedValue({
_embedded: { 'dav:calendar': mockCalendars }
})
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock
getUserDetailsMock
.mockResolvedValueOnce({
firstname: 'Alice',
lastname: 'Smith',
emails: ['alice@example.com']
})
.mockResolvedValueOnce({
firstname: 'Bob',
lastname: 'Jones',
emails: ['bob@example.com']
})
.mockResolvedValueOnce({
firstname: 'Charlie',
lastname: 'Brown',
emails: ['charlie@example.com']
})
const store = storeFactory()
await store.dispatch(getCalendarsListAsync() as any)
expect(getUserDetailsMock).toHaveBeenCalledTimes(3)
expect(getUserDetailsMock).toHaveBeenCalledWith('u1')
expect(getUserDetailsMock).toHaveBeenCalledWith('u2')
expect(getUserDetailsMock).toHaveBeenCalledWith('u3')
const state = store.getState().calendars
expect(state.list['u1/cal1'].owner.firstname).toContain('Alice')
expect(state.list['u2/cal2'].owner.firstname).toContain('Bob')
expect(state.list['u3/cal3'].owner.firstname).toContain('Charlie')
})
it('getCalendarsListAsync deduplicates getUserDetails calls for same ownerId', async () => {
const mockCalendars = [
{
_links: { self: { href: '/calendars/u1/cal1.json' } },
'dav:name': 'Calendar 1',
'apple:color': '#FF0000',
acl: []
},
{
_links: { self: { href: '/calendars/u1/cal2.json' } },
'dav:name': 'Calendar 2',
'apple:color': '#00FF00',
acl: []
},
{
_links: { self: { href: '/calendars/u1/cal3.json' } },
'dav:name': 'Calendar 3',
'apple:color': '#0000FF',
acl: []
}
]
;(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: 'u1' })
;(calAPI.getCalendars as jest.Mock).mockResolvedValue({
_embedded: { 'dav:calendar': mockCalendars }
})
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock
getUserDetailsMock.mockResolvedValue({
firstname: 'Alice',
lastname: 'Smith',
emails: ['alice@example.com']
})
const store = storeFactory()
await store.dispatch(getCalendarsListAsync() as any)
expect(getUserDetailsMock).toHaveBeenCalledTimes(1)
expect(getUserDetailsMock).toHaveBeenCalledWith('u1')
})
it('getCalendarsListAsync processes owners in batches of 20', async () => {
const mockCalendars = Array.from({ length: 45 }, (_, i) => ({
_links: { self: { href: `/calendars/u${i + 1}/cal${i + 1}.json` } },
'dav:name': `Calendar ${i + 1}`,
'apple:color': '#FF0000',
acl: []
}))
;(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: 'u1' })
;(calAPI.getCalendars as jest.Mock).mockResolvedValue({
_embedded: { 'dav:calendar': mockCalendars }
})
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock
getUserDetailsMock.mockImplementation((ownerId: string) =>
Promise.resolve({
firstname: 'User',
lastname: ownerId,
emails: [`${ownerId}@example.com`]
})
)
const store = storeFactory()
await store.dispatch(getCalendarsListAsync() as any)
expect(getUserDetailsMock).toHaveBeenCalledTimes(45)
const state = store.getState().calendars
expect(Object.keys(state.list)).toHaveLength(45)
})
it('getCalendarsListAsync doesnt call getUserDetails if userdata exist in store', async () => {
const existingCalendars = {
'u1/cal1': {
id: 'u1/cal1',
name: 'Existing Calendar',
events: {}
} as Calendar
}
const store = storeFactory()
store.dispatch({
type: 'calendars/getCalendars/fulfilled',
payload: { importedCalendars: existingCalendars, errors: '' }
})
store.dispatch(setUserData({ openpaasId: 'bla' }))
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock
await store.dispatch(getCalendarsListAsync() as any)
expect(getUserDetailsMock).not.toHaveBeenCalled()
const state = store.getState().calendars
expect(state.list).toEqual(existingCalendars)
})
it('getCalendarsListAsync handles errors in getUserDetails gracefully', async () => {
const mockCalendars = [
{
_links: { self: { href: '/calendars/u1/cal1.json' } },
'dav:name': 'Calendar 1',
'apple:color': '#FF0000',
acl: []
},
{
_links: { self: { href: '/calendars/u2/cal2.json' } },
'dav:name': 'Calendar 2',
'apple:color': '#00FF00',
acl: []
}
]
;(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: 'u1' })
;(calAPI.getCalendars as jest.Mock).mockResolvedValue({
_embedded: { 'dav:calendar': mockCalendars }
})
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock
getUserDetailsMock
.mockResolvedValueOnce({
firstname: 'Alice',
lastname: 'Smith',
emails: ['alice@example.com']
})
.mockRejectedValueOnce(new Error('Failed to fetch user'))
const store = storeFactory()
const result = await store.dispatch(getCalendarsListAsync() as any)
expect(getUserDetailsMock).toHaveBeenCalledTimes(2)
const state = store.getState().calendars
expect(state.list['u1/cal1'].owner.firstname).toContain('Alice')
expect(state.list['u2/cal2'].owner.lastname).toContain('Unknown User')
expect(result.payload.errors).toBeTruthy()
})
it('patchACLCalendarAsync.fulfilled sets visibility', () => {
const prev = {
...initialState,
list: { c1: { id: 'c1', visibility: 'public' } as any }
}
const state = reducer(
prev,
patchACLCalendarAsync.fulfilled(
{ calId: 'c1', calLink: 'l', request: '' },
'req3',
{ calId: 'c1', calLink: 'l', request: '' }
)
)
expect(state.list.c1.visibility).toBe('private')
})
it('createCalendarAsync.fulfilled adds a new calendar', () => {
const payload = {
userData: {
openpaasId: 'u1',
family_name: 'Owner',
email: 'o@example.com',
sid: '',
sub: '',
given_name: 'Test',
name: 'Test Owner'
},
calId: 'cal1',
color: { 'apple:color': '#f00' },
name: 'Test',
desc: 'Desc'
}
const payloadResponse = {
userId: 'u1',
calId: 'cal1',
color: { 'apple:color': '#f00' },
name: 'Test',
desc: 'Desc',
owner: { firstname: 'Owner', emails: ['o@example.com'] }
}
const state = reducer(
initialState,
createCalendarAsync.fulfilled(payloadResponse, 'req4', payload)
)
expect(state.list['u1/cal1'].name).toBe('Test')
expect(state.list['u1/cal1'].color?.['apple:color']).toBe('#f00')
})
it('addSharedCalendarAsync.fulfilled adds shared calendar', () => {
const payload = {
calId: 'c1',
color: { 'apple:color': '#0f0' },
link: '/calendars/u1/c1.json',
name: 'Shared',
desc: 'Shared Desc',
owner: { firstname: 'O', emails: ['o@example.com'] }
}
const mockCal = {
cal: {
_links: { self: { href: '/calendars/u1/c1.json' } },
'apple:color': '#0f0',
'caldav:description': 'Shared Desc',
'dav:name': 'Shared'
}
}
const state = reducer(
initialState,
addSharedCalendarAsync.fulfilled(payload, 'req5', {
userId: 'u1',
calId: 'c1',
cal: mockCal
})
)
expect(state.list['c1'].name).toBe('Shared')
})
it('getTempCalendarsListAsync.fulfilled updates templist', () => {
const payload = {
t1: {
id: 't1',
name: 'Temp',
color: { 'apple:color': '#aaa' },
events: {},
visibility: 'public',
owner: { firstname: 'O', emails: ['o@o.com'] },
link: '/calendars/t1.json',
description: 'desc'
} as Calendar
}
const state = reducer(
initialState,
getTempCalendarsListAsync.fulfilled(payload, 'req7', {
openpaasId: 'u1',
color: { 'apple:color': '#aaa' },
displayName: 'test',
avatarUrl: '',
email: 'test@test.com'
})
)
expect(state.templist.t1.name).toBe('Temp')
})
it('getEventAsync.fulfilled adds single event', () => {
const payload = { calId: 'c1', event: { uid: 'e1' } as any }
const state = reducer(
initialState,
getEventAsync.fulfilled(payload, 'req9', { uid: 'e1' } as any)
)
expect(state.list.c1.events.e1.uid).toBe('e1')
})
it('getCalendarDetailAsync.fulfilled adds calendar events', () => {
const payload = { calId: 'c1', events: [{ uid: 'e1' }] as any[] }
const state = reducer(
{
...initialState,
list: {
['c1']: {
id: 'c1',
events: {}
} as unknown as Calendar
}
},
getCalendarDetailAsync.fulfilled(payload, 'req11', {
calId: 'c1',
match: { start: '', end: '' }
})
)
expect(state.list.c1.events.e1.uid).toBe('e1')
})
it('getEventAsync.fulfilled doesnt create new events when there are already event with base UID', () => {
const baseUid = 'recurring-event-base'
const existingEvent = {
uid: `${baseUid}/20240115`,
title: 'Existing Recurring Event',
recurrenceId: null
} as unknown as CalendarEvent
const newEventInstance = {
uid: baseUid,
title: 'Fetched Master Event',
recurrenceId: '20240115'
} as CalendarEvent
const stateWithEvent = {
...initialState,
list: {
c1: {
id: 'c1',
events: { [`${baseUid}/20240115`]: existingEvent }
} as unknown as Calendar
}
}
const payload = { calId: 'c1', event: newEventInstance }
const state = reducer(
stateWithEvent,
getEventAsync.fulfilled(payload, 'req', newEventInstance)
)
// Should still only have the base event, not the instance
expect(Object.keys(state.list.c1.events)).toHaveLength(1)
expect(state.list.c1.events[`${baseUid}/20240115`]).toBeDefined()
expect(state.list.c1.events[baseUid]).toBeUndefined()
})
it("getEventAsync.fulfilled create new event when there isn't any event with base UID", () => {
const eventUid = 'new-event-uid'
const newEvent = {
uid: eventUid,
title: 'New Event',
recurrenceId: null
} as unknown as CalendarEvent
const stateWithoutEvent = {
...initialState,
list: {
c1: {
id: 'c1',
events: {}
} as unknown as Calendar
}
}
const payload = { calId: 'c1', event: newEvent }
const state = reducer(
stateWithoutEvent,
getEventAsync.fulfilled(payload, 'req', newEvent)
)
// Should create the new event
expect(Object.keys(state.list.c1.events)).toHaveLength(1)
expect(state.list.c1.events[eventUid]).toBeDefined()
expect(state.list.c1.events[eventUid].uid).toBe(eventUid)
expect(state.list.c1.events[eventUid].title).toBe('New Event')
})
})
})