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:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user