@qontinui/ui-bridge-native 0.6.7 → 0.6.9

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.
@@ -28,7 +28,7 @@ function inferActions(type) {
28
28
  case "pressable":
29
29
  return [...baseActions, "click", "press", "longPress", "doubleTap"];
30
30
  case "input":
31
- return [...baseActions, "click", "press", "type", "clear"];
31
+ return [...baseActions, "click", "press", "type", "setValue", "clear"];
32
32
  case "text":
33
33
  return [...baseActions, "click", "press", "longPress"];
34
34
  case "view":
@@ -990,6 +990,8 @@ var DefaultNativeActionExecutor = class {
990
990
  return this.performDoubleTap(props);
991
991
  case "type":
992
992
  return this.performType(element, props, params);
993
+ case "setValue":
994
+ return this.performSetValue(element, props, params);
993
995
  case "clear":
994
996
  return this.performClear(element, props);
995
997
  case "focus":
@@ -1082,6 +1084,38 @@ var DefaultNativeActionExecutor = class {
1082
1084
  this.registry.updateElementState(element.id, { value: params.text });
1083
1085
  }
1084
1086
  }
1087
+ /**
1088
+ * Perform setValue action.
1089
+ *
1090
+ * Web/runner UI Bridge parity: `setValue` replaces the input's value
1091
+ * atomically in a single call (no per-character keystroke simulation),
1092
+ * mirroring the web bridge's `setValue` semantics. On native there is no
1093
+ * DOM value to assign, so we drive the controlled-input handler
1094
+ * (`onChangeText`, falling back to `onChange`) with the full string. The
1095
+ * registry's `state.value` is then synced so a subsequent GET on the
1096
+ * element reflects the new value.
1097
+ *
1098
+ * Unlike `type`, this does not append to existing content and ignores
1099
+ * `delay` — it is the deterministic "set the field to exactly this" action.
1100
+ */
1101
+ async performSetValue(element, props, params) {
1102
+ const next = typeof params?.text === "string" ? params.text : typeof params?.value === "string" ? params.value : void 0;
1103
+ if (typeof next !== "string") {
1104
+ throw new Error('setValue action requires a string "text" (or "value") parameter');
1105
+ }
1106
+ if (typeof props.onChangeText === "function") {
1107
+ props.onChangeText(next);
1108
+ } else if (typeof props.onChange === "function") {
1109
+ props.onChange({
1110
+ nativeEvent: { text: next }
1111
+ });
1112
+ } else {
1113
+ throw new Error("No text change handler found on element");
1114
+ }
1115
+ if (element) {
1116
+ this.registry.updateElementState(element.id, { value: next });
1117
+ }
1118
+ }
1085
1119
  /**
1086
1120
  * Perform clear action
1087
1121
  */
@@ -2045,6 +2079,35 @@ function diagnosePageHealth(elements, viewport) {
2045
2079
  }
2046
2080
 
2047
2081
  // src/server/handlers.ts
2082
+ function matchElementsByText(registry, text, opts) {
2083
+ const needle = text.toLowerCase();
2084
+ const contains = (v) => typeof v === "string" && v.toLowerCase().includes(needle);
2085
+ let all = registry.getAllElements();
2086
+ if (opts?.visibleOnly) {
2087
+ all = all.filter((e) => e.getState().visible);
2088
+ }
2089
+ const scored = [];
2090
+ for (const e of all) {
2091
+ let rank = Number.POSITIVE_INFINITY;
2092
+ if (contains(e.label)) rank = Math.min(rank, 0);
2093
+ const ident = e.getIdentifier();
2094
+ if (ident) {
2095
+ if (contains(ident.testId)) rank = Math.min(rank, 1);
2096
+ if (contains(ident.accessibilityLabel)) rank = Math.min(rank, 1);
2097
+ if (contains(ident.treePath)) rank = Math.min(rank, 1);
2098
+ if (contains(ident.uiId)) rank = Math.min(rank, 1);
2099
+ }
2100
+ if (rank === Number.POSITIVE_INFINITY) {
2101
+ const handlers = extractHandlerNames(e.props);
2102
+ if (handlers.some((h) => h.toLowerCase().includes(needle))) rank = 2;
2103
+ }
2104
+ if (rank !== Number.POSITIVE_INFINITY) {
2105
+ scored.push({ rank, element: { id: e.id, type: e.type, label: e.label } });
2106
+ }
2107
+ }
2108
+ scored.sort((a, b) => a.rank - b.rank);
2109
+ return scored.map((s) => s.element);
2110
+ }
2048
2111
  async function executeWorkflowStep(step, registry, executor) {
2049
2112
  const startTime = Date.now();
2050
2113
  try {
@@ -2641,6 +2704,89 @@ function createServerHandlers(registry, executor, config) {
2641
2704
  const failedCount = results.length - succeededCount;
2642
2705
  return success2({ results, succeededCount, failedCount });
2643
2706
  },
2707
+ // ========================================================================
2708
+ // App-agnostic interaction parity (cross-platform `/control/page/*`)
2709
+ //
2710
+ // Mirrors the web/runner convenience endpoints so the SAME contract drives
2711
+ // mobile. Text/label interactions compose `executor.find` (registry-based
2712
+ // discovery) + `executor.executeAction` (press/type). Selector-only
2713
+ // variants have no React-Native analog and return NOT_SUPPORTED — the
2714
+ // mobile bridge's snapshot + `/control/find` (testID / accessibilityLabel)
2715
+ // cover what a selector would on web.
2716
+ // ========================================================================
2717
+ clickByText: async (ctx) => {
2718
+ const body = ctx.body;
2719
+ const text = typeof body?.text === "string" ? body.text.trim() : "";
2720
+ if (!text) {
2721
+ return error2("text is required and must not be empty", "INVALID_REQUEST");
2722
+ }
2723
+ const match = matchElementsByText(registry, text, { visibleOnly: body?.visibleOnly })[0];
2724
+ if (!match) {
2725
+ return error2(`No element found with text "${text}"`, "ELEMENT_NOT_FOUND");
2726
+ }
2727
+ const response = await executor.executeAction(match.id, { action: "press" });
2728
+ if (!response.success) {
2729
+ return error2(response.error || "Press failed", "ACTION_FAILED");
2730
+ }
2731
+ return success2({
2732
+ clicked: true,
2733
+ element: { id: match.id, type: match.type, label: match.label }
2734
+ });
2735
+ },
2736
+ clickBySelector: async () => {
2737
+ return error2(
2738
+ "clickBySelector is not supported on native platform \u2014 React Native has no CSS-selector DOM. Use clickByText, or /control/find with testIdPattern/accessibilityLabelPattern then /control/element/:id/action.",
2739
+ "NOT_SUPPORTED"
2740
+ );
2741
+ },
2742
+ typeInto: async (ctx) => {
2743
+ const body = ctx.body;
2744
+ const label = typeof body?.label === "string" ? body.label.trim() : "";
2745
+ if (!label) {
2746
+ return error2(
2747
+ "label is required on native (selector-based typeInto is web-only)",
2748
+ "INVALID_REQUEST"
2749
+ );
2750
+ }
2751
+ const text = typeof body?.text === "string" ? body.text : "";
2752
+ const match = matchElementsByText(registry, label, { visibleOnly: body?.visibleOnly })[0];
2753
+ if (!match) {
2754
+ return error2(`No input found for label "${label}"`, "ELEMENT_NOT_FOUND");
2755
+ }
2756
+ const response = await executor.executeAction(match.id, {
2757
+ action: "type",
2758
+ params: { text, clear: body?.clear === true }
2759
+ });
2760
+ if (!response.success) {
2761
+ return error2(response.error || "Type failed", "ACTION_FAILED");
2762
+ }
2763
+ return success2({
2764
+ typed: true,
2765
+ element: { id: match.id, type: match.type, label: match.label }
2766
+ });
2767
+ },
2768
+ readValue: async () => {
2769
+ return error2(
2770
+ "readValue is not supported on native platform \u2014 React Native has no CSS-selector DOM. Use /control/element/:id/state to read an element value.",
2771
+ "NOT_SUPPORTED"
2772
+ );
2773
+ },
2774
+ findByText: async (ctx) => {
2775
+ const body = ctx.body;
2776
+ const text = typeof body?.text === "string" ? body.text.trim() : "";
2777
+ if (!text) {
2778
+ return error2("text is required and must not be empty", "INVALID_REQUEST");
2779
+ }
2780
+ const matches = matchElementsByText(registry, text, { visibleOnly: body?.visibleOnly });
2781
+ return success2(
2782
+ matches.map((el, index) => ({
2783
+ index,
2784
+ id: el.id,
2785
+ type: el.type,
2786
+ label: el.label
2787
+ }))
2788
+ );
2789
+ },
2644
2790
  // Coord-based tap
2645
2791
  //
2646
2792
  // Synthesizes a press at the given screen coordinates by scanning the
@@ -2735,7 +2881,8 @@ function createServerHandlers(registry, executor, config) {
2735
2881
  return success2({
2736
2882
  entries: [],
2737
2883
  count: 0,
2738
- bufferSize: 0
2884
+ bufferSize: 0,
2885
+ installed: false
2739
2886
  });
2740
2887
  }
2741
2888
  const entries = buf.entries({
@@ -2745,7 +2892,8 @@ function createServerHandlers(registry, executor, config) {
2745
2892
  return success2({
2746
2893
  entries,
2747
2894
  count: entries.length,
2748
- bufferSize: buf.size()
2895
+ bufferSize: buf.size(),
2896
+ installed: buf.isInstalled()
2749
2897
  });
2750
2898
  },
2751
2899
  getNetworkRequests: async (ctx) => {
@@ -2756,7 +2904,8 @@ function createServerHandlers(registry, executor, config) {
2756
2904
  return success2({
2757
2905
  entries: [],
2758
2906
  count: 0,
2759
- bufferSize: 0
2907
+ bufferSize: 0,
2908
+ installed: false
2760
2909
  });
2761
2910
  }
2762
2911
  const entries = buf.entries({
@@ -2766,7 +2915,8 @@ function createServerHandlers(registry, executor, config) {
2766
2915
  return success2({
2767
2916
  entries,
2768
2917
  count: entries.length,
2769
- bufferSize: buf.size()
2918
+ bufferSize: buf.size(),
2919
+ installed: buf.isInstalled()
2770
2920
  });
2771
2921
  },
2772
2922
  // Test hooks — modal stack manipulation
@@ -3185,7 +3335,10 @@ var WS_ROUTES = [
3185
3335
  // same handler at both paths to keep agents portable across platforms.
3186
3336
  { pattern: "ai/find", handler: "find", httpMethods: ["POST"] },
3187
3337
  { pattern: "control/snapshot", handler: "getSnapshot", httpMethods: ["GET"] },
3188
- { pattern: "control/discover", handler: "getSnapshot" },
3338
+ // Discovery alias — web/runner parity with `POST /control/discover` (a
3339
+ // legacy alias for snapshot/find). Mounted on HTTP (GET+POST) so mobile
3340
+ // doesn't 404 where web returns 200; delegates to the snapshot handler.
3341
+ { pattern: "control/discover", handler: "getSnapshot", httpMethods: ["GET", "POST"] },
3189
3342
  // Page health — accepts both GET and POST so the canonical skill invocation
3190
3343
  // (`POST .../control/page-health` with an empty body) and ad-hoc curls both
3191
3344
  // work without a body-shape gotcha.
@@ -3247,6 +3400,17 @@ var WS_ROUTES = [
3247
3400
  // Coord-based tap — synthesizes a press at the given screen coords by
3248
3401
  // matching against registered elements' layout rects.
3249
3402
  { pattern: "control/tap", handler: "tapAt", httpMethods: ["POST"] },
3403
+ // App-agnostic interaction parity — mirrors the web/runner convenience
3404
+ // endpoints (`/control/page/click-by-text` etc.) so the SAME cross-platform
3405
+ // contract drives mobile. Text/label-based interactions compose
3406
+ // `executor.find` + `executor.executeAction` over the registry; the
3407
+ // DOM-selector-only variants (`click-by-selector`, `read-value`) have no RN
3408
+ // analog and return a structured NOT_SUPPORTED envelope.
3409
+ { pattern: "control/page/click-by-text", handler: "clickByText", httpMethods: ["POST"] },
3410
+ { pattern: "control/page/click-by-selector", handler: "clickBySelector", httpMethods: ["POST"] },
3411
+ { pattern: "control/page/type-into", handler: "typeInto", httpMethods: ["POST"] },
3412
+ { pattern: "control/page/read-value", handler: "readValue", httpMethods: ["POST"] },
3413
+ { pattern: "control/page/find-by-text", handler: "findByText", httpMethods: ["POST"] },
3250
3414
  // Test hooks — gated behind `features.testHooks`. Let external runners
3251
3415
  // drive the ModalDetector over HTTP so tests can flip `snapshot.modalStack`
3252
3416
  // without mounting React components.
@@ -3268,19 +3432,26 @@ var WS_ROUTES = [
3268
3432
  httpMethods: ["POST"],
3269
3433
  requiresTestHooks: true
3270
3434
  },
3271
- // Observability — last-N console errors + network requests. testHooks-gated
3272
- // because patching `console.error` / `fetch` is invasive in production.
3435
+ // Observability — last-N console errors + network requests.
3436
+ //
3437
+ // Always mounted (not testHooks-gated) so they return a schema-valid 200
3438
+ // on every build instead of a 404 that agents can't distinguish from a
3439
+ // missing route. The INVASIVE part — monkey-patching `console.error` /
3440
+ // `fetch` / `XMLHttpRequest` — stays gated behind `testHooks` (see
3441
+ // NativeUIBridgeServer.start → buffer.install()). When the patches aren't
3442
+ // installed the handlers report `installed: false` + empty entries, so
3443
+ // production builds expose the contract without observing real users'
3444
+ // console/network traffic. Set `features.testHooks` (typically on
3445
+ // `__DEV__`) to populate the buffers with live data.
3273
3446
  {
3274
3447
  pattern: "control/console-errors",
3275
3448
  handler: "getConsoleErrors",
3276
- httpMethods: ["GET"],
3277
- requiresTestHooks: true
3449
+ httpMethods: ["GET"]
3278
3450
  },
3279
3451
  {
3280
3452
  pattern: "sdk/network-requests",
3281
3453
  handler: "getNetworkRequests",
3282
- httpMethods: ["GET"],
3283
- requiresTestHooks: true
3454
+ httpMethods: ["GET"]
3284
3455
  }
3285
3456
  ];
3286
3457
  var HANDLER_DESCRIPTIONS = {
@@ -3322,8 +3493,13 @@ var HANDLER_DESCRIPTIONS = {
3322
3493
  aiChangeBufferEnable: "STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Use WS event subscriptions for change notifications.",
3323
3494
  aiWaitForElement: "STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Use the WS waitForElement / waitForCondition JSON-RPC methods.",
3324
3495
  tapAt: "Synthesize a press at given screen coords by matching registered element layout rects: body { x, y, action? }",
3325
- getConsoleErrors: "TEST HOOK \u2014 last-N console.error/console.warn entries; ?since=<ms>&limit=<n>",
3326
- getNetworkRequests: "TEST HOOK \u2014 last-N fetch/XHR entries with status + duration; ?since=<ms>&limit=<n>",
3496
+ clickByText: "App-agnostic: press the first element matching free-text (label/accessibilityLabel/testID): body { text, visibleOnly? }",
3497
+ clickBySelector: "STUB \u2014 selector-based; NOT_SUPPORTED on native (no DOM). Use clickByText or /control/find + /control/element/:id/action.",
3498
+ typeInto: "App-agnostic: type into the input matching a label: body { label, text, clear?, visibleOnly? }. (selector form is web-only)",
3499
+ readValue: "STUB \u2014 selector-based; NOT_SUPPORTED on native (no DOM). Use /control/element/:id/state to read a value.",
3500
+ findByText: "App-agnostic: list elements matching free-text (no press): body { text, visibleOnly? }",
3501
+ getConsoleErrors: "Last-N console.error/console.warn entries; ?since=<ms>&limit=<n>. `installed:false` when capture is off (no testHooks) \u2014 entries empty but schema-valid.",
3502
+ getNetworkRequests: "Last-N fetch/XHR entries with status + duration; ?since=<ms>&limit=<n>. `installed:false` when capture is off (no testHooks) \u2014 entries empty but schema-valid.",
3327
3503
  pushModal: "TEST HOOK \u2014 push a modal onto the snapshot.modalStack via the registry ModalDetector",
3328
3504
  dismissModal: "TEST HOOK \u2014 dismiss a modal by id from the snapshot.modalStack via the registry ModalDetector",
3329
3505
  keepAwake: "TEST HOOK \u2014 keep the device screen awake via the injected keepAwakeProvider: body { enabled: boolean, durationMs?: number }"
@@ -5494,7 +5670,8 @@ function useUIElement(options) {
5494
5670
  parentPath,
5495
5671
  style,
5496
5672
  stateStyles: stateStylesProp,
5497
- handlers: handlersProp
5673
+ handlers: handlersProp,
5674
+ value: valueProp
5498
5675
  } = options;
5499
5676
  const treePath = parentPath ? `${parentPath}/${id}` : id;
5500
5677
  const bridgeProps = react.useMemo(
@@ -5520,9 +5697,12 @@ function useUIElement(options) {
5520
5697
  });
5521
5698
  registeredRef.current = true;
5522
5699
  registeredIdRef.current = id;
5700
+ if (typeof valueProp === "string") {
5701
+ bridge.registry.updateElementState(id, { value: valueProp });
5702
+ }
5523
5703
  setRegistered(true);
5524
5704
  scheduleDeadButtonWarning(bridge, id, type);
5525
- }, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp]);
5705
+ }, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp]);
5526
5706
  const unregister = react.useCallback(() => {
5527
5707
  if (!bridge || !registeredRef.current) return;
5528
5708
  bridge.registry.unregisterElement(registeredIdRef.current ?? id);
@@ -5652,6 +5832,13 @@ function useUIElement(options) {
5652
5832
  }
5653
5833
  };
5654
5834
  }, [bridge, registered, id, handlersProp]);
5835
+ react.useEffect(() => {
5836
+ if (!bridge || !registered) return;
5837
+ if (typeof valueProp !== "string") return;
5838
+ const current = bridge.registry.getElement(id)?.getState().value;
5839
+ if (current === valueProp) return;
5840
+ bridge.registry.updateElementState(id, { value: valueProp });
5841
+ }, [bridge, registered, id, valueProp]);
5655
5842
  react.useCallback(
5656
5843
  (props) => {
5657
5844
  propsRef.current = { ...propsRef.current, ...props };