@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.
- package/dist/control/index.d.mts +4 -4
- package/dist/control/index.d.ts +4 -4
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +39 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +39 -1
- package/dist/core/index.mjs.map +1 -1
- package/dist/debug/index.js +2 -2
- package/dist/debug/index.js.map +1 -1
- package/dist/debug/index.mjs +2 -2
- package/dist/debug/index.mjs.map +1 -1
- package/dist/design/index.d.mts +1 -1
- package/dist/design/index.d.ts +1 -1
- package/dist/{http-server-DCdR8WlL.d.mts → http-server-D2U1k48d.d.mts} +89 -8
- package/dist/{http-server-Dj3grQW4.d.ts → http-server-ck8clUsZ.d.ts} +89 -8
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +364 -39
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +364 -39
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.d.mts +5 -5
- package/dist/react/index.d.ts +5 -5
- package/dist/react/index.js +346 -39
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +346 -39
- package/dist/react/index.mjs.map +1 -1
- package/dist/{registry-Cahd8Gn2.d.mts → registry-BxXaxmsT.d.ts} +24 -1
- package/dist/{registry-z-3WJsxp.d.ts → registry-KdUW_koV.d.mts} +24 -1
- package/dist/server/index.d.mts +5 -5
- package/dist/server/index.d.ts +5 -5
- package/dist/server/index.js +272 -16
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +272 -16
- package/dist/server/index.mjs.map +1 -1
- package/dist/{types-Oz13YLZq.d.mts → types-BwdPNpII.d.mts} +52 -4
- package/dist/{types-Oz13YLZq.d.ts → types-BwdPNpII.d.ts} +52 -4
- package/dist/{types-ajakyp-l.d.ts → types-DCEv5qb6.d.ts} +1 -1
- package/dist/{types-DAfCeWXc.d.mts → types-DjB0zJJH.d.mts} +1 -1
- package/dist/{useUIBridgeToast-GmT1TzUe.d.mts → useUIBridgeToast-BPIEbsEc.d.mts} +11 -5
- package/dist/{useUIBridgeToast-D32KXyL1.d.ts → useUIBridgeToast-DD13oF3k.d.ts} +11 -5
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as BridgeEventListener,
|
|
1
|
+
import { a as BridgeEventListener, s as NativeSnapshotEnrichers, r as NativeSnapshotEnricher, j as NativeElementRef, l as NativeElementType, w as NativeStandardAction, h as NativeCustomAction, z as RegisteredNativeElement, k as NativeElementState, R as RegisteredNativeComponent, E as Workflow, b as BridgeEventType, e as NativeBridgeSnapshot, q as NativeRegistrationCoverage } from './types-BwdPNpII.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Native Element and Component Registry
|
|
@@ -69,12 +69,27 @@ declare class NativeUIBridgeRegistry {
|
|
|
69
69
|
private config;
|
|
70
70
|
private enrichers;
|
|
71
71
|
private snapshotExtras;
|
|
72
|
+
/**
|
|
73
|
+
* Sticky flag: flips `true` the first time any element is registered and
|
|
74
|
+
* stays `true` even after elements are unregistered. Lets agents distinguish
|
|
75
|
+
* "this route never wired any elements" from "this route registered then
|
|
76
|
+
* unmounted".
|
|
77
|
+
*/
|
|
78
|
+
private everHadRegistrations;
|
|
72
79
|
constructor(config?: NativeRegistryConfig);
|
|
73
80
|
/**
|
|
74
81
|
* Register/replace canonical enrichers (modal/toast/undo). HMR-safe — calling
|
|
75
82
|
* with a partial set merges into existing slots instead of clobbering them.
|
|
76
83
|
*/
|
|
77
84
|
setEnrichers(e: Partial<NativeSnapshotEnrichers>): void;
|
|
85
|
+
/**
|
|
86
|
+
* Read-only accessor for the canonical enrichers slot. Returned object is
|
|
87
|
+
* the live reference, so callers can detect "no detector wired" via
|
|
88
|
+
* `getEnrichers().modalDetector === undefined`. Used by the test-hook HTTP
|
|
89
|
+
* handlers (`control/modal/push`, `control/modal/dismiss/:id`) which need
|
|
90
|
+
* to drive the ModalDetector directly without re-instantiating it.
|
|
91
|
+
*/
|
|
92
|
+
getEnrichers(): Readonly<NativeSnapshotEnrichers>;
|
|
78
93
|
/**
|
|
79
94
|
* Register a custom snapshot enricher. The returned object will be
|
|
80
95
|
* `Object.assign`ed onto the snapshot, so use unique top-level keys to avoid
|
|
@@ -209,6 +224,14 @@ declare class NativeUIBridgeRegistry {
|
|
|
209
224
|
components: number;
|
|
210
225
|
workflows: number;
|
|
211
226
|
};
|
|
227
|
+
/**
|
|
228
|
+
* Compute registration coverage metadata for the current registry state.
|
|
229
|
+
*
|
|
230
|
+
* Groups currently-registered elements by `registrationRoute`, bucketing
|
|
231
|
+
* elements without a route under `'?'`. `everHadRegistrations` is sticky —
|
|
232
|
+
* once true, it stays true for the lifetime of the registry instance.
|
|
233
|
+
*/
|
|
234
|
+
getRegistrationCoverage(): NativeRegistrationCoverage;
|
|
212
235
|
/**
|
|
213
236
|
* Clear all registrations
|
|
214
237
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as BridgeEventListener,
|
|
1
|
+
import { a as BridgeEventListener, s as NativeSnapshotEnrichers, r as NativeSnapshotEnricher, j as NativeElementRef, l as NativeElementType, w as NativeStandardAction, h as NativeCustomAction, z as RegisteredNativeElement, k as NativeElementState, R as RegisteredNativeComponent, E as Workflow, b as BridgeEventType, e as NativeBridgeSnapshot, q as NativeRegistrationCoverage } from './types-BwdPNpII.mjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Native Element and Component Registry
|
|
@@ -69,12 +69,27 @@ declare class NativeUIBridgeRegistry {
|
|
|
69
69
|
private config;
|
|
70
70
|
private enrichers;
|
|
71
71
|
private snapshotExtras;
|
|
72
|
+
/**
|
|
73
|
+
* Sticky flag: flips `true` the first time any element is registered and
|
|
74
|
+
* stays `true` even after elements are unregistered. Lets agents distinguish
|
|
75
|
+
* "this route never wired any elements" from "this route registered then
|
|
76
|
+
* unmounted".
|
|
77
|
+
*/
|
|
78
|
+
private everHadRegistrations;
|
|
72
79
|
constructor(config?: NativeRegistryConfig);
|
|
73
80
|
/**
|
|
74
81
|
* Register/replace canonical enrichers (modal/toast/undo). HMR-safe — calling
|
|
75
82
|
* with a partial set merges into existing slots instead of clobbering them.
|
|
76
83
|
*/
|
|
77
84
|
setEnrichers(e: Partial<NativeSnapshotEnrichers>): void;
|
|
85
|
+
/**
|
|
86
|
+
* Read-only accessor for the canonical enrichers slot. Returned object is
|
|
87
|
+
* the live reference, so callers can detect "no detector wired" via
|
|
88
|
+
* `getEnrichers().modalDetector === undefined`. Used by the test-hook HTTP
|
|
89
|
+
* handlers (`control/modal/push`, `control/modal/dismiss/:id`) which need
|
|
90
|
+
* to drive the ModalDetector directly without re-instantiating it.
|
|
91
|
+
*/
|
|
92
|
+
getEnrichers(): Readonly<NativeSnapshotEnrichers>;
|
|
78
93
|
/**
|
|
79
94
|
* Register a custom snapshot enricher. The returned object will be
|
|
80
95
|
* `Object.assign`ed onto the snapshot, so use unique top-level keys to avoid
|
|
@@ -209,6 +224,14 @@ declare class NativeUIBridgeRegistry {
|
|
|
209
224
|
components: number;
|
|
210
225
|
workflows: number;
|
|
211
226
|
};
|
|
227
|
+
/**
|
|
228
|
+
* Compute registration coverage metadata for the current registry state.
|
|
229
|
+
*
|
|
230
|
+
* Groups currently-registered elements by `registrationRoute`, bucketing
|
|
231
|
+
* elements without a route under `'?'`. `everHadRegistrations` is sticky —
|
|
232
|
+
* once true, it stays true for the lifetime of the registry instance.
|
|
233
|
+
*/
|
|
234
|
+
getRegistrationCoverage(): NativeRegistrationCoverage;
|
|
212
235
|
/**
|
|
213
236
|
* Clear all registrations
|
|
214
237
|
*/
|
package/dist/server/index.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { H as HTTPRequest,
|
|
3
|
-
import { a as NativeUIBridgeRegistry } from '../registry-
|
|
4
|
-
import { N as NativeActionExecutor } from '../types-
|
|
1
|
+
import { k as NativeServerHandlers, e as HandlerContext, A as APIResponse } from '../http-server-D2U1k48d.mjs';
|
|
2
|
+
export { D as DismissModalResponse, F as FillFormFieldInput, a as FillFormFieldResult, b as FillFormRequest, c as FillFormResponse, H as HTTPRequest, d as HTTPResponse, f as HandlerFunction, J as JsonRpcBatchRequest, g as JsonRpcBatchResponse, u as JsonRpcClientMessage, h as JsonRpcEvent, i as JsonRpcRequest, j as JsonRpcResponse, v as JsonRpcServerMessage, w as JsonRpcSubscribe, x as JsonRpcUnsubscribe, N as NativeServerConfig, l as NativeUIBridgeServer, m as NavigationProvider, P as PushModalRequest, y as PushModalResponse, R as RequestHandler, n as RouteDefinition, o as RouteProvider, S as ScreenshotProvider, p as ServerAdapter, U as UI_BRIDGE_NATIVE_ROUTES, W as WebSocketConnection, q as WebSocketEventBridge, r as WebSocketMessageHandler, s as WebSocketServerAdapter, t as createNativeServer, z as isBatchRequest, B as isSubscribe, C as isUnsubscribe } from '../http-server-D2U1k48d.mjs';
|
|
3
|
+
import { a as NativeUIBridgeRegistry } from '../registry-KdUW_koV.mjs';
|
|
4
|
+
import { N as NativeActionExecutor } from '../types-DjB0zJJH.mjs';
|
|
5
5
|
import { E as ElementDesignData, S as StateStyles, R as ResponsiveSnapshot } from '../design-types-BHr6Q1FX.mjs';
|
|
6
|
-
import '../types-
|
|
6
|
+
import '../types-BwdPNpII.mjs';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* UI Bridge Native Server Handlers
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { H as HTTPRequest,
|
|
3
|
-
import { a as NativeUIBridgeRegistry } from '../registry-
|
|
4
|
-
import { N as NativeActionExecutor } from '../types-
|
|
1
|
+
import { k as NativeServerHandlers, e as HandlerContext, A as APIResponse } from '../http-server-ck8clUsZ.js';
|
|
2
|
+
export { D as DismissModalResponse, F as FillFormFieldInput, a as FillFormFieldResult, b as FillFormRequest, c as FillFormResponse, H as HTTPRequest, d as HTTPResponse, f as HandlerFunction, J as JsonRpcBatchRequest, g as JsonRpcBatchResponse, u as JsonRpcClientMessage, h as JsonRpcEvent, i as JsonRpcRequest, j as JsonRpcResponse, v as JsonRpcServerMessage, w as JsonRpcSubscribe, x as JsonRpcUnsubscribe, N as NativeServerConfig, l as NativeUIBridgeServer, m as NavigationProvider, P as PushModalRequest, y as PushModalResponse, R as RequestHandler, n as RouteDefinition, o as RouteProvider, S as ScreenshotProvider, p as ServerAdapter, U as UI_BRIDGE_NATIVE_ROUTES, W as WebSocketConnection, q as WebSocketEventBridge, r as WebSocketMessageHandler, s as WebSocketServerAdapter, t as createNativeServer, z as isBatchRequest, B as isSubscribe, C as isUnsubscribe } from '../http-server-ck8clUsZ.js';
|
|
3
|
+
import { a as NativeUIBridgeRegistry } from '../registry-BxXaxmsT.js';
|
|
4
|
+
import { N as NativeActionExecutor } from '../types-DCEv5qb6.js';
|
|
5
5
|
import { E as ElementDesignData, S as StateStyles, R as ResponsiveSnapshot } from '../design-types-BHr6Q1FX.js';
|
|
6
|
-
import '../types-
|
|
6
|
+
import '../types-BwdPNpII.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* UI Bridge Native Server Handlers
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
|
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
|
|
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 {
|
|
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 {
|
|
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;
|