@qontinui/ui-bridge-native 0.2.0 → 0.2.1

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.
Files changed (43) hide show
  1. package/dist/control/index.d.mts +4 -4
  2. package/dist/control/index.d.ts +4 -4
  3. package/dist/core/index.d.mts +3 -3
  4. package/dist/core/index.d.ts +3 -3
  5. package/dist/core/index.js +39 -1
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +39 -1
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/debug/index.js +2 -2
  10. package/dist/debug/index.js.map +1 -1
  11. package/dist/debug/index.mjs +2 -2
  12. package/dist/debug/index.mjs.map +1 -1
  13. package/dist/design/index.d.mts +1 -1
  14. package/dist/design/index.d.ts +1 -1
  15. package/dist/{http-server-DCdR8WlL.d.mts → http-server-D2U1k48d.d.mts} +89 -8
  16. package/dist/{http-server-Dj3grQW4.d.ts → http-server-ck8clUsZ.d.ts} +89 -8
  17. package/dist/index.d.mts +5 -5
  18. package/dist/index.d.ts +5 -5
  19. package/dist/index.js +364 -39
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +364 -39
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/react/index.d.mts +5 -5
  24. package/dist/react/index.d.ts +5 -5
  25. package/dist/react/index.js +346 -39
  26. package/dist/react/index.js.map +1 -1
  27. package/dist/react/index.mjs +346 -39
  28. package/dist/react/index.mjs.map +1 -1
  29. package/dist/{registry-Cahd8Gn2.d.mts → registry-BxXaxmsT.d.ts} +24 -1
  30. package/dist/{registry-z-3WJsxp.d.ts → registry-KdUW_koV.d.mts} +24 -1
  31. package/dist/server/index.d.mts +5 -5
  32. package/dist/server/index.d.ts +5 -5
  33. package/dist/server/index.js +272 -16
  34. package/dist/server/index.js.map +1 -1
  35. package/dist/server/index.mjs +272 -16
  36. package/dist/server/index.mjs.map +1 -1
  37. package/dist/{types-Oz13YLZq.d.mts → types-BwdPNpII.d.mts} +52 -4
  38. package/dist/{types-Oz13YLZq.d.ts → types-BwdPNpII.d.ts} +52 -4
  39. package/dist/{types-ajakyp-l.d.ts → types-DCEv5qb6.d.ts} +1 -1
  40. package/dist/{types-DAfCeWXc.d.mts → types-DjB0zJJH.d.mts} +1 -1
  41. package/dist/{useUIBridgeToast-GmT1TzUe.d.mts → useUIBridgeToast-BPIEbsEc.d.mts} +11 -5
  42. package/dist/{useUIBridgeToast-D32KXyL1.d.ts → useUIBridgeToast-DD13oF3k.d.ts} +11 -5
  43. package/package.json +2 -2
@@ -133,6 +133,25 @@ var UI_BRIDGE_NATIVE_ROUTES = {
133
133
  path: "/ui-bridge/design/style-guide",
134
134
  description: "Clear loaded style guide"
135
135
  },
136
+ // AI helpers
137
+ AI_FILL_FORM: {
138
+ method: "POST",
139
+ path: "/ui-bridge/ai/fill-form",
140
+ description: "Fill multiple input elements in a single call"
141
+ },
142
+ // Test hooks — drive ModalDetector state from outside the React tree
143
+ // (gated by `features.testHooks`). Mirrors `pushModal` / `dismissModal`
144
+ // calls that components normally make in-process.
145
+ PUSH_MODAL: {
146
+ method: "POST",
147
+ path: "/ui-bridge/control/modal/push",
148
+ description: "Push a modal onto the modal stack (testHooks)"
149
+ },
150
+ DISMISS_MODAL: {
151
+ method: "POST",
152
+ path: "/ui-bridge/control/modal/dismiss/:id",
153
+ description: "Dismiss a modal by id (testHooks)"
154
+ },
136
155
  // Health
