Files
kandev/apps/web/lib/ws/use-websocket.test.tsx
T
Clément Fiere 15c0c35df6 feat(auth): resolve session hostnames (#2741)
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
2026-09-04 13:06:33 +00:00

117 lines
3.4 KiB
TypeScript

import { act, renderHook } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { ConnectionStatus } from "@/lib/types/connection";
import { createAppStore } from "@/lib/state/store";
import { useWebSocket } from "./use-websocket";
const mocks = vi.hoisted(() => ({
clients: [] as Array<{
disconnect: ReturnType<typeof vi.fn>;
subscribeUser: ReturnType<typeof vi.fn>;
}>,
onStatusChange: null as ((status: ConnectionStatus) => void) | null,
}));
vi.mock("@/lib/ws/client", () => ({
WebSocketClient: class {
disconnect = vi.fn();
connect = vi.fn();
subscribeUser = vi.fn();
on = vi.fn(() => vi.fn());
constructor(_url: string, onStatusChange: (status: ConnectionStatus) => void) {
mocks.onStatusChange = onStatusChange;
mocks.clients.push(this);
}
},
}));
vi.mock("@/lib/ws/router", () => ({
registerWsHandlers: vi.fn(() => ({ handlers: {}, dispose: vi.fn() })),
}));
vi.mock("@/lib/ws/connection", () => ({ setWebSocketClient: vi.fn() }));
vi.mock("@/lib/debug/log", () => ({ createDebugLogger: () => vi.fn() }));
describe("useWebSocket", () => {
afterEach(() => {
mocks.clients = [];
mocks.onStatusChange = null;
vi.useRealTimers();
});
it("reflects a sustained WebSocket outage in the UI store and clears on reconnect", () => {
vi.useFakeTimers();
const store = createAppStore();
const { unmount } = renderHook(() => useWebSocket(store, "ws://kandev.test/ws"));
act(() => {
mocks.onStatusChange?.("reconnecting");
vi.advanceTimersByTime(3_000);
});
expect(store.getState().connection.issueSeverity).toBe("unstable");
act(() => mocks.onStatusChange?.("connected"));
expect(store.getState().connection.issueSeverity).toBe("none");
unmount();
});
it("clears an active outage when replacing the WebSocket URL", () => {
vi.useFakeTimers();
const store = createAppStore();
const { rerender, unmount } = renderHook(
({ url }: { url: string }) => useWebSocket(store, url),
{ initialProps: { url: "ws://kandev.test/first" } },
);
act(() => {
mocks.onStatusChange?.("reconnecting");
vi.advanceTimersByTime(3_000);
});
expect(store.getState().connection.issueSeverity).toBe("unstable");
rerender({ url: "ws://kandev.test/replacement" });
expect(store.getState().connection.issueSeverity).toBe("none");
unmount();
});
it("reconnects when the authenticated identity changes", () => {
const store = createAppStore();
const { unmount } = renderHook(() => useWebSocket(store, "ws://kandev.test/ws"));
act(() => {
store.getState().setAuthState({
mode: "enabled",
authenticated: true,
user: {
id: "user-1",
email: "user@example.test",
display_name: "User",
role: "member",
status: "active",
},
});
});
expect(mocks.clients).toHaveLength(2);
expect(mocks.clients[0].disconnect).toHaveBeenCalledOnce();
unmount();
});
it("registers the user subscription once per client", () => {
const store = createAppStore();
const { unmount } = renderHook(() => useWebSocket(store, "ws://kandev.test/ws"));
const client = mocks.clients[0];
act(() => {
mocks.onStatusChange?.("connected");
mocks.onStatusChange?.("connected");
});
expect(client.subscribeUser).toHaveBeenCalledOnce();
unmount();
});
});