@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/control/action-executor.d.ts +15 -0
- package/dist/control/action-executor.d.ts.map +1 -1
- package/dist/control/index.js +34 -0
- package/dist/control/index.js.map +1 -1
- package/dist/control/index.mjs +34 -0
- package/dist/control/index.mjs.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +1 -1
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/types.d.ts +1 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.js +220 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +220 -16
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.js +203 -16
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +203 -16
- package/dist/react/index.mjs.map +1 -1
- package/dist/react/useUIElement.d.ts +10 -0
- package/dist/react/useUIElement.d.ts.map +1 -1
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/http-server.d.ts.map +1 -1
- package/dist/server/index.js +172 -13
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +172 -13
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/types.d.ts +30 -0
- package/dist/server/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/server/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
2139
|
-
//
|
|
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
|
-
|
|
2193
|
-
|
|
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 }"
|