@avasapp/agent-bridge 0.4.1 → 0.5.0

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.
@@ -515,7 +515,12 @@ var SWITCH_HOSTS = /* @__PURE__ */ new Set(["RCTSwitch", "AndroidSwitch"]);
515
515
  var isFn = (v) => typeof v === "function";
516
516
  var isInputProps = (p) => isFn(p.onChangeText) || isFn(p.onChange) && typeof p.value !== "boolean" && ("value" in p || "defaultValue" in p || "placeholder" in p);
517
517
  var labelOf = (p) => p.accessibilityLabel ?? p["aria-label"];
518
- var interesting = (p) => isFn(p.onPress) || isFn(p.onValueChange) || isInputProps(p) || typeof p.testID === "string" || typeof labelOf(p) === "string" || webViewNameOf(p) !== void 0;
518
+ function actionNamesOf(p) {
519
+ if (!isFn(p.onAccessibilityAction) || !Array.isArray(p.accessibilityActions)) return null;
520
+ return p.accessibilityActions.map((a) => a?.name).filter((name) => typeof name === "string");
521
+ }
522
+ var activates = (rec) => !!rec.actions && !!rec.element.actions?.includes("activate");
523
+ var interesting = (p) => isFn(p.onPress) || actionNamesOf(p) !== null || isFn(p.onValueChange) || isInputProps(p) || typeof p.testID === "string" || typeof labelOf(p) === "string" || webViewNameOf(p) !== void 0;
519
524
  var ICON_GLYPHS = /[\uE000-\uF8FF]/g;
