feat(报销): 收敛只读内容与审批上下文 [09-02-reimbursement-readonly-content-consistency]

这个提交包含在:
2026-09-02 21:13:08 +08:00
父节点 dda625b15f
当前提交 e48514d542
共修改 20 个文件,包含 740 行新增和 47 行删除
@@ -387,6 +387,14 @@ test.describe("R7E 审批动作二次确认", () => {
submitter: { id: "owner", employeeNo: "E001", name: "申请人" },
},
leaveRequest: null,
history: Array.from({ length: 6 }, (_, index) => ({
id: `history-${index + 1}`,
action: index === 0 ? "submit" : "approve",
actorName: index === 0 ? "申请人" : `审批人 ${index}`,
actorEmployeeNo: `E${index + 1}`,
comment: index === 5 ? "这是一条用于验证右栏长意见自然换行且不会把审批动作推到历史之后的合成长意见。".repeat(3) : `第 ${index + 1} 条处理意见`,
createdAt: pendingTaskCreatedAt,
})),
};
const reimbursementDetail = {
id: reimbursementId,
@@ -425,6 +433,22 @@ test.describe("R7E 审批动作二次确认", () => {
await expect(page.getByRole("heading", { name: "报销单 BX-APPROVAL-001" })).toHaveCount(1);
await expect(page.getByText("客户拜访交通费", { exact: true })).toHaveCount(1);
await expect(page.getByTestId("reimbursement-approval-evidence")).toContainText("客户拜访打车");
await expect(page.getByTestId("reimbursement-amount-summary")).toContainText("明细合计");
await expect(page.getByTestId("reimbursement-amount-summary")).toContainText("¥180.00");
await expect(page.getByText("报销总额与明细汇总不一致,请联系管理员")).toHaveCount(0);
await expect(page.getByText("明细金额无法完整核对,请联系管理员")).toHaveCount(0);
await expect(page.getByTestId("workflow-task-approve")).toBeEnabled();
await expect(page.getByTestId("workflow-task-return-toggle")).toBeEnabled();
await expect(page.getByTestId("workflow-task-reject-toggle")).toBeEnabled();
await expect(page.getByTestId("workflow-task-transfer-menu-item")).toBeEnabled();
await expect(page.getByTestId("reimbursement-workflow-history").locator(".detail-timeline-item")).toHaveCount(6);
await expect(page.getByRole("heading", { name: "处理历史" })).toHaveCount(1);
const actionBox = await page.getByTestId("workflow-task-action-panel").boundingBox();
const historyBox = await page.getByTestId("reimbursement-workflow-history").boundingBox();
expect(actionBox).not.toBeNull();
expect(historyBox).not.toBeNull();
expect(actionBox!.y + actionBox!.height).toBeLessThanOrEqual(historyBox!.y);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth + 2)).toBe(true);
await expect(page.getByTestId("approval-unlinked-invoices")).toContainText("FP-002");
await expect(page.locator("#reimbursement-workspace-snapshot")).toContainText("该次提交未留存预算快照");
await expect(page.getByText("当前申报金额高于已读取的票面金额合计")).toBeVisible();
@@ -469,4 +493,146 @@ test.describe("R7E 审批动作二次确认", () => {
await expect(page.getByTestId("workflow-task-reject-toggle")).toBeDisabled();
await expect(page.getByTestId("workflow-task-transfer-menu-item")).toBeDisabled();
});
test("移动报销审批保留正文末尾历史且底部动作不遮挡", async ({ page }) => {
const reimbursementId = "reimbursement-mobile-history";
const taskDetail = {
...pendingTaskDetail,
workflowInstance: {
...pendingTaskDetail.workflowInstance,
entityType: "reimbursement",
contextData: { standardViolations: [], budgetCheck: null, budgetCheckStatus: "not_applicable", parseStatus: "ready" },
},
reimbursement: {
id: reimbursementId,
formNumber: "BX-MOBILE-HISTORY",
status: "reviewing",
reimbursementDate: "2026-09-01",
totalAmount: "100.00",
description: "移动端历史布局",
owner: { id: "owner", employeeNo: "E001", name: "申请人" },
submitter: { id: "owner", employeeNo: "E001", name: "申请人" },
},
leaveRequest: null,
history: Array.from({ length: 6 }, (_, index) => ({
id: `mobile-history-${index + 1}`,
action: index === 0 ? "submit" : "approve",
actorName: index === 0 ? "申请人" : `审批人 ${index}`,
actorEmployeeNo: `M${index + 1}`,
comment: index === 5 ? "用于验证移动端长意见换行与底部安全区的合成长意见。".repeat(4) : `移动端第 ${index + 1} 条意见`,
createdAt: pendingTaskCreatedAt,
})),
};
const reimbursementDetail = {
id: reimbursementId,
formNumber: "BX-MOBILE-HISTORY",
status: "reviewing",
reimbursementDate: "2026-09-01",
totalAmount: "100.00",
description: "移动端历史布局",
owner: { id: "owner", employeeNo: "E001", name: "申请人" },
items: [{
id: "mobile-item",
expenseTypeId: "mobile-type",
expenseDate: "2026-09-01",
amount: "100.00",
description: "移动端合成明细",
assetPriorApprovalBasis: null,
expenseType: { id: "mobile-type", code: "travel_meal", name: "伙食补助费", category: "travel", enabled: true, requiresInvoice: false },
}],
invoices: [],
attachments: [],
};
await mockWorkflowConfirmApis(page, { taskDetail, reimbursementDetail });
await page.setViewportSize({ width: 375, height: 667 });
await login(page, MANAGER);
await page.goto(`/mobile/approvals/${pendingTask.id}`);
await expect(page.getByTestId("reimbursement-amount-summary")).toContainText("¥100.00");
await expect(page.getByTestId("reimbursement-workflow-history").locator(".detail-timeline-item")).toHaveCount(6);
const assertMobileGeometry = async () => {
expect(await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth + 2)).toBe(true);
const lastHistory = page.getByTestId("reimbursement-workflow-history").locator(".detail-timeline-item").last();
await page.evaluate(() => {
const scroller = document.querySelector<HTMLElement>(".hyb-mobile-shell .overflow-y-auto");
scroller?.scrollTo(0, scroller.scrollHeight);
});
const historyBox = await lastHistory.boundingBox();
const stickyBox = await page.getByTestId("mobile-approvals-sticky-actions").boundingBox();
expect(historyBox).not.toBeNull();
expect(stickyBox).not.toBeNull();
expect(historyBox!.y + historyBox!.height).toBeLessThanOrEqual(stickyBox!.y);
};
await assertMobileGeometry();
await page.setViewportSize({ width: 667, height: 375 });
await assertMobileGeometry();
});
for (const scenario of [
{
name: "权威总额与明细汇总不一致",
itemAmount: "99.99",
expectedMessage: "报销总额与明细汇总不一致,请联系管理员",
},
{
name: "明细金额无法解析",
itemAmount: "invalid",
expectedMessage: "明细金额无法完整核对,请联系管理员",
},
]) {
test(`PC 报销${scenario.name}时四类审批入口 fail closed`, async ({ page }) => {
const reimbursementId = `reimbursement-integrity-${scenario.itemAmount === "invalid" ? "unavailable" : "mismatch"}`;
const taskDetail = {
...pendingTaskDetail,
workflowInstance: {
...pendingTaskDetail.workflowInstance,
entityType: "reimbursement",
contextData: { standardViolations: [], budgetCheck: null, budgetCheckStatus: "not_applicable", parseStatus: "ready" },
},
reimbursement: {
id: reimbursementId,
formNumber: "BX-INTEGRITY-001",
status: "reviewing",
reimbursementDate: "2026-09-01",
totalAmount: "100.00",
description: "合成完整性场景",
owner: { id: "owner", employeeNo: "E001", name: "申请人" },
submitter: { id: "owner", employeeNo: "E001", name: "申请人" },
},
leaveRequest: null,
};
const reimbursementDetail = {
id: reimbursementId,
formNumber: "BX-INTEGRITY-001",
status: "reviewing",
reimbursementDate: "2026-09-01",
totalAmount: "100.00",
description: "合成完整性场景",
owner: { id: "owner", employeeNo: "E001", name: "申请人" },
items: [{
id: "item-integrity",
expenseTypeId: "type-integrity",
expenseDate: "2026-09-01",
amount: scenario.itemAmount,
description: "完整性核对明细",
assetPriorApprovalBasis: null,
expenseType: { id: "type-integrity", code: "office_supplies", name: "办公用品", category: "office", enabled: true, requiresInvoice: false },
}],
invoices: [],
attachments: [],
};
await mockWorkflowConfirmApis(page, { taskDetail, reimbursementDetail });
await login(page, MANAGER);
await page.goto(`/workflow/tasks/${pendingTask.id}`);
await expect(page.getByTestId("reimbursement-amount-summary")).toContainText("¥100.00");
await expect(page.getByRole("alert").filter({ hasText: scenario.expectedMessage })).toBeVisible();
await expect(page.getByText("差额", { exact: true })).toHaveCount(0);
await expect(page.getByTestId("workflow-task-approve")).toBeDisabled();
await expect(page.getByTestId("workflow-task-return-toggle")).toBeDisabled();
await expect(page.getByTestId("workflow-task-reject-toggle")).toBeDisabled();
await expect(page.getByTestId("workflow-task-transfer-menu-item")).toBeDisabled();
});
}
});