import type { Locator, Page } from "playwright"; import { RpaError } from "@/modules/rpa/errors"; const FIELD_VERIFY_MS = 8_000; const FIELD_VERIFY_POLL_MS = 150; function dispatchInputCommit(input: HTMLInputElement): void { input.dispatchEvent(new Event("input", { bubbles: true })); input.dispatchEvent(new Event("change", { bubbles: true })); input.blur(); } /** 触发 change/blur,确保 React 表单捕获变更 */ export async function commitLocatorInput(loc: Locator): Promise { await loc.evaluate((el) => { if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) { return; } dispatchInputCommit(el); }); } function valuesEquivalent(expected: string, actual: string): boolean { if (expected === actual) { return true; } const expNum = Number(expected); const actNum = Number(actual); if (Number.isFinite(expNum) && Number.isFinite(actNum)) { return expNum === actNum; } return false; } /** 填写后循环校验 value,失败则重填一次 */ export async function fillLocatorWithVerify( page: Page, loc: Locator, expected: string, options?: { forceClear?: boolean; commit?: boolean }, ): Promise { const deadline = Date.now() + FIELD_VERIFY_MS; while (Date.now() < deadline) { await loc.click({ timeout: 3_000 }).catch(() => undefined); if (options?.forceClear) { await loc.click({ clickCount: 3, timeout: 3_000 }).catch(() => undefined); await loc.press("Control+A").catch(() => undefined); await loc.press("Backspace").catch(() => undefined); } await loc.fill(expected, { timeout: 8_000 }); let actual = await loc.inputValue().catch(() => ""); if (actual !== expected) { await loc.pressSequentially(expected, { delay: 20 }); actual = await loc.inputValue().catch(() => ""); } if (options?.commit ?? true) { await commitLocatorInput(loc); await loc.press("Tab").catch(() => page.keyboard.press("Tab")); } actual = await loc.inputValue().catch(() => ""); if (valuesEquivalent(expected, actual)) { return; } await page.waitForTimeout(FIELD_VERIFY_POLL_MS); } const finalValue = await loc.inputValue().catch(() => ""); if (valuesEquivalent(expected, finalValue)) { return; } throw new RpaError( "CARGO_WEIGHT_MISMATCH", `货物字段填写未生效(expected=${expected} actual=${finalValue})`, { retryable: true }, ); } export type CargoEditorFieldValues = { pallet: string | null; weight: string | null; length: string | null; width: string | null; height: string | null; weightFieldHint: string | null; }; /** editor 仍打开时读取各 input value */ export async function readCargoEditorFieldValues( page: Page, widgetSelector: string, ): Promise { return page.evaluate((sel) => { const widget = document.querySelector(sel); if (!widget) { return { pallet: null, weight: null, length: null, width: null, height: null, weightFieldHint: null, }; } const result: CargoEditorFieldValues = { pallet: null, weight: null, length: null, width: null, height: null, weightFieldHint: null, }; const inputs = widget.querySelectorAll("input"); for (const inp of inputs) { const visible = inp.offsetParent !== null; if (!visible) { continue; } const ph = inp.placeholder ?? ""; const val = inp.value?.trim() ?? ""; if (!val) { continue; } if (/托盘|pallet/i.test(ph)) { result.pallet = val; } else if (/weight|重量|lbs|kg|公斤|磅/i.test(ph)) { result.weight = val; result.weightFieldHint = ph || inp.getAttribute("aria-label") || ""; } else if (/length|长度/i.test(ph)) { result.length = val; } else if (/width|宽度/i.test(ph)) { result.width = val; } else if (/height|高度/i.test(ph)) { result.height = val; } } return result; }, widgetSelector); } export async function waitForCargoEditorFieldValues( page: Page, widgetSelector: string, expected: Record, ): Promise { const deadline = Date.now() + FIELD_VERIFY_MS; while (Date.now() < deadline) { const current = await readCargoEditorFieldValues(page, widgetSelector); const ok = Object.entries(expected).every(([key, val]) => { const actual = current[key as keyof CargoEditorFieldValues]; return actual === val; }); if (ok) { return current; } await page.waitForTimeout(FIELD_VERIFY_POLL_MS); } const failed = await readCargoEditorFieldValues(page, widgetSelector); throw new RpaError( "CARGO_WEIGHT_MISMATCH", `editor 内字段未对齐(expected=${JSON.stringify(expected)} actual=${JSON.stringify(failed)})`, { retryable: true }, ); }