汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。 统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。 纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。 关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。 预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。 完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
151 行
8.5 KiB
TypeScript
151 行
8.5 KiB
TypeScript
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("<MobileStickyActionBar");
|
|
expect(mobileApprovals).toContain("<MobileStickyActionBar");
|
|
expect(reimbursementApproval).toContain("<MobileStickyActionBar");
|
|
expect(mobileReimbursement).toContain("<MobileStickyActionBar");
|
|
expect(reimbursementMobileShell).toContain("MOBILE_STICKY_ACTION_PAGE_CLASS");
|
|
expect(reimbursementMobileShell).toContain("slots.mobileActions ? MOBILE_STICKY_ACTION_PAGE_CLASS : undefined");
|
|
expect(mobileApprovals).not.toContain('className="mobile-sticky-action-surface fixed');
|
|
expect(reimbursementApproval).not.toContain('className="mobile-sticky-action-surface fixed');
|
|
expect(mobileReimbursement).not.toContain('className="mobile-sticky-action-surface fixed');
|
|
});
|
|
|
|
test("B-2b 报销工作台双端共享内容合同且 task detail 只由路由查询一次", () => {
|
|
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("<ReimbursementItemEvidenceList");
|
|
expect(shell.indexOf("slots.beforeEvidence")).toBeLessThan(shell.indexOf("<ReimbursementItemEvidenceList"));
|
|
expect(shell.indexOf("<ReimbursementItemEvidenceList")).toBeLessThan(shell.indexOf("slots.afterEvidence"));
|
|
}
|
|
|
|
expect(desktopShell).not.toContain("slots.timeline");
|
|
expect(desktopShell.indexOf("slots.actionPanel")).toBeLessThan(desktopShell.indexOf("slots.asideContext"));
|
|
expect(mobileShell.indexOf("slots.afterEvidence")).toBeLessThan(mobileShell.indexOf("slots.timeline"));
|
|
|
|
expect(desktopRoute.match(/fetchWorkflowTaskDetail\(/g)).toHaveLength(1);
|
|
expect(mobileRoute.match(/fetchWorkflowTaskDetail\(/g)).toHaveLength(1);
|
|
expect(approvalAdapter).not.toContain("fetchWorkflowTaskDetail");
|
|
expect(workflowDetail).not.toContain("ReimbursementItemEvidenceList");
|
|
expect(workflowDetail).not.toContain("提交时检查");
|
|
expect(mobileApprovals).not.toContain("selectedTask?.reimbursement");
|
|
expect(mobileApprovals).not.toContain("reimbursementDecisionReady");
|
|
});
|
|
|
|
test("R1/R6 未知状态和个人事项状态不回落到技术枚举", () => {
|
|
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()');
|
|
});
|