import { fireEvent, screen, waitFor } from "@testing-library/react"; import { describe, expect, mock, test } from "bun:test"; import { createElement } from "react"; import type { Conversation, Model } from "../../../src/shared/api"; import { ChatPanel } from "../../../src/web/consoles/workbench/components/chat/ChatPanel"; import { installFetchMock, jsonResponse, renderWithProviders } from "../test-utils"; const PROJECT_ID = "proj-1"; const TEXT_MODEL: Model = { capabilities: ["text"], contextLength: null, createdAt: "2024-01-01T00:00:00.000Z", id: "model-1", maxOutputTokens: null, modelId: "gpt-4o", name: "GPT-4o", providerId: "pv1", updatedAt: "2024-01-01T00:00:00.000Z", }; const CONVERSATION: Conversation = { createdAt: "2024-01-01T00:00:00.000Z", id: "conv-1", modelId: "model-1", projectId: PROJECT_ID, title: "新会话", updatedAt: "2024-01-01T00:00:00.000Z", }; const noop = () => { return undefined; }; void mock.module("@ai-sdk/react", () => ({ useChat: () => ({ messages: [], regenerate: noop, sendMessage: noop, setMessages: (msgs: unknown) => msgs, status: "ready", stop: noop, }), })); void mock.module("ai", () => ({ DefaultChatTransport: function () { return undefined; }, })); function getSendButton() { return screen.getByRole("button", { name: /发.*送/ }); } function setupFetchMock() { return installFetchMock((call) => { if (call.url.includes("/models")) { return jsonResponse({ items: [TEXT_MODEL], total: 1 }); } if (call.url.endsWith("/conversations") && call.method === "POST") { return jsonResponse({ conversation: { ...CONVERSATION, id: "conv-new" } }, { status: 201 }); } if (/\/conversations\/conv-1$/.exec(call.url)) { return jsonResponse({ conversation: CONVERSATION }); } if (call.url.includes("/messages")) { return jsonResponse({ items: [], total: 0 }); } if (/\/conversations$/.exec(call.url) && call.method === "GET") { return jsonResponse({ items: [], total: 0 }); } return jsonResponse({ error: "not found" }, { status: 404 }); }); } describe("ChatPanel", () => { describe("欢迎页", () => { test("无会话时显示欢迎页和输入框", () => { setupFetchMock(); renderWithProviders( createElement(ChatPanel, { conversationId: null, onConversationCreated: noop, projectId: PROJECT_ID, }), ); expect(screen.getByText("你好,我是阿福")).toBeTruthy(); expect(screen.getByText("有什么我可以帮助你的吗?")).toBeTruthy(); expect(screen.getByPlaceholderText("输入消息...")).toBeTruthy(); expect(getSendButton()).toBeTruthy(); }); }); describe("自动创建会话", () => { test("输入并发送后自动创建会话并通知父组件", async () => { const calls = setupFetchMock(); const onCreated = mock<(id: string) => void>(() => undefined); renderWithProviders( createElement(ChatPanel, { conversationId: null, onConversationCreated: onCreated, projectId: PROJECT_ID, }), ); const input = screen.getByPlaceholderText("输入消息..."); fireEvent.change(input, { target: { value: "你好" } }); fireEvent.click(getSendButton()); await waitFor(() => { expect(onCreated).toHaveBeenCalledWith("conv-new"); }); const createCall = calls.find((c) => c.url.endsWith("/conversations") && c.method === "POST"); expect(createCall).toBeTruthy(); }); test("创建会话失败时恢复输入文本", async () => { installFetchMock((call) => { if (call.url.includes("/models")) { return jsonResponse({ items: [TEXT_MODEL], total: 1 }); } if (call.url.endsWith("/conversations") && call.method === "POST") { return jsonResponse({ error: "服务器错误" }, { status: 500 }); } return jsonResponse({ error: "not found" }, { status: 404 }); }); renderWithProviders( createElement(ChatPanel, { conversationId: null, onConversationCreated: noop, projectId: PROJECT_ID, }), ); const input = screen.getByPlaceholderText("输入消息..."); fireEvent.change(input, { target: { value: "测试输入" } }); fireEvent.click(getSendButton()); await waitFor(() => { expect((input as HTMLTextAreaElement).value).toBe("测试输入"); }); }); }); describe("聊天面板", () => { test("选中会话时显示消息列表区域", () => { setupFetchMock(); renderWithProviders( createElement(ChatPanel, { conversationId: "conv-1", onConversationCreated: noop, projectId: PROJECT_ID, }), ); expect(screen.queryByText("你好,我是阿福")).toBeNull(); expect(screen.getByPlaceholderText("输入消息...")).toBeTruthy(); }); }); });