/** * 登录后 Quick 表单填完后的页面回读校验(不提交) */ import type { Page } from "playwright"; import { FLOCK_LOCATION_ACCESSORIALS } from "@/components/flock/flock-logged-in-quote-form"; import { flockLocationRpaLabels } from "@/lib/flock/logged-in-rpa-options"; import { flockLocatorCount } from "@/workers/rpa/flock/page-alive"; function locationAccessorialFlags(typeId: string): { liftgate: boolean; inside: boolean; palletJack: boolean; } | null { const flags = FLOCK_LOCATION_ACCESSORIALS[ typeId as keyof typeof FLOCK_LOCATION_ACCESSORIALS ]; return flags ?? null; } export type LoggedInFormVerifyExpect = { pickupTypeId: string; deliveryTypeId: string; pickupZip: string; deliveryZip: string; pickupLiftgate?: boolean; pickupInside?: boolean; pickupPalletJack?: boolean; deliveryLiftgate?: boolean; deliveryInside?: boolean; deliveryPalletJack?: boolean; }; export type LoggedInFormVerifyIssue = { field: string; expected: string; actual: string; analysis: string; }; async function readComboboxDisplay( page: Page, fieldName: RegExp, ): Promise { for (const role of ["combobox", "button"] as const) { const loc = page.getByRole(role, { name: fieldName }).first(); if ((await flockLocatorCount(loc)) === 0) continue; const inner = ((await loc.innerText().catch(() => "")) || "").trim(); if (inner) return inner; const input = await loc.inputValue().catch(() => ""); if (input.trim()) return input.trim(); } return null; } async function readTextbox( page: Page, fieldName: RegExp, ): Promise { const loc = page.getByRole("textbox", { name: fieldName }).first(); if ((await flockLocatorCount(loc)) === 0) return null; return (await loc.inputValue().catch(() => "")) || null; } function matchLocationLabel( actual: string | null, typeId: string, ): { ok: boolean; expected: string } { const candidates = [...flockLocationRpaLabels(typeId)]; const primary = candidates[0] ?? typeId; if (!actual?.trim()) { return { ok: false, expected: primary }; } const norm = actual.replace(/\s+/g, " ").trim(); const ok = candidates.some((c) => norm.toLowerCase().includes(c.toLowerCase()), ); return { ok, expected: candidates.join(" | ") }; } function pushComboboxIssue( issues: LoggedInFormVerifyIssue[], side: "Pickup" | "Delivery", actual: string | null, typeId: string, ): void { const { ok, expected } = matchLocationLabel(actual, typeId); if (ok) return; issues.push({ field: `${side} Type`, expected, actual: actual ?? "(空/未读到)", analysis: actual == null ? `官网未出现 ${side} Type 下拉或 RPA 未点开选项;检查 combobox 名称是否仍为「${side} Type *」` : `选项文案与映射表不一致:系统 id=${typeId},请核对 lib/flock/logged-in-rpa-options.ts`, }); } function pushCheckboxIssue( issues: LoggedInFormVerifyIssue[], field: string, want: boolean | undefined, visible: boolean, checked: boolean | null, index: number, ): void { if (want === undefined) return; if (want && !visible) { issues.push({ field, expected: "勾选且可见", actual: "页面上无此 checkbox", analysis: "系统要求勾选,但官网该地点类型区块未渲染此选项;可能类型切换后附属项 DOM 不同步或 selector 索引错误", }); return; } if (!want && visible && checked === true) { issues.push({ field, expected: "不勾选", actual: "已勾选", analysis: "系统未要求勾选,但页面仍为选中态;RPA setCheckbox(false) 未生效或官网默认强制勾选", }); return; } if (want && visible && checked !== true) { issues.push({ field, expected: "已勾选", actual: checked === false ? "未勾选" : "状态未知", analysis: index > 0 ? `同名 checkbox 第 ${index + 1} 个未命中;提货/派送区 Liftgate 或 Pallet Jack 可能需调整 nth 索引` : "RPA applyLoggedInLocationAccessorials 点击失败,或选项在类型切换后才出现需加长等待", }); } } /** 填表后回读官网 DOM,对比系统期望 */ export async function verifyLoggedInFormOnPage( page: Page, expect: LoggedInFormVerifyExpect, ): Promise<{ ok: boolean; issues: LoggedInFormVerifyIssue[] }> { const issues: LoggedInFormVerifyIssue[] = []; const pickupZip = await readTextbox(page, /Pickup ZIP/i); const deliveryZip = await readTextbox(page, /Delivery ZIP/i); if (pickupZip?.trim() !== expect.pickupZip) { issues.push({ field: "Pickup ZIP", expected: expect.pickupZip, actual: pickupZip ?? "(空)", analysis: "ZIP 未写入或字段名变化", }); } if (deliveryZip?.trim() !== expect.deliveryZip) { issues.push({ field: "Delivery ZIP", expected: expect.deliveryZip, actual: deliveryZip ?? "(空)", analysis: "ZIP 未写入或字段名变化", }); } const pickupTypeText = await readComboboxDisplay(page, /Pickup Type/i); const deliveryTypeText = await readComboboxDisplay(page, /Delivery Type/i); pushComboboxIssue(issues, "Pickup", pickupTypeText, expect.pickupTypeId); pushComboboxIssue(issues, "Delivery", deliveryTypeText, expect.deliveryTypeId); const pickupFlags = locationAccessorialFlags(expect.pickupTypeId); const deliveryFlags = locationAccessorialFlags(expect.deliveryTypeId); const liftgatePickup = page.getByRole("checkbox", { name: /Liftgate Required for Pickup/i, }); const liftgateDelivery = page.getByRole("checkbox", { name: /Liftgate Required for Delivery/i, }); const pallet = page.getByRole("checkbox", { name: /Location has Pallet Jack and Forklift/i, }); const palletCount = await flockLocatorCount(pallet); // 提货侧无 Pallet 时,nth(0) 实际是派送侧——禁止误读 const pickupPalletIndex = pickupFlags?.palletJack ? 0 : -1; const deliveryPalletIndex = deliveryFlags?.palletJack ? pickupFlags?.palletJack && palletCount > 1 ? 1 : 0 : -1; const checks: Array<{ field: string; loc: ReturnType; want: boolean | undefined; index: number; enabled: boolean; }> = [ { field: "Pickup Liftgate", loc: liftgatePickup, want: expect.pickupLiftgate, index: 0, enabled: Boolean(pickupFlags?.liftgate), }, { field: "Delivery Liftgate", loc: liftgateDelivery, want: expect.deliveryLiftgate, index: 0, enabled: Boolean(deliveryFlags?.liftgate), }, { field: "Pickup Inside", loc: page.getByRole("checkbox", { name: /Inside Pickup/i }), want: expect.pickupInside, index: 0, enabled: Boolean(pickupFlags?.inside), }, { field: "Delivery Inside", loc: page.getByRole("checkbox", { name: /Inside Delivery/i }), want: expect.deliveryInside, index: 0, enabled: Boolean(deliveryFlags?.inside), }, { field: "Pickup Pallet Jack", loc: pallet, want: expect.pickupPalletJack, index: pickupPalletIndex, enabled: pickupPalletIndex >= 0, }, { field: "Delivery Pallet Jack", loc: pallet, want: expect.deliveryPalletJack, index: deliveryPalletIndex, enabled: deliveryPalletIndex >= 0, }, ]; for (const c of checks) { if (!c.enabled || c.want === undefined || c.index < 0) continue; const box = c.loc.nth(c.index); const visible = (await flockLocatorCount(box)) > 0; if (!visible && c.want === false) continue; const checked = visible ? await box.isChecked().catch(() => false) : null; pushCheckboxIssue(issues, c.field, c.want, visible, checked, c.index); } return { ok: issues.length === 0, issues }; }