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(); } });