#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>
This commit is contained in:
lethemanh
2026-04-01 22:15:10 +07:00
committed by GitHub
parent 2bff6aae78
commit cadfa70e60
321 changed files with 27452 additions and 27600 deletions
File diff suppressed because it is too large Load Diff
@@ -1,46 +1,46 @@
import { api } from "@/utils/apiUtils";
import { fetchWebSocketTicket } from "@/websocket/api/fetchWebSocketTicket";
import { api } from '@/utils/apiUtils'
import { fetchWebSocketTicket } from '@/websocket/api/fetchWebSocketTicket'
jest.mock("@/utils/apiUtils");
jest.mock('@/utils/apiUtils')
describe("fetchWebSocketTicket", () => {
describe('fetchWebSocketTicket', () => {
const mockTicket = {
clientAddress: "127.0.0.1",
value: "test-ticket-123",
generatedOn: "2025-01-12T10:00:00Z",
validUntil: "2025-01-12T11:00:00Z",
username: "testuser",
};
clientAddress: '127.0.0.1',
value: 'test-ticket-123',
generatedOn: '2025-01-12T10:00:00Z',
validUntil: '2025-01-12T11:00:00Z',
username: 'testuser'
}
beforeEach(() => {
jest.clearAllMocks();
});
jest.clearAllMocks()
})
it("should fetch ticket successfully", async () => {
(api.post as jest.Mock).mockResolvedValue({
it('should fetch ticket successfully', async () => {
;(api.post as jest.Mock).mockResolvedValue({
ok: true,
json: async () => mockTicket,
});
json: async () => mockTicket
})
const ticket = await fetchWebSocketTicket();
const ticket = await fetchWebSocketTicket()
expect(api.post).toHaveBeenCalledWith("ws/ticket");
expect(ticket).toEqual(mockTicket);
});
expect(api.post).toHaveBeenCalledWith('ws/ticket')
expect(ticket).toEqual(mockTicket)
})
it("should throw error when response is not ok", async () => {
(api.post as jest.Mock).mockResolvedValue({
ok: false,
});
it('should throw error when response is not ok', async () => {
;(api.post as jest.Mock).mockResolvedValue({
ok: false
})
await expect(fetchWebSocketTicket()).rejects.toThrow(
"Failed to fetch WebSocket ticket"
);
});
'Failed to fetch WebSocket ticket'
)
})
it("should throw error when network fails", async () => {
(api.post as jest.Mock).mockRejectedValue(new Error("Network error"));
it('should throw error when network fails', async () => {
;(api.post as jest.Mock).mockRejectedValue(new Error('Network error'))
await expect(fetchWebSocketTicket()).rejects.toThrow("Network error");
});
});
await expect(fetchWebSocketTicket()).rejects.toThrow('Network error')
})
})
@@ -1,211 +1,210 @@
import { fetchWebSocketTicket } from "@/websocket/api/fetchWebSocketTicket";
import { createWebSocketConnection } from "@/websocket/connection/createConnection";
import { WS_INBOUND_EVENTS } from "@/websocket/protocols";
import { waitFor } from "@testing-library/dom";
import { setupWebsocket } from "./utils/setupWebsocket";
import { fetchWebSocketTicket } from '@/websocket/api/fetchWebSocketTicket'
import { createWebSocketConnection } from '@/websocket/connection/createConnection'
import { WS_INBOUND_EVENTS } from '@/websocket/protocols'
import { waitFor } from '@testing-library/dom'
import { setupWebsocket } from './utils/setupWebsocket'
jest.mock("@/websocket/api/fetchWebSocketTicket");
jest.mock('@/websocket/api/fetchWebSocketTicket')
describe("createWebSocketConnection", () => {
let mockWebSocket: jest.Mock;
let cleanup: () => void;
let webSocketInstances: any[] = [];
describe('createWebSocketConnection', () => {
let mockWebSocket: jest.Mock
let cleanup: () => void
let webSocketInstances: any[] = []
const mockTicket = {
value: "test-ticket-123",
clientAddress: "127.0.0.1",
generatedOn: "2025-01-12T10:00:00Z",
validUntil: "2025-01-12T11:00:00Z",
username: "testuser",
};
value: 'test-ticket-123',
clientAddress: '127.0.0.1',
generatedOn: '2025-01-12T10:00:00Z',
validUntil: '2025-01-12T11:00:00Z',
username: 'testuser'
}
/** ---------- Helpers ---------- */
const getWs = () => webSocketInstances[0];
const getWs = () => webSocketInstances[0]
const triggerEvent = (ws: any, event: string, payload?: any) => {
ws._listeners[event]?.[0]?.(payload);
};
ws._listeners[event]?.[0]?.(payload)
}
const createAndOpenConnection = async () => {
const mockCallbacks = {
onMessage: jest.fn(),
onClose: jest.fn(),
onError: jest.fn(),
};
const promise = createWebSocketConnection(mockCallbacks);
onError: jest.fn()
}
const promise = createWebSocketConnection(mockCallbacks)
await waitFor(() => {
expect(webSocketInstances.length).toBe(1);
});
expect(webSocketInstances.length).toBe(1)
})
triggerEvent(getWs(), WS_INBOUND_EVENTS.CONNECTION_OPENED);
const socket = await promise;
triggerEvent(getWs(), WS_INBOUND_EVENTS.CONNECTION_OPENED)
const socket = await promise
return { socket, ws: getWs(), promise, mockCallbacks };
};
return { socket, ws: getWs(), promise, mockCallbacks }
}
/** ---------- Setup ---------- */
beforeEach(() => {
({ webSocketInstances, mockWebSocket, cleanup } = setupWebsocket());
window.WEBSOCKET_URL = "wss://calendar.example.com";
(fetchWebSocketTicket as jest.Mock).mockResolvedValue(mockTicket);
});
;({ webSocketInstances, mockWebSocket, cleanup } = setupWebsocket())
window.WEBSOCKET_URL = 'wss://calendar.example.com'
;(fetchWebSocketTicket as jest.Mock).mockResolvedValue(mockTicket)
})
afterEach(() => {
jest.clearAllMocks();
delete window.WEBSOCKET_URL;
delete window.CALENDAR_BASE_URL;
cleanup();
});
jest.clearAllMocks()
delete window.WEBSOCKET_URL
delete window.CALENDAR_BASE_URL
cleanup()
})
/** ---------- Tests ---------- */
it("throws when WEBSOCKET_URL is not defined", async () => {
delete window.WEBSOCKET_URL;
it('throws when WEBSOCKET_URL is not defined', async () => {
delete window.WEBSOCKET_URL
const mockCallbacks = {
onMessage: jest.fn(),
};
onMessage: jest.fn()
}
await expect(createWebSocketConnection(mockCallbacks)).rejects.toThrow(
"WEBSOCKET_URL is not defined"
);
});
'WEBSOCKET_URL is not defined'
)
})
it("fetches WebSocket ticket", async () => {
await createAndOpenConnection();
expect(fetchWebSocketTicket).toHaveBeenCalledTimes(1);
});
it('fetches WebSocket ticket', async () => {
await createAndOpenConnection()
expect(fetchWebSocketTicket).toHaveBeenCalledTimes(1)
})
it("creates WebSocket with correct URL and ticket", async () => {
await createAndOpenConnection();
it('creates WebSocket with correct URL and ticket', async () => {
await createAndOpenConnection()
expect(mockWebSocket).toHaveBeenCalledWith(
"wss://calendar.example.com/ws?ticket=test-ticket-123"
);
});
'wss://calendar.example.com/ws?ticket=test-ticket-123'
)
})
it("creates WebSocket with correct URL and ticket without the WEBSOCKET_URL", async () => {
delete window.WEBSOCKET_URL;
window.CALENDAR_BASE_URL = "https://calendar.example.com";
await createAndOpenConnection();
it('creates WebSocket with correct URL and ticket without the WEBSOCKET_URL', async () => {
delete window.WEBSOCKET_URL
window.CALENDAR_BASE_URL = 'https://calendar.example.com'
await createAndOpenConnection()
expect(mockWebSocket).toHaveBeenCalledWith(
"wss://calendar.example.com/ws?ticket=test-ticket-123"
);
});
'wss://calendar.example.com/ws?ticket=test-ticket-123'
)
})
it("resolves with socket when connection opens", async () => {
const { socket, ws } = await createAndOpenConnection();
expect(socket).toBe(ws);
});
it('resolves with socket when connection opens', async () => {
const { socket, ws } = await createAndOpenConnection()
expect(socket).toBe(ws)
})
it("rejects when connection fails", async () => {
it('rejects when connection fails', async () => {
const mockCallbacks = {
onMessage: jest.fn(),
};
const promise = createWebSocketConnection(mockCallbacks);
onMessage: jest.fn()
}
const promise = createWebSocketConnection(mockCallbacks)
await waitFor(() => {
expect(webSocketInstances.length).toBe(1);
});
expect(webSocketInstances.length).toBe(1)
})
triggerEvent(getWs(), WS_INBOUND_EVENTS.ERROR, new Event("error"));
triggerEvent(getWs(), WS_INBOUND_EVENTS.ERROR, new Event('error'))
await expect(promise).rejects.toThrow("WebSocket connection failed");
});
await expect(promise).rejects.toThrow('WebSocket connection failed')
})
it("attaches message event listener", async () => {
const { ws } = await createAndOpenConnection();
it('attaches message event listener', async () => {
const { ws } = await createAndOpenConnection()
expect(ws._listeners[WS_INBOUND_EVENTS.MESSAGE]).toBeDefined();
expect(ws._listeners[WS_INBOUND_EVENTS.MESSAGE].length).toBeGreaterThan(0);
});
expect(ws._listeners[WS_INBOUND_EVENTS.MESSAGE]).toBeDefined()
expect(ws._listeners[WS_INBOUND_EVENTS.MESSAGE].length).toBeGreaterThan(0)
})
it("attaches close event listener", async () => {
const { ws } = await createAndOpenConnection();
expect(ws._listeners[WS_INBOUND_EVENTS.CONNECTION_CLOSED]).toBeDefined();
});
it('attaches close event listener', async () => {
const { ws } = await createAndOpenConnection()
expect(ws._listeners[WS_INBOUND_EVENTS.CONNECTION_CLOSED]).toBeDefined()
})
it("handles invalid JSON messages", async () => {
const errorSpy = jest.spyOn(console, "error").mockImplementation();
it('handles invalid JSON messages', async () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation()
const { ws } = await createAndOpenConnection();
const { ws } = await createAndOpenConnection()
triggerEvent(ws, WS_INBOUND_EVENTS.MESSAGE, { data: "invalid json" });
triggerEvent(ws, WS_INBOUND_EVENTS.MESSAGE, { data: 'invalid json' })
expect(errorSpy).toHaveBeenCalledWith(
"Failed to parse WebSocket message:",
'Failed to parse WebSocket message:',
expect.any(Error)
);
)
errorSpy.mockRestore();
});
errorSpy.mockRestore()
})
it("rejects on timeout", async () => {
jest.useFakeTimers();
it('rejects on timeout', async () => {
jest.useFakeTimers()
const mockCallbacks = {
onMessage: jest.fn(),
};
const promise = createWebSocketConnection(mockCallbacks);
onMessage: jest.fn()
}
const promise = createWebSocketConnection(mockCallbacks)
await waitFor(() => {
expect(webSocketInstances.length).toBe(1);
});
expect(webSocketInstances.length).toBe(1)
})
jest.advanceTimersByTime(10000);
jest.advanceTimersByTime(10000)
await expect(promise).rejects.toThrow("WebSocket connection timed out");
await expect(promise).rejects.toThrow('WebSocket connection timed out')
jest.useRealTimers();
});
jest.useRealTimers()
})
it("calls onMessage callback when message received", async () => {
const { ws, mockCallbacks } = await createAndOpenConnection();
it('calls onMessage callback when message received', async () => {
const { ws, mockCallbacks } = await createAndOpenConnection()
const testMessage = { type: "test", payload: "data" };
const testMessage = { type: 'test', payload: 'data' }
triggerEvent(ws, WS_INBOUND_EVENTS.MESSAGE, {
data: JSON.stringify(testMessage),
});
data: JSON.stringify(testMessage)
})
expect(mockCallbacks.onMessage).toHaveBeenCalledWith(testMessage);
});
expect(mockCallbacks.onMessage).toHaveBeenCalledWith(testMessage)
})
it("does not call onMessage when JSON parsing fails", async () => {
const errorSpy = jest.spyOn(console, "error").mockImplementation();
const { ws, mockCallbacks } = await createAndOpenConnection();
it('does not call onMessage when JSON parsing fails', async () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation()
const { ws, mockCallbacks } = await createAndOpenConnection()
triggerEvent(ws, WS_INBOUND_EVENTS.MESSAGE, { data: "invalid json" });
triggerEvent(ws, WS_INBOUND_EVENTS.MESSAGE, { data: 'invalid json' })
expect(mockCallbacks.onMessage).not.toHaveBeenCalled();
expect(mockCallbacks.onMessage).not.toHaveBeenCalled()
expect(errorSpy).toHaveBeenCalledWith(
"Failed to parse WebSocket message:",
'Failed to parse WebSocket message:',
expect.any(Error)
);
)
errorSpy.mockRestore();
});
errorSpy.mockRestore()
})
it("calls onClose callback when connection closes", async () => {
const { ws, mockCallbacks } = await createAndOpenConnection();
it('calls onClose callback when connection closes', async () => {
const { ws, mockCallbacks } = await createAndOpenConnection()
const closeEvent = new CloseEvent("close", {
const closeEvent = new CloseEvent('close', {
code: 1000,
reason: "Normal closure",
});
triggerEvent(ws, WS_INBOUND_EVENTS.CONNECTION_CLOSED, closeEvent);
reason: 'Normal closure'
})
triggerEvent(ws, WS_INBOUND_EVENTS.CONNECTION_CLOSED, closeEvent)
expect(mockCallbacks.onClose).toHaveBeenCalledWith(closeEvent);
});
expect(mockCallbacks.onClose).toHaveBeenCalledWith(closeEvent)
})
it("calls onError callback when error occurs", async () => {
const { ws, mockCallbacks } = await createAndOpenConnection();
it('calls onError callback when error occurs', async () => {
const { ws, mockCallbacks } = await createAndOpenConnection()
const errorEvent = new Event("error");
triggerEvent(ws, WS_INBOUND_EVENTS.ERROR, errorEvent);
const errorEvent = new Event('error')
triggerEvent(ws, WS_INBOUND_EVENTS.ERROR, errorEvent)
expect(mockCallbacks.onError).toHaveBeenCalledWith(errorEvent);
});
});
expect(mockCallbacks.onError).toHaveBeenCalledWith(errorEvent)
})
})
@@ -1,41 +1,41 @@
import { getRetryDelay } from "@/utils/getRetryDelay";
import { getRetryDelay } from '@/utils/getRetryDelay'
import {
MAX_RECONNECT_ATTEMPTS,
RECONNECT_CONFIG,
useWebSocketReconnect,
} from "@/websocket/connection/lifecycle/useWebSocketReconnect";
import { act, renderHook } from "@testing-library/react";
import { MutableRefObject } from "react";
useWebSocketReconnect
} from '@/websocket/connection/lifecycle/useWebSocketReconnect'
import { act, renderHook } from '@testing-library/react'
import { MutableRefObject } from 'react'
// Mock the retry delay utility
jest.mock("@/utils/getRetryDelay");
jest.mock('@/utils/getRetryDelay')
const mockGetRetryDelay = getRetryDelay as jest.MockedFunction<
typeof getRetryDelay
>;
>
describe("useWebSocketReconnect", () => {
let reconnectTimeoutRef: MutableRefObject<NodeJS.Timeout | null>;
let isAuthenticatedRef: MutableRefObject<boolean>;
let reconnectAttemptsRef: MutableRefObject<number>;
let setShouldConnect: jest.Mock;
describe('useWebSocketReconnect', () => {
let reconnectTimeoutRef: MutableRefObject<NodeJS.Timeout | null>
let isAuthenticatedRef: MutableRefObject<boolean>
let reconnectAttemptsRef: MutableRefObject<number>
let setShouldConnect: jest.Mock
beforeEach(() => {
jest.useFakeTimers();
reconnectTimeoutRef = { current: null };
isAuthenticatedRef = { current: true };
reconnectAttemptsRef = { current: 0 };
setShouldConnect = jest.fn();
mockGetRetryDelay.mockReturnValue(1000); // Default 1 second delay
});
jest.useFakeTimers()
reconnectTimeoutRef = { current: null }
isAuthenticatedRef = { current: true }
reconnectAttemptsRef = { current: 0 }
setShouldConnect = jest.fn()
mockGetRetryDelay.mockReturnValue(1000) // Default 1 second delay
})
afterEach(() => {
jest.runOnlyPendingTimers();
jest.useRealTimers();
jest.clearAllMocks();
});
jest.runOnlyPendingTimers()
jest.useRealTimers()
jest.clearAllMocks()
})
describe("scheduleReconnect", () => {
it("should schedule a reconnection with correct delay", () => {
describe('scheduleReconnect', () => {
it('should schedule a reconnection with correct delay', () => {
const { result } = renderHook(() =>
useWebSocketReconnect(
reconnectTimeoutRef,
@@ -43,26 +43,26 @@ describe("useWebSocketReconnect", () => {
reconnectAttemptsRef,
setShouldConnect
)
);
)
act(() => {
result.current.scheduleReconnect();
});
result.current.scheduleReconnect()
})
expect(mockGetRetryDelay).toHaveBeenCalledWith(0, RECONNECT_CONFIG);
expect(reconnectTimeoutRef.current).not.toBeNull();
expect(setShouldConnect).not.toHaveBeenCalled();
expect(mockGetRetryDelay).toHaveBeenCalledWith(0, RECONNECT_CONFIG)
expect(reconnectTimeoutRef.current).not.toBeNull()
expect(setShouldConnect).not.toHaveBeenCalled()
act(() => {
jest.advanceTimersByTime(1000);
});
jest.advanceTimersByTime(1000)
})
expect(setShouldConnect).toHaveBeenCalledWith(expect.any(Function));
expect(reconnectAttemptsRef.current).toBe(1);
});
expect(setShouldConnect).toHaveBeenCalledWith(expect.any(Function))
expect(reconnectAttemptsRef.current).toBe(1)
})
it("should not schedule reconnection if not authenticated", () => {
isAuthenticatedRef = { current: false };
it('should not schedule reconnection if not authenticated', () => {
isAuthenticatedRef = { current: false }
const { result } = renderHook(() =>
useWebSocketReconnect(
reconnectTimeoutRef,
@@ -70,19 +70,19 @@ describe("useWebSocketReconnect", () => {
reconnectAttemptsRef,
setShouldConnect
)
);
)
act(() => {
result.current.scheduleReconnect();
});
result.current.scheduleReconnect()
})
expect(reconnectTimeoutRef.current).toBeNull();
expect(mockGetRetryDelay).not.toHaveBeenCalled();
});
expect(reconnectTimeoutRef.current).toBeNull()
expect(mockGetRetryDelay).not.toHaveBeenCalled()
})
it("should stop after MAX_RECONNECT_ATTEMPTS", () => {
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation();
reconnectAttemptsRef.current = MAX_RECONNECT_ATTEMPTS;
it('should stop after MAX_RECONNECT_ATTEMPTS', () => {
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
reconnectAttemptsRef.current = MAX_RECONNECT_ATTEMPTS
const { result } = renderHook(() =>
useWebSocketReconnect(
@@ -91,23 +91,23 @@ describe("useWebSocketReconnect", () => {
reconnectAttemptsRef,
setShouldConnect
)
);
)
act(() => {
result.current.scheduleReconnect();
});
result.current.scheduleReconnect()
})
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining(
`Max WebSocket reconnection attempts (${MAX_RECONNECT_ATTEMPTS})`
)
);
expect(reconnectTimeoutRef.current).toBeNull();
)
expect(reconnectTimeoutRef.current).toBeNull()
consoleErrorSpy.mockRestore();
});
consoleErrorSpy.mockRestore()
})
it("should increment attempt counter on each reconnection", () => {
it('should increment attempt counter on each reconnection', () => {
const { result } = renderHook(() =>
useWebSocketReconnect(
reconnectTimeoutRef,
@@ -115,26 +115,26 @@ describe("useWebSocketReconnect", () => {
reconnectAttemptsRef,
setShouldConnect
)
);
)
expect(reconnectAttemptsRef.current).toBe(0);
expect(reconnectAttemptsRef.current).toBe(0)
act(() => {
result.current.scheduleReconnect();
jest.advanceTimersByTime(1000);
});
result.current.scheduleReconnect()
jest.advanceTimersByTime(1000)
})
expect(reconnectAttemptsRef.current).toBe(1);
expect(reconnectAttemptsRef.current).toBe(1)
act(() => {
result.current.scheduleReconnect();
jest.advanceTimersByTime(1000);
});
result.current.scheduleReconnect()
jest.advanceTimersByTime(1000)
})
expect(reconnectAttemptsRef.current).toBe(2);
});
expect(reconnectAttemptsRef.current).toBe(2)
})
it("should toggle setShouldConnect correctly", () => {
it('should toggle setShouldConnect correctly', () => {
const { result } = renderHook(() =>
useWebSocketReconnect(
reconnectTimeoutRef,
@@ -142,24 +142,24 @@ describe("useWebSocketReconnect", () => {
reconnectAttemptsRef,
setShouldConnect
)
);
)
act(() => {
result.current.scheduleReconnect();
jest.advanceTimersByTime(1000);
});
result.current.scheduleReconnect()
jest.advanceTimersByTime(1000)
})
expect(setShouldConnect).toHaveBeenCalledTimes(1);
expect(setShouldConnect).toHaveBeenCalledTimes(1)
// Test the toggle function
const toggleFn = setShouldConnect.mock.calls[0][0];
expect(toggleFn(false)).toBe(true);
expect(toggleFn(true)).toBe(false);
});
});
const toggleFn = setShouldConnect.mock.calls[0][0]
expect(toggleFn(false)).toBe(true)
expect(toggleFn(true)).toBe(false)
})
})
describe("clearReconnectTimeout", () => {
it("should clear pending timeout", () => {
describe('clearReconnectTimeout', () => {
it('should clear pending timeout', () => {
const { result } = renderHook(() =>
useWebSocketReconnect(
reconnectTimeoutRef,
@@ -167,29 +167,29 @@ describe("useWebSocketReconnect", () => {
reconnectAttemptsRef,
setShouldConnect
)
);
)
act(() => {
result.current.scheduleReconnect();
});
result.current.scheduleReconnect()
})
expect(reconnectTimeoutRef.current).not.toBeNull();
expect(reconnectTimeoutRef.current).not.toBeNull()
act(() => {
result.current.clearReconnectTimeout();
});
result.current.clearReconnectTimeout()
})
expect(reconnectTimeoutRef.current).toBeNull();
expect(reconnectTimeoutRef.current).toBeNull()
// Timeout should not fire
act(() => {
jest.advanceTimersByTime(1000);
});
jest.advanceTimersByTime(1000)
})
expect(setShouldConnect).not.toHaveBeenCalled();
});
expect(setShouldConnect).not.toHaveBeenCalled()
})
it("should handle multiple clears gracefully", () => {
it('should handle multiple clears gracefully', () => {
const { result } = renderHook(() =>
useWebSocketReconnect(
reconnectTimeoutRef,
@@ -197,18 +197,18 @@ describe("useWebSocketReconnect", () => {
reconnectAttemptsRef,
setShouldConnect
)
);
)
act(() => {
result.current.clearReconnectTimeout();
result.current.clearReconnectTimeout();
result.current.clearReconnectTimeout();
});
result.current.clearReconnectTimeout()
result.current.clearReconnectTimeout()
result.current.clearReconnectTimeout()
})
expect(reconnectTimeoutRef.current).toBeNull();
});
expect(reconnectTimeoutRef.current).toBeNull()
})
it("should clear timeout before scheduling new one", () => {
it('should clear timeout before scheduling new one', () => {
const { result } = renderHook(() =>
useWebSocketReconnect(
reconnectTimeoutRef,
@@ -216,35 +216,35 @@ describe("useWebSocketReconnect", () => {
reconnectAttemptsRef,
setShouldConnect
)
);
)
// Schedule first reconnection
act(() => {
result.current.scheduleReconnect();
});
result.current.scheduleReconnect()
})
const firstTimeout = reconnectTimeoutRef.current;
expect(firstTimeout).not.toBeNull();
const firstTimeout = reconnectTimeoutRef.current
expect(firstTimeout).not.toBeNull()
// Schedule second reconnection (should clear first)
act(() => {
result.current.scheduleReconnect();
});
result.current.scheduleReconnect()
})
const secondTimeout = reconnectTimeoutRef.current;
expect(secondTimeout).not.toBeNull();
expect(secondTimeout).not.toBe(firstTimeout);
const secondTimeout = reconnectTimeoutRef.current
expect(secondTimeout).not.toBeNull()
expect(secondTimeout).not.toBe(firstTimeout)
// Only second timeout should fire
act(() => {
jest.advanceTimersByTime(1000);
});
jest.advanceTimersByTime(1000)
})
expect(setShouldConnect).toHaveBeenCalledTimes(1);
});
});
it("should stop reconnecting after MAX_RECONNECT_ATTEMPTS (10)", () => {
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation();
expect(setShouldConnect).toHaveBeenCalledTimes(1)
})
})
it('should stop reconnecting after MAX_RECONNECT_ATTEMPTS (10)', () => {
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
const { result } = renderHook(() =>
useWebSocketReconnect(
reconnectTimeoutRef,
@@ -252,32 +252,32 @@ describe("useWebSocketReconnect", () => {
reconnectAttemptsRef,
setShouldConnect
)
);
)
// Simulate 10 reconnection attempts
for (let i = 0; i < MAX_RECONNECT_ATTEMPTS; i++) {
act(() => {
result.current.scheduleReconnect();
result.current.scheduleReconnect()
jest.advanceTimersByTime(
mockGetRetryDelay.mock.results[i]?.value || 1000
);
});
)
})
}
expect(reconnectAttemptsRef.current).toBe(MAX_RECONNECT_ATTEMPTS);
expect(setShouldConnect).toHaveBeenCalledTimes(MAX_RECONNECT_ATTEMPTS);
expect(reconnectAttemptsRef.current).toBe(MAX_RECONNECT_ATTEMPTS)
expect(setShouldConnect).toHaveBeenCalledTimes(MAX_RECONNECT_ATTEMPTS)
// Try to schedule one more reconnection - should fail
act(() => {
result.current.scheduleReconnect();
});
result.current.scheduleReconnect()
})
expect(consoleErrorSpy).toHaveBeenCalledWith(
`Max WebSocket reconnection attempts (${MAX_RECONNECT_ATTEMPTS}) reached. Giving up.`
);
expect(reconnectTimeoutRef.current).toBeNull();
expect(setShouldConnect).toHaveBeenCalledTimes(MAX_RECONNECT_ATTEMPTS); // Should not increment
)
expect(reconnectTimeoutRef.current).toBeNull()
expect(setShouldConnect).toHaveBeenCalledTimes(MAX_RECONNECT_ATTEMPTS) // Should not increment
consoleErrorSpy.mockRestore();
});
});
consoleErrorSpy.mockRestore()
})
})
@@ -1,135 +1,135 @@
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";
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('@/features/Calendars/services', () => ({
refreshCalendarWithSyncToken: jest.fn()
}))
jest.mock("@/utils", () => ({
jest.mock('@/utils', () => ({
getDisplayedCalendarRange: jest.fn(),
findCalendarById: jest.requireActual("@/utils").findCalendarById,
}));
findCalendarById: jest.requireActual('@/utils').findCalendarById
}))
jest.mock("@/app/store", () => ({
jest.mock('@/app/store', () => ({
store: {
getState: jest.fn(),
},
}));
getState: jest.fn()
}
}))
jest.useFakeTimers();
const mockDispatch = 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"),
};
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 },
'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;
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: 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,
};
debouncedUpdateFn: undefined
}
describe("websocket messages storm", () => {
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;
mockAccumulators.calendarsToRefresh = new Map<string, any>();
mockAccumulators.calendarsToHide = new Set();
mockAccumulators.shouldRefreshCalendarListRef.current = false;
mockAccumulators.currentDebouncePeriod = 0;
;(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
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", () => {
mockAccumulators.debouncedUpdateFn = undefined
})
it('debounces calendar updates during message storm', () => {
const mockMessage = {
"/calendars/cal1/entry1": {
syncToken: "ldsk",
},
};
'/calendars/cal1/entry1': {
syncToken: 'ldsk'
}
}
for (let i = 0; i < 50; i++) {
updateCalendars(mockMessage, mockDispatch, mockAccumulators);
updateCalendars(mockMessage, mockDispatch, mockAccumulators)
}
// Dispatch called once because of leading edge
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(1);
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(1)
// Trailing edge
jest.advanceTimersByTime(500);
jest.advanceTimersByTime(500)
// only one call for the last message + leading edge message
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(2);
});
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(2)
})
it("debounces calendar updates during message storm with multiple updates", () => {
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 } },
{ '/calendars/cal/A': { syncToken: 'ldskfjsld' + i } },
mockDispatch,
mockAccumulators
);
)
else if (i % 3 === 1)
updateCalendars(
{ "/calendars/cal/B": { syncToken: "ldskfjsld" + i } },
{ '/calendars/cal/B': { syncToken: 'ldskfjsld' + i } },
mockDispatch,
mockAccumulators
);
)
else
updateCalendars(
{ "/calendars/cal/C": { syncToken: "ldskfjsld" + i } },
{ '/calendars/cal/C': { syncToken: 'ldskfjsld' + i } },
mockDispatch,
mockAccumulators
);
)
}
// Dispatch called once because of leading edge
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(1);
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(1)
// Trailing edge
jest.advanceTimersByTime(500);
jest.advanceTimersByTime(500)
// Trailing edge updates once per calendar + the original leading edge
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(4);
});
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(4)
})
it("executes immediately when debounce is disabled", () => {
window.WS_DEBOUNCE_PERIOD_MS = 0;
it('executes immediately when debounce is disabled', () => {
window.WS_DEBOUNCE_PERIOD_MS = 0
updateCalendars(
{ "/calendars/cal1/entry1": { syncToken: "abc" } },
{ '/calendars/cal1/entry1': { syncToken: 'abc' } },
mockDispatch,
mockAccumulators
);
)
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(1);
});
});
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(1)
})
})
@@ -1,6 +1,6 @@
export function setupWebsocket() {
const originalWebSocket = global.WebSocket;
const webSocketInstances: any[] = [];
const originalWebSocket = global.WebSocket
const webSocketInstances: any[] = []
const mockWebSocket = jest.fn().mockImplementation((url: string) => {
const ws = {
url,
@@ -9,27 +9,27 @@ export function setupWebsocket() {
removeEventListener: jest.fn(),
send: jest.fn(),
close: jest.fn(),
_listeners: {} as Record<string, Function[]>,
};
_listeners: {} as Record<string, Function[]>
}
ws.addEventListener.mockImplementation((event, handler) => {
ws._listeners[event] ??= [];
ws._listeners[event].push(handler);
});
ws._listeners[event] ??= []
ws._listeners[event].push(handler)
})
ws.removeEventListener.mockImplementation((event, handler) => {
ws._listeners[event] =
ws._listeners[event]?.filter((h) => h !== handler) ?? [];
});
ws._listeners[event]?.filter(h => h !== handler) ?? []
})
webSocketInstances.push(ws);
return ws;
});
webSocketInstances.push(ws)
return ws
})
global.WebSocket = mockWebSocket as any;
global.WebSocket = mockWebSocket as any
const cleanup = () => {
global.WebSocket = originalWebSocket;
webSocketInstances.length = 0;
};
return { webSocketInstances, mockWebSocket, cleanup };
global.WebSocket = originalWebSocket
webSocketInstances.length = 0
}
return { webSocketInstances, mockWebSocket, cleanup }
}
@@ -1,147 +1,145 @@
import { AppDispatch, RootState, store } from "@/app/store";
import { refreshCalendarWithSyncToken } from "@/features/Calendars/services/refreshCalendar";
import { getDisplayedCalendarRange } from "@/utils/CalendarRangeManager";
import { updateCalendars } from "@/websocket/messaging/updateCalendars";
import { WS_INBOUND_EVENTS } from "@/websocket/protocols";
import { waitFor } from "@testing-library/dom";
import { AppDispatch, RootState, store } from '@/app/store'
import { refreshCalendarWithSyncToken } from '@/features/Calendars/services/refreshCalendar'
import { getDisplayedCalendarRange } from '@/utils/CalendarRangeManager'
import { updateCalendars } from '@/websocket/messaging/updateCalendars'
import { WS_INBOUND_EVENTS } from '@/websocket/protocols'
import { waitFor } from '@testing-library/dom'
jest.mock("@/features/Calendars/services/refreshCalendar");
jest.mock("@/utils/CalendarRangeManager");
jest.mock("@/app/store", () => ({
jest.mock('@/features/Calendars/services/refreshCalendar')
jest.mock('@/utils/CalendarRangeManager')
jest.mock('@/app/store', () => ({
store: {
getState: jest.fn(),
},
}));
getState: jest.fn()
}
}))
describe("updateCalendars", () => {
let mockDispatch: jest.Mock;
describe('updateCalendars', () => {
let mockDispatch: jest.Mock
const mockRange = {
start: new Date("2025-01-15T10:00:00Z"),
end: new Date("2025-01-16T10:00:00Z"),
};
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 },
'cal1/entry1': { id: 'cal1/entry1', name: 'Calendar 1', syncToken: 1 },
'cal2/entry2': { id: 'cal2/entry2', name: 'Calendar 2', syncToken: 1 }
},
templist: {},
},
} as unknown as RootState;
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: 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: jest.fn(),
};
debouncedUpdateFn: jest.fn()
}
beforeEach(() => {
jest.clearAllMocks();
mockDispatch = jest.fn();
(getDisplayedCalendarRange as jest.Mock).mockReturnValue(mockRange);
(store.getState as jest.Mock).mockReturnValue(mockState);
mockAccumulators.calendarsToRefresh = new Map<string, any>();
mockAccumulators.calendarsToHide = new Set();
mockAccumulators.currentDebouncePeriod = 0;
mockAccumulators.shouldRefreshCalendarListRef.current = false;
mockAccumulators.debouncedUpdateFn = jest.fn();
});
jest.clearAllMocks()
mockDispatch = jest.fn()
;(getDisplayedCalendarRange as jest.Mock).mockReturnValue(mockRange)
;(store.getState as jest.Mock).mockReturnValue(mockState)
mockAccumulators.calendarsToRefresh = new Map<string, any>()
mockAccumulators.calendarsToHide = new Set()
mockAccumulators.currentDebouncePeriod = 0
mockAccumulators.shouldRefreshCalendarListRef.current = false
mockAccumulators.debouncedUpdateFn = jest.fn()
})
it("should not dispatch for non-object messages", () => {
updateCalendars(null, mockDispatch, mockAccumulators);
updateCalendars("string", mockDispatch, mockAccumulators);
updateCalendars(123, mockDispatch, mockAccumulators);
it('should not dispatch for non-object messages', () => {
updateCalendars(null, mockDispatch, mockAccumulators)
updateCalendars('string', mockDispatch, mockAccumulators)
updateCalendars(123, mockDispatch, mockAccumulators)
expect(refreshCalendarWithSyncToken).not.toHaveBeenCalled();
});
expect(refreshCalendarWithSyncToken).not.toHaveBeenCalled()
})
it("should dispatch for registered calendars", async () => {
it('should dispatch for registered calendars', async () => {
const message = {
[WS_INBOUND_EVENTS.CLIENT_REGISTERED]: [
"/calendars/cal1/entry1",
"/calendars/cal2/entry2",
],
};
'/calendars/cal1/entry1',
'/calendars/cal2/entry2'
]
}
updateCalendars(message, mockDispatch, mockAccumulators);
updateCalendars(message, mockDispatch, mockAccumulators)
await waitFor(() =>
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(2)
);
});
)
})
it("should dispatch for calendar path updates", async () => {
it('should dispatch for calendar path updates', async () => {
const message = {
"/calendars/cal1/entry1": { updated: true },
};
'/calendars/cal1/entry1': { updated: true }
}
updateCalendars(message, mockDispatch, mockAccumulators);
await waitFor(() =>
expect(refreshCalendarWithSyncToken).toHaveBeenCalled()
);
updateCalendars(message, mockDispatch, mockAccumulators)
await waitFor(() => expect(refreshCalendarWithSyncToken).toHaveBeenCalled())
expect(refreshCalendarWithSyncToken).toHaveBeenCalledWith({
calendar: mockState.calendars.list["cal1/entry1"],
calendar: mockState.calendars.list['cal1/entry1'],
calType: undefined,
calendarRange: mockRange,
});
});
calendarRange: mockRange
})
})
it("should use displayed calendar range", async () => {
it('should use displayed calendar range', async () => {
const message = {
"/calendars/cal1/entry1": {},
};
'/calendars/cal1/entry1': {}
}
updateCalendars(message, mockDispatch, mockAccumulators);
await waitFor(() => expect(getDisplayedCalendarRange).toHaveBeenCalled());
});
updateCalendars(message, mockDispatch, mockAccumulators)
await waitFor(() => expect(getDisplayedCalendarRange).toHaveBeenCalled())
})
it("should handle temp calendars", async () => {
it('should handle temp calendars', async () => {
const stateWithTemp = {
calendars: {
list: {},
templist: {
"temp1/entry1": {
id: "temp1/entry1",
name: "Temp Calendar",
syncToken: 1,
},
},
},
};
'temp1/entry1': {
id: 'temp1/entry1',
name: 'Temp Calendar',
syncToken: 1
}
}
}
}
(store.getState as jest.Mock).mockReturnValue(stateWithTemp);
;(store.getState as jest.Mock).mockReturnValue(stateWithTemp)
const message = {
"/calendars/temp1/entry1": {},
};
'/calendars/temp1/entry1': {}
}
updateCalendars(message, mockDispatch, mockAccumulators);
updateCalendars(message, mockDispatch, mockAccumulators)
await waitFor(() =>
expect(refreshCalendarWithSyncToken).toHaveBeenCalledWith({
calendar: stateWithTemp.calendars.templist["temp1/entry1"],
calType: "temp",
calendarRange: mockRange,
calendar: stateWithTemp.calendars.templist['temp1/entry1'],
calType: 'temp',
calendarRange: mockRange
})
);
});
)
})
it("should handle invalid calendar paths gracefully", () => {
it('should handle invalid calendar paths gracefully', () => {
const message = {
"/invalid/path": {},
"not-a-path": {},
};
'/invalid/path': {},
'not-a-path': {}
}
updateCalendars(message, mockDispatch, mockAccumulators);
updateCalendars(message, mockDispatch, mockAccumulators)
expect(refreshCalendarWithSyncToken).not.toHaveBeenCalled();
});
});
expect(refreshCalendarWithSyncToken).not.toHaveBeenCalled()
})
})
@@ -1,76 +1,76 @@
import { parseMessage } from "@/websocket/messaging/parseMessage";
import { WS_INBOUND_EVENTS } from "@/websocket/protocols";
import { parseMessage } from '@/websocket/messaging/parseMessage'
import { WS_INBOUND_EVENTS } from '@/websocket/protocols'
describe("parseMessage", () => {
it("should return empty set for non-object messages", () => {
const result1 = parseMessage(null);
const result2 = parseMessage("string");
const result3 = parseMessage(123);
describe('parseMessage', () => {
it('should return empty set for non-object messages', () => {
const result1 = parseMessage(null)
const result2 = parseMessage('string')
const result3 = parseMessage(123)
expect(result1.calendarsToRefresh).toEqual(new Set<string>());
expect(result2.calendarsToRefresh).toEqual(new Set<string>());
expect(result3.calendarsToRefresh).toEqual(new Set<string>());
});
expect(result1.calendarsToRefresh).toEqual(new Set<string>())
expect(result2.calendarsToRefresh).toEqual(new Set<string>())
expect(result3.calendarsToRefresh).toEqual(new Set<string>())
})
it("should handle registered event", () => {
it('should handle registered event', () => {
const message = {
[WS_INBOUND_EVENTS.CLIENT_REGISTERED]: [
"/calendars/cal1/entry1",
"/calendars/cal2/entry2",
],
};
'/calendars/cal1/entry1',
'/calendars/cal2/entry2'
]
}
const result = parseMessage(message);
const result = parseMessage(message)
expect(result.calendarsToRefresh).toContain("/calendars/cal1/entry1");
expect(result.calendarsToRefresh).toContain("/calendars/cal2/entry2");
expect(result.calendarsToRefresh.size).toBe(2);
});
expect(result.calendarsToRefresh).toContain('/calendars/cal1/entry1')
expect(result.calendarsToRefresh).toContain('/calendars/cal2/entry2')
expect(result.calendarsToRefresh.size).toBe(2)
})
it("should handle unregistered event", () => {
it('should handle unregistered event', () => {
const message = {
[WS_INBOUND_EVENTS.CLIENT_UNREGISTERED]: ["/calendars/cal1/entry1"],
};
[WS_INBOUND_EVENTS.CLIENT_UNREGISTERED]: ['/calendars/cal1/entry1']
}
const result = parseMessage(message);
const result = parseMessage(message)
expect(result.calendarsToHide).toContain("/calendars/cal1/entry1");
});
expect(result.calendarsToHide).toContain('/calendars/cal1/entry1')
})
it("should handle calendar path updates", () => {
it('should handle calendar path updates', () => {
const message = {
"/calendars/cal1/entry1": { updated: true },
};
'/calendars/cal1/entry1': { updated: true }
}
const result = parseMessage(message);
const result = parseMessage(message)
expect(result.calendarsToRefresh).toContain("/calendars/cal1/entry1");
expect(result.calendarsToRefresh.size).toBe(1);
});
expect(result.calendarsToRefresh).toContain('/calendars/cal1/entry1')
expect(result.calendarsToRefresh.size).toBe(1)
})
it("should parse multiple calendar paths", () => {
it('should parse multiple calendar paths', () => {
const message = {
"/calendars/cal1/entry1": {},
"/calendars/cal2/entry2": {},
};
'/calendars/cal1/entry1': {},
'/calendars/cal2/entry2': {}
}
const result = parseMessage(message);
const result = parseMessage(message)
expect(result.calendarsToRefresh.size).toBe(2);
expect(result.calendarsToRefresh).toContain("/calendars/cal1/entry1");
expect(result.calendarsToRefresh).toContain("/calendars/cal2/entry2");
});
expect(result.calendarsToRefresh.size).toBe(2)
expect(result.calendarsToRefresh).toContain('/calendars/cal1/entry1')
expect(result.calendarsToRefresh).toContain('/calendars/cal2/entry2')
})
it("should handle multiple event types in single message", () => {
it('should handle multiple event types in single message', () => {
const message = {
[WS_INBOUND_EVENTS.CLIENT_REGISTERED]: ["/calendars/cal1/entry1"],
[WS_INBOUND_EVENTS.CLIENT_UNREGISTERED]: ["/calendars/cal2/entry2"],
"/calendars/cal1/entry1": {},
};
[WS_INBOUND_EVENTS.CLIENT_REGISTERED]: ['/calendars/cal1/entry1'],
[WS_INBOUND_EVENTS.CLIENT_UNREGISTERED]: ['/calendars/cal2/entry2'],
'/calendars/cal1/entry1': {}
}
const result = parseMessage(message);
const result = parseMessage(message)
expect(result.calendarsToRefresh.size).toBe(1);
expect(result.calendarsToRefresh).toContain("/calendars/cal1/entry1");
});
});
expect(result.calendarsToRefresh.size).toBe(1)
expect(result.calendarsToRefresh).toContain('/calendars/cal1/entry1')
})
})
@@ -1,57 +1,57 @@
import { registerToCalendars } from "@/websocket/operations/registerToCalendars";
import { registerToCalendars } from '@/websocket/operations/registerToCalendars'
describe("registerToCalendars", () => {
let mockSocket: any;
describe('registerToCalendars', () => {
let mockSocket: any
beforeEach(() => {
mockSocket = {
readyState: WebSocket.OPEN,
send: jest.fn(),
};
});
send: jest.fn()
}
})
it("should send registration message with calendar URIs", () => {
const calendarURIs = ["/calendars/cal1", "/calendars/cal2"];
it('should send registration message with calendar URIs', () => {
const calendarURIs = ['/calendars/cal1', '/calendars/cal2']
registerToCalendars(mockSocket, calendarURIs);
registerToCalendars(mockSocket, calendarURIs)
expect(mockSocket.send).toHaveBeenCalledWith(
JSON.stringify({
register: calendarURIs,
register: calendarURIs
})
);
});
)
})
it("should throw error if socket is not open", () => {
mockSocket.readyState = WebSocket.CONNECTING;
const calendarURIs = ["/calendars/cal1"];
it('should throw error if socket is not open', () => {
mockSocket.readyState = WebSocket.CONNECTING
const calendarURIs = ['/calendars/cal1']
expect(() => registerToCalendars(mockSocket, calendarURIs)).toThrow(
"Cannot register: WebSocket is not open"
);
});
'Cannot register: WebSocket is not open'
)
})
it("should handle empty calendar list", () => {
registerToCalendars(mockSocket, []);
it('should handle empty calendar list', () => {
registerToCalendars(mockSocket, [])
expect(mockSocket.send).toHaveBeenCalledWith(
JSON.stringify({
register: [],
register: []
})
);
});
)
})
it("should log registration", () => {
const consoleInfoSpy = jest.spyOn(console, "info").mockImplementation();
const calendarURIs = ["/calendars/cal1", "/calendars/cal2"];
it('should log registration', () => {
const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation()
const calendarURIs = ['/calendars/cal1', '/calendars/cal2']
registerToCalendars(mockSocket, calendarURIs);
registerToCalendars(mockSocket, calendarURIs)
expect(consoleInfoSpy).toHaveBeenCalledWith(
"Registered to calendars",
'Registered to calendars',
calendarURIs
);
)
consoleInfoSpy.mockRestore();
});
});
consoleInfoSpy.mockRestore()
})
})
@@ -1,58 +1,58 @@
import { unregisterToCalendars } from "@/websocket/operations/unregisterToCalendars";
import { unregisterToCalendars } from '@/websocket/operations/unregisterToCalendars'
describe("unregisterToCalendars", () => {
let mockSocket: any;
describe('unregisterToCalendars', () => {
let mockSocket: any
beforeEach(() => {
mockSocket = {
readyState: WebSocket.OPEN,
send: jest.fn(),
};
jest.clearAllMocks();
});
send: jest.fn()
}
jest.clearAllMocks()
})
it("should send unregistration message with calendar URIs", () => {
const calendarURIs = ["/calendars/cal1", "/calendars/cal2"];
it('should send unregistration message with calendar URIs', () => {
const calendarURIs = ['/calendars/cal1', '/calendars/cal2']
unregisterToCalendars(mockSocket, calendarURIs);
unregisterToCalendars(mockSocket, calendarURIs)
expect(mockSocket.send).toHaveBeenCalledWith(
JSON.stringify({
unregister: calendarURIs,
unregister: calendarURIs
})
);
});
)
})
it("should throw error if socket is not open", () => {
mockSocket.readyState = WebSocket.CONNECTING;
const calendarURIs = ["/calendars/cal1"];
it('should throw error if socket is not open', () => {
mockSocket.readyState = WebSocket.CONNECTING
const calendarURIs = ['/calendars/cal1']
expect(() => unregisterToCalendars(mockSocket, calendarURIs)).toThrow(
"Cannot unregister: WebSocket is not open"
);
});
'Cannot unregister: WebSocket is not open'
)
})
it("should handle empty calendar list", () => {
unregisterToCalendars(mockSocket, []);
it('should handle empty calendar list', () => {
unregisterToCalendars(mockSocket, [])
expect(mockSocket.send).toHaveBeenCalledWith(
JSON.stringify({
unregister: [],
unregister: []
})
);
});
)
})
it("should log unregistration", () => {
const consoleInfoSpy = jest.spyOn(console, "info").mockImplementation();
const calendarURIs = ["/calendars/cal1", "/calendars/cal2"];
it('should log unregistration', () => {
const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation()
const calendarURIs = ['/calendars/cal1', '/calendars/cal2']
unregisterToCalendars(mockSocket, calendarURIs);
unregisterToCalendars(mockSocket, calendarURIs)
expect(consoleInfoSpy).toHaveBeenCalledWith(
"Unregistered to calendars",
'Unregistered to calendars',
calendarURIs
);
)
consoleInfoSpy.mockRestore();
});
});
consoleInfoSpy.mockRestore()
})
})