测试基础设施 - 统一 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 使用说明
170 lines
6.5 KiB
TypeScript
170 lines
6.5 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
|
|
import type { StaticAssets } from "../../src/server/static";
|
|
|
|
import { startServer } from "../../src/server/server";
|
|
import { createMigratedTestDatabase } from "../helpers";
|
|
|
|
function createStaticAssets(): StaticAssets {
|
|
return {
|
|
files: {
|
|
"/assets/app.js": new Blob(["console.log('app')"], { type: "text/javascript" }),
|
|
},
|
|
indexHtml: new Blob(["<!doctype html><main>Alfred</main>"], { type: "text/html" }),
|
|
};
|
|
}
|
|
|
|
function createTestApp() {
|
|
const handle = createMigratedTestDatabase("app-test");
|
|
const server = startServer({
|
|
config: { host: "127.0.0.1", port: 0 },
|
|
db: handle.db,
|
|
logger: handle.logger,
|
|
mode: "production",
|
|
staticAssets: createStaticAssets(),
|
|
version: "9.8.7-test",
|
|
});
|
|
|
|
return {
|
|
baseUrl: server.url.toString().replace(/\/$/, ""),
|
|
close: () => {
|
|
void server.stop(true);
|
|
handle.cleanup();
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("后端应用集成", () => {
|
|
test("/api/meta 返回服务元信息和生产安全 header", async () => {
|
|
const app = createTestApp();
|
|
try {
|
|
const res = await fetch(`${app.baseUrl}/api/meta`);
|
|
expect(res.status).toBe(200);
|
|
expect(res.headers.get("Content-Type")).toBe("application/json; charset=utf-8");
|
|
expect(res.headers.get("X-Content-Type-Options")).toBe("nosniff");
|
|
expect(res.headers.get("Referrer-Policy")).toBe("strict-origin-when-cross-origin");
|
|
|
|
const body = (await res.json()) as { ok: boolean; service: string; timestamp: string; version: string };
|
|
expect(body.ok).toBe(true);
|
|
expect(body.service).toBe("alfred");
|
|
expect(body.version).toBe("9.8.7-test");
|
|
expect(Number.isNaN(Date.parse(body.timestamp))).toBe(false);
|
|
} finally {
|
|
app.close();
|
|
}
|
|
});
|
|
|
|
test("未知 API 返回 JSON 404", async () => {
|
|
const app = createTestApp();
|
|
try {
|
|
const res = await fetch(`${app.baseUrl}/api/missing`);
|
|
expect(res.status).toBe(404);
|
|
expect(res.headers.get("Content-Type")).toBe("application/json; charset=utf-8");
|
|
expect(await res.json()).toEqual({ error: "API route not found", status: 404 });
|
|
} finally {
|
|
app.close();
|
|
}
|
|
});
|
|
|
|
test("静态资源和 SPA fallback 由真实服务返回", async () => {
|
|
const app = createTestApp();
|
|
try {
|
|
const assetRes = await fetch(`${app.baseUrl}/assets/app.js`);
|
|
expect(assetRes.status).toBe(200);
|
|
expect(assetRes.headers.get("Cache-Control")).toBe("public, max-age=31536000, immutable");
|
|
expect(await assetRes.text()).toBe("console.log('app')");
|
|
|
|
const fallbackRes = await fetch(`${app.baseUrl}/projects`);
|
|
expect(fallbackRes.status).toBe(200);
|
|
expect(fallbackRes.headers.get("Cache-Control")).toBe("no-cache");
|
|
expect(await fallbackRes.text()).toContain("Alfred");
|
|
} finally {
|
|
app.close();
|
|
}
|
|
});
|
|
|
|
test("项目 API 覆盖创建、列表、详情、更新、归档、恢复和删除", async () => {
|
|
const app = createTestApp();
|
|
try {
|
|
const createdRes = await fetch(`${app.baseUrl}/api/projects`, {
|
|
body: JSON.stringify({ description: "集成测试", name: "集成项目" }),
|
|
headers: { "Content-Type": "application/json" },
|
|
method: "POST",
|
|
});
|
|
expect(createdRes.status).toBe(201);
|
|
const createdBody = (await createdRes.json()) as { project: { id: string; name: string; status: string } };
|
|
const id = createdBody.project.id;
|
|
expect(createdBody.project.name).toBe("集成项目");
|
|
|
|
const listRes = await fetch(`${app.baseUrl}/api/projects?keyword=集成&page=1&pageSize=20`);
|
|
expect(listRes.status).toBe(200);
|
|
const listBody = (await listRes.json()) as { items: Array<{ id: string }>; total: number };
|
|
expect(listBody.total).toBe(1);
|
|
expect(listBody.items[0]!.id).toBe(id);
|
|
|
|
const getRes = await fetch(`${app.baseUrl}/api/projects/${id}`);
|
|
expect(getRes.status).toBe(200);
|
|
|
|
const updateRes = await fetch(`${app.baseUrl}/api/projects/${id}`, {
|
|
body: JSON.stringify({ name: "更新项目" }),
|
|
headers: { "Content-Type": "application/json" },
|
|
method: "PATCH",
|
|
});
|
|
expect(updateRes.status).toBe(200);
|
|
expect(((await updateRes.json()) as { project: { name: string } }).project.name).toBe("更新项目");
|
|
|
|
const archiveRes = await fetch(`${app.baseUrl}/api/projects/${id}/archive`, { method: "POST" });
|
|
expect(archiveRes.status).toBe(200);
|
|
expect(((await archiveRes.json()) as { project: { status: string } }).project.status).toBe("archived");
|
|
|
|
const restoreRes = await fetch(`${app.baseUrl}/api/projects/${id}/restore`, { method: "POST" });
|
|
expect(restoreRes.status).toBe(200);
|
|
expect(((await restoreRes.json()) as { project: { status: string } }).project.status).toBe("active");
|
|
|
|
await fetch(`${app.baseUrl}/api/projects/${id}/archive`, { method: "POST" });
|
|
const deleteRes = await fetch(`${app.baseUrl}/api/projects/${id}`, { method: "DELETE" });
|
|
expect(deleteRes.status).toBe(204);
|
|
} finally {
|
|
app.close();
|
|
}
|
|
});
|
|
|
|
test("项目 API 覆盖关键错误路径", async () => {
|
|
const app = createTestApp();
|
|
try {
|
|
const invalidIdRes = await fetch(`${app.baseUrl}/api/projects/-bad`);
|
|
expect(invalidIdRes.status).toBe(400);
|
|
|
|
const createOnce = await fetch(`${app.baseUrl}/api/projects`, {
|
|
body: JSON.stringify({ name: "错误项目" }),
|
|
headers: { "Content-Type": "application/json" },
|
|
method: "POST",
|
|
});
|
|
const id = ((await createOnce.json()) as { project: { id: string } }).project.id;
|
|
|
|
const duplicateRes = await fetch(`${app.baseUrl}/api/projects`, {
|
|
body: JSON.stringify({ name: "错误项目" }),
|
|
headers: { "Content-Type": "application/json" },
|
|
method: "POST",
|
|
});
|
|
expect(duplicateRes.status).toBe(409);
|
|
|
|
const deleteActiveRes = await fetch(`${app.baseUrl}/api/projects/${id}`, { method: "DELETE" });
|
|
expect(deleteActiveRes.status).toBe(409);
|
|
|
|
await fetch(`${app.baseUrl}/api/projects/${id}/archive`, { method: "POST" });
|
|
const updateArchivedRes = await fetch(`${app.baseUrl}/api/projects/${id}`, {
|
|
body: JSON.stringify({ name: "不可改" }),
|
|
headers: { "Content-Type": "application/json" },
|
|
method: "PATCH",
|
|
});
|
|
expect(updateArchivedRes.status).toBe(409);
|
|
|
|
const missingRes = await fetch(`${app.baseUrl}/api/projects/00000000-0000-4000-8000-000000000000`);
|
|
expect(missingRes.status).toBe(404);
|
|
} finally {
|
|
app.close();
|
|
}
|
|
});
|
|
});
|