完成五人团队正式内用最小收口(07-18-small-team-internal-readiness)
这个提交包含在:
@@ -53,6 +53,8 @@ const me = {
|
||||
|
||||
const reimbursementId = "reimbursement-action-confirm";
|
||||
const paymentBatchId = "payment-batch-action-confirm";
|
||||
const reimbursementAttachmentId = "reimbursement-attachment-action-confirm";
|
||||
const paymentReceiptId = "payment-receipt-action-confirm";
|
||||
|
||||
function reimbursementListItem() {
|
||||
return {
|
||||
@@ -78,6 +80,50 @@ function reimbursementListItem() {
|
||||
};
|
||||
}
|
||||
|
||||
function reimbursementDetail() {
|
||||
return {
|
||||
...reimbursementListItem(),
|
||||
submitterId: me.id,
|
||||
ownerId: me.id,
|
||||
departmentId: "department-action-confirm",
|
||||
department: { id: "department-action-confirm", name: "合成部门" },
|
||||
submittedAt: null,
|
||||
projectId: null,
|
||||
linkedTravelRequestId: null,
|
||||
linkedTravelRequest: null,
|
||||
owner: { id: me.id, employee_no: me.employeeNo, name: me.name },
|
||||
submitter: { id: me.id, employee_no: me.employeeNo, name: me.name },
|
||||
items: [],
|
||||
invoices: [],
|
||||
attachments: [
|
||||
{
|
||||
id: reimbursementAttachmentId,
|
||||
originalName: "合成报销敏感附件.pdf",
|
||||
mimeType: "application/pdf",
|
||||
fileSize: 1024,
|
||||
fileSha256: "synthetic-attachment-hash",
|
||||
governanceStatus: "approved",
|
||||
sensitivityLevel: "confidential",
|
||||
attachmentType: "supporting_document",
|
||||
downloadPolicy: "reason_required",
|
||||
reviewedBy: null,
|
||||
reviewedAt: null,
|
||||
reviewNote: null,
|
||||
archivedBy: null,
|
||||
archivedAt: null,
|
||||
archiveNote: null,
|
||||
uploadedBy: { id: me.id, employeeNo: me.employeeNo, name: me.name },
|
||||
createdAt: "2026-05-15T08:05:00.000Z",
|
||||
},
|
||||
],
|
||||
budgetHint: null,
|
||||
pendingWorkflow: null,
|
||||
workflowProgress: null,
|
||||
approvalHistory: [],
|
||||
riskFlags: [],
|
||||
};
|
||||
}
|
||||
|
||||
function paymentBatch(status: "draft" | "reviewed" = "reviewed") {
|
||||
return {
|
||||
id: paymentBatchId,
|
||||
@@ -96,7 +142,7 @@ function paymentBatch(status: "draft" | "reviewed" = "reviewed") {
|
||||
completedReviewCount: status === "draft" ? 0 : 1,
|
||||
nextStep: status === "draft" ? "first_review" : "ready_for_execute",
|
||||
makerCheckerRequired: false,
|
||||
reviewerPermission: "risk.review.manage",
|
||||
reviewerPermission: "payment.batch.review",
|
||||
},
|
||||
reviewComment: null,
|
||||
secondaryReviewComment: null,
|
||||
@@ -111,6 +157,7 @@ function paymentBatch(status: "draft" | "reviewed" = "reviewed") {
|
||||
executedBy: null,
|
||||
cancelledAt: null,
|
||||
cancelledBy: null,
|
||||
cancelledReason: null,
|
||||
items: [
|
||||
{
|
||||
id: "payment-batch-item-action-confirm",
|
||||
@@ -133,11 +180,20 @@ function paymentBatch(status: "draft" | "reviewed" = "reviewed") {
|
||||
};
|
||||
}
|
||||
|
||||
async function mockSystemActionApis(page: Page) {
|
||||
async function mockSystemActionApis(
|
||||
page: Page,
|
||||
options: { batchStatus?: "draft" | "reviewed"; meOverride?: Partial<typeof me> } = {},
|
||||
) {
|
||||
const currentMe = { ...me, ...options.meOverride };
|
||||
const batchStatus = options.batchStatus ?? "reviewed";
|
||||
const calls = {
|
||||
deleteReimbursement: 0,
|
||||
deleteReimbursementAttachment: 0,
|
||||
exportReimbursements: 0,
|
||||
executePaymentBatch: 0,
|
||||
voidPaymentReceipt: 0,
|
||||
attachmentDeleteReasons: [] as string[],
|
||||
receiptVoidReasons: [] as string[],
|
||||
};
|
||||
|
||||
await page.route((url) => url.pathname.startsWith("/api/"), async (route) => {
|
||||
@@ -191,7 +247,7 @@ async function mockSystemActionApis(page: Page) {
|
||||
}
|
||||
|
||||
if (path === "/api/me") {
|
||||
await fulfillJson(route, me);
|
||||
await fulfillJson(route, currentMe);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -220,6 +276,22 @@ async function mockSystemActionApis(page: Page) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === `/api/reimbursements/${reimbursementId}` && request.method() === "GET") {
|
||||
await fulfillJson(route, reimbursementDetail());
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
path === `/api/reimbursements/${reimbursementId}/attachments/${reimbursementAttachmentId}` &&
|
||||
request.method() === "DELETE"
|
||||
) {
|
||||
calls.deleteReimbursementAttachment += 1;
|
||||
const body = request.postDataJSON() as { reason?: string };
|
||||
calls.attachmentDeleteReasons.push(body.reason ?? "");
|
||||
await fulfillJson(route, { attachmentId: reimbursementAttachmentId, outcome: "deleted" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === `/api/reimbursements/${reimbursementId}` && request.method() === "DELETE") {
|
||||
calls.deleteReimbursement += 1;
|
||||
await fulfillJson(route, { id: reimbursementId, deleted: true });
|
||||
@@ -250,7 +322,45 @@ async function mockSystemActionApis(page: Page) {
|
||||
}
|
||||
|
||||
if (path === "/api/payment-batches" && request.method() === "GET") {
|
||||
await fulfillJson(route, { items: [paymentBatch("reviewed")] });
|
||||
await fulfillJson(route, { items: [paymentBatch(batchStatus)] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === `/api/payment-batches/${paymentBatchId}/receipt-attachments` && request.method() === "GET") {
|
||||
await fulfillJson(route, {
|
||||
items: [
|
||||
{
|
||||
id: paymentReceiptId,
|
||||
originalName: "合成银行回单.pdf",
|
||||
mimeType: "application/pdf",
|
||||
fileSize: 2048,
|
||||
fileSha256: "synthetic-receipt-hash",
|
||||
governanceStatus: "approved",
|
||||
sensitivityLevel: "confidential",
|
||||
attachmentType: "payment_receipt",
|
||||
downloadPolicy: "reason_required",
|
||||
reviewedBy: null,
|
||||
reviewedAt: null,
|
||||
reviewNote: null,
|
||||
archivedBy: null,
|
||||
archivedAt: null,
|
||||
archiveNote: null,
|
||||
uploadedBy: { id: me.id, employeeNo: me.employeeNo, name: me.name },
|
||||
createdAt: "2026-05-15T08:20:00.000Z",
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
path === `/api/payment-batches/${paymentBatchId}/receipt-attachments/${paymentReceiptId}/void` &&
|
||||
request.method() === "POST"
|
||||
) {
|
||||
calls.voidPaymentReceipt += 1;
|
||||
const body = request.postDataJSON() as { reason?: string };
|
||||
calls.receiptVoidReasons.push(body.reason ?? "");
|
||||
await fulfillJson(route, { attachmentId: paymentReceiptId, outcome: "voided", voidedAt: "2026-05-15T08:30:00.000Z" });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -320,4 +430,69 @@ test.describe("R7E 全站高风险操作二次确认", () => {
|
||||
await page.getByTestId("action-confirm-submit").click();
|
||||
await expect.poll(() => calls.executePaymentBatch).toBe(1);
|
||||
});
|
||||
|
||||
test("高敏附件删除要求对象、后果和理由,取消与理由不足均为零请求", async ({ page }) => {
|
||||
const calls = await mockSystemActionApis(page);
|
||||
await login(page, user);
|
||||
await page.goto(`/reimbursements/${reimbursementId}`);
|
||||
|
||||
const attachment = page.locator(".detail-evidence-card").filter({ hasText: "合成报销敏感附件.pdf" });
|
||||
await expect(attachment).toBeVisible();
|
||||
await attachment.getByRole("button", { name: "删除" }).click();
|
||||
const dialog = page.getByTestId("attachment-delete-confirm-dialog");
|
||||
await expect(dialog).toContainText("合成报销敏感附件.pdf");
|
||||
await expect(dialog).toContainText("无法在 OA 内恢复");
|
||||
expect(calls.deleteReimbursementAttachment).toBe(0);
|
||||
|
||||
await page.getByTestId("attachment-delete-confirm-submit").click();
|
||||
await expect(dialog).toContainText("请填写至少 2 个字的删除理由");
|
||||
expect(calls.deleteReimbursementAttachment).toBe(0);
|
||||
await dialog.getByRole("button", { name: "取消" }).click();
|
||||
expect(calls.deleteReimbursementAttachment).toBe(0);
|
||||
|
||||
await attachment.getByRole("button", { name: "删除" }).click();
|
||||
await page.getByRole("textbox", { name: "删除理由" }).fill("附件重复上传");
|
||||
await page.getByTestId("attachment-delete-confirm-submit").click();
|
||||
await expect.poll(() => calls.deleteReimbursementAttachment).toBe(1);
|
||||
expect(calls.attachmentDeleteReasons).toEqual(["附件重复上传"]);
|
||||
});
|
||||
|
||||
test("付款回单作废要求对象、后果和理由,取消与理由不足均为零请求", async ({ page }) => {
|
||||
const calls = await mockSystemActionApis(page);
|
||||
await login(page, user);
|
||||
await page.goto(`/payment-batches?id=${paymentBatchId}`);
|
||||
|
||||
const receipt = page.locator(".detail-evidence-card").filter({ hasText: "合成银行回单.pdf" });
|
||||
await expect(receipt).toBeVisible();
|
||||
await receipt.getByRole("button", { name: "作废" }).click();
|
||||
const dialog = page.getByRole("dialog");
|
||||
await expect(dialog).toContainText("合成银行回单.pdf");
|
||||
await expect(dialog).toContainText("文件记录会保留并标记为已作废");
|
||||
await expect(page.getByRole("button", { name: "确认作废" })).toBeDisabled();
|
||||
expect(calls.voidPaymentReceipt).toBe(0);
|
||||
|
||||
await page.getByRole("textbox", { name: "回单作废原因" }).fill("一");
|
||||
await expect(page.getByRole("button", { name: "确认作废" })).toBeDisabled();
|
||||
expect(calls.voidPaymentReceipt).toBe(0);
|
||||
await dialog.getByRole("button", { name: "返回" }).click();
|
||||
expect(calls.voidPaymentReceipt).toBe(0);
|
||||
|
||||
await receipt.getByRole("button", { name: "作废" }).click();
|
||||
await page.getByRole("textbox", { name: "回单作废原因" }).fill("回单文件上传有误");
|
||||
await page.getByRole("button", { name: "确认作废" }).click();
|
||||
await expect.poll(() => calls.voidPaymentReceipt).toBe(1);
|
||||
expect(calls.receiptVoidReasons).toEqual(["回单文件上传有误"]);
|
||||
});
|
||||
|
||||
test("只有超级管理员职责且未兼任经理或负责人时不显示付款复核", async ({ page }) => {
|
||||
await mockSystemActionApis(page, {
|
||||
batchStatus: "draft",
|
||||
meOverride: { roleCodes: ["admin"] },
|
||||
});
|
||||
|
||||
await login(page, user);
|
||||
await page.goto(`/payment-batches?id=${paymentBatchId}`);
|
||||
await expect(page.getByText("PAY-ACTION-001").first()).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "复核", exact: true })).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户