import { fireEvent, screen, waitFor } from "@testing-library/react"; import { describe, expect, test, vi } from "bun:test"; import { createElement } from "react"; import type { Model, Project } from "../../../src/shared/api"; import { ChatPage } from "../../../src/web/features/chat/ChatPage"; import { ProjectContext } from "../../../src/web/shared/hooks/use-current-project"; import { installFetchMock, jsonResponse, renderWithProviders } from "../test-utils"; const PROJECT_ID = "proj-1"; const MOCK_PROJECT: Project = { createdAt: "2026-01-01T00:00:00.000Z", description: "", id: PROJECT_ID, name: "测试项目", status: "active", updatedAt: "2026-01-01T00:00:00.000Z", }; const TEXT_MODEL: Model = { capabilities: ["text"], contextLength: null, createdAt: "2024-01-01T00:00:00.000Z", externalId: "gpt-4o", id: "model-1", maxOutputTokens: null, name: "GPT-4o", providerId: "pv1", updatedAt: "2024-01-01T00:00:00.000Z", }; const CONVERSATION = { createdAt: "2026-06-03T00:00:00.000Z", id: "conv-1", modelId: "model-1", projectId: PROJECT_ID, title: "测试对话", updatedAt: "2026-06-03T00:00:00.000Z", }; function renderChatPage() { return renderWithProviders( createElement(ProjectContext.Provider, { children: createElement(ChatPage), value: MOCK_PROJECT, }), ); } function setupFetchMock() { return installFetchMock((call) => { if (call.url.includes("/models")) { return jsonResponse({ items: [TEXT_MODEL], total: 1 }); } if (call.url.includes("/conversations") && call.method === "GET") { return jsonResponse({ items: [CONVERSATION], page: 1, pageSize: 200, total: 1 }); } if (call.url.endsWith("/conversations") && call.method === "POST") { return jsonResponse({ conversation: { ...CONVERSATION, id: "conv-new", title: "新会话" } }, { status: 201 }); } if (call.method === "DELETE" && call.url.includes("/conversations/")) { return new Response(null, { status: 204 }); } if (call.url.includes("/messages")) { return jsonResponse({ items: [], total: 0 }); } if (/\/conversations\/conv-1$/.exec(call.url)) { return jsonResponse({ conversation: CONVERSATION }); } return jsonResponse({ error: "not found" }, { status: 404 }); }); } void vi.mock("@ai-sdk/react", () => ({ useChat: () => ({ messages: [], regenerate: () => undefined, sendMessage: () => undefined, setMessages: (msgs: unknown) => msgs, status: "ready", stop: () => undefined, }), })); void vi.mock("ai", () => ({ DefaultChatTransport: function () { return undefined; }, })); describe("ChatPage", () => { test("渲染对话侧边栏和欢迎页", async () => { setupFetchMock(); renderChatPage(); await waitFor(() => { expect(screen.getByText("测试对话")).not.toBeNull(); }); expect(screen.getByText("你好,我是阿福")).not.toBeNull(); }); test("点击新对话按钮创建并选中对话", async () => { const calls = setupFetchMock(); renderChatPage(); await waitFor(() => { expect(screen.getByText("新对话")).not.toBeNull(); }); fireEvent.click(screen.getByText("新对话")); await waitFor(() => { const createCall = calls.find((c) => c.url.endsWith("/conversations") && c.method === "POST"); expect(createCall).toBeTruthy(); }); }); test("点击对话切换选中", async () => { setupFetchMock(); renderChatPage(); await waitFor(() => { expect(screen.getByText("测试对话")).not.toBeNull(); }); fireEvent.click(screen.getByText("测试对话")); await waitFor(() => { expect(screen.queryByText("你好,我是阿福")).toBeNull(); }); }); test("删除对话后列表更新", async () => { let deleted = false; installFetchMock((call) => { if (call.method === "DELETE" && call.url.includes("/conversations/conv-1")) { deleted = true; return new Response(null, { status: 204 }); } if (call.url.includes("/models")) { return jsonResponse({ items: [TEXT_MODEL], total: 1 }); } if (call.url.includes("/conversations") && call.method === "GET") { if (deleted) { return jsonResponse({ items: [], page: 1, pageSize: 200, total: 0 }); } return jsonResponse({ items: [CONVERSATION], page: 1, pageSize: 200, total: 1 }); } if (call.url.includes("/messages")) { return jsonResponse({ items: [], total: 0 }); } return jsonResponse({ error: "not found" }, { status: 404 }); }); renderChatPage(); await waitFor(() => { expect(screen.getByText("测试对话")).not.toBeNull(); }); fireEvent.click(screen.getByLabelText("删除")); await waitFor(() => { expect(screen.getByText("确认删除该对话?")).not.toBeNull(); }); fireEvent.click(screen.getByText("删 除")); await waitFor(() => { expect(screen.getByText("暂无对话")).not.toBeNull(); }); }); });