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

995 lines
28 KiB
TypeScript

import { setSelectedCalendars } from '@/utils/storage/setSelectedCalendars'
import { createWebSocketConnection } from '@/websocket/connection/createConnection'
import { registerToCalendars } from '@/websocket/operations/registerToCalendars'
import { unregisterToCalendars } from '@/websocket/operations/unregisterToCalendars'
import { WebSocketGate } from '@/websocket/WebSocketGate'
import { configureStore, Store } from '@reduxjs/toolkit'
import { act, cleanup, render, waitFor } from '@testing-library/react'
import { Provider } from 'react-redux'
import { I18nContext } from 'twake-i18n'
jest.mock('@/websocket/connection/createConnection')
jest.mock('@/websocket/operations/registerToCalendars')
jest.mock('@/websocket/operations/unregisterToCalendars')
jest.mock('@/websocket/connection/lifecycle/pingWebSocket')
function TestWrapper({ store }: { store: Store }) {
return (
<I18nContext.Provider
value={{
t: (key: string, vars?: Record<string, string>) => {
if (key === 'locale') return 'en'
if (vars) {
const params = Object.entries(vars)
.map(([k, v]) => `${k}=${v}`)
.join(',')
return `${key}(${params})`
}
return key
},
f: (date: Date) => date.toString(),
lang: 'en'
}}
>
<Provider store={store}>
<WebSocketGate />
</Provider>
</I18nContext.Provider>
)
}
describe('WebSocketGate', () => {
let store: any
let mockSocket: any
const createMockStore = (
userData: any = { id: '1' },
tokens: any = { access: 'token' }
) => {
return configureStore({
reducer: {
user: (state = { userData, tokens }) => state,
calendars: (state = { list: {} }) => state
}
})
}
const createMockSocket = (readyState = WebSocket.OPEN) => ({
readyState,
close: jest.fn(),
send: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
cleanup: jest.fn(),
onmessage: null
})
beforeEach(() => {
store = createMockStore()
mockSocket = createMockSocket()
localStorage.clear()
})
afterEach(() => {
cleanup()
jest.clearAllMocks()
jest.clearAllTimers()
localStorage.clear()
})
describe('Authentication', () => {
beforeEach(() => {
jest.useFakeTimers()
jest.clearAllMocks()
jest.resetModules()
})
afterEach(() => {
jest.runOnlyPendingTimers()
jest.useRealTimers()
})
it('should not create connection when user is not authenticated', () => {
const unauthStore = createMockStore(null, null)
render(<TestWrapper store={unauthStore} />)
expect(createWebSocketConnection).not.toHaveBeenCalled()
})
it('should create connection when user is authenticated', async () => {
;(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalledTimes(1)
})
})
it('should close existing socket when user logs out', async () => {
;(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket)
const { rerender } = render(<TestWrapper store={store} />)
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalled()
})
const unauthStore = createMockStore(null, null)
rerender(<TestWrapper store={unauthStore} />)
await waitFor(() => {
expect(mockSocket.close).toHaveBeenCalled()
})
})
})
describe('Socket Connection Management', () => {
it('should create connection with callbacks', async () => {
;(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalledWith(
expect.objectContaining({
onMessage: expect.any(Function),
onClose: expect.any(Function),
onError: expect.any(Function)
})
)
})
})
it('should handle socket close via callback', async () => {
let onCloseCallback: Function | undefined
;(createWebSocketConnection as jest.Mock).mockImplementation(
callbacks => {
onCloseCallback = callbacks.onClose
return Promise.resolve(mockSocket)
}
)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalled()
})
// Simulate close event
await act(async () => {
onCloseCallback?.(new CloseEvent('close'))
})
// Verify that subsequent calendar changes don't try to register
await act(async () => {
setSelectedCalendars(['cal1'])
})
await waitFor(() => {
expect(registerToCalendars).not.toHaveBeenCalled()
})
})
it('should handle connection errors gracefully', async () => {
const consoleError = jest.spyOn(console, 'error').mockImplementation()
;(createWebSocketConnection as jest.Mock).mockRejectedValue(
new Error('Connection failed')
)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(consoleError).toHaveBeenCalledWith(
'Failed to create WebSocket connection:',
expect.any(Error)
)
})
consoleError.mockRestore()
})
it('should close socket on component unmount', async () => {
;(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket)
const { unmount } = render(<TestWrapper store={store} />)
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalled()
})
unmount()
expect(mockSocket.close).toHaveBeenCalled()
})
})
describe('Reconnection Logic', () => {
beforeEach(() => {
jest.useFakeTimers()
jest.clearAllMocks()
jest.resetModules()
})
afterEach(() => {
jest.runOnlyPendingTimers()
jest.useRealTimers()
})
it('should trigger reconnection on unexpected close (code 1006)', async () => {
jest.useFakeTimers()
const consoleWarn = jest.spyOn(console, 'warn').mockImplementation()
let onCloseCallback: Function | undefined
;(createWebSocketConnection as jest.Mock).mockImplementation(
callbacks => {
onCloseCallback = callbacks.onClose
return Promise.resolve(mockSocket)
}
)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalledTimes(1)
})
// Simulate unexpected close
await act(async () => {
onCloseCallback?.(
new CloseEvent('close', { code: 1006, reason: 'Connection lost' })
)
})
expect(consoleWarn).toHaveBeenCalledWith(
expect.stringContaining('WebSocket closed unexpectedly (code: 1006')
)
// Advance timer to trigger reconnection
await act(async () => {
jest.advanceTimersByTime(1000)
})
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalledTimes(2)
})
consoleWarn.mockRestore()
jest.useRealTimers()
})
it('should NOT reconnect on normal close (code 1000)', async () => {
jest.useFakeTimers()
let onCloseCallback: Function | undefined
;(createWebSocketConnection as jest.Mock).mockImplementation(
callbacks => {
onCloseCallback = callbacks.onClose
return Promise.resolve(mockSocket)
}
)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalledTimes(1)
})
// Simulate normal close
await act(async () => {
onCloseCallback?.(new CloseEvent('close', { code: 1000 }))
})
// Advance timer
await act(async () => {
jest.advanceTimersByTime(5000)
})
// Should NOT reconnect
expect(createWebSocketConnection).toHaveBeenCalledTimes(1)
jest.useRealTimers()
})
it('should NOT reconnect on going away (code 1001)', async () => {
jest.useFakeTimers()
let onCloseCallback: Function | undefined
;(createWebSocketConnection as jest.Mock).mockImplementation(
callbacks => {
onCloseCallback = callbacks.onClose
return Promise.resolve(mockSocket)
}
)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalledTimes(1)
})
// Simulate going away
await act(async () => {
onCloseCallback?.(new CloseEvent('close', { code: 1001 }))
})
await act(async () => {
jest.advanceTimersByTime(5000)
})
expect(createWebSocketConnection).toHaveBeenCalledTimes(1)
jest.useRealTimers()
})
it('should reset reconnection attempts counter on successful connection', async () => {
jest.useFakeTimers()
const consoleLog = jest.spyOn(console, 'info').mockImplementation()
let onCloseCallback: Function | undefined
;(createWebSocketConnection as jest.Mock).mockImplementation(
callbacks => {
onCloseCallback = callbacks.onClose
return Promise.resolve(mockSocket)
}
)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalledTimes(1)
})
// First failure
await act(async () => {
onCloseCallback?.(new CloseEvent('close', { code: 1006 }))
jest.advanceTimersByTime(1000)
})
expect(consoleLog).toHaveBeenCalledWith(
expect.stringContaining('(attempt 1/10)')
)
// Success - should reset counter
// Next failure should start from attempt 1 again
await act(async () => {
onCloseCallback?.(new CloseEvent('close', { code: 1006 }))
})
// Should schedule with initial delay (attempt 1)
expect(consoleLog).toHaveBeenCalledWith(
expect.stringContaining('(attempt 1/10)')
)
consoleLog.mockRestore()
jest.useRealTimers()
})
it('should not reconnect if authentication is lost during reconnection timeout', async () => {
jest.useFakeTimers()
let onCloseCallback: Function | undefined
;(createWebSocketConnection as jest.Mock).mockImplementation(
callbacks => {
onCloseCallback = callbacks.onClose
return Promise.resolve(mockSocket)
}
)
const { rerender } = render(<TestWrapper store={store} />)
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalledTimes(1)
})
// Trigger reconnection
await act(async () => {
onCloseCallback?.(new CloseEvent('close', { code: 1006 }))
})
// Lose authentication before timeout fires
const unauthStore = createMockStore(null, null)
rerender(<TestWrapper store={unauthStore} />)
// Advance timer
await act(async () => {
jest.advanceTimersByTime(1000)
})
// Should NOT reconnect (still only 1 connection)
expect(createWebSocketConnection).toHaveBeenCalledTimes(1)
jest.useRealTimers()
})
it('should clear reconnection timeout on component unmount', async () => {
jest.useFakeTimers()
let onCloseCallback: Function | undefined
;(createWebSocketConnection as jest.Mock).mockImplementation(
callbacks => {
onCloseCallback = callbacks.onClose
return Promise.resolve(mockSocket)
}
)
const { unmount } = render(<TestWrapper store={store} />)
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalledTimes(1)
})
// Trigger reconnection
await act(async () => {
onCloseCallback?.(new CloseEvent('close', { code: 1006 }))
})
// Unmount before timeout fires
unmount()
// Advance timer
await act(async () => {
jest.advanceTimersByTime(1000)
})
// Should NOT reconnect after unmount
expect(createWebSocketConnection).toHaveBeenCalledTimes(1)
jest.useRealTimers()
})
it('should re-sync calendars after reconnection', async () => {
jest.useFakeTimers()
localStorage.setItem(
'selectedCalendars',
JSON.stringify(['cal1', 'cal2'])
)
let onCloseCallback: Function | undefined
;(createWebSocketConnection as jest.Mock).mockImplementation(
callbacks => {
onCloseCallback = callbacks.onClose
return Promise.resolve(mockSocket)
}
)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(registerToCalendars).toHaveBeenCalledWith(mockSocket, [
'/calendars/cal1',
'/calendars/cal2'
])
})
jest.clearAllMocks()
// Close and reconnect
await act(async () => {
onCloseCallback?.(new CloseEvent('close', { code: 1006 }))
jest.advanceTimersByTime(1000)
})
// Should re-register all calendars after reconnection
await waitFor(() => {
expect(registerToCalendars).toHaveBeenCalledWith(mockSocket, [
'/calendars/cal1',
'/calendars/cal2'
])
})
jest.useRealTimers()
})
})
describe('Browser Online/Offline Events', () => {
beforeEach(() => {
jest.useFakeTimers()
jest.clearAllMocks()
jest.resetModules()
})
afterEach(() => {
jest.runOnlyPendingTimers()
jest.useRealTimers()
})
it('should trigger immediate reconnection when browser goes online', async () => {
let onCloseCallback: Function | undefined
;(createWebSocketConnection as jest.Mock).mockImplementation(
callbacks => {
onCloseCallback = callbacks.onClose
return Promise.resolve(mockSocket)
}
)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalledTimes(1)
})
// Close connection
await act(async () => {
onCloseCallback?.(new CloseEvent('close', { code: 1006 }))
})
// Trigger online event
await act(async () => {
window.dispatchEvent(new Event('online'))
})
// Should trigger immediate reconnection (no delay)
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalledTimes(2)
})
})
it('should pause reconnection attempts when browser goes offline', async () => {
jest.useFakeTimers()
let onCloseCallback: Function | undefined
;(createWebSocketConnection as jest.Mock).mockImplementation(
callbacks => {
onCloseCallback = callbacks.onClose
return Promise.resolve(mockSocket)
}
)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalledTimes(1)
})
// Close connection
await act(async () => {
onCloseCallback?.(new CloseEvent('close', { code: 1006 }))
})
// Go offline before reconnection fires
await act(async () => {
window.dispatchEvent(new Event('offline'))
})
// Advance timer - should NOT reconnect
await act(async () => {
jest.advanceTimersByTime(10000)
})
expect(createWebSocketConnection).toHaveBeenCalledTimes(1)
jest.useRealTimers()
})
it('should not reconnect when online event fires if already connected', async () => {
;(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalledTimes(1)
})
// Trigger online event while connected
await act(async () => {
window.dispatchEvent(new Event('online'))
})
// Should NOT trigger new connection
expect(createWebSocketConnection).toHaveBeenCalledTimes(1)
})
it('should reset attempt counter when online event fires', async () => {
jest.useFakeTimers()
const consoleLog = jest.spyOn(console, 'info').mockImplementation()
let onCloseCallback: Function | undefined
;(createWebSocketConnection as jest.Mock).mockImplementation(
callbacks => {
onCloseCallback = callbacks.onClose
return Promise.resolve(mockSocket)
}
)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalledTimes(1)
})
// Multiple failed attempts
await act(async () => {
onCloseCallback?.(new CloseEvent('close', { code: 1006 }))
jest.advanceTimersByTime(1000)
})
await act(async () => {
onCloseCallback?.(new CloseEvent('close', { code: 1006 }))
jest.advanceTimersByTime(2000)
})
// Go offline then online
await act(async () => {
window.dispatchEvent(new Event('offline'))
window.dispatchEvent(new Event('online'))
})
// Next reconnection should start from attempt 1
await act(async () => {
onCloseCallback?.(new CloseEvent('close', { code: 1006 }))
})
expect(consoleLog).toHaveBeenCalledWith(
expect.stringContaining('(attempt 1/10)')
)
consoleLog.mockRestore()
jest.useRealTimers()
})
})
describe('Calendar Registration', () => {
it('should not register if socket is not open', async () => {
localStorage.setItem(
'selectedCalendars',
JSON.stringify(['cal1', 'cal2'])
)
const connectingSocket = createMockSocket(WebSocket.CONNECTING)
;(createWebSocketConnection as jest.Mock).mockResolvedValue(
connectingSocket
)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalled()
})
expect(registerToCalendars).not.toHaveBeenCalled()
})
it('should register to calendars when socket opens and calendars are selected', async () => {
localStorage.setItem(
'selectedCalendars',
JSON.stringify(['cal1', 'cal2'])
)
;(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(registerToCalendars).toHaveBeenCalledWith(mockSocket, [
'/calendars/cal1',
'/calendars/cal2'
])
})
})
it('should register only new calendars when calendar list changes', async () => {
localStorage.setItem('selectedCalendars', JSON.stringify(['cal1']))
;(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(registerToCalendars).toHaveBeenCalledWith(mockSocket, [
'/calendars/cal1'
])
})
jest.clearAllMocks()
await act(async () => {
setSelectedCalendars(['cal1', 'cal2', 'cal3'])
})
await waitFor(() => {
expect(registerToCalendars).toHaveBeenCalledWith(mockSocket, [
'/calendars/cal2',
'/calendars/cal3'
])
})
})
it('should unregister removed calendars when calendar list changes', async () => {
localStorage.setItem(
'selectedCalendars',
JSON.stringify(['cal1', 'cal2', 'cal3'])
)
;(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(registerToCalendars).toHaveBeenCalled()
})
jest.clearAllMocks()
await act(async () => {
setSelectedCalendars(['cal1'])
})
await waitFor(() => {
expect(unregisterToCalendars).toHaveBeenCalledWith(mockSocket, [
'/calendars/cal2',
'/calendars/cal3'
])
})
})
it('should both register and unregister when calendar list partially changes', async () => {
localStorage.setItem(
'selectedCalendars',
JSON.stringify(['cal1', 'cal2'])
)
;(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(registerToCalendars).toHaveBeenCalled()
})
jest.clearAllMocks()
await act(async () => {
setSelectedCalendars(['cal1', 'cal3'])
})
await waitFor(() => {
expect(registerToCalendars).toHaveBeenCalledWith(mockSocket, [
'/calendars/cal3'
])
expect(unregisterToCalendars).toHaveBeenCalledWith(mockSocket, [
'/calendars/cal2'
])
})
})
it('should handle registration errors and not update previous calendar list', async () => {
const consoleError = jest.spyOn(console, 'error').mockImplementation()
localStorage.setItem('selectedCalendars', JSON.stringify(['cal1']))
;(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket)
;(registerToCalendars as jest.Mock).mockImplementation(() => {
throw new Error('Registration failed')
})
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(consoleError).toHaveBeenCalledWith(
'Failed to register calendar:',
expect.any(Error)
)
})
await waitFor(() => {
expect(mockSocket.close).not.toHaveBeenCalled()
})
jest.clearAllMocks()
;(registerToCalendars as jest.Mock).mockImplementation(() => {})
await act(async () => {
setSelectedCalendars(['cal1', 'cal2'])
})
await waitFor(() => {
// Should register both calendars since previous update failed and didn't update the ref
expect(registerToCalendars).toHaveBeenCalledWith(mockSocket, [
'/calendars/cal1',
'/calendars/cal2'
])
})
consoleError.mockRestore()
})
it('should not attempt registration when no calendars are selected', async () => {
localStorage.setItem('selectedCalendars', JSON.stringify([]))
;(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalled()
})
expect(registerToCalendars).not.toHaveBeenCalled()
expect(unregisterToCalendars).not.toHaveBeenCalled()
})
})
describe('Edge Cases', () => {
it('should handle rapid calendar changes correctly', async () => {
localStorage.setItem('selectedCalendars', JSON.stringify(['cal1']))
;(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(registerToCalendars).toHaveBeenCalledTimes(1)
})
// Rapid changes
await act(async () => {
setSelectedCalendars(['cal1', 'cal2'])
setSelectedCalendars(['cal1', 'cal2', 'cal3'])
})
await waitFor(() => {
expect(registerToCalendars).toHaveBeenCalled()
})
})
it('should handle socket closed during calendar update', async () => {
localStorage.setItem('selectedCalendars', JSON.stringify(['cal1']))
;(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(registerToCalendars).toHaveBeenCalled()
})
jest.clearAllMocks()
await act(async () => {
mockSocket.readyState = WebSocket.CLOSED
setSelectedCalendars(['cal1', 'cal2'])
})
// Wait a bit to ensure the effect would have run if it was going to
jest.useFakeTimers()
jest.advanceTimersByTime(100)
jest.useRealTimers()
expect(registerToCalendars).not.toHaveBeenCalled()
})
it('should handle unregistration errors gracefully', async () => {
const consoleError = jest.spyOn(console, 'error').mockImplementation()
localStorage.setItem(
'selectedCalendars',
JSON.stringify(['cal1', 'cal2'])
)
;(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(registerToCalendars).toHaveBeenCalled()
})
jest.clearAllMocks()
;(unregisterToCalendars as jest.Mock).mockImplementation(() => {
throw new Error('Unregistration failed')
})
await act(async () => {
setSelectedCalendars(['cal1'])
})
await waitFor(() => {
expect(consoleError).toHaveBeenCalledWith(
'Failed to unregister calendar:',
expect.any(Error)
)
})
consoleError.mockRestore()
})
it('should handle socket that becomes open after initial connection', async () => {
const closedSocket = createMockSocket(WebSocket.CONNECTING)
localStorage.setItem('selectedCalendars', JSON.stringify(['cal1']))
;(createWebSocketConnection as jest.Mock).mockResolvedValue(closedSocket)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalled()
})
// Socket is not open yet, should not register
expect(registerToCalendars).not.toHaveBeenCalled()
// Simulate socket becoming open
await act(async () => {
closedSocket.readyState = WebSocket.OPEN
// Trigger a calendar change to re-run the effect
setSelectedCalendars(['cal1', 'cal2'])
})
// Still should not register because isSocketOpen state is false
// The component only sets isSocketOpen to true when socket.readyState is OPEN during connection
expect(registerToCalendars).not.toHaveBeenCalled()
})
})
describe('Ping/Pong Integration', () => {
beforeEach(() => {
jest.useFakeTimers()
})
afterEach(() => {
jest.runOnlyPendingTimers()
jest.useRealTimers()
})
it('should trigger reconnection when ping detects dead connection (via socket close)', async () => {
const consoleWarn = jest.spyOn(console, 'warn').mockImplementation()
let onCloseCallback: ((event: CloseEvent) => void) | undefined
;(createWebSocketConnection as jest.Mock).mockImplementation(
callbacks => {
onCloseCallback = callbacks.onClose
return Promise.resolve(mockSocket)
}
)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalledTimes(1)
})
;(createWebSocketConnection as jest.Mock).mockClear()
// In the real implementation, when ping detects dead connection,
// it calls socket.close() which triggers the onClose callback
// This simulates that flow
await act(async () => {
if (onCloseCallback) {
onCloseCallback(new CloseEvent('close', { code: 1006 }))
}
})
// Should schedule reconnection
expect(consoleWarn).toHaveBeenCalledWith(
expect.stringContaining('WebSocket closed unexpectedly')
)
// Advance to trigger reconnection
await act(async () => {
jest.advanceTimersByTime(1500)
})
// Should reconnect
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalledTimes(1)
})
consoleWarn.mockRestore()
})
it('should stop ping monitoring when socket closes normally', async () => {
let onCloseCallback: ((event: CloseEvent) => void) | undefined
;(createWebSocketConnection as jest.Mock).mockImplementation(
callbacks => {
onCloseCallback = callbacks.onClose
return Promise.resolve(mockSocket)
}
)
render(<TestWrapper store={store} />)
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalled()
})
// Normal close (code 1000) - like logout or page navigation
await act(async () => {
if (onCloseCallback) {
onCloseCallback(new CloseEvent('close', { code: 1000 }))
}
})
// Should not attempt reconnection
await act(async () => {
jest.advanceTimersByTime(5000)
})
expect(createWebSocketConnection).toHaveBeenCalledTimes(1)
})
it('should cleanup ping monitoring on component unmount', async () => {
;(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket)
const { unmount } = render(<TestWrapper store={store} />)
await waitFor(() => {
expect(createWebSocketConnection).toHaveBeenCalled()
})
// Unmount should cleanup
unmount()
expect(mockSocket.close).toHaveBeenCalled()
})
})
})