Files
stack/apps/web/src/hooks/useWebSocket.test.tsx
Jason Woltje f0704db560
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
fix: Resolve web package lint and typecheck errors
Fixes ESLint and TypeScript errors in web package to pass CI checks:

- Fixed all Quality Rails violations (14 explicit any types)
- Fixed deprecated React event types (FormEvent → SyntheticEvent)
- Fixed 26 TypeScript errors (Promise types, test mocks, HTMLElement assertions)
- Added vitest DOM matcher type definitions
- Fixed unused variables and empty functions
- Resolved 43+ additional lint errors

Typecheck:  0 errors
Lint: 542 remaining (non-blocking in CI)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-30 21:34:12 -06:00

236 lines
6.7 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderHook, act, waitFor } from "@testing-library/react";
import { useWebSocket } from "./useWebSocket";
import type { Socket } from "socket.io-client";
import { io } from "socket.io-client";
// Mock socket.io-client
vi.mock("socket.io-client");
describe("useWebSocket", (): void => {
let mockSocket: Partial<Socket>;
let eventHandlers: Record<string, (data: unknown) => void>;
beforeEach((): void => {
eventHandlers = {};
mockSocket = {
on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
eventHandlers[event] = handler;
return mockSocket;
}) as unknown as Socket["on"],
off: vi.fn((event?: string) => {
if (event && Object.hasOwn(eventHandlers, event)) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete eventHandlers[event];
}
return mockSocket;
}) as unknown as Socket["off"],
connect: vi.fn(),
disconnect: vi.fn(),
connected: false,
};
(io as unknown as ReturnType<typeof vi.fn>).mockReturnValue(mockSocket);
});
afterEach((): void => {
vi.clearAllMocks();
});
it("should connect to WebSocket server on mount", (): void => {
const workspaceId = "workspace-123";
const token = "auth-token";
renderHook(() => useWebSocket(workspaceId, token));
expect(io).toHaveBeenCalledWith(expect.any(String), {
auth: { token },
query: { workspaceId },
});
});
it("should disconnect on unmount", (): void => {
const { unmount } = renderHook(() => useWebSocket("workspace-123", "token"));
unmount();
expect(mockSocket.disconnect).toHaveBeenCalled();
});
it("should update connection status on connect event", async (): Promise<void> => {
mockSocket.connected = false;
const { result } = renderHook(() => useWebSocket("workspace-123", "token"));
expect(result.current.isConnected).toBe(false);
act(() => {
mockSocket.connected = true;
eventHandlers.connect?.(undefined);
});
await waitFor(() => {
expect(result.current.isConnected).toBe(true);
});
});
it("should update connection status on disconnect event", async (): Promise<void> => {
mockSocket.connected = true;
const { result } = renderHook(() => useWebSocket("workspace-123", "token"));
act(() => {
eventHandlers.connect?.(undefined);
});
await waitFor(() => {
expect(result.current.isConnected).toBe(true);
});
act(() => {
mockSocket.connected = false;
eventHandlers.disconnect?.(undefined);
});
await waitFor(() => {
expect(result.current.isConnected).toBe(false);
});
});
it("should handle task:created events", async (): Promise<void> => {
const onTaskCreated = vi.fn();
renderHook(() => useWebSocket("workspace-123", "token", { onTaskCreated }));
const task = { id: "task-1", title: "New Task" };
act(() => {
eventHandlers["task:created"]?.(task);
});
await waitFor(() => {
expect(onTaskCreated).toHaveBeenCalledWith(task);
});
});
it("should handle task:updated events", async (): Promise<void> => {
const onTaskUpdated = vi.fn();
renderHook(() => useWebSocket("workspace-123", "token", { onTaskUpdated }));
const task = { id: "task-1", title: "Updated Task" };
act(() => {
eventHandlers["task:updated"]?.(task);
});
await waitFor(() => {
expect(onTaskUpdated).toHaveBeenCalledWith(task);
});
});
it("should handle task:deleted events", async (): Promise<void> => {
const onTaskDeleted = vi.fn();
renderHook(() => useWebSocket("workspace-123", "token", { onTaskDeleted }));
const payload = { id: "task-1" };
act(() => {
eventHandlers["task:deleted"]?.(payload);
});
await waitFor(() => {
expect(onTaskDeleted).toHaveBeenCalledWith(payload);
});
});
it("should handle event:created events", async (): Promise<void> => {
const onEventCreated = vi.fn();
renderHook(() => useWebSocket("workspace-123", "token", { onEventCreated }));
const event = { id: "event-1", title: "New Event" };
act(() => {
eventHandlers["event:created"]?.(event);
});
await waitFor(() => {
expect(onEventCreated).toHaveBeenCalledWith(event);
});
});
it("should handle event:updated events", async (): Promise<void> => {
const onEventUpdated = vi.fn();
renderHook(() => useWebSocket("workspace-123", "token", { onEventUpdated }));
const event = { id: "event-1", title: "Updated Event" };
act(() => {
eventHandlers["event:updated"]?.(event);
});
await waitFor(() => {
expect(onEventUpdated).toHaveBeenCalledWith(event);
});
});
it("should handle event:deleted events", async (): Promise<void> => {
const onEventDeleted = vi.fn();
renderHook(() => useWebSocket("workspace-123", "token", { onEventDeleted }));
const payload = { id: "event-1" };
act(() => {
eventHandlers["event:deleted"]?.(payload);
});
await waitFor(() => {
expect(onEventDeleted).toHaveBeenCalledWith(payload);
});
});
it("should handle project:updated events", async (): Promise<void> => {
const onProjectUpdated = vi.fn();
renderHook(() => useWebSocket("workspace-123", "token", { onProjectUpdated }));
const project = { id: "project-1", name: "Updated Project" };
act(() => {
eventHandlers["project:updated"]?.(project);
});
await waitFor(() => {
expect(onProjectUpdated).toHaveBeenCalledWith(project);
});
});
it("should reconnect with new workspace ID", (): void => {
const { rerender } = renderHook(
({ workspaceId }: { workspaceId: string }) => useWebSocket(workspaceId, "token"),
{ initialProps: { workspaceId: "workspace-1" } }
);
expect(io).toHaveBeenCalledTimes(1);
rerender({ workspaceId: "workspace-2" });
expect(mockSocket.disconnect).toHaveBeenCalled();
expect(io).toHaveBeenCalledTimes(2);
});
it("should clean up all event listeners on unmount", (): void => {
const { unmount } = renderHook(() =>
useWebSocket("workspace-123", "token", {
onTaskCreated: vi.fn(),
onTaskUpdated: vi.fn(),
onTaskDeleted: vi.fn(),
})
);
unmount();
expect(mockSocket.off).toHaveBeenCalledWith("connect", expect.any(Function));
expect(mockSocket.off).toHaveBeenCalledWith("disconnect", expect.any(Function));
expect(mockSocket.off).toHaveBeenCalledWith("task:created", expect.any(Function));
expect(mockSocket.off).toHaveBeenCalledWith("task:updated", expect.any(Function));
expect(mockSocket.off).toHaveBeenCalledWith("task:deleted", expect.any(Function));
});
});