|
|
/**
|
|
|
* 登录后 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<string | null> {
|
|
|
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<string | null> {
|
|
|
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<Page["getByRole"]>;
|
|
|
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 };
|
|
|
}
|