文件
hyb-oa/client/tests/e2e/system-action-confirmation.spec.ts
T

325 行
10 KiB
TypeScript

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