import type { Page, Route } from "@playwright/test"; import { test, expect } from "./helpers/fixtures"; import { login, MANAGER } from "./helpers/auth"; import { TEST_IDS } from "./helpers/testIds"; test.use({ serviceWorkers: "block" }); function envelope(data: T) { return { requestId: "mock-workflow-confirm", data }; } async function fulfillJson(route: Route, data: T) { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope(data)), }); } const mockMe = { id: "employee-confirm-manager", employeeNo: "M-confirm", name: "李研发", email: "manager@hyinda.com", phone: null, roleCodes: ["manager"], profileCompleteness: { hasEmergencyContact: true, hasBankAccount: true, hasAvatar: false, hasIdNumber: false, score: 70, }, }; const pendingTaskCreatedAt = new Date().toISOString(); const pendingTask = { id: "task-confirm-1", instanceId: "instance-confirm-1", stepId: "manager-review", stepName: "主管审批", assigneeId: mockMe.id, assigneeRole: "manager", status: "pending", outcome: null, createdAt: pendingTaskCreatedAt, completedAt: null, workflowInstance: { entityType: "leave", entityId: "leave-confirm-1", currentStatus: "reviewing", }, businessSummary: { reference: "QJ-CONFIRM-001", title: "R7E 审批确认测试", applicantName: "Employee", amount: null, duration: "1 天", businessStatus: "reviewing", }, allowedActions: ["approve", "reject", "return", "transfer"], }; const pendingTaskDetail = { task: { id: pendingTask.id, instanceId: pendingTask.instanceId, stepId: pendingTask.stepId, stepName: pendingTask.stepName, assigneeId: pendingTask.assigneeId, assigneeRole: pendingTask.assigneeRole, status: pendingTask.status, outcome: null, comment: null, createdAt: pendingTask.createdAt, completedAt: null, }, workflowInstance: { id: pendingTask.instanceId, entityType: "leave", currentStatus: "reviewing", currentStepId: "manager-review", contextData: { standardViolations: [] }, startedAt: pendingTaskCreatedAt, completedAt: null, }, reimbursement: null, leaveRequest: { id: "leave-confirm-1", status: "reviewing", startDate: "2026-06-01", endDate: "2026-06-01", startSession: "am", endSession: "pm", requestedDays: 1, reason: "R7E 审批动作确认", attachment: null, leaveType: { id: "annual", code: "annual", name: "年假" }, owner: { id: "employee-confirm-applicant", employeeNo: "E-confirm", name: "陈工程" }, }, purchaseRequest: null, travelRequest: null, allowedActions: pendingTask.allowedActions, history: [], }; function navigationItem(key: string, href: string, label: string, icon: string, placement = "main") { return { key, href, label, icon, placement, permissionCodes: [], featureFlagKey: null }; } async function mockWorkflowConfirmApis(page: Page) { const actionCalls = { approve: 0, reject: 0, return: 0, }; await page.route((url) => url.pathname.startsWith("/api/"), async (route) => { const request = route.request(); const url = new URL(request.url()); const path = url.pathname; if (path === "/api/auth/sso/providers") { await fulfillJson(route, []); return; } if (path === "/api/auth/wecom/config") { await fulfillJson(route, { enabled: true, entryEnabled: true, oauthEnabled: true, corpId: "wx-test-corp", agentId: "1000002", loginUrl: "/api/auth/wecom/login?returnTo=%2Fportal", bindEnabled: true, defaultReturnTo: "/portal", status: "ready", statusReasons: [], }); return; } if (path === "/api/auth/login" || path === "/api/auth/refresh") { await route.fulfill({ status: 200, contentType: "application/json", headers: { "set-cookie": "has_session=1; Path=/; SameSite=Lax" }, body: JSON.stringify(envelope({ accessToken: "mock-workflow-confirm-token", expiresIn: 900, mustChangePassword: false, })), }); return; } if (path === "/api/auth/logout") { await route.fulfill({ status: 200, contentType: "application/json", headers: { "set-cookie": "has_session=; Path=/; Max-Age=0" }, body: JSON.stringify(envelope({})), }); return; } if (path === "/api/me") { await fulfillJson(route, mockMe); return; } if (path === "/api/system/navigation") { await fulfillJson(route, { workspace: { id: "workspace-confirm", persisted: true, profile: "small_team_internal", orgMode: "single_company", primaryEntryMode: "password", notificationStrategy: "in_app", devicePriority: "desktop", moduleVisibility: {}, createdAt: null, updatedAt: null, updatedBy: null, }, sections: { main: [ navigationItem("portal", "/portal", "工作台", "home_app_logo"), navigationItem("workflow", "/workflow/tasks", "待我审批", "fact_check"), ], admin: [], backup: [], }, recommendedRolePresets: [], }); return; } if (path === "/api/notifications") { await fulfillJson(route, { page: 1, pageSize: Number(url.searchParams.get("pageSize") ?? 5), total: 0, unreadOnly: url.searchParams.get("unreadOnly") === "1", unreadCount: 0, items: [], }); return; } if (path === "/api/portal/home") { await fulfillJson(route, { hero: { title: "工作台", subtitle: "审批动作确认测试" }, metrics: [], todos: [], notifications: [], }); return; } if (path === "/api/workflow/tasks/summary") { await fulfillJson(route, { scope: "mine", pendingTotal: actionCalls.approve > 0 ? 0 : 1, overdueTotal: 0, earliestPendingAt: pendingTask.createdAt, averagePendingAgeHours: 1, byEntityType: [], byStep: [], byAssigneeRole: [], }); return; } if (path === "/api/workflow/tasks") { const status = url.searchParams.get("status") ?? "pending"; const items = status === "pending" && actionCalls.approve === 0 ? [pendingTask] : []; await fulfillJson(route, { page: 1, pageSize: Number(url.searchParams.get("pageSize") ?? 20), total: items.length, items, }); return; } const actionMatch = path.match(/^\/api\/workflow\/tasks\/[^/]+\/(approve|reject|return)$/); if (actionMatch) { const action = actionMatch[1] as keyof typeof actionCalls; actionCalls[action] += 1; await fulfillJson(route, { ok: true }); return; } if (/^\/api\/workflow\/tasks\/[^/]+$/.test(path)) { await fulfillJson(route, pendingTaskDetail); return; } if (path === "/api/workflow/approvers") { await fulfillJson(route, { page: 1, pageSize: 100, total: 1, items: [{ id: "employee-transfer-1", employeeNo: "E-transfer", name: "周审批" }], }); return; } await fulfillJson(route, {}); }); return actionCalls; } test.describe("R7E 审批动作二次确认", () => { test("PC 列表页不提供企微主办请假快捷通过", async ({ page }) => { const actionCalls = await mockWorkflowConfirmApis(page); await login(page, MANAGER); await page.goto("/workflow/tasks"); await expect(page.getByTestId(TEST_IDS.workflow.tasksTable)).toBeVisible(); const taskRow = page.getByTestId(TEST_IDS.workflow.taskRow).filter({ hasText: "QJ-CONFIRM-001" }); await expect(taskRow).toHaveCount(1); await expect(taskRow.getByTestId(TEST_IDS.workflow.detailLink)).toBeVisible(); await expect(taskRow.getByTestId(TEST_IDS.workflow.quickApprove)).toHaveCount(0); expect(actionCalls.approve).toBe(0); }); test("PC 审批详情通过、驳回、退回都先解释动作后果", async ({ page }) => { const actionCalls = await mockWorkflowConfirmApis(page); await login(page, MANAGER); await page.goto(`/workflow/tasks/${pendingTask.id}`); await expect(page.getByTestId("workflow-task-overview")).toBeVisible(); await expect(page.getByTestId("workflow-action-consequence-help")).toContainText("退回:"); await expect(page.getByTestId("workflow-action-consequence-help")).toContainText("驳回:"); await page.getByTestId("workflow-task-approve").click(); await expect(page.getByTestId("workflow-action-confirm-dialog")).toContainText("确认后将进入下一审批节点"); await page.getByTestId("workflow-action-confirm-cancel").click(); expect(actionCalls.approve).toBe(0); await page.getByTestId("workflow-task-reject-toggle").click(); await page.getByTestId("workflow-task-reject-comment").fill("资料不符合要求,请重新发起。"); await page.getByTestId("workflow-task-reject").click(); await expect(page.getByTestId("workflow-action-confirm-dialog")).toContainText("流程将终止"); await page.getByTestId("workflow-action-confirm-cancel").click(); expect(actionCalls.reject).toBe(0); await page.locator("summary", { hasText: "更多" }).click(); await page.getByTestId("workflow-task-transfer-menu-item").click(); await expect(page.getByTestId("workflow-task-transfer-disabled-reason")).toHaveText("请先选择目标审批人。"); await expect(page.getByTestId("workflow-task-transfer")).toBeDisabled(); const transferApprover = page.getByTestId("workflow-task-transfer-approver"); const transferReason = page.getByTestId("workflow-task-transfer-reason"); await transferApprover.focus(); await expect(transferApprover).toBeFocused(); await page.keyboard.press("Tab"); await expect(transferReason).toBeFocused(); await transferApprover.selectOption("employee-transfer-1"); await transferReason.fill("理由太短"); await expect(page.getByTestId("workflow-task-transfer-disabled-reason")).toContainText("至少 10 个字"); await expect(page.getByTestId("workflow-task-transfer")).toBeDisabled(); await transferReason.fill("当前事项需转交对应审批人继续处理"); await expect(page.getByTestId("workflow-task-transfer-disabled-reason")).toHaveText("转审理由已满足要求。"); await expect(page.getByTestId("workflow-task-transfer")).toBeEnabled(); await transferReason.focus(); await page.keyboard.press("Tab"); await expect(page.getByTestId("workflow-task-transfer")).toBeFocused(); await page.getByTestId("workflow-task-return-toggle").click(); await page.getByTestId("workflow-task-return-comment").fill("请补充附件后重新提交。"); await page.getByTestId("workflow-task-return").click(); await expect(page.getByTestId("workflow-action-confirm-dialog")).toContainText("回到申请人修改态"); await page.getByTestId("workflow-action-confirm-submit").click(); await expect.poll(() => actionCalls.return).toBe(1); }); });