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; }; 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(); await page.getByTestId("invoice-upload-file").setInputFiles(invoiceFilePath); await page.getByTestId("invoice-upload-submit").click(); 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 = 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: extractField(detailText, "票据ID:"), invoiceCode, invoiceNumber, sellerName: extractField(detailText, "销售方:"), totalAmount: extractField(detailText, "总金额:"), invoiceDate: extractField(detailText, "开票日期:"), }; }