You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

167 lines
4.9 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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<void> {
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<void> {
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<CargoEditorFieldValues> {
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<HTMLInputElement>("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<string, string>,
): Promise<CargoEditorFieldValues> {
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 },
);
}