diff --git a/tests/setup.ts b/tests/setup.ts index 72ad5ac..0613f69 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -1,10 +1,35 @@ /** * 全局测试配置 - * 后端测试无需 DOM 环境,前端测试依赖 jsdom 及 antd polyfill + * 为所有测试初始化 jsdom — bun worker 的 process.argv 无法保证携带文件路径 + * 噪声过滤对所有测试生效 */ -// 仅当前端测试需要时初始化 jsdom(所有测试共享 preload,后端测试也在此环境中运行) -import { JSDOM } from "jsdom"; +const originalStderrWrite = process.stderr.write.bind(process.stderr); +process.stderr.write = (chunk: string | Uint8Array, encodingOrCb?: unknown, cb?: unknown) => { + const str = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString(); + if (str.includes("NaN") && str.includes("height") && str.includes("css style property")) return true; + return originalStderrWrite( + chunk, + encodingOrCb as Parameters[1], + cb as Parameters[2], + ); +}; + +const originalConsoleError = console.error; +console.error = (...args: unknown[]) => { + const message = args.map(String).join(" "); + if (message.includes("NaN") && message.includes("height") && message.includes("css style property")) return; + originalConsoleError(...args); +}; + +const originalConsoleWarn = console.warn; +console.warn = (...args: unknown[]) => { + const message = args.map(String).join(" "); + if (message.includes("NaN") && message.includes("height") && message.includes("css style property")) return; + originalConsoleWarn(...args); +}; + +const { JSDOM } = await import("jsdom"); const dom = new JSDOM("", { pretendToBeVisual: true, @@ -41,31 +66,6 @@ Object.defineProperty(elementProto, "detachEvent", { configurable: true, value: Object.defineProperty(htmlElementProto, "attachEvent", { configurable: true, value: attachEventFn, writable: true }); Object.defineProperty(htmlElementProto, "detachEvent", { configurable: true, value: detachEventFn, writable: true }); -const originalStderrWrite = process.stderr.write.bind(process.stderr); -process.stderr.write = (chunk: string | Uint8Array, encodingOrCb?: unknown, cb?: unknown) => { - const str = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString(); - if (str.includes("NaN") && str.includes("height") && str.includes("css style property")) return true; - return originalStderrWrite( - chunk, - encodingOrCb as Parameters[1], - cb as Parameters[2], - ); -}; - -const originalConsoleError = console.error; -console.error = (...args: unknown[]) => { - const message = args.map(String).join(" "); - if (message.includes("NaN") && message.includes("height") && message.includes("css style property")) return; - originalConsoleError(...args); -}; - -const originalConsoleWarn = console.warn; -console.warn = (...args: unknown[]) => { - const message = args.map(String).join(" "); - if (message.includes("NaN") && message.includes("height") && message.includes("css style property")) return; - originalConsoleWarn(...args); -}; - globalThis.ResizeObserver = class { disconnect() {} observe() {} @@ -128,7 +128,7 @@ globalThis.Selection = class Selection { } as unknown as typeof Selection; globalThis.Range = class Range extends dom.window.DocumentFragment {} as unknown as typeof Range; -import { afterEach } from "bun:test"; +const { afterEach } = await import("bun:test"); afterEach(() => { document.body.innerHTML = ""; diff --git a/tests/web/components/ResourceTable.test.tsx b/tests/web/components/ResourceTable.test.tsx index c6ee43e..d872655 100644 --- a/tests/web/components/ResourceTable.test.tsx +++ b/tests/web/components/ResourceTable.test.tsx @@ -168,7 +168,7 @@ describe("ResourceTable", () => { expect(onEdit).toHaveBeenCalledWith(tc.editArg); fireEvent.click(screen.getAllByRole("button", { name: /删除/ })[0]!); - await waitFor(() => expect(screen.getByText(tc.deleteConfirmText)).not.toBeNull()); + await screen.findByText(tc.deleteConfirmText); clickLatestConfirmButton(); await waitFor(() => expect(onDelete).toHaveBeenCalledWith(tc.deleteId)); }); diff --git a/tests/web/routes/models.test.tsx b/tests/web/routes/models.test.tsx index 33400b6..30b1ca5 100644 --- a/tests/web/routes/models.test.tsx +++ b/tests/web/routes/models.test.tsx @@ -67,7 +67,7 @@ describe("ModelFormModal", () => { }), ); - await waitFor(() => expect(screen.getByPlaceholderText("请输入模型名称")).not.toBeNull()); + await screen.findByPlaceholderText("请输入模型名称"); fireEvent.change(screen.getByPlaceholderText("请输入模型名称"), { target: { value: "GPT-4o Mini" } }); clickLatestConfirmButton(); @@ -93,7 +93,7 @@ describe("ModelFormModal", () => { }), ); - await waitFor(() => expect(screen.getByPlaceholderText("请输入模型名称")).not.toBeNull()); + await screen.findByPlaceholderText("请输入模型名称"); clickLatestConfirmButton(); expect(onCreate).not.toHaveBeenCalled(); }); @@ -114,7 +114,7 @@ describe("ModelFormModal", () => { }), ); - await waitFor(() => expect(screen.getByLabelText("文本")).not.toBeNull()); + await screen.findByLabelText("文本"); const textCheckbox = screen.getByLabelText("文本"); const reasoningCheckbox = screen.getByLabelText("推理"); expect((textCheckbox as { checked?: boolean }).checked).toBe(true); @@ -137,7 +137,7 @@ describe("ModelFormModal", () => { }), ); - await waitFor(() => expect(screen.getByPlaceholderText("请输入模型名称")).not.toBeNull()); + await screen.findByPlaceholderText("请输入模型名称"); fireEvent.mouseDown(screen.getByRole("combobox")); expect(await screen.findByText("OpenAI")).not.toBeNull(); @@ -160,7 +160,7 @@ describe("ModelFormModal", () => { }), ); - await waitFor(() => expect(screen.getByPlaceholderText("请输入模型名称")).not.toBeNull()); + await screen.findByPlaceholderText("请输入模型名称"); fireEvent.mouseDown(screen.getByRole("combobox")); expect(await screen.findByText("供应商加载失败:options failed")).not.toBeNull(); @@ -185,7 +185,7 @@ describe("ModelFormModal", () => { }), ); - await waitFor(() => expect(screen.getByRole("button", { name: "测试连接" })).not.toBeNull()); + await screen.findByRole("button", { name: "测试连接" }); fireEvent.click(screen.getByRole("button", { name: "测试连接" })); await waitFor(() => @@ -213,7 +213,7 @@ describe("ModelFormModal", () => { }), ); - await waitFor(() => expect(screen.getByRole("button", { name: "测试连接" })).not.toBeNull()); + await screen.findByRole("button", { name: "测试连接" }); }); }); @@ -295,9 +295,7 @@ describe("ModelListPage", () => { renderWithProviders(createElement(App), { initialRoute: "/models" }); - await waitFor(() => { - expect(screen.getByText("GPT-4o")).not.toBeNull(); - }); + await screen.findByText("GPT-4o"); expect(screen.getByPlaceholderText("搜索模型名称或 ID")).not.toBeNull(); expect(screen.getByRole("button", { name: /新建模型/ })).not.toBeNull(); @@ -308,7 +306,7 @@ describe("ModelListPage", () => { const calls = createModelFetchMock(); renderWithProviders(createElement(App), { initialRoute: "/models" }); - await waitFor(() => expect(screen.getByText("GPT-4o")).not.toBeNull()); + await screen.findByText("GPT-4o"); const input = screen.getByPlaceholderText("搜索模型名称或 ID"); fireEvent.change(input, { target: { value: "gpt" } }); @@ -320,9 +318,9 @@ describe("ModelListPage", () => { createModelFetchMock(); renderWithProviders(createElement(App), { initialRoute: "/models" }); - await waitFor(() => expect(screen.getByRole("button", { name: /新建模型/ })).not.toBeNull()); + await screen.findByRole("button", { name: /新建模型/ }); fireEvent.click(screen.getByRole("button", { name: /新建模型/ })); - await waitFor(() => expect(screen.getByPlaceholderText("请输入模型名称")).not.toBeNull()); + await screen.findByPlaceholderText("请输入模型名称"); }, 15000); }); diff --git a/tests/web/routes/projects.test.tsx b/tests/web/routes/projects.test.tsx index 7fd9f52..2dbd3a1 100644 --- a/tests/web/routes/projects.test.tsx +++ b/tests/web/routes/projects.test.tsx @@ -130,7 +130,7 @@ describe("ProjectsPage", () => { const calls = createProjectFetchMock(); renderWithProviders(createElement(App), { initialRoute: "/projects" }); - await waitFor(() => expect(screen.getByText("活跃项目")).not.toBeNull()); + await screen.findByText("活跃项目"); const searchInput1 = screen.getByPlaceholderText("搜索名称或描述"); fireEvent.change(searchInput1, { target: { value: "归档" } }); @@ -147,14 +147,14 @@ describe("ProjectsPage", () => { }); await waitFor(() => expect(calls.some((call) => call.url.includes("status=archived"))).toBe(true)); - await waitFor(() => expect(screen.getByText("归档项目")).not.toBeNull()); + await screen.findByText("归档项目"); }); test("清空搜索条件复位请求参数并重新展示全部项目", async () => { const calls = createProjectFetchMock(); renderWithProviders(createElement(App), { initialRoute: "/projects" }); - await waitFor(() => expect(screen.getByText("活跃项目")).not.toBeNull()); + await screen.findByText("活跃项目"); const searchInput2 = screen.getByPlaceholderText("搜索名称或描述"); fireEvent.change(searchInput2, { target: { value: "归档" } }); @@ -169,23 +169,23 @@ describe("ProjectsPage", () => { const lastProjectCall = [...calls].reverse().find((call) => call.url.includes("/api/projects")); expect(lastProjectCall && !lastProjectCall.url.includes("keyword=")).toBe(true); }); - await waitFor(() => expect(screen.getByText("活跃项目")).not.toBeNull()); + await screen.findByText("活跃项目"); }); test("新建项目提交请求 body 并显示创建结果", async () => { const calls = createProjectFetchMock([]); renderWithProviders(createElement(App), { initialRoute: "/projects" }); - await waitFor(() => expect(screen.getByRole("button", { name: /新建项目/ })).not.toBeNull()); + await screen.findByRole("button", { name: /新建项目/ }); fireEvent.click(screen.getByRole("button", { name: /新建项目/ })); await waitFor(() => expect(screen.getAllByText("新建项目").length).toBeGreaterThan(1)); - await waitFor(() => expect(screen.getByPlaceholderText("请输入项目名称")).not.toBeNull()); + await screen.findByPlaceholderText("请输入项目名称"); fireEvent.change(screen.getByPlaceholderText("请输入项目名称"), { target: { value: "新增项目" } }); fireEvent.change(screen.getByPlaceholderText("请输入项目描述"), { target: { value: "新增描述" } }); await clickLatestConfirmButton(); - await waitFor(() => expect(screen.getByText("新增项目")).not.toBeNull()); + await screen.findByText("新增项目"); const createCall = calls.find((call) => call.url.endsWith("/api/projects") && call.method === "POST"); expect(createCall).toBeDefined(); @@ -211,7 +211,7 @@ describe("ProjectsPage", () => { }), ); - await waitFor(() => expect(screen.getByPlaceholderText("请输入项目名称")).not.toBeNull()); + await screen.findByPlaceholderText("请输入项目名称"); fireEvent.change(screen.getByPlaceholderText("请输入项目名称"), { target: { value: "编辑项目" } }); await clickLatestConfirmButton(); @@ -235,7 +235,7 @@ describe("ProjectsPage", () => { }), ); - await waitFor(() => expect(screen.getByPlaceholderText("请输入项目名称")).not.toBeNull()); + await screen.findByPlaceholderText("请输入项目名称"); await clickLatestConfirmButton(); expect(onCreate).not.toHaveBeenCalled(); @@ -274,17 +274,17 @@ describe("ProjectsPage", () => { expect(screen.getByText("当前路径:/workbench/p1")).not.toBeNull(); fireEvent.click(screen.getByRole("button", { name: /归档/ })); - await waitFor(() => expect(screen.getByText("确认归档此项目?")).not.toBeNull()); + await screen.findByText("确认归档此项目?"); await clickLatestConfirmButton(); await waitFor(() => expect(onArchive).toHaveBeenCalledWith("p1")); fireEvent.click(screen.getByRole("button", { name: /恢复/ })); - await waitFor(() => expect(screen.getByText("确认恢复此项目?")).not.toBeNull()); + await screen.findByText("确认恢复此项目?"); await clickLatestConfirmButton(); await waitFor(() => expect(onRestore).toHaveBeenCalledWith("p2")); fireEvent.click(screen.getByRole("button", { name: /删除/ })); - await waitFor(() => expect(screen.getByText("确认永久删除此项目?")).not.toBeNull()); + await screen.findByText("确认永久删除此项目?"); await clickLatestConfirmButton(); await waitFor(() => expect(onDelete).toHaveBeenCalledWith("p2")); }, 15000); diff --git a/tests/web/routes/providers.test.tsx b/tests/web/routes/providers.test.tsx index d332044..8902b62 100644 --- a/tests/web/routes/providers.test.tsx +++ b/tests/web/routes/providers.test.tsx @@ -43,7 +43,7 @@ describe("ProviderFormModal", () => { }), ); - await waitFor(() => expect(screen.getByPlaceholderText("请输入供应商名称")).not.toBeNull()); + await screen.findByPlaceholderText("请输入供应商名称"); fireEvent.change(screen.getByPlaceholderText("请输入供应商名称"), { target: { value: "New OpenAI" } }); clickLatestConfirmButton(); @@ -70,7 +70,7 @@ describe("ProviderFormModal", () => { }), ); - await waitFor(() => expect(screen.getByPlaceholderText("请输入供应商名称")).not.toBeNull()); + await screen.findByPlaceholderText("请输入供应商名称"); fireEvent.change(screen.getByPlaceholderText("请输入供应商名称"), { target: { value: "兼容供应商" } }); fireEvent.change(screen.getByPlaceholderText("https://api.openai.com/v1"), { target: { value: "https://api.test.com/v1" }, @@ -106,7 +106,7 @@ describe("ProviderFormModal", () => { }), ); - await waitFor(() => expect(screen.getByPlaceholderText("请输入供应商名称")).not.toBeNull()); + await screen.findByPlaceholderText("请输入供应商名称"); fireEvent.change(screen.getByPlaceholderText("请输入供应商名称"), { target: { value: "兼容供应商" } }); fireEvent.change(screen.getByPlaceholderText("https://api.openai.com/v1"), { target: { value: "https://api.test.com/v1" }, @@ -215,9 +215,9 @@ describe("ProviderListPage", () => { createProviderFetchMock(); renderWithProviders(createElement(App), { initialRoute: "/models/providers" }); - await waitFor(() => expect(screen.getByRole("button", { name: /新建供应商/ })).not.toBeNull()); + await screen.findByRole("button", { name: /新建供应商/ }); fireEvent.click(screen.getByRole("button", { name: /新建供应商/ })); - await waitFor(() => expect(screen.getByPlaceholderText("请输入供应商名称")).not.toBeNull()); + await screen.findByPlaceholderText("请输入供应商名称"); }, 15000); }); diff --git a/tests/web/routes/workbench.test.tsx b/tests/web/routes/workbench.test.tsx index 4831915..c460497 100644 --- a/tests/web/routes/workbench.test.tsx +++ b/tests/web/routes/workbench.test.tsx @@ -74,12 +74,7 @@ describe("Workbench 路由", () => { initialRoute: `/workbench/${MOCK_PROJECT.id}`, }); - await waitFor( - () => { - expect(screen.getByText("返回管理台")).not.toBeNull(); - }, - { timeout: 10000 }, - ); + await screen.findByText("返回管理台", {}, { timeout: 10000 }); }); test("不存在项目显示不可访问", async () => { @@ -89,12 +84,7 @@ describe("Workbench 路由", () => { initialRoute: "/workbench/nonexistent-id", }); - await waitFor( - () => { - expect(screen.getByText("项目不存在或不可访问")).not.toBeNull(); - }, - { timeout: 10000 }, - ); + await screen.findByText("项目不存在或不可访问", {}, { timeout: 10000 }); }); test("archived 项目显示不可访问", async () => { @@ -104,12 +94,7 @@ describe("Workbench 路由", () => { initialRoute: `/workbench/${MOCK_PROJECT.id}`, }); - await waitFor( - () => { - expect(screen.getByText("项目不存在或不可访问")).not.toBeNull(); - }, - { timeout: 10000 }, - ); + await screen.findByText("项目不存在或不可访问", {}, { timeout: 10000 }); }); test("Workbench 显示聊天室菜单", async () => { @@ -119,12 +104,7 @@ describe("Workbench 路由", () => { initialRoute: `/workbench/${MOCK_PROJECT.id}`, }); - await waitFor( - () => { - expect(screen.getByText("聊天室")).not.toBeNull(); - }, - { timeout: 10000 }, - ); + await screen.findByText("聊天室", {}, { timeout: 10000 }); }); test("Workbench 收集箱路由可达", async () => { @@ -134,12 +114,7 @@ describe("Workbench 路由", () => { initialRoute: `/workbench/${MOCK_PROJECT.id}/inbox`, }); - await waitFor( - () => { - expect(screen.getByText("新增素材")).not.toBeNull(); - }, - { timeout: 10000 }, - ); + await screen.findByText("新增素材", {}, { timeout: 10000 }); }); test("Workbench 显示收集箱菜单", async () => { @@ -149,11 +124,6 @@ describe("Workbench 路由", () => { initialRoute: `/workbench/${MOCK_PROJECT.id}`, }); - await waitFor( - () => { - expect(screen.getByText("收集箱")).not.toBeNull(); - }, - { timeout: 10000 }, - ); + await screen.findByText("收集箱", {}, { timeout: 10000 }); }); }); diff --git a/tests/web/test-utils.tsx b/tests/web/test-utils.tsx index 447ea36..c618df2 100644 --- a/tests/web/test-utils.tsx +++ b/tests/web/test-utils.tsx @@ -90,6 +90,23 @@ export function renderWithProviders(ui: React.ReactElement, options?: RenderWith ); } +export function renderWithBasicProviders(ui: React.ReactElement, options?: RenderWithProvidersOptions) { + const queryClient = createTestQueryClient(); + const initialRoute = options?.initialRoute ?? "/"; + + return render( + createElement( + StrictMode, + null, + createElement( + QueryClientProvider, + { client: queryClient }, + createElement(MemoryRouter, { initialEntries: [initialRoute] }, ui), + ), + ), + ); +} + function createTestQueryClient() { return new QueryClient({ defaultOptions: {