fix: 修复测试套件质量审查问题——act环境、正则匹配、mock排序、超时设置

This commit is contained in:
2026-06-08 14:13:45 +08:00
parent 74266dc5cc
commit d02abce58d
11 changed files with 204 additions and 179 deletions

View File

@@ -4,6 +4,13 @@
* 噪声过滤对所有测试生效
*/
declare global {
// eslint-disable-next-line no-var
var IS_REACT_ACT_ENVIRONMENT: boolean;
}
globalThis.IS_REACT_ACT_ENVIRONMENT = 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();
@@ -19,6 +26,7 @@ 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;
if (message.includes("not wrapped in act")) return;
originalConsoleError(...args);
};
@@ -79,7 +87,9 @@ globalThis.Selection = class Selection {
} as unknown as typeof Selection;
const { afterEach } = await import("bun:test");
const { cleanup } = await import("@testing-library/react");
afterEach(() => {
cleanup();
document.body.innerHTML = "";
});

View File

@@ -54,6 +54,12 @@ function setupFetchMock() {
if (call.url.includes("/models")) {
return jsonResponse({ items: [TEXT_MODEL], total: 1 });
}
if (call.url.includes("/messages")) {
return jsonResponse({ items: [], total: 0 });
}
if (call.method === "GET" && /\/conversations\/conv-/.exec(call.url)) {
return jsonResponse({ conversation: CONVERSATION });
}
if (call.url.includes("/conversations") && call.method === "GET") {
return jsonResponse({ items: [CONVERSATION], page: 1, pageSize: 200, total: 1 });
}
@@ -63,12 +69,6 @@ function setupFetchMock() {
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 });
});
}
@@ -143,15 +143,18 @@ describe("ChatPage", () => {
if (call.url.includes("/models")) {
return jsonResponse({ items: [TEXT_MODEL], total: 1 });
}
if (call.url.includes("/messages")) {
return jsonResponse({ items: [], total: 0 });
}
if (call.method === "GET" && /\/conversations\/conv-/.exec(call.url)) {
return jsonResponse({ conversation: CONVERSATION });
}
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 });
});

View File

@@ -65,7 +65,7 @@ const DEEPSEEK_PROVIDER: Provider = {
};
function clickLatestConfirmButton() {
const buttons = screen.getAllByRole("button", { name: /OK|确定/ });
const buttons = screen.getAllByRole("button", { name: /OK|确\s*定/ });
fireEvent.click(buttons[buttons.length - 1]!);
}
@@ -149,28 +149,37 @@ const TABLE_ACTION_TEST_CASES = [
];
describe("ResourceTable", () => {
// Ant Design Table 在 happy-dom 中渲染较慢,并行测试时需要更多时间
for (const tc of TABLE_TEST_CASES) {
test(`${tc.componentName} 渲染表格数据`, () => {
tc.render();
tc.assertData();
tc.assertNoExtra();
});
test(
`${tc.componentName} 渲染表格数据`,
() => {
tc.render();
tc.assertData();
tc.assertNoExtra();
},
{ timeout: 30000 },
);
}
for (const tc of TABLE_ACTION_TEST_CASES) {
test(`${tc.componentName} 表格操作触发 edit/delete`, async () => {
const onDelete = mock(() => Promise.resolve());
const onEdit = mock(() => undefined);
test(
`${tc.componentName} 表格操作触发 edit/delete`,
async () => {
const onDelete = mock(() => Promise.resolve());
const onEdit = mock(() => undefined);
tc.render({ onDelete, onEdit });
tc.render({ onDelete, onEdit });
fireEvent.click(screen.getAllByRole("button", { name: /编辑/ })[0]!);
expect(onEdit).toHaveBeenCalledWith(tc.editArg);
fireEvent.click(screen.getAllByRole("button", { name: /编辑/ })[0]!);
expect(onEdit).toHaveBeenCalledWith(tc.editArg);
fireEvent.click(screen.getAllByRole("button", { name: /删除/ })[0]!);
await screen.findByText(tc.deleteConfirmText);
clickLatestConfirmButton();
await waitFor(() => expect(onDelete).toHaveBeenCalledWith(tc.deleteId));
});
fireEvent.click(screen.getAllByRole("button", { name: /删除/ })[0]!);
await screen.findByText(tc.deleteConfirmText);
clickLatestConfirmButton();
await waitFor(() => expect(onDelete).toHaveBeenCalledWith(tc.deleteId));
},
{ timeout: 30000 },
);
}
});

