文件
hyb-oa/client/tests/e2e/helpers/liveInvoiceOcr.ts
T

161 行
5.6 KiB
TypeScript
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
import { expect, type Page } from "@playwright/test";
import type { E2EUser } from "./auth";
export type LiveInvoiceOcrStatus = "available" | "ocr_failed";
export type LiveInvoiceOcrResult = {
status: LiveInvoiceOcrStatus;
detailText: string;
bannerText: string;
durationMs: number;
invoiceId: string | null;
invoiceCode: string | null;
invoiceNumber: string | null;
sellerName: string | null;
totalAmount: string | null;
invoiceDate: string | null;
};
type UploadInvoiceResponse = {
data?: {
invoicePoolId?: string | null;
status?: string | null;
uploadAction?: string | null;
unavailableReason?: string | null;
unavailableMessage?: string | null;
availabilityReason?: string | null;
availabilityMessage?: string | null;
};
};
function extractField(detailText: string, label: string) {
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const match = detailText.match(new RegExp(`${escaped}([^\\n]+)`));
return match?.[1]?.trim() || null;
}
export function resolveLiveOcrUser(fallback: E2EUser): E2EUser {
const identifier =
process.env.PLAYWRIGHT_OCR_LIVE_IDENTIFIER?.trim() ||
process.env.LIVE_SMOKE_EMPLOYEE_IDENTIFIER?.trim() ||
"";
const password =
process.env.PLAYWRIGHT_OCR_LIVE_PASSWORD?.trim() ||
process.env.LIVE_SMOKE_EMPLOYEE_PASSWORD?.trim() ||
"";
if (!identifier && !password) {
return fallback;
}
if (!identifier || !password) {
throw new Error("PLAYWRIGHT_OCR_LIVE_IDENTIFIER/PASSWORD 或 LIVE_SMOKE_EMPLOYEE_* 必须成对提供");
}
return {
identifier,
password,
name: process.env.PLAYWRIGHT_OCR_LIVE_NAME?.trim() || "Smoke Employee",
};
}
export async function openInvoicePool(page: Page) {
await page.goto("/invoice-pool");
await expect(page.getByTestId("invoice-pool-table")).toBeVisible();
}
export async function uploadLiveInvoiceAndWait(page: Page, invoiceFilePath: string, timeout = 180_000): Promise<LiveInvoiceOcrResult> {
const startedAt = Date.now();
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(invoiceFilePath);
await page.getByTestId("invoice-upload-submit").click();
const uploadPayload = (await (await uploadResponsePromise).json()) as UploadInvoiceResponse;
const uploadData = uploadPayload.data;
const uploadedInvoiceId = uploadData?.invoicePoolId ?? null;
if (!uploadedInvoiceId) {
const message =
uploadData?.unavailableMessage ??
uploadData?.availabilityMessage ??
uploadData?.unavailableReason ??
uploadData?.availabilityReason ??
"发票上传未生成新票据";
const reason = uploadData?.unavailableReason ?? uploadData?.availabilityReason ?? uploadData?.uploadAction ?? "-";
const detailText = [
"票据上传结果",
"状态:识别失败",
`文件名:${invoiceFilePath.split(/[\\/]/).pop() ?? invoiceFilePath}`,
`上传动作:${uploadData?.uploadAction ?? "-"}`,
`原因:${reason}`,
`提示:${message}`,
].join("\n");
return {
status: "ocr_failed",
detailText,
bannerText: message,
durationMs: Date.now() - startedAt,
invoiceId: null,
invoiceCode: null,
invoiceNumber: null,
sellerName: null,
totalAmount: null,
invoiceDate: null,
};
}
const detailPanel = page.getByTestId("invoice-detail-panel");
await expect(detailPanel).toBeVisible({ timeout: 30_000 });
try {
await expect
.poll(async () => {
const text = await detailPanel.innerText().catch(() => "");
if (text.includes("状态:available") || text.includes("状态:可用")) return "available";
if (text.includes("状态:ocr_failed") || text.includes("状态:识别失败")) return "ocr_failed";
return "pending_ocr";
}, { timeout })
.toMatch(/^(available|ocr_failed)$/);
} catch (error) {
const detailText = await detailPanel.innerText().catch(() => "");
const bannerText = await page.getByTestId("invoice-ocr-status-banner").innerText().catch(() => "");
const invoiceId = uploadedInvoiceId ?? extractField(detailText, "票据ID:");
throw new Error(
[
"live 腾讯 OCR 等待终态超时",
`invoiceId=${invoiceId ?? "unknown"}`,
`timeoutMs=${timeout}`,
`detail=${detailText || "<empty>"}`,
`banner=${bannerText || "<empty>"}`,
`cause=${error instanceof Error ? error.message : String(error)}`,
].join("\n"),
);
}
const detailText = await detailPanel.innerText();
const bannerText = await page.getByTestId("invoice-ocr-status-banner").innerText().catch(() => "");
const status = detailText.includes("状态:ocr_failed") || detailText.includes("状态:识别失败") ? "ocr_failed" : "available";
const invoiceNo = extractField(detailText, "发票号:") ?? extractField(detailText, "发票号码:");
const [invoiceCode, invoiceNumber] = invoiceNo && invoiceNo.includes("-")
? invoiceNo.split("-", 2).map((item) => item.trim() || null)
: [invoiceNo, null];
return {
status,
detailText,
bannerText,
durationMs: Date.now() - startedAt,
invoiceId: uploadedInvoiceId ?? extractField(detailText, "票据ID:"),
invoiceCode,
invoiceNumber,
sellerName: extractField(detailText, "销售方:"),
totalAmount: extractField(detailText, "总金额:"),
invoiceDate: extractField(detailText, "开票日期:"),
};
}