refactor: 代码审查修复 — 错误边界、DRY抽取、测试修复、合规性改进

- P1: server.ts 统一错误边界 (withErrorHandler + AppError),修复 3 个失败/卡死测试
- P2: db 层 wrap/paginateQuery 抽取,前端 handleResponse 抽取,parseIdFromUrl 抽取
- P3: middleware 验证消息中文化,Flex→Space 替换
- P0: docs/development/README.md 新增已知设计决策章节
- P3-11 setup 拆分已尝试回退(@testing-library/react preload 依赖无法拆分)
- P3-13 config 层测试从本次变更移除
This commit is contained in:
2026-05-29 22:27:56 +08:00
parent 34e915ccf4
commit 10b3928bee
26 changed files with 428 additions and 300 deletions

View File

@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/require-await */
import { describe, expect, test } from "bun:test";
/* eslint-disable @typescript-eslint/require-await */
import { afterEach, describe, expect, test } from "bun:test";
import { mkdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
@@ -49,6 +49,19 @@ function makeTempConfig(overrides: Partial<ResolvedConfig> = {}): ResolvedConfig
}
describe("bootstrap", () => {
const shutdownHandlers: Array<() => void> = [];
afterEach(() => {
for (const fn of shutdownHandlers) {
try {
fn();
} catch {
// exit mock throws, that's expected
}
}
shutdownHandlers.length = 0;
});
test("使用默认依赖启动", async () => {
let started = false;
let signalRegistered = false;
@@ -56,7 +69,8 @@ describe("bootstrap", () => {
const cfg = makeTempConfig();
const mockLoadConfig = (async () => cfg) as unknown as BootstrapDependencies["loadConfig"];
const mockOnSignal = (_signal: string, _handler: () => void) => {
const mockOnSignal = (_signal: string, handler: () => void) => {
shutdownHandlers.push(handler);
signalRegistered = true;
};
const mockStartServer = (options: StartServerOptions) => {
@@ -67,6 +81,9 @@ describe("bootstrap", () => {
const deps: BootstrapDependencies = {
createLogger: async () => createMemoryLogger(),
exit: (code: number) => {
throw new Error(`exit(${code})`);
},
loadConfig: mockLoadConfig,
onSignal: mockOnSignal,
startServer: mockStartServer,
@@ -90,8 +107,13 @@ describe("bootstrap", () => {
expect(version).toBe("1.2.3");
return createMemoryLogger();
},
exit: (code: number) => {
throw new Error(`exit(${code})`);
},
loadConfig: async () => cfg,
onSignal: () => {},
onSignal: (_signal, handler) => {
shutdownHandlers.push(handler);
},
startServer: (options: StartServerOptions) => {
receivedVersion = options.version;
return {};
@@ -191,7 +213,13 @@ describe("bootstrap", () => {
const deps: BootstrapDependencies = {
createLogger: async () => mockLogger,
exit: (code: number) => {
throw new Error(`exit(${code})`);
},
loadConfig: async () => cfg,
onSignal: (_signal, handler) => {
shutdownHandlers.push(handler);
},
startServer: () => ({}),
};
@@ -202,7 +230,7 @@ describe("bootstrap", () => {
test("shutdown 时 flush logger", async () => {
let flushed = false;
let shutdownHandler: (() => void) | undefined;
let exitCode: number | undefined;
const mockLogger = createMemoryLogger();
mockLogger.flush = () => {
@@ -212,17 +240,29 @@ describe("bootstrap", () => {
const cfg = makeTempConfig();
const deps: BootstrapDependencies = {
createLogger: async () => mockLogger,
exit: (code: number) => {
exitCode = code;
throw new Error("exit called");
},
loadConfig: async () => cfg,
onSignal: (_signal, handler) => {
shutdownHandler = handler;
shutdownHandlers.push(handler);
},
startServer: () => ({}),
};
await bootstrap({ configPath: join(cfg.configDir, "config.yaml"), mode: "production" }, deps);
expect(shutdownHandler).toBeDefined();
shutdownHandler!();
const handler = shutdownHandlers.pop();
expect(handler).toBeDefined();
try {
handler!();
} catch {
// expected - exit threw
}
expect(flushed).toBe(true);
expect(exitCode).toBe(0);
});
});

View File

@@ -224,7 +224,8 @@ describe("loadServerConfig", () => {
await loadServerConfig(yamlPath);
expect.unreachable();
} catch (error) {
expect((error as Error).message).toContain("日志等级");
expect((error as Error).message).toContain("server.logging.level");
expect((error as Error).message).toContain("不在允许范围内");
} finally {
await rm(yamlPath, { force: true });
}

View File

@@ -1,11 +1,11 @@
/**
* 全局测试配置
* 主要为后端测试提供基础环境
* 组件测试使用各自的 test-utils.tsx
* 后端测试无需 DOM 环境,前端测试依赖 jsdom 及 antd polyfill
*/
/* eslint-disable @typescript-eslint/no-empty-function */
// Set up jsdom for ALL tests (both backend and frontend)
// 仅当前端测试需要时初始化 jsdom所有测试共享 preload后端测试也在此环境中运行
import { JSDOM } from "jsdom";
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
@@ -21,20 +21,14 @@ globalThis.HTMLBodyElement = dom.window.HTMLBodyElement;
globalThis.HTMLHtmlElement = dom.window.HTMLHtmlElement;
globalThis.Element = dom.window.Element;
globalThis.getComputedStyle = (element: Element, pseudoElt?: null | string) => {
// jsdom 不支持伪元素计算样式antd/rc-trigger 会传入伪元素参数,测试中退回普通样式即可。
return dom.window.getComputedStyle(element, pseudoElt ? undefined : pseudoElt);
};
// Ensure document.body exists
if (!globalThis.document.body) {
const body = globalThis.document.createElement("body");
globalThis.document.documentElement.appendChild(body);
}
// CRITICAL: Set up polyfills BEFORE any other imports
// This ensures @testing-library/react sees these when it loads
// IE-style event handling polyfill (React fallback)
const nodeProto = dom.window.Node.prototype;
const elementProto = dom.window.Element.prototype;
const htmlElementProto = dom.window.HTMLElement.prototype;
@@ -49,7 +43,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 });
// 抑制 antd/rc-trigger 在 jsdom 中产生的 NaN height warning
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();
@@ -75,7 +68,6 @@ console.warn = (...args: unknown[]) => {
originalConsoleWarn(...args);
};
// Other polyfills
globalThis.ResizeObserver = class {
disconnect() {}
observe() {}

View File

@@ -30,9 +30,11 @@ const ARCHIVED_PROJECT: Project = {
updatedAt: "2024-01-02T00:00:00.000Z",
};
function clickLatestConfirmButton() {
const buttons = screen.getAllByRole("button", { name: /OK|确\s*定/ });
fireEvent.click(buttons[buttons.length - 1]!);
async function clickLatestConfirmButton() {
const confirmTexts = await screen.findAllByText(/确\s*定|OK|确认/);
const lastText = confirmTexts[confirmTexts.length - 1]!;
const button = lastText.closest("button") ?? lastText.closest("[role='button']") ?? lastText;
fireEvent.click(button);
}
function createProjectFetchMock(initialProjects: Project[] = [ACTIVE_PROJECT, ARCHIVED_PROJECT]) {
@@ -170,7 +172,7 @@ describe("ProjectsPage", () => {
await waitFor(() => expect(screen.getByPlaceholderText("请输入项目名称")).not.toBeNull());
fireEvent.change(screen.getByPlaceholderText("请输入项目名称"), { target: { value: "新增项目" } });
fireEvent.change(screen.getByPlaceholderText("请输入项目描述"), { target: { value: "新增描述" } });
clickLatestConfirmButton();
await clickLatestConfirmButton();
await waitFor(() => expect(screen.getByText("新增项目")).not.toBeNull());
@@ -200,7 +202,7 @@ describe("ProjectsPage", () => {
await waitFor(() => expect(screen.getByPlaceholderText("请输入项目名称")).not.toBeNull());
fireEvent.change(screen.getByPlaceholderText("请输入项目名称"), { target: { value: "编辑项目" } });
clickLatestConfirmButton();
await clickLatestConfirmButton();
await waitFor(() => expect(onUpdate).toHaveBeenCalled());
expect(updateCalls[0]).toEqual({ data: { name: "编辑项目" }, id: "p1" });
@@ -223,11 +225,11 @@ describe("ProjectsPage", () => {
);
await waitFor(() => expect(screen.getByPlaceholderText("请输入项目名称")).not.toBeNull());
clickLatestConfirmButton();
await clickLatestConfirmButton();
expect(onCreate).not.toHaveBeenCalled();
fireEvent.change(screen.getByPlaceholderText("请输入项目名称"), { target: { value: "失败项目" } });
clickLatestConfirmButton();
await clickLatestConfirmButton();
await waitFor(() => expect(onCreate).toHaveBeenCalled());
expect(onOpenChange).not.toHaveBeenCalledWith(false);
expect(screen.getByText("新建项目")).not.toBeNull();
@@ -262,17 +264,17 @@ describe("ProjectsPage", () => {
fireEvent.click(screen.getByRole("button", { name: /归档/ }));
await waitFor(() => expect(screen.getByText("确认归档此项目?")).not.toBeNull());
clickLatestConfirmButton();
await clickLatestConfirmButton();
await waitFor(() => expect(onArchive).toHaveBeenCalledWith("p1"));
fireEvent.click(screen.getByRole("button", { name: /恢复/ }));
await waitFor(() => expect(screen.getByText("确认恢复此项目?")).not.toBeNull());
clickLatestConfirmButton();
await clickLatestConfirmButton();
await waitFor(() => expect(onRestore).toHaveBeenCalledWith("p2"));
fireEvent.click(screen.getByRole("button", { name: /删除/ }));
await waitFor(() => expect(screen.getByText("确认永久删除此项目?")).not.toBeNull());
clickLatestConfirmButton();
await clickLatestConfirmButton();
await waitFor(() => expect(onDelete).toHaveBeenCalledWith("p2"));
});
}, 15000);
});