@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
- // Plain fill, then combobox/label fill (the field may be a combobox or
29
- // typeahead, not a bare textbox), then click + keyboard type (typeaheads).
30
- if (await loc.fill(text, { timeout: 6_000 }).then(() => true).catch(() => false))
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)) === 0)
50
- return false;
51
- return opt.click({ timeout: 2_500 }).then(() => true).catch(() => false);
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
- return { value, invalid, err: err.replace(/\s+/g, " ").slice(0, 180) };
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.err)
263
- parts.unshift(`⚠ "${action.name}" REJECTED: "${info.err}"`);
264
- else if (info.invalid)
265
- parts.unshift(`⚠ "${action.name}" is marked invalid`);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",