@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.
package/dist/index.js CHANGED
@@ -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":
@@ -1042,6 +1042,8 @@ var DefaultNativeActionExecutor = class {
1042
1042
  return this.performDoubleTap(props);
1043
1043
  case "type":
1044
1044
  return this.performType(element, props, params);
1045
+ case "setValue":
1046
+ return this.performSetValue(element, props, params);
1045
1047
  case "clear":
1046
1048
  return this.performClear(element, props);
1047
1049
  case "focus":
@@ -1134,6 +1136,38 @@ var DefaultNativeActionExecutor = class {
1134
1136
  this.registry.updateElementState(element.id, { value: params.text });
1135
1137
  }
1136
1138
  }
1139
+ /**
1140
+ * Perform setValue action.
1141
+ *
1142
+ * Web/runner UI Bridge parity: `setValue` replaces the input's value
1143
+ * atomically in a single call (no per-character keystroke simulation),
1144
+ * mirroring the web bridge's `setValue` semantics. On native there is no
1145
+ * DOM value to assign, so we drive the controlled-input handler
1146
+ * (`onChangeText`, falling back to `onChange`) with the full string. The
1147
+ * registry's `state.value` is then synced so a subsequent GET on the
1148
+ * element reflects the new value.
1149
+ *
1150
+ * Unlike `type`, this does not append to existing content and ignores
1151
+ * `delay` — it is the deterministic "set the field to exactly this" action.
1152
+ */
1153
+ async performSetValue(element, props, params) {
1154
+ const next = typeof params?.text === "string" ? params.text : typeof params?.value === "string" ? params.value : void 0;
1155
+ if (typeof next !== "string") {
1156
+ throw new Error('setValue action requires a string "text" (or "value") parameter');
1157
+ }
1158
+ if (typeof props.onChangeText === "function") {
1159
+ props.onChangeText(next);
1160
+ } else if (typeof props.onChange === "function") {
1161
+ props.onChange({
1162
+ nativeEvent: { text: next }
1163
+ });
1164
+ } else {
1165
+ throw new Error("No text change handler found on element");
1166
+ }
1167
+ if (element) {
1168
+ this.registry.updateElementState(element.id, { value: next });
1169
+ }
1170
+ }
1137
1171
  /**
1138
1172
  * Perform clear action
1139
1173
  */
@@ -2097,6 +2131,35 @@ function diagnosePageHealth(elements, viewport) {
2097
2131
  }
2098
2132
 
2099
2133
  // src/server/handlers.ts
