/**
* @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");
});
});
});
});