137
156
  HEALTH: {
138
157
  method: "GET",
@@ -935,6 +954,20 @@ function createServerHandlers(registry, executor, config) {
935
954
  return success2(response);
936
955
  },
937
956
  // Discovery
957
+ //
958
+ // Native parity with the runner's `POST /ui-bridge/ai/find`. The runner
959
+ // matches a free-text `query` against label > text > value > aria-label >
960
+ // placeholder > name. We replicate that here against the native registry's
961
+ // analogues:
962
+ // 1. `label` (the registered display label)
963
+ // 2. identifier fields: `testId`, `accessibilityLabel`, `treePath`, `uiId`
964
+ // 3. registered handler names (e.g. `onPress`, `onChangeText`)
965
+ //
966
+ // Match is case-insensitive substring. Empty/missing query falls back to
967
+ // the structured filters (types/visibleOnly/etc.) so callers that don't
968
+ // pass `query` still get the legacy behaviour. Once we've narrowed by
969
+ // query the response is shaped through the executor's NativeFindResponse
970
+ // envelope so the wire format stays stable.
938
971
  find: async (ctx) => {
939
972
  const body = ctx.body;
940
973
  const response = await executor.find({
@@ -942,9 +975,49 @@ function createServerHandlers(registry, executor, config) {
942
975
  testIdPattern: body?.testIdPattern,
943
976
  accessibilityLabelPattern: body?.accessibilityLabelPattern,
944
977
  visibleOnly: body?.visibleOnly,
945
- limit: body?.limit
978
+ // When `query` is supplied we apply our own limit *after* ranking so
979
+ // the highest-priority matches survive the cap; defer slicing to us
980
+ // by widening here.
981
+ limit: body?.query && body.query.length > 0 ? void 0 : body?.limit
982
+ });
983
+ const rawQuery = typeof body?.query === "string" ? body.query.trim() : "";
984
+ if (rawQuery.length === 0) {
985
+ return success2(response);
986
+ }
987
+ const needle = rawQuery.toLowerCase();
988
+ const containsNeedle = (value) => typeof value === "string" && value.toLowerCase().includes(needle);
989
+ const scored = [];
990
+ for (const el of response.elements) {
991
+ let priorityRank = Number.POSITIVE_INFINITY;
992
+ if (containsNeedle(el.label)) {
993
+ priorityRank = Math.min(priorityRank, 0);
994
+ }
995
+ const ident = el.identifier;
996
+ if (ident) {
997
+ if (containsNeedle(ident.testId)) priorityRank = Math.min(priorityRank, 1);
998
+ if (containsNeedle(ident.accessibilityLabel)) priorityRank = Math.min(priorityRank, 1);
999
+ if (containsNeedle(ident.treePath)) priorityRank = Math.min(priorityRank, 1);
1000
+ if (containsNeedle(ident.uiId)) priorityRank = Math.min(priorityRank, 1);
1001
+ }
1002
+ if (priorityRank === Number.POSITIVE_INFINITY) {
1003
+ const registered = registry.getElement(el.id);
1004
+ const handlers = extractHandlerNames(registered?.props);
1005
+ if (handlers.some((h) => h.toLowerCase().includes(needle))) {
1006
+ priorityRank = 2;
1007
+ }
1008
+ }
1009
+ if (priorityRank !== Number.POSITIVE_INFINITY) {
1010
+ scored.push({ priorityRank, element: el });
1011
+ }
1012
+ }
1013
+ scored.sort((a, b) => a.priorityRank - b.priorityRank);
1014
+ const limit = body?.limit && body.limit > 0 ? body.limit : void 0;
1015
+ const filtered = limit ? scored.slice(0, limit) : scored;
1016
+ return success2({
1017
+ ...response,
1018
+ elements: filtered.map((s) => s.element),
1019
+ total: filtered.length
946
1020
  });
947
- return success2(response);
948
1021
  },
949
1022
  getSnapshot: async (ctx) => {
950
1023
  const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
@@ -1045,15 +1118,162 @@ function createServerHandlers(registry, executor, config) {
1045
1118
  getMethods: async () => {
1046
1119
  return error2("Methods introspection not configured", "NOT_SUPPORTED");
1047
1120
  },
1121
+ // AI helpers
1122
+ //
1123
+ // Mobile-side parity with the runner's `POST /ui-bridge/ai/fill-form`.
1124
+ // Each entry in `fields` is dispatched through the existing executor as a
1125
+ // `type` action with `clear: true` so the input value is replaced
1126
+ // atomically (the closest native equivalent to the web bridge's
1127
+ // "setValue" semantics — there is no `setValue` action on native).
1128
+ //
1129
+ // Failures do not short-circuit: every field is attempted, and the
1130
+ // response carries per-field success/error plus aggregate counts.
1131
+ fillForm: async (ctx) => {
1132
+ const body = ctx.body;
1133
+ const rawFields = body?.fields;
1134
+ if (!Array.isArray(rawFields)) {
1135
+ return error2('Request body must include "fields" array', "INVALID_REQUEST");
1136
+ }
1137
+ const results = [];
1138
+ for (let i = 0; i < rawFields.length; i++) {
1139
+ const field = rawFields[i];
1140
+ if (!field || typeof field !== "object") {
1141
+ results.push({
1142
+ elementId: "",
1143
+ success: false,
1144
+ error: `fields[${i}] must be an object with "elementId" and "value"`
1145
+ });
1146
+ continue;
1147
+ }
1148
+ const { elementId, value } = field;
1149
+ if (typeof elementId !== "string" || elementId.length === 0) {
1150
+ results.push({
1151
+ elementId: typeof elementId === "string" ? elementId : "",
1152
+ success: false,
1153
+ error: `fields[${i}] is missing a non-empty "elementId"`
1154
+ });
1155
+ continue;
1156
+ }
1157
+ if (typeof value !== "string") {
1158
+ results.push({
1159
+ elementId,
1160
+ success: false,
1161
+ error: `fields[${i}] is missing a string "value"`
1162
+ });
1163
+ continue;
1164
+ }
1165
+ try {
1166
+ const response = await executor.executeAction(elementId, {
1167
+ action: "type",
1168
+ params: { text: value, clear: true }
1169
+ });
1170
+ if (response.success) {
1171
+ results.push({ elementId, success: true });
1172
+ } else {
1173
+ results.push({
1174
+ elementId,
1175
+ success: false,
1176
+ error: response.error ?? "Action failed"
1177
+ });
1178
+ }
1179
+ } catch (err) {
1180
+ results.push({
1181
+ elementId,
1182
+ success: false,
1183
+ error: err instanceof Error ? err.message : String(err)
1184
+ });
1185
+ }
1186
+ }
1187
+ const succeededCount = results.filter((r) => r.success).length;
1188
+ const failedCount = results.length - succeededCount;
1189
+ return success2({ results, succeededCount, failedCount });
1190
+ },
1191
+ // Test hooks — modal stack manipulation
1192
+ //
1193
+ // These two endpoints exist so external automation runners can drive
1194
+ // ModalDetector state via HTTP, mirroring the in-tree `pushModal` /
1195
+ // `dismissModal` calls that components normally make. They're gated by
1196
+ // `features.testHooks` at the route layer (see http-server.ts); the
1197
+ // handlers themselves still defend against a missing modalDetector
1198
+ // because the bare server path used by CloudRelayClient may run before
1199
+ // enrichers are wired.
1200
+ pushModal: async (ctx) => {
1201
+ const detector = registry.getEnrichers().modalDetector;
1202
+ if (!detector) {
1203
+ return error2(
1204
+ "modal detector not registered",
1205
+ "ENRICHER_UNAVAILABLE"
1206
+ );
1207
+ }
1208
+ const body = ctx.body;
1209
+ const id = body?.id;
1210
+ if (typeof id !== "string" || id.length === 0) {
1211
+ return error2(
1212
+ 'pushModal requires a non-empty "id"',
1213
+ "INVALID_REQUEST"
1214
+ );
1215
+ }
1216
+ const allowedTypes = ["modal", "sheet", "drawer", "popover", "alertdialog", "dialog"];
1217
+ const requestedType = body?.type;
1218
+ if (requestedType !== void 0 && (typeof requestedType !== "string" || !allowedTypes.includes(requestedType))) {
1219
+ return error2(
1220
+ `pushModal "type" must be one of: ${allowedTypes.join(", ")}`,
1221
+ "INVALID_REQUEST"
1222
+ );
1223
+ }
1224
+ detector.pushModal({
1225
+ id,
1226
+ title: body?.title,
1227
+ // requestedType is narrowed by the allowedTypes guard above; the cast
1228
+ // converts the validated string to the NativeModalInfo['type'] union.
1229
+ type: requestedType,
1230
+ blocking: body?.blocking,
1231
+ dismissible: body?.dismissible
1232
+ });
1233
+ const stored = detector.getActive().find((m) => m.id === id);
1234
+ return success2({
1235
+ pushed: true,
1236
+ id,
1237
+ title: stored?.title,
1238
+ type: stored?.type,
1239
+ blocking: stored?.blocking,
1240
+ dismissible: stored?.dismissible,
1241
+ pushedAt: stored?.detectedAt ?? Date.now()
1242
+ });
1243
+ },
1244
+ dismissModal: async (ctx) => {
1245
+ const detector = registry.getEnrichers().modalDetector;
1246
+ if (!detector) {
1247
+ return error2(
1248
+ "modal detector not registered",
1249
+ "ENRICHER_UNAVAILABLE"
1250
+ );
1251
+ }
1252
+ const { id } = ctx.params;
1253
+ if (typeof id !== "string" || id.length === 0) {
1254
+ return error2(
1255
+ "dismissModal requires a modal id in the path",
1256
+ "INVALID_REQUEST"
1257
+ );
1258
+ }
1259
+ const wasPresent = detector.getActive().some((m) => m.id === id);
1260
+ if (!wasPresent) {
1261
+ return error2(`modal not found: ${id}`, "MODAL_NOT_FOUND");
1262
+ }
1263
+ detector.dismissModal(id);
1264
+ return success2({ dismissed: id });
1265
+ },
1048
1266
  // Design Review
1049
1267
  ...designHandlers,
1050
1268
  // Health
1051
1269
  health: async () => {
1052
1270
  const stats = registry.getStats();
1271
+ const registration = registry.getRegistrationCoverage();
1053
1272
  const response = {
1054
1273
  status: "healthy",
1055
1274
  timestamp: Date.now(),
1056
- ...stats
1275
+ ...stats,
1276
+ registration
1057
1277
  };
1058
1278
  if (config?.appInfo) {
1059
1279
  response.uiBridge = {
@@ -1154,7 +1374,24 @@ var WS_ROUTES = [
1154
1374
  { pattern: "design/evaluate/baseline", handler: "saveBaseline", httpMethods: ["POST"] },
1155
1375
  { pattern: "design/evaluate/diff", handler: "diffBaseline", httpMethods: ["POST"] },
1156
1376
  // Meta / Introspection
1157
- { pattern: "meta/methods", handler: "getMethods", httpMethods: ["GET"] }
1377
+ { pattern: "meta/methods", handler: "getMethods", httpMethods: ["GET"] },
1378
+ // AI helpers — mobile parity with the runner's `/ui-bridge/ai/*` family.
1379
+ { pattern: "ai/fill-form", handler: "fillForm", httpMethods: ["POST"] },
1380
+ // Test hooks — gated behind `features.testHooks`. Let external runners
1381
+ // drive the ModalDetector over HTTP so tests can flip `snapshot.modalStack`
1382
+ // without mounting React components.
1383
+ {
1384
+ pattern: "control/modal/push",
1385
+ handler: "pushModal",
1386
+ httpMethods: ["POST"],
1387
+ requiresTestHooks: true
1388
+ },
1389
+ {
1390
+ pattern: "control/modal/dismiss/:id",
1391
+ handler: "dismissModal",
1392
+ httpMethods: ["POST"],
1393
+ requiresTestHooks: true
1394
+ }
1158
1395
  ];
1159
1396
  var HANDLER_DESCRIPTIONS = {
1160
1397
  health: "Liveness probe; returns app metadata + element count",
@@ -1187,7 +1424,10 @@ var HANDLER_DESCRIPTIONS = {
1187
1424
  getQualityContexts: "List available quality evaluation contexts",
1188
1425
  saveBaseline: "Save a quality baseline snapshot",
1189
1426
  diffBaseline: "Diff current UI against a saved baseline",
1190
- getMethods: "List every handler name (legacy introspection)"
1427
+ getMethods: "List every handler name (legacy introspection)",
1428
+ fillForm: "Fill multiple inputs in one call: body { fields: [{elementId, value}] } \u2014 each field dispatches `type` + `clear:true`",
1429
+ pushModal: "TEST HOOK \u2014 push a modal onto the snapshot.modalStack via the registry ModalDetector",
1430
+ dismissModal: "TEST HOOK \u2014 dismiss a modal by id from the snapshot.modalStack via the registry ModalDetector"
1191
1431
  };
1192
1432
  var ACTION_REFERENCE = [
1193
1433
  { name: "press", aliases: ["click"], params: "PressActionParams (optional position)" },
@@ -1204,10 +1444,11 @@ var ACTION_REFERENCE = [
1204
1444
  { name: "swipe", params: "SwipeActionParams { direction, distance }" },
1205
1445
  { name: "toggle", params: "none \u2014 flips boolean controls" }
1206
1446
  ];
1207
- function buildRoutesPayload() {
1447
+ function buildRoutesPayload(testHooks) {
1208
1448
  const entries = [];
1209
1449
  for (const r of WS_ROUTES) {
1210
1450
  if (!r.httpMethods) continue;
1451
+ if (r.requiresTestHooks && !testHooks) continue;
1211
1452
  for (const m of r.httpMethods) {
1212
1453
  entries.push({ method: m, path: `/ui-bridge/${r.pattern}` });
1213
1454
  }
@@ -1217,11 +1458,11 @@ function buildRoutesPayload() {
1217
1458
  entries.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
1218
1459
  return { routes: entries, count: entries.length };
1219
1460
  }
1220
- function buildHelpPayload() {
1461
+ function buildHelpPayload(testHooks) {
1221
1462
  return {
1222
1463
  version: 1,
1223
1464
  description: "UI Bridge Native HTTP surface. Prefix every path with /ui-bridge/.",
1224
- routes: WS_ROUTES.map((r) => ({
1465
+ routes: WS_ROUTES.filter((r) => !r.requiresTestHooks || testHooks).map((r) => ({
1225
1466
  pattern: r.pattern,
1226
1467
  httpMethods: r.httpMethods ?? [],
1227
1468
  wsOnly: !r.httpMethods,
@@ -1257,7 +1498,8 @@ var NativeUIBridgeServer = class {
1257
1498
  appInfo: this.config.appInfo
1258
1499
  });
1259
1500
  this.handlers.getMethods = async () => {
1260
- const methods = WS_ROUTES.map((r) => ({
1501
+ const testHooks = this.config.testHooks === true;
1502
+ const methods = WS_ROUTES.filter((r) => !r.requiresTestHooks || testHooks).map((r) => ({
1261
1503
  method: r.pattern,
1262
1504
  httpMethods: r.httpMethods ? [...r.httpMethods] : void 0
1263
1505
  }));
@@ -1300,7 +1542,11 @@ var NativeUIBridgeServer = class {
1300
1542
  timestamp: Date.now()
1301
1543
  };
1302
1544
  } catch (e) {
1303
- return { success: false, error: `Navigation failed: ${e.message}`, timestamp: Date.now() };
1545
+ return {
1546
+ success: false,
1547
+ error: `Navigation failed: ${e instanceof Error ? e.message : String(e)}`,
1548
+ timestamp: Date.now()
1549
+ };
1304
1550
  }
1305
1551
  };
1306
1552
  this.handlers.pageReplace = async (ctx) => {
@@ -1326,7 +1572,7 @@ var NativeUIBridgeServer = class {
1326
1572
  } catch (e) {
1327
1573
  return {
1328
1574
  success: false,
1329
- error: `Replace navigation failed: ${e.message}`,
1575
+ error: `Replace navigation failed: ${e instanceof Error ? e.message : String(e)}`,
1330
1576
  timestamp: Date.now()
1331
1577
  };
1332
1578
  }
@@ -1345,7 +1591,7 @@ var NativeUIBridgeServer = class {
1345
1591
  } catch (e) {
1346
1592
  return {
1347
1593
  success: false,
1348
- error: `Back navigation failed: ${e.message}`,
1594
+ error: `Back navigation failed: ${e instanceof Error ? e.message : String(e)}`,
1349
1595
  timestamp: Date.now()
1350
1596
  };
1351
1597
  }
@@ -1366,7 +1612,11 @@ var NativeUIBridgeServer = class {
1366
1612
  timestamp: Date.now()
1367
1613
  };
1368
1614
  } catch (e) {
1369
- return { success: false, error: `Refresh failed: ${e.message}`, timestamp: Date.now() };
1615
+ return {
1616
+ success: false,
1617
+ error: `Refresh failed: ${e instanceof Error ? e.message : String(e)}`,
1618
+ timestamp: Date.now()
1619
+ };
1370
1620
  }
1371
1621
  };
1372
1622
  }
@@ -1424,7 +1674,7 @@ var NativeUIBridgeServer = class {
1424
1674
  } catch (e) {
1425
1675
  return {
1426
1676
  success: false,
1427
- error: `Screenshot capture failed: ${e.message}`,
1677
+ error: `Screenshot capture failed: ${e instanceof Error ? e.message : String(e)}`,
1428
1678
  timestamp: Date.now()
1429
1679
  };
1430
1680
  }
@@ -1979,9 +2229,11 @@ var NativeUIBridgeServer = class {
1979
2229
  * "control/component/my-comp/action/submit" → handlers.executeComponentAction
1980
2230
  */
1981
2231
  async routeMethodToHandler(method, params) {
2232
+ const testHooks = this.config.testHooks === true;
1982
2233
  for (const route of WS_ROUTES) {
1983
2234
  const pathParams = parsePath(route.pattern, method);
1984
2235
  if (!pathParams) continue;
2236
+ if (route.requiresTestHooks && !testHooks) continue;
1985
2237
  const ctx = {
1986
2238
  params: pathParams,
1987
2239
  query: params.query && typeof params.query === "object" ? params.query : {},
@@ -2059,17 +2311,18 @@ var NativeUIBridgeServer = class {
2059
2311
  };
2060
2312
  }
2061
2313
  const stripped = path.slice(PREFIX.length);
2314
+ const testHooks = this.config.testHooks === true;
2062
2315
  if (method === "GET" && stripped === "_help") {
2063
2316
  return {
2064
2317
  success: true,
2065
- data: buildHelpPayload(),
2318
+ data: buildHelpPayload(testHooks),
2066
2319
  timestamp: Date.now()
2067
2320
  };
2068
2321
  }
2069
2322
  if (method === "GET" && stripped === "_routes") {
2070
2323
  return {
2071
2324
  success: true,
2072
- data: buildRoutesPayload(),
2325
+ data: buildRoutesPayload(testHooks),
2073
2326
  timestamp: Date.now()
2074
2327
  };
2075
2328
  }
@@ -2080,6 +2333,9 @@ var NativeUIBridgeServer = class {
2080
2333
  if (!route.httpMethods) {
2081
2334
  continue;
2082
2335
  }
2336
+ if (route.requiresTestHooks && !testHooks) {
2337
+ continue;
2338
+ }
2083
2339
  if (!route.httpMethods.includes(method)) {
2084
2340
  patternMatchedButWrongVerb = true;
2085
2341
  continue;