R7E-16E/R7E-16F 高风险操作确认统一
这个提交包含在:
@@ -0,0 +1,321 @@
|
||||
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 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.getByTestId("action-confirm-submit").click();
|
||||
await expect.poll(() => calls.deleteReimbursement).toBe(1);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户