@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
- // 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,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
- if (info.err)
251
- parts.push(`⚠ "${action.name}" REJECTED: "${info.err}"`);
252
- if (info.value && (info.invalid || info.err || (typed && info.value !== typed))) {
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
- if (info.invalid && !info.err)
256
- parts.push(`"${action.name}" is marked invalid`);
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",