import type { Page, Route } from "@playwright/test"; import { test, expect } from "./helpers/fixtures"; import { login } from "./helpers/auth"; test.use({ serviceWorkers: "block" }); function envelope(data: T) { return { requestId: "mock-system-action-confirm", data }; } async function fulfillJson(route: Route, data: T) { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope(data)), }); } function navigationItem(key: string, href: string, label: string, icon: string, placement = "main") { return { key, href, label, icon, placement, permissionCodes: [], featureFlagKey: null }; } const user = { identifier: "action-confirm@hyinda.com", password: "ActionConfirm1234", name: "Action Confirm", }; const me = { id: "employee-action-confirm", employeeNo: "E-action-confirm", name: user.name, email: user.identifier, phone: null, roleCodes: ["manager", "finance"], permissions: [ "expense.create", "expense.view_own", "expense.delete_own", "expense.approve", "payment.batch.manage", "risk.review.manage", ], profileCompleteness: { hasEmergencyContact: true, hasBankAccount: true, hasAvatar: false, hasIdNumber: false, score: 80, }, }; const reimbursementId = "reimbursement-action-confirm"; const paymentBatchId = "payment-batch-action-confirm"; function reimbursementListItem() { return { id: reimbursementId, formNumber: "BX-ACTION-001", status: "draft", reimbursementDate: "2026-05-15", totalAmount: "88.00", description: "R7E 高风险确认测试", payment: { paymentVoucherNo: null, paymentDate: null, paymentChannel: null, paidAt: null, paidBy: null, }, owner: { id: me.id, employeeNo: me.employeeNo, name: me.name }, submitter: { id: me.id, employeeNo: me.employeeNo, name: me.name }, project: null, pendingWorkflow: null, createdAt: "2026-05-15T08:00:00.000Z", updatedAt: "2026-05-15T08:10:00.000Z", }; } function paymentBatch(status: "draft" | "reviewed" = "reviewed") { return { id: paymentBatchId, batchNo: "PAY-ACTION-001", entityType: "reimbursement", name: "R7E 高风险付款确认", status, defaultPaymentDate: "2026-05-15", defaultPaymentChannel: "bank", voucherPrefix: "R7E", totalAmount: "88.00", highRisk: false, reviewRequirement: { thresholdAmount: "1000.00", requiredReviewCount: 1, completedReviewCount: status === "draft" ? 0 : 1, nextStep: status === "draft" ? "first_review" : "ready_for_execute", makerCheckerRequired: false, reviewerPermission: "risk.review.manage", }, reviewComment: null, secondaryReviewComment: null, executionSummaryJson: null, createdAt: "2026-05-15T08:00:00.000Z", createdBy: me.id, reviewedAt: status === "draft" ? null : "2026-05-15T08:10:00.000Z", reviewedBy: status === "draft" ? null : me.id, secondaryReviewedAt: null, secondaryReviewedBy: null, executedAt: null, executedBy: null, cancelledAt: null, cancelledBy: null, items: [ { id: "payment-batch-item-action-confirm", entityId: reimbursementId, entityCode: "BX-ACTION-001", entityTitle: "R7E 高风险确认测试", amount: "88.00", currencyCode: "CNY", paymentVoucherNoOverride: null, paymentDateOverride: null, paymentChannelOverride: null, status: "pending", failureCode: null, failureMessage: null, snapshotJson: {}, createdAt: "2026-05-15T08:00:00.000Z", openRiskFlags: [], }, ], }; } async function mockSystemActionApis(page: Page) { const calls = { deleteReimbursement: 0, exportReimbursements: 0, executePaymentBatch: 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-system-action-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, me); return; } if (path === "/api/system/navigation") { await fulfillJson(route, { workspace: { id: "workspace-action-confirm", persisted: true, profile: "small_team_internal", orgMode: "single_company", primaryEntryMode: "password", enabledGroups: ["main"], quickActions: [], }, items: [ navigationItem("portal", "/portal", "首页", "home"), navigationItem("reimbursements", "/reimbursements", "报销", "receipt"), navigationItem("payment-batches", "/payment-batches", "付款", "credit_card"), ], }); return; } if (path === "/api/reimbursements" && request.method() === "GET") { await fulfillJson(route, { page: 1, pageSize: 20, total: 1, scope: "own", items: [reimbursementListItem()] }); return; } if (path === `/api/reimbursements/${reimbursementId}` && request.method() === "DELETE") { calls.deleteReimbursement += 1; await fulfillJson(route, { id: reimbursementId, deleted: true }); return; } if (path === "/api/exports/reimbursements") { calls.exportReimbursements += 1; await route.fulfill({ status: 200, contentType: url.searchParams.get("format") === "xlsx" ? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" : "text/csv", headers: { "content-disposition": "attachment; filename=\"reimbursements.csv\"" }, body: "formNumber,totalAmount\nBX-ACTION-001,88.00\n", }); return; } if (path === "/api/reimbursement-templates") { await fulfillJson(route, { page: 1, pageSize: 20, total: 0, items: [] }); return; } if (path === "/api/reimbursements/linkable-travel-requests") { await fulfillJson(route, { ownerEmployeeId: me.id, items: [] }); return; } if (path === "/api/payment-batches" && request.method() === "GET") { await fulfillJson(route, { items: [paymentBatch("reviewed")] }); return; } if (path === `/api/payment-batches/${paymentBatchId}/execute` && request.method() === "POST") { calls.executePaymentBatch += 1; await fulfillJson(route, { item: paymentBatch("reviewed") }); return; } await fulfillJson(route, {}); }); return calls; } test.describe("R7E 全站高风险操作二次确认", () => { test("报销删除和敏感导出必须确认后才调用接口", async ({ page }) => { const calls = await mockSystemActionApis(page); page.on("dialog", (dialog) => { throw new Error(`不应出现浏览器原生弹窗:${dialog.message()}`); }); await login(page, user); await page.goto("/reimbursements"); await expect(page.getByTestId("reimbursements-table")).toBeVisible(); await page.getByTestId("reimbursement-row").getByRole("button", { name: "更多" }).click(); await page.getByRole("menuitem", { name: "删除草稿" }).click(); await expect(page.getByTestId("action-confirm-dialog")).toContainText("删除后无法恢复"); expect(calls.deleteReimbursement).toBe(0); await page.getByTestId("action-confirm-cancel").click(); expect(calls.deleteReimbursement).toBe(0); await page.getByTestId("reimbursement-row").getByRole("button", { name: "更多" }).click(); await page.getByRole("menuitem", { name: "删除草稿" }).click(); await page.getByTestId("action-confirm-submit").click(); await expect.poll(() => calls.deleteReimbursement).toBe(1); await page.getByRole("button", { name: "高级 / 台账" }).click(); await page.getByTestId("reimbursements-export").click(); await page.getByTestId("reimbursements-export-csv").click(); await expect(page.getByTestId("action-confirm-dialog")).toContainText("导出文件可能包含业务或个人信息"); expect(calls.exportReimbursements).toBe(0); await page.getByTestId("action-confirm-submit").click(); await expect.poll(() => calls.exportReimbursements).toBe(1); }); test("付款批次执行必须确认后才调用接口", async ({ page }) => { const calls = await mockSystemActionApis(page); page.on("dialog", (dialog) => { throw new Error(`不应出现浏览器原生弹窗:${dialog.message()}`); }); await login(page, user); await page.goto("/payment-batches"); await expect(page.getByText("PAY-ACTION-001").first()).toBeVisible(); await page.getByRole("button", { name: "执行" }).click(); await expect(page.getByTestId("action-confirm-dialog")).toContainText("确认后将执行付款批次"); expect(calls.executePaymentBatch).toBe(0); await page.getByTestId("action-confirm-cancel").click(); expect(calls.executePaymentBatch).toBe(0); await page.getByRole("button", { name: "执行" }).click(); await page.getByTestId("action-confirm-submit").click(); await expect.poll(() => calls.executePaymentBatch).toBe(1); }); });