import { mkdirSync } from "node:fs"; import path from "node:path"; import type { Page, Route } from "@playwright/test"; import { expect, test } from "./helpers/fixtures"; test.use({ serviceWorkers: "block" }); const outputDir = path.resolve(process.cwd(), "../output/playwright/pc-finance-admin-visual-wave4"); const taskOutputDir = path.resolve( process.cwd(), "../output/playwright/07-18-small-team-internal-readiness", ); const visualUser = { identifier: "visual-wave4@example.test", password: "VisualWave4Password1234", name: "Wave4 Admin", }; const allPermissions = [ "expense.create", "expense.submit", "expense.view_own", "expense.view_all", "expense.approve", "invoice.upload", "invoice.view_own", "invoice.manage", "invoice.verify", "expense.finance.override_duplicate", "expense.proxy_submit", "payment.batch.manage", "risk.review.manage", "travel.payment.manage", "system.user.manage", "system.users", "system.settings", "system.audit.view", "masterdata.manage", "notification.channel.manage", ]; const visualMe = { id: "employee-wave4", employeeNo: "HYD-W4", name: visualUser.name, email: visualUser.identifier, phone: null, roleCodes: ["admin", "finance", "finance_mgr", "administration", "manager"], permissions: allPermissions, profileCompleteness: { hasEmergencyContact: true, hasBankAccount: true, hasAvatar: false, hasIdNumber: false, score: 92, }, notificationPushEnabled: true, departmentId: "dept-wave4", }; function envelope(data: T) { return { requestId: "mock-pc-wave4", data }; } async function fulfillJson(route: Route, data: T) { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope(data)), }); } async function fulfillApiError(route: Route, status: number, details: Record) { await route.fulfill({ status, contentType: "application/json", body: JSON.stringify({ requestId: "mock-pc-wave4", error: { code: "VALID_001", message: "参数校验失败", details, }, }), }); } function navigationItem(key: string, href: string, label: string, icon: string, placement = "main") { return { key, href, label, icon, placement, permissionCodes: [], featureFlagKey: null }; } const riskFlag = { id: "risk-wave4-1", entityType: "reimbursement", entityId: "reim-wave4-1", riskCode: "budget_overspend", level: "medium", title: "预算超额待复核", detail: "Wave4 视觉验证用风险摘要", source: "mock", status: "open", payloadJson: {}, createdAt: "2026-05-27T09:20:00.000Z", createdBy: visualMe.id, resolvedAt: null, resolvedBy: null, }; function paymentBatch(status: "draft" | "reviewed" | "failed", suffix: string) { const failed = status === "failed"; return { id: `batch-wave4-${suffix}`, batchNo: `PB20260527-${suffix}`, entityType: "reimbursement", name: failed ? "异常付款批次" : "五月报销付款批次", status, defaultPaymentDate: "2026-05-27", defaultPaymentChannel: "bank", voucherPrefix: "PB", totalAmount: failed ? "860.00" : "2860.00", highRisk: failed, reviewRequirement: { thresholdAmount: "5000.00", requiredReviewCount: status === "draft" ? 1 : 2, completedReviewCount: status === "draft" ? 0 : 1, nextStep: status === "reviewed" ? "ready_for_execute" : "first_review", makerCheckerRequired: false, reviewerPermission: "payment.batch.review", }, reviewComment: null, secondaryReviewComment: null, executionSummaryJson: null, createdAt: "2026-05-27T09:00:00.000Z", createdBy: visualMe.id, reviewedAt: status === "draft" ? null : "2026-05-27T09:30:00.000Z", reviewedBy: status === "draft" ? null : visualMe.name, secondaryReviewedAt: null, secondaryReviewedBy: null, executedAt: null, executedBy: null, cancelledAt: null, cancelledBy: null, items: [ { id: `batch-item-wave4-${suffix}`, entityId: `reim-wave4-${suffix}`, entityCode: `BX20260527${suffix}`, entityTitle: "Wave4 报销付款", amount: failed ? "860.00" : "2860.00", currencyCode: "CNY", paymentVoucherNoOverride: null, paymentDateOverride: null, paymentChannelOverride: null, status: failed ? "failed" : "pending", failureCode: failed ? "BANK_RECEIPT_REQUIRED" : null, failureMessage: failed ? "缺少银行回单附件" : null, snapshotJson: {}, createdAt: "2026-05-27T09:05:00.000Z", openRiskFlags: failed ? [riskFlag] : [], }, ], }; } const paymentBatches = [paymentBatch("draft", "001"), paymentBatch("reviewed", "002"), paymentBatch("failed", "003")]; const financeLedgerItem = { id: "ledger-wave4-1", formNumber: "BX20260527001", reimbursementDate: "2026-05-26", status: "approved", totalAmount: "2860.00", description: "Wave4 财务台账验证", owner: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name }, submitter: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name }, department: { id: "dept-wave4", name: "研发部" }, project: { id: "project-wave4", projectNo: "HYB-W4", name: "OA 试运行" }, payment: { status: "unpaid", paymentVoucherNo: null, paymentDate: null, paymentChannel: null, paidAt: null, paidBy: null, }, invoice: { status: "available", counts: { total: 2, pendingOcr: 0, ocrFailed: 0, available: 2, bound: 0, archived: 0, usedComplete: 0 }, }, attachmentCount: 1, createdAt: "2026-05-27T08:50:00.000Z", updatedAt: "2026-05-27T09:30:00.000Z", }; const invoiceListItem = { id: "invoice-wave4-1", status: "available", availabilityReason: null, availabilityMessage: null, ocrType: "vat", ocrProvider: "pdf", ocrAttempts: 1, ocrLastError: null, ocrCompletedAt: "2026-05-27T09:00:00.000Z", invoiceCode: "0440026", invoiceNumber: "00012890", invoiceDate: "2026-05-26", amount: "1000.00", taxAmount: "60.00", totalAmount: "1060.00", linkedReimbursementId: null, linkedItemId: null, linkedTravelOrderId: null, originalName: "wave4-invoice.pdf", mimeType: "application/pdf", fileSize: 128000, verification: { id: "verify-wave4-1", provider: "mock", status: "success", resultCode: "OK", resultMessage: "核验通过", errorCode: null, errorMessage: null, durationMs: 260, requestedBy: visualMe.id, requestedAt: "2026-05-27T09:05:00.000Z", completedAt: "2026-05-27T09:06:00.000Z", retryOfId: null, attempts: 1, }, owner: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name }, uploadedBy: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name }, createdAt: "2026-05-27T08:55:00.000Z", updatedAt: "2026-05-27T09:06:00.000Z", }; const departments = [ { id: "dept-wave4", name: "研发部", parentId: null, archivedAt: null, createdAt: null, updatedAt: null }, { id: "dept-finance-wave4", name: "财务部", parentId: null, archivedAt: null, createdAt: null, updatedAt: null }, ]; const employees = [ { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name, phone: "13800000000", email: visualUser.identifier, status: "active", departmentId: "dept-wave4", department: departments[0], directManagerId: null, directManager: null, title: "管理员", employmentStatus: "active", hireDate: "2026-01-01", avatarUrl: null, roleCodes: visualMe.roleCodes, createdAt: null, updatedAt: null, }, ]; const legalEntities = [ { id: "legal-wave4", code: "HYD", name: "海音达科技", shortName: "海音达", currencyCode: "CNY", taxNo: null, enabled: true, archivedAt: null, createdAt: null, updatedAt: null, }, ]; const costCenters = [ { id: "cost-wave4", legalEntityId: "legal-wave4", code: "RD", name: "研发中心", parentId: null, ownerEmployeeId: visualMe.id, enabled: true, archivedAt: null, createdAt: null, updatedAt: null, legalEntity: { id: "legal-wave4", code: "HYD", name: "海音达科技" }, parent: null, owner: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name }, }, ]; const documentCategories = [ { id: "doc-wave4", code: "BX", name: "报销单", entityType: "reimbursement", sequencePrefix: "BX", numberLength: 5, resetPeriod: "daily", defaultWorkflowKey: "reimbursement-default", enabled: true, archivedAt: null, createdAt: null, updatedAt: null, }, ]; const featureFlags = [ { id: "flag-payment", flagKey: "payment_batch_v1", overrideMode: "force_on", rolloutType: "global", departmentIds: [], note: "Wave4 视觉验证", version: 3, createdAt: null, createdBy: null, updatedAt: "2026-05-27T09:00:00.000Z", updatedBy: visualMe.id, envEnabled: true, effectiveEnabled: true, effectiveReason: "force_on", matchedDepartmentId: null, requiresDepartment: false, }, { id: "flag-erp", flagKey: "erp_sync", overrideMode: "force_off", rolloutType: "global", departmentIds: [], note: "保持冻结", version: 2, createdAt: null, createdBy: null, updatedAt: null, updatedBy: null, envEnabled: false, effectiveEnabled: false, effectiveReason: "force_off", matchedDepartmentId: null, requiresDepartment: false, }, ]; const workflowDefinitions = [ { id: "workflow-wave4-1", entityType: "leave", name: "请假审批默认流程", version: 1, status: "published", steps: [ { id: "manager-review", name: "直属主管审批", assigneeRule: { type: "department_manager" } }, { id: "admin-archive", name: "行政留档", assigneeRule: { type: "role", value: "administration" } }, ], notes: "Wave4 视觉验证", source: "manual", createdAt: "2026-05-27T08:00:00.000Z", updatedAt: "2026-05-27T09:00:00.000Z", publishedAt: "2026-05-27T09:00:00.000Z", archivedAt: null, }, ]; const notificationChannels = [ { id: "channel-wave4-1", channelType: "wecom", name: "企业微信工作台应用消息", enabled: true, configJson: { mode: "wecom_app_message", webhookUrl: null, timeoutMs: 3000, routing: { roleCodes: ["manager", "finance"], domainScopes: ["workflow", "reimbursement"], allowedTypes: [] }, secretConfigured: true, secretMasked: "******", }, templateJson: { title: "{{title}}", body: "{{body}}", footer: "进入 OA 查看详情", deepLinkLabel: "查看", }, createdAt: "2026-05-27T08:30:00.000Z", updatedAt: "2026-05-27T09:00:00.000Z", }, ]; const notificationDeliveries = [ { notificationId: "notify-wave4-1", channelId: "channel-wave4-1", channelName: "企业微信工作台应用消息", channelType: "wecom", notificationType: "workflow.task_assigned", notificationTitle: "你有新的审批", status: "failed", attemptCount: 2, latestAttemptAt: "2026-05-27T09:20:00.000Z", resultCode: "MOCK_FAILED", errorMessage: "Wave4 mock delivery", responseBody: null, notificationCreatedAt: "2026-05-27T09:10:00.000Z", }, ]; type MockVerificationRecord = { id: string; provider: string; status: string; resultCode: string | null; resultMessage: string | null; errorCode: string | null; errorMessage: string | null; durationMs: number | null; requestedBy: string | null; requestedAt: string | null; completedAt: string | null; retryOfId: string | null; attempts: number; }; type Wave4MockOptions = { detailVerification?: MockVerificationRecord | null; verificationRecords?: MockVerificationRecord[]; triggerVerificationError?: Record; retryVerificationError?: Record; onTriggerVerification?: (route: Route) => void; onRetryVerification?: (route: Route) => void; }; async function mockWave4Apis(page: Page, options: Wave4MockOptions = {}) { const defaultVerification = invoiceListItem.verification as MockVerificationRecord; const initialVerificationRecords = options.verificationRecords ?? [options.detailVerification ?? defaultVerification].filter( (item): item is MockVerificationRecord => Boolean(item), ); let currentVerificationRecords = initialVerificationRecords; let currentDetailVerification = options.detailVerification ?? currentVerificationRecords[0] ?? defaultVerification; await page.route((url) => url.pathname.startsWith("/api/"), async (route) => { const request = route.request(); const url = new URL(request.url()); const pathName = url.pathname; if (pathName === "/api/auth/capabilities") { await fulfillJson(route, { emailDelivery: { ready: true, statusReasons: [] }, emailLoginCode: { enabled: true }, passwordReset: { enabled: true }, }); return; } if (pathName === "/api/auth/sso/providers") { await fulfillJson(route, []); return; } if (pathName === "/api/auth/wecom/config") { await fulfillJson(route, { enabled: true, entryEnabled: true, oauthEnabled: true, corpId: "wx-wave4", agentId: "1000002", loginUrl: "/api/auth/wecom/login?returnTo=%2Fportal", bindEnabled: true, defaultReturnTo: "/portal", status: "ready", statusReasons: [], }); return; } if (pathName === "/api/auth/login" || pathName === "/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-pc-wave4-token", expiresIn: 900, mustChangePassword: false })), }); return; } if (pathName === "/api/me") { await fulfillJson(route, visualMe); return; } if (pathName === "/api/system/navigation") { await fulfillJson(route, { workspace: { id: "workspace-wave4", 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("payment-batches", "/payment-batches", "付款", "payments"), navigationItem("ap-operations", "/ap-operations", "应付账款", "account_balance_wallet"), navigationItem("invoice-pool", "/invoice-pool", "票据", "receipt_long"), ], admin: [ navigationItem("settings", "/system/settings", "管理", "settings", "admin"), ], backup: [], }, recommendedRolePresets: [], }); return; } if (pathName === "/api/system/workspace") { await fulfillJson(route, { id: "workspace-wave4", persisted: true, profile: "small_team_internal", orgMode: "single_company", primaryEntryMode: "password", notificationStrategy: "in_app", devicePriority: "desktop", moduleVisibility: { portal: "main", workflowTasks: "main", reimbursements: "main", paymentBatches: "admin", apOperations: "admin", invoicePool: "admin", people: "admin", organization: "admin", approvalRules: "admin", featureFlags: "admin", travelRequests: "hidden", }, createdAt: null, updatedAt: "2026-05-29T08:00:00.000Z", updatedBy: visualMe.id, }); return; } if (pathName === "/api/notifications") { await fulfillJson(route, { page: 1, pageSize: 5, total: 0, unreadOnly: false, unreadCount: 0, items: [] }); return; } if (pathName === "/api/portal/home") { await fulfillJson(route, { workspace: { id: "workspace-wave4", persisted: true, profile: "small_team_internal" }, greeting: { name: visualMe.name }, cards: [], pendingTasks: { count: 0, href: "/workflow/tasks" }, myApplications: [], riskSummary: null, announcements: [], quickLinks: [], todayPresence: null, teamPresence: null, weeklyReport: null, weeklySummary: null, todoSummary: null, projectSummary: null, peopleLifecycle: null, financeSummary: null, adoptionSummary: null, roleCapabilities: { canSubmitApplications: true, canApproveTasks: true, canUseFinance: true, canManageSystem: true, canViewTeam: true, isAdminOnly: false, }, recentActivities: [], teamStatusBoard: [], }); return; } if (pathName === "/api/payment-batches") { await fulfillJson(route, { items: paymentBatches }); return; } const paymentReceiptMatch = pathName.match(/^\/api\/payment-batches\/([^/]+)\/receipt-attachments$/); if (paymentReceiptMatch) { await fulfillJson(route, { items: [ { id: `receipt-${paymentReceiptMatch[1]}`, originalName: "bank-receipt-wave4.pdf", mimeType: "application/pdf", fileSize: 248000, fileSha256: "hash-wave4", uploadedBy: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name }, createdAt: "2026-05-27T10:00:00.000Z", }, ], }); return; } const paymentBatchDetailMatch = pathName.match(/^\/api\/payment-batches\/([^/]+)$/); if (paymentBatchDetailMatch) { const item = paymentBatches.find((batch) => batch.id === paymentBatchDetailMatch[1]) ?? paymentBatches[0]; await fulfillJson(route, { item }); return; } if (pathName === "/api/ap-operations") { await fulfillJson(route, { summary: { pendingPaymentCount: 2, partialMatchCount: 1, exceptionCount: 1, budgetOverspendCount: 1, }, pendingPaymentBatches: [paymentBatches[0]], failedPaymentBatchItems: [], exceptionRiskFlags: [riskFlag], budgetOverspendRisks: [riskFlag], partialMatchItems: [ { id: "recon-item-wave4-1", travelReconciliationBatchId: "recon-wave4-1", travelOrderId: "travel-order-wave4-1", travelRequestId: "travel-wave4-1", matchStatus: "amount_mismatch", resolutionStatus: "pending", anomalyCode: "amount_mismatch", orderAmount: "860.00", transactionAmount: "900.00", currency: "CNY", createdAt: "2026-05-27T08:00:00.000Z", }, ], }); return; } if (pathName === "/api/ap-operations/reimbursement-ledger") { await fulfillJson(route, { page: 1, pageSize: 20, total: 1, items: [financeLedgerItem] }); return; } if (pathName === "/api/risk-flags") { await fulfillJson(route, { items: [riskFlag] }); return; } if (pathName === "/api/invoice-pool") { await fulfillJson(route, { page: 1, pageSize: 20, total: 1, ownerScope: "own", items: [{ ...invoiceListItem, verification: currentDetailVerification }], }); return; } if (pathName === `/api/invoice-pool/${invoiceListItem.id}`) { await fulfillJson(route, { ...invoiceListItem, verification: currentDetailVerification, archivedReason: null, archivedAt: null, ownerEmployeeId: visualMe.id, uploadedByEmployeeId: visualMe.id, fileSha256: "hash-wave4", remarks: "Wave4 票据详情", sellerName: "示例服务有限公司", buyerName: "海音达科技", isDuplicateOverride: false, overrideRefFormNumber: null, overrideReason: null, overriddenBy: null, }); return; } if (pathName === `/api/invoice-pool/${invoiceListItem.id}/verify`) { options.onTriggerVerification?.(route); if (options.triggerVerificationError) { await fulfillApiError(route, 409, options.triggerVerificationError); return; } const pendingRecord: MockVerificationRecord = { ...defaultVerification, id: "verify-wave4-triggered", status: "pending", resultCode: null, resultMessage: null, errorCode: null, errorMessage: null, completedAt: null, attempts: 1, }; currentDetailVerification = pendingRecord; currentVerificationRecords = [pendingRecord, ...currentVerificationRecords]; await fulfillJson(route, { invoiceId: invoiceListItem.id, verificationId: pendingRecord.id, provider: "mock", triggerMode: "queued", }); return; } if (pathName === `/api/invoice-pool/${invoiceListItem.id}/verification`) { await fulfillJson(route, { invoiceId: invoiceListItem.id, items: currentVerificationRecords }); return; } if (pathName === `/api/invoice-pool/${invoiceListItem.id}/verification/retry`) { options.onRetryVerification?.(route); if (options.retryVerificationError) { await fulfillApiError(route, 409, options.retryVerificationError); return; } const retryRecord: MockVerificationRecord = { ...defaultVerification, id: "verify-wave4-retry", status: "pending", resultCode: null, resultMessage: null, errorCode: null, errorMessage: null, completedAt: null, retryOfId: currentDetailVerification?.id ?? null, attempts: (currentDetailVerification?.attempts ?? 1) + 1, }; currentDetailVerification = retryRecord; currentVerificationRecords = [retryRecord, ...currentVerificationRecords]; await fulfillJson(route, { invoiceId: invoiceListItem.id, verificationId: retryRecord.id, status: "pending", }); return; } if (pathName === "/api/reimbursements") { await fulfillJson(route, { page: 1, pageSize: 50, total: 1, scope: "own", items: [ { id: "reim-wave4-1", formNumber: "BX20260527001", status: "approved", reimbursementDate: "2026-05-26", totalAmount: "2860.00", description: "Wave4 绑定验证", owner: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name }, submitter: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name }, project: null, pendingWorkflow: null, createdAt: "2026-05-27T08:50:00.000Z", updatedAt: "2026-05-27T09:30:00.000Z", }, ], }); return; } if (pathName === "/api/travel-requests") { await fulfillJson(route, { page: 1, pageSize: 50, total: 0, mine: false, items: [] }); return; } if (pathName === "/api/delegations/mine") { await fulfillJson(route, { status: "active", initiated: [], received: [] }); return; } if (pathName === "/api/users") { await fulfillJson(route, { page: 1, pageSize: 20, total: 1, items: [ { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name, phone: "13800000000", email: visualUser.identifier, status: "active", departmentId: "dept-wave4", title: "管理员", roleCodes: visualMe.roleCodes, createdAt: "2026-05-27T08:00:00.000Z", updatedAt: "2026-05-27T09:00:00.000Z", }, ], }); return; } if (pathName === "/api/departments") { await fulfillJson(route, { includeArchived: false, items: departments, tree: departments.map((item) => ({ ...item, children: [] })) }); return; } if (pathName === "/api/employees") { await fulfillJson(route, { page: 1, pageSize: 200, total: employees.length, items: employees }); return; } if (pathName === "/api/employees/organization") { await fulfillJson(route, { items: [ { department: departments[0], roots: [ { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name, title: "管理员", status: "active", avatarUrl: null, department: departments[0], externalManager: null, children: [], }, ], }, { department: departments[1], roots: [], }, ], }); return; } if (pathName === "/api/employees/wecom-hr-master-sync/runs") { await fulfillJson(route, { items: [ { id: "wecom-hr-run-wave4", mode: "dry_run", status: "success", tokenSource: null, requestedUserids: null, limit: 50, summary: { total: 1, created: 0, updated: 1, skipped: 0, conflicts: 0, failed: 0 }, errorCode: null, errorMessage: null, triggeredBy: visualMe.id, startedAt: "2026-05-29T07:50:00.000Z", finishedAt: "2026-05-29T07:51:00.000Z", createdAt: "2026-05-29T07:50:00.000Z", updatedAt: "2026-05-29T07:51:00.000Z", itemCount: 1, items: [], }, ], }); return; } if (pathName === "/api/approval-rules") { await fulfillJson(route, { enabled: null, items: [ { id: "approval-rule-wave5-1", name: "5000 以下经理审批", minAmount: "0.00", maxAmount: "5000.00", level1Role: "manager", level2Role: null, level3Role: null, ownerSelfFallbackRole: "finance_mgr", enabled: true, priority: 1, createdAt: "2026-05-29T08:00:00.000Z", updatedAt: "2026-05-29T08:00:00.000Z", }, { id: "approval-rule-wave5-2", name: "高额财务负责人复核", minAmount: "5000.01", maxAmount: null, level1Role: "manager", level2Role: "finance_mgr", level3Role: "ceo", ownerSelfFallbackRole: null, enabled: false, priority: 2, createdAt: "2026-05-29T08:00:00.000Z", updatedAt: "2026-05-29T08:00:00.000Z", }, ], }); return; } if (pathName === "/api/master-data/legal-entities") { await fulfillJson(route, { includeArchived: false, items: legalEntities }); return; } if (pathName === "/api/master-data/cost-centers") { await fulfillJson(route, { includeArchived: false, items: costCenters }); return; } if (pathName === "/api/master-data/document-categories") { await fulfillJson(route, { includeArchived: false, items: documentCategories }); return; } if (pathName === "/api/notifications/channels") { await fulfillJson(route, { items: notificationChannels }); return; } if (pathName === "/api/notifications/deliveries") { await fulfillJson(route, { items: notificationDeliveries }); return; } if (pathName === "/api/system/feature-flags") { await fulfillJson(route, { items: featureFlags }); return; } if (pathName === "/api/workflow-definitions") { await fulfillJson(route, { items: workflowDefinitions }); return; } if (pathName === "/api/audit-logs") { await fulfillJson(route, { page: 1, pageSize: 20, total: 1, items: [ { id: "audit-wave4-1", requestId: "mock-wave4-request", actorId: visualMe.id, actorName: visualMe.name, actorEmail: visualUser.identifier, action: "PAYMENT_BATCH_REVIEW", targetType: "payment_batch", targetId: "batch-wave4-001", ipAddress: "127.0.0.1", userAgent: "Playwright", extra: {}, createdAt: "2026-05-27T09:30:00.000Z", }, ], }); return; } await fulfillJson(route, {}); }); } async function login(page: Page) { await page.goto("/login"); await expect(page.getByTestId("login-password-primary-panel")).toBeVisible(); await page.getByTestId("login-identifier").fill(visualUser.identifier); await page.getByTestId("login-password").fill(visualUser.password); await page.getByTestId("login-submit").click(); await expect(page).toHaveURL(/\/portal/); await expect(page.getByTestId("topbar-profile-trigger").first()).toBeVisible(); } async function capture(page: Page, name: string) { mkdirSync(outputDir, { recursive: true }); await page.screenshot({ path: path.join(outputDir, `${name}.png`), fullPage: true }); } async function captureTaskEvidence(page: Page, name: string) { mkdirSync(taskOutputDir, { recursive: true }); await page.screenshot({ path: path.join(taskOutputDir, `${name}.png`), fullPage: true }); } const pages = [ { path: "/payment-batches", proof: "data-testid=payment-batches-table", screenshot: "01-payment-batches", expectStatusStrip: false }, { path: "/ap-operations", proof: "text=异常付款", screenshot: "02-ap-operations", expectStatusStrip: false }, { path: "/invoice-pool", proof: "data-testid=invoice-pool-table", screenshot: "03-invoice-pool", expectStatusStrip: false }, { path: "/system/approval-rules", summary: "approval-rules-summary", proof: "data-testid=approval-rules-table", screenshot: "04-approval-rules" }, { path: "/people", summary: "people-summary", proof: "data-testid=people-table", screenshot: "05-people" }, { path: "/organization", summary: "organization-summary", proof: "text=研发部", screenshot: "06-organization" }, { path: "/system/workspace", summary: "workspace-settings-summary", proof: "data-testid=workspace-modules-table", screenshot: "07-workspace" }, { path: "/system/users", summary: "users-summary", proof: "data-testid=users-table", screenshot: "08-users" }, { path: "/system/master-data", summary: "master-data-summary", proof: "text=法体列表", screenshot: "09-master-data" }, { path: "/system/notification-channels", summary: "notification-channels-summary", proof: "data-testid=notification-channels-table", screenshot: "10-notification-channels" }, { path: "/system/settings", summary: "system-settings-summary", proof: "text=管理", screenshot: "11-system-settings" }, { path: "/system/feature-flags", summary: "feature-flags-summary", proof: "data-testid=feature-flags-table", screenshot: "12-feature-flags" }, { path: "/system/workflow-definitions", summary: "workflow-definitions-summary", proof: "text=版本列表", screenshot: "13-workflow-definitions" }, { path: "/audit-logs", summary: "audit-summary", proof: "text=PAYMENT_BATCH_REVIEW", screenshot: "14-audit-logs" }, ]; test("PC 财务与管理页 Wave4-5 视觉冒烟", async ({ page }) => { await mockWave4Apis(page); await page.setViewportSize({ width: 1440, height: 960 }); await login(page); for (const target of pages) { await page.goto(target.path); if ("summary" in target) { await expect(page.getByTestId(target.summary)).toBeVisible(); } await expect(page.locator(target.proof).first()).toBeVisible(); if (!("expectStatusStrip" in target) || target.expectStatusStrip !== false) { await expect(page.locator(".finance-status-strip").first()).toBeVisible(); } await capture(page, target.screenshot); } }); test("PC 财务列表使用明确动作入口", async ({ page }) => { await mockWave4Apis(page); await page.setViewportSize({ width: 1440, height: 960 }); await login(page); await page.goto("/payment-batches"); await expect(page.getByTestId("payment-batch-create-panel")).toHaveCount(0); await page.getByRole("button", { name: "新建批次" }).click(); await expect(page.getByTestId("payment-batch-create-panel")).toBeVisible(); await expect(page.getByTestId("payment-batch-create-panel")).toContainText("新建批次"); await page.goto("/ap-operations"); await expect(page.getByTestId("ap-export")).toBeVisible(); await page.getByTestId("ap-export").click(); await expect(page.getByTestId("ap-export-csv")).toBeVisible(); await page.goto("/invoice-pool"); await expect(page.getByTestId("invoice-actions-panel")).toHaveCount(0); await page.getByRole("button", { name: "绑定" }).click(); await expect(page.getByTestId("invoice-actions-panel")).toBeVisible(); await page.getByTestId("invoice-export").click(); await expect(page.getByTestId("invoice-export-csv")).toBeVisible(); }); test("/payment-batches 在 768x1024 与 1024x768 下操作列可达", async ({ page }) => { await mockWave4Apis(page); await login(page); for (const viewport of [ { width: 768, height: 1024 }, { width: 1024, height: 768 }, ]) { await page.setViewportSize(viewport); await page.goto("/payment-batches"); await expect(page.getByRole("heading", { name: "付款", exact: true })).toBeVisible(); const table = page.getByTestId("payment-batches-table"); await expect(table).toBeVisible(); const scrollRegion = table.locator("xpath=ancestor::*[@data-testid='business-table-scroll']").first(); const metrics = await scrollRegion.evaluate((element) => { element.scrollLeft = element.scrollWidth; return { overflowX: window.getComputedStyle(element).overflowX, clientWidth: element.clientWidth, scrollWidth: element.scrollWidth, documentOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth, }; }); expect(["auto", "scroll"]).toContain(metrics.overflowX); expect(metrics.scrollWidth).toBeGreaterThanOrEqual(metrics.clientWidth); expect(metrics.documentOverflow).toBeLessThanOrEqual(2); const operation = table.getByRole("button").last(); await expect(operation).toBeVisible(); await operation.click({ trial: true }); await captureTaskEvidence(page, `payment-batches-${viewport.width}x${viewport.height}`); } }); test("PC 票据辅助核验动作可提交并重试", async ({ page }) => { const failedVerification: MockVerificationRecord = { ...(invoiceListItem.verification as MockVerificationRecord), id: "verify-wave4-failed", status: "error", resultCode: "tax_provider_not_configured", resultMessage: null, errorCode: "tax_provider_not_configured", errorMessage: "tax provider is not configured in current environment", attempts: 1, }; const triggerRequests: string[] = []; const retryRequests: string[] = []; await mockWave4Apis(page, { detailVerification: failedVerification, verificationRecords: [failedVerification], onTriggerVerification: (route) => triggerRequests.push(route.request().method()), onRetryVerification: (route) => retryRequests.push(route.request().method()), }); await page.setViewportSize({ width: 1440, height: 960 }); await login(page); await page.goto(`/invoice-pool?id=${invoiceListItem.id}`); await expect(page.getByTestId("invoice-detail-panel")).toBeVisible(); await expect(page.getByTestId("invoice-trigger-verification")).toBeVisible(); await expect(page.getByTestId("invoice-retry-verification")).toBeVisible(); await expect(page.getByTestId("invoice-verification-records")).toContainText("当前环境未配置自动核验服务"); await page.getByTestId("invoice-retry-verification").click(); await expect(page.getByText("发票核验已重新提交")).toBeVisible(); expect(retryRequests).toEqual(["POST"]); await page.getByTestId("invoice-trigger-verification").click(); await expect(page.getByText("发票核验已提交")).toBeVisible(); expect(triggerRequests).toEqual(["POST"]); }); test("PC 票据辅助核验错误提示可理解", async ({ page }) => { const failedVerification: MockVerificationRecord = { ...(invoiceListItem.verification as MockVerificationRecord), id: "verify-wave4-attempts", status: "error", resultCode: "mock_transport_error", resultMessage: null, errorCode: "mock_transport_error", errorMessage: "mock provider transport error", attempts: 3, }; await mockWave4Apis(page, { detailVerification: failedVerification, verificationRecords: [failedVerification], triggerVerificationError: { verification: "too_frequent" }, retryVerificationError: { verification: "attempts_exceeded" }, }); await page.setViewportSize({ width: 1440, height: 960 }); await login(page); await page.goto(`/invoice-pool?id=${invoiceListItem.id}`); await expect(page.getByTestId("invoice-detail-panel")).toBeVisible(); await page.getByTestId("invoice-trigger-verification").click(); await expect(page.getByRole("main").getByText("VALID_001: 验真触发过于频繁,请稍后重试。")).toBeVisible(); await page.getByTestId("invoice-retry-verification").click(); await expect(page.getByRole("main").getByText("VALID_001: 验真重试次数已达上限,请联系财务人工核验。")).toBeVisible(); });