102 行
5.3 KiB
TypeScript
102 行
5.3 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
||
import { createAuthenticatedApiClient, EMPLOYEE, FINANCE_MGR, login, logout, MANAGER } from "./helpers/auth";
|
||
import { createAndSubmitTravelRequestViaUi, importTravelPaymentSeed } from "./helpers/travel";
|
||
|
||
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`;
|
||
const uploadResponsePromise = page.waitForResponse((response) => {
|
||
const url = new URL(response.url());
|
||
return response.request().method() === "POST" && url.pathname === "/api/invoice-pool/upload" && response.ok();
|
||
});
|
||
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();
|
||
const uploadPayload = (await (await uploadResponsePromise).json()) as { data?: { invoicePoolId?: string | null } };
|
||
const invoiceId = uploadPayload.data?.invoicePoolId;
|
||
if (!invoiceId) {
|
||
throw new Error(`上传发票后未返回票据 ID: ${JSON.stringify(uploadPayload)}`);
|
||
}
|
||
|
||
await expect(page.getByTestId("invoice-detail-panel")).toBeVisible();
|
||
await expect(page.getByTestId("invoice-detail-panel")).toContainText("状态:可用", { timeout: 90_000 });
|
||
const invoiceDetailText = await page.getByTestId("invoice-detail-panel").innerText();
|
||
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()]);
|
||
}
|
||
});
|