View File

@@ -55,10 +55,13 @@ describe("AddMaterialModal", () => {
fireEvent.click(screen.getByText("确 定"));
await waitFor(() => {
expect(screen.getByText("请输入描述")).not.toBeNull();
});
});
await waitFor(
() => {
expect(screen.getByText("请输入描述")).not.toBeNull();
},
{ timeout: 10000 },
);
}, 30000);
test("点击确定触发表单提交", async () => {
const onAdd = vi.fn<(body: CreateMaterialRequest) => Promise<Material>>();
@@ -76,16 +79,19 @@ describe("AddMaterialModal", () => {
fireEvent.click(screen.getByText("确 定"));
await waitFor(() => {
expect(onAdd).toHaveBeenCalledTimes(1);
});
await waitFor(
() => {
expect(onAdd).toHaveBeenCalledTimes(1);
},
{ timeout: 10000 },
);
const callArgs = onAdd.mock.calls[0];
expect(callArgs).toBeDefined();
const calledBody = callArgs![0];
expect(calledBody.description).toBe("测试描述");
expect(calledBody.associatedDate).toMatch(/^\d{4}-\d{2}-\d{2}$/);
});
}, 30000);
test("提交失败显示错误提示", async () => {
const onAdd = vi.fn<(body: CreateMaterialRequest) => Promise<Material>>();
@@ -103,8 +109,11 @@ describe("AddMaterialModal", () => {
fireEvent.click(screen.getByText("确 定"));
await waitFor(() => {
expect(onAdd).toHaveBeenCalledTimes(1);
});
});
await waitFor(
() => {
expect(onAdd).toHaveBeenCalledTimes(1);
},
{ timeout: 10000 },
);
}, 30000);
});

View File

@@ -96,7 +96,7 @@ describe("InboxPage", () => {
const cards = screen.getAllByText("新增的素材");
expect(cards.length).toBeGreaterThanOrEqual(1);
});
});
}, 30000);
test("删除素材后列表更新", async () => {
let deleted = false;
@@ -131,5 +131,5 @@ describe("InboxPage", () => {
await waitFor(() => {
expect(screen.getByText("暂无素材")).not.toBeNull();
});
});
}, 30000);
});

View File

@@ -41,7 +41,7 @@ describe("ModelSettingsCard", () => {
expect(screen.getByText("音频生成")).not.toBeNull();
expect(screen.getByText("视频生成")).not.toBeNull();
});
});
}, 30000);
test("回显已保存的默认模型值", async () => {
installFetchMock((call) => {
@@ -63,5 +63,5 @@ describe("ModelSettingsCard", () => {
expect(screen.getByText("GPT-4")).not.toBeNull();
expect(screen.getByText("Claude Vision")).not.toBeNull();
});
});
}, 30000);
});

View File

