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