汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。 统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。 纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。 关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。 预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。 完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
92 行
4.6 KiB
TypeScript
92 行
4.6 KiB
TypeScript
import type { APIRequestContext } from "@playwright/test";
|
|
|
|
import { test, expect } from "./helpers/fixtures";
|
|
import { EMPLOYEE, MANAGER, apiGetData, createAuthenticatedApiClient, login, logout } from "./helpers/auth";
|
|
import { openNavLink } from "./helpers/navigation";
|
|
|
|
async function firstEnabledExpenseTypeId(api: APIRequestContext) {
|
|
const response = await api.get("/api/expense-types?enabled=true");
|
|
const payload = await response.json() as { data?: { items?: Array<{ id: string; enabled?: boolean; category?: string | null }> } };
|
|
const item = payload.data?.items?.find((candidate) => candidate.enabled !== false && candidate.category === "office");
|
|
if (!response.ok() || !item?.id) {
|
|
throw new Error(`无法读取费用类型:HTTP ${response.status()}`);
|
|
}
|
|
return item.id;
|
|
}
|
|
|
|
async function createReimbursementItem(api: APIRequestContext, reimbursementId: string) {
|
|
const expenseTypeId = await firstEnabledExpenseTypeId(api);
|
|
const response = await api.post(`/api/reimbursements/${reimbursementId}/items`, {
|
|
data: {
|
|
expenseTypeId,
|
|
expenseDate: "2026-05-13",
|
|
amount: "10.00",
|
|
description: "E2E 报销审批进度明细",
|
|
},
|
|
});
|
|
if (!response.ok()) {
|
|
throw new Error(`创建报销明细失败:HTTP ${response.status()}`);
|
|
}
|
|
}
|
|
|
|
test("E2E 报销处理进度:提交后按真实 workflow 展示职责进度", async ({ page, reimbursementDetail }) => {
|
|
const apiClient = await createAuthenticatedApiClient(EMPLOYEE);
|
|
await login(page, EMPLOYEE);
|
|
|
|
try {
|
|
await (await openNavLink(page, "我的报销")).click();
|
|
await expect(page).toHaveURL(/\/reimbursements/);
|
|
await expect(page.getByTestId("reimbursements-create")).toBeVisible();
|
|
|
|
const createResponsePromise = page.waitForResponse((response) => {
|
|
const url = new URL(response.url());
|
|
return response.request().method() === "POST" && url.pathname === "/api/reimbursements" && response.ok();
|
|
});
|
|
await page.getByTestId("reimbursements-create").click();
|
|
await page.getByRole("menuitem", { name: "空白报销" }).click();
|
|
const createPayload = (await (await createResponsePromise).json()) as { data?: { id?: string } };
|
|
const reimbursementId = createPayload.data?.id;
|
|
if (!reimbursementId) {
|
|
throw new Error(`创建报销单后未返回 id: ${JSON.stringify(createPayload)}`);
|
|
}
|
|
|
|
await createReimbursementItem(apiClient.api, reimbursementId);
|
|
|
|
await page.goto(`/reimbursements/${reimbursementId}`);
|
|
await reimbursementDetail.assertLoaded();
|
|
|
|
const marker = `MOCK_E2E_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
await reimbursementDetail.uploadInvoicePdf(
|
|
`progress-${Date.now()}.pdf`,
|
|
Buffer.from(`%PDF-1.4\n${marker}\n%%EOF\n`, "utf8"),
|
|
);
|
|
|
|
await page.goto(`/reimbursements/${reimbursementId}`);
|
|
await reimbursementDetail.assertLoaded();
|
|
await reimbursementDetail.submitReimbursement();
|
|
const actual = await apiGetData<{ workflowProgress: { steps: Array<{id: string}> }; processingProgress: {stages: Array<{key: string; state: string}>} }>(apiClient.api, `/api/reimbursements/${reimbursementId}`);
|
|
expect(actual.workflowProgress.steps.map(step => step.id)).toEqual(["approval_1_manager", "approval_2_finance"]);
|
|
|
|
await expect(page.getByText(/reviewing|审批中/).first()).toBeVisible({ timeout: 30_000 });
|
|
await expect(reimbursementDetail.workflowProgress).toBeVisible({ timeout: 30_000 });
|
|
await expect(reimbursementDetail.workflowProgress.locator(":scope > li")).toHaveCount(6);
|
|
await expect(reimbursementDetail.workflowProgress).toContainText("申请提交");
|
|
await expect(reimbursementDetail.workflowProgress.locator(":scope > li").nth(1)).toContainText(MANAGER.name);
|
|
await expect(reimbursementDetail.workflowProgress).toContainText("财务复核");
|
|
await expect(reimbursementDetail.workflowProgress).toContainText("组建付款批次");
|
|
await expect(reimbursementDetail.workflowProgress).not.toContainText("审批节点1");
|
|
await expect(reimbursementDetail.workflowProgress).toContainText("付款复核");
|
|
await expect(reimbursementDetail.workflowProgress).toContainText("付款完成");
|
|
// Payment work is separate from the two actual approval steps; no legacy badge is required.
|
|
expect(actual.processingProgress.stages.slice(3).map(stage => stage.key)).toEqual([
|
|
"payment_batch_created", "payment_batch_review", "payment_and_receipt",
|
|
]);
|
|
for (const stage of actual.processingProgress.stages.slice(3)) expect(stage.state).toBe("pending");
|
|
await expect(reimbursementDetail.workflowProgress).not.toContainText(/第 \d+ 步/);
|
|
|
|
await logout(page, EMPLOYEE.name);
|
|
} finally {
|
|
await apiClient.dispose();
|
|
}
|
|
});
|