文件
hyb-oa/client/tests/e2e/pc-detail-mobile-visual-wave3.spec.ts
T

1603 行
61 KiB
TypeScript

import { mkdirSync, readFileSync } 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", timezoneId: "America/Los_Angeles" });
const outputDir = path.resolve(process.cwd(), "../output/playwright/pc-detail-mobile-visual-wave3");
const visualUser = {
identifier: "[email protected]",
password: "VisualWave3Password1234",
name: "王顺理",
};
const visualMe = {
id: "employee-wave3",
employeeNo: "HYD-W3",
name: visualUser.name,
email: visualUser.identifier,
phone: null,
roleCodes: ["employee", "manager", "finance_mgr"],
permissions: [
"expense.create",
"expense.submit",
"expense.view_own",
"expense.view_all",
"expense.delete_own",
"expense.approve",
"leave.approve",
"procurement.approve",
"invoice.upload",
"invoice.view_own",
"invoice.manage",
"payment.batch.manage",
"risk.review.manage",
"asset.view_own",
"asset.view_all",
"asset.manage",
"asset.transfer",
"leave.view_own",
"attendance.view_own",
],
profileCompleteness: {
hasEmergencyContact: true,
hasBankAccount: true,
hasAvatar: false,
hasIdNumber: false,
score: 88,
},
notificationPushEnabled: true,
departmentId: "dept-wave3",
};
const workflowTask = {
id: "task-wave3",
instanceId: "workflow-wave3",
stepId: "manager-review",
stepName: "主管审批",
assigneeId: visualMe.id,
assigneeRole: "manager",
status: "pending",
outcome: null,
createdAt: "2026-05-27T09:05:00.000Z",
completedAt: null,
workflowInstance: {
entityType: "reimbursement",
entityId: "reim-wave3",
currentStatus: "reviewing",
},
businessSummary: {
reference: "BX20260527003",
title: "项目差旅资料补充",
applicantName: "李明",
amount: "¥1,860.00",
duration: null,
businessStatus: "reviewing",
},
allowedActions: ["approve", "reject", "return", "transfer"],
};
const reimbursementDetail = {
id: "reim-wave3",
formNumber: "BX20260527003",
submitterId: visualMe.id,
ownerId: visualMe.id,
departmentId: "dept-wave3",
department: { id: "dept-wave3", name: "研发部" },
status: "reviewing",
createdAt: "2026-05-27T08:50:00.000Z",
submittedAt: "2026-05-27T09:00:00.000Z",
reimbursementDate: "2026-05-27",
totalAmount: "1860.00",
description: "项目现场资料与交通报销",
payment: {
paymentVoucherNo: null,
paymentDate: null,
paymentChannel: null,
paidAt: null,
paidBy: null,
},
projectId: "project-wave3",
linkedTravelRequestId: null,
linkedTravelRequest: null,
project: { id: "project-wave3", projectNo: "HYB-W3", name: "OA 试运行" },
owner: { id: visualMe.id, employee_no: visualMe.employeeNo, name: visualMe.name },
submitter: { id: visualMe.id, employee_no: visualMe.employeeNo, name: visualMe.name },
invoices: [
{
id: "invoice-wave3",
status: "available",
invoiceCode: "0440026",
invoiceNumber: "00012890",
invoiceDate: "2026-05-26",
sellerName: "北京示例服务有限公司",
totalAmount: "860.00",
originalName: "invoice-wave3.pdf",
linkedItemId: "item-wave3-1",
},
],
attachments: [
{
id: "att-wave3",
originalName: "现场说明.pdf",
mimeType: "application/pdf",
fileSize: 128000,
fileSha256: "hash-wave3",
uploadedBy: { id: visualMe.id, name: visualMe.name, employeeNo: visualMe.employeeNo },
createdAt: "2026-05-27T09:10:00.000Z",
},
],
budgetHint: null,
pendingWorkflow: {
instanceId: "workflow-wave3",
stepId: "manager-review",
task: {
id: "task-wave3",
step_name: "主管审批",
assignee_id: visualMe.id,
assignee_role: "manager",
},
},
workflowProgress: {
currentStepIndex: 0,
totalSteps: 2,
steps: [
{ id: "step-1", name: "主管审批", status: "current" },
{ id: "step-2", name: "财务复核", status: "pending" },
],
},
processingProgress: {
stages: [
{ key: "submitted", label: "申请提交", state: "completed", actorName: visualMe.name, occurredAt: "2026-05-27T09:00:00.000Z", result: "已提交", comment: null, detail: null, targetHref: null },
{ key: "manager_approval", label: "主管审批", state: "current", actorName: visualMe.name, occurredAt: "2026-05-27T09:00:00.000Z", result: "处理中", comment: null, detail: "待主管审批处理", targetHref: null },
{ key: "approval_extra-review", label: "负责人复核", state: "pending", actorName: null, occurredAt: null, result: null, comment: null, detail: "待处理", targetHref: null },
{ key: "finance_review", label: "财务复核", state: "pending", actorName: null, occurredAt: null, result: null, comment: null, detail: "待处理", targetHref: null },
{ key: "payment_batch_review", label: "付款复核", state: "pending", actorName: null, occurredAt: null, result: null, comment: null, detail: "待财务安排付款", targetHref: null },
{ key: "payment_and_receipt", label: "付款完成", state: "pending", actorName: null, occurredAt: null, result: null, comment: null, detail: "待付款复核后由财务登记,非审批环节", targetHref: null },
],
},
approvalHistory: [
{
id: "history-wave3-1",
stepName: "提交",
action: "start",
actorName: visualMe.name,
comment: "请审批",
createdAt: "2026-05-27T09:00:00.000Z",
},
],
riskFlags: [],
items: [
{
id: "item-wave3-1",
expenseTypeId: "expense-travel",
expenseDate: "2026-05-26",
amount: "860.00",
description: "交通与资料",
expenseType: {
id: "expense-travel",
code: "TRAVEL",
name: "交通费",
category: "travel",
enabled: true,
},
},
{
id: "item-wave3-2",
expenseTypeId: "expense-service",
expenseDate: "2026-05-27",
amount: "1000.00",
description: "现场服务",
expenseType: {
id: "expense-service",
code: "SERVICE",
name: "服务费",
category: "service",
enabled: true,
},
},
],
};
const workflowDetail = {
task: {
id: workflowTask.id,
instanceId: workflowTask.instanceId,
stepId: workflowTask.stepId,
stepName: workflowTask.stepName,
assigneeId: workflowTask.assigneeId,
assigneeRole: workflowTask.assigneeRole,
status: workflowTask.status,
outcome: null,
comment: null,
createdAt: workflowTask.createdAt,
completedAt: null,
},
workflowInstance: {
id: "workflow-wave3",
entityType: "reimbursement",
currentStatus: "reviewing",
currentStepId: "manager-review",
contextData: {
standardViolations: [],
budgetCheck: null,
budgetCheckStatus: "not_applicable",
parseStatus: "ready",
},
startedAt: "2026-05-27T09:00:00.000Z",
completedAt: null,
},
reimbursement: {
id: reimbursementDetail.id,
formNumber: reimbursementDetail.formNumber,
status: reimbursementDetail.status,
reimbursementDate: reimbursementDetail.reimbursementDate,
totalAmount: reimbursementDetail.totalAmount,
description: reimbursementDetail.description,
owner: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name },
submitter: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name },
},
leaveRequest: null,
attendanceCorrection: null,
purchaseRequest: null,
travelRequest: null,
allowedActions: workflowTask.allowedActions,
history: [
{
id: "workflow-history-wave3",
taskId: workflowTask.id,
actorId: visualMe.id,
actorName: visualMe.name,
actorEmployeeNo: visualMe.employeeNo,
action: "start",
comment: "进入审批",
payload: null,
createdAt: "2026-05-27T09:00:00.000Z",
},
],
};
const completedWorkflowDetail = {
...workflowDetail,
task: {
...workflowDetail.task,
id: "task-wave3-completed",
assigneeRole: "finance",
status: "completed",
outcome: "approved",
completedAt: "2026-05-27T10:10:00.000Z",
},
workflowInstance: {
...workflowDetail.workflowInstance,
currentStatus: "approved",
completedAt: "2026-05-27T10:10:00.000Z",
},
allowedActions: [],
history: [
...workflowDetail.history,
{
id: "workflow-history-wave3-completed",
taskId: "task-wave3-completed",
actorId: visualMe.id,
actorName: visualMe.name,
actorEmployeeNo: visualMe.employeeNo,
action: "approve",
comment: "资料已核对,同意通过",
payload: null,
createdAt: "2026-05-27T10:10:00.000Z",
},
],
};
const assetDetail = {
id: "asset-wave3",
assetNo: "ZC-2026-研发中心-000018",
name: "研发中心高性能移动工作站与现场采集设备",
assetType: "fixed_asset",
sensitivityLevel: "internal",
categoryId: "asset-category-wave3",
legalEntityId: "legal-wave3",
costCenterId: "cost-wave3",
purchaseRequestId: null,
serialNo: "SN-WAVE3-20260527-000018",
location: "研发中心设备区 A-018",
status: "in_use",
assigneeEmployeeId: visualMe.id,
ownerEmployeeId: visualMe.id,
keeperEmployeeId: visualMe.id,
archiveLocation: null,
externalRef: null,
sourceHash: null,
verificationNo: "VER-2026-WAVE3-0018",
confidentialityNote: null,
metadata: null,
sensitiveFieldsRedacted: false,
lastVerifiedAt: "2026-05-27T09:20:00.000Z",
expiresAt: null,
activatedAt: "2026-05-20",
retiredAt: null,
createdAt: "2026-05-20T08:00:00.000Z",
updatedAt: "2026-05-27T09:20:00.000Z",
category: { id: "asset-category-wave3", code: "DEV", name: "研发设备", documentCategoryId: null, documentCategory: null },
legalEntity: { id: "legal-wave3", code: "HYB", name: "本公司" },
costCenter: { id: "cost-wave3", code: "RD", name: "研发中心", legalEntityId: "legal-wave3" },
purchaseRequest: null,
assignee: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name, department: { id: "dept-wave3", name: "研发部" } },
owner: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name, department: { id: "dept-wave3", name: "研发部" } },
keeper: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name, department: { id: "dept-wave3", name: "研发部" } },
permissions: { canEdit: true },
};
const notifications = [
{
id: "notification-wave3-1",
type: "workflow.task_assigned",
title: "你有新的报销审批",
body: "BX20260527003 等待主管审批",
targetType: "workflow_task",
targetId: workflowTask.id,
readAt: null,
createdAt: "2026-05-27T09:12:00.000Z",
},
{
id: "notification-wave3-2",
type: "reimbursement.approved",
title: "报销已通过",
body: "项目资料报销进入财务复核",
targetType: "reimbursement",
targetId: reimbursementDetail.id,
readAt: "2026-05-27T09:20:00.000Z",
createdAt: "2026-05-27T09:18:00.000Z",
},
];
const projectDetail = {
id: "project-wave3",
projectNo: "HYB-W3",
name: "OA 试运行",
clientName: "海音达内部",
clientAlias: "海音达",
ownerEmployeeId: visualMe.id,
managerEmployeeId: visualMe.id,
status: "active",
startsAt: "2026-05-20",
targetEndAt: "2026-06-03",
actualEndAt: null,
summary: "真实试运行阶段,重点验证审批、报销、合同和移动入口。",
isConfidential: true,
createdAt: "2026-05-20T09:00:00.000Z",
updatedAt: "2026-05-27T09:30:00.000Z",
owner: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name },
manager: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name },
counts: {
members: 2,
milestones: 2,
tasks: 3,
risks: 1,
reimbursements: 1,
purchaseRequests: 1,
travelRequests: 0,
todoItems: 2,
weeklyReports: 1,
communicationLogs: 1,
closureChecklistItems: 3,
contracts: 1,
deliveries: 1,
documentRefs: 1,
},
members: [
{
id: "member-wave3",
employeeId: visualMe.id,
role: "manager",
joinedAt: "2026-05-20",
leftAt: null,
employee: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name },
},
],
milestones: [
{
id: "milestone-wave3",
title: "公网试运行准备",
description: null,
status: "in_progress",
dueDate: "2026-05-29",
completedAt: null,
ownerEmployeeId: visualMe.id,
sortOrder: 1,
},
],
tasks: [
{
id: "project-task-wave3",
title: "整理试运行反馈",
description: "跟进移动端和合同反馈",
status: "doing",
priority: "high",
assigneeEmployeeId: visualMe.id,
dueDate: "2026-05-30",
blockedReason: null,
createdBy: visualMe.id,
assignee: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name },
creator: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name },
createdAt: "2026-05-27T09:00:00.000Z",
updatedAt: "2026-05-27T09:10:00.000Z",
},
],
risks: [
{
id: "risk-wave3",
title: "移动端首屏信息过多",
detail: "需要保持办理摘要和主要动作优先。",
level: "medium",
status: "monitoring",
ownerEmployeeId: visualMe.id,
owner: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name },
createdAt: "2026-05-27T09:00:00.000Z",
updatedAt: "2026-05-27T09:00:00.000Z",
},
],
communicationSummary: {
totalCount: 1,
latestHappenedAt: "2026-05-27T10:00:00.000Z",
followUpDueTodayCount: 1,
overdueFollowUpCount: 0,
},
closureSummary: {
totalCount: 3,
pendingCount: 1,
waivedCount: 0,
warned: true,
facts: {
openRiskCount: 1,
openReimbursementCount: 1,
openTravelCount: 0,
deliveryRecordCount: 1,
contractCount: 1,
confidentialDocRefCount: 1,
},
},
capabilities: {
communicationLogEnabled: true,
closureChecklistEnabled: true,
},
};
const contractRegistryItem = {
id: "contract-wave3",
contractNo: "HT-2026-W3",
projectId: projectDetail.id,
contractType: "service",
counterpartyName: "北京示例服务有限公司",
summary: "OA 试运行服务协议台账摘要",
amount: "30000.00",
currency: "CNY",
signedDate: "2026-05-20",
effectiveDate: "2026-05-21",
expiryDate: "2026-06-20",
confidentialityLevel: "internal",
archiveLocation: "离线档案柜 A-01",
offlineArchiveNo: "OFF-2026-W3",
ownerEmployeeId: visualMe.id,
status: "active",
persistedStatus: "active",
createdAt: "2026-05-20T09:00:00.000Z",
updatedAt: "2026-05-27T09:00:00.000Z",
owner: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name },
project: { id: projectDetail.id, projectNo: projectDetail.projectNo, name: projectDetail.name },
};
const secondContractRegistryItem = {
...contractRegistryItem,
id: "contract-wave3-second",
contractNo: "HT-2026-W3-02",
counterpartyName: "上海合成技术服务有限公司",
summary: "用于验证合同记录切换保护的第二条合成台账",
amount: "18000.00",
expiryDate: "2026-07-20",
archiveLocation: "离线档案柜 A-02",
offlineArchiveNo: "OFF-2026-W3-02",
};
const contractRegistryDetail = {
...contractRegistryItem,
terminatedAt: null,
terminatedReason: null,
terminatedBy: null,
terminator: null,
documentRefs: [],
history: [
{
id: "audit-contract-wave3",
action: "CONTRACT_REGISTRY_CREATED",
summary: "创建合同台账",
createdAt: "2026-05-20T09:00:00.000Z",
actor: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name },
},
],
};
const secondContractRegistryDetail = {
...contractRegistryDetail,
...secondContractRegistryItem,
history: contractRegistryDetail.history,
};
const purchaseDetail = {
id: "purchase-wave3",
requestNo: "CG20260527001",
applicantId: visualMe.id,
submitterId: visualMe.id,
departmentId: "dept-wave3",
legalEntityId: "legal-wave3",
costCenterId: "cost-wave3",
supplierId: "supplier-wave3",
documentCategoryId: "category-wave3",
budgetPoolId: "budget-wave3",
projectId: projectDetail.id,
title: "试运行办公物料采购",
reason: "补齐试运行期间需要的标签纸和档案用品。",
currencyCode: "CNY",
totalAmount: "1280.00",
expectedDeliveryDate: "2026-05-30",
status: "reviewing",
createdAt: "2026-05-27T08:30:00.000Z",
updatedAt: "2026-05-27T09:30:00.000Z",
applicant: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name },
submitter: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name },
department: { id: "dept-wave3", name: "研发部" },
legalEntity: { id: "legal-wave3", code: "HYD", name: "海音达" },
costCenter: { id: "cost-wave3", code: "RD", name: "研发中心", legalEntityId: "legal-wave3" },
supplier: { id: "supplier-wave3", code: "SUP-W3", name: "北京示例服务有限公司" },
documentCategory: { id: "category-wave3", code: "OFFICE", name: "办公用品", entityType: "purchase_request" },
budgetPool: { id: "budget-wave3", code: "OPS", name: "运营预算" },
project: { id: projectDetail.id, projectNo: projectDetail.projectNo, name: projectDetail.name, status: projectDetail.status },
items: [
{
id: "purchase-item-wave3",
description: "档案标签纸",
specification: "A4 / 100 张",
quantity: "2",
unitPrice: "640.00",
lineAmount: "1280.00",
isAssetCandidate: false,
createdAt: "2026-05-27T08:30:00.000Z",
updatedAt: "2026-05-27T08:30:00.000Z",
},
],
attachments: [
{
id: "purchase-att-wave3",
originalName: "采购说明.pdf",
mimeType: "application/pdf",
fileSize: 64000,
fileSha256: "purchase-hash-wave3",
uploadedBy: visualMe.id,
createdAt: "2026-05-27T08:40:00.000Z",
uploader: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name },
governanceStatus: "approved",
sensitivityLevel: "internal",
attachmentType: "supporting_material",
downloadPolicy: "reason_required",
reviewedBy: visualMe.id,
reviewedAt: "2026-05-27T08:50:00.000Z",
reviewNote: "已复核",
archivedBy: null,
archivedAt: null,
archiveNote: null,
},
],
pendingWorkflow: {
instanceId: "workflow-purchase-wave3",
currentStatus: "reviewing",
currentStepId: "manager-review",
task: {
id: "task-purchase-wave3",
stepId: "manager-review",
stepName: "主管审批",
assigneeId: visualMe.id,
assigneeRole: "manager",
},
},
approvalHistory: [
{
id: "purchase-history-wave3",
taskId: "task-purchase-wave3",
actorId: visualMe.id,
actorName: visualMe.name,
actorEmployeeNo: visualMe.employeeNo,
action: "submit",
comment: "请审批",
createdAt: "2026-05-27T09:00:00.000Z",
},
],
archiveOrder: null,
permissions: { canEditDraft: false, canSubmit: false, canWithdraw: false, canArchiveOrder: false },
riskFlags: [],
};
function envelope<T>(data: T) {
return { requestId: "mock-wave3", data };
}
async function fulfillJson<T>(route: Route, data: T) {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(envelope(data)),
});
}
function screenshotPath(fileName: string) {
mkdirSync(outputDir, { recursive: true });
return path.join(outputDir, fileName);
}
function navigationItem(key: string, href: string, label: string, icon: string, placement = "main") {
return { key, href, label, icon, placement, permissionCodes: [], featureFlagKey: null };
}
async function mockWave3Apis(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-wave3",
agentId: "1000002",
loginUrl: "/api/auth/wecom/login?returnTo=%2Fmobile",
bindEnabled: true,
defaultReturnTo: "/mobile",
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-wave3-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-wave3",
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"),
navigationItem("expense", "/reimbursements", "报销", "request_quote"),
navigationItem("notifications", "/notifications", "通知", "notifications"),
],
admin: [],
backup: [],
},
recommendedRolePresets: [],
});
return;
}
if (pathName === "/api/notifications/mark-all-read") {
await fulfillJson(route, { updatedCount: 1 });
return;
}
if (pathName.startsWith("/api/notifications/") && pathName.endsWith("/mark-read")) {
await fulfillJson(route, { id: pathName.split("/")[3], readAt: "2026-05-27T09:30:00.000Z" });
return;
}
if (pathName === "/api/notifications") {
const unreadOnly = url.searchParams.get("unreadOnly") === "1";
const items = unreadOnly ? notifications.filter((item) => !item.readAt) : notifications;
await fulfillJson(route, {
page: Number(url.searchParams.get("page") ?? 1),
pageSize: Number(url.searchParams.get("pageSize") ?? 20),
total: items.length,
unreadOnly,
unreadCount: notifications.filter((item) => !item.readAt).length,
items,
});
return;
}
if (pathName === "/api/reimbursements/reim-wave3") {
await fulfillJson(route, reimbursementDetail);
return;
}
if (pathName === "/api/reimbursements") {
await fulfillJson(route, {
page: 1,
pageSize: Number(url.searchParams.get("pageSize") ?? 20),
total: 2,
scope: url.searchParams.get("scope") ?? "own",
items: [
{
id: reimbursementDetail.id,
formNumber: reimbursementDetail.formNumber,
status: reimbursementDetail.status,
reimbursementDate: reimbursementDetail.reimbursementDate,
totalAmount: reimbursementDetail.totalAmount,
description: reimbursementDetail.description,
payment: reimbursementDetail.payment,
owner: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name },
submitter: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name },
project: reimbursementDetail.project,
pendingWorkflow: {
instanceId: "workflow-wave3",
stepId: "manager-review",
allowedActions: ["approve", "reject"],
task: { id: workflowTask.id, stepName: "主管审批", assigneeId: visualMe.id, assigneeRole: "manager" },
},
createdAt: reimbursementDetail.createdAt,
updatedAt: "2026-05-27T09:20:00.000Z",
},
],
});
return;
}
if (pathName === "/api/expense-types") {
await fulfillJson(route, {
enabled: true,
items: [
{
id: "expense-travel",
code: "TRAVEL",
name: "交通费",
description: null,
category: "travel",
sortOrder: 1,
accountCodeDefault: null,
isSystemDefault: true,
enabled: true,
createdAt: null,
updatedAt: null,
},
],
});
return;
}
if (pathName === "/api/projects/project-wave3") {
await fulfillJson(route, { item: projectDetail });
return;
}
if (pathName === "/api/projects/project-wave3/members") {
await fulfillJson(route, { items: projectDetail.members });
return;
}
if (pathName === "/api/projects/project-wave3/milestones") {
await fulfillJson(route, { items: projectDetail.milestones });
return;
}
if (pathName === "/api/projects/project-wave3/tasks") {
await fulfillJson(route, { items: projectDetail.tasks });
return;
}
if (pathName === "/api/projects/project-wave3/risks") {
await fulfillJson(route, { items: projectDetail.risks });
return;
}
if (pathName === "/api/projects/project-wave3/expenses/summary") {
await fulfillJson(route, {
totalAmount: reimbursementDetail.totalAmount,
items: [
{
id: reimbursementDetail.id,
formNumber: reimbursementDetail.formNumber,
status: reimbursementDetail.status,
totalAmount: reimbursementDetail.totalAmount,
reimbursementDate: reimbursementDetail.reimbursementDate,
updatedAt: "2026-05-27T09:20:00.000Z",
},
],
});
return;
}
if (pathName === "/api/projects/project-wave3/travel/summary") {
await fulfillJson(route, { totalAmount: "0.00", items: [] });
return;
}
if (pathName === "/api/projects/project-wave3/purchase-requests/summary") {
await fulfillJson(route, {
totalAmount: purchaseDetail.totalAmount,
items: [
{
id: purchaseDetail.id,
requestNo: purchaseDetail.requestNo,
title: purchaseDetail.title,
status: purchaseDetail.status,
totalAmount: purchaseDetail.totalAmount,
updatedAt: purchaseDetail.updatedAt,
},
],
});
return;
}
if (pathName === "/api/projects/project-wave3/weekly-reports") {
await fulfillJson(route, {
items: [
{
id: "weekly-link-wave3",
weeklyReportId: "weekly-wave3",
weekStart: "2026-05-25",
status: "submitted",
employee: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name },
progressText: "完成 Wave3 视觉复核",
riskText: null,
nextStepText: "继续公网验证",
updatedAt: "2026-05-27T09:20:00.000Z",
},
],
});
return;
}
if (pathName === "/api/projects/project-wave3/deliveries") {
await fulfillJson(route, {
items: [
{
id: "delivery-wave3",
title: "试运行准备记录",
recordType: "stage_acceptance",
summary: "完成页面体验基线复核。",
occurredAt: "2026-05-27",
owner: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name },
createdAt: "2026-05-27T09:20:00.000Z",
},
],
});
return;
}
if (pathName === "/api/projects/project-wave3/document-refs") {
await fulfillJson(route, {
items: [
{
id: "doc-ref-wave3",
title: "试运行资料索引",
documentNo: "DOC-W3",
confidentialityLevel: "internal",
storageType: "offline_archive",
storagePath: "离线档案柜 A-01",
offlineArchiveNo: "OFF-2026-W3",
updatedAt: "2026-05-27T09:00:00.000Z",
},
],
});
return;
}
if (pathName === "/api/projects/project-wave3/communication-logs") {
await fulfillJson(route, {
items: [
{
id: "comm-wave3",
happenedAt: "2026-05-27T10:00:00.000Z",
channel: "meeting",
externalContactName: "试运行负责人",
externalOrgName: "海音达",
summary: "确认先优化详情页和移动入口。",
nextAction: "继续收集反馈",
nextFollowUpDate: "2026-05-29",
createdBy: visualMe.id,
createdAt: "2026-05-27T10:10:00.000Z",
updatedAt: "2026-05-27T10:10:00.000Z",
canEdit: true,
creator: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name },
},
],
});
return;
}
if (pathName === "/api/projects/project-wave3/closure-checklist") {
await fulfillJson(route, {
items: [
{
id: "closure-wave3",
itemKey: "contract",
title: "合同归档位置已登记",
status: "pending",
note: null,
sortOrder: 1,
completedAt: null,
completedBy: null,
canEdit: true,
completer: null,
},
],
});
return;
}
if (pathName === "/api/projects") {
await fulfillJson(route, {
items: [
{
id: projectDetail.id,
projectNo: projectDetail.projectNo,
name: projectDetail.name,
clientName: projectDetail.clientName,
clientAlias: projectDetail.clientAlias,
status: projectDetail.status,
startsAt: projectDetail.startsAt,
targetEndAt: projectDetail.targetEndAt,
actualEndAt: projectDetail.actualEndAt,
summary: projectDetail.summary,
isConfidential: projectDetail.isConfidential,
owner: projectDetail.owner,
manager: projectDetail.manager,
counts: {
members: projectDetail.counts.members,
milestones: projectDetail.counts.milestones,
tasks: projectDetail.counts.tasks,
risks: projectDetail.counts.risks,
overdueMilestones: 0,
highPriorityTasks: 1,
openRisks: projectDetail.closureSummary.facts.openRiskCount,
},
updatedAt: projectDetail.updatedAt,
},
],
});
return;
}
if (pathName === "/api/employees") {
await fulfillJson(route, {
page: 1,
pageSize: 100,
total: 1,
items: [
{
id: visualMe.id,
employeeNo: visualMe.employeeNo,
name: visualMe.name,
email: visualMe.email,
phone: visualMe.phone,
status: "active",
},
],
});
return;
}
if (pathName === "/api/contracts-registry/expiring") {
await fulfillJson(route, {
days: Number(url.searchParams.get("days") ?? 30),
total: 1,
items: [contractRegistryItem],
});
return;
}
if (request.method() === "GET" && ["/api/contracts-registry/contract-wave3", "/api/contracts-registry/contract-wave3-second"].includes(pathName)) {
await fulfillJson(route, { item: pathName.endsWith("-second") ? secondContractRegistryDetail : contractRegistryDetail });
return;
}
if (pathName === "/api/contracts-registry/contract-wave3" && request.method() === "PATCH") {
await fulfillJson(route, { item: { ...contractRegistryDetail, status: "archived", persistedStatus: "archived" } });
return;
}
if (pathName === "/api/contracts-registry") {
await fulfillJson(route, { items: [contractRegistryItem, secondContractRegistryItem] });
return;
}
if (pathName === "/api/purchase-requests/purchase-wave3") {
await fulfillJson(route, purchaseDetail);
return;
}
if (pathName === "/api/workflow/tasks/summary") {
await fulfillJson(route, {
scope: "mine",
pendingTotal: 1,
overdueTotal: 0,
earliestPendingAt: workflowTask.createdAt,
averagePendingAgeHours: 1,
byEntityType: [{ key: "reimbursement", pendingTotal: 1, overdueTotal: 0 }],
byStep: [{ key: "主管审批", pendingTotal: 1, overdueTotal: 0 }],
byAssigneeRole: [{ key: "manager", pendingTotal: 1, overdueTotal: 0 }],
});
return;
}
if (pathName === "/api/workflow/tasks/task-wave3") {
await fulfillJson(route, workflowDetail);
return;
}
if (pathName === "/api/workflow/tasks/task-wave3-completed") {
await fulfillJson(route, completedWorkflowDetail);
return;
}
if (pathName === "/api/workflow/tasks") {
await fulfillJson(route, {
page: 1,
pageSize: Number(url.searchParams.get("pageSize") ?? 100),
total: 1,
items: [workflowTask],
});
return;
}
if (pathName === "/api/workflow/approvers") {
await fulfillJson(route, {
page: 1,
pageSize: 50,
total: 1,
items: [
{
id: "approver-wave3",
employeeNo: "HYD-A1",
name: "审批人",
email: "[email protected]",
roleCodes: ["manager"],
},
],
});
return;
}
if (pathName === "/api/asset-categories") {
await fulfillJson(route, {
includeArchived: false,
items: [assetDetail.category],
});
return;
}
if (pathName === "/api/assets/assignees") {
await fulfillJson(route, { items: [assetDetail.assignee] });
return;
}
if (pathName === "/api/assets/asset-wave3") {
await fulfillJson(route, { asset: assetDetail });
return;
}
if (pathName === "/api/assets") {
await fulfillJson(route, { mine: false, items: [assetDetail] });
return;
}
if (pathName === "/api/master-data/legal-entities") {
await fulfillJson(route, { includeArchived: false, items: [assetDetail.legalEntity] });
return;
}
if (pathName === "/api/master-data/cost-centers") {
await fulfillJson(route, { includeArchived: false, items: [assetDetail.costCenter] });
return;
}
if (pathName === "/api/master-data/document-categories") {
await fulfillJson(route, { includeArchived: false, items: [] });
return;
}
if (pathName === "/api/portal/home") {
await fulfillJson(route, {
workspace: { id: "workspace-wave3", persisted: true, profile: "small_team_internal" },
greeting: { name: visualMe.name },
cards: [],
pendingTasks: { count: 1, href: "/workflow/tasks" },
myApplications: [],
riskSummary: null,
announcements: [],
quickLinks: [],
todayPresence: null,
teamPresence: null,
weeklyReport: null,
weeklySummary: null,
todoSummary: null,
projectSummary: null,
peopleLifecycle: null,
financeSummary: {
pendingBatchCount: 2,
failedItemCount: 0,
riskCount: 1,
paymentBatchEnabled: true,
href: "/payment-batches",
},
adoptionSummary: null,
recentActivities: [],
teamStatusBoard: [],
});
return;
}
if (pathName === "/api/leave-requests") {
await fulfillJson(route, { page: 1, pageSize: 8, total: 0, scope: "own", items: [] });
return;
}
if (pathName === "/api/purchase-requests") {
await fulfillJson(route, { page: 1, pageSize: 8, total: 0, items: [] });
return;
}
if (pathName === "/api/employees/employee-wave3/profile") {
await fulfillJson(route, {
employee: {
id: visualMe.id,
employeeNo: visualMe.employeeNo,
name: visualMe.name,
phone: "",
email: visualMe.email,
status: "active",
employmentStatus: "active",
departmentId: "dept-wave3",
title: "项目经理",
directManagerId: null,
hireDate: "2026-01-01",
probationEndDate: null,
lastWorkingDate: null,
workLocation: "北京",
emergencyContactName: null,
emergencyContactPhone: null,
notes: null,
avatarUrl: null,
archivedAt: null,
roleCodes: visualMe.roleCodes,
department: { id: "dept-wave3", name: "研发部" },
directManager: null,
},
lifecycle: { events: [], checklistTasks: [], blockers: { openAssetCount: 0, pendingWorkflowTaskCount: 0, canCompleteOffboarding: true } },
assignedAssets: [],
organization: { reportingChain: [], directReports: [] },
});
return;
}
await fulfillJson(route, {});
});
}
async function login(page: Page) {
await page.goto("/login");
await page.getByTestId("login-identifier").fill(visualUser.identifier);
await page.getByTestId("login-password").fill(visualUser.password);
await page.getByTestId("login-submit").click();
await page.waitForURL(/\/(portal|mobile)/);
await page.waitForLoadState("networkidle").catch(() => undefined);
}
async function expectNoHorizontalOverflow(page: Page) {
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
expect(overflow).toBeLessThanOrEqual(2);
}
test.beforeEach(async ({ page }) => {
await mockWave3Apis(page);
await login(page);
});
test("PC 壳层与详情断点保持结构一致", async () => {
const appShell = readFileSync(path.resolve(process.cwd(), "src/layouts/AppShellLayout.tsx"), "utf8");
const sidebar = readFileSync(path.resolve(process.cwd(), "src/components/navigation/Sidebar.tsx"), "utf8");
const topbar = readFileSync(path.resolve(process.cwd(), "src/components/navigation/Topbar.tsx"), "utf8");
const detailTemplate = readFileSync(path.resolve(process.cwd(), "src/components/patterns/BusinessDetailTemplate.tsx"), "utf8");
const styles = readFileSync(path.resolve(process.cwd(), "src/index.css"), "utf8");
expect(appShell).toContain('"xl:ml-72"');
expect(appShell).not.toContain('"lg:ml-72"');
expect(sidebar).toContain("xl:flex xl:flex-col");
expect(sidebar).not.toContain("lg:flex lg:flex-col");
expect(topbar).toContain('className="xl:hidden"');
expect(topbar).not.toContain('className="lg:hidden" size="icon" variant="secondary" aria-label="打开菜单"');
expect(detailTemplate).toContain("detail-action-panel lg:sticky lg:top-24");
expect(detailTemplate).toMatch(/if \(!hasStatus && !hasActionContent && !hasSummary && !hasChildren\) \{\s*return null;/u);
expect(styles).toMatch(/\.detail-workspace-grid\s*\{[\s\S]*?lg:grid-cols-/u);
expect(styles).toMatch(/\.workflow-action-panel\s*\{[\s\S]*?relative z-30/u);
});
test("已完成审批只保留中文只读摘要", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto("/workflow/tasks/task-wave3-completed");
await expect(page.getByTestId("workflow-task-action-panel")).toBeVisible();
await expect(page.getByTestId("reimbursement-desktop-workspace")).toBeVisible();
await expect(page.getByTestId("workflow-task-action-panel")).toContainText("审批角色:财务");
await expect(page.getByTestId("reimbursement-desktop-workspace")).toContainText("2026年5月27日 18:10");
await expect(page.getByTestId("workflow-task-approve")).toHaveCount(0);
await expect(page.getByTestId("workflow-task-return-toggle")).toHaveCount(0);
await expect(page.getByTestId("workflow-task-reject-toggle")).toHaveCount(0);
await expect(page.getByText("更多", { exact: true })).toHaveCount(0);
await expect(page.getByText("通过 · 王顺理", { exact: true })).toBeVisible();
await expect(page.getByText("2026年5月27日 18:10", { exact: true })).toBeVisible();
await expect(page.getByRole("link", { name: "返回审批列表" })).toBeVisible();
});
test("1280px 资产详情保持可用双栏和可点击操作区", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto("/assets/asset-wave3");
await expect(page.getByTestId("asset-name")).toHaveValue(assetDetail.name);
await expect(page.getByTestId("asset-detail-action-panel")).toBeVisible();
await expect(page.locator(".detail-action-panel")).toHaveCSS("position", "sticky");
const assetLayout = await page.locator(".detail-workspace-grid").evaluate((workspace) => {
const [main, aside] = Array.from(workspace.children) as HTMLElement[];
return {
mainWidth: main.getBoundingClientRect().width,
asideWidth: aside.getBoundingClientRect().width,
columns: getComputedStyle(workspace).gridTemplateColumns.split(" ").filter(Boolean).length,
overflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
};
});
expect(assetLayout.columns).toBe(2);
expect(assetLayout.mainWidth).toBeGreaterThanOrEqual(560);
expect(assetLayout.asideWidth).toBeGreaterThanOrEqual(280);
expect(assetLayout.overflow).toBeLessThanOrEqual(2);
const saveButtonHitTest = await page.getByTestId("asset-save").evaluate((button) => {
const rect = button.getBoundingClientRect();
const hit = document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
return hit === button || button.contains(hit);
});
expect(saveButtonHitTest).toBe(true);
});
test("报销详情固定使用上海时区并收敛只读信息", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 960 });
await page.goto("/reimbursements/reim-wave3");
await expect(page.getByTestId("reimbursement-desktop-workspace")).toBeVisible();
await expect(page.getByRole("heading", { name: "当前状态与下一步" })).toHaveCount(0);
await expect(page.getByRole("heading", { name: "业务摘要" })).toHaveCount(0);
await expect(page.getByRole("heading", { name: "预算与审核提示" })).toHaveCount(0);
await expect(page.getByTestId("reimbursement-back")).toContainText("返回报销列表");
await expect(page.getByTestId("reimbursement-desktop-workspace")).toContainText("¥1,860.00");
await expect(page.getByTestId("reimbursement-approval-evidence")).toContainText("¥860.00");
await expect(page.getByTestId("approval-attachments")).toContainText("现场说明.pdf");
await expect(page.getByTestId("reimbursement-approval-evidence").getByRole("button", { name: /查看发票原件/ })).toHaveCount(1);
await expect(page.getByTestId("reimbursement-approval-evidence").getByRole("button", { name: /查看补充附件/ })).toHaveCount(1);
await expect(page.getByTestId("reimbursement-approval-evidence").getByRole("button", { name: /移除|删除/ })).toHaveCount(0);
const pcInvoicePreview = page.getByTestId("reimbursement-approval-evidence").getByRole("button", { name: /查看发票原件/ });
const pcMaterialPreview = page.getByTestId("reimbursement-approval-evidence").getByRole("button", { name: "查看完整材料" });
const pcAttachmentPreview = page.getByTestId("reimbursement-approval-evidence").getByRole("button", { name: /查看补充附件/ });
await pcInvoicePreview.focus();
await expect(pcInvoicePreview).toBeFocused();
await page.keyboard.press("Tab");
await expect(pcMaterialPreview).toBeFocused();
await page.keyboard.press("Tab");
await expect(pcAttachmentPreview).toBeFocused();
await expect(page.getByTestId("reimbursement-workflow-progress")).toContainText("负责人复核");
await expect(page.getByTestId("reimbursement-workflow-progress")).toContainText("付款复核");
await expect(page.getByTestId("reimbursement-workflow-progress")).not.toContainText(/第 \d+ 步/);
await page.goto("/workflow/tasks/task-wave3");
await expect(page.getByTestId("reimbursement-desktop-workspace")).toContainText("2026年5月27日 17:00");
await expect(page.getByText(`提交 · ${visualMe.name}`)).toBeVisible();
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/mobile/reimbursements/reim-wave3");
await expect(page.getByTestId("mobile-reimbursement-detail-header")).toBeVisible();
await expect(page.getByRole("heading", { name: "明细与票据" })).toBeVisible();
await expect(page.getByRole("heading", { name: "当前办理摘要" })).toHaveCount(0);
await expect(page.getByTestId("reimbursement-mobile-workspace")).toContainText(visualMe.name);
await expect(page.getByTestId("reimbursement-mobile-workspace")).toContainText("¥1,860.00");
await expect(page.getByTestId("reimbursement-approval-evidence")).toContainText("¥860.00");
await expect(page.getByTestId("mobile-reimbursement-processing-progress")).toContainText("负责人复核");
await expect(page.getByTestId("approval-attachments")).toContainText("现场说明.pdf");
await expect(page.getByTestId("reimbursement-approval-evidence").getByRole("button", { name: /查看发票原件/ })).toHaveCount(1);
await expect(page.getByTestId("reimbursement-approval-evidence").getByRole("button", { name: /查看补充附件/ })).toHaveCount(1);
await expect(page.getByTestId("reimbursement-approval-evidence").getByRole("button", { name: /移除|删除/ })).toHaveCount(0);
const mobileInvoicePreview = page.getByTestId("reimbursement-approval-evidence").getByRole("button", { name: /查看发票原件/ });
const mobileMaterialPreview = page.getByTestId("reimbursement-approval-evidence").getByRole("button", { name: "查看完整材料" });
const mobileAttachmentPreview = page.getByTestId("reimbursement-approval-evidence").getByRole("button", { name: /查看补充附件/ });
await mobileInvoicePreview.focus();
await expect(mobileInvoicePreview).toBeFocused();
await page.keyboard.press("Tab");
await expect(mobileMaterialPreview).toBeFocused();
await page.keyboard.press("Tab");
await expect(mobileAttachmentPreview).toBeFocused();
await expect(page.getByTestId("mobile-reimbursement-processing-progress")).toContainText("付款完成");
await expect(page.getByTestId("mobile-reimbursement-actions")).toHaveCount(0);
await expect(page.getByText("PDF/JPG/PNG/WebP,单次最多 5 个")).toHaveCount(0);
await expectNoHorizontalOverflow(page);
});
test("Wave3 PC 详情和通知中心视觉截图", async ({ page }) => {
test.setTimeout(300_000);
await page.setViewportSize({ width: 1440, height: 960 });
await page.goto("/reimbursements/reim-wave3");
await expect(page.getByTestId("reimbursement-desktop-workspace")).toBeVisible();
await expect(page.getByRole("heading", { name: `报销单 ${reimbursementDetail.formNumber}` })).toBeVisible();
await expect(page.getByRole("heading", { name: "明细与票据" })).toBeVisible();
await expect(page.getByRole("heading", { name: "当前状态与下一步" })).toHaveCount(0);
await expect(page.getByRole("heading", { name: "业务摘要" })).toHaveCount(0);
await expect(page.getByRole("heading", { name: "预算与审核提示" })).toHaveCount(0);
await expect(page.getByTestId("approval-attachments")).toContainText("现场说明.pdf");
await expect(page.getByTestId("reimbursement-first-screen-actions")).toHaveCount(0);
await expect(page.getByTestId("reimbursement-action-panel")).toBeVisible();
await expect(page.getByTestId("reimbursement-action-panel").getByRole("button", { name: "撤回" })).toBeVisible();
await expect(page.getByRole("link", { name: /看审批进度|看处理进度|查看付款结果/ })).toHaveCount(0);
await expect(page.getByTestId("reimbursement-payment-status")).toHaveCount(0);
await page.getByTestId("reimbursement-more-toggle").click();
await expect(page.getByTestId("reimbursement-secondary-detail")).toBeVisible();
await expect(page.getByTestId("reimbursement-secondary-payment-status")).toHaveCount(0);
await page.screenshot({ path: screenshotPath("01-pc-reimbursement-detail.png"), fullPage: true });
await page.goto("/workflow/tasks/task-wave3");
await page.setViewportSize({ width: 1100, height: 768 });
await expect(page.getByTestId("reimbursement-desktop-workspace")).toBeVisible();
await expect(page.locator(".app-shell-sidebar")).toBeHidden();
await expect(page.getByRole("button", { name: "打开菜单" })).toBeVisible();
await expect(page.locator(".detail-workspace-grid")).toHaveCSS("grid-template-columns", /.+ .+/);
await expectNoHorizontalOverflow(page);
await page.setViewportSize({ width: 1280, height: 800 });
await expect(page.locator(".app-shell-sidebar")).toBeVisible();
await expect(page.getByRole("button", { name: "打开菜单" })).toBeHidden();
await expect(page.getByTestId("workflow-task-next-action")).toHaveCount(0);
await expect(page.getByText("审批动作")).toHaveCount(0);
await expect(page.getByTestId("workflow-task-action-panel")).toBeVisible();
await expect(page.getByTestId("reimbursement-desktop-workspace")).toContainText("2026年5月27日 17:00");
const workflowActionPanel = page.getByTestId("workflow-task-action-panel").locator("xpath=ancestor::section[1]");
await expect(workflowActionPanel).toHaveCSS("position", "sticky");
await expect(workflowActionPanel).toHaveCSS("top", "96px");
const workflowLayout = await page.locator(".detail-workspace-grid").evaluate((workspace) => {
const [main, aside] = Array.from(workspace.children) as HTMLElement[];
return {
mainWidth: main.getBoundingClientRect().width,
asideWidth: aside.getBoundingClientRect().width,
columns: getComputedStyle(workspace).gridTemplateColumns.split(" ").filter(Boolean).length,
overflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
};
});
expect(workflowLayout.columns).toBe(2);
expect(workflowLayout.mainWidth).toBeGreaterThanOrEqual(560);
expect(workflowLayout.asideWidth).toBeGreaterThanOrEqual(280);
expect(workflowLayout.overflow).toBeLessThanOrEqual(2);
const formNumberLineCount = await page.getByRole("heading", { name: `报销单 ${reimbursementDetail.formNumber}` }).evaluate((element) => {
const range = document.createRange();
range.selectNodeContents(element);
return range.getClientRects().length;
});
expect(formNumberLineCount).toBe(1);
await page.setViewportSize({ width: 1440, height: 960 });
await expect(page.getByTestId("workflow-task-reimbursement-detail-link")).toHaveCount(0);
await expect(page.getByTestId("reimbursement-approval-evidence")).toBeVisible();
await page.screenshot({ path: screenshotPath("02-pc-workflow-detail.png"), fullPage: true });
await page.goto("/notifications");
await expect(page.getByRole("heading", { name: "通知" })).toBeVisible();
await expect(page.getByTestId("notification-row").first()).toBeVisible();
await expect(page.locator("table")).toHaveCount(0);
await page.screenshot({ path: screenshotPath("03-pc-notifications.png"), fullPage: true });
await page.goto("/projects");
await expect(page.getByRole("heading", { name: "项目", exact: true })).toBeVisible();
await expect(page.getByPlaceholder("搜索项目编号、项目名、客户名")).toBeVisible();
await expect(page.getByRole("button", { name: "创建项目" })).toHaveCount(0);
await page.getByRole("button", { name: "新建项目" }).click();
await expect(page.getByRole("button", { name: "创建项目" })).toBeVisible();
await page.goto("/projects/project-wave3");
await expect(page.getByRole("heading", { name: /HYB-W3/ })).toBeVisible();
await expect(page.getByTestId("project-current-command")).toBeVisible();
await expect(page.locator("table").first()).toBeVisible();
await expect(page.getByText("离线档案柜 A-01")).toHaveCount(0);
await page.screenshot({ path: screenshotPath("03-pc-project-detail.png"), fullPage: true });
await page.goto("/contracts-registry?projectId=project-wave3");
await expect(page.getByRole("heading", { name: "合同", exact: true })).toBeVisible();
const contractRows = page.getByTestId("contracts-registry-table").getByTestId("contracts-registry-row");
await expect(contractRows).toHaveCount(2);
await expect(contractRows.first()).toBeVisible();
await page.getByRole("button", { name: "更多筛选", exact: true }).click();
await expect(page.getByLabel("到期开始日期")).toBeVisible();
await expect(page.getByRole("button", { name: "编辑", exact: true })).toHaveCount(0);
await expect(page.getByRole("button", { name: "更多", exact: true })).toHaveCount(0);
await page.getByTestId("contracts-registry-table").getByRole("link", { name: "查看详情" }).first().click();
await expect(page).toHaveURL(/\/contracts-registry\/contract-wave3$/);
await expect(page.getByText("状态与下一步", { exact: true })).toBeVisible();
await expect(page.getByText("涉密资料请登记线下归档位置", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "归档", exact: true }).click();
await expect(page.getByTestId("action-confirm-dialog")).toContainText("确认归档");
await page.getByTestId("action-confirm-cancel").click();
await expect(page.getByTestId("action-confirm-dialog")).toHaveCount(0);
await page.screenshot({ path: screenshotPath("04-pc-contracts-registry.png"), fullPage: true });
});
test("PC 报销审批只接受安全审批列表返回目标", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 960 });
await page.goto("/reimbursements?status=reviewing&page=1&scope=all");
await page.getByTestId("reimbursement-detail-link").first().click();
await expect(page).toHaveURL(/\/workflow\/tasks\/task-wave3$/);
await expect(page.getByRole("link", { name: "返回审批列表" })).toBeVisible();
await page.getByRole("link", { name: "返回审批列表" }).click();
await expect(page).toHaveURL(/\/workflow\/tasks$/);
await page.goto("/workflow/tasks/task-wave3");
await expect(page.getByTestId("reimbursement-desktop-workspace")).toBeVisible();
await expect(page.getByTestId("workflow-task-reimbursement-detail-link")).toHaveCount(0);
});
test("/contracts-registry 在 768x1024 与 1024x768 下操作列可达", async ({ page }) => {
for (const viewport of [
{ width: 768, height: 1024 },
{ width: 1024, height: 768 },
]) {
await page.setViewportSize(viewport);
await page.goto("/contracts-registry");
await expect(page.getByRole("heading", { name: "合同", exact: true })).toBeVisible();
const table = page.getByTestId("contracts-registry-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("link", { name: "查看详情" }).first();
await expect(operation).toBeVisible();
await operation.click({ trial: true });
await page.screenshot({
path: screenshotPath(`contracts-registry-${viewport.width}x${viewport.height}.png`),
fullPage: true,
});
}
});
test("合同详情编辑离开路由时支持取消保持与确认放弃", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 900 });
await page.goto("/contracts-registry");
const table = page.getByTestId("contracts-registry-table");
await table.getByRole("link", { name: "查看详情" }).first().click();
await expect(page).toHaveURL(/\/contracts-registry\/contract-wave3$/);
await page.getByRole("button", { name: "编辑", exact: true }).first().click();
const counterparty = page.getByTestId("contract-counterparty");
await counterparty.fill("合成未保存合同相对方");
await page.getByRole("link", { name: "返回合同列表" }).click();
await expect(page.getByTestId("unsaved-changes-dialog")).toContainText("合同表单尚未保存");
await page.getByTestId("unsaved-changes-continue").click();
await expect(page).toHaveURL(/\/contracts-registry\/contract-wave3$/);
await expect(counterparty).toHaveValue("合成未保存合同相对方");
await page.getByRole("link", { name: "返回合同列表" }).click();
await page.getByTestId("unsaved-changes-discard").click();
await expect(page).toHaveURL(/\/contracts-registry$/);
await page.getByTestId("contracts-registry-table").getByRole("link", { name: "查看详情" }).nth(1).click();
await expect(page.getByText(secondContractRegistryItem.counterpartyName, { exact: true })).toBeVisible();
});
test("Wave3 移动关键页视觉截图", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/mobile");
await expect(page.getByRole("heading", { name: "今日待处理" })).toBeVisible();
await expect(page.getByTestId("mobile-action-grid")).toBeVisible();
await expect(page.locator("table")).toHaveCount(0);
await expectNoHorizontalOverflow(page);
await page.screenshot({ path: screenshotPath("04-mobile-home.png"), fullPage: true });
await page.goto("/mobile/reimbursements");
await expect(page.getByRole("heading", { name: "移动报销" })).toBeVisible();
await expect(page.getByTestId("mobile-reimbursements-focus")).toBeVisible();
await expect(page.getByTestId("mobile-reimbursements-list")).toBeVisible();
await expect(page.locator("table")).toHaveCount(0);
await expectNoHorizontalOverflow(page);
await page.screenshot({ path: screenshotPath("05-mobile-reimbursements.png"), fullPage: true });
await page.goto("/mobile/reimbursements/reim-wave3");
await expect(page.getByTestId("reimbursement-mobile-workspace")).toBeVisible();
await expect(page.getByRole("heading", { name: "明细与票据" })).toBeVisible();
await expect(page.getByRole("heading", { name: "当前办理摘要" })).toHaveCount(0);
await expect(page.getByTestId("reimbursement-mobile-workspace")).toContainText(visualMe.name);
await expect(page.getByTestId("approval-attachments")).toContainText("现场说明.pdf");
await expect(page.getByTestId("mobile-reimbursement-actions")).toHaveCount(0);
await expect(page.getByText("PDF/JPG/PNG/WebP,单次最多 5 个")).toHaveCount(0);
await expect(page.getByTestId("reimbursement-approval-evidence")).toBeVisible();
await expect(page.locator("table")).toHaveCount(0);
await expectNoHorizontalOverflow(page);
await page.screenshot({ path: screenshotPath("06-mobile-reimbursement-detail.png"), fullPage: true });
await page.goto("/mobile/approvals");
await expect(page.locator("h1", { hasText: "待我审批" })).toBeVisible();
await expect(page.getByTestId("mobile-approvals-focus")).toBeVisible();
await expect(page.getByTestId("mobile-approvals-list")).toBeVisible();
await expect(page.locator("table")).toHaveCount(0);
await expectNoHorizontalOverflow(page);
await page.screenshot({ path: screenshotPath("07-mobile-approvals.png"), fullPage: true });
await page.goto("/mobile/approvals/task-wave3");
await expect(page.getByTestId("reimbursement-mobile-workspace")).toBeVisible();
await expect(page.getByTestId("reimbursement-approval-evidence")).toBeVisible();
await expect(page.locator("table")).toHaveCount(0);
await expectNoHorizontalOverflow(page);
await page.screenshot({ path: screenshotPath("08-mobile-approval-detail.png"), fullPage: true });
await page.goto("/mobile/notifications");
await expect(page.getByTestId("mobile-notifications-focus")).toBeVisible();
await expect(page.getByTestId("mobile-notifications-list")).toBeVisible();
await expect(page.getByTestId("mobile-notification-card").first()).toBeVisible();
await expect(page.locator("table")).toHaveCount(0);
await expectNoHorizontalOverflow(page);
await page.screenshot({ path: screenshotPath("09-mobile-notifications.png"), fullPage: true });
await page.goto("/mobile/purchase-requests/purchase-wave3");
await expect(page.getByTestId("mobile-purchase-detail-summary")).toBeVisible();
await expect(page.getByTestId("mobile-purchase-detail-related")).toBeVisible();
await expect(page.getByTestId("mobile-purchase-detail-items")).toBeVisible();
await expect(page.locator("table")).toHaveCount(0);
await expectNoHorizontalOverflow(page);
await page.screenshot({ path: screenshotPath("10-mobile-purchase-detail.png"), fullPage: true });
await page.goto("/mobile/me");
await expect(page.getByRole("heading", { name: "我的" })).toBeVisible();
await expect(page.getByTestId("mobile-more-links")).toBeVisible();
await expect(page.locator("table")).toHaveCount(0);
await expectNoHorizontalOverflow(page);
await page.screenshot({ path: screenshotPath("11-mobile-me.png"), fullPage: true });
});