@@ -46,34 +46,38 @@ function clickLatestConfirmButton() {
}
describe("ModelFormModal", () => {
test("编辑模型表单只提交变更字段", async () => {
const updateCalls: unknown[] = [];
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,
}),
);
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 screen.findByPlaceholderText("请输入模型名称");
fireEvent.change(screen.getByPlaceholderText("请输入模型名称"), { target: { value: "GPT-4o Mini" } });
clickLatestConfirmButton();
await screen.findByPlaceholderText("请输入模型名称");
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" });
});
await waitFor(() => expect(updateCalls.length).toBe(1));
expect(updateCalls[0]).toEqual({ data: { name: "GPT-4o Mini" }, id: "m1" });
},
{ timeout: 15000 },
);
test("模型表单校验失败不会提交", async () => {
const onCreate = mock(() => Promise.resolve());
@@ -121,80 +125,92 @@ describe("ModelFormModal", () => {
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,
}),
);
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 screen.findByPlaceholderText("请输入模型名称");
fireEvent.mouseDown(screen.getByRole("combobox"));
await screen.findByPlaceholderText("请输入模型名称");
fireEvent.mouseDown(screen.getByRole("combobox"));
expect(await screen.findByText("OpenAI")).not.toBeNull();
expect(await screen.findByText("DeepSeek")).not.toBeNull();
});
expect(await screen.findByText("OpenAI")).not.toBeNull();
expect(await screen.findByText("DeepSeek")).not.toBeNull();
},
{ timeout: 15000 },
);
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,
}),
);
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 screen.findByPlaceholderText("请输入模型名称");
fireEvent.mouseDown(screen.getByRole("combobox"));
await screen.findByPlaceholderText("请输入模型名称");
fireEvent.mouseDown(screen.getByRole("combobox"));
expect(await screen.findByText("供应商加载失败options failed")).not.toBeNull();
});
expect(await screen.findByText("供应商加载失败options failed")).not.toBeNull();
},
{ timeout: 15000 },
);
test("编辑模型时可测试模型连接", async () => {
const testModelConnection = mock(() => Promise.resolve({ message: "模型连接成功", ok: true }));
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,
}),
);
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 screen.findByRole("button", { name: "测试连接" });
fireEvent.click(screen.getByRole("button", { name: "测试连接" }));
await screen.findByRole("button", { name: "测试连接" });
fireEvent.click(screen.getByRole("button", { name: "测试连接" }));
await waitFor(() =>
expect(testModelConnection).toHaveBeenCalledWith({
externalId: "gpt-4o",
providerId: "pv1",
}),
);
});
await waitFor(() =>
expect(testModelConnection).toHaveBeenCalledWith({
externalId: "gpt-4o",
providerId: "pv1",
}),
);
},
{ timeout: 15000 },
);
test("新建模型也显示测试连接按钮", async () => {
renderWithProviders(
@@ -300,7 +316,7 @@ describe("ModelListPage", () => {
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);
}, 30000);
test("搜索模型更新请求参数", async () => {
const calls = createModelFetchMock();
@@ -312,7 +328,7 @@ describe("ModelListPage", () => {
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);
}, 30000);
test("新建模型弹窗可以打开", async () => {
createModelFetchMock();
@@ -322,5 +338,5 @@ describe("ModelListPage", () => {
fireEvent.click(screen.getByRole("button", { name: /新建模型/ }));
await screen.findByPlaceholderText("请输入模型名称");
}, 15000);
}, 30000);
});

View File

@@ -148,7 +148,7 @@ describe("ProjectsPage", () => {
await waitFor(() => expect(calls.some((call) => call.url.includes("status=archived"))).toBe(true));
await screen.findByText("归档项目");
});
}, 30000);
test("清空搜索条件复位请求参数并重新展示全部项目", async () => {
const calls = createProjectFetchMock();
@@ -190,7 +190,7 @@ describe("ProjectsPage", () => {
const createCall = calls.find((call) => call.url.endsWith("/api/projects") && call.method === "POST");
expect(createCall).toBeDefined();
expect(jsonBody(createCall?.body)).toEqual({ description: "新增描述", name: "新增项目" });
});
}, 30000);
test("编辑项目表单只提交变更字段", async () => {
const updateCalls: unknown[] = [];
@@ -217,7 +217,7 @@ describe("ProjectsPage", () => {
await waitFor(() => expect(onUpdate).toHaveBeenCalled());
expect(updateCalls[0]).toEqual({ data: { name: "编辑项目" }, id: "p1" });
});
}, 30000);
test("项目表单校验失败不会提交,接口失败时保留弹窗", async () => {
const onCreate = mock(() => Promise.reject(new Error("创建失败")));
@@ -244,7 +244,7 @@ describe("ProjectsPage", () => {
await waitFor(() => expect(onCreate).toHaveBeenCalled());
expect(onOpenChange).not.toHaveBeenCalledWith(false);
expect(screen.getByText("新建项目")).not.toBeNull();
});
}, 30000);
test("项目表格操作触发导航和行级动作", async () => {
const onArchive = mock(() => Promise.resolve());
@@ -287,5 +287,5 @@ describe("ProjectsPage", () => {
await screen.findByText("确认永久删除此项目?");
await clickLatestConfirmButton();
await waitFor(() => expect(onDelete).toHaveBeenCalledWith("p2"));
}, 15000);
}, 30000);
});

View File

