322 行
9.9 KiB
TypeScript
322 行
9.9 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 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);
|
|
});
|
|
});
|