Files
Alfred/tests/setup.ts
lanyuanxiaoyao 2ea4bd4410 test: 测试体系全面优化,修复 Windows SQLite EBUSY 和前端产品缺陷
测试基础设施
- 统一 SQLite 测试 DB/临时目录 helper(tests/helpers.ts),支持 Windows EBUSY 重试清理
- 测试库使用 PRAGMA journal_mode=DELETE 避免 WAL 句柄延迟
- 路由 handler 测试改用 createMigratedMemoryTestDatabase 避免 File DB 锁
- SQLite 聚焦 --rerun-each=20 全部通过(720 pass)

后端测试补强
- 新增 tests/server/app.test.ts 真实 startServer 集成测试
- 覆盖 /api/meta、项目 CRUD、错误路径、静态 fallback、安全 header
- bootstrap/logger 测试捕获预期输出,消除测试噪音

前端测试补强
- 移除 .ant-* 内部类名依赖,改为角色/文本/导航/请求契约断言
- 项目页补充搜索、Tab 切换、表单、表格操作、错误反馈行为测试
- 新增 hooks(use-theme-preference、use-sidebar-collapsed、use-projects)纯逻辑测试
- 新增 ErrorBoundary 错误展示和刷新按钮测试
- 新增搜索清空行为测试
- 测试 setup 过滤 antd/rc-trigger NaN height warning

产品修复(测试暴露)
- 修复 ProjectToolbar 搜索框无法输入(新增 draftKeyword 状态)
- 加固 ProjectFormModal 表单字段同步(useEffect 替代不可靠的 afterOpenChange)
- 清理 ProjectFormModal 冗余 afterOpenChange 同步逻辑

重构与合规
- ProjectContext 拆分为三文件满足 React Fast Refresh 规则
- use-projects.ts 导出内部 helper 函数供测试验证
- scripts/build.ts 提取纯生成函数供测试使用,修复构建步骤日志编号
- 修复 build 测试覆盖真实生成逻辑

文档同步
- 更新后端/前端/开发文档测试规范、质量门禁和 helper 使用说明
2026-05-29 00:45:21 +08:00

146 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 全局测试配置
* 主要为后端测试提供基础环境
* 组件测试使用各自的 test-utils.tsx
*/
/* eslint-disable @typescript-eslint/no-empty-function */
// Set up jsdom for ALL tests (both backend and frontend)
import { JSDOM } from "jsdom";
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
pretendToBeVisual: true,
url: "http://localhost",
});
globalThis.document = dom.window.document;
globalThis.window = dom.window as unknown as typeof globalThis & Window;
globalThis.navigator = dom.window.navigator;
globalThis.HTMLElement = dom.window.HTMLElement;
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;
const attachEventFn = () => {};
const detachEventFn = () => {};
Object.defineProperty(nodeProto, "attachEvent", { configurable: true, value: attachEventFn, writable: true });
Object.defineProperty(nodeProto, "detachEvent", { configurable: true, value: detachEventFn, writable: true });
Object.defineProperty(elementProto, "attachEvent", { configurable: true, value: attachEventFn, writable: true });
Object.defineProperty(elementProto, "detachEvent", { configurable: true, value: detachEventFn, writable: true });
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();
if (str.includes("NaN") && str.includes("height") && str.includes("css style property")) return true;
return originalStderrWrite(
chunk,
encodingOrCb as Parameters<typeof process.stderr.write>[1],
cb as Parameters<typeof process.stderr.write>[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);
};
// Other polyfills
globalThis.ResizeObserver = class {
disconnect() {}
observe() {}
unobserve() {}
};
globalThis.MutationObserver = class {
disconnect() {}
observe() {}
takeRecords() {
return [];
}
unobserve() {}
};
globalThis.IntersectionObserver = class {
disconnect() {}
observe() {}
takeRecords() {
return [];
}
unobserve() {}
} as unknown as typeof IntersectionObserver;
globalThis.requestAnimationFrame = (cb: FrameRequestCallback) => setTimeout(cb, 16);
globalThis.cancelAnimationFrame = (id: number) => clearTimeout(id);
Object.defineProperty(dom.window, "matchMedia", {
value: (query: string) => ({
addEventListener: () => {},
addListener: () => {},
dispatchEvent: () => true,
matches: false,
media: query,
onchange: null,
removeEventListener: () => {},
removeListener: () => {},
}),
writable: true,
});
dom.window.Element.prototype.scrollTo = () => {};
dom.window.Element.prototype.scrollIntoView = () => {};
Object.defineProperty(dom.window, "customElements", {
value: {
define: () => {},
get: () => undefined,
},
writable: true,
});
globalThis.customElements = dom.window.customElements;
globalThis.ShadowRoot = class ShadowRoot extends dom.window.DocumentFragment {} as unknown as typeof ShadowRoot;
globalThis.SVGElement = class SVGElement extends dom.window.Element {} as unknown as typeof SVGElement;
globalThis.Selection = class Selection {
addRange() {}
removeAllRanges() {}
} as unknown as typeof Selection;
globalThis.Range = class Range extends dom.window.DocumentFragment {} as unknown as typeof Range;
import { afterEach } from "bun:test";
afterEach(() => {
document.body.innerHTML = "";
});