520
525
  function firstHost(fiber) {
521
526
  if (fiber.tag === HOST_COMPONENT) return fiber;
@@ -532,6 +537,11 @@ function absorb(rec, fiber, p) {
532
537
  const e = rec.element;
533
538
  if (!rec.input && isInputProps(p)) rec.input = fiber;
534
539
  if (!rec.press && isFn(p.onPress)) rec.press = fiber;
540
+ const actions = actionNamesOf(p);
541
+ if (!rec.actions && actions) {
542
+ rec.actions = fiber;
543
+ if (actions.length) e.actions = actions;
544
+ }
535
545
  const fill = (key, v) => {
536
546
  if (e[key] === void 0 && typeof v === "string") e[key] = v;
537
547
  };
@@ -617,8 +627,8 @@ var round = (r) => r && {
617
627
  var handles = (control, p, name) => !!control && isFn(p[name]) && propsOf(control)?.[name] === p[name];
618
628
  function sameControl(rec, p) {
619
629
  for (let r = rec; r; r = r.parent) {
620
- if (!r.press && !r.input) continue;
621
- const same = handles(r.press, p, "onPress") || handles(r.input, p, "onChangeText") || handles(r.input, p, "onChange");
630
+ if (!r.press && !r.input && !r.actions) continue;
631
+ const same = handles(r.press, p, "onPress") || handles(r.actions, p, "onAccessibilityAction") || handles(r.input, p, "onChangeText") || handles(r.input, p, "onChange");
622
632
  return same ? r : null;
623
633
  }
624
634
  return null;
@@ -648,6 +658,7 @@ function collectScreen(roots, window) {
648
658
  host,
649
659
  press: null,
650
660
  input: null,
661
+ actions: null,
651
662
  hidden,
652
663
  scroller,
653
664
  modal,
@@ -661,7 +672,7 @@ function collectScreen(roots, window) {
661
672
  return rec;
662
673
  };
663
674
  const buttonOf = (rec) => {
664
- for (let r = rec; r; r = r.parent) if (r.press) return r;
675
+ for (let r = rec; r; r = r.parent) if (r.press || activates(r)) return r;
665
676
  return null;
666
677
  };
667
678
  const textHost = (host) => {
@@ -752,7 +763,7 @@ function collectScreen(roots, window) {
752
763
  rec.hidden || (rec.hidden = covers2(rec));
753
764
  const text = rec.parts.map((part) => part.text.replace(ICON_GLYPHS, "").replace(/\s+/g, " ").trim()).filter(Boolean).join(" ");
754
765
  if (text) e.text = text;
755
- e.kind = e.webview !== void 0 ? "webview" : rec.input ? "input" : rec.press ? "button" : rec.parts.length ? "text" : "view";
766
+ e.kind = e.webview !== void 0 ? "webview" : rec.input ? "input" : rec.press || activates(rec) ? "button" : rec.parts.length ? "text" : "view";
756
767
  if (e.kind === "input" && e.editable === void 0) e.editable = true;
757
768
  const rect = measureHost(rec.host);
758
769
  e.rect = round(rect);
@@ -777,10 +788,21 @@ function collectScreen(roots, window) {
777
788
  }
778
789
  var focused = (found) => found.filter((f) => !f.hidden);
779
790
  function pressFiberOf(found) {
780
- if (found.press) return found.press;
791
+ if (found.press) return { fiber: found.press, via: "onPress" };
792
+ if (activates(found)) return { fiber: found.actions, via: "accessibilityAction" };
793
+ for (let f = found.fiber.return; f; f = f.return) {
794
+ const p = propsOf(f);
795
+ if (!p) continue;
796
+ if (isFn(p.onPress)) return { fiber: f, via: "onPress" };
797
+ if (actionNamesOf(p)?.includes("activate")) return { fiber: f, via: "accessibilityAction" };
798
+ }
799
+ return null;
800
+ }
801
+ function actionFiberOf(found, name) {
802
+ if (found.actions && found.element.actions?.includes(name)) return found.actions;
781
803
  for (let f = found.fiber.return; f; f = f.return) {
782
804
  const p = propsOf(f);
783
- if (p && isFn(p.onPress)) return f;
805
+ if (p && actionNamesOf(p)?.includes(name)) return f;
784
806
  }
785
807
  return null;
786
808
  }
@@ -824,7 +846,7 @@ function tiers(target) {
824
846
  (e) => fields(e) && !!e.text?.includes(text)
825
847
  ];
826
848
  }
827
- var labelOnly = ({ element: e, press, input }) => e.kind === "view" && !e.testID && !e.text && !e.placeholder && !press && !input;
849
+ var labelOnly = ({ element: e, press, input, actions }) => e.kind === "view" && !e.testID && !e.text && !e.placeholder && !press && !input && !actions;
828
850
  function matchTarget(found, target) {
829
851
  checkTarget(target);
830
852
  for (const pool of [found.filter((f) => !labelOnly(f)), found.filter(labelOnly)]) {
@@ -942,13 +964,36 @@ function fillInput(found, text, options = {}) {
942
964
  return value;
943
965
  }
944
966
  var center = (rect) => rect ? { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 } : { x: 0, y: 0 };
967
+ var disabledProps = (fiber) => {
968
+ const p = propsOf(fiber);
969
+ const state = p?.accessibilityState;
970
+ return p?.disabled === true || state?.disabled === true || p?.["aria-disabled"] === true;
971
+ };
972
+ function pressTargetOf(found, action) {
973
+ if (action === void 0) return pressFiberOf(found);
974
+ const fiber = actionFiberOf(found, action);
975
+ return fiber && { fiber, via: "accessibilityAction" };
976
+ }
945
977
  function pressElement(found, options = {}) {
946
- const fiber = pressFiberOf(found);
947
- if (!fiber) throw new Error(`${describe(found.element)} has no onPress`);
948
- if (!options.force && (found.element.disabled || propsOf(fiber)?.disabled === true))
978
+ const { action } = options;
979
+ const target = pressTargetOf(found, action);
980
+ if (!target) {
981
+ const names = found.element.actions ?? [];
982
+ const has = names.length ? `; it has ${names.map((a) => JSON.stringify(a)).join(", ")}` : "";
983
+ throw new Error(
984
+ action === void 0 ? `${describe(found.element)} has no onPress or "activate" accessibility action` : `${describe(found.element)} has no accessibility action ${JSON.stringify(action)}${has}`
985
+ );
986
+ }
987
+ const { fiber, via } = target;
988
+ if (!options.force && (found.element.disabled || disabledProps(fiber)))
949
989
  throw new Error(
950
990
  `${describe(found.element)} is disabled; pass {"force":true} as the last argument to press anyway`
951
991
  );
992
+ if (via === "accessibilityAction") {
993
+ const actionName = action ?? "activate";
994
+ handler(fiber, "onAccessibilityAction")?.(event({ actionName }));
995
+ return `accessibilityAction:${actionName}`;
996
+ }
952
997
  const { x, y } = center(found.element.rect);
953
998
  const e = () => event({
954
999
  locationX: found.element.rect ? found.element.rect.width / 2 : 0,
@@ -960,6 +1005,7 @@ function pressElement(found, options = {}) {
960
1005
  handler(fiber, "onPressIn")?.(e());
961
1006
  handler(fiber, "onPress")?.(e());
962
1007
  handler(fiber, "onPressOut")?.(e());
1008
+ return "onPress";
963
1009
  }
964
1010
 
965
1011
  // src/runtime/screen/find-text.ts
@@ -1266,9 +1312,19 @@ var POLL_MS = 50;
1266
1312
  function nearMissNote(found, target) {
1267
1313
  const text = typeof target === "string" ? target : target.text;
1268
1314
  const near = text ? nearMisses(found, text) : [];
1269
- return near.length ? `. Near misses: ${near.join("; ")}` : "";
1270
- }
1271
- function waitForTarget(collect, target, options = {}) {
1315
+ if (!near.length) return "";
1316
+ const offscreen = matchTarget(
1317
+ found.filter((f) => !f.onScreen && !f.hidden),
1318
+ target
1319
+ ).length;
1320
+ const hint = offscreen ? ". {scroll:true} scrolls an off-screen match into view" : "";
1321
+ return `. Near misses: ${near.join("; ")}${hint}`;
1322
+ }
1323
+ function waitForTarget(collect, target, options = {}, reveal) {
1324
+ if (options.gone && options.scroll)
1325
+ return Promise.reject(
1326
+ new Error("{gone:true} and {scroll:true} can't be combined: gone waits for the target to leave the screen")
1327
+ );
1272
1328
  const timeoutMs = options.timeoutMs ?? 5e3;
1273
1329
  const t0 = performance.now();
1274
1330
  const ms = () => Math.round((performance.now() - t0) * 100) / 100;
@@ -1285,12 +1341,17 @@ function waitForTarget(collect, target, options = {}) {
1285
1341
  if (finished) return;
1286
1342
  try {
1287
1343
  const found = collect();
1288
- const matches = matchTarget(
1289
- found.filter((f) => f.onScreen && !f.hidden),
1344
+ const reachable = found.filter((f) => !f.hidden);
1345
+ let matches = matchTarget(
1346
+ reachable.filter((f) => f.onScreen),
1290
1347
  target
1291
1348
  );
1349
+ if (!matches.length && options.scroll && reveal) matches = matchTarget(reachable, target);
1292
1350
  const hit = matches[indexOf(target) ?? 0];
1293
- if (options.gone ? !hit : hit) {
1351
+ if (hit && !hit.onScreen && reveal) {
1352
+ finish();
1353
+ reveal(hit).then((f) => resolve({ ms: ms(), element: f.element }), reject);
1354
+ } else if (options.gone ? !hit : hit) {
1294
1355
  finish();
1295
1356
  resolve(hit ? { ms: ms(), element: hit.element } : { ms: ms() });
1296
1357
  } else if (performance.now() - t0 >= timeoutMs) {
@@ -1366,18 +1427,20 @@ function createScreen(env) {
1366
1427
  if (!metrics && current.scroller) metrics = metricsOf(current.scroller);
1367
1428
  return { found: current, metrics };
1368
1429
  };
1430
+ const reveal = async (found) => {
1431
+ const shown = (await bringIntoView(found)).found;
1432
+ if (!shown.onScreen)
1433
+ throw new Error(
1434
+ `${describe(shown.element)} is not on screen and no scrollable around it can bring it into view`
1435
+ );
1436
+ return shown;
1437
+ };
1369
1438
  const resolve = async (target, prefer, options) => {
1370
- let found = resolveTarget(collect(), target, prefer, {
1439
+ const found = resolveTarget(collect(), target, prefer, {
1371
1440
  ...options,
1372
1441
  offscreen: options.scroll
1373
1442
  });
1374
- if (found.onScreen) return found;
1375
- found = (await bringIntoView(found)).found;
1376
- if (!found.onScreen)
1377
- throw new Error(
1378
- `${describe(found.element)} is not on screen and no scrollable around it can bring it into view`
1379
- );
1380
- return found;
1443
+ return found.onScreen ? found : reveal(found);
1381
1444
  };
1382
1445
  return {
1383
1446
  snapshot(options = {}) {
@@ -1407,10 +1470,17 @@ function createScreen(env) {
1407
1470
  return { filled, element: reread(found).element };
1408
1471
  },
1409
1472
  async press(target, options = {}) {
1410
- const found = await resolve(target, (f) => !!f.press, options);
1411
- pressElement(found, options);
1473
+ const { action } = options;
1474
+ if (action !== void 0 && typeof action !== "string")
1475
+ throw new Error(`"action" must be an accessibility action name; got ${JSON.stringify(action)}`);
1476
+ const found = await resolve(
1477
+ target,
1478
+ (f) => action === void 0 ? !!f.press || f.element.kind === "button" : !!f.element.actions?.includes(action),
1479
+ options
1480
+ );
1481
+ const via = pressElement(found, options);
1412
1482
  await settle();
1413
- return found.element;
1483
+ return via === "onPress" ? found.element : { ...found.element, via };
1414
1484
  },
1415
1485
  /**
1416
1486
  * Scrolls the nearest scrollable of a target (or `within`, or the main one
@@ -1490,7 +1560,7 @@ function createScreen(env) {
1490
1560
  await settle();
1491
1561
  return { refreshed: true };
1492
1562
  },
1493
- waitFor: (target, options) => waitForTarget(collect, target, options)
1563
+ waitFor: (target, options) => waitForTarget(collect, target, options, reveal)
1494
1564
  };
1495
1565
  }
1496
1566
 
@@ -1508,7 +1578,7 @@ function screenTools() {
1508
1578
  run: (text, options) => screen.findText(text, options)
1509
1579
  },
1510
1580
  "screen.snapshot": {
1511
- description: "Buttons, inputs, text, and testID or labelled views on screen, with rects and checked/selected/expanded state (a Switch's value is checked). Only the focused screen: unfocused tabs and stack screens, content under an open modal, display:none and accessibility-hidden subtrees are left out, and `hidden` counts the elements skipped. {all:true} lists everything, marking off-screen (onScreen:false) and hidden (hidden:true) ones. A WebView registered with the react-native-webview adapter shows as one `webview` element with its name: use webview.* inside it.",
1581
+ description: 'Buttons, inputs, text, and testID or labelled views on screen, with rects and checked/selected/expanded state (a Switch\'s value is checked). `actions` lists the accessibility actions an element handles; one with "activate" and no onPress (a swipe-to-confirm) is a button screen.press can press. Only the focused screen: unfocused tabs and stack screens, content under an open modal, display:none and accessibility-hidden subtrees are left out, and `hidden` counts the elements skipped. {all:true} lists everything, marking off-screen (onScreen:false) and hidden (hidden:true) ones. A WebView registered with the react-native-webview adapter shows as one `webview` element with its name: use webview.* inside it.',
1512
1582
  run: (options) => screen.snapshot(options)
1513
1583
  },
1514
1584
  "screen.fill": {
@@ -1516,7 +1586,7 @@ function screenTools() {
1516
1586
  run: (target, text, options) => screen.fill(target, text, options)
1517
1587
  },
1518
1588
  "screen.press": {
1519
- description: `Press a button, then wait for the render. ${TARGET_HELP} Disabled elements are refused; {force:true} presses anyway, to see what the app does on tap. {scroll:true} scrolls an off-screen target into view first.`,
1589
+ description: `Press a button, then wait for the render. ${TARGET_HELP} Disabled elements are refused; {force:true} presses anyway, to see what the app does on tap. {scroll:true} scrolls an off-screen target into view first. With no onPress it sends the "activate" accessibility action, as a screen reader's double tap does (swipe-to-confirm buttons, sliders), and the reply says via:"accessibilityAction:activate". {action:"increment"} (or any name in the element's snapshot \`actions\`) sends that action instead.`,
1520
1590
  run: (target, options) => screen.press(target, options)
1521
1591
  },
1522
1592
  "screen.scroll": {
@@ -1528,7 +1598,7 @@ function screenTools() {
1528
1598
  run: (target, options) => screen.refresh(target, options)
1529
1599
  },
1530
1600
  "screen.waitFor": {
1531
- description: "Wait until a target is on screen, or gone with {gone:true}. Matches testID, label, placeholder, then the same joined text screen.snapshot shows. Default timeout 5000 ms; a timeout lists near misses.",
1601
+ description: "Wait until a target is on screen, or gone with {gone:true}. Matches testID, label, placeholder, then the same joined text screen.snapshot shows. {scroll:true} also accepts a match rendered off screen and scrolls it into view (not with gone). Default timeout 5000 ms; a timeout lists near misses.",
1532
1602
  run: (target, options) => screen.waitFor(target, options)
1533
1603
  }
1534
1604
  };
@@ -82,6 +82,8 @@ type ScreenElement = {
82
82
  expanded?: boolean;
83
83
  /** A WebView registered with the react-native-webview adapter: drive it with `webview.*`. */
84
84
  webview?: string;
85
+ /** `accessibilityActions` it handles with `onAccessibilityAction`, e.g. `activate` on a swipe button. */
86
+ actions?: string[];
85
87
  rect: Rect | null;
86
88
  /** Only in `screen.snapshot({ all: true })`. */
87
89
  onScreen?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avasapp/agent-bridge",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Let coding agents drive a running React Native app directly: seed state, flip flags, navigate and check the screen in about a millisecond per call, with no taps.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -103,8 +103,9 @@
103
103
  "build": "bun run generate && rm -rf dist && tsup",
104
104
  "test": "bun run generate && bun test src/",
105
105
  "typecheck": "bun run generate && tsc -p tsconfig.json --noEmit",
106
- "check": "bun run typecheck && bun run test && bun run build && bun run check:gates",
106
+ "check": "bun run typecheck && bun run test && bun run build && bun run check:gates && bun run check:consumer-types",
107
107
  "check:gates": "node scripts/check-gates.mjs",
108
+ "check:consumer-types": "node scripts/check-consumer-types.mjs",
108
109
  "changeset": "changeset",
109
110
  "release": "bun run check && changeset publish",
110
111
  "prepublishOnly": "bun run check"