@proagentstore/cli 0.3.5 → 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
- // 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,7 @@ 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)
@@ -263,6 +325,11 @@ export async function inspectField(page, action) {
263
325
  parts.unshift(`⚠ "${action.name}" REJECTED: "${info.err}"`);
264
326
  else if (info.invalid)
265
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}`);
266
333
  }
267
334
  return parts.join("; ");
268
335
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",