收敛报销退回动作与审批布局 [09-04-approval-return-reject-actions]

这个提交包含在:
2026-09-04 10:31:32 +08:00
父节点 59df0594bb
当前提交 8551601818
共修改 17 个文件,包含 520 行新增和 56 行删除
@@ -231,7 +231,7 @@ test("E2E 移动审批:报销证据不进入 IndexedDB 且离线动作 fail cl
attendanceCorrection: null,
purchaseRequest: null,
travelRequest: null,
allowedActions: ["approve", "reject", "return"],
allowedActions: ["approve", "return"],
history: [],
};
const reimbursementDetail = {
@@ -351,9 +351,9 @@ test("E2E 移动审批:报销证据不进入 IndexedDB 且离线动作 fail cl
}
await page.context().setOffline(true);
await expect(page.getByText("报销审批需联网核对完整报销内容;当前不能通过、退回、驳回或转审。")).toBeVisible();
await expect(page.getByText("报销审批需联网核对完整报销内容;当前不能通过、退回修改或转审。")).toBeVisible();
await expect(page.getByTestId("mobile-approvals-approve")).toBeDisabled();
await expect(page.getByTestId("mobile-approvals-return")).toBeDisabled();
await expect(page.getByTestId("mobile-approvals-reject")).toBeDisabled();
await expect(page.getByTestId("mobile-approvals-reject")).toHaveCount(0);
await expect(page.getByTestId("mobile-approvals-transfer")).toBeDisabled();
});
@@ -77,7 +77,7 @@ const workflowTask = {
duration: null,
businessStatus: "reviewing",
},
allowedActions: ["approve", "reject", "return", "transfer"],
allowedActions: ["approve", "return", "transfer"],
};
const reimbursementDetail = {
@@ -52,7 +52,7 @@ test("报销审批五类阻断原因由同一投影给出且正常态保持安
};
expect(resolveReimbursementApprovalBlockReason(base)).toBeNull();
expect(resolveReimbursementApprovalBlockReason({ ...base, online: false })).toBe("报销审批需联网核对完整报销内容;当前不能通过、退回、驳回或转审。");
expect(resolveReimbursementApprovalBlockReason({ ...base, online: false })).toBe("报销审批需联网核对完整报销内容;当前不能通过、退回修改或转审。");
expect(resolveReimbursementApprovalBlockReason({ ...base, detailState: "loading" })).toBe("正在加载完整报销内容和票据;加载完成前不能作出审批决定。");
expect(resolveReimbursementApprovalBlockReason({ ...base, detailState: "error" })).toBe("报销内容未加载完整,暂不能审批,请刷新后重试");
expect(resolveReimbursementApprovalBlockReason({ ...base, snapshotParseStatus: "invalid" })).toBe("提交时检查记录无法解析,暂时不能审批;请联系系统管理员核对。");
@@ -73,6 +73,13 @@ test("动作面板复用唯一阻断投影且预算三态判断保持原合同",
expect(adapter).not.toContain("内容未就绪,审批动作已停用");
expect(adapter).not.toContain("statusSummary");
expect(adapter).toContain("actionError || activeSecondaryAction ?");
expect(adapter).toContain('data-testid="reimbursement-approval-primary-actions"');
expect(adapter).toContain('className="grid grid-cols-2 gap-2"');
expect(adapter).toContain("退回修改");
expect(adapter).not.toContain("rejectWorkflowTask");
expect(adapter).not.toContain('allowedActions.has("reject")');
expect(adapter).not.toContain("workflow-task-reject-toggle");
expect(adapter).not.toContain("mobile-approvals-reject");
});
test("旧审批层叠 class 与更多菜单样式无定义或消费者", () => {
@@ -458,6 +458,7 @@ test.describe("R7E 审批动作二次确认", () => {
submitter: { id: "owner", employeeNo: "E001", name: "申请人" },
},
leaveRequest: null,
allowedActions: ["approve", "return", "transfer"],
history: Array.from({ length: 6 }, (_, index) => ({
id: `history-${index + 1}`,
action: index === 0 ? "submit" : "approve",
@@ -515,8 +516,18 @@ test.describe("R7E 审批动作二次确认", () => {
await expect(actionPanel.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-reject-toggle")).toHaveCount(0);
await expect(page.getByTestId("workflow-task-transfer-menu-item")).toBeEnabled();
const primaryActions = page.getByTestId("reimbursement-approval-primary-actions");
await expect(primaryActions.getByRole("button")).toHaveCount(2);
await expect(primaryActions.getByRole("button", { name: "通过" })).toBeVisible();
await expect(primaryActions.getByRole("button", { name: "退回修改" })).toBeVisible();
const approveBox = await page.getByTestId("workflow-task-approve").boundingBox();
const returnBox = await page.getByTestId("workflow-task-return-toggle").boundingBox();
expect(approveBox).not.toBeNull();
expect(returnBox).not.toBeNull();
expect(Math.abs(approveBox!.height - returnBox!.height)).toBeLessThanOrEqual(1);
expect(approveBox!.y).toBe(returnBox!.y);
await expect(page.getByTestId("reimbursement-workflow-history").locator(".detail-timeline-item")).toHaveCount(6);
await expect(page.getByRole("heading", { name: "处理历史" })).toHaveCount(1);
const actionBox = await actionPanel.boundingBox();
@@ -544,7 +555,7 @@ test.describe("R7E 审批动作二次确认", () => {
await expect(page.getByTestId("workflow-task-return-comment")).toHaveValue(/【明细:交通费 · 2026-09-01 · ¥180\.00】/);
});
test("PC 报销内容加载失败时四类审批入口 fail closed", async ({ page }) => {
test("PC 报销内容加载失败时全部可见审批入口 fail closed", async ({ page }) => {
const taskDetail = {
...pendingTaskDetail,
workflowInstance: {
@@ -558,6 +569,7 @@ test.describe("R7E 审批动作二次确认", () => {
owner: { id: "owner", employeeNo: "E001", name: "申请人" }, submitter: { id: "owner", employeeNo: "E001", name: "申请人" },
},
leaveRequest: null,
allowedActions: ["approve", "return", "transfer"],
};
await mockWorkflowConfirmApis(page, { taskDetail, reimbursementDetailFailure: true });
@@ -566,7 +578,7 @@ test.describe("R7E 审批动作二次确认", () => {
await expect(page.getByRole("alert")).toContainText("报销内容未加载完整,暂不能审批,请刷新后重试");
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-reject-toggle")).toHaveCount(0);
await expect(page.getByTestId("workflow-task-transfer-menu-item")).toBeDisabled();
});
@@ -590,6 +602,7 @@ test.describe("R7E 审批动作二次确认", () => {
submitter: { id: "owner", employeeNo: "E001", name: "申请人" },
},
leaveRequest: null,
allowedActions: ["approve", "return", "transfer"],
history: Array.from({ length: 6 }, (_, index) => ({
id: `mobile-history-${index + 1}`,
action: index === 0 ? "submit" : "approve",
@@ -627,6 +640,10 @@ test.describe("R7E 审批动作二次确认", () => {
await expect(page.getByTestId("reimbursement-amount-summary")).toContainText("¥100.00");
await expect(page.locator("#reimbursement-workspace-snapshot")).toHaveCount(0);
await expect(page.getByTestId("reimbursement-workflow-history").locator(".detail-timeline-item")).toHaveCount(6);
const stickyActions = page.getByTestId("mobile-approvals-sticky-actions");
await expect(stickyActions.getByRole("button", { name: "退回修改" })).toBeVisible();
await expect(stickyActions.getByRole("button", { name: "通过" })).toBeVisible();
await expect(page.getByTestId("mobile-approvals-reject")).toHaveCount(0);
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();
@@ -657,7 +674,7 @@ test.describe("R7E 审批动作二次确认", () => {
expectedMessage: "明细金额无法完整核对,请联系管理员",
},
]) {
test(`PC 报销${scenario.name}时四类审批入口 fail closed`, async ({ page }) => {
test(`PC 报销${scenario.name}时全部可见审批入口 fail closed`, async ({ page }) => {
const reimbursementId = `reimbursement-integrity-${scenario.itemAmount === "invalid" ? "unavailable" : "mismatch"}`;
const taskDetail = {
...pendingTaskDetail,
@@ -677,6 +694,7 @@ test.describe("R7E 审批动作二次确认", () => {
submitter: { id: "owner", employeeNo: "E001", name: "申请人" },
},
leaveRequest: null,
allowedActions: ["approve", "return", "transfer"],
};
const reimbursementDetail = {
id: reimbursementId,
@@ -716,7 +734,7 @@ test.describe("R7E 审批动作二次确认", () => {
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-reject-toggle")).toHaveCount(0);
await expect(page.getByTestId("workflow-task-transfer-menu-item")).toBeDisabled();
});
}