feat(08-05-interaction-pattern-unification): 统一前端交互与反馈模式
这个提交包含在:
@@ -0,0 +1,101 @@
|
||||
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("<MobileStickyActionBar");
|
||||
expect(mobileApprovals).toContain("<MobileStickyActionBar");
|
||||
expect(mobileReimbursement).toContain("<MobileStickyActionBar");
|
||||
expect(mobileApprovals).not.toContain('className="mobile-sticky-action-surface fixed');
|
||||
expect(mobileReimbursement).not.toContain('className="mobile-sticky-action-surface fixed');
|
||||
});
|
||||
|
||||
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()');
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户