import { readFileSync, readdirSync } from "node:fs"; import path from "node:path"; import { expect, test } from "@playwright/test"; import { ApiError } from "../../src/api/types"; import { resolveErrorMessage } from "../../src/lib/resolveErrorMessage"; function source(relativePath: string) { return readFileSync(path.resolve(process.cwd(), relativePath), "utf8"); } function sourceFiles(relativeDirectory: string): string[] { const absoluteDirectory = path.resolve(process.cwd(), relativeDirectory); return readdirSync(absoluteDirectory, { withFileTypes: true }).flatMap((entry) => { const relativePath = path.join(relativeDirectory, entry.name); if (entry.isDirectory()) { return sourceFiles(relativePath); } return /\.(ts|tsx)$/.test(entry.name) ? [relativePath] : []; }); } test("R12 普通用户错误消息只经过共享解析入口且不显示内部 code", () => { const resolver = source("src/lib/resolveErrorMessage.ts"); expect(resolver).toContain("mapApiErrorMessage(error.code, error.details, error.message)"); expect(resolver).not.toMatch(/`\$\{error\.code\}:/); const ordinaryFiles = ["src/pages", "src/components", "src/lib"] .flatMap(sourceFiles) .filter((file) => file !== "src/pages/AuditLogsPage.tsx"); const residuals = ordinaryFiles.filter((file) => { const text = source(file); return /\$\{[^}]*\.code\}:|\{[^}\n]*\.code\}:/.test(text); }); const directMapperConsumers = ordinaryFiles.filter((file) => { if (file === "src/lib/resolveErrorMessage.ts") return false; return source(file).includes("mapApiErrorMessage"); }); const rawThrownMessageConsumers = ordinaryFiles.filter((file) => { if (file === "src/lib/resolveErrorMessage.ts") return false; return /\b(?:error|err)\.message\b/.test(source(file)); }); expect(residuals).toEqual([]); expect(directMapperConsumers).toEqual([]); expect(rawThrownMessageConsumers).toEqual([]); expect(resolveErrorMessage(new ApiError({ status: 400, code: "VALID_001", message: "VALID_001: 请补充审批意见" }))).toBe("请补充审批意见"); expect(resolveErrorMessage(new Error("500: 保存失败"), "保存失败,请稍后重试")).toBe("保存失败"); expect(resolveErrorMessage(new Error("Failed to fetch /api/workflow/tasks"), "加载审批任务失败")).toBe("加载审批任务失败"); expect(resolveErrorMessage(new Error("读取失败 /tmp/private/payload.json"), "读取资料失败")).toBe("读取资料失败"); }); test("R10 移动审批与移动报销复用同一底部动作容器", () => { const sharedActionBar = source("src/components/patterns/MobileStickyActionBar.tsx"); const businessEditTemplate = source("src/components/patterns/BusinessEditTemplate.tsx"); const mobileApprovals = source("src/pages/MobileApprovalsPage.tsx"); const mobileReimbursement = source("src/pages/MobileReimbursementDetailPage.tsx"); expect(sharedActionBar).toContain("mobile-sticky-action-surface fixed inset-x-0 bottom-0"); expect(sharedActionBar).toContain("env(safe-area-inset-bottom,0px)"); expect(businessEditTemplate).toContain("MOBILE_STICKY_ACTION_PAGE_CLASS"); expect(businessEditTemplate).toContain(" { const statusBadge = source("src/components/common/StatusBadge.tsx"); const todosPage = source("src/pages/TodosPage.tsx"); expect(statusBadge).toContain('label: "未知状态"'); expect(statusBadge).not.toContain('label: status, className: "badge-neutral"'); expect(todosPage).toContain('todo: "待处理"'); expect(todosPage).toContain('doing: "进行中"'); expect(todosPage).toContain('done: "已完成"'); expect(todosPage).toContain('cancelled: "已取消"'); }); test("R4 WelcomeDialog 只在存在权限过滤后的入口时打开", () => { const portalHome = source("src/pages/PortalHomePage.tsx"); expect(portalHome).toContain("open={showWelcome && welcomeLinks.length > 0}"); expect(portalHome).toContain("if (roleCapabilities.canApproveTasks && hasAnyPermission(permissions, APPROVAL_PORTAL_PERMISSIONS))"); }); test("R9/R11 工作台加载失败提供重试且主要入口具有焦点样式", () => { const portalHome = source("src/pages/PortalHomePage.tsx"); expect(portalHome).toContain('data-testid="portal-load-error"'); expect(portalHome).toContain('data-testid="portal-load-retry"'); expect(portalHome).toContain("portalQuery.refetch()"); expect(portalHome).toContain("focus-visible:ring-2 focus-visible:ring-blue-600"); }); test("R14 差旅列表不再自动选择第一条记录", () => { const travelRequests = source("src/pages/TravelRequestsPage.tsx"); expect(travelRequests).not.toContain("listQuery.data?.items?.[0]?.id"); expect(travelRequests).toContain('const [searchParams, setSearchParams] = useSearchParams()'); });