@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.
@@ -55,6 +55,11 @@ var UI_BRIDGE_NATIVE_ROUTES = {
55
55
  path: "/ui-bridge/control/snapshot",
56
56
  description: "Get full bridge snapshot"
57
57
  },
58
+ DISCOVER: {
59
+ method: "GET",
60
+ path: "/ui-bridge/control/discover",
61
+ description: "Discover elements (alias for snapshot; web/runner parity)"
62
+ },
58
63
  PAGE_HEALTH: {
59
64
  method: "POST",
60
65
  path: "/ui-bridge/control/page-health",
@@ -182,6 +187,18 @@ var UI_BRIDGE_NATIVE_ROUTES = {
182
187
  path: "/ui-bridge/control/modal/dismiss/:id",
183
188
  description: "Dismiss a modal by id (testHooks)"
184
189
  },
190
+ // Observability — last-N ring buffers (always mounted; `installed:false`
191
+ // when the console/network patches are off, i.e. no testHooks).
192
+ CONSOLE_ERRORS: {
193
+ method: "GET",
194
+ path: "/ui-bridge/control/console-errors",
195
+ description: "Last-N captured console.error/console.warn entries"
196
+ },
197
+ NETWORK_REQUESTS: {
198
+ method: "GET",
199
+ path: "/ui-bridge/sdk/network-requests",
200
+ description: "Last-N captured fetch/XHR requests with status + duration"
201
+ },
185
202
  // Health
186
203
  HEALTH: {
187
204
  method: "GET",
@@ -912,6 +929,35 @@ function diagnosePageHealth(elements, viewport) {
912
929
  }
913
930
 
914
931
  // src/server/handlers.ts
932
+ function matchElementsByText(registry, text, opts) {
933
+ const needle = text.toLowerCase();
934
+ const contains = (v) => typeof v === "string" && v.toLowerCase().includes(needle);
935
+ let all = registry.getAllElements();
936
+ if (opts?.visibleOnly) {
937
+ all = all.filter((e) => e.getState().visible);
938
+ }
939
+ const scored = [];
940
+ for (const e of all) {
941
+ let rank = Number.POSITIVE_INFINITY;
942
+ if (contains(e.label)) rank = Math.min(rank, 0);
943
+ const ident = e.getIdentifier();
944
+ if (ident) {
945
+ if (contains(ident.testId)) rank = Math.min(rank, 1);
946
+ if (contains(ident.accessibilityLabel)) rank = Math.min(rank, 1);
947
+ if (contains(ident.treePath)) rank = Math.min(rank, 1);
948
+ if (contains(ident.uiId)) rank = Math.min(rank, 1);
949
+ }
950
+ if (rank === Number.POSITIVE_INFINITY) {
951
+ const handlers = extractHandlerNames(e.props);
952
+ if (handlers.some((h) => h.toLowerCase().includes(needle))) rank = 2;
953
+ }
954
+ if (rank !== Number.POSITIVE_INFINITY) {
955
+ scored.push({ rank, element: { id: e.id, type: e.type, label: e.label } });
956
+ }
957
+ }
958
+ scored.sort((a, b) => a.rank - b.rank);
959
+ return scored.map((s) => s.element);
960
+ }
915
961
  async function executeWorkflowStep(step, registry, executor) {
916
962
  const startTime = Date.now();
917
963
  try {
@@ -1508,6 +1554,89 @@ function createServerHandlers(registry, executor, config) {
1508
1554
  const failedCount = results.length - succeededCount;
1509
1555
  return success2({ results, succeededCount, failedCount });
1510
1556
  },
1557
+ // ========================================================================
1558
+ // App-agnostic interaction parity (cross-platform `/control/page/*`)
1559
+ //
1560
+ // Mirrors the web/runner convenience endpoints so the SAME contract drives
1561
+ // mobile. Text/label interactions compose `executor.find` (registry-based
1562
+ // discovery) + `executor.executeAction` (press/type). Selector-only
1563
+ // variants have no React-Native analog and return NOT_SUPPORTED — the
1564
+ // mobile bridge's snapshot + `/control/find` (testID / accessibilityLabel)
1565
+ // cover what a selector would on web.
1566
+ // ========================================================================
1567
+ clickByText: async (ctx) => {
1568
+ const body = ctx.body;
1569
+ const text = typeof body?.text === "string" ? body.text.trim() : "";
1570
+ if (!text) {
1571
+ return error2("text is required and must not be empty", "INVALID_REQUEST");
1572
+ }
1573
+ const match = matchElementsByText(registry, text, { visibleOnly: body?.visibleOnly })[0];
1574
+ if (!match) {
1575
+ return error2(`No element found with text "${text}"`, "ELEMENT_NOT_FOUND");
1576
+ }
1577
+ const response = await executor.executeAction(match.id, { action: "press" });
1578
+ if (!response.success) {
1579
+ return error2(response.error || "Press failed", "ACTION_FAILED");
1580
+ }
1581
+ return success2({
1582
+ clicked: true,
1583
+ element: { id: match.id, type: match.type, label: match.label }
1584
+ });
1585
+ },
1586
+ clickBySelector: async () => {
1587
+ return error2(
1588
+ "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.",
1589
+ "NOT_SUPPORTED"
1590
+ );
1591
+ },
1592
+ typeInto: async (ctx) => {
1593
+ const body = ctx.body;
1594
+ const label = typeof body?.label === "string" ? body.label.trim() : "";
1595
+ if (!label) {
1596
+ return error2(
1597
+ "label is required on native (selector-based typeInto is web-only)",
1598
+ "INVALID_REQUEST"
1599
+ );
1600
+ }
1601
+ const text = typeof body?.text === "string" ? body.text : "";
1602
+ const match = matchElementsByText(registry, label, { visibleOnly: body?.visibleOnly })[0];
1603
+ if (!match) {
1604
+ return error2(`No input found for label "${label}"`, "ELEMENT_NOT_FOUND");
1605
+ }
1606
+ const response = await executor.executeAction(match.id, {
1607
+ action: "type",
1608
+ params: { text, clear: body?.clear === true }
1609
+ });
1610
+ if (!response.success) {
1611
+ return error2(response.error || "Type failed", "ACTION_FAILED");
1612
+ }
1613
+ return success2({
1614
+ typed: true,
1615
+ element: { id: match.id, type: match.type, label: match.label }
1616
+ });
1617
+ },
1618
+ readValue: async () => {
1619
+ return error2(
1620
+ "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.",
1621
+ "NOT_SUPPORTED"
1622
+ );
1623
+ },
1624
+ findByText: async (ctx) => {
1625
+ const body = ctx.body;
1626
+ const text = typeof body?.text === "string" ? body.text.trim() : "";
1627
+ if (!text) {
1628
+ return error2("text is required and must not be empty", "INVALID_REQUEST");
1629
+ }
1630
+ const matches = matchElementsByText(registry, text, { visibleOnly: body?.visibleOnly });
1631
+ return success2(
1632
+ matches.map((el, index) => ({
1633
+ index,
1634
+ id: el.id,
1635
+ type: el.type,
1636
+ label: el.label
1637
+ }))
1638
+ );
1639
+ },
1511
1640
  // Coord-based tap
1512
1641
  //
1513
1642
  // Synthesizes a press at the given screen coordinates by scanning the
@@ -1602,7 +1731,8 @@ function createServerHandlers(registry, executor, config) {
1602
1731
  return success2({
1603
1732
  entries: [],
1604
1733
  count: 0,
1605
- bufferSize: 0
1734
+ bufferSize: 0,
1735
+ installed: false
1606
1736
  });
1607
1737
  }
1608
1738
  const entries = buf.entries({
@@ -1612,7 +1742,8 @@ function createServerHandlers(registry, executor, config) {
1612
1742
  return success2({
1613
1743
  entries,
1614
1744
  count: entries.length,
1615
- bufferSize: buf.size()
1745
+ bufferSize: buf.size(),
1746
+ installed: buf.isInstalled()
1616
1747
  });
1617
1748
  },
1618
1749
  getNetworkRequests: async (ctx) => {
@@ -1623,7 +1754,8 @@ function createServerHandlers(registry, executor, config) {
1623
1754
  return success2({
1624
1755
  entries: [],
1625
1756
  count: 0,
1626
- bufferSize: 0
1757
+ bufferSize: 0,
1758
+ installed: false
1627
1759
  });
1628
1760
  }
1629
1761
  const entries = buf.entries({
@@ -1633,7 +1765,8 @@ function createServerHandlers(registry, executor, config) {
1633
1765
  return success2({
1634
1766
  entries,
1635
1767
  count: entries.length,
1636
- bufferSize: buf.size()
1768
+ bufferSize: buf.size(),
1769
+ installed: buf.isInstalled()
1637
1770
  });
1638
1771
  },
1639
1772
  // Test hooks — modal stack manipulation
@@ -2052,7 +2185,10 @@ var WS_ROUTES = [
2052
2185
  // same handler at both paths to keep agents portable across platforms.
2053
2186
  { pattern: "ai/find", handler: "find", httpMethods: ["POST"] },
2054
2187
  { pattern: "control/snapshot", handler: "getSnapshot", httpMethods: ["GET"] },
2055
- { pattern: "control/discover", handler: "getSnapshot" },
2188
+ // Discovery alias — web/runner parity with `POST /control/discover` (a
2189
+ // legacy alias for snapshot/find). Mounted on HTTP (GET+POST) so mobile
2190
+ // doesn't 404 where web returns 200; delegates to the snapshot handler.
2191
+ { pattern: "control/discover", handler: "getSnapshot", httpMethods: ["GET", "POST"] },
2056
2192
  // Page health — accepts both GET and POST so the canonical skill invocation
2057
2193
  // (`POST .../control/page-health` with an empty body) and ad-hoc curls both
2058
2194
  // work without a body-shape gotcha.
@@ -2114,6 +2250,17 @@ var WS_ROUTES = [
2114
2250
  // Coord-based tap — synthesizes a press at the given screen coords by
2115
2251
  // matching against registered elements' layout rects.
2116
2252
  { pattern: "control/tap", handler: "tapAt", httpMethods: ["POST"] },
2253
+ // App-agnostic interaction parity — mirrors the web/runner convenience
2254
+ // endpoints (`/control/page/click-by-text` etc.) so the SAME cross-platform
2255
+ // contract drives mobile. Text/label-based interactions compose
2256
+ // `executor.find` + `executor.executeAction` over the registry; the
2257
+ // DOM-selector-only variants (`click-by-selector`, `read-value`) have no RN
2258
+ // analog and return a structured NOT_SUPPORTED envelope.
2259
+ { pattern: "control/page/click-by-text", handler: "clickByText", httpMethods: ["POST"] },
2260
+ { pattern: "control/page/click-by-selector", handler: "clickBySelector", httpMethods: ["POST"] },
2261
+ { pattern: "control/page/type-into", handler: "typeInto", httpMethods: ["POST"] },
2262
+ { pattern: "control/page/read-value", handler: "readValue", httpMethods: ["POST"] },
2263
+ { pattern: "control/page/find-by-text", handler: "findByText", httpMethods: ["POST"] },
2117
2264
  // Test hooks — gated behind `features.testHooks`. Let external runners
2118
2265
  // drive the ModalDetector over HTTP so tests can flip `snapshot.modalStack`
2119
2266
  // without mounting React components.
@@ -2135,19 +2282,26 @@ var WS_ROUTES = [
2135
2282
  httpMethods: ["POST"],
2136
2283
  requiresTestHooks: true
2137
2284
  },
2138
- // Observability — last-N console errors + network requests. testHooks-gated
2139
- // because patching `console.error` / `fetch` is invasive in production.
2285
+ // Observability — last-N console errors + network requests.
2286
+ //
2287
+ // Always mounted (not testHooks-gated) so they return a schema-valid 200
2288
+ // on every build instead of a 404 that agents can't distinguish from a
2289
+ // missing route. The INVASIVE part — monkey-patching `console.error` /
2290
+ // `fetch` / `XMLHttpRequest` — stays gated behind `testHooks` (see
2291
+ // NativeUIBridgeServer.start → buffer.install()). When the patches aren't
2292
+ // installed the handlers report `installed: false` + empty entries, so
2293
+ // production builds expose the contract without observing real users'
2294
+ // console/network traffic. Set `features.testHooks` (typically on
2295
+ // `__DEV__`) to populate the buffers with live data.
2140
2296
  {
2141
2297
  pattern: "control/console-errors",
2142
2298
  handler: "getConsoleErrors",
2143
- httpMethods: ["GET"],
2144
- requiresTestHooks: true
2299
+ httpMethods: ["GET"]
2145
2300
  },
2146
2301
  {
2147
2302
  pattern: "sdk/network-requests",
2148
2303
  handler: "getNetworkRequests",
2149
- httpMethods: ["GET"],
2150
- requiresTestHooks: true
2304
+ httpMethods: ["GET"]
2151
2305
  }
2152
2306
  ];
2153
2307
  var HANDLER_DESCRIPTIONS = {
@@ -2189,8 +2343,13 @@ var HANDLER_DESCRIPTIONS = {
2189
2343
  aiChangeBufferEnable: "STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Use WS event subscriptions for change notifications.",
2190
2344
  aiWaitForElement: "STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Use the WS waitForElement / waitForCondition JSON-RPC methods.",
2191
2345
  tapAt: "Synthesize a press at given screen coords by matching registered element layout rects: body { x, y, action? }",
2192
- getConsoleErrors: "TEST HOOK \u2014 last-N console.error/console.warn entries; ?since=<ms>&limit=<n>",
2193
- getNetworkRequests: "TEST HOOK \u2014 last-N fetch/XHR entries with status + duration; ?since=<ms>&limit=<n>",
2346
+ clickByText: "App-agnostic: press the first element matching free-text (label/accessibilityLabel/testID): body { text, visibleOnly? }",
2347
+ clickBySelector: "STUB \u2014 selector-based; NOT_SUPPORTED on native (no DOM). Use clickByText or /control/find + /control/element/:id/action.",
2348
+ typeInto: "App-agnostic: type into the input matching a label: body { label, text, clear?, visibleOnly? }. (selector form is web-only)",
2349
+ readValue: "STUB \u2014 selector-based; NOT_SUPPORTED on native (no DOM). Use /control/element/:id/state to read a value.",
2350
+ findByText: "App-agnostic: list elements matching free-text (no press): body { text, visibleOnly? }",
2351
+ 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.",
2352
+ 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.",
2194
2353
  pushModal: "TEST HOOK \u2014 push a modal onto the snapshot.modalStack via the registry ModalDetector",
2195
2354
  dismissModal: "TEST HOOK \u2014 dismiss a modal by id from the snapshot.modalStack via the registry ModalDetector",
2196
2355
  keepAwake: "TEST HOOK \u2014 keep the device screen awake via the injected keepAwakeProvider: body { enabled: boolean, durationMs?: number }"