@proagentstore/cli 0.3.4 → 0.3.6
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,20 +302,35 @@ export async function inspectField(page, action) {
|
|
|
240
302
|
if (t && rx.test(t) && t.length < 180)
|
|
241
303
|
err = t;
|
|
242
304
|
}
|
|
243
|
-
return { value, invalid, err: err.replace(/\s+/g, " ").slice(0, 180) };
|
|
305
|
+
return { value, invalid, err: err.replace(/\s+/g, " ").slice(0, 180), tag: e.tagName.toLowerCase(), html: (e.outerHTML || "").replace(/\s+/g, " ").slice(0, 500) };
|
|
244
306
|
})
|
|
245
307
|
.catch(() => null);
|
|
246
308
|
if (!info)
|
|
247
309
|
return "";
|
|
248
310
|
const typed = String(action.text ?? "").trim();
|
|
311
|
+
const stuck = !!typed && info.value === typed; // our value actually took
|
|
249
312
|
const parts = [];
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (info.value && (info.invalid ||
|
|
313
|
+
// Report the field's real value when it differs from what we sent, or when the
|
|
314
|
+
// field is authoritatively invalid.
|
|
315
|
+
if (info.value && (info.invalid || !stuck)) {
|
|
253
316
|
parts.push(`"${action.name}" now reads "${info.value}"${typed && info.value !== typed ? ` (you sent "${typed}")` : ""}`);
|
|
254
317
|
}
|
|
255
|
-
|
|
256
|
-
|
|
318
|
+
// Flag REJECTED ONLY on an authoritative per-field signal (aria-invalid / :invalid)
|
|
319
|
+
// or when our value clearly did NOT take. A nearby error while the value stuck is
|
|
320
|
+
// often stale (widgets show a "format" error mid-type that clears on blur/submit)
|
|
321
|
+
// or belongs to another field — reporting it as REJECTED sent the brain into a
|
|
322
|
+
// false-retry loop on a value that was actually accepted.
|
|
323
|
+
if (info.invalid || (typed && !stuck)) {
|
|
324
|
+
if (info.err)
|
|
325
|
+
parts.unshift(`⚠ "${action.name}" REJECTED: "${info.err}"`);
|
|
326
|
+
else if (info.invalid)
|
|
327
|
+
parts.unshift(`⚠ "${action.name}" is marked invalid`);
|
|
328
|
+
// Value didn't take / field invalid → show the widget's real DOM so the brain
|
|
329
|
+
// can pick the right interaction (custom dropdown → click to open + click the
|
|
330
|
+
// option by text; masked input → a different shape) instead of blindly retrying.
|
|
331
|
+
if (info.html)
|
|
332
|
+
parts.push(`DOM: <${info.tag}> ${info.html}`);
|
|
333
|
+
}
|
|
257
334
|
return parts.join("; ");
|
|
258
335
|
}
|
|
259
336
|
catch {
|