import { expect, test, type Page } from "@playwright/test"; const EMPLOYEE = { identifier: "employee@hyinda.com", password: "EmployeePassword1234", name: "Employee" }; const MANAGER = { identifier: "manager@hyinda.com", password: "ManagerPassword1234", name: "Manager" }; const FINANCE = { identifier: "finance@hyinda.com", password: "FinancePassword1234", name: "Finance" }; async function login(page: Page, user: { identifier: string; password: string }) { await page.goto("/login"); await page.getByTestId("login-identifier").fill(user.identifier); await page.getByTestId("login-password").fill(user.password); await page.getByTestId("login-submit").click(); await expect(page).toHaveURL(/\/dashboard/); } async function logout(page: Page, userDisplayName: string) { await page.getByRole("button", { name: userDisplayName }).click(); await page.getByRole("menuitem", { name: "退出登录" }).click(); await expect(page).toHaveURL(/\/login/); } function extractId(text: string, label: string) { const match = text.match(new RegExp(`${label}:([0-9a-f-]{36})`)); if (!match?.[1]) { throw new Error(`Failed to parse ${label} from text: ${text}`); } return match[1]; } test("E2E 主链路:登录→报销→待办审批→票据OCR→支付→登出", async ({ page }) => { // 1) employee: create reimbursement await login(page, EMPLOYEE); await page.getByRole("link", { name: "我的报销" }).click(); await page.getByTestId("reimbursements-create").click(); await expect(page.getByTestId("reimbursements-table")).toBeVisible(); const firstRow = page.getByTestId("reimbursements-table").getByTestId("reimbursement-row").first(); await expect(firstRow).toBeVisible(); await firstRow.getByTestId("reimbursement-detail-link").click(); await expect(page.getByTestId("reimbursement-summary")).toBeVisible(); const reimbursementSummaryText = await page.getByTestId("reimbursement-summary").innerText(); const reimbursementId = extractId(reimbursementSummaryText, "报销ID"); // 2) employee: upload invoice and wait OCR await page.getByRole("link", { name: "票据中心" }).click(); const marker = `MOCK_E2E_${Date.now()}_${Math.random().toString(16).slice(2)}`; await page.getByTestId("invoice-upload-file").setInputFiles({ name: `e2e-${Date.now()}.png`, mimeType: "image/png", buffer: Buffer.from(marker, "utf8"), }); await page.getByTestId("invoice-upload-submit").click(); await expect(page.getByTestId("invoice-detail-panel")).toBeVisible(); await expect(page.getByTestId("invoice-detail-panel")).toContainText("状态:pending_ocr"); await expect(page.getByTestId("invoice-detail-panel")).toContainText("状态:available", { timeout: 90_000 }); const invoiceDetailText = await page.getByTestId("invoice-detail-panel").innerText(); const invoiceId = extractId(invoiceDetailText, "票据ID"); // 预览(popup 存在即可) const popupPromise = page.waitForEvent("popup"); const invoiceRow = page.getByTestId("invoice-row").filter({ hasText: invoiceId }).first(); await expect(invoiceRow).toBeVisible(); await invoiceRow.getByRole("button").click(); await page.getByTestId("invoice-preview-button").click(); const popup = await popupPromise; await expect(popup).toHaveURL(new RegExp(`/api/invoice-pool/${invoiceId}`)); await popup.close(); // 3) bind invoice to reimbursement await page.getByTestId("invoice-bind-invoice").selectOption(invoiceId); await page.getByTestId("invoice-bind-reimbursement").selectOption(reimbursementId); await page.getByTestId("invoice-bind-submit").click(); // 4) submit reimbursement await page.goto(`/reimbursements/${reimbursementId}`); await page.getByTestId("reimbursement-submit").click(); await expect(page.getByText(/状态:(reviewing|审批中)/)).toBeVisible(); // 5) manager approve await logout(page, EMPLOYEE.name); await login(page, MANAGER); await page.getByRole("link", { name: "待我处理" }).click(); await expect(page.getByTestId("workflow-tasks-table")).toBeVisible(); const managerTaskRow = page.getByTestId("workflow-task-row").filter({ hasText: reimbursementId }).first(); await expect(managerTaskRow).toBeVisible(); await managerTaskRow.getByTestId("workflow-task-detail-link").click(); await page.getByTestId("workflow-task-approve").click(); await expect(page.getByTestId("workflow-task-summary")).toContainText(/状态:(completed|已完成)/); await logout(page, MANAGER.name); // 6) finance approve + pay await login(page, FINANCE); await page.getByRole("link", { name: "待我处理" }).click(); const financeTaskRow = page.getByTestId("workflow-task-row").filter({ hasText: reimbursementId }).first(); await expect(financeTaskRow).toBeVisible(); await financeTaskRow.getByTestId("workflow-task-detail-link").click(); await page.getByTestId("workflow-task-approve").click(); await expect(page.getByTestId("workflow-task-summary")).toContainText(/状态:(completed|已完成)/); await page.goto(`/reimbursements/${reimbursementId}`); await expect(page.getByText(/状态:(approved|已批准)/)).toBeVisible({ timeout: 30_000 }); await page.getByTestId("reimbursement-pay-voucher-no").fill(`E2E-${Date.now()}`); await page.getByTestId("reimbursement-pay").click(); await expect(page.getByText(/状态:(paid|已支付)/)).toBeVisible({ timeout: 30_000 }); await logout(page, FINANCE.name); // 7) guard: unauth -> login await page.goto("/dashboard"); await expect(page).toHaveURL(/\/login/); });