@proagentstore/cli 0.3.5 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { RunnerInputError } from "./errors.js";
|
|
4
|
+
/**
|
|
5
|
+
* Enter text and VERIFY it actually took, escalating through the best-practice ladder
|
|
6
|
+
* for masked / framework-controlled inputs (intl-tel, React-controlled, etc.):
|
|
7
|
+
* 1) fill() 2) pressSequentially() + blur() 3) native value setter + dispatch events.
|
|
8
|
+
* Returns true once the field holds the value. Generic — no site/widget-specific logic.
|
|
9
|
+
*/
|
|
10
|
+
async function typeRobustly(loc, text) {
|
|
11
|
+
const norm = (s) => s.replace(/\s+/g, "");
|
|
12
|
+
const took = async () => {
|
|
13
|
+
const v = await loc.inputValue({ timeout: 1_500 }).catch(() => null);
|
|
14
|
+
if (v == null)
|
|
15
|
+
return false;
|
|
16
|
+
return v === text || norm(v) === norm(text) || (norm(text).length > 3 && norm(v).includes(norm(text)));
|
|
17
|
+
};
|
|
18
|
+
// 1. Playwright fill — fires input/change for most inputs.
|
|
19
|
+
if (await loc.fill(text, { timeout: 6_000 }).then(() => true).catch(() => false)) {
|
|
20
|
+
if (await took())
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
// 2. Real per-character typing + blur — for masks/typeaheads that only validate on key events.
|
|
24
|
+
try {
|
|
25
|
+
await loc.click({ timeout: 3_000 });
|
|
26
|
+
await loc.fill("", { timeout: 2_000 }).catch(() => undefined); // clear first
|
|
27
|
+
await loc.pressSequentially(text, { delay: 25, timeout: 8_000 });
|
|
28
|
+
await loc.blur().catch(() => undefined);
|
|
29
|
+
if (await took())
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
/* fall through */
|
|
34
|
+
}
|
|
35
|
+
// 3. Native value setter + dispatched events — for React/Vue-controlled inputs where
|
|
36
|
+
// fill() sets the DOM value but the framework's onChange never fires.
|
|
37
|
+
try {
|
|
38
|
+
await loc.evaluate((el, val) => {
|
|
39
|
+
const input = el;
|
|
40
|
+
const proto = input instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
41
|
+
const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
|
|
42
|
+
if (setter)
|
|
43
|
+
setter.call(input, val);
|
|
44
|
+
else
|
|
45
|
+
input.value = val;
|
|
46
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
47
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
48
|
+
input.dispatchEvent(new Event("blur", { bubbles: true }));
|
|
49
|
+
}, text);
|
|
50
|
+
if (await took())
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
/* fall through */
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
4
58
|
/**
|
|
5
59
|
* Execute one selector-free browser action — address by ARIA role + accessible
|
|
6
60
|
* name, with robust fallbacks for custom comboboxes, typeaheads, fuzzy <select>
|
|
@@ -25,10 +79,11 @@ export async function performBrowserAction(page, action, clickRobustly, resumeFi
|
|
|
25
79
|
case "type": {
|
|
26
80
|
const text = String(action.text ?? "");
|
|
27
81
|
const loc = locate();
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
if (await loc
|
|
82
|
+
// Verified ladder (fill → pressSequentially+blur → setter+dispatch) — makes the
|
|
83
|
+
// value actually take on masked / framework-controlled inputs.
|
|
84
|
+
if (await typeRobustly(loc, text))
|
|
31
85
|
break;
|
|
86
|
+
// The field may be a combobox or typeahead rather than a bare textbox.
|
|
32
87
|
if (action.name && (await page.getByRole("combobox", { name: action.name }).fill(text, { timeout: 3_000 }).then(() => true).catch(() => false)))
|
|
33
88
|
break;
|
|
34
89
|
if (action.name && (await page.getByLabel(action.name).fill(text, { timeout: 3_000 }).then(() => true).catch(() => false)))
|
|
@@ -45,10 +100,17 @@ export async function performBrowserAction(page, action, clickRobustly, resumeFi
|
|
|
45
100
|
const value = String(action.text ?? "");
|
|
46
101
|
const loc = locate();
|
|
47
102
|
const pickOption = async () => {
|
|
103
|
+
// Custom dropdowns render options async/with animation — WAIT for the list
|
|
104
|
+
// to appear before clicking (else you click an overlay or nothing).
|
|
105
|
+
await page.locator('[role="option"], [role="listbox"] li, ul[role="listbox"] > *').first().waitFor({ state: "visible", timeout: 2_000 }).catch(() => undefined);
|
|
48
106
|
const opt = page.getByRole("option", { name: value, exact: false }).first();
|
|
49
|
-
if ((await opt.count().catch(() => 0))
|
|
50
|
-
return false;
|
|
51
|
-
|
|
107
|
+
if ((await opt.count().catch(() => 0)) > 0)
|
|
108
|
+
return opt.click({ timeout: 2_500 }).then(() => true).catch(() => false);
|
|
109
|
+
// Fallback: any visible option/li whose text contains the value.
|
|
110
|
+
const alt = page.locator('[role="option"], li').filter({ hasText: value }).first();
|
|
111
|
+
if ((await alt.count().catch(() => 0)) > 0)
|
|
112
|
+
return alt.click({ timeout: 2_500 }).then(() => true).catch(() => false);
|
|
113
|
+
return false;
|
|
52
114
|
};
|
|
53
115
|
// 1. Native <select>: exact label/value, then a fuzzy (case/punctuation
|
|
54
116
|
// -insensitive) match so "Decline to self-identify" hits the option
|
|
@@ -240,7 +302,8 @@ export async function inspectField(page, action) {
|
|
|
240
302
|
if (t && rx.test(t) && t.length < 180)
|
|
241
303
|
err = t;
|
|
242
304
|
}
|
|
243
|
-
|
|
305
|
+
const disabled = e.hasAttribute("disabled") || e.getAttribute("aria-disabled") === "true" || e.hasAttribute("readonly");
|
|
306
|
+
return { value, invalid, err: err.replace(/\s+/g, " ").slice(0, 180), tag: e.tagName.toLowerCase(), html: (e.outerHTML || "").replace(/\s+/g, " ").slice(0, 500), disabled };
|
|
244
307
|
})
|
|
245
308
|
.catch(() => null);
|
|
246
309
|
if (!info)
|
|
@@ -259,10 +322,22 @@ export async function inspectField(page, action) {
|
|
|
259
322
|
// or belongs to another field — reporting it as REJECTED sent the brain into a
|
|
260
323
|
// false-retry loop on a value that was actually accepted.
|
|
261
324
|
if (info.invalid || (typed && !stuck)) {
|
|
262
|
-
if (info.
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
parts.unshift(
|
|
325
|
+
if (info.disabled) {
|
|
326
|
+
// A disabled/read-only control can't be changed — it's almost always already
|
|
327
|
+
// set correctly. Tell the brain to move on instead of fixating on it.
|
|
328
|
+
parts.unshift(`ℹ "${action.name}" is DISABLED / read-only — it is likely already set correctly; do NOT keep trying, move on to other fields.`);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
if (info.err)
|
|
332
|
+
parts.unshift(`⚠ "${action.name}" REJECTED: "${info.err}"`);
|
|
333
|
+
else if (info.invalid)
|
|
334
|
+
parts.unshift(`⚠ "${action.name}" is marked invalid`);
|
|
335
|
+
// Value didn't take / field invalid → show the widget's real DOM so the brain
|
|
336
|
+
// can pick the right interaction (custom dropdown → click to open + click the
|
|
337
|
+
// option by text; masked input → a different shape) instead of blindly retrying.
|
|
338
|
+
if (info.html)
|
|
339
|
+
parts.push(`DOM: <${info.tag}> ${info.html}`);
|
|
340
|
+
}
|
|
266
341
|
}
|
|
267
342
|
return parts.join("; ");
|
|
268
343
|
}
|