feat(reimbursement-small-team-form-validation): 完善小团队报销填报校验

这个提交包含在:
2026-08-04 23:28:56 +08:00
父节点 f33c67eb20
当前提交 50c14c1b85
共修改 28 个文件,包含 1095 行新增和 30 行删除
@@ -66,7 +66,7 @@ function reimbursementListItem() {
};
}
function reimbursementDetail() {
function reimbursementDetail(overrides: { reimbursementDate?: string; description?: string } = {}) {
return {
id: reimbursementId,
formNumber,
@@ -77,9 +77,9 @@ function reimbursementDetail() {
status: "draft",
createdAt: "2026-05-06T09:00:00.000Z",
submittedAt: null,
reimbursementDate: "2026-05-06",
reimbursementDate: overrides.reimbursementDate ?? "2026-05-06",
totalAmount: "168.30",
description: "客户拜访交通费",
description: overrides.description ?? "客户拜访交通费",
projectId: null,
linkedTravelRequestId: null,
linkedTravelRequest: null,
@@ -113,6 +113,8 @@ function reimbursementDetail() {
async function mockApis(page: Page) {
let draftExists = true;
let detailReimbursementDate = "2026-05-06";
let detailDescription = "客户拜访交通费";
await page.route((url) => url.pathname.startsWith("/api/"), async (route) => {
const request = route.request();
@@ -245,7 +247,25 @@ async function mockApis(page: Page) {
}
if (path === `/api/reimbursements/${reimbursementId}` && request.method() === "GET") {
await fulfillJson(route, reimbursementDetail());
await fulfillJson(route, reimbursementDetail({
reimbursementDate: detailReimbursementDate,
description: detailDescription,
}));
return;
}
if (path === `/api/reimbursements/${reimbursementId}` && request.method() === "PATCH") {
const body = request.postDataJSON() as { reimbursementDate?: string; description?: string | null };
detailReimbursementDate = body.reimbursementDate ?? detailReimbursementDate;
detailDescription = body.description ?? "";
await fulfillJson(route, {
id: reimbursementId,
status: "draft",
reimbursementDate: detailReimbursementDate,
description: detailDescription || null,
totalAmount: "168.30",
travelSummary: { purpose: null, location: null, startDate: null, endDate: null },
});
return;
}
@@ -256,7 +276,25 @@ async function mockApis(page: Page) {
}
if (path === `/api/reimbursements/${reimbursementId}/precheck`) {
await fulfillJson(route, { decision: "pass", standardChecks: [], riskFlags: [] });
await fulfillJson(route, {
entityType: "reimbursement",
entityId: reimbursementId,
decision: "warn",
matchedBudgetControlRule: null,
budgetHint: null,
standardChecks: [],
evidenceSummary: {
invoiceCount: 0,
attachmentCount: 0,
items: [{ itemId, linkedInvoiceCount: 0, invoiceState: "unlinked" }],
},
timelinessChecks: [],
assetHint: { required: false, itemCount: 0 },
warnings: [],
blockingReasons: [],
riskFlags: [],
nextActions: ["确认提示后可继续提交"],
});
return;
}
@@ -369,6 +407,26 @@ test.describe("R102 reimbursement main flow", () => {
await expect(page.getByTestId("reimbursement-invoice-dialog")).toHaveCount(0);
});
test("PC 详情显示权威合计并可编辑报销日期和事由", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 960 });
await page.goto(`/reimbursements/${reimbursementId}`);
await expect(page.getByTestId("reimbursement-summary")).toContainText("报销总额");
await expect(page.getByTestId("reimbursement-items-table")).toContainText("共 1 条明细");
await expect(page.getByTestId("reimbursement-items-table")).toContainText("报销合计");
await expect(page.getByTestId("reimbursement-operational-hints")).toContainText("待关联发票或补充无票说明");
await page.getByTestId("reimbursement-business-summary").getByRole("button", { name: "编辑", exact: true }).click();
const editor = page.getByTestId("reimbursement-header-editor");
await editor.getByLabel("报销日期").fill("2026-05-07");
await editor.getByLabel("报销事由").fill("办公网络费用");
await editor.getByRole("button", { name: "保存", exact: true }).click();
await expect(page.getByTestId("reimbursement-header-editor")).toHaveCount(0);
await expect(page.getByTestId("reimbursement-business-summary")).toContainText("办公网络费用");
await expect(page.getByTestId("reimbursement-summary")).toContainText("2026-05-07");
});
test("移动报销卡片显示业务编号", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
const listRequest = page.waitForRequest((request) => {
@@ -386,6 +444,10 @@ test.describe("R102 reimbursement main flow", () => {
await expect(page.getByText(formNumber)).toBeVisible();
await expect(page.getByText(reimbursementId)).toHaveCount(0);
await expect(page.locator("table:visible")).toHaveCount(0);
await page.getByTestId("mobile-reimbursement-card").getByRole("link").click();
await expect(page.getByTestId("mobile-reimbursement-items")).toContainText("共 1 条明细");
await expect(page.getByTestId("mobile-reimbursement-items")).toContainText("报销合计 ¥168.30");
});
test("移动管理员默认查看本人并可切换全部报销", async ({ page }) => {
@@ -402,6 +464,22 @@ test.describe("R102 reimbursement main flow", () => {
await page.route("**/api/me", async (route) => {
await fulfillJson(route, adminMe);
});
await page.route(`**/api/reimbursements/${otherReimbursementId}`, async (route) => {
if (route.request().method() !== "GET") {
await route.fallback();
return;
}
await fulfillJson(route, {
...reimbursementDetail(),
id: otherReimbursementId,
formNumber: "BX-20260731-0002",
status: "reviewing",
ownerId: "employee-other-r102",
submitterId: "employee-other-r102",
owner: { id: "employee-other-r102", employee_no: "E103", name: "其他员工" },
submitter: { id: "employee-other-r102", employee_no: "E103", name: "其他员工" },
});
});
await page.route((url) => url.pathname === "/api/reimbursements", async (route) => {
const request = route.request();
if (request.method() !== "GET") {