测试基础设施 - 统一 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 使用说明
146 lines
5.1 KiB
TypeScript
146 lines
5.1 KiB
TypeScript
/**
|
||
* 全局测试配置
|
||
* 主要为后端测试提供基础环境
|
||
* 组件测试使用各自的 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 = "";
|
||
});
|