/** * @file Chat.test.tsx * @description Tests for Chat component error handling in imperative handle methods */ import { createRef } from "react"; import { render } from "@testing-library/react"; import { describe, it, expect, beforeEach, vi, afterEach, type MockedFunction } from "vitest"; import { Chat, type ChatRef } from "./Chat"; import * as useChatModule from "@/hooks/useChat"; import * as useWebSocketModule from "@/hooks/useWebSocket"; import * as authModule from "@/lib/auth/auth-context"; // Mock scrollIntoView (not available in JSDOM) Element.prototype.scrollIntoView = vi.fn(); // Mock dependencies vi.mock("@/lib/auth/auth-context", () => ({ useAuth: vi.fn(), })); vi.mock("@/hooks/useChat", () => ({ useChat: vi.fn(), })); vi.mock("@/hooks/useWebSocket", () => ({ useWebSocket: vi.fn(), })); vi.mock("./MessageList", () => ({ MessageList: (): React.ReactElement =>
, })); vi.mock("./ChatInput", () => ({ ChatInput: ({ onSend, }: { onSend: (content: string) => Promise; disabled: boolean; inputRef: React.RefObject; }): React.ReactElement => ( ), })); const mockUseAuth = authModule.useAuth as MockedFunction; const mockUseChat = useChatModule.useChat as MockedFunction; const mockUseWebSocket = useWebSocketModule.useWebSocket as MockedFunction< typeof useWebSocketModule.useWebSocket >; function createMockUseChatReturn( overrides: Partial = {} ): useChatModule.UseChatReturn { return { messages: [ { id: "welcome", role: "assistant", content: "Hello!", createdAt: new Date().toISOString(), }, ], isLoading: false, error: null, conversationId: null, conversationTitle: null, sendMessage: vi.fn().mockResolvedValue(undefined), loadConversation: vi.fn().mockResolvedValue(undefined), startNewConversation: vi.fn(), setMessages: vi.fn(), clearError: vi.fn(), ...overrides, }; } describe("Chat", () => { let consoleSpy: ReturnType; beforeEach(() => { vi.clearAllMocks(); consoleSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); mockUseAuth.mockReturnValue({ user: { id: "user-1", name: "Test User", email: "test@test.com" }, isAuthenticated: true, isLoading: false, login: vi.fn(), logout: vi.fn(), } as unknown as ReturnType); mockUseWebSocket.mockReturnValue({ isConnected: true, socket: null, connectionError: null, }); }); afterEach(() => { consoleSpy.mockRestore(); }); describe("loadConversation via ref", () => { it("should delegate to useChat.loadConversation", async () => { const mockLoadConversation = vi.fn().mockResolvedValue(undefined); mockUseChat.mockReturnValue( createMockUseChatReturn({ loadConversation: mockLoadConversation }) ); const ref = createRef(); render(); await ref.current?.loadConversation("conv-123"); expect(mockLoadConversation).toHaveBeenCalledWith("conv-123"); }); it("should not re-throw when useChat.loadConversation handles errors internally", async () => { // useChat.loadConversation handles errors internally (sets error state, logs, calls onError) // and does NOT re-throw, so the imperative handle should resolve cleanly const mockLoadConversation = vi.fn().mockResolvedValue(undefined); mockUseChat.mockReturnValue( createMockUseChatReturn({ loadConversation: mockLoadConversation }) ); const ref = createRef(); render(); // Should resolve without throwing await expect(ref.current?.loadConversation("conv-123")).resolves.toBeUndefined(); }); }); describe("sendMessage delegation", () => { it("should delegate to useChat.sendMessage", async () => { const mockSendMessage = vi.fn().mockResolvedValue(undefined); mockUseChat.mockReturnValue(createMockUseChatReturn({ sendMessage: mockSendMessage })); const ref = createRef(); const { getByTestId } = render(); const sendButton = getByTestId("chat-input"); sendButton.click(); await vi.waitFor(() => { expect(mockSendMessage).toHaveBeenCalledWith("test message"); }); }); }); });