Files
workavia-calendar-front/__test__/features/websocket/utils/notificationStorm.test.tsx
T
2026-04-24 10:05:42 +02:00

137 lines
4.2 KiB
TypeScript

import type { AppDispatch, RootState } from '@/app/store'
import { store } from '@/app/store'
import { refreshCalendarWithSyncToken } from '@/features/Calendars/services'
import { getDisplayedCalendarRange } from '@/utils'
import { updateCalendars } from '@/websocket/messaging/updateCalendars'
jest.mock('@/features/Calendars/services', () => ({
refreshCalendarWithSyncToken: jest.fn()
}))
jest.mock('@/utils', () => ({
getDisplayedCalendarRange: jest.fn(),
findCalendarById: jest.requireActual('@/utils').findCalendarById
}))
jest.mock('@/app/store', () => ({
store: {
getState: jest.fn()
}
}))
jest.useFakeTimers()
const mockDispatch = jest.fn()
const mockRange = {
start: new Date('2025-01-15T10:00:00Z'),
end: new Date('2025-01-16T10:00:00Z')
}
const mockState = {
calendars: {
list: {
'cal1/entry1': { id: 'cal1/entry1', name: 'Calendar 1', syncToken: 1 },
'cal2/entry2': { id: 'cal2/entry2', name: 'Calendar 2', syncToken: 1 },
'cal/A': { id: 'cal/A', name: 'Cal A', syncToken: 1 },
'cal/B': { id: 'cal/B', name: 'Cal B', syncToken: 1 },
'cal/C': { id: 'cal/C', name: 'Cal C', syncToken: 1 }
},
templist: {}
}
} as unknown as RootState
const mockAccumulators: {
calendarsToRefresh: Map<string, any>
calendarsToHide: Set<string>
debouncedUpdateFn?: (dispatch: AppDispatch) => void
shouldRefreshCalendarListRef: React.MutableRefObject<boolean>
currentDebouncePeriod?: number
} = {
calendarsToRefresh: new Map<string, any>(),
calendarsToHide: new Set(),
shouldRefreshCalendarListRef: { current: false },
currentDebouncePeriod: 0,
debouncedUpdateFn: undefined
}
describe('websocket messages storm', () => {
beforeEach(() => {
;(refreshCalendarWithSyncToken as unknown as jest.Mock).mockClear()
jest.clearAllMocks()
jest.clearAllTimers()
jest.resetModules()
;(getDisplayedCalendarRange as jest.Mock).mockReturnValue(mockRange)
;(store.getState as jest.Mock).mockReturnValue(mockState)
window.WS_DEBOUNCE_PERIOD_MS = 500
window.WS_SKIP_DELAY_MS = 0
mockAccumulators.calendarsToRefresh = new Map<string, any>()
mockAccumulators.calendarsToHide = new Set()
mockAccumulators.shouldRefreshCalendarListRef.current = false
mockAccumulators.currentDebouncePeriod = 0
mockAccumulators.debouncedUpdateFn = undefined
})
it('debounces calendar updates during message storm', () => {
const mockMessage = {
'/calendars/cal1/entry1': {
syncToken: 'ldsk'
}
}
for (let i = 0; i < 50; i++) {
updateCalendars(mockMessage, mockDispatch, mockAccumulators)
}
// Dispatch called once because of leading edge
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(1)
// Trailing edge
jest.advanceTimersByTime(500)
// only one call for the last message + leading edge message
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(2)
})
it('debounces calendar updates during message storm with multiple updates', () => {
// Send a storm with mixed messages
for (let i = 0; i < 50; i++) {
if (i % 3 === 0)
updateCalendars(
{ '/calendars/cal/A': { syncToken: 'ldskfjsld' + i } },
mockDispatch,
mockAccumulators
)
else if (i % 3 === 1)
updateCalendars(
{ '/calendars/cal/B': { syncToken: 'ldskfjsld' + i } },
mockDispatch,
mockAccumulators
)
else
updateCalendars(
{ '/calendars/cal/C': { syncToken: 'ldskfjsld' + i } },
mockDispatch,
mockAccumulators
)
}
// Dispatch called once because of leading edge
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(1)
// Trailing edge
jest.advanceTimersByTime(500)
// Trailing edge updates once per calendar + the original leading edge
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(4)
})
it('executes immediately when debounce is disabled', () => {
window.WS_DEBOUNCE_PERIOD_MS = 0
updateCalendars(
{ '/calendars/cal1/entry1': { syncToken: 'abc' } },
mockDispatch,
mockAccumulators
)
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(1)
})
})