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 { 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 || ""}`, `banner=${bannerText || ""}`, `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, "开票日期:"), }; }