import { fireEvent, screen, waitFor } from "@testing-library/react"; import { describe, expect, mock, test } from "bun:test"; import { createElement } from "react"; import type { Model, Provider } from "../../../src/shared/api"; import { App } from "../../../src/web/app"; import { ModelFormModal } from "../../../src/web/features/models/components/ModelFormModal"; import { installFetchMock, jsonResponse, mockMetaResponse, renderWithProviders } from "../test-utils"; const ENABLED_PROVIDER: Provider = { apiKey: "sk-test", baseUrl: "https://api.openai.com/v1", createdAt: "2024-01-01T00:00:00.000Z", id: "pv1", name: "OpenAI", type: "openai", updatedAt: "2024-01-01T00:00:00.000Z", }; const DISABLED_PROVIDER: Provider = { apiKey: "sk-off", baseUrl: "https://api.deepseek.com/v1", createdAt: "2024-01-01T00:00:00.000Z", id: "pv2", name: "DeepSeek", type: "openai-compatible", updatedAt: "2024-01-01T00:00:00.000Z", }; const ENABLED_MODEL: Model = { capabilities: ["text", "reasoning"], contextLength: 128000, createdAt: "2024-01-01T00:00:00.000Z", externalId: "gpt-4o", id: "m1", maxOutputTokens: 4096, name: "GPT-4o", providerId: "pv1", updatedAt: "2024-01-01T00:00:00.000Z", }; function clickLatestConfirmButton() { const buttons = screen.getAllByRole("button", { name: /OK|确\s*定/ }); fireEvent.click(buttons[buttons.length - 1]!); } describe("ModelFormModal", () => { test("编辑模型表单只提交变更字段", async () => { const updateCalls: unknown[] = []; renderWithProviders( createElement(ModelFormModal, { editingModel: ENABLED_MODEL, onCancel: () => undefined, onCreate: () => Promise.resolve(), onOpenChange: () => undefined, onUpdate: (args: unknown) => { updateCalls.push(args); return Promise.resolve(); }, open: true, providers: [ENABLED_PROVIDER, DISABLED_PROVIDER], providersError: null, providersLoading: false, submitting: false, }), ); await waitFor(() => expect(screen.getByPlaceholderText("请输入模型名称")).not.toBeNull()); fireEvent.change(screen.getByPlaceholderText("请输入模型名称"), { target: { value: "GPT-4o Mini" } }); clickLatestConfirmButton(); await waitFor(() => expect(updateCalls.length).toBe(1)); expect(updateCalls[0]).toEqual({ data: { name: "GPT-4o Mini" }, id: "m1" }); }); test("模型表单校验失败不会提交", async () => { const onCreate = mock(() => Promise.resolve()); renderWithProviders( createElement(ModelFormModal, { editingModel: null, onCancel: () => undefined, onCreate, onOpenChange: () => undefined, onUpdate: () => Promise.resolve(), open: true, providers: [ENABLED_PROVIDER], providersError: null, providersLoading: false, submitting: false, }), ); await waitFor(() => expect(screen.getByPlaceholderText("请输入模型名称")).not.toBeNull()); clickLatestConfirmButton(); expect(onCreate).not.toHaveBeenCalled(); }); test("新建模型默认选中文本和推理能力", async () => { renderWithProviders( createElement(ModelFormModal, { editingModel: null, onCancel: () => undefined, onCreate: () => Promise.resolve(), onOpenChange: () => undefined, onUpdate: () => Promise.resolve(), open: true, providers: [ENABLED_PROVIDER], providersError: null, providersLoading: false, submitting: false, }), ); await waitFor(() => expect(screen.getByLabelText("文本")).not.toBeNull()); const textCheckbox = screen.getByLabelText("文本"); const reasoningCheckbox = screen.getByLabelText("推理"); expect((textCheckbox as { checked?: boolean }).checked).toBe(true); expect((reasoningCheckbox as { checked?: boolean }).checked).toBe(true); }); test("新建模型展示供应商 options 列表", async () => { renderWithProviders( createElement(ModelFormModal, { editingModel: null, onCancel: () => undefined, onCreate: () => Promise.resolve(), onOpenChange: () => undefined, onUpdate: () => Promise.resolve(), open: true, providers: [ENABLED_PROVIDER, DISABLED_PROVIDER], providersError: null, providersLoading: false, submitting: false, }), ); await waitFor(() => expect(screen.getByPlaceholderText("请输入模型名称")).not.toBeNull()); fireEvent.mouseDown(screen.getByRole("combobox")); expect(await screen.findByText("OpenAI")).not.toBeNull(); expect(await screen.findByText("DeepSeek")).not.toBeNull(); }); test("供应商下拉展示加载错误提示", async () => { renderWithProviders( createElement(ModelFormModal, { editingModel: null, onCancel: () => undefined, onCreate: () => Promise.resolve(), onOpenChange: () => undefined, onUpdate: () => Promise.resolve(), open: true, providers: [], providersError: new Error("options failed"), providersLoading: false, submitting: false, }), ); await waitFor(() => expect(screen.getByPlaceholderText("请输入模型名称")).not.toBeNull()); fireEvent.mouseDown(screen.getByRole("combobox")); expect(await screen.findByText("供应商加载失败:options failed")).not.toBeNull(); }); test("编辑模型时可测试模型连接", async () => { const testModelConnection = mock(() => Promise.resolve({ message: "模型连接成功", ok: true })); renderWithProviders( createElement(ModelFormModal, { editingModel: ENABLED_MODEL, onCancel: () => undefined, onCreate: () => Promise.resolve(), onOpenChange: () => undefined, onUpdate: () => Promise.resolve(), open: true, providers: [ENABLED_PROVIDER], providersError: null, providersLoading: false, submitting: false, testModelConnection, }), ); await waitFor(() => expect(screen.getByRole("button", { name: "测试连接" })).not.toBeNull()); fireEvent.click(screen.getByRole("button", { name: "测试连接" })); await waitFor(() => expect(testModelConnection).toHaveBeenCalledWith({ externalId: "gpt-4o", providerId: "pv1", }), ); }); test("新建模型也显示测试连接按钮", async () => { renderWithProviders( createElement(ModelFormModal, { editingModel: null, onCancel: () => undefined, onCreate: () => Promise.resolve(), onOpenChange: () => undefined, onUpdate: () => Promise.resolve(), open: true, providers: [ENABLED_PROVIDER], providersError: null, providersLoading: false, submitting: false, testModelConnection: () => Promise.resolve({ message: "ok", ok: true }), }), ); await waitFor(() => expect(screen.getByRole("button", { name: "测试连接" })).not.toBeNull()); }); }); const TEST_PROVIDER: Provider = { apiKey: "sk-test", baseUrl: "https://api.openai.com/v1", createdAt: "2024-01-01T00:00:00.000Z", id: "pv1", name: "OpenAI", type: "openai", updatedAt: "2024-01-01T00:00:00.000Z", }; const TEST_MODEL: Model = { capabilities: ["text"], contextLength: 128000, createdAt: "2024-01-01T00:00:00.000Z", externalId: "gpt-4o", id: "m1", maxOutputTokens: 4096, name: "GPT-4o", providerId: "pv1", updatedAt: "2024-01-01T00:00:00.000Z", }; function createModelFetchMock() { let models = [TEST_MODEL]; return installFetchMock((call) => { if (call.url.includes("/api/meta")) return mockMetaResponse(); const url = new URL(call.url, "http://localhost"); if (url.pathname === "/api/providers/options" && call.method === "GET") { return jsonResponse({ items: [TEST_PROVIDER] }); } if (url.pathname === "/api/models" && call.method === "POST") { const data = JSON.parse(typeof call.body === "string" ? call.body : "{}") as Record; const created: Model = { ...TEST_MODEL, ...data, createdAt: "2024-01-02T00:00:00.000Z", id: "m-new", updatedAt: "2024-01-02T00:00:00.000Z", }; models = [created, ...models]; return jsonResponse({ model: created }, { status: 201 }); } if (/^\/api\/models\/[^/]+$/.exec(url.pathname) && call.method === "PATCH") { const id = url.pathname.split("/").pop()!; const data = JSON.parse(typeof call.body === "string" ? call.body : "{}") as Record; const existing = models.find((m) => m.id === id) ?? TEST_MODEL; const updated = { ...existing, ...(data as Partial) }; models = models.map((m) => (m.id === id ? updated : m)); return jsonResponse({ model: updated }); } if (/^\/api\/models\/[^/]+$/.exec(url.pathname) && call.method === "DELETE") { const id = url.pathname.split("/").pop()!; models = models.filter((m) => m.id !== id); return new Response(null, { status: 204 }); } if (url.pathname === "/api/models" && call.method === "GET") { const keyword = url.searchParams.get("keyword") ?? ""; const items = keyword ? models.filter((m) => `${m.name}${m.externalId}`.includes(keyword)) : models; return jsonResponse({ items, page: 1, pageSize: 20, total: items.length }); } return jsonResponse({ error: "Not Found" }, { status: 404 }); }); } describe("ModelListPage", () => { test("渲染模型列表页并请求模型数据", async () => { const calls = createModelFetchMock(); renderWithProviders(createElement(App), { initialRoute: "/models" }); await waitFor(() => { expect(screen.getByText("GPT-4o")).not.toBeNull(); }); expect(screen.getByPlaceholderText("搜索模型名称或 ID")).not.toBeNull(); expect(screen.getByRole("button", { name: /新建模型/ })).not.toBeNull(); expect(calls.some((call) => call.url.includes("/api/models"))).toBe(true); }, 15000); test("搜索模型更新请求参数", async () => { const calls = createModelFetchMock(); renderWithProviders(createElement(App), { initialRoute: "/models" }); await waitFor(() => expect(screen.getByText("GPT-4o")).not.toBeNull()); const input = screen.getByPlaceholderText("搜索模型名称或 ID"); fireEvent.change(input, { target: { value: "gpt" } }); fireEvent.keyDown(input, { key: "Enter" }); await waitFor(() => expect(calls.some((call) => call.url.includes("keyword=gpt"))).toBe(true)); }, 15000); test("新建模型弹窗可以打开", async () => { createModelFetchMock(); renderWithProviders(createElement(App), { initialRoute: "/models" }); await waitFor(() => expect(screen.getByRole("button", { name: /新建模型/ })).not.toBeNull()); fireEvent.click(screen.getByRole("button", { name: /新建模型/ })); await waitFor(() => expect(screen.getByPlaceholderText("请输入模型名称")).not.toBeNull()); }, 15000); });