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([]); const unknownValidation = "操作未完成,请核对页面提示;如仍无法继续,请联系管理员协助处理。"; expect(resolveErrorMessage(new ApiError({ status: 400, code: "VALID_001", message: "VALID_001: 请补充审批意见" }))).toBe(unknownValidation); expect(resolveErrorMessage(new ApiError({ status: 400, code: "VALID_001", message: "合成内部解析内容", requestId: "synthetic-request-12", details: { reasonCode: "synthetic.unknown_reason", file: "合成单元格" }, }))).toBe(`${unknownValidation} 排障编号:synthetic-request-12`); expect(resolveErrorMessage(new ApiError({ status: 400, code: "VALID_001", message: "合成内部解析内容", requestId: "/synthetic/private/path", }))).toBe(unknownValidation); expect(resolveErrorMessage(new ApiError({ status: 400, code: "VALID_001", message: "合成内部解析内容", details: { reasonCode: "user.import_file_invalid", file: "invalid_format" }, }))).toBe("导入文件格式损坏或无法解析,请重新导出为CSV或XLSX后再上传。"); 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 reimbursementApproval = source("src/pages/reimbursement-workspace/ReimbursementApprovalAdapter.tsx"); const mobileReimbursement = source("src/pages/MobileReimbursementDetailPage.tsx"); const reimbursementMobileShell = source("src/components/reimbursements/ReimbursementMobileWorkspaceShell.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 desktopShell = source("src/components/reimbursements/ReimbursementDesktopWorkspaceShell.tsx"); const mobileShell = source("src/components/reimbursements/ReimbursementMobileWorkspaceShell.tsx"); const desktopRoute = source("src/pages/WorkflowTaskRoutePage.tsx"); const mobileRoute = source("src/pages/MobileWorkflowTaskRoutePage.tsx"); const approvalAdapter = source("src/pages/reimbursement-workspace/ReimbursementApprovalAdapter.tsx"); const workflowDetail = source("src/pages/WorkflowTaskDetailPage.tsx"); const mobileApprovals = source("src/pages/MobileApprovalsPage.tsx"); for (const shell of [desktopShell, mobileShell]) { expect(shell).toContain("ReimbursementWorkspaceModel"); expect(shell).toContain("ReimbursementWorkspaceSlots"); expect(shell).toContain("ReimbursementEvidenceCapabilities"); expect(shell).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()'); });