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