2134
+ function matchElementsByText(registry, text, opts) {
2135
+ const needle = text.toLowerCase();
2136
+ const contains = (v) => typeof v === "string" && v.toLowerCase().includes(needle);
2137
+ let all = registry.getAllElements();
2138
+ if (opts?.visibleOnly) {
2139
+ all = all.filter((e) => e.getState().visible);
2140
+ }
2141
+ const scored = [];
2142
+ for (const e of all) {
2143
+ let rank = Number.POSITIVE_INFINITY;
2144
+ if (contains(e.label)) rank = Math.min(rank, 0);
2145
+ const ident = e.getIdentifier();
2146
+ if (ident) {
2147
+ if (contains(ident.testId)) rank = Math.min(rank, 1);
2148
+ if (contains(ident.accessibilityLabel)) rank = Math.min(rank, 1);
2149
+ if (contains(ident.treePath)) rank = Math.min(rank, 1);
2150
+ if (contains(ident.uiId)) rank = Math.min(rank, 1);
2151
+ }
2152
+ if (rank === Number.POSITIVE_INFINITY) {
2153
+ const handlers = extractHandlerNames(e.props);
2154
+ if (handlers.some((h) => h.toLowerCase().includes(needle))) rank = 2;
2155
+ }
2156
+ if (rank !== Number.POSITIVE_INFINITY) {
2157
+ scored.push({ rank, element: { id: e.id, type: e.type, label: e.label } });
2158
+ }
2159
+ }
2160
+ scored.sort((a, b) => a.rank - b.rank);
2161
+ return scored.map((s) => s.element);
2162
+ }
2100
2163
  async function executeWorkflowStep(step, registry, executor) {
2101
2164
  const startTime = Date.now();
2102
2165
  try {
@@ -2693,6 +2756,89 @@ function createServerHandlers(registry, executor, config) {
2693
2756
  const failedCount = results.length - succeededCount;
2694
2757
  return success2({ results, succeededCount, failedCount });
2695
2758
  },
2759
+ // ========================================================================
2760
+ // App-agnostic interaction parity (cross-platform `/control/page/*`)
2761
+ //
2762
+ // Mirrors the web/runner convenience endpoints so the SAME contract drives
2763
+ // mobile. Text/label interactions compose `executor.find` (registry-based
2764
+ // discovery) + `executor.executeAction` (press/type). Selector-only
2765
+ // variants have no React-Native analog and return NOT_SUPPORTED — the
2766
+ // mobile bridge's snapshot + `/control/find` (testID / accessibilityLabel)
2767
+ // cover what a selector would on web.
2768
+ // ========================================================================
2769
+ clickByText: async (ctx) => {
2770
+ const body = ctx.body;
2771
+ const text = typeof body?.text === "string" ? body.text.trim() : "";
2772
+ if (!text) {
2773
+ return error2("text is required and must not be empty", "INVALID_REQUEST");
2774
+ }
2775
+ const match = matchElementsByText(registry, text, { visibleOnly: body?.visibleOnly })[0];
2776
+ if (!match) {
2777
+ return error2(`No element found with text "${text}"`, "ELEMENT_NOT_FOUND");
2778
+ }
2779
+ const response = await executor.executeAction(match.id, { action: "press" });
2780
+ if (!response.success) {
2781
+ return error2(response.error || "Press failed", "ACTION_FAILED");
2782
+ }
2783
+ return success2({
2784
+ clicked: true,
2785
+ element: { id: match.id, type: match.type, label: match.label }
2786
+ });
2787
+ },
2788
+ clickBySelector: async () => {
2789
+ return error2(
2790
+ "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.",
2791
+ "NOT_SUPPORTED"
2792
+ );
2793
+ },
2794
+ typeInto: async (ctx) => {
2795
+ const body = ctx.body;
2796
+ const label = typeof body?.label === "string" ? body.label.trim() : "";
2797
+ if (!label) {
2798
+ return error2(
2799
+ "label is required on native (selector-based typeInto is web-only)",
2800
+ "INVALID_REQUEST"
2801
+ );
2802
+ }
2803
+ const text = typeof body?.text === "string" ? body.text : "";
2804
+ const match = matchElementsByText(registry, label, { visibleOnly: body?.visibleOnly })[0];
2805
+ if (!match) {
2806
+ return error2(`No input found for label "${label}"`, "ELEMENT_NOT_FOUND");
2807
+ }
2808
+ const response = await executor.executeAction(match.id, {
2809
+ action: "type",
2810
+ params: { text, clear: body?.clear === true }
2811
+ });
2812
+ if (!response.success) {
2813
+ return error2(response.error || "Type failed", "ACTION_FAILED");
2814
+ }
2815
+ return success2({
2816
+ typed: true,
2817
+ element: { id: match.id, type: match.type, label: match.label }
2818
+ });
2819
+ },
2820
+ readValue: async () => {
2821
+ return error2(
2822
+ "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.",
2823
+ "NOT_SUPPORTED"
2824
+ );
2825
+ },
2826
+ findByText: async (ctx) => {
2827
+ const body = ctx.body;
2828
+ const text = typeof body?.text === "string" ? body.text.trim() : "";
2829
+ if (!text) {
2830
+ return error2("text is required and must not be empty", "INVALID_REQUEST");
2831
+ }
2832
+ const matches = matchElementsByText(registry, text, { visibleOnly: body?.visibleOnly });
2833
+ return success2(
2834
+ matches.map((el, index) => ({
2835
+ index,
2836
+ id: el.id,
2837
+ type: el.type,
2838
+ label: el.label
2839
+ }))
2840
+ );
2841
+ },
2696
2842
  // Coord-based tap
2697
2843
  //
2698
2844
  // Synthesizes a press at the given screen coordinates by scanning the
@@ -2787,7 +2933,8 @@ function createServerHandlers(registry, executor, config) {
2787
2933
  return success2({
2788
2934
  entries: [],
2789
2935
  count: 0,
2790
- bufferSize: 0
2936
+ bufferSize: 0,
2937
+ installed: false
2791
2938
  });
2792
2939
  }
2793
2940
  const entries = buf.entries({
@@ -2797,7 +2944,8 @@ function createServerHandlers(registry, executor, config) {
2797
2944
  return success2({
2798
2945
  entries,
2799
2946
  count: entries.length,
2800
- bufferSize: buf.size()
2947
+ bufferSize: buf.size(),
2948
+ installed: buf.isInstalled()
2801
2949
  });
2802
2950
  },
2803
2951
  getNetworkRequests: async (ctx) => {
@@ -2808,7 +2956,8 @@ function createServerHandlers(registry, executor, config) {
2808
2956
  return success2({
2809
2957
  entries: [],
2810
2958
  count: 0,
2811
- bufferSize: 0
2959
+ bufferSize: 0,
2960
+ installed: false
2812
2961
  });
2813
2962
  }
2814
2963
  const entries = buf.entries({
@@ -2818,7 +2967,8 @@ function createServerHandlers(registry, executor, config) {
2818
2967
  return success2({
2819
2968
  entries,
2820
2969
  count: entries.length,
2821
- bufferSize: buf.size()
2970
+ bufferSize: buf.size(),
2971
+ installed: buf.isInstalled()
2822
2972
  });
2823
2973
  },
2824
2974
  // Test hooks — modal stack manipulation
@@ -3237,7 +3387,10 @@ var WS_ROUTES = [
3237
3387
  // same handler at both paths to keep agents portable across platforms.
3238
3388
  { pattern: "ai/find", handler: "find", httpMethods: ["POST"] },
3239
3389
  { pattern: "control/snapshot", handler: "getSnapshot", httpMethods: ["GET"] },
3240
- { pattern: "control/discover", handler: "getSnapshot" },
3390
+ // Discovery alias — web/runner parity with `POST /control/discover` (a
3391
+ // legacy alias for snapshot/find). Mounted on HTTP (GET+POST) so mobile
3392
+ // doesn't 404 where web returns 200; delegates to the snapshot handler.
3393
+ { pattern: "control/discover", handler: "getSnapshot", httpMethods: ["GET", "POST"] },
3241
3394
  // Page health — accepts both GET and POST so the canonical skill invocation
3242
3395
  // (`POST .../control/page-health` with an empty body) and ad-hoc curls both
3243
3396
  // work without a body-shape gotcha.
@@ -3299,6 +3452,17 @@ var WS_ROUTES = [
3299
3452
  // Coord-based tap — synthesizes a press at the given screen coords by
3300
3453
  // matching against registered elements' layout rects.
3301
3454
  { pattern: "control/tap", handler: "tapAt", httpMethods: ["POST"] },
3455
+ // App-agnostic interaction parity — mirrors the web/runner convenience
3456
+ // endpoints (`/control/page/click-by-text` etc.) so the SAME cross-platform
3457
+ // contract drives mobile. Text/label-based interactions compose
3458
+ // `executor.find` + `executor.executeAction` over the registry; the
3459
+ // DOM-selector-only variants (`click-by-selector`, `read-value`) have no RN
3460
+ // analog and return a structured NOT_SUPPORTED envelope.
3461
+ { pattern: "control/page/click-by-text", handler: "clickByText", httpMethods: ["POST"] },
3462
+ { pattern: "control/page/click-by-selector", handler: "clickBySelector", httpMethods: ["POST"] },
3463
+ { pattern: "control/page/type-into", handler: "typeInto", httpMethods: ["POST"] },
3464
+ { pattern: "control/page/read-value", handler: "readValue", httpMethods: ["POST"] },
3465
+ { pattern: "control/page/find-by-text", handler: "findByText", httpMethods: ["POST"] },
3302
3466
  // Test hooks — gated behind `features.testHooks`. Let external runners
3303
3467
  // drive the ModalDetector over HTTP so tests can flip `snapshot.modalStack`
3304
3468
  // without mounting React components.
@@ -3320,19 +3484,26 @@ var WS_ROUTES = [
3320
3484
  httpMethods: ["POST"],
3321
3485
  requiresTestHooks: true
3322
3486
  },
3323
- // Observability — last-N console errors + network requests. testHooks-gated
3324
- // because patching `console.error` / `fetch` is invasive in production.
3487
+ // Observability — last-N console errors + network requests.
3488
+ //
3489
+ // Always mounted (not testHooks-gated) so they return a schema-valid 200
3490
+ // on every build instead of a 404 that agents can't distinguish from a
3491
+ // missing route. The INVASIVE part — monkey-patching `console.error` /
3492
+ // `fetch` / `XMLHttpRequest` — stays gated behind `testHooks` (see
3493
+ // NativeUIBridgeServer.start → buffer.install()). When the patches aren't
3494
+ // installed the handlers report `installed: false` + empty entries, so
3495
+ // production builds expose the contract without observing real users'
3496
+ // console/network traffic. Set `features.testHooks` (typically on
3497
+ // `__DEV__`) to populate the buffers with live data.
3325
3498
  {
3326
3499
  pattern: "control/console-errors",
3327
3500
  handler: "getConsoleErrors",
3328
- httpMethods: ["GET"],
3329
- requiresTestHooks: true
3501
+ httpMethods: ["GET"]
3330
3502
  },
3331
3503
  {
3332
3504
  pattern: "sdk/network-requests",
3333
3505
  handler: "getNetworkRequests",
3334
- httpMethods: ["GET"],
3335
- requiresTestHooks: true
3506
+ httpMethods: ["GET"]
3336
3507
  }
3337
3508
  ];
3338
3509
  var HANDLER_DESCRIPTIONS = {
@@ -3374,8 +3545,13 @@ var HANDLER_DESCRIPTIONS = {
3374
3545
  aiChangeBufferEnable: "STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Use WS event subscriptions for change notifications.",
3375
3546
  aiWaitForElement: "STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Use the WS waitForElement / waitForCondition JSON-RPC methods.",
3376
3547
  tapAt: "Synthesize a press at given screen coords by matching registered element layout rects: body { x, y, action? }",
3377
- getConsoleErrors: "TEST HOOK \u2014 last-N console.error/console.warn entries; ?since=<ms>&limit=<n>",
3378
- getNetworkRequests: "TEST HOOK \u2014 last-N fetch/XHR entries with status + duration; ?since=<ms>&limit=<n>",
3548
+ clickByText: "App-agnostic: press the first element matching free-text (label/accessibilityLabel/testID): body { text, visibleOnly? }",
3549
+ clickBySelector: "STUB \u2014 selector-based; NOT_SUPPORTED on native (no DOM). Use clickByText or /control/find + /control/element/:id/action.",
3550
+ typeInto: "App-agnostic: type into the input matching a label: body { label, text, clear?, visibleOnly? }. (selector form is web-only)",
3551
+ readValue: "STUB \u2014 selector-based; NOT_SUPPORTED on native (no DOM). Use /control/element/:id/state to read a value.",
3552
+ findByText: "App-agnostic: list elements matching free-text (no press): body { text, visibleOnly? }",
3553
+ 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.",
3554
+ 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.",
3379
3555
  pushModal: "TEST HOOK \u2014 push a modal onto the snapshot.modalStack via the registry ModalDetector",
3380
3556
  dismissModal: "TEST HOOK \u2014 dismiss a modal by id from the snapshot.modalStack via the registry ModalDetector",
3381
3557
  keepAwake: "TEST HOOK \u2014 keep the device screen awake via the injected keepAwakeProvider: body { enabled: boolean, durationMs?: number }"
@@ -5546,7 +5722,8 @@ function useUIElement(options) {
5546
5722
  parentPath,
5547
5723
  style,
5548
5724
  stateStyles: stateStylesProp,
5549
- handlers: handlersProp
5725
+ handlers: handlersProp,
5726
+ value: valueProp
5550
5727
  } = options;
5551
5728
  const treePath = parentPath ? `${parentPath}/${id}` : id;
5552
5729
  const bridgeProps = react.useMemo(
@@ -5572,9 +5749,12 @@ function useUIElement(options) {
5572
5749
  });
5573
5750
  registeredRef.current = true;
5574
5751
  registeredIdRef.current = id;
5752
+ if (typeof valueProp === "string") {
5753
+ bridge.registry.updateElementState(id, { value: valueProp });
5754
+ }
5575
5755
  setRegistered(true);
5576
5756
  scheduleDeadButtonWarning(bridge, id, type);
5577
- }, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp]);
5757
+ }, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp]);
5578
5758
  const unregister = react.useCallback(() => {
5579
5759
  if (!bridge || !registeredRef.current) return;
5580
5760
  bridge.registry.unregisterElement(registeredIdRef.current ?? id);
@@ -5704,6 +5884,13 @@ function useUIElement(options) {
5704
5884
  }
5705
5885
  };
5706
5886
  }, [bridge, registered, id, handlersProp]);
5887
+ react.useEffect(() => {
5888
+ if (!bridge || !registered) return;
5889
+ if (typeof valueProp !== "string") return;
5890
+ const current = bridge.registry.getElement(id)?.getState().value;
5891
+ if (current === valueProp) return;
5892
+ bridge.registry.updateElementState(id, { value: valueProp });
5893
+ }, [bridge, registered, id, valueProp]);
5707
5894
  react.useCallback(
5708
5895
  (props) => {
5709
5896
  propsRef.current = { ...propsRef.current, ...props };
@@ -6095,6 +6282,11 @@ var UI_BRIDGE_NATIVE_ROUTES = {
6095
6282
  path: "/ui-bridge/control/snapshot",
6096
6283
  description: "Get full bridge snapshot"
6097
6284
  },
6285
+ DISCOVER: {
6286
+ method: "GET",
6287
+ path: "/ui-bridge/control/discover",
6288
+ description: "Discover elements (alias for snapshot; web/runner parity)"
6289
+ },
6098
6290
  PAGE_HEALTH: {
6099
6291
  method: "POST",
6100
6292
  path: "/ui-bridge/control/page-health",
@@ -6222,6 +6414,18 @@ var UI_BRIDGE_NATIVE_ROUTES = {
6222
6414
  path: "/ui-bridge/control/modal/dismiss/:id",
6223
6415
  description: "Dismiss a modal by id (testHooks)"
6224
6416
  },
6417
+ // Observability — last-N ring buffers (always mounted; `installed:false`
6418
+ // when the console/network patches are off, i.e. no testHooks).
6419
+ CONSOLE_ERRORS: {
6420
+ method: "GET",
6421
+ path: "/ui-bridge/control/console-errors",
6422
+ description: "Last-N captured console.error/console.warn entries"
6423
+ },
6424
+ NETWORK_REQUESTS: {
6425
+ method: "GET",
6426
+ path: "/ui-bridge/sdk/network-requests",
6427
+ description: "Last-N captured fetch/XHR requests with status + duration"
6428
+ },
6225
6429
  // Health
6226
6430
  HEALTH: {
6227
6431
  method: "GET",