@@ -49,7 +49,7 @@ describe("ProviderFormModal", () => {
await waitFor(() => expect(updateCalls.length).toBe(1));
expect(updateCalls[0]).toEqual({ data: { name: "New OpenAI" }, id: "pv1" });
});
}, 30000);
test("新建供应商默认使用 openai-compatible 类型", async () => {
const createCalls: unknown[] = [];
@@ -85,7 +85,7 @@ describe("ProviderFormModal", () => {
name: "兼容供应商",
type: "openai-compatible",
});
});
}, 30000);
test("供应商表单可使用当前表单配置测试连接", async () => {
const testCalls: unknown[] = [];
@@ -121,7 +121,7 @@ describe("ProviderFormModal", () => {
name: "兼容供应商",
type: "openai-compatible",
});
});
}, 30000);
});
const TEST_PROVIDER: Provider = {
@@ -197,7 +197,7 @@ describe("ProviderListPage", () => {
expect(screen.getByPlaceholderText("搜索供应商名称")).not.toBeNull();
expect(screen.getByRole("button", { name: /新建供应商/ })).not.toBeNull();
expect(calls.some((call) => call.url.includes("/api/providers"))).toBe(true);
}, 15000);
}, 30000);
test("搜索供应商更新请求参数", async () => {
const calls = createProviderFetchMock();
@@ -209,7 +209,7 @@ describe("ProviderListPage", () => {
fireEvent.change(input, { target: { value: "Open" } });
fireEvent.keyDown(input, { key: "Enter" });
await waitFor(() => expect(calls.some((call) => call.url.includes("keyword=Open"))).toBe(true));
}, 15000);
}, 30000);
test("新建供应商弹窗可以打开", async () => {
createProviderFetchMock();
@@ -219,5 +219,5 @@ describe("ProviderListPage", () => {
fireEvent.click(screen.getByRole("button", { name: /新建供应商/ }));
await screen.findByPlaceholderText("请输入供应商名称");
}, 15000);
}, 30000);
});

View File

@@ -65,7 +65,7 @@ describe("Workbench 路由", () => {
},
{ timeout: 10000 },
);
});
}, 30000);
test("Workbench 显示返回管理台按钮", async () => {
createMockHandler();
@@ -75,7 +75,7 @@ describe("Workbench 路由", () => {
});
await screen.findByText("返回管理台", {}, { timeout: 10000 });
});
}, 30000);
test("不存在项目显示不可访问", async () => {
createMockHandler();
@@ -85,7 +85,7 @@ describe("Workbench 路由", () => {
});
await screen.findByText("项目不存在或不可访问", {}, { timeout: 10000 });
});
}, 30000);
test("archived 项目显示不可访问", async () => {
createMockHandler({ status: "archived" });
@@ -95,7 +95,7 @@ describe("Workbench 路由", () => {
});
await screen.findByText("项目不存在或不可访问", {}, { timeout: 10000 });
});
}, 30000);
test("Workbench 显示聊天室菜单", async () => {
createMockHandler();
@@ -105,7 +105,7 @@ describe("Workbench 路由", () => {
});
await screen.findByText("聊天室", {}, { timeout: 10000 });
});
}, 30000);
test("Workbench 收集箱路由可达", async () => {
createMockHandler();
@@ -115,7 +115,7 @@ describe("Workbench 路由", () => {
});
await screen.findByText("新增素材", {}, { timeout: 10000 });
});
}, 30000);
test("Workbench 显示收集箱菜单", async () => {
createMockHandler();
@@ -125,5 +125,5 @@ describe("Workbench 路由", () => {
});
await screen.findByText("收集箱", {}, { timeout: 10000 });
});
}, 30000);
});

View File

@@ -24,32 +24,10 @@ describe("getDateGroup", () => {
});
test("本周内的日期返回 thisWeek", () => {
const now = new Date();
const dayOfWeek = now.getDay();
const mondayOffset = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
const wednesday = new Date(now);
wednesday.setDate(now.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1) + 2);
if (wednesday > now) {
const tuesday = new Date(now);
tuesday.setDate(now.getDate() - mondayOffset + 1);
if (tuesday < now && tuesday.getDate() !== now.getDate() - 1) {
const result = getDateGroup(tuesday.toISOString(), now);
expect(result).toBe("thisWeek");
return;
}
}
const tuesday = new Date(now);
tuesday.setDate(now.getDate() - mondayOffset + 1);
if (tuesday.toDateString() !== now.toDateString()) {
const yesterday = new Date(now);
yesterday.setDate(yesterday.getDate() - 1);
if (tuesday.toDateString() !== yesterday.toDateString()) {
const result = getDateGroup(tuesday.toISOString(), now);
expect(result).toBe("thisWeek");
return;
}
}
expect(true).toBe(true);
const now = new Date(2026, 5, 10);
const monday = new Date(2026, 5, 8);
const result = getDateGroup(monday.toISOString(), now);
expect(result).toBe("thisWeek");
});
test("本月内的日期返回 thisMonth", () => {