test(P22PROD1-5): 差旅支付与对账E2E补齐及稳态修复
这个提交包含在:
@@ -0,0 +1,101 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { createAuthenticatedApiClient, EMPLOYEE, FINANCE_MGR, login, logout, MANAGER } from "./helpers/auth";
|
||||
import { createAndSubmitTravelRequestViaUi, importTravelPaymentSeed } from "./helpers/travel";
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
function extractInvoiceCode(text: string) {
|
||||
const match = text.match(/发票号:([A-Z0-9]+)-/);
|
||||
if (!match?.[1]) {
|
||||
throw new Error(`Failed to parse invoice code from text: ${text}`);
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
||||
test("E2E 差旅支付主链路:申请→审批→支付→绑票", async ({ page }) => {
|
||||
const employeeApi = await createAuthenticatedApiClient(EMPLOYEE);
|
||||
const financeMgrApi = await createAuthenticatedApiClient(FINANCE_MGR);
|
||||
|
||||
try {
|
||||
await login(page, EMPLOYEE);
|
||||
const draft = await createAndSubmitTravelRequestViaUi(page, {
|
||||
purposePrefix: "Playwright差旅支付",
|
||||
amount: "1888.00",
|
||||
});
|
||||
|
||||
await logout(page, EMPLOYEE.name);
|
||||
await login(page, MANAGER);
|
||||
await page.goto("/workflow/tasks?status=pending&page=1");
|
||||
const taskRow = page.getByTestId("workflow-task-row").filter({ hasText: draft.requestId }).first();
|
||||
await expect(taskRow).toBeVisible({ timeout: 30_000 });
|
||||
await taskRow.getByTestId("workflow-task-detail-link").click();
|
||||
await page.getByTestId("workflow-task-approve").click();
|
||||
await expect(page.getByTestId("workflow-task-summary")).toContainText("状态:");
|
||||
await expect(page.getByTestId("workflow-task-summary")).toContainText("已完成");
|
||||
await logout(page, MANAGER.name);
|
||||
|
||||
const paymentSeed = await importTravelPaymentSeed(financeMgrApi.api, {
|
||||
requestId: draft.requestId,
|
||||
employeeId: employeeApi.me.id,
|
||||
amount: "1888.00",
|
||||
occurredAtDate: draft.startDate,
|
||||
});
|
||||
|
||||
await login(page, FINANCE_MGR);
|
||||
await page.goto(`/travel-requests?tab=requests&id=${draft.requestId}`);
|
||||
const orderCard = page.locator(".rounded-xl.border.border-zinc-200").filter({ hasText: paymentSeed.orderNo }).last();
|
||||
await expect(orderCard).toBeVisible({ timeout: 30_000 });
|
||||
await expect(orderCard).toContainText("已匹配交易");
|
||||
await orderCard.getByRole("button", { name: "标记已支付" }).click();
|
||||
await page.locator("#travel-order-payment-voucher").fill(`BANK-${Date.now()}`);
|
||||
await page.locator("#travel-order-payment-date").fill(draft.startDate);
|
||||
await page.getByRole("button", { name: "确认已支付" }).click();
|
||||
await expect(orderCard).toContainText("已支付", { timeout: 30_000 });
|
||||
await expect(orderCard).not.toContainText("标记已支付");
|
||||
|
||||
await page.goto("/invoice-pool");
|
||||
const invoiceFileName = `travel-payment-${Date.now()}.png`;
|
||||
await page.getByTestId("invoice-upload-file").setInputFiles({
|
||||
name: invoiceFileName,
|
||||
mimeType: "image/png",
|
||||
buffer: Buffer.from(`TRAVEL_PAYMENT_${Date.now()}`, "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");
|
||||
const invoiceCode = extractInvoiceCode(invoiceDetailText);
|
||||
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("invoice-pool-table")).toBeVisible();
|
||||
const bindCard = page.locator(".card-std").filter({ hasText: "绑定业务单据" }).first();
|
||||
await bindCard.getByRole("combobox").first().selectOption("travelOrder");
|
||||
await page.getByTestId("invoice-bind-invoice").selectOption(invoiceId);
|
||||
await page.getByTestId("invoice-bind-travel-request").selectOption(draft.requestId);
|
||||
await page.getByTestId("invoice-bind-travel-order").selectOption(paymentSeed.orderId);
|
||||
const bindResponsePromise = page.waitForResponse((response) => response.url().includes(`/api/invoice-pool/${invoiceId}/bind`) && response.request().method() === "POST");
|
||||
await page.getByTestId("invoice-bind-submit").click();
|
||||
const bindResponse = await bindResponsePromise;
|
||||
expect(bindResponse.ok()).toBeTruthy();
|
||||
await expect(page.getByText("绑定成功")).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
await page.goto(`/travel-requests?tab=requests&id=${draft.requestId}`);
|
||||
const paidOrderCard = page.locator(".rounded-xl.border.border-zinc-200").filter({ hasText: paymentSeed.orderNo }).last();
|
||||
await expect(paidOrderCard).toContainText("已支付");
|
||||
await expect(paidOrderCard).not.toContainText("当前没有已绑定发票。", { timeout: 30_000 });
|
||||
await expect(paidOrderCard).toContainText(invoiceCode);
|
||||
await expect(paidOrderCard).toContainText("ERP 状态:");
|
||||
|
||||
await logout(page, FINANCE_MGR.name);
|
||||
} finally {
|
||||
await Promise.all([employeeApi.dispose(), financeMgrApi.dispose()]);
|
||||
}
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户