474 websocket add a snack bar to notify the user on websocket status changes (#483)
* [#474] added snackbar for websocket status * [#474 & #447] added ping to complete browser online/offline status * [#447] ping timeout as an env var
This commit is contained in:
@@ -3,13 +3,40 @@ import { createWebSocketConnection } from "@/websocket/connection/createConnecti
|
||||
import { registerToCalendars } from "@/websocket/operations/registerToCalendars";
|
||||
import { unregisterToCalendars } from "@/websocket/operations/unregisterToCalendars";
|
||||
import { WebSocketGate } from "@/websocket/WebSocketGate";
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
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;
|
||||
@@ -30,9 +57,11 @@ describe("WebSocketGate", () => {
|
||||
const createMockSocket = (readyState = WebSocket.OPEN) => ({
|
||||
readyState,
|
||||
close: jest.fn(),
|
||||
send: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
cleanup: jest.fn(),
|
||||
onmessage: null,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -44,6 +73,7 @@ describe("WebSocketGate", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
jest.clearAllMocks();
|
||||
jest.clearAllTimers();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
@@ -61,11 +91,7 @@ describe("WebSocketGate", () => {
|
||||
it("should not create connection when user is not authenticated", () => {
|
||||
const unauthStore = createMockStore(null, null);
|
||||
|
||||
render(
|
||||
<Provider store={unauthStore}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={unauthStore} />);
|
||||
|
||||
expect(createWebSocketConnection).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -73,11 +99,7 @@ describe("WebSocketGate", () => {
|
||||
it("should create connection when user is authenticated", async () => {
|
||||
(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createWebSocketConnection).toHaveBeenCalledTimes(1);
|
||||
@@ -87,22 +109,14 @@ describe("WebSocketGate", () => {
|
||||
it("should close existing socket when user logs out", async () => {
|
||||
(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket);
|
||||
|
||||
const { rerender } = render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
const { rerender } = render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createWebSocketConnection).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const unauthStore = createMockStore(null, null);
|
||||
rerender(
|
||||
<Provider store={unauthStore}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
rerender(<TestWrapper store={unauthStore} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSocket.close).toHaveBeenCalled();
|
||||
@@ -114,11 +128,7 @@ describe("WebSocketGate", () => {
|
||||
it("should create connection with callbacks", async () => {
|
||||
(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createWebSocketConnection).toHaveBeenCalledWith(
|
||||
@@ -141,11 +151,7 @@ describe("WebSocketGate", () => {
|
||||
}
|
||||
);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createWebSocketConnection).toHaveBeenCalled();
|
||||
@@ -172,11 +178,7 @@ describe("WebSocketGate", () => {
|
||||
new Error("Connection failed")
|
||||
);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(consoleError).toHaveBeenCalledWith(
|
||||
@@ -191,11 +193,7 @@ describe("WebSocketGate", () => {
|
||||
it("should close socket on component unmount", async () => {
|
||||
(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket);
|
||||
|
||||
const { unmount } = render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
const { unmount } = render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createWebSocketConnection).toHaveBeenCalled();
|
||||
@@ -230,11 +228,7 @@ describe("WebSocketGate", () => {
|
||||
}
|
||||
);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createWebSocketConnection).toHaveBeenCalledTimes(1);
|
||||
@@ -275,11 +269,7 @@ describe("WebSocketGate", () => {
|
||||
}
|
||||
);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createWebSocketConnection).toHaveBeenCalledTimes(1);
|
||||
@@ -311,11 +301,7 @@ describe("WebSocketGate", () => {
|
||||
}
|
||||
);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createWebSocketConnection).toHaveBeenCalledTimes(1);
|
||||
@@ -346,11 +332,7 @@ describe("WebSocketGate", () => {
|
||||
}
|
||||
);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createWebSocketConnection).toHaveBeenCalledTimes(1);
|
||||
@@ -362,19 +344,11 @@ describe("WebSocketGate", () => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
// Second failure
|
||||
await act(async () => {
|
||||
onCloseCallback?.(new CloseEvent("close", { code: 1006 }));
|
||||
jest.advanceTimersByTime(2000);
|
||||
});
|
||||
|
||||
// Success - should log and reset counter
|
||||
await waitFor(() => {
|
||||
expect(consoleLog).toHaveBeenCalledWith(
|
||||
"WebSocket connected successfully"
|
||||
);
|
||||
});
|
||||
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 }));
|
||||
@@ -400,11 +374,7 @@ describe("WebSocketGate", () => {
|
||||
}
|
||||
);
|
||||
|
||||
const { rerender } = render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
const { rerender } = render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createWebSocketConnection).toHaveBeenCalledTimes(1);
|
||||
@@ -417,11 +387,7 @@ describe("WebSocketGate", () => {
|
||||
|
||||
// Lose authentication before timeout fires
|
||||
const unauthStore = createMockStore(null, null);
|
||||
rerender(
|
||||
<Provider store={unauthStore}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
rerender(<TestWrapper store={unauthStore} />);
|
||||
|
||||
// Advance timer
|
||||
await act(async () => {
|
||||
@@ -444,11 +410,7 @@ describe("WebSocketGate", () => {
|
||||
}
|
||||
);
|
||||
|
||||
const { unmount } = render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
const { unmount } = render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createWebSocketConnection).toHaveBeenCalledTimes(1);
|
||||
@@ -487,11 +449,7 @@ describe("WebSocketGate", () => {
|
||||
}
|
||||
);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(registerToCalendars).toHaveBeenCalledWith(mockSocket, [
|
||||
@@ -540,11 +498,7 @@ describe("WebSocketGate", () => {
|
||||
}
|
||||
);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createWebSocketConnection).toHaveBeenCalledTimes(1);
|
||||
@@ -577,11 +531,7 @@ describe("WebSocketGate", () => {
|
||||
}
|
||||
);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createWebSocketConnection).toHaveBeenCalledTimes(1);
|
||||
@@ -609,11 +559,7 @@ describe("WebSocketGate", () => {
|
||||
it("should not reconnect when online event fires if already connected", async () => {
|
||||
(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createWebSocketConnection).toHaveBeenCalledTimes(1);
|
||||
@@ -640,11 +586,7 @@ describe("WebSocketGate", () => {
|
||||
}
|
||||
);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createWebSocketConnection).toHaveBeenCalledTimes(1);
|
||||
@@ -693,11 +635,7 @@ describe("WebSocketGate", () => {
|
||||
connectingSocket
|
||||
);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createWebSocketConnection).toHaveBeenCalled();
|
||||
@@ -713,11 +651,7 @@ describe("WebSocketGate", () => {
|
||||
);
|
||||
(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(registerToCalendars).toHaveBeenCalledWith(mockSocket, [
|
||||
@@ -731,11 +665,7 @@ describe("WebSocketGate", () => {
|
||||
localStorage.setItem("selectedCalendars", JSON.stringify(["cal1"]));
|
||||
(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(registerToCalendars).toHaveBeenCalledWith(mockSocket, [
|
||||
@@ -764,11 +694,7 @@ describe("WebSocketGate", () => {
|
||||
);
|
||||
(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(registerToCalendars).toHaveBeenCalled();
|
||||
@@ -795,11 +721,7 @@ describe("WebSocketGate", () => {
|
||||
);
|
||||
(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(registerToCalendars).toHaveBeenCalled();
|
||||
@@ -829,11 +751,7 @@ describe("WebSocketGate", () => {
|
||||
throw new Error("Registration failed");
|
||||
});
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(consoleError).toHaveBeenCalledWith(
|
||||
@@ -868,11 +786,7 @@ describe("WebSocketGate", () => {
|
||||
localStorage.setItem("selectedCalendars", JSON.stringify([]));
|
||||
(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createWebSocketConnection).toHaveBeenCalled();
|
||||
@@ -888,11 +802,7 @@ describe("WebSocketGate", () => {
|
||||
localStorage.setItem("selectedCalendars", JSON.stringify(["cal1"]));
|
||||
(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(registerToCalendars).toHaveBeenCalledTimes(1);
|
||||
@@ -913,11 +823,7 @@ describe("WebSocketGate", () => {
|
||||
localStorage.setItem("selectedCalendars", JSON.stringify(["cal1"]));
|
||||
(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(registerToCalendars).toHaveBeenCalled();
|
||||
@@ -945,11 +851,7 @@ describe("WebSocketGate", () => {
|
||||
);
|
||||
(createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(registerToCalendars).toHaveBeenCalled();
|
||||
@@ -979,11 +881,7 @@ describe("WebSocketGate", () => {
|
||||
localStorage.setItem("selectedCalendars", JSON.stringify(["cal1"]));
|
||||
(createWebSocketConnection as jest.Mock).mockResolvedValue(closedSocket);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<WebSocketGate />
|
||||
</Provider>
|
||||
);
|
||||
render(<TestWrapper store={store} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createWebSocketConnection).toHaveBeenCalled();
|
||||
@@ -1004,4 +902,107 @@ describe("WebSocketGate", () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user