setSteps((prev) => prev.map((item, itemIndex) => itemIndex === index ? { ...item, id: event.target.value } : item))} />
setSteps((prev) => prev.map((item, itemIndex) => itemIndex === index ? { ...item, name: event.target.value } : item))} />
diff --git a/client/tests/e2e/pc-finance-admin-visual-wave4.spec.ts b/client/tests/e2e/pc-finance-admin-visual-wave4.spec.ts
new file mode 100644
index 00000000..9fe7d76e
--- /dev/null
+++ b/client/tests/e2e/pc-finance-admin-visual-wave4.spec.ts
@@ -0,0 +1,798 @@
+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 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.settings",
+ "system.audit.view",
+];
+
+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)),
+ });
+}
+
+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: "risk.review.manage",
+ },
+ 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",
+ },
+];
+
+async function mockWave4Apis(page: Page) {
+ 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", "AP 运营", "account_balance_wallet"),
+ navigationItem("invoice-pool", "/invoice-pool", "票据中心", "receipt_long"),
+ ],
+ admin: [
+ navigationItem("users", "/system/users", "用户管理", "group", "admin"),
+ navigationItem("master-data", "/system/master-data", "主数据", "database", "admin"),
+ navigationItem("notification-channels", "/system/notification-channels", "通知渠道", "notifications", "admin"),
+ navigationItem("settings", "/system/settings", "系统设置", "settings", "admin"),
+ navigationItem("feature-flags", "/system/feature-flags", "功能开关", "toggle_on", "admin"),
+ navigationItem("workflow-definitions", "/system/workflow-definitions", "流程定义", "schema", "admin"),
+ navigationItem("audit-logs", "/audit-logs", "审计日志", "manage_search", "admin"),
+ ],
+ backup: [],
+ },
+ recommendedRolePresets: [],
+ });
+ 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] });
+ return;
+ }
+
+ if (pathName === `/api/invoice-pool/${invoiceListItem.id}`) {
+ await fulfillJson(route, {
+ ...invoiceListItem,
+ 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}/verification`) {
+ await fulfillJson(route, { invoiceId: invoiceListItem.id, items: [invoiceListItem.verification] });
+ 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/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 });
+}
+
+const pages = [
+ { path: "/payment-batches", summary: "payment-batches-summary", proof: "text=批次列表", screenshot: "01-payment-batches" },
+ { path: "/ap-operations", summary: "ap-operations-summary", proof: "text=异常付款", screenshot: "02-ap-operations" },
+ { path: "/invoice-pool", summary: "invoice-pool-summary", proof: "data-testid=invoice-pool-table", screenshot: "03-invoice-pool" },
+ { path: "/system/users", summary: "users-summary", proof: "data-testid=users-table", screenshot: "04-users" },
+ { path: "/system/master-data", summary: "master-data-summary", proof: "text=法体列表", screenshot: "05-master-data" },
+ { path: "/system/notification-channels", summary: "notification-channels-summary", proof: "data-testid=notification-channels-table", screenshot: "06-notification-channels" },
+ { path: "/system/settings", summary: "system-settings-summary", proof: "text=系统设置", screenshot: "07-system-settings" },
+ { path: "/system/feature-flags", summary: "feature-flags-summary", proof: "data-testid=feature-flags-table", screenshot: "08-feature-flags" },
+ { path: "/system/workflow-definitions", summary: "workflow-definitions-summary", proof: "text=版本列表", screenshot: "09-workflow-definitions" },
+ { path: "/audit-logs", summary: "audit-summary", proof: "text=PAYMENT_BATCH_REVIEW", screenshot: "10-audit-logs" },
+];
+
+test("PC 财务与管理页 Wave4 视觉冒烟", 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);
+ await expect(page.getByTestId(target.summary)).toBeVisible();
+ await expect(page.locator(target.proof).first()).toBeVisible();
+ await expect(page.locator(".finance-status-strip").first()).toBeVisible();
+ await capture(page, target.screenshot);
+ }
+});