@appilots/sdk 0.7.0 → 0.10.0
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/README.md +46 -4
- package/dist/.build-meta.json +5 -0
- package/dist/{chunk-KYFAXT6V.js → chunk-2GCWCPSD.js} +19 -16
- package/dist/{chunk-R4D34FEW.mjs → chunk-AHHDOUVX.mjs} +2151 -333
- package/dist/{chunk-DR75QTYK.mjs → chunk-IHPLS5FE.mjs} +16 -13
- package/dist/{chunk-HFRIB4YN.js → chunk-VX7AL4SA.js} +1049 -1162
- package/dist/{chunk-KUFWRJC4.mjs → chunk-XHR65V2W.mjs} +831 -949
- package/dist/{chunk-DZ7QRFHD.js → chunk-YB77RYCC.js} +2178 -332
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.js +11 -11
- package/dist/hooks/index.mjs +2 -2
- package/dist/{index-nI-s3Exg.d.mts → index-BPZ0j71S.d.mts} +315 -15
- package/dist/{index-nI-s3Exg.d.ts → index-BPZ0j71S.d.ts} +315 -15
- package/dist/index.d.mts +325 -22
- package/dist/index.d.ts +325 -22
- package/dist/index.js +340 -227
- package/dist/index.mjs +274 -173
- package/dist/navigation/index.d.mts +171 -3
- package/dist/navigation/index.d.ts +171 -3
- package/dist/navigation/index.js +13 -13
- package/dist/navigation/index.mjs +2 -2
- package/metro.d.ts +16 -1
- package/metro.js +104 -10
- package/package.json +16 -6
- package/dist/registerScreen-D1oGm28T.d.mts +0 -159
- package/dist/registerScreen-D1oGm28T.d.ts +0 -159
|
@@ -1,659 +1,7 @@
|
|
|
1
|
-
import { getFiberRoot, recordIntrospectionFailure, getCurrentScreen, getActiveRouteNames, deriveInteractionElements, attachElementIdsToChoiceGroups, elementRegistry,
|
|
2
|
-
import { useMemo, useRef, useSyncExternalStore, useEffect, useCallback, useState } from 'react';
|
|
1
|
+
import { getFiberRoot, appilotsDebugWarn, recordIntrospectionFailure, getCurrentScreen, walkFiber, getActiveRouteNames, clampSnapshotToWireLimits, deriveInteractionElements, rectForNode, toWireElement, attachElementIdsToChoiceGroups, elementRegistry, appilotsDebugLog, getLastWalkDiagnostics, SDK_VERSION, geometryStats, geometryAvailability, getNameManglingProbe, emptyDetectorCounts, getAutoTrackingState, getIntrospectionDiagnostics, getDefaultRegistry, useAppilotsContext, ChatSessionMachine, getCurrentScreenSignature, ActionQueueMachine, getScreenMetadata, setCurrentScreen, useResolvedRegistry, totalDetectorHits, listRegistry, getOpenNativeDialog, componentRegistry, getLastAnsweredNativeDialog, probeLoadingState, isListItemPressTarget, waitForLoadingSettle, describeValue, waitForScreenSettle, screenDepartedBaseline, routesBelongToSameFeature, actionPressTargetId, getNavigationStateSnapshot, getNavigationRef, isOptionalStepAutomationFailure, OPTIONAL_STEP_AUTOMATION_HINT, normalize3, matchScore, FORM_VALIDATION_TEXT_RE, idLooselyMatches, parseStableElementId, shouldSkipSubtree, isAlwaysListContainer, isHeuristicListContainer, findListItems, getTypeName, foldForCompare, isModalFiber, getAllScreens, snapshotShowsOpenCreateForm, switchValue, isReferenceable, looksDestructiveActionLabel, isGenericSelectValue, FORM_WRITE_SUCCESS_TEXT_RE, isSwitchLike, TOUCHABLE_NAMES } from './chunk-AHHDOUVX.mjs';
|
|
2
|
+
import React, { useMemo, useRef, useSyncExternalStore, useEffect, useCallback, useState } from 'react';
|
|
3
3
|
import { Alert } from 'react-native';
|
|
4
4
|
|
|
5
|
-
// src/introspection/walkFiber.ts
|
|
6
|
-
function getTypeName(type) {
|
|
7
|
-
if (!type) return null;
|
|
8
|
-
if (typeof type === "string") return type;
|
|
9
|
-
if (typeof type === "function") {
|
|
10
|
-
return type.displayName ?? type.name ?? null;
|
|
11
|
-
}
|
|
12
|
-
if (typeof type === "object") {
|
|
13
|
-
if (type.displayName) return type.displayName;
|
|
14
|
-
if (type.render) {
|
|
15
|
-
return type.render.displayName ?? type.render.name ?? null;
|
|
16
|
-
}
|
|
17
|
-
if (type.type) return getTypeName(type.type);
|
|
18
|
-
}
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
function matchesName(fiber, name) {
|
|
22
|
-
return getTypeName(fiber.type) === name || getTypeName(fiber.elementType) === name;
|
|
23
|
-
}
|
|
24
|
-
function extractTextFromChildren(children) {
|
|
25
|
-
if (children == null) return "";
|
|
26
|
-
if (typeof children === "string") return children;
|
|
27
|
-
if (typeof children === "number") return String(children);
|
|
28
|
-
if (Array.isArray(children)) {
|
|
29
|
-
return children.map((c) => extractTextFromChildren(c)).filter(Boolean).join("");
|
|
30
|
-
}
|
|
31
|
-
return "";
|
|
32
|
-
}
|
|
33
|
-
function hasMeaningfulText(text) {
|
|
34
|
-
return !!text && /[\p{L}\p{N}]/u.test(text);
|
|
35
|
-
}
|
|
36
|
-
function findChildText(fiber) {
|
|
37
|
-
const stack = [];
|
|
38
|
-
if (fiber.child) stack.push(fiber.child);
|
|
39
|
-
const visited = /* @__PURE__ */ new WeakSet();
|
|
40
|
-
const texts = [];
|
|
41
|
-
while (stack.length > 0) {
|
|
42
|
-
const node = stack.pop();
|
|
43
|
-
if (!node || visited.has(node)) continue;
|
|
44
|
-
visited.add(node);
|
|
45
|
-
if (matchesName(node, "Text")) {
|
|
46
|
-
const text = extractTextFromChildren(node.memoizedProps?.children);
|
|
47
|
-
const trimmed = text.trim();
|
|
48
|
-
if (hasMeaningfulText(trimmed)) {
|
|
49
|
-
texts.push(trimmed);
|
|
50
|
-
if (texts.length >= 3) break;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
if (node.sibling) stack.push(node.sibling);
|
|
54
|
-
if (node.child) stack.push(node.child);
|
|
55
|
-
}
|
|
56
|
-
return texts.length > 0 ? texts.join(" \xB7 ") : void 0;
|
|
57
|
-
}
|
|
58
|
-
var SELF_REFERENTIAL_COMPONENTS = /* @__PURE__ */ new Set([
|
|
59
|
-
"AppilotsChat",
|
|
60
|
-
"AppilotsChatInner",
|
|
61
|
-
"ActionBreadcrumb",
|
|
62
|
-
"ConfirmDialog"
|
|
63
|
-
]);
|
|
64
|
-
function isAppilotsSkipMarked(props) {
|
|
65
|
-
if (!props) return false;
|
|
66
|
-
return props.__appilotsSkip === true || props["data-appilots-skip"] === true || props.appilotsSkip === true;
|
|
67
|
-
}
|
|
68
|
-
function isAppilotsSensitiveMarked(props) {
|
|
69
|
-
if (!props) return false;
|
|
70
|
-
return props.__appilotsSensitive === true || props["data-appilots-sensitive"] === true || props.appilotsSensitive === true;
|
|
71
|
-
}
|
|
72
|
-
function stringProp(props, name) {
|
|
73
|
-
const value = props?.[name];
|
|
74
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
75
|
-
}
|
|
76
|
-
function numberProp(props, name) {
|
|
77
|
-
const value = props?.[name];
|
|
78
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
79
|
-
}
|
|
80
|
-
function shouldSkipSubtree(fiber) {
|
|
81
|
-
const props = fiber.memoizedProps;
|
|
82
|
-
if (!props) return false;
|
|
83
|
-
const typeName = getTypeName(fiber.type) ?? getTypeName(fiber.elementType);
|
|
84
|
-
if (typeName && SELF_REFERENTIAL_COMPONENTS.has(typeName)) return true;
|
|
85
|
-
if (isAppilotsSkipMarked(props)) return true;
|
|
86
|
-
if (matchesName(fiber, "Modal") && props.visible === false) return true;
|
|
87
|
-
if (matchesName(fiber, "Screen") && props.activityState === 0) return true;
|
|
88
|
-
const style = props.style;
|
|
89
|
-
if (style) {
|
|
90
|
-
const flat = Array.isArray(style) ? Object.assign({}, ...style.filter(Boolean)) : style;
|
|
91
|
-
if (flat?.display === "none") return true;
|
|
92
|
-
if (flat?.opacity === 0 && props.pointerEvents === "none") return true;
|
|
93
|
-
}
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
function inferInputType(props) {
|
|
97
|
-
if (props.secureTextEntry || isAppilotsSensitiveMarked(props)) return "password";
|
|
98
|
-
switch (props.keyboardType) {
|
|
99
|
-
case "email-address":
|
|
100
|
-
return "email";
|
|
101
|
-
case "numeric":
|
|
102
|
-
case "number-pad":
|
|
103
|
-
case "decimal-pad":
|
|
104
|
-
return "number";
|
|
105
|
-
case "phone-pad":
|
|
106
|
-
return "phone";
|
|
107
|
-
default:
|
|
108
|
-
return "text";
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
function inferLabel(props) {
|
|
112
|
-
return props.accessibilityLabel ?? props.label ?? props.placeholder ?? props.testID;
|
|
113
|
-
}
|
|
114
|
-
function bestId(props) {
|
|
115
|
-
return props.testID ?? props.accessibilityLabel ?? void 0;
|
|
116
|
-
}
|
|
117
|
-
var ALWAYS_LIST_NAMES = /* @__PURE__ */ new Set([
|
|
118
|
-
"FlatList",
|
|
119
|
-
"SectionList",
|
|
120
|
-
"VirtualizedList",
|
|
121
|
-
// Shopify's high-perf FlatList replacement — same usage shape, same
|
|
122
|
-
// virtualization, same per-item key.
|
|
123
|
-
"FlashList"
|
|
124
|
-
]);
|
|
125
|
-
var HEURISTIC_LIST_NAMES = /* @__PURE__ */ new Set(["ScrollView"]);
|
|
126
|
-
var LIST_CELL_NAMES = /* @__PURE__ */ new Set([
|
|
127
|
-
"CellRenderer",
|
|
128
|
-
"CellRendererComponent",
|
|
129
|
-
"VirtualizedListCellContextProvider"
|
|
130
|
-
]);
|
|
131
|
-
function isAlwaysListContainer(fiber) {
|
|
132
|
-
for (const name of ALWAYS_LIST_NAMES) {
|
|
133
|
-
if (matchesName(fiber, name)) return true;
|
|
134
|
-
}
|
|
135
|
-
return false;
|
|
136
|
-
}
|
|
137
|
-
function isHeuristicListContainer(fiber) {
|
|
138
|
-
for (const name of HEURISTIC_LIST_NAMES) {
|
|
139
|
-
if (matchesName(fiber, name)) return true;
|
|
140
|
-
}
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
function isKnownListCell(fiber) {
|
|
144
|
-
const typeName = getTypeName(fiber.type) ?? getTypeName(fiber.elementType);
|
|
145
|
-
return !!typeName && LIST_CELL_NAMES.has(typeName);
|
|
146
|
-
}
|
|
147
|
-
function isModalFiber(fiber) {
|
|
148
|
-
return matchesName(fiber, "Modal");
|
|
149
|
-
}
|
|
150
|
-
function isVisibleModalFiber(fiber) {
|
|
151
|
-
return isModalFiber(fiber) && fiber?.memoizedProps?.visible !== false;
|
|
152
|
-
}
|
|
153
|
-
function collectVisibleModalFibers(container, excludeRoots = []) {
|
|
154
|
-
const excluded = /* @__PURE__ */ new WeakSet();
|
|
155
|
-
for (const rootFiber of excludeRoots) {
|
|
156
|
-
if (rootFiber) excluded.add(rootFiber);
|
|
157
|
-
}
|
|
158
|
-
const found = [];
|
|
159
|
-
const visited = /* @__PURE__ */ new WeakSet();
|
|
160
|
-
const stack = [];
|
|
161
|
-
if (container?.child) stack.push(container.child);
|
|
162
|
-
while (stack.length > 0) {
|
|
163
|
-
const node = stack.pop();
|
|
164
|
-
if (!node || visited.has(node)) continue;
|
|
165
|
-
visited.add(node);
|
|
166
|
-
if (node.sibling) stack.push(node.sibling);
|
|
167
|
-
if (excluded.has(node) || shouldSkipSubtree(node)) continue;
|
|
168
|
-
if (isVisibleModalFiber(node)) {
|
|
169
|
-
found.push(node);
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
if (node.child) stack.push(node.child);
|
|
173
|
-
}
|
|
174
|
-
return found;
|
|
175
|
-
}
|
|
176
|
-
function isMarkedListItem(fiber, listId) {
|
|
177
|
-
const props = fiber?.memoizedProps;
|
|
178
|
-
if (!props || props.__appilotsListItem !== true) return false;
|
|
179
|
-
if (!listId) return true;
|
|
180
|
-
return props.__appilotsListId === listId;
|
|
181
|
-
}
|
|
182
|
-
function listIdFromProps(props) {
|
|
183
|
-
return stringProp(props, "__appilotsListId") ?? stringProp(props, "appilotsListId");
|
|
184
|
-
}
|
|
185
|
-
function listMetadataFromProps(props) {
|
|
186
|
-
const id = listIdFromProps(props);
|
|
187
|
-
const itemCount = numberProp(props, "__appilotsItemCount");
|
|
188
|
-
const label = stringProp(props, "__appilotsListLabel") ?? stringProp(props, "accessibilityLabel") ?? stringProp(props, "testID");
|
|
189
|
-
const refreshing = props?.refreshing === true || props?.__appilotsRefreshing === true;
|
|
190
|
-
const empty = props?.__appilotsEmpty === true || typeof itemCount === "number" && itemCount === 0;
|
|
191
|
-
return {
|
|
192
|
-
id,
|
|
193
|
-
itemCount,
|
|
194
|
-
refreshing,
|
|
195
|
-
empty,
|
|
196
|
-
label,
|
|
197
|
-
source: id ? "auto-tracked" : void 0
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
function hasRenderableContent(fiber) {
|
|
201
|
-
const stack = [fiber];
|
|
202
|
-
const visited = /* @__PURE__ */ new WeakSet();
|
|
203
|
-
while (stack.length > 0) {
|
|
204
|
-
const node = stack.pop();
|
|
205
|
-
if (!node || visited.has(node)) continue;
|
|
206
|
-
visited.add(node);
|
|
207
|
-
if (shouldSkipSubtree(node) || isModalFiber(node)) continue;
|
|
208
|
-
const props = node.memoizedProps ?? {};
|
|
209
|
-
if (matchesName(node, "Text")) {
|
|
210
|
-
const text = extractTextFromChildren(props.children);
|
|
211
|
-
if (text && text.trim().length > 0) return true;
|
|
212
|
-
}
|
|
213
|
-
if (matchesName(node, "TextInput") || matchesName(node, "Switch")) {
|
|
214
|
-
return true;
|
|
215
|
-
}
|
|
216
|
-
if (TOUCHABLE_NAMES.has(getTypeName(node.type) ?? "") || TOUCHABLE_NAMES.has(getTypeName(node.elementType) ?? "")) {
|
|
217
|
-
return true;
|
|
218
|
-
}
|
|
219
|
-
if (node.sibling) stack.push(node.sibling);
|
|
220
|
-
if (node.child) stack.push(node.child);
|
|
221
|
-
}
|
|
222
|
-
return false;
|
|
223
|
-
}
|
|
224
|
-
function findListCellsByType(container, minItems) {
|
|
225
|
-
const cells = [];
|
|
226
|
-
const visited = /* @__PURE__ */ new WeakSet();
|
|
227
|
-
const stack = [];
|
|
228
|
-
if (container?.child) stack.push(container.child);
|
|
229
|
-
while (stack.length > 0) {
|
|
230
|
-
const node = stack.pop();
|
|
231
|
-
if (!node || visited.has(node)) continue;
|
|
232
|
-
visited.add(node);
|
|
233
|
-
if (node.sibling) stack.push(node.sibling);
|
|
234
|
-
if (shouldSkipSubtree(node) || isModalFiber(node)) continue;
|
|
235
|
-
if (isKnownListCell(node) && hasRenderableContent(node)) {
|
|
236
|
-
cells.push(node);
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
if (node.child) stack.push(node.child);
|
|
240
|
-
}
|
|
241
|
-
return cells.length >= minItems ? cells : null;
|
|
242
|
-
}
|
|
243
|
-
function findMarkedListItems(container, minItems, listId) {
|
|
244
|
-
const items = [];
|
|
245
|
-
const visited = /* @__PURE__ */ new WeakSet();
|
|
246
|
-
const stack = [];
|
|
247
|
-
if (container?.child) stack.push(container.child);
|
|
248
|
-
while (stack.length > 0) {
|
|
249
|
-
const node = stack.pop();
|
|
250
|
-
if (!node || visited.has(node)) continue;
|
|
251
|
-
visited.add(node);
|
|
252
|
-
if (node.sibling) stack.push(node.sibling);
|
|
253
|
-
if (shouldSkipSubtree(node) || isModalFiber(node)) continue;
|
|
254
|
-
if (isMarkedListItem(node, listId)) {
|
|
255
|
-
items.push(node);
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
if (node.child) stack.push(node.child);
|
|
259
|
-
}
|
|
260
|
-
if (items.length < minItems) return null;
|
|
261
|
-
items.sort((a, b) => {
|
|
262
|
-
const ai = numberProp(a.memoizedProps, "__appilotsItemIndex");
|
|
263
|
-
const bi = numberProp(b.memoizedProps, "__appilotsItemIndex");
|
|
264
|
-
if (ai === void 0 && bi === void 0) return 0;
|
|
265
|
-
if (ai === void 0) return 1;
|
|
266
|
-
if (bi === void 0) return -1;
|
|
267
|
-
return ai - bi;
|
|
268
|
-
});
|
|
269
|
-
return items;
|
|
270
|
-
}
|
|
271
|
-
function hasAuthoredReactKey(fiber) {
|
|
272
|
-
const key = fiber?.key;
|
|
273
|
-
if (key === null || key === void 0 || key === "") return false;
|
|
274
|
-
const s = String(key);
|
|
275
|
-
if (s.startsWith(".") && !s.startsWith(".$")) return false;
|
|
276
|
-
return true;
|
|
277
|
-
}
|
|
278
|
-
function findKeyedSiblingItems(container, minItems) {
|
|
279
|
-
if (!container) return null;
|
|
280
|
-
let current = container;
|
|
281
|
-
const visited = /* @__PURE__ */ new WeakSet();
|
|
282
|
-
while (current && !visited.has(current)) {
|
|
283
|
-
visited.add(current);
|
|
284
|
-
const siblings = [];
|
|
285
|
-
let cur = current.child;
|
|
286
|
-
while (cur) {
|
|
287
|
-
if (!isModalFiber(cur)) siblings.push(cur);
|
|
288
|
-
cur = cur.sibling;
|
|
289
|
-
}
|
|
290
|
-
if (siblings.length === 0) return null;
|
|
291
|
-
const keyed = siblings.filter(hasAuthoredReactKey);
|
|
292
|
-
if (keyed.length >= minItems) {
|
|
293
|
-
return keyed;
|
|
294
|
-
}
|
|
295
|
-
if (siblings.length > 1) {
|
|
296
|
-
return null;
|
|
297
|
-
}
|
|
298
|
-
current = siblings[0];
|
|
299
|
-
}
|
|
300
|
-
return null;
|
|
301
|
-
}
|
|
302
|
-
function findListItems(container, minItems = 2, preferCells = false) {
|
|
303
|
-
if (!container) return null;
|
|
304
|
-
const listId = listIdFromProps(container.memoizedProps);
|
|
305
|
-
const markedItems = findMarkedListItems(container, minItems, listId);
|
|
306
|
-
if (markedItems) return markedItems;
|
|
307
|
-
if (preferCells) {
|
|
308
|
-
const cells = findListCellsByType(container, minItems);
|
|
309
|
-
if (cells) return cells;
|
|
310
|
-
}
|
|
311
|
-
if (minItems <= 1) {
|
|
312
|
-
const multiItemLevel = findKeyedSiblingItems(container, 2);
|
|
313
|
-
if (multiItemLevel) return multiItemLevel;
|
|
314
|
-
}
|
|
315
|
-
return findKeyedSiblingItems(container, minItems) ?? findListCellsByType(container, minItems);
|
|
316
|
-
}
|
|
317
|
-
var TOUCHABLE_NAMES = /* @__PURE__ */ new Set([
|
|
318
|
-
"TouchableOpacity",
|
|
319
|
-
"TouchableHighlight",
|
|
320
|
-
"TouchableWithoutFeedback",
|
|
321
|
-
"TouchableNativeFeedback",
|
|
322
|
-
"Pressable"
|
|
323
|
-
]);
|
|
324
|
-
function hasPressHandler(fiber) {
|
|
325
|
-
const props = fiber?.memoizedProps ?? {};
|
|
326
|
-
return typeof props.onPress === "function" || typeof props.onPressIn === "function";
|
|
327
|
-
}
|
|
328
|
-
function isPressableTarget(fiber) {
|
|
329
|
-
return TOUCHABLE_NAMES.has(getTypeName(fiber.type) ?? "") || TOUCHABLE_NAMES.has(getTypeName(fiber.elementType) ?? "") || hasPressHandler(fiber);
|
|
330
|
-
}
|
|
331
|
-
function walkSubtree(root, detectLists, startInModal = false) {
|
|
332
|
-
const result = {
|
|
333
|
-
texts: [],
|
|
334
|
-
inputs: [],
|
|
335
|
-
buttons: [],
|
|
336
|
-
toggles: [],
|
|
337
|
-
sliders: [],
|
|
338
|
-
visitedFibers: 0,
|
|
339
|
-
skippedHidden: 0,
|
|
340
|
-
loading: false,
|
|
341
|
-
modalOpen: false,
|
|
342
|
-
lists: []
|
|
343
|
-
};
|
|
344
|
-
if (!root) return result;
|
|
345
|
-
const visited = /* @__PURE__ */ new WeakSet();
|
|
346
|
-
const stack = [];
|
|
347
|
-
if (root.child) stack.push({ fiber: root.child, inModal: startInModal });
|
|
348
|
-
while (stack.length > 0) {
|
|
349
|
-
const { fiber, inModal } = stack.pop();
|
|
350
|
-
if (!fiber || visited.has(fiber)) continue;
|
|
351
|
-
visited.add(fiber);
|
|
352
|
-
result.visitedFibers++;
|
|
353
|
-
if (fiber.sibling) stack.push({ fiber: fiber.sibling, inModal });
|
|
354
|
-
if (shouldSkipSubtree(fiber)) {
|
|
355
|
-
result.skippedHidden++;
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
358
|
-
if (detectLists) {
|
|
359
|
-
const isAlways = isAlwaysListContainer(fiber);
|
|
360
|
-
const isHeuristic = !isAlways && isHeuristicListContainer(fiber);
|
|
361
|
-
if (isAlways || isHeuristic) {
|
|
362
|
-
const minItems = isAlways ? 1 : 2;
|
|
363
|
-
const items = findListItems(fiber, minItems, isAlways);
|
|
364
|
-
if (items && items.length >= minItems) {
|
|
365
|
-
const containerType = getTypeName(fiber.type) ?? getTypeName(fiber.elementType) ?? "List";
|
|
366
|
-
const listIndex = result.lists.length;
|
|
367
|
-
const itemSnapshots = items.map((itemFiber, i) => {
|
|
368
|
-
const itemResult = walkSubtree(itemFiber, false, inModal);
|
|
369
|
-
result.visitedFibers += itemResult.visitedFibers;
|
|
370
|
-
result.skippedHidden += itemResult.skippedHidden;
|
|
371
|
-
if (itemResult.loading) result.loading = true;
|
|
372
|
-
if (itemResult.modalOpen) result.modalOpen = true;
|
|
373
|
-
const reactKey = typeof itemFiber.key === "string" || typeof itemFiber.key === "number" ? String(itemFiber.key) : typeof itemFiber.memoizedProps?.__appilotsItemKey === "string" || typeof itemFiber.memoizedProps?.__appilotsItemKey === "number" ? String(itemFiber.memoizedProps.__appilotsItemKey) : void 0;
|
|
374
|
-
const itemKey = typeof itemFiber.memoizedProps?.__appilotsItemKey === "string" || typeof itemFiber.memoizedProps?.__appilotsItemKey === "number" ? String(itemFiber.memoizedProps.__appilotsItemKey) : void 0;
|
|
375
|
-
const dataIndex = numberProp(itemFiber.memoizedProps, "__appilotsItemIndex");
|
|
376
|
-
return {
|
|
377
|
-
index: i + 1,
|
|
378
|
-
dataIndex,
|
|
379
|
-
reactKey,
|
|
380
|
-
itemKey,
|
|
381
|
-
syntheticId: `list-${listIndex}-item-${i + 1}`,
|
|
382
|
-
texts: dedupeStringsInternal(itemResult.texts),
|
|
383
|
-
buttons: dedupeButtonsInternal(itemResult.buttons),
|
|
384
|
-
inputs: dedupeInputsInternal(itemResult.inputs),
|
|
385
|
-
toggles: dedupeTogglesInternal(itemResult.toggles)
|
|
386
|
-
};
|
|
387
|
-
});
|
|
388
|
-
const meta2 = listMetadataFromProps(fiber.memoizedProps);
|
|
389
|
-
result.lists.push({
|
|
390
|
-
index: listIndex,
|
|
391
|
-
id: meta2.id,
|
|
392
|
-
containerType,
|
|
393
|
-
source: meta2.source ?? "fiber",
|
|
394
|
-
itemCount: meta2.itemCount,
|
|
395
|
-
visibleItemCount: itemSnapshots.length,
|
|
396
|
-
refreshing: meta2.refreshing,
|
|
397
|
-
empty: meta2.empty,
|
|
398
|
-
label: meta2.label,
|
|
399
|
-
items: itemSnapshots
|
|
400
|
-
});
|
|
401
|
-
for (const modalFiber of collectVisibleModalFibers(fiber, items)) {
|
|
402
|
-
result.modalOpen = true;
|
|
403
|
-
if (modalFiber.child) stack.push({ fiber: modalFiber.child, inModal: true });
|
|
404
|
-
}
|
|
405
|
-
continue;
|
|
406
|
-
}
|
|
407
|
-
const meta = listMetadataFromProps(fiber.memoizedProps);
|
|
408
|
-
if (meta.id || meta.itemCount !== void 0 || meta.refreshing || meta.empty) {
|
|
409
|
-
const containerType = getTypeName(fiber.type) ?? getTypeName(fiber.elementType) ?? "List";
|
|
410
|
-
result.lists.push({
|
|
411
|
-
index: result.lists.length,
|
|
412
|
-
id: meta.id,
|
|
413
|
-
containerType,
|
|
414
|
-
source: meta.source ?? "auto-tracked",
|
|
415
|
-
itemCount: meta.itemCount,
|
|
416
|
-
visibleItemCount: 0,
|
|
417
|
-
refreshing: meta.refreshing,
|
|
418
|
-
empty: meta.empty,
|
|
419
|
-
label: meta.label,
|
|
420
|
-
items: []
|
|
421
|
-
});
|
|
422
|
-
for (const modalFiber of collectVisibleModalFibers(fiber)) {
|
|
423
|
-
result.modalOpen = true;
|
|
424
|
-
if (modalFiber.child) stack.push({ fiber: modalFiber.child, inModal: true });
|
|
425
|
-
}
|
|
426
|
-
continue;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
const props = fiber.memoizedProps ?? {};
|
|
431
|
-
if (matchesName(fiber, "Text")) {
|
|
432
|
-
const text = extractTextFromChildren(props.children);
|
|
433
|
-
if (text && text.trim().length > 0) {
|
|
434
|
-
result.texts.push(text.trim());
|
|
435
|
-
}
|
|
436
|
-
} else if (matchesName(fiber, "TextInput")) {
|
|
437
|
-
if (props.__appilotsInternal !== true) {
|
|
438
|
-
const isSecure = !!props.secureTextEntry || isAppilotsSensitiveMarked(props);
|
|
439
|
-
const rawValue = typeof props.value === "string" ? props.value : void 0;
|
|
440
|
-
const safeValue = isSecure ? rawValue && rawValue.length > 0 ? "<hidden>" : void 0 : rawValue;
|
|
441
|
-
result.inputs.push({
|
|
442
|
-
id: bestId(props),
|
|
443
|
-
label: inferLabel(props),
|
|
444
|
-
value: safeValue,
|
|
445
|
-
placeholder: props.placeholder,
|
|
446
|
-
editable: props.editable !== false,
|
|
447
|
-
secure: isSecure,
|
|
448
|
-
type: inferInputType(props),
|
|
449
|
-
...inModal ? { inModal: true } : {}
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
} else if (props.accessibilityRole === "adjustable") {
|
|
453
|
-
if (props.__appilotsInternal !== true) {
|
|
454
|
-
const av = props.accessibilityValue ?? {};
|
|
455
|
-
result.sliders.push({
|
|
456
|
-
id: bestId(props),
|
|
457
|
-
label: props.accessibilityLabel ?? props.testID,
|
|
458
|
-
value: typeof av.now === "number" ? av.now : void 0,
|
|
459
|
-
min: typeof av.min === "number" ? av.min : void 0,
|
|
460
|
-
max: typeof av.max === "number" ? av.max : void 0,
|
|
461
|
-
...inModal ? { inModal: true } : {}
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
} else if (isPressableTarget(fiber)) {
|
|
465
|
-
if (props.__appilotsInternal !== true) {
|
|
466
|
-
const label = findChildText(fiber);
|
|
467
|
-
const selected = props.accessibilityState?.selected === true;
|
|
468
|
-
result.buttons.push({
|
|
469
|
-
id: bestId(props) ?? (label ? normalizeLabel(label) : void 0),
|
|
470
|
-
label,
|
|
471
|
-
disabled: !!props.disabled,
|
|
472
|
-
...selected ? { selected: true } : {},
|
|
473
|
-
...inModal ? { inModal: true } : {}
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
} else if (matchesName(fiber, "Switch")) {
|
|
477
|
-
result.toggles.push({
|
|
478
|
-
id: bestId(props),
|
|
479
|
-
label: props.accessibilityLabel ?? props.testID,
|
|
480
|
-
value: !!props.value,
|
|
481
|
-
...inModal ? { inModal: true } : {}
|
|
482
|
-
});
|
|
483
|
-
} else if (matchesName(fiber, "ActivityIndicator")) {
|
|
484
|
-
result.loading = true;
|
|
485
|
-
} else if (matchesName(fiber, "Modal")) {
|
|
486
|
-
result.modalOpen = true;
|
|
487
|
-
if (fiber.child) stack.push({ fiber: fiber.child, inModal: true });
|
|
488
|
-
continue;
|
|
489
|
-
}
|
|
490
|
-
if (fiber.child) stack.push({ fiber: fiber.child, inModal });
|
|
491
|
-
}
|
|
492
|
-
return result;
|
|
493
|
-
}
|
|
494
|
-
function walkFiber(root) {
|
|
495
|
-
const snapshot = {
|
|
496
|
-
route: getCurrentScreen(),
|
|
497
|
-
texts: [],
|
|
498
|
-
inputs: [],
|
|
499
|
-
buttons: [],
|
|
500
|
-
toggles: [],
|
|
501
|
-
// Sliders come from the ComponentRegistry (useAppilotsSlider) —
|
|
502
|
-
// captureSnapshot merges them so observation only ever advertises
|
|
503
|
-
// sliders the executor can actually set (min/max/step + setValue).
|
|
504
|
-
sliders: [],
|
|
505
|
-
loading: false,
|
|
506
|
-
modalOpen: false,
|
|
507
|
-
lists: [],
|
|
508
|
-
choiceGroups: [],
|
|
509
|
-
elements: [],
|
|
510
|
-
stats: { visitedFibers: 0, skippedHidden: 0 }
|
|
511
|
-
};
|
|
512
|
-
if (!root) return snapshot;
|
|
513
|
-
const sub = walkSubtree(root, true);
|
|
514
|
-
snapshot.texts = dedupeStringsInternal(sub.texts);
|
|
515
|
-
snapshot.inputs = dedupeInputsInternal(sub.inputs);
|
|
516
|
-
snapshot.buttons = dedupeButtonsInternal(sub.buttons);
|
|
517
|
-
snapshot.toggles = dedupeTogglesInternal(sub.toggles);
|
|
518
|
-
snapshot.sliders = dedupeSliders(sub.sliders);
|
|
519
|
-
snapshot.loading = sub.loading;
|
|
520
|
-
snapshot.modalOpen = sub.modalOpen;
|
|
521
|
-
snapshot.lists = sub.lists;
|
|
522
|
-
snapshot.choiceGroups = deriveChoiceGroups(snapshot);
|
|
523
|
-
snapshot.stats.visitedFibers = sub.visitedFibers;
|
|
524
|
-
snapshot.stats.skippedHidden = sub.skippedHidden;
|
|
525
|
-
return snapshot;
|
|
526
|
-
}
|
|
527
|
-
function dedupeButtonsInternal(buttons) {
|
|
528
|
-
return dedupeButtons(buttons);
|
|
529
|
-
}
|
|
530
|
-
function dedupeInputsInternal(inputs) {
|
|
531
|
-
return dedupeInputs(inputs);
|
|
532
|
-
}
|
|
533
|
-
function dedupeTogglesInternal(toggles) {
|
|
534
|
-
return dedupeToggles(toggles);
|
|
535
|
-
}
|
|
536
|
-
function dedupeStringsInternal(strings) {
|
|
537
|
-
return dedupeStrings(strings);
|
|
538
|
-
}
|
|
539
|
-
function isCommandLikeLabel(label) {
|
|
540
|
-
if (!label) return true;
|
|
541
|
-
const normalized = normalizeLabel(label);
|
|
542
|
-
if (!normalized) return true;
|
|
543
|
-
return /^(proximo|prox|next|continuar|continue|voltar|back|confirmar|confirm|cancelar|cancel|salvar|save|enviar|submit|ok|limpar|clear|fechar|close|adicionar|add|novo|nova|new)$/i.test(
|
|
544
|
-
normalized
|
|
545
|
-
);
|
|
546
|
-
}
|
|
547
|
-
function deriveChoiceGroups(snapshot) {
|
|
548
|
-
const groups = [];
|
|
549
|
-
for (const list of snapshot.lists) {
|
|
550
|
-
const options = list.items.filter((item) => item.texts.length > 0 || item.buttons.length > 0).map((item, i) => {
|
|
551
|
-
const texts = item.texts.length > 0 ? item.texts : item.buttons.map((b) => b.label ?? b.id ?? "").filter((text) => text.length > 0);
|
|
552
|
-
return {
|
|
553
|
-
index: i + 1,
|
|
554
|
-
syntheticId: `${list.id ?? `list-${list.index}`}-choice-${i + 1}`,
|
|
555
|
-
targetId: item.syntheticId,
|
|
556
|
-
label: texts.slice(0, 2).join(" \xB7 "),
|
|
557
|
-
texts,
|
|
558
|
-
disabled: false
|
|
559
|
-
};
|
|
560
|
-
});
|
|
561
|
-
if (options.length > 0) {
|
|
562
|
-
groups.push({
|
|
563
|
-
index: groups.length,
|
|
564
|
-
id: list.id,
|
|
565
|
-
label: list.label,
|
|
566
|
-
source: "list",
|
|
567
|
-
options
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
const buttonOptions = snapshot.buttons.filter((button) => !button.disabled).filter((button) => hasMeaningfulText(button.label ?? button.id)).filter((button) => !isCommandLikeLabel(button.label ?? button.id)).map((button, i) => {
|
|
572
|
-
const label = button.label ?? button.id ?? "";
|
|
573
|
-
return {
|
|
574
|
-
index: i + 1,
|
|
575
|
-
syntheticId: `choice-buttons-option-${i + 1}`,
|
|
576
|
-
targetId: button.id ?? button.label,
|
|
577
|
-
label,
|
|
578
|
-
texts: [label],
|
|
579
|
-
selected: !!button.selected,
|
|
580
|
-
disabled: !!button.disabled
|
|
581
|
-
};
|
|
582
|
-
});
|
|
583
|
-
if (buttonOptions.length >= 2) {
|
|
584
|
-
groups.push({
|
|
585
|
-
index: groups.length,
|
|
586
|
-
id: "visible-options",
|
|
587
|
-
label: "Visible options",
|
|
588
|
-
source: "buttons",
|
|
589
|
-
options: buttonOptions
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
return groups;
|
|
593
|
-
}
|
|
594
|
-
function dedupeButtons(buttons) {
|
|
595
|
-
const seen = /* @__PURE__ */ new Set();
|
|
596
|
-
const out = [];
|
|
597
|
-
for (const b of buttons) {
|
|
598
|
-
const idTrim = b.id?.trim();
|
|
599
|
-
const labelTrim = b.label?.trim();
|
|
600
|
-
if (!idTrim && (!labelTrim || !/[a-z0-9]/i.test(labelTrim))) continue;
|
|
601
|
-
const key = `${idTrim ?? ""}|${labelTrim ?? ""}|${b.disabled ? 1 : 0}|${b.inModal ? 1 : 0}|${b.selected ? 1 : 0}`;
|
|
602
|
-
if (seen.has(key)) continue;
|
|
603
|
-
seen.add(key);
|
|
604
|
-
out.push(b);
|
|
605
|
-
}
|
|
606
|
-
return out;
|
|
607
|
-
}
|
|
608
|
-
function dedupeInputs(inputs) {
|
|
609
|
-
const seen = /* @__PURE__ */ new Set();
|
|
610
|
-
const out = [];
|
|
611
|
-
for (const i of inputs) {
|
|
612
|
-
const key = `${i.id ?? ""}|${i.label ?? ""}|${i.placeholder ?? ""}|${i.inModal ? 1 : 0}`;
|
|
613
|
-
if (seen.has(key)) continue;
|
|
614
|
-
seen.add(key);
|
|
615
|
-
out.push(i);
|
|
616
|
-
}
|
|
617
|
-
return out;
|
|
618
|
-
}
|
|
619
|
-
function dedupeSliders(sliders) {
|
|
620
|
-
const seen = /* @__PURE__ */ new Set();
|
|
621
|
-
const out = [];
|
|
622
|
-
for (const s of sliders) {
|
|
623
|
-
const key = `${s.id ?? ""}|${s.label ?? ""}|${s.inModal ? 1 : 0}`;
|
|
624
|
-
if (seen.has(key)) continue;
|
|
625
|
-
seen.add(key);
|
|
626
|
-
out.push(s);
|
|
627
|
-
}
|
|
628
|
-
return out;
|
|
629
|
-
}
|
|
630
|
-
function dedupeToggles(toggles) {
|
|
631
|
-
const seen = /* @__PURE__ */ new Set();
|
|
632
|
-
const out = [];
|
|
633
|
-
for (const t of toggles) {
|
|
634
|
-
const key = `${t.id ?? ""}|${t.label ?? ""}|${t.inModal ? 1 : 0}`;
|
|
635
|
-
if (seen.has(key)) continue;
|
|
636
|
-
seen.add(key);
|
|
637
|
-
out.push(t);
|
|
638
|
-
}
|
|
639
|
-
return out;
|
|
640
|
-
}
|
|
641
|
-
function dedupeStrings(strings) {
|
|
642
|
-
const seen = /* @__PURE__ */ new Set();
|
|
643
|
-
const out = [];
|
|
644
|
-
for (const s of strings) {
|
|
645
|
-
const trimmed = s.trim();
|
|
646
|
-
if (!trimmed) continue;
|
|
647
|
-
if (seen.has(trimmed)) continue;
|
|
648
|
-
seen.add(trimmed);
|
|
649
|
-
out.push(trimmed);
|
|
650
|
-
}
|
|
651
|
-
return out;
|
|
652
|
-
}
|
|
653
|
-
function normalizeLabel(label) {
|
|
654
|
-
return label.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9\s]/g, "").trim().replace(/\s+(.)/g, (_, c) => c.toUpperCase()).replace(/\s+/g, "");
|
|
655
|
-
}
|
|
656
|
-
|
|
657
5
|
// src/introspection/captureSnapshot.ts
|
|
658
6
|
function normalizeSliderKey(value) {
|
|
659
7
|
return (value ?? "").toLowerCase().replace(/^slider[-_]/, "");
|
|
@@ -694,6 +42,29 @@ function reconcileSliders(snapshot) {
|
|
|
694
42
|
}
|
|
695
43
|
snapshot.sliders = out;
|
|
696
44
|
}
|
|
45
|
+
function normalizeFieldKey(value) {
|
|
46
|
+
return (value ?? "").toLowerCase().replace(/^(input|field)[-_]/, "");
|
|
47
|
+
}
|
|
48
|
+
function canonicalizeRegisteredFields(snapshot) {
|
|
49
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
50
|
+
for (const { id, kind, label } of componentRegistry.snapshot()) {
|
|
51
|
+
if (kind !== "field") continue;
|
|
52
|
+
if (snapshot.inputs.some((input) => input.id === id)) continue;
|
|
53
|
+
const registryLabel = (label ?? "").toLowerCase();
|
|
54
|
+
const registryKey = normalizeFieldKey(id);
|
|
55
|
+
const candidates = snapshot.inputs.filter((input) => {
|
|
56
|
+
if (claimed.has(input)) return false;
|
|
57
|
+
const inputLabel = (input.label ?? "").toLowerCase();
|
|
58
|
+
if (registryLabel && inputLabel && inputLabel === registryLabel) return true;
|
|
59
|
+
const inputKey = normalizeFieldKey(input.id);
|
|
60
|
+
return !!inputKey && !!registryKey && inputKey === registryKey;
|
|
61
|
+
});
|
|
62
|
+
if (candidates.length !== 1) continue;
|
|
63
|
+
const match = candidates[0];
|
|
64
|
+
claimed.add(match);
|
|
65
|
+
match.id = id;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
697
68
|
function mergeRegisteredLists(snapshot) {
|
|
698
69
|
const registered = listRegistry.snapshot();
|
|
699
70
|
if (registered.length === 0) return;
|
|
@@ -702,6 +73,7 @@ function mergeRegisteredLists(snapshot) {
|
|
|
702
73
|
if (list.id) byId.set(list.id, list);
|
|
703
74
|
}
|
|
704
75
|
for (const entry of registered) {
|
|
76
|
+
if (entry.kind === "scroll") continue;
|
|
705
77
|
const metrics = entry.getScrollMetrics?.();
|
|
706
78
|
const scrollFacts = metrics ? {
|
|
707
79
|
scrollOffsetY: Math.max(0, Math.round(metrics.offset)),
|
|
@@ -742,11 +114,66 @@ function mergeRegisteredLists(snapshot) {
|
|
|
742
114
|
});
|
|
743
115
|
}
|
|
744
116
|
}
|
|
117
|
+
function mergeRegisteredScrollables(snapshot) {
|
|
118
|
+
const registered = listRegistry.snapshot().filter((e) => e.kind === "scroll");
|
|
119
|
+
if (registered.length === 0) return;
|
|
120
|
+
const listsById = /* @__PURE__ */ new Map();
|
|
121
|
+
for (const list of snapshot.lists) {
|
|
122
|
+
if (list.id) listsById.set(list.id, list);
|
|
123
|
+
}
|
|
124
|
+
const out = [];
|
|
125
|
+
for (const entry of registered) {
|
|
126
|
+
const metrics = entry.getScrollMetrics?.();
|
|
127
|
+
const facts = metrics ? {
|
|
128
|
+
scrollOffsetY: Math.max(0, Math.round(metrics.offset)),
|
|
129
|
+
canScrollUp: metrics.offset > 8,
|
|
130
|
+
canScrollDown: metrics.offset + metrics.visibleLength < metrics.contentLength - 8
|
|
131
|
+
} : void 0;
|
|
132
|
+
const asList = listsById.get(entry.id);
|
|
133
|
+
if (asList) {
|
|
134
|
+
if (facts) {
|
|
135
|
+
asList.scrollOffsetY = asList.scrollOffsetY ?? facts.scrollOffsetY;
|
|
136
|
+
asList.canScrollUp = asList.canScrollUp ?? facts.canScrollUp;
|
|
137
|
+
asList.canScrollDown = asList.canScrollDown ?? facts.canScrollDown;
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
out.push({
|
|
142
|
+
id: entry.id,
|
|
143
|
+
containerType: entry.component,
|
|
144
|
+
...entry.label ? { label: entry.label } : {},
|
|
145
|
+
...facts ?? {}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (out.length > 0) snapshot.scrollables = out;
|
|
149
|
+
}
|
|
150
|
+
function mergeNativeDialog(snapshot) {
|
|
151
|
+
const dialog = getOpenNativeDialog();
|
|
152
|
+
if (!dialog) return;
|
|
153
|
+
snapshot.nativeDialog = {
|
|
154
|
+
id: dialog.id,
|
|
155
|
+
...dialog.title ? { title: dialog.title } : {},
|
|
156
|
+
...dialog.message ? { message: dialog.message } : {},
|
|
157
|
+
buttons: dialog.buttons.map((b) => ({
|
|
158
|
+
label: b.label,
|
|
159
|
+
...b.style ? { style: b.style } : {}
|
|
160
|
+
}))
|
|
161
|
+
};
|
|
162
|
+
snapshot.modalOpen = true;
|
|
163
|
+
}
|
|
164
|
+
var MIN_FIBERS_FOR_BLINDNESS = 200;
|
|
165
|
+
function recordBlindWalk() {
|
|
166
|
+
const walk = getLastWalkDiagnostics();
|
|
167
|
+
if (!walk) return;
|
|
168
|
+
if (walk.visitedFibers < MIN_FIBERS_FOR_BLINDNESS) return;
|
|
169
|
+
if (totalDetectorHits(walk.detectors) > 0) return;
|
|
170
|
+
recordIntrospectionFailure("walk-recognized-nothing", null);
|
|
171
|
+
}
|
|
745
172
|
function captureSnapshot() {
|
|
746
173
|
const root = getFiberRoot();
|
|
747
174
|
if (!root) {
|
|
748
|
-
|
|
749
|
-
"
|
|
175
|
+
appilotsDebugWarn(
|
|
176
|
+
"captureSnapshot: fiber root not captured yet. Ensure <AppilotsProvider> has rendered before calling this."
|
|
750
177
|
);
|
|
751
178
|
recordIntrospectionFailure("never-mounted", null);
|
|
752
179
|
return {
|
|
@@ -765,34 +192,83 @@ function captureSnapshot() {
|
|
|
765
192
|
}
|
|
766
193
|
const start = Date.now();
|
|
767
194
|
const snapshot = walkFiber(root);
|
|
195
|
+
recordBlindWalk();
|
|
768
196
|
const activePath = getActiveRouteNames();
|
|
769
197
|
const deepestRoute = activePath[activePath.length - 1];
|
|
770
198
|
if (deepestRoute) snapshot.route = deepestRoute;
|
|
771
199
|
mergeRegisteredLists(snapshot);
|
|
200
|
+
mergeRegisteredScrollables(snapshot);
|
|
201
|
+
mergeNativeDialog(snapshot);
|
|
772
202
|
reconcileSliders(snapshot);
|
|
773
|
-
|
|
774
|
-
snapshot
|
|
775
|
-
|
|
776
|
-
|
|
203
|
+
canonicalizeRegisteredFields(snapshot);
|
|
204
|
+
clampSnapshotToWireLimits(snapshot);
|
|
205
|
+
const entries = deriveInteractionElements(snapshot, { rectOf: rectForNode });
|
|
206
|
+
const rectById = new Map(
|
|
207
|
+
entries.flatMap((entry) => entry.rect ? [[entry.id, entry.rect]] : [])
|
|
777
208
|
);
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
209
|
+
snapshot.elements = entries.map(toWireElement);
|
|
210
|
+
snapshot.choiceGroups = attachElementIdsToChoiceGroups(snapshot.choiceGroups, snapshot.elements);
|
|
211
|
+
clampSnapshotToWireLimits(snapshot);
|
|
212
|
+
elementRegistry.replaceAll(
|
|
213
|
+
snapshot.elements.map((element) => {
|
|
214
|
+
const rect = rectById.get(element.id);
|
|
215
|
+
return rect ? { ...element, rect } : element;
|
|
216
|
+
})
|
|
783
217
|
);
|
|
218
|
+
const elapsed = Date.now() - start;
|
|
219
|
+
const totalListItems = snapshot.lists.reduce((acc, l) => acc + l.items.length, 0);
|
|
784
220
|
const totalListDataItems = snapshot.lists.reduce(
|
|
785
221
|
(acc, l) => acc + (typeof l.itemCount === "number" ? l.itemCount : 0),
|
|
786
222
|
0
|
|
787
223
|
);
|
|
788
|
-
|
|
789
|
-
`
|
|
224
|
+
appilotsDebugLog(
|
|
225
|
+
`captureSnapshot: route="${snapshot.route}" texts=${snapshot.texts.length} inputs=${snapshot.inputs.length} buttons=${snapshot.buttons.length} toggles=${snapshot.toggles.length} sliders=${snapshot.sliders.length} loading=${snapshot.loading} modal=${snapshot.modalOpen} lists=${snapshot.lists.length}(visibleItems=${totalListItems}, dataItems=${totalListDataItems}) choices=${snapshot.choiceGroups.length} elements=${snapshot.elements.length} ` + (snapshot.truncated ? "truncated=true " : "") + `(walked ${snapshot.stats?.visitedFibers ?? 0} fibers, skipped ${snapshot.stats?.skippedHidden ?? 0} hidden, ${elapsed}ms)`
|
|
790
226
|
);
|
|
791
|
-
|
|
792
|
-
`
|
|
227
|
+
appilotsDebugLog(
|
|
228
|
+
`captureSnapshot detail: lists=[${snapshot.lists.map((l) => `${l.containerType}:${l.id ?? l.label ?? "?"}(rows=${l.items.length})`).join(", ")}] inputs=[${snapshot.inputs.map((i) => i.id ?? i.label ?? "?").slice(0, 12).join(", ")}] buttons=[${snapshot.buttons.map((b) => b.id ?? b.label ?? "?").slice(0, 15).join(", ")}]`
|
|
793
229
|
);
|
|
794
230
|
return snapshot;
|
|
795
231
|
}
|
|
232
|
+
function registryCounts() {
|
|
233
|
+
const counts = {
|
|
234
|
+
fields: 0,
|
|
235
|
+
toggles: 0,
|
|
236
|
+
sliders: 0,
|
|
237
|
+
targets: 0,
|
|
238
|
+
lists: listRegistry.snapshot().length,
|
|
239
|
+
elements: elementRegistry.size
|
|
240
|
+
};
|
|
241
|
+
for (const { kind } of componentRegistry.snapshot()) {
|
|
242
|
+
if (kind === "field") counts.fields++;
|
|
243
|
+
else if (kind === "toggle") counts.toggles++;
|
|
244
|
+
else if (kind === "slider") counts.sliders++;
|
|
245
|
+
else if (kind === "target") counts.targets++;
|
|
246
|
+
}
|
|
247
|
+
return counts;
|
|
248
|
+
}
|
|
249
|
+
function appilotsSelfCheck() {
|
|
250
|
+
const root = getFiberRoot();
|
|
251
|
+
const start = Date.now();
|
|
252
|
+
if (root) walkFiber(root);
|
|
253
|
+
const elapsedMs = Date.now() - start;
|
|
254
|
+
const walk = getLastWalkDiagnostics();
|
|
255
|
+
return {
|
|
256
|
+
sdkVersion: SDK_VERSION,
|
|
257
|
+
reactVersion: React.version ?? null,
|
|
258
|
+
route: getCurrentScreen() || null,
|
|
259
|
+
introspection: getIntrospectionDiagnostics(),
|
|
260
|
+
autoTracking: getAutoTrackingState(),
|
|
261
|
+
walk: {
|
|
262
|
+
visitedFibers: root ? walk?.visitedFibers ?? 0 : 0,
|
|
263
|
+
skippedHidden: root ? walk?.skippedHidden ?? 0 : 0,
|
|
264
|
+
elapsedMs
|
|
265
|
+
},
|
|
266
|
+
detectors: root ? walk?.detectors ?? emptyDetectorCounts() : emptyDetectorCounts(),
|
|
267
|
+
registry: registryCounts(),
|
|
268
|
+
nameMangling: getNameManglingProbe(),
|
|
269
|
+
geometry: { availability: geometryAvailability(), ...geometryStats() }
|
|
270
|
+
};
|
|
271
|
+
}
|
|
796
272
|
|
|
797
273
|
// src/introspection/findInteractive.ts
|
|
798
274
|
function getTypeName2(type) {
|
|
@@ -806,7 +282,7 @@ function getTypeName2(type) {
|
|
|
806
282
|
}
|
|
807
283
|
return null;
|
|
808
284
|
}
|
|
809
|
-
function
|
|
285
|
+
function matchesName(fiber, name) {
|
|
810
286
|
return getTypeName2(fiber.type) === name || getTypeName2(fiber.elementType) === name;
|
|
811
287
|
}
|
|
812
288
|
var TOUCHABLE_NAMES2 = /* @__PURE__ */ new Set([
|
|
@@ -819,33 +295,33 @@ var TOUCHABLE_NAMES2 = /* @__PURE__ */ new Set([
|
|
|
819
295
|
function isTouchable(fiber) {
|
|
820
296
|
return TOUCHABLE_NAMES2.has(getTypeName2(fiber.type) ?? "") || TOUCHABLE_NAMES2.has(getTypeName2(fiber.elementType) ?? "");
|
|
821
297
|
}
|
|
822
|
-
function
|
|
298
|
+
function hasPressHandler(fiber) {
|
|
823
299
|
const props = fiber?.memoizedProps ?? {};
|
|
824
300
|
return typeof props.onPress === "function" || typeof props.onPressIn === "function";
|
|
825
301
|
}
|
|
826
|
-
function
|
|
827
|
-
return isTouchable(fiber) ||
|
|
302
|
+
function isPressableTarget(fiber) {
|
|
303
|
+
return isTouchable(fiber) || hasPressHandler(fiber);
|
|
828
304
|
}
|
|
829
|
-
var
|
|
305
|
+
var SELF_REFERENTIAL_COMPONENTS = /* @__PURE__ */ new Set([
|
|
830
306
|
"AppilotsChat",
|
|
831
307
|
"AppilotsChatInner",
|
|
832
308
|
"ActionBreadcrumb",
|
|
833
309
|
"ConfirmDialog"
|
|
834
310
|
]);
|
|
835
|
-
function
|
|
311
|
+
function isAppilotsSkipMarked(props) {
|
|
836
312
|
if (!props) return false;
|
|
837
313
|
return props.__appilotsSkip === true || props["data-appilots-skip"] === true || props.appilotsSkip === true;
|
|
838
314
|
}
|
|
839
|
-
function
|
|
315
|
+
function extractTextFromChildren(children) {
|
|
840
316
|
if (children == null) return "";
|
|
841
317
|
if (typeof children === "string") return children;
|
|
842
318
|
if (typeof children === "number") return String(children);
|
|
843
319
|
if (Array.isArray(children)) {
|
|
844
|
-
return children.map(
|
|
320
|
+
return children.map(extractTextFromChildren).filter(Boolean).join("");
|
|
845
321
|
}
|
|
846
322
|
return "";
|
|
847
323
|
}
|
|
848
|
-
function
|
|
324
|
+
function findChildText(fiber) {
|
|
849
325
|
const stack = [];
|
|
850
326
|
if (fiber.child) stack.push(fiber.child);
|
|
851
327
|
const visited = /* @__PURE__ */ new WeakSet();
|
|
@@ -853,8 +329,8 @@ function findChildText2(fiber) {
|
|
|
853
329
|
const node = stack.pop();
|
|
854
330
|
if (!node || visited.has(node)) continue;
|
|
855
331
|
visited.add(node);
|
|
856
|
-
if (
|
|
857
|
-
const text =
|
|
332
|
+
if (matchesName(node, "Text")) {
|
|
333
|
+
const text = extractTextFromChildren(node.memoizedProps?.children);
|
|
858
334
|
if (text && text.trim().length > 0) return text.trim();
|
|
859
335
|
}
|
|
860
336
|
if (node.sibling) stack.push(node.sibling);
|
|
@@ -866,10 +342,10 @@ function shouldSkipSubtree2(fiber) {
|
|
|
866
342
|
const props = fiber.memoizedProps;
|
|
867
343
|
if (!props) return false;
|
|
868
344
|
const typeName = getTypeName2(fiber.type) ?? getTypeName2(fiber.elementType);
|
|
869
|
-
if (typeName &&
|
|
870
|
-
if (
|
|
871
|
-
if (
|
|
872
|
-
if (
|
|
345
|
+
if (typeName && SELF_REFERENTIAL_COMPONENTS.has(typeName)) return true;
|
|
346
|
+
if (isAppilotsSkipMarked(props)) return true;
|
|
347
|
+
if (isModalFiber(fiber) && props.visible === false) return true;
|
|
348
|
+
if (matchesName(fiber, "Screen") && props.activityState === 0) return true;
|
|
873
349
|
const style = props.style;
|
|
874
350
|
if (style) {
|
|
875
351
|
const flat = Array.isArray(style) ? Object.assign({}, ...style.filter(Boolean)) : style;
|
|
@@ -878,20 +354,19 @@ function shouldSkipSubtree2(fiber) {
|
|
|
878
354
|
}
|
|
879
355
|
return false;
|
|
880
356
|
}
|
|
881
|
-
|
|
882
|
-
if (!s) return "";
|
|
883
|
-
return s.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
|
|
884
|
-
}
|
|
357
|
+
var normalize = foldForCompare;
|
|
885
358
|
function scoreMatch(fiber, queryNorm, queryRaw) {
|
|
886
359
|
const props = fiber.memoizedProps ?? {};
|
|
887
360
|
const candidates = [];
|
|
888
361
|
if (typeof props.testID === "string") candidates.push({ val: props.testID, weight: 100 });
|
|
889
|
-
if (typeof props.accessibilityLabel === "string")
|
|
362
|
+
if (typeof props.accessibilityLabel === "string")
|
|
363
|
+
candidates.push({ val: props.accessibilityLabel, weight: 95 });
|
|
890
364
|
if (typeof props.label === "string") candidates.push({ val: props.label, weight: 90 });
|
|
891
|
-
if (typeof props.placeholder === "string")
|
|
365
|
+
if (typeof props.placeholder === "string")
|
|
366
|
+
candidates.push({ val: props.placeholder, weight: 70 });
|
|
892
367
|
if (typeof props.title === "string") candidates.push({ val: props.title, weight: 85 });
|
|
893
|
-
if (
|
|
894
|
-
const childText =
|
|
368
|
+
if (isPressableTarget(fiber)) {
|
|
369
|
+
const childText = findChildText(fiber);
|
|
895
370
|
if (childText) candidates.push({ val: childText, weight: 80 });
|
|
896
371
|
}
|
|
897
372
|
let best = 0;
|
|
@@ -900,7 +375,8 @@ function scoreMatch(fiber, queryNorm, queryRaw) {
|
|
|
900
375
|
if (!vNorm) continue;
|
|
901
376
|
if (val === queryRaw) best = Math.max(best, weight + 20);
|
|
902
377
|
else if (vNorm === queryNorm) best = Math.max(best, weight + 10);
|
|
903
|
-
else if (vNorm.includes(queryNorm) || queryNorm.includes(vNorm))
|
|
378
|
+
else if (queryNorm && (vNorm.includes(queryNorm) || queryNorm.includes(vNorm)))
|
|
379
|
+
best = Math.max(best, weight - 20);
|
|
904
380
|
}
|
|
905
381
|
if (best > 0 && props.disabled === true) {
|
|
906
382
|
best = Math.max(1, best - 60);
|
|
@@ -931,6 +407,12 @@ function wrapTextInput(fiber) {
|
|
|
931
407
|
if (node && typeof node.focus === "function") {
|
|
932
408
|
node.focus();
|
|
933
409
|
}
|
|
410
|
+
},
|
|
411
|
+
submitFromKeyboard: () => {
|
|
412
|
+
const { onSubmitEditing, value } = getProps();
|
|
413
|
+
if (typeof onSubmitEditing !== "function") return false;
|
|
414
|
+
onSubmitEditing({ nativeEvent: { text: value ?? "" } });
|
|
415
|
+
return true;
|
|
934
416
|
}
|
|
935
417
|
};
|
|
936
418
|
}
|
|
@@ -970,14 +452,14 @@ function wrapTouchable(fiber, label) {
|
|
|
970
452
|
function findInteractiveByIdentifier(query, options = {}) {
|
|
971
453
|
const root = getFiberRoot();
|
|
972
454
|
if (!root) {
|
|
973
|
-
|
|
455
|
+
appilotsDebugWarn("findInteractiveByIdentifier: fiber root not captured yet");
|
|
974
456
|
return null;
|
|
975
457
|
}
|
|
976
458
|
const queryNorm = normalize(query);
|
|
977
459
|
const queryRaw = query;
|
|
978
460
|
let bestScore = 0;
|
|
979
461
|
let bestHandle = null;
|
|
980
|
-
let
|
|
462
|
+
let bestId;
|
|
981
463
|
const visited = /* @__PURE__ */ new WeakSet();
|
|
982
464
|
const stack = [];
|
|
983
465
|
if (root.child) stack.push(root.child);
|
|
@@ -988,40 +470,50 @@ function findInteractiveByIdentifier(query, options = {}) {
|
|
|
988
470
|
if (fiber.sibling) stack.push(fiber.sibling);
|
|
989
471
|
if (shouldSkipSubtree2(fiber)) continue;
|
|
990
472
|
let kind = null;
|
|
991
|
-
if (
|
|
992
|
-
else if (
|
|
993
|
-
else if (
|
|
473
|
+
if (matchesName(fiber, "TextInput")) kind = "field";
|
|
474
|
+
else if (matchesName(fiber, "Switch")) kind = "toggle";
|
|
475
|
+
else if (isPressableTarget(fiber)) kind = "target";
|
|
994
476
|
if (kind && (!options.kind || options.kind === kind)) {
|
|
995
477
|
const score = scoreMatch(fiber, queryNorm, queryRaw);
|
|
996
478
|
if (score > bestScore) {
|
|
997
479
|
bestScore = score;
|
|
998
480
|
const props = fiber.memoizedProps ?? {};
|
|
999
|
-
|
|
481
|
+
bestId = props.testID ?? props.accessibilityLabel ?? props.placeholder ?? (kind === "target" ? findChildText(fiber) : void 0);
|
|
1000
482
|
if (kind === "field") bestHandle = wrapTextInput(fiber);
|
|
1001
483
|
else if (kind === "toggle") bestHandle = wrapSwitch(fiber);
|
|
1002
|
-
else bestHandle = wrapTouchable(fiber,
|
|
484
|
+
else bestHandle = wrapTouchable(fiber, findChildText(fiber));
|
|
1003
485
|
}
|
|
1004
486
|
}
|
|
1005
487
|
if (fiber.child) stack.push(fiber.child);
|
|
1006
488
|
}
|
|
1007
489
|
if (bestHandle) {
|
|
1008
|
-
|
|
1009
|
-
`
|
|
490
|
+
appilotsDebugLog(
|
|
491
|
+
`findInteractiveByIdentifier: query="${query}" \u2192 kind=${bestHandle.kind} label="${bestHandle.label ?? bestId}" score=${bestScore}`
|
|
1010
492
|
);
|
|
1011
493
|
} else {
|
|
1012
|
-
|
|
494
|
+
appilotsDebugLog(`findInteractiveByIdentifier: query="${query}" \u2192 no match`);
|
|
1013
495
|
}
|
|
1014
496
|
return bestHandle;
|
|
1015
497
|
}
|
|
1016
498
|
|
|
1017
499
|
// src/executor/handlers/navigateHandler.ts
|
|
500
|
+
var _reportedMissingNavRef = false;
|
|
501
|
+
function reportMissingNavigationRef() {
|
|
502
|
+
if (_reportedMissingNavRef) return;
|
|
503
|
+
_reportedMissingNavRef = true;
|
|
504
|
+
console.error(
|
|
505
|
+
"[Appilots] Navigation is not wired. The SDK never received a navigation ref, so every navigate the agent attempts will fail.\n Fix: wrap your NavigationContainer with <AppilotsNavigationContainer>:\n <AppilotsProvider config={...}>\n <AppilotsNavigationContainer>\n <NavigationContainer>{/* your navigators */}</NavigationContainer>\n </AppilotsNavigationContainer>\n <AppilotsChat />\n </AppilotsProvider>\n Docs: https://docs.appilots.com/getting-started/quick-start"
|
|
506
|
+
);
|
|
507
|
+
}
|
|
1018
508
|
function categorizeNavigationError(message) {
|
|
1019
509
|
if (/screen named|was not handled by any navigator|no route|not found|not registered in any navigator|couldn'?t find a route|route .* (?:doesn'?t|does not) exist/i.test(
|
|
1020
510
|
message
|
|
1021
511
|
)) {
|
|
1022
512
|
return "component-not-found";
|
|
1023
513
|
}
|
|
1024
|
-
if (/couldn'?t find a navigation object|navigator (?:is )?not (?:rendered|ready)|not been (?:mounted|rendered)/i.test(
|
|
514
|
+
if (/couldn'?t find a navigation object|navigator (?:is )?not (?:rendered|ready)|not been (?:mounted|rendered)/i.test(
|
|
515
|
+
message
|
|
516
|
+
)) {
|
|
1025
517
|
return "screen-timeout";
|
|
1026
518
|
}
|
|
1027
519
|
return "unknown";
|
|
@@ -1048,7 +540,9 @@ function screenExistsAnywhere(state, screenName) {
|
|
|
1048
540
|
function safeNavigate(nav, screenName, params, path) {
|
|
1049
541
|
if (path && path.length > 0) {
|
|
1050
542
|
const { root, nestedParams } = buildNestedParams(path, screenName, params);
|
|
1051
|
-
|
|
543
|
+
appilotsDebugLog(
|
|
544
|
+
`safeNavigate: using path-based navigation \u2014 root="${root}", nested=${JSON.stringify(nestedParams)}`
|
|
545
|
+
);
|
|
1052
546
|
nav.navigate(root, nestedParams);
|
|
1053
547
|
return;
|
|
1054
548
|
}
|
|
@@ -1063,11 +557,17 @@ function safeNavigate(nav, screenName, params, path) {
|
|
|
1063
557
|
`Screen "${screenName}" is not registered in any navigator. The screen may have been removed, may live inside a flow that is not currently mounted (e.g. Auth stack after login), or the MCP doc is stale.`
|
|
1064
558
|
);
|
|
1065
559
|
}
|
|
1066
|
-
|
|
560
|
+
appilotsDebugLog(
|
|
561
|
+
`safeNavigate: screen exists in nested navigator, attempting direct navigation to "${screenName}"`
|
|
562
|
+
);
|
|
1067
563
|
nav.navigate(screenName, params);
|
|
1068
564
|
}
|
|
1069
565
|
function navigateHandler(payload, context) {
|
|
1070
|
-
|
|
566
|
+
appilotsDebugLog(
|
|
567
|
+
// Params carry record ids and, on some screens, the user's own
|
|
568
|
+
// data — the count answers "were params passed?" and nothing else.
|
|
569
|
+
`navigateHandler called: screenName="${payload.screenName}" action="${payload.navigationAction}" params=${describeValue(payload.params)}`
|
|
570
|
+
);
|
|
1071
571
|
const { navigationRef, permissions } = context;
|
|
1072
572
|
const nav = navigationRef.current;
|
|
1073
573
|
if (!permissions.canNavigate) {
|
|
@@ -1077,14 +577,15 @@ function navigateHandler(payload, context) {
|
|
|
1077
577
|
diagnose: { category: "unknown", screen: payload.screenName }
|
|
1078
578
|
};
|
|
1079
579
|
}
|
|
1080
|
-
|
|
580
|
+
const isGoBack = payload.navigationAction === "goBack";
|
|
581
|
+
if (!isGoBack && permissions.blockedScreens?.includes(payload.screenName)) {
|
|
1081
582
|
return {
|
|
1082
583
|
success: false,
|
|
1083
584
|
error: `Navigation to "${payload.screenName}" is blocked`,
|
|
1084
585
|
diagnose: { category: "unknown", screen: payload.screenName }
|
|
1085
586
|
};
|
|
1086
587
|
}
|
|
1087
|
-
if (permissions.allowedScreens && permissions.allowedScreens.length > 0 && !permissions.allowedScreens.includes(payload.screenName)) {
|
|
588
|
+
if (!isGoBack && permissions.allowedScreens && permissions.allowedScreens.length > 0 && !permissions.allowedScreens.includes(payload.screenName)) {
|
|
1088
589
|
return {
|
|
1089
590
|
success: false,
|
|
1090
591
|
error: `Navigation to "${payload.screenName}" is not in allowed screens`,
|
|
@@ -1092,10 +593,11 @@ function navigateHandler(payload, context) {
|
|
|
1092
593
|
};
|
|
1093
594
|
}
|
|
1094
595
|
if (!nav) {
|
|
596
|
+
reportMissingNavigationRef();
|
|
1095
597
|
return {
|
|
1096
598
|
success: false,
|
|
1097
|
-
error: "Navigation
|
|
1098
|
-
diagnose: { category: "
|
|
599
|
+
error: "Navigation is not wired: <AppilotsNavigationContainer> is missing, so the SDK never received the navigation ref. Wrap your <NavigationContainer> with it. This cannot be retried \u2014 it needs a change in the host app.",
|
|
600
|
+
diagnose: { category: "unknown", screen: payload.screenName }
|
|
1099
601
|
};
|
|
1100
602
|
}
|
|
1101
603
|
if (!nav.isReady || typeof nav.isReady === "function" && !nav.isReady()) {
|
|
@@ -1106,7 +608,15 @@ function navigateHandler(payload, context) {
|
|
|
1106
608
|
};
|
|
1107
609
|
}
|
|
1108
610
|
try {
|
|
1109
|
-
const {
|
|
611
|
+
const { params, navigationAction, path } = payload;
|
|
612
|
+
if (!isGoBack && !payload.screenName) {
|
|
613
|
+
return {
|
|
614
|
+
success: false,
|
|
615
|
+
error: `Navigation action "${navigationAction ?? "navigate"}" requires a screenName.`,
|
|
616
|
+
diagnose: { category: "unknown" }
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
const screenName = payload.screenName;
|
|
1110
620
|
switch (navigationAction) {
|
|
1111
621
|
case "navigate":
|
|
1112
622
|
safeNavigate(nav, screenName, params ?? {}, path);
|
|
@@ -1149,10 +659,14 @@ function navigateHandler(payload, context) {
|
|
|
1149
659
|
diagnose: { category: "unknown", screen: payload.screenName }
|
|
1150
660
|
};
|
|
1151
661
|
}
|
|
1152
|
-
|
|
662
|
+
appilotsDebugLog(
|
|
663
|
+
`navigateHandler: navigation succeeded for "${payload.screenName}" (action="${payload.navigationAction}")`
|
|
664
|
+
);
|
|
1153
665
|
return { success: true };
|
|
1154
666
|
} catch (err) {
|
|
1155
|
-
|
|
667
|
+
appilotsDebugWarn(
|
|
668
|
+
`navigateHandler: navigation failed for "${payload.screenName}": ${err?.message}`
|
|
669
|
+
);
|
|
1156
670
|
const msg = String(err?.message ?? "Navigation failed");
|
|
1157
671
|
const category = categorizeNavigationError(msg);
|
|
1158
672
|
return {
|
|
@@ -1175,7 +689,7 @@ function parseSyntheticListItemId(id) {
|
|
|
1175
689
|
if (listIndex < 0 || itemIndex < 1) return null;
|
|
1176
690
|
return { listIndex, itemIndex };
|
|
1177
691
|
}
|
|
1178
|
-
function
|
|
692
|
+
function stringProp(props, key) {
|
|
1179
693
|
const value = props?.[key];
|
|
1180
694
|
if (typeof value === "string" && value.trim()) return value.trim();
|
|
1181
695
|
if (typeof value === "number") return String(value);
|
|
@@ -1183,30 +697,30 @@ function stringProp2(props, key) {
|
|
|
1183
697
|
}
|
|
1184
698
|
function listIdFromContainer(container) {
|
|
1185
699
|
const props = container?.memoizedProps ?? {};
|
|
1186
|
-
return
|
|
700
|
+
return stringProp(props, "__appilotsListId") ?? stringProp(props, "appilotsListId") ?? stringProp(props, "testID") ?? stringProp(props, "accessibilityLabel");
|
|
1187
701
|
}
|
|
1188
702
|
function listLabelFromContainer(container) {
|
|
1189
703
|
const props = container?.memoizedProps ?? {};
|
|
1190
|
-
return
|
|
704
|
+
return stringProp(props, "__appilotsListLabel") ?? stringProp(props, "accessibilityLabel") ?? stringProp(props, "aria-label") ?? stringProp(props, "label");
|
|
1191
705
|
}
|
|
1192
706
|
function itemKeyFromFiber(fiber) {
|
|
1193
707
|
const props = fiber?.memoizedProps ?? {};
|
|
1194
|
-
return
|
|
708
|
+
return stringProp(props, "__appilotsItemKey") ?? (fiber?.key !== void 0 && fiber?.key !== null ? String(fiber.key) : void 0);
|
|
1195
709
|
}
|
|
1196
710
|
function markedItemIndexFromFiber(fiber) {
|
|
1197
711
|
const value = fiber?.memoizedProps?.__appilotsItemIndex;
|
|
1198
712
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1199
713
|
}
|
|
1200
714
|
function normalize2(value) {
|
|
1201
|
-
return String(value ?? "")
|
|
715
|
+
return foldForCompare(String(value ?? ""));
|
|
1202
716
|
}
|
|
1203
|
-
function
|
|
717
|
+
function hasPressHandler2(fiber) {
|
|
1204
718
|
const props = fiber?.memoizedProps ?? {};
|
|
1205
719
|
return typeof props.onPress === "function" || typeof props.onPressIn === "function";
|
|
1206
720
|
}
|
|
1207
721
|
function isPressableFiber(fiber) {
|
|
1208
722
|
const typeName = getTypeName(fiber.type) ?? getTypeName(fiber.elementType) ?? "";
|
|
1209
|
-
return TOUCHABLE_NAMES.has(typeName) ||
|
|
723
|
+
return TOUCHABLE_NAMES.has(typeName) || hasPressHandler2(fiber);
|
|
1210
724
|
}
|
|
1211
725
|
function findListContainers(options = {}) {
|
|
1212
726
|
const root = getFiberRoot();
|
|
@@ -1351,13 +865,10 @@ function collectTexts(fiber) {
|
|
|
1351
865
|
}
|
|
1352
866
|
function hasUsefulTouchableIdentity(fiber) {
|
|
1353
867
|
const props = fiber?.memoizedProps ?? {};
|
|
1354
|
-
if (
|
|
1355
|
-
if (
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
if (typeof props.label === "string" && /[a-z0-9]/i.test(props.label)) return true;
|
|
1359
|
-
const label = findFirstText(fiber);
|
|
1360
|
-
return !!label && /[a-z0-9]/i.test(label);
|
|
868
|
+
if (isReferenceable(props.testID)) return true;
|
|
869
|
+
if (isReferenceable(props.accessibilityLabel)) return true;
|
|
870
|
+
if (isReferenceable(props.label)) return true;
|
|
871
|
+
return isReferenceable(findFirstText(fiber));
|
|
1361
872
|
}
|
|
1362
873
|
function describeTouchable(fiber) {
|
|
1363
874
|
const props = fiber?.memoizedProps ?? {};
|
|
@@ -1370,12 +881,7 @@ function touchableLooksDestructive(fiber) {
|
|
|
1370
881
|
const descriptor = describeTouchable(fiber);
|
|
1371
882
|
if (descriptor && looksDestructiveActionLabel(descriptor)) return true;
|
|
1372
883
|
const props = fiber?.memoizedProps ?? {};
|
|
1373
|
-
const haystack = [
|
|
1374
|
-
props.testID,
|
|
1375
|
-
props.accessibilityLabel,
|
|
1376
|
-
props.accessibilityHint,
|
|
1377
|
-
props.label
|
|
1378
|
-
].filter((value) => typeof value === "string").join(" ");
|
|
884
|
+
const haystack = [props.testID, props.accessibilityLabel, props.accessibilityHint, props.label].filter((value) => typeof value === "string").join(" ");
|
|
1379
885
|
return looksDestructiveActionLabel(haystack);
|
|
1380
886
|
}
|
|
1381
887
|
function describeItemCandidate(listIndex, zeroBasedIndex, item) {
|
|
@@ -1409,8 +915,8 @@ function pressResolvedListItem(list, itemFiber, listIndex, itemIndex) {
|
|
|
1409
915
|
(candidate) => candidate !== touchable && containsFiber(touchable, candidate)
|
|
1410
916
|
);
|
|
1411
917
|
if (primaryWrapsNestedTargets) {
|
|
1412
|
-
|
|
1413
|
-
`
|
|
918
|
+
appilotsDebugLog(
|
|
919
|
+
`listItemDispatch: item "list-${listIndex}-item-${itemIndex}" has nested pressables; pressing outer row target`
|
|
1414
920
|
);
|
|
1415
921
|
} else {
|
|
1416
922
|
const candidates = safeIdentifiableTouchables.map(describeTouchable).filter((d) => !!d).slice(0, 8);
|
|
@@ -1520,14 +1026,14 @@ function pressListItemAtOrdinal(listIndex, itemIndex) {
|
|
|
1520
1026
|
}
|
|
1521
1027
|
return pressResolvedListItem(list, items[zeroBased], listIndex, itemIndex);
|
|
1522
1028
|
}
|
|
1523
|
-
function
|
|
1524
|
-
if (!getFiberRoot()) return { ok: false, reason: "no-fiber-root" };
|
|
1029
|
+
function resolveListItemByIdentity(locator) {
|
|
1030
|
+
if (!getFiberRoot()) return { ok: false, result: { ok: false, reason: "no-fiber-root" } };
|
|
1525
1031
|
const parsed = locator.syntheticId ? parseSyntheticListItemId(locator.syntheticId) : null;
|
|
1526
1032
|
const effective = parsed ? { ...locator, listIndex: parsed.listIndex, itemIndex: parsed.itemIndex } : locator;
|
|
1527
1033
|
const containers = findListContainers({ allowSingleHeuristic: true });
|
|
1528
1034
|
const lists = candidateLists(containers, effective);
|
|
1529
1035
|
if (lists.length === 0) {
|
|
1530
|
-
return { ok: false, reason: "list-not-found" };
|
|
1036
|
+
return { ok: false, result: { ok: false, reason: "list-not-found" } };
|
|
1531
1037
|
}
|
|
1532
1038
|
const scoredCandidates = [];
|
|
1533
1039
|
for (const { list, listIndex } of lists) {
|
|
@@ -1538,7 +1044,14 @@ function pressListItemByIdentity(locator) {
|
|
|
1538
1044
|
}
|
|
1539
1045
|
}
|
|
1540
1046
|
if (scoredCandidates.length === 0) {
|
|
1541
|
-
return {
|
|
1047
|
+
return {
|
|
1048
|
+
ok: false,
|
|
1049
|
+
result: {
|
|
1050
|
+
ok: false,
|
|
1051
|
+
reason: "item-not-found",
|
|
1052
|
+
containerType: lists[0]?.list.containerType
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1542
1055
|
}
|
|
1543
1056
|
scoredCandidates.sort((a, b) => b.score - a.score);
|
|
1544
1057
|
const best = scoredCandidates[0];
|
|
@@ -1547,13 +1060,104 @@ function pressListItemByIdentity(locator) {
|
|
|
1547
1060
|
const candidates = tiedCandidates.map((c) => describeItemCandidate(c.listIndex, c.itemIndex - 1, c.item)).slice(0, 8);
|
|
1548
1061
|
return {
|
|
1549
1062
|
ok: false,
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1063
|
+
result: {
|
|
1064
|
+
ok: false,
|
|
1065
|
+
reason: "ambiguous-items",
|
|
1066
|
+
error: "Multiple visible list items matched the requested target with equal confidence." + (candidates.length > 0 ? ` Candidates: ${candidates.join(", ")}.` : ""),
|
|
1067
|
+
containerType: best.list.containerType,
|
|
1068
|
+
candidates
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
return {
|
|
1073
|
+
ok: true,
|
|
1074
|
+
list: best.list,
|
|
1075
|
+
item: best.item,
|
|
1076
|
+
listIndex: best.listIndex,
|
|
1077
|
+
itemIndex: best.itemIndex
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
function findSwitchFibers(item) {
|
|
1081
|
+
if (!item) return [];
|
|
1082
|
+
const found = [];
|
|
1083
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
1084
|
+
const stack = [
|
|
1085
|
+
{ fiber: item, includeSibling: false }
|
|
1086
|
+
];
|
|
1087
|
+
while (stack.length > 0) {
|
|
1088
|
+
const { fiber, includeSibling } = stack.pop();
|
|
1089
|
+
if (!fiber || visited.has(fiber)) continue;
|
|
1090
|
+
visited.add(fiber);
|
|
1091
|
+
if (shouldSkipSubtree(fiber)) continue;
|
|
1092
|
+
const props = fiber.memoizedProps ?? {};
|
|
1093
|
+
if (isSwitchLike(fiber, props)) {
|
|
1094
|
+
if (props.__appilotsInternal !== true) found.push(fiber);
|
|
1095
|
+
if (includeSibling && fiber.sibling) {
|
|
1096
|
+
stack.push({ fiber: fiber.sibling, includeSibling: true });
|
|
1097
|
+
}
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
if (includeSibling && fiber.sibling) {
|
|
1101
|
+
stack.push({ fiber: fiber.sibling, includeSibling: true });
|
|
1102
|
+
}
|
|
1103
|
+
if (fiber.child) stack.push({ fiber: fiber.child, includeSibling: true });
|
|
1104
|
+
}
|
|
1105
|
+
return found;
|
|
1106
|
+
}
|
|
1107
|
+
function toggleListItemByIdentity(locator, options) {
|
|
1108
|
+
const resolved = resolveListItemByIdentity(locator);
|
|
1109
|
+
if (!resolved.ok) return resolved.result;
|
|
1110
|
+
const { list, item } = resolved;
|
|
1111
|
+
const switches = findSwitchFibers(item);
|
|
1112
|
+
if (switches.length === 0) {
|
|
1113
|
+
return { ok: false, reason: "no-touchable", containerType: list.containerType };
|
|
1114
|
+
}
|
|
1115
|
+
if (switches.length > 1) {
|
|
1116
|
+
const candidates = switches.map((fiber, index) => describeTouchable(fiber) ?? `switch-${index + 1}`).slice(0, 8);
|
|
1117
|
+
return {
|
|
1118
|
+
ok: false,
|
|
1119
|
+
reason: "ambiguous-touchables",
|
|
1120
|
+
error: 'This row exposes more than one switch, so flipping "the" switch would be a guess.' + (candidates.length > 0 ? ` Candidates: ${candidates.join(", ")}.` : ""),
|
|
1121
|
+
containerType: list.containerType,
|
|
1553
1122
|
candidates
|
|
1554
1123
|
};
|
|
1555
1124
|
}
|
|
1556
|
-
|
|
1125
|
+
const target = switches[0];
|
|
1126
|
+
const props = target?.memoizedProps ?? {};
|
|
1127
|
+
const current = switchValue(props);
|
|
1128
|
+
if (options?.expected !== void 0 && current === options.expected) {
|
|
1129
|
+
return { ok: true, containerType: list.containerType, value: current };
|
|
1130
|
+
}
|
|
1131
|
+
const onValueChange = props.onValueChange ?? props.onChange;
|
|
1132
|
+
if (typeof onValueChange !== "function") {
|
|
1133
|
+
return {
|
|
1134
|
+
ok: false,
|
|
1135
|
+
reason: "no-touchable",
|
|
1136
|
+
error: "The row has a switch but it exposes no onValueChange handler.",
|
|
1137
|
+
containerType: list.containerType
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
try {
|
|
1141
|
+
onValueChange(!current);
|
|
1142
|
+
} catch (err) {
|
|
1143
|
+
return {
|
|
1144
|
+
ok: false,
|
|
1145
|
+
reason: "press-threw",
|
|
1146
|
+
error: err?.message ?? "switch handler threw",
|
|
1147
|
+
containerType: list.containerType
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
return { ok: true, containerType: list.containerType, value: !current };
|
|
1151
|
+
}
|
|
1152
|
+
function pressListItemByIdentity(locator) {
|
|
1153
|
+
const resolved = resolveListItemByIdentity(locator);
|
|
1154
|
+
if (!resolved.ok) return resolved.result;
|
|
1155
|
+
return pressResolvedListItem(
|
|
1156
|
+
resolved.list,
|
|
1157
|
+
resolved.item,
|
|
1158
|
+
resolved.listIndex,
|
|
1159
|
+
resolved.itemIndex
|
|
1160
|
+
);
|
|
1557
1161
|
}
|
|
1558
1162
|
|
|
1559
1163
|
// src/executor/handlers/elementDispatch.ts
|
|
@@ -1782,6 +1386,46 @@ function dispatchElement(element, registry, action) {
|
|
|
1782
1386
|
};
|
|
1783
1387
|
}
|
|
1784
1388
|
if (element.role === "toggle" && (action === "toggle" || isPressLike(action))) {
|
|
1389
|
+
if (element.listContext) {
|
|
1390
|
+
const toggled = toggleListItemByIdentity(
|
|
1391
|
+
{
|
|
1392
|
+
listIndex: element.listContext.listIndex,
|
|
1393
|
+
listId: element.listContext.listId,
|
|
1394
|
+
listLabel: element.listContext.listLabel,
|
|
1395
|
+
itemIndex: element.listContext.itemIndex,
|
|
1396
|
+
itemKey: element.listContext.itemKey,
|
|
1397
|
+
reactKey: element.listContext.reactKey,
|
|
1398
|
+
syntheticId: element.listContext.syntheticId,
|
|
1399
|
+
label: element.label,
|
|
1400
|
+
texts: element.texts
|
|
1401
|
+
},
|
|
1402
|
+
// The observation's `selected` is the state BEFORE this action.
|
|
1403
|
+
// Passing the desired end state makes the flip idempotent: a
|
|
1404
|
+
// retry or a recovery hop must not undo the work and call it
|
|
1405
|
+
// success. A toggle only knows how to invert, so without this the
|
|
1406
|
+
// second attempt silently returns the row to where it started.
|
|
1407
|
+
typeof element.selected === "boolean" ? { expected: !element.selected } : void 0
|
|
1408
|
+
);
|
|
1409
|
+
if (toggled.ok) {
|
|
1410
|
+
return {
|
|
1411
|
+
success: true,
|
|
1412
|
+
// Grounded: read back off the fiber after the handler ran, not
|
|
1413
|
+
// inferred from "the call did not throw".
|
|
1414
|
+
effect: "changed"
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
return {
|
|
1418
|
+
success: false,
|
|
1419
|
+
error: toggled.error ?? `Could not flip the switch in row "${element.label ?? element.id}" (${toggled.reason})`,
|
|
1420
|
+
diagnose: {
|
|
1421
|
+
...screenDiagnose(
|
|
1422
|
+
element.id,
|
|
1423
|
+
toggled.reason === "ambiguous-touchables" || toggled.reason === "ambiguous-items" ? "ambiguous-target" : "component-not-found"
|
|
1424
|
+
),
|
|
1425
|
+
...toggled.candidates && toggled.candidates.length > 0 ? { candidates: toggled.candidates } : {}
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1785
1429
|
const targetId = element.targetId ?? element.label ?? element.id;
|
|
1786
1430
|
return dispatchLegacyTarget(targetId, registry, "toggle");
|
|
1787
1431
|
}
|
|
@@ -1811,8 +1455,8 @@ function dispatchStructuredElementId(identifier, action) {
|
|
|
1811
1455
|
texts: parsed.itemKey ? [parsed.itemKey] : void 0
|
|
1812
1456
|
});
|
|
1813
1457
|
if (pressed.ok) {
|
|
1814
|
-
|
|
1815
|
-
`
|
|
1458
|
+
appilotsDebugLog(
|
|
1459
|
+
`elementDispatch: resolved stale el id "${identifier}" via structural identity (listId="${parsed.listId}", itemKey="${parsed.itemKey}")`
|
|
1816
1460
|
);
|
|
1817
1461
|
return { success: true };
|
|
1818
1462
|
}
|
|
@@ -1836,7 +1480,6 @@ function pressElementOrTarget(identifier, registry) {
|
|
|
1836
1480
|
|
|
1837
1481
|
// src/executor/formFieldRewrite.ts
|
|
1838
1482
|
var LIST_ORDINAL_RE = /^list-\d+-item-\d+$/i;
|
|
1839
|
-
var PLATE_RE = /^[A-Z]{3}-?\d[A-Z0-9]\d{2}$/i;
|
|
1840
1483
|
function normalize4(value) {
|
|
1841
1484
|
return value.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
|
|
1842
1485
|
}
|
|
@@ -1876,9 +1519,6 @@ function collectKnownFormFields(registry) {
|
|
|
1876
1519
|
}
|
|
1877
1520
|
return registryFields;
|
|
1878
1521
|
}
|
|
1879
|
-
function fieldHaystack(field) {
|
|
1880
|
-
return normalize4(`${field.id} ${field.label ?? ""}`);
|
|
1881
|
-
}
|
|
1882
1522
|
function isMisplacedFieldId(fieldId, knownIds) {
|
|
1883
1523
|
const trimmed = fieldId.trim();
|
|
1884
1524
|
if (!trimmed) return true;
|
|
@@ -1902,50 +1542,68 @@ function valueMatchesOption(field, rawValue) {
|
|
|
1902
1542
|
return label === value || optionValue === value || value.includes(label) || value.includes(optionValue);
|
|
1903
1543
|
});
|
|
1904
1544
|
}
|
|
1545
|
+
var NUMERIC_FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
1546
|
+
"number",
|
|
1547
|
+
"numeric",
|
|
1548
|
+
"integer",
|
|
1549
|
+
"int",
|
|
1550
|
+
"decimal",
|
|
1551
|
+
"float",
|
|
1552
|
+
"range",
|
|
1553
|
+
"slider",
|
|
1554
|
+
"tel",
|
|
1555
|
+
"phone"
|
|
1556
|
+
]);
|
|
1557
|
+
var DATE_FIELD_TYPES = /* @__PURE__ */ new Set(["date", "datetime", "datetimelocal", "time", "month"]);
|
|
1558
|
+
var EMAIL_FIELD_TYPES = /* @__PURE__ */ new Set(["email"]);
|
|
1559
|
+
var CHOICE_FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
1560
|
+
"select",
|
|
1561
|
+
"picker",
|
|
1562
|
+
"dropdown",
|
|
1563
|
+
"combobox",
|
|
1564
|
+
"radio",
|
|
1565
|
+
"checkbox",
|
|
1566
|
+
"switch",
|
|
1567
|
+
"toggle",
|
|
1568
|
+
"segmented",
|
|
1569
|
+
"boolean"
|
|
1570
|
+
]);
|
|
1571
|
+
function readValueShape(rawValue) {
|
|
1572
|
+
const value = String(rawValue ?? "").trim();
|
|
1573
|
+
if (!value) return "text";
|
|
1574
|
+
if (/^-?\d+(?:[.,]\d+)?$/.test(value)) return "number";
|
|
1575
|
+
if (/^\S+@\S+\.\S+$/.test(value)) return "email";
|
|
1576
|
+
if (/^\d{4}-\d{2}-\d{2}(?:[T\s]\d{2}:\d{2}(?::\d{2})?)?$/.test(value)) return "date";
|
|
1577
|
+
if (/^\d{1,2}[/-]\d{1,2}[/-]\d{2,4}$/.test(value)) return "date";
|
|
1578
|
+
return "text";
|
|
1579
|
+
}
|
|
1580
|
+
function fieldTypeMatchesShape(field, shape) {
|
|
1581
|
+
const type = normalize4(field.type ?? "text");
|
|
1582
|
+
if (CHOICE_FIELD_TYPES.has(type)) return false;
|
|
1583
|
+
if (shape === "number") return NUMERIC_FIELD_TYPES.has(type);
|
|
1584
|
+
if (shape === "date") return DATE_FIELD_TYPES.has(type);
|
|
1585
|
+
if (shape === "email") return EMAIL_FIELD_TYPES.has(type);
|
|
1586
|
+
return false;
|
|
1587
|
+
}
|
|
1588
|
+
function fieldAcceptsFreeText(field, shape) {
|
|
1589
|
+
const type = normalize4(field.type ?? "text");
|
|
1590
|
+
if (CHOICE_FIELD_TYPES.has(type)) return false;
|
|
1591
|
+
if (NUMERIC_FIELD_TYPES.has(type)) return shape === "number";
|
|
1592
|
+
if (DATE_FIELD_TYPES.has(type)) return shape === "date";
|
|
1593
|
+
if (EMAIL_FIELD_TYPES.has(type)) return shape === "email";
|
|
1594
|
+
return true;
|
|
1595
|
+
}
|
|
1905
1596
|
function inferFieldIdForValue(rawValue, fields, used) {
|
|
1906
1597
|
const value = String(rawValue ?? "").trim();
|
|
1907
1598
|
if (!value) return void 0;
|
|
1908
|
-
const
|
|
1909
|
-
for (const field of
|
|
1910
|
-
if (used.has(field.id)) continue;
|
|
1599
|
+
const available = fields.filter((field) => field.id && !used.has(field.id));
|
|
1600
|
+
for (const field of available) {
|
|
1911
1601
|
if (valueMatchesOption(field, value)) return field.id;
|
|
1912
1602
|
}
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
if (/^(19|20)\d{2}$/.test(value)) {
|
|
1918
|
-
const field = fields.find((f) => /ano|year/.test(fieldHaystack(f)) && !used.has(f.id));
|
|
1919
|
-
if (field) return field.id;
|
|
1920
|
-
}
|
|
1921
|
-
if (/^\d+$/.test(value)) {
|
|
1922
|
-
const mileage = fields.find((f) => /quilometragem|mileage|km/.test(fieldHaystack(f)) && !used.has(f.id));
|
|
1923
|
-
if (mileage) return mileage.id;
|
|
1924
|
-
const year = fields.find((f) => /ano|year/.test(fieldHaystack(f)) && !used.has(f.id));
|
|
1925
|
-
if (year) return year.id;
|
|
1926
|
-
}
|
|
1927
|
-
if (/preto|branco|azul|vermelho|prata|black|white|red|blue|silver|gray|grey/.test(normalizedValue)) {
|
|
1928
|
-
const cor = fields.find((f) => /cor|color/.test(fieldHaystack(f)) && !used.has(f.id));
|
|
1929
|
-
if (cor) return cor.id;
|
|
1930
|
-
}
|
|
1931
|
-
const marca = fields.find((f) => /marca|brand|make/.test(fieldHaystack(f)) && !used.has(f.id));
|
|
1932
|
-
if (marca && !/model/i.test(normalizedValue)) return marca.id;
|
|
1933
|
-
const modelo = fields.find((f) => /modelo|model/.test(fieldHaystack(f)) && !used.has(f.id));
|
|
1934
|
-
if (modelo) return modelo.id;
|
|
1935
|
-
const tipo = fields.find((f) => /\btipo\b|type|vehicletype/.test(fieldHaystack(f)) && !used.has(f.id));
|
|
1936
|
-
if (tipo && /carro|car|moto|truck|van|flex|gasoline|diesel|hybrid|electric|el[eé]trico/.test(normalizedValue)) {
|
|
1937
|
-
return tipo.id;
|
|
1938
|
-
}
|
|
1939
|
-
const combustivel = fields.find((f) => /combustivel|fuel/.test(fieldHaystack(f)) && !used.has(f.id));
|
|
1940
|
-
if (combustivel && /flex|gasoline|diesel|hybrid|electric|el[eé]trico/.test(normalizedValue)) {
|
|
1941
|
-
return combustivel.id;
|
|
1942
|
-
}
|
|
1943
|
-
const nextText = fields.find((field) => {
|
|
1944
|
-
if (used.has(field.id)) return false;
|
|
1945
|
-
const type = normalize4(field.type ?? "text");
|
|
1946
|
-
return type === "text" || type === "input" || type === "number" || type === "";
|
|
1947
|
-
});
|
|
1948
|
-
return nextText?.id;
|
|
1603
|
+
const shape = readValueShape(value);
|
|
1604
|
+
const typed = available.find((field) => fieldTypeMatchesShape(field, shape));
|
|
1605
|
+
if (typed) return typed.id;
|
|
1606
|
+
return available.find((field) => fieldAcceptsFreeText(field, shape))?.id;
|
|
1949
1607
|
}
|
|
1950
1608
|
function rewriteMisplacedFormFillFields(fields, registry) {
|
|
1951
1609
|
const knownFields = collectKnownFormFields(registry);
|
|
@@ -1970,28 +1628,36 @@ function rewriteMisplacedFormFillFields(fields, registry) {
|
|
|
1970
1628
|
}
|
|
1971
1629
|
|
|
1972
1630
|
// src/executor/formValidation.ts
|
|
1973
|
-
var
|
|
1974
|
-
var FORM_SUCCESS_TEXT_RE =
|
|
1631
|
+
var FORM_VALIDATION_TEXT_RE2 = FORM_VALIDATION_TEXT_RE;
|
|
1632
|
+
var FORM_SUCCESS_TEXT_RE = FORM_WRITE_SUCCESS_TEXT_RE;
|
|
1975
1633
|
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
]
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1634
|
+
function inferValidationFieldId(message, inputs) {
|
|
1635
|
+
const haystack = normalizeText(message);
|
|
1636
|
+
if (!haystack || !inputs?.length) return void 0;
|
|
1637
|
+
let best;
|
|
1638
|
+
for (const input of inputs) {
|
|
1639
|
+
const id = input?.id;
|
|
1640
|
+
if (!id) continue;
|
|
1641
|
+
for (const naming of [input.label, input.placeholder, id]) {
|
|
1642
|
+
const needle = normalizeText(naming ?? "");
|
|
1643
|
+
if (needle.length < 3) continue;
|
|
1644
|
+
if (!containsWord(haystack, needle)) continue;
|
|
1645
|
+
if (!best || needle.length > best.length) best = { id, length: needle.length };
|
|
1646
|
+
}
|
|
1987
1647
|
}
|
|
1988
|
-
return
|
|
1648
|
+
return best?.id;
|
|
1649
|
+
}
|
|
1650
|
+
function normalizeText(value) {
|
|
1651
|
+
return String(value ?? "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
1652
|
+
}
|
|
1653
|
+
function containsWord(haystack, needle) {
|
|
1654
|
+
return ` ${haystack} `.includes(` ${needle} `);
|
|
1989
1655
|
}
|
|
1990
1656
|
function readVisibleFormValidation(snap) {
|
|
1991
1657
|
if (!snap) return void 0;
|
|
1992
1658
|
for (const text of snap.texts ?? []) {
|
|
1993
|
-
if (!text?.trim() || !
|
|
1994
|
-
return { fieldId: inferValidationFieldId(text), message: text };
|
|
1659
|
+
if (!text?.trim() || !FORM_VALIDATION_TEXT_RE2.test(text)) continue;
|
|
1660
|
+
return { fieldId: inferValidationFieldId(text, snap.inputs), message: text };
|
|
1995
1661
|
}
|
|
1996
1662
|
return void 0;
|
|
1997
1663
|
}
|
|
@@ -2213,7 +1879,9 @@ function resolveComponentId(componentId, registry, action) {
|
|
|
2213
1879
|
const snapshot = registry.snapshot();
|
|
2214
1880
|
for (const comp of snapshot) {
|
|
2215
1881
|
if (comp.id.toLowerCase() === lower) {
|
|
2216
|
-
|
|
1882
|
+
appilotsDebugLog(
|
|
1883
|
+
`resolveComponentId: case-insensitive match "${componentId}" \u2192 "${comp.id}"`
|
|
1884
|
+
);
|
|
2217
1885
|
return comp.id;
|
|
2218
1886
|
}
|
|
2219
1887
|
}
|
|
@@ -2228,12 +1896,16 @@ function resolveComponentId(componentId, registry, action) {
|
|
|
2228
1896
|
if (metaAction && typeof metaAction === "object" && metaAction.label) {
|
|
2229
1897
|
const metaLabel = metaAction.label;
|
|
2230
1898
|
const normalizedMetaLabel = metaLabel.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
|
|
2231
|
-
|
|
1899
|
+
appilotsDebugLog(
|
|
1900
|
+
`resolveComponentId: found screen metadata for "${componentId}" \u2192 label="${metaLabel}"`
|
|
1901
|
+
);
|
|
2232
1902
|
for (const comp of snapshot) {
|
|
2233
1903
|
if (!comp.label) continue;
|
|
2234
1904
|
const compLabel = comp.label.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
|
|
2235
1905
|
if (compLabel === normalizedMetaLabel || compLabel.includes(normalizedMetaLabel) || normalizedMetaLabel.includes(compLabel)) {
|
|
2236
|
-
|
|
1906
|
+
appilotsDebugLog(
|
|
1907
|
+
`resolveComponentId: FOUND via metadata: "${componentId}" \u2192 "${comp.id}" (label="${comp.label}")`
|
|
1908
|
+
);
|
|
2237
1909
|
return comp.id;
|
|
2238
1910
|
}
|
|
2239
1911
|
}
|
|
@@ -2248,8 +1920,8 @@ function resolveComponentId(componentId, registry, action) {
|
|
|
2248
1920
|
}));
|
|
2249
1921
|
const match = resolveTarget(componentId, candidates, { action });
|
|
2250
1922
|
if (match.status === "matched") {
|
|
2251
|
-
|
|
2252
|
-
`
|
|
1923
|
+
appilotsDebugLog(
|
|
1924
|
+
`resolveComponentId: matched "${componentId}" -> "${match.id}" (tier=${match.tier})`
|
|
2253
1925
|
);
|
|
2254
1926
|
return match.id;
|
|
2255
1927
|
}
|
|
@@ -2338,8 +2010,8 @@ function setValueHandler(rawComponentId, requested, registry) {
|
|
|
2338
2010
|
try {
|
|
2339
2011
|
slider.setValue(applied);
|
|
2340
2012
|
if (applied !== requestedValue) {
|
|
2341
|
-
|
|
2342
|
-
`
|
|
2013
|
+
appilotsDebugLog(
|
|
2014
|
+
`uiInteractionHandler: set_value snapped ${requestedValue} \u2192 ${applied} for "${componentId}" (step=${step})`
|
|
2343
2015
|
);
|
|
2344
2016
|
}
|
|
2345
2017
|
return { success: true, effect: "changed" };
|
|
@@ -2373,6 +2045,21 @@ function checkModalOcclusion(rawComponentId, action) {
|
|
|
2373
2045
|
return null;
|
|
2374
2046
|
}
|
|
2375
2047
|
if (typeof rawComponentId !== "string" || rawComponentId.trim().length === 0) return null;
|
|
2048
|
+
const dialog = getOpenNativeDialog();
|
|
2049
|
+
if (dialog) {
|
|
2050
|
+
const needle = normalizeIdent(rawComponentId);
|
|
2051
|
+
if (dialog.buttons.some((b) => normalizeIdent(b.label) === needle)) return null;
|
|
2052
|
+
return {
|
|
2053
|
+
success: false,
|
|
2054
|
+
error: `"${rawComponentId}" is behind a native dialog${dialog.title ? ` ("${dialog.title}")` : ""} and cannot receive touches. Answer the dialog first \u2014 press one of: ` + dialog.buttons.map((b) => b.label).join(", "),
|
|
2055
|
+
diagnose: {
|
|
2056
|
+
category: "ambiguous-target",
|
|
2057
|
+
targetId: rawComponentId,
|
|
2058
|
+
screen: getCurrentScreen() ?? void 0,
|
|
2059
|
+
candidates: dialog.buttons.map((b) => b.label).slice(0, 8)
|
|
2060
|
+
}
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2376
2063
|
const fiberRoot = getFiberRoot();
|
|
2377
2064
|
if (!fiberRoot) return null;
|
|
2378
2065
|
let snap;
|
|
@@ -2404,9 +2091,12 @@ function checkModalOcclusion(rawComponentId, action) {
|
|
|
2404
2091
|
modalOptionLabels.push(button.label ?? button.id);
|
|
2405
2092
|
}
|
|
2406
2093
|
}
|
|
2407
|
-
for (const toggle of snap.toggles ?? [])
|
|
2408
|
-
|
|
2409
|
-
for (const
|
|
2094
|
+
for (const toggle of snap.toggles ?? [])
|
|
2095
|
+
add(toggle.inModal ? inModal : outside, toggle.id, toggle.label);
|
|
2096
|
+
for (const input of snap.inputs ?? [])
|
|
2097
|
+
add(input.inModal ? inModal : outside, input.id, input.label);
|
|
2098
|
+
for (const slider of snap.sliders ?? [])
|
|
2099
|
+
add(slider.inModal ? inModal : outside, slider.id, slider.label);
|
|
2410
2100
|
for (const list of snap.lists ?? []) {
|
|
2411
2101
|
for (const item of list.items ?? []) {
|
|
2412
2102
|
const itemInModal = (item.buttons ?? []).some((b) => b.inModal) || (item.inputs ?? []).some((i) => i.inModal) || (item.toggles ?? []).some((t) => t.inModal);
|
|
@@ -2441,7 +2131,10 @@ function uiInteractionHandler(payload, context) {
|
|
|
2441
2131
|
return {
|
|
2442
2132
|
success: false,
|
|
2443
2133
|
error: "UI interaction is not permitted",
|
|
2444
|
-
diagnose: buildUiDiagnose(
|
|
2134
|
+
diagnose: buildUiDiagnose(
|
|
2135
|
+
payload.componentId ?? payload.targetId ?? "",
|
|
2136
|
+
"unknown"
|
|
2137
|
+
)
|
|
2445
2138
|
};
|
|
2446
2139
|
}
|
|
2447
2140
|
const { action } = payload;
|
|
@@ -2455,8 +2148,8 @@ function uiInteractionHandler(payload, context) {
|
|
|
2455
2148
|
if (sliderId) {
|
|
2456
2149
|
const smuggled = payload.value ?? payload.params?.value;
|
|
2457
2150
|
if (smuggled !== void 0) {
|
|
2458
|
-
|
|
2459
|
-
`
|
|
2151
|
+
appilotsDebugLog(
|
|
2152
|
+
`uiInteractionHandler: "${actionStr}" on slider "${sliderId}" carried value=${JSON.stringify(smuggled)} \u2014 routing to set_value`
|
|
2460
2153
|
);
|
|
2461
2154
|
return setValueHandler(rawComponentId, smuggled, registry);
|
|
2462
2155
|
}
|
|
@@ -2466,18 +2159,53 @@ function uiInteractionHandler(payload, context) {
|
|
|
2466
2159
|
diagnose: { ...buildUiDiagnose(sliderId, "unknown"), recoverable: true }
|
|
2467
2160
|
};
|
|
2468
2161
|
}
|
|
2162
|
+
const openDialog = getOpenNativeDialog();
|
|
2163
|
+
const isPressVerb = actionStr === "press" || actionStr === "tap" || actionStr === "click";
|
|
2164
|
+
if (openDialog && isPressVerb) {
|
|
2165
|
+
const needle = normalizeIdent(rawComponentId);
|
|
2166
|
+
const button = openDialog.buttons.find((b) => normalizeIdent(b.label) === needle);
|
|
2167
|
+
if (button) {
|
|
2168
|
+
button.press();
|
|
2169
|
+
appilotsDebugLog(`uiInteractionHandler: pressed native dialog button "${button.label}"`);
|
|
2170
|
+
return { success: true, effect: "changed" };
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
if (!openDialog && isPressVerb) {
|
|
2174
|
+
const answered = getLastAnsweredNativeDialog();
|
|
2175
|
+
const needle = normalizeIdent(rawComponentId);
|
|
2176
|
+
if (answered && needle) {
|
|
2177
|
+
if (answered.answeredWith && normalizeIdent(answered.answeredWith) === needle) {
|
|
2178
|
+
appilotsDebugLog(
|
|
2179
|
+
`uiInteractionHandler: native dialog button "${answered.answeredWith}" was already pressed and the dialog is closed \u2014 treating the repeat as done`
|
|
2180
|
+
);
|
|
2181
|
+
return { success: true, effect: "completed" };
|
|
2182
|
+
}
|
|
2183
|
+
if (answered.buttons.some((label) => normalizeIdent(label) === needle)) {
|
|
2184
|
+
return {
|
|
2185
|
+
success: false,
|
|
2186
|
+
error: `The dialog${answered.title ? ` ("${answered.title}")` : ""} was already answered with "${answered.answeredWith ?? "dismiss"}", so "${rawComponentId}" is no longer on screen. Read the current screen before acting again.`,
|
|
2187
|
+
diagnose: {
|
|
2188
|
+
category: "component-not-found",
|
|
2189
|
+
targetId: rawComponentId,
|
|
2190
|
+
screen: getCurrentScreen() ?? void 0,
|
|
2191
|
+
recoverable: false
|
|
2192
|
+
}
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2469
2197
|
const occluded = checkModalOcclusion(rawComponentId, actionStr);
|
|
2470
2198
|
if (occluded) {
|
|
2471
|
-
|
|
2472
|
-
`
|
|
2199
|
+
appilotsDebugLog(
|
|
2200
|
+
`uiInteractionHandler: blocked "${actionStr}" on "${rawComponentId}" \u2014 target is behind an open modal`
|
|
2473
2201
|
);
|
|
2474
2202
|
return occluded;
|
|
2475
2203
|
}
|
|
2476
2204
|
if (typeof rawComponentId === "string" && rawComponentId.trim().toLowerCase().startsWith("el:")) {
|
|
2477
2205
|
const result = dispatchElementOrTarget(rawComponentId, registry, actionStr);
|
|
2478
2206
|
if (result.success) {
|
|
2479
|
-
|
|
2480
|
-
`
|
|
2207
|
+
appilotsDebugLog(
|
|
2208
|
+
`uiInteractionHandler: interaction element dispatch succeeded for "${rawComponentId}"`
|
|
2481
2209
|
);
|
|
2482
2210
|
return { success: true };
|
|
2483
2211
|
}
|
|
@@ -2485,13 +2213,13 @@ function uiInteractionHandler(payload, context) {
|
|
|
2485
2213
|
}
|
|
2486
2214
|
const ordinal = parseSyntheticListItemId(rawComponentId);
|
|
2487
2215
|
if (ordinal && (action === "press" || actionStr === "tap" || actionStr === "click" || actionStr === "longPress")) {
|
|
2488
|
-
|
|
2489
|
-
`
|
|
2216
|
+
appilotsDebugLog(
|
|
2217
|
+
`uiInteractionHandler: dispatching synthetic list ordinal \u2014 componentId="${rawComponentId}" list=${ordinal.listIndex} item=${ordinal.itemIndex}`
|
|
2490
2218
|
);
|
|
2491
2219
|
const result = pressListItemAtOrdinal(ordinal.listIndex, ordinal.itemIndex);
|
|
2492
2220
|
if (result.ok) {
|
|
2493
|
-
|
|
2494
|
-
`
|
|
2221
|
+
appilotsDebugLog(
|
|
2222
|
+
`uiInteractionHandler: synthetic ordinal press succeeded (container=${result.containerType ?? "List"})`
|
|
2495
2223
|
);
|
|
2496
2224
|
return { success: true };
|
|
2497
2225
|
}
|
|
@@ -2536,7 +2264,9 @@ function uiInteractionHandler(payload, context) {
|
|
|
2536
2264
|
};
|
|
2537
2265
|
}
|
|
2538
2266
|
const componentId = resolveComponentId(rawComponentId, registry, actionStr);
|
|
2539
|
-
|
|
2267
|
+
appilotsDebugLog(
|
|
2268
|
+
`uiInteractionHandler: resolved componentId="${payload.componentId}" \u2192 "${componentId}"`
|
|
2269
|
+
);
|
|
2540
2270
|
const target = registry.getTarget(componentId);
|
|
2541
2271
|
if (target) {
|
|
2542
2272
|
try {
|
|
@@ -2587,7 +2317,9 @@ function uiInteractionHandler(payload, context) {
|
|
|
2587
2317
|
return { success: false, error: err?.message ?? "Focus failed" };
|
|
2588
2318
|
}
|
|
2589
2319
|
}
|
|
2590
|
-
|
|
2320
|
+
appilotsDebugLog(
|
|
2321
|
+
`uiInteractionHandler: registry miss for "${componentId}", falling back to fiber tree`
|
|
2322
|
+
);
|
|
2591
2323
|
const handle = findInteractiveByIdentifier(componentId, {
|
|
2592
2324
|
kind: actionStr === "toggle" ? "toggle" : actionStr === "focus" ? "field" : "target"
|
|
2593
2325
|
});
|
|
@@ -2595,7 +2327,9 @@ function uiInteractionHandler(payload, context) {
|
|
|
2595
2327
|
try {
|
|
2596
2328
|
if (actionStr === "press" && handle.kind === "target" && handle.press) {
|
|
2597
2329
|
handle.press();
|
|
2598
|
-
|
|
2330
|
+
appilotsDebugLog(
|
|
2331
|
+
`uiInteractionHandler: fiber fallback press succeeded for "${componentId}"`
|
|
2332
|
+
);
|
|
2599
2333
|
return { success: true };
|
|
2600
2334
|
}
|
|
2601
2335
|
if (actionStr === "press" && handle.kind === "toggle" && handle.setValue) {
|
|
@@ -2741,12 +2475,10 @@ function readVisibleValidation(fieldId, label) {
|
|
|
2741
2475
|
try {
|
|
2742
2476
|
const snap = captureSnapshot();
|
|
2743
2477
|
const texts = snap.texts ?? [];
|
|
2744
|
-
const needles = [fieldId, label].filter(
|
|
2745
|
-
(s) => !!s && s.length > 0
|
|
2746
|
-
);
|
|
2478
|
+
const needles = [fieldId, label].filter((s) => !!s && s.length > 0);
|
|
2747
2479
|
for (const text of texts) {
|
|
2748
2480
|
const lower = text.toLowerCase();
|
|
2749
|
-
if (
|
|
2481
|
+
if (FORM_VALIDATION_TEXT_RE2.test(lower)) {
|
|
2750
2482
|
for (const needle of needles) {
|
|
2751
2483
|
if (lower.includes(needle.toLowerCase())) return text;
|
|
2752
2484
|
}
|
|
@@ -2867,8 +2599,10 @@ async function tryPressSelectOption(fieldId, value, registry, options = {}) {
|
|
|
2867
2599
|
function normalizeFieldType(type) {
|
|
2868
2600
|
if (typeof type !== "string") return void 0;
|
|
2869
2601
|
const normalized = normalize3(type);
|
|
2870
|
-
if (normalized === "select" || normalized === "picker" || normalized === "dropdown")
|
|
2871
|
-
|
|
2602
|
+
if (normalized === "select" || normalized === "picker" || normalized === "dropdown")
|
|
2603
|
+
return "select";
|
|
2604
|
+
if (normalized === "toggle" || normalized === "switch" || normalized === "checkbox")
|
|
2605
|
+
return "toggle";
|
|
2872
2606
|
if (normalized === "date") return "date";
|
|
2873
2607
|
if (normalized === "number" || normalized === "numeric") return "number";
|
|
2874
2608
|
if (normalized === "custom") return "custom";
|
|
@@ -2902,10 +2636,7 @@ function fieldCueTerms(fieldId, label) {
|
|
|
2902
2636
|
function snapshotHasFieldCue(snap, fieldId, label) {
|
|
2903
2637
|
const terms = fieldCueTerms(fieldId, label);
|
|
2904
2638
|
if (terms.length === 0) return false;
|
|
2905
|
-
const visible = [
|
|
2906
|
-
...snap.texts ?? [],
|
|
2907
|
-
...(snap.buttons ?? []).map((b) => b.label ?? b.id ?? "")
|
|
2908
|
-
].map((text) => normalize3(text)).filter(Boolean);
|
|
2639
|
+
const visible = [...snap.texts ?? [], ...(snap.buttons ?? []).map((b) => b.label ?? b.id ?? "")].map((text) => normalize3(text)).filter(Boolean);
|
|
2909
2640
|
return visible.some((text) => terms.some((term) => text.includes(term) || term.includes(text)));
|
|
2910
2641
|
}
|
|
2911
2642
|
function scoreCandidateText(value, texts) {
|
|
@@ -2979,9 +2710,7 @@ function buildChoiceCandidates(snap, fieldId, value, allowFirstVisibleWhenNoCue,
|
|
|
2979
2710
|
fieldCueVisible && elements.length <= 12 ? 50 : 0,
|
|
2980
2711
|
...cueTexts.map((text) => {
|
|
2981
2712
|
const normalized = normalize3(text);
|
|
2982
|
-
return fieldTerms.some(
|
|
2983
|
-
(term) => normalized.includes(term) || term.includes(normalized)
|
|
2984
|
-
) ? 80 : 0;
|
|
2713
|
+
return fieldTerms.some((term) => normalized.includes(term) || term.includes(normalized)) ? 80 : 0;
|
|
2985
2714
|
})
|
|
2986
2715
|
);
|
|
2987
2716
|
const valueScore = genericValue ? 0 : scoreCandidateText(value, texts);
|
|
@@ -3053,7 +2782,11 @@ async function selectVisibleChoiceAsync(fieldId, value, registry, options) {
|
|
|
3053
2782
|
return {
|
|
3054
2783
|
success: false,
|
|
3055
2784
|
error: `No visible option found for select field "${fieldId}"`,
|
|
3056
|
-
diagnose: {
|
|
2785
|
+
diagnose: {
|
|
2786
|
+
category: "component-not-found",
|
|
2787
|
+
fieldId,
|
|
2788
|
+
screen: getCurrentScreen() ?? void 0
|
|
2789
|
+
}
|
|
3057
2790
|
};
|
|
3058
2791
|
}
|
|
3059
2792
|
const pressed = pressTargetByIdOrLabel(candidate.targetId, registry);
|
|
@@ -3085,17 +2818,19 @@ function pressContinuationButton(registry) {
|
|
|
3085
2818
|
return pressTargetByIdOrLabel(id, registry);
|
|
3086
2819
|
}
|
|
3087
2820
|
function findField(fieldId, registry) {
|
|
3088
|
-
|
|
2821
|
+
appilotsDebugLog(`findField: searching for fieldId="${fieldId}"`);
|
|
3089
2822
|
const exact = registry.getField(fieldId);
|
|
3090
2823
|
if (exact) {
|
|
3091
|
-
|
|
2824
|
+
appilotsDebugLog(`findField: FOUND exact match for fieldId="${fieldId}"`);
|
|
3092
2825
|
return exact;
|
|
3093
2826
|
}
|
|
3094
2827
|
const lower = fieldId.toLowerCase();
|
|
3095
2828
|
const snapshot = registry.snapshot();
|
|
3096
2829
|
for (const comp of snapshot) {
|
|
3097
2830
|
if (comp.kind === "field" && comp.id.toLowerCase() === lower) {
|
|
3098
|
-
|
|
2831
|
+
appilotsDebugLog(
|
|
2832
|
+
`findField: FOUND case-insensitive match: registered="${comp.id}" for searched="${fieldId}"`
|
|
2833
|
+
);
|
|
3099
2834
|
return registry.getField(comp.id);
|
|
3100
2835
|
}
|
|
3101
2836
|
}
|
|
@@ -3123,8 +2858,8 @@ function findField(fieldId, registry) {
|
|
|
3123
2858
|
}
|
|
3124
2859
|
}
|
|
3125
2860
|
if (bestComp && bestScore >= 75) {
|
|
3126
|
-
|
|
3127
|
-
`
|
|
2861
|
+
appilotsDebugLog(
|
|
2862
|
+
`findField: scored match "${fieldId}" -> "${bestComp.id}" (label="${bestComp.label}", score=${bestScore})`
|
|
3128
2863
|
);
|
|
3129
2864
|
return registry.getField(bestComp.id);
|
|
3130
2865
|
}
|
|
@@ -3150,8 +2885,8 @@ function findField(fieldId, registry) {
|
|
|
3150
2885
|
}
|
|
3151
2886
|
}
|
|
3152
2887
|
if (mBest && mBestScore >= 60) {
|
|
3153
|
-
|
|
3154
|
-
`
|
|
2888
|
+
appilotsDebugLog(
|
|
2889
|
+
`findField: FOUND via screen metadata translation: fieldId="${fieldId}" -> metaLabel="${metaLabel}" -> registered="${mBest.id}" (score=${mBestScore})`
|
|
3155
2890
|
);
|
|
3156
2891
|
return registry.getField(mBest.id);
|
|
3157
2892
|
}
|
|
@@ -3159,8 +2894,8 @@ function findField(fieldId, registry) {
|
|
|
3159
2894
|
}
|
|
3160
2895
|
}
|
|
3161
2896
|
if (bestComp && bestScore >= 40) {
|
|
3162
|
-
|
|
3163
|
-
`
|
|
2897
|
+
appilotsDebugLog(
|
|
2898
|
+
`findField: weak substring match "${fieldId}" -> "${bestComp.id}" (label="${bestComp.label}", score=${bestScore})`
|
|
3164
2899
|
);
|
|
3165
2900
|
return registry.getField(bestComp.id);
|
|
3166
2901
|
}
|
|
@@ -3170,17 +2905,19 @@ function findField(fieldId, registry) {
|
|
|
3170
2905
|
return void 0;
|
|
3171
2906
|
}
|
|
3172
2907
|
function findToggle(fieldId, registry) {
|
|
3173
|
-
|
|
2908
|
+
appilotsDebugLog(`findToggle: searching for fieldId="${fieldId}"`);
|
|
3174
2909
|
const exact = registry.getToggle(fieldId);
|
|
3175
2910
|
if (exact) {
|
|
3176
|
-
|
|
2911
|
+
appilotsDebugLog(`findToggle: FOUND exact match for "${fieldId}"`);
|
|
3177
2912
|
return exact;
|
|
3178
2913
|
}
|
|
3179
2914
|
const lower = fieldId.toLowerCase();
|
|
3180
2915
|
const snapshot = registry.snapshot();
|
|
3181
2916
|
for (const comp of snapshot) {
|
|
3182
2917
|
if (comp.kind === "toggle" && comp.id.toLowerCase() === lower) {
|
|
3183
|
-
|
|
2918
|
+
appilotsDebugLog(
|
|
2919
|
+
`findToggle: FOUND case-insensitive match: registered="${comp.id}" for searched="${fieldId}"`
|
|
2920
|
+
);
|
|
3184
2921
|
return registry.getToggle(comp.id);
|
|
3185
2922
|
}
|
|
3186
2923
|
}
|
|
@@ -3195,7 +2932,9 @@ function findSlider(fieldId, registry) {
|
|
|
3195
2932
|
const lower = fieldId.toLowerCase();
|
|
3196
2933
|
for (const comp of registry.snapshot()) {
|
|
3197
2934
|
if (comp.kind === "slider" && comp.id.toLowerCase() === lower) {
|
|
3198
|
-
|
|
2935
|
+
appilotsDebugLog(
|
|
2936
|
+
`findSlider: FOUND case-insensitive match: registered="${comp.id}" for searched="${fieldId}"`
|
|
2937
|
+
);
|
|
3199
2938
|
return registry.getSlider(comp.id);
|
|
3200
2939
|
}
|
|
3201
2940
|
}
|
|
@@ -3227,7 +2966,7 @@ async function formFillHandler(payload, context) {
|
|
|
3227
2966
|
"formFillHandler: registry is EMPTY \u2014 auto-tracking may not be active. Ensure enableAppilotsAutoTracking() runs before components mount."
|
|
3228
2967
|
);
|
|
3229
2968
|
} else {
|
|
3230
|
-
|
|
2969
|
+
appilotsDebugLog(
|
|
3231
2970
|
`[Appilots formFillHandler] Registry has ${registrySnapshot.length} components:`,
|
|
3232
2971
|
registrySnapshot.map((c) => `${c.kind}:${c.id}`).join(", ")
|
|
3233
2972
|
);
|
|
@@ -3235,7 +2974,7 @@ async function formFillHandler(payload, context) {
|
|
|
3235
2974
|
const fieldResults = [];
|
|
3236
2975
|
let allSuccess = true;
|
|
3237
2976
|
let selectedChoiceThisBatch = false;
|
|
3238
|
-
|
|
2977
|
+
appilotsDebugLog(`formFillHandler: processing ${payload.fields.length} fields`);
|
|
3239
2978
|
const availableFieldIds = () => registry.snapshot().filter((comp) => comp.kind === "field").map((comp) => `${comp.id}${comp.label ? ` (${comp.label})` : ""}`).slice(0, 8);
|
|
3240
2979
|
const isElementIdField = (field) => typeof field.fieldId === "string" && field.fieldId.trim().toLowerCase().startsWith("el:");
|
|
3241
2980
|
for (const field of payload.fields.filter(isElementIdField)) {
|
|
@@ -3262,8 +3001,8 @@ async function formFillHandler(payload, context) {
|
|
|
3262
3001
|
const { fieldId, fieldType, value } = field;
|
|
3263
3002
|
const effectiveFieldType = getEffectiveFieldType(fieldId, fieldType, registry);
|
|
3264
3003
|
let selectFailure;
|
|
3265
|
-
|
|
3266
|
-
`
|
|
3004
|
+
appilotsDebugLog(
|
|
3005
|
+
`formFillHandler: looking up fieldId="${fieldId}" fieldType="${fieldType}" effectiveFieldType="${effectiveFieldType}" value=${describeValue(value)}`
|
|
3267
3006
|
);
|
|
3268
3007
|
const fieldEntry = findField(fieldId, registry);
|
|
3269
3008
|
if (fieldEntry) {
|
|
@@ -3314,7 +3053,11 @@ async function formFillHandler(payload, context) {
|
|
|
3314
3053
|
fieldId,
|
|
3315
3054
|
success: false,
|
|
3316
3055
|
error: selectResult.error ?? `Select value "${String(value)}" was not applied for "${fieldId}"`,
|
|
3317
|
-
diagnose: selectResult.diagnose ?? {
|
|
3056
|
+
diagnose: selectResult.diagnose ?? {
|
|
3057
|
+
category: "component-not-found",
|
|
3058
|
+
fieldId,
|
|
3059
|
+
screen: screenPath
|
|
3060
|
+
}
|
|
3318
3061
|
});
|
|
3319
3062
|
allSuccess = false;
|
|
3320
3063
|
continue;
|
|
@@ -3369,7 +3112,9 @@ async function formFillHandler(payload, context) {
|
|
|
3369
3112
|
continue;
|
|
3370
3113
|
}
|
|
3371
3114
|
try {
|
|
3372
|
-
sliderEntry.setValue(
|
|
3115
|
+
sliderEntry.setValue(
|
|
3116
|
+
snapSliderValue(numeric, sliderEntry.min, sliderEntry.max, sliderEntry.step)
|
|
3117
|
+
);
|
|
3373
3118
|
fieldResults.push({ fieldId, success: true });
|
|
3374
3119
|
} catch (err) {
|
|
3375
3120
|
fieldResults.push({
|
|
@@ -3418,8 +3163,8 @@ async function formFillHandler(payload, context) {
|
|
|
3418
3163
|
}
|
|
3419
3164
|
if (selectResult.success) {
|
|
3420
3165
|
selectedChoiceThisBatch = true;
|
|
3421
|
-
|
|
3422
|
-
`
|
|
3166
|
+
appilotsDebugLog(
|
|
3167
|
+
`formFillHandler: selected visible option for "${fieldId}"` + (selectResult.selectedLabel ? ` \u2192 "${selectResult.selectedLabel}"` : "")
|
|
3423
3168
|
);
|
|
3424
3169
|
await sleep2(120);
|
|
3425
3170
|
fieldResults.push({ fieldId, success: true });
|
|
@@ -3427,7 +3172,7 @@ async function formFillHandler(payload, context) {
|
|
|
3427
3172
|
}
|
|
3428
3173
|
selectFailure = selectResult;
|
|
3429
3174
|
}
|
|
3430
|
-
|
|
3175
|
+
appilotsDebugLog(`formFillHandler: registry miss for "${fieldId}", falling back to fiber tree`);
|
|
3431
3176
|
const handle = findInteractiveByIdentifier(fieldId, {
|
|
3432
3177
|
kind: effectiveFieldType === "toggle" ? "toggle" : "field"
|
|
3433
3178
|
});
|
|
@@ -3435,7 +3180,9 @@ async function formFillHandler(payload, context) {
|
|
|
3435
3180
|
try {
|
|
3436
3181
|
handle.focus?.();
|
|
3437
3182
|
handle.setValue(value);
|
|
3438
|
-
|
|
3183
|
+
appilotsDebugLog(
|
|
3184
|
+
`formFillHandler: fiber fallback succeeded for "${fieldId}" \u2192 label="${handle.label}"`
|
|
3185
|
+
);
|
|
3439
3186
|
fieldResults.push({ fieldId, success: true });
|
|
3440
3187
|
continue;
|
|
3441
3188
|
} catch (err) {
|
|
@@ -3477,7 +3224,15 @@ async function formFillHandler(payload, context) {
|
|
|
3477
3224
|
);
|
|
3478
3225
|
} else {
|
|
3479
3226
|
await sleep2(120);
|
|
3480
|
-
const submitResult = pressSubmitButton(
|
|
3227
|
+
const submitResult = pressSubmitButton(
|
|
3228
|
+
{
|
|
3229
|
+
screen,
|
|
3230
|
+
screenPath,
|
|
3231
|
+
activePath,
|
|
3232
|
+
filledFieldIds: fieldResults.filter((r) => r.success).map((r) => r.fieldId)
|
|
3233
|
+
},
|
|
3234
|
+
registry
|
|
3235
|
+
);
|
|
3481
3236
|
if (!submitResult.success) {
|
|
3482
3237
|
appilotsDebugWarn(`formFillHandler: submitAfterFill failed \u2014 ${submitResult.error}`);
|
|
3483
3238
|
return {
|
|
@@ -3515,7 +3270,7 @@ async function formFillHandler(payload, context) {
|
|
|
3515
3270
|
}
|
|
3516
3271
|
};
|
|
3517
3272
|
}
|
|
3518
|
-
|
|
3273
|
+
appilotsDebugLog("formFillHandler: submitAfterFill \u2014 submit completed");
|
|
3519
3274
|
}
|
|
3520
3275
|
}
|
|
3521
3276
|
const aggregatedDiagnose = aggregateFieldDiagnoses(fieldResults, screenPath);
|
|
@@ -3537,8 +3292,8 @@ function pressSubmitButton(context, registry) {
|
|
|
3537
3292
|
if (target) {
|
|
3538
3293
|
try {
|
|
3539
3294
|
target.press();
|
|
3540
|
-
|
|
3541
|
-
`
|
|
3295
|
+
appilotsDebugLog(
|
|
3296
|
+
`formFillHandler: submitAfterFill \u2014 pressed registry target "${candidate}"` + (resolvedScreen ? ` on screen "${resolvedScreen}"` : "")
|
|
3542
3297
|
);
|
|
3543
3298
|
return { success: true, pressedTargetId: candidate };
|
|
3544
3299
|
} catch (err) {
|
|
@@ -3562,9 +3317,7 @@ function pressSubmitButton(context, registry) {
|
|
|
3562
3317
|
if (handle?.press) {
|
|
3563
3318
|
try {
|
|
3564
3319
|
handle.press();
|
|
3565
|
-
|
|
3566
|
-
`[Appilots] formFillHandler: submitAfterFill \u2014 fiber fallback "${submitLabel}"`
|
|
3567
|
-
);
|
|
3320
|
+
appilotsDebugLog(`formFillHandler: submitAfterFill \u2014 fiber fallback "${submitLabel}"`);
|
|
3568
3321
|
return { success: true, pressedTargetId: submitId ?? submitLabel };
|
|
3569
3322
|
} catch (err) {
|
|
3570
3323
|
return {
|
|
@@ -3586,9 +3339,7 @@ function pressSubmitButton(context, registry) {
|
|
|
3586
3339
|
if (id) {
|
|
3587
3340
|
const pressed = pressTargetByIdOrLabel(id, registry);
|
|
3588
3341
|
if (pressed.success) {
|
|
3589
|
-
|
|
3590
|
-
`[Appilots] formFillHandler: submitAfterFill \u2014 pressed visible heuristic "${id}"`
|
|
3591
|
-
);
|
|
3342
|
+
appilotsDebugLog(`formFillHandler: submitAfterFill \u2014 pressed visible heuristic "${id}"`);
|
|
3592
3343
|
return { success: true, pressedTargetId: id };
|
|
3593
3344
|
}
|
|
3594
3345
|
}
|
|
@@ -3602,8 +3353,8 @@ function pressSubmitButton(context, registry) {
|
|
|
3602
3353
|
if (target) {
|
|
3603
3354
|
try {
|
|
3604
3355
|
target.press();
|
|
3605
|
-
|
|
3606
|
-
`
|
|
3356
|
+
appilotsDebugLog(
|
|
3357
|
+
`formFillHandler: submitAfterFill \u2014 pressed by label heuristic "${comp.label}"`
|
|
3607
3358
|
);
|
|
3608
3359
|
return { success: true, pressedTargetId: comp.id };
|
|
3609
3360
|
} catch (err) {
|
|
@@ -3615,9 +3366,16 @@ function pressSubmitButton(context, registry) {
|
|
|
3615
3366
|
}
|
|
3616
3367
|
}
|
|
3617
3368
|
}
|
|
3369
|
+
for (const fieldId of [...context.filledFieldIds ?? []].reverse()) {
|
|
3370
|
+
const handle = findInteractiveByIdentifier(fieldId, { kind: "field" });
|
|
3371
|
+
if (handle?.submitFromKeyboard?.()) {
|
|
3372
|
+
appilotsDebugLog(`formFillHandler: submitAfterFill \u2014 return key on "${fieldId}"`);
|
|
3373
|
+
return { success: true, pressedTargetId: fieldId };
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3618
3376
|
return {
|
|
3619
3377
|
success: false,
|
|
3620
|
-
error: 'submitAfterFill requested but no submit button
|
|
3378
|
+
error: 'submitAfterFill requested but the screen has no submit button and no field that submits on return (declare it via registerScreen({ actions: [{ type: "submit", ... }] }) for reliable behavior)',
|
|
3621
3379
|
diagnose: { category: "component-not-found", screen: resolvedScreen }
|
|
3622
3380
|
};
|
|
3623
3381
|
}
|
|
@@ -3671,6 +3429,8 @@ function resolveListEntry(listId) {
|
|
|
3671
3429
|
return { candidates: entries.map((e) => e.id) };
|
|
3672
3430
|
}
|
|
3673
3431
|
if (entries.length === 1) return { entry: entries[0] };
|
|
3432
|
+
const lists = entries.filter((e) => e.kind === "list");
|
|
3433
|
+
if (lists.length === 1) return { entry: lists[0] };
|
|
3674
3434
|
return { candidates: entries.map((e) => e.id) };
|
|
3675
3435
|
}
|
|
3676
3436
|
function pageOffset(metrics, direction) {
|
|
@@ -3710,7 +3470,7 @@ async function scrollListHandler(payload, context) {
|
|
|
3710
3470
|
}
|
|
3711
3471
|
const { entry, candidates } = resolveListEntry(payload.listId);
|
|
3712
3472
|
if (!entry) {
|
|
3713
|
-
const detail = candidates && candidates.length > 0 ? payload.listId ? `List "${payload.listId}" is not registered. Known lists: ${candidates.join(", ")}` : `Multiple lists are on screen \u2014 specify listId. Known lists: ${candidates.join(", ")}` : "
|
|
3473
|
+
const detail = candidates && candidates.length > 0 ? payload.listId ? `List "${payload.listId}" is not registered. Known lists: ${candidates.join(", ")}` : `Multiple lists are on screen \u2014 specify listId. Known lists: ${candidates.join(", ")}` : "Nothing scrollable is registered on the current screen (enableAppilotsAutoTracking() may be off).";
|
|
3714
3474
|
return {
|
|
3715
3475
|
success: false,
|
|
3716
3476
|
error: detail,
|
|
@@ -3718,6 +3478,13 @@ async function scrollListHandler(payload, context) {
|
|
|
3718
3478
|
};
|
|
3719
3479
|
}
|
|
3720
3480
|
if (toIndex !== void 0) {
|
|
3481
|
+
if (entry.kind === "scroll") {
|
|
3482
|
+
return {
|
|
3483
|
+
success: false,
|
|
3484
|
+
error: `"${entry.id}" is a scrollable screen, not a list \u2014 it has no numbered items. Use direction "down"/"up" to page through it.`,
|
|
3485
|
+
diagnose: buildDiagnose("validation", entry.id)
|
|
3486
|
+
};
|
|
3487
|
+
}
|
|
3721
3488
|
if (typeof entry.itemCount === "number" && toIndex > entry.itemCount) {
|
|
3722
3489
|
return {
|
|
3723
3490
|
success: false,
|
|
@@ -3752,14 +3519,14 @@ async function scrollListHandler(payload, context) {
|
|
|
3752
3519
|
}
|
|
3753
3520
|
return {
|
|
3754
3521
|
success: false,
|
|
3755
|
-
error: `
|
|
3522
|
+
error: `"${entry.id}" does not expose a scrollable handle`,
|
|
3756
3523
|
diagnose: buildDiagnose("unknown", entry.id)
|
|
3757
3524
|
};
|
|
3758
3525
|
}
|
|
3759
3526
|
|
|
3760
3527
|
// src/executor/handlers/confirmHandler.ts
|
|
3761
3528
|
async function confirmHandler(_actionId, _payload) {
|
|
3762
|
-
return { success: true };
|
|
3529
|
+
return { success: true, effect: "unknown" };
|
|
3763
3530
|
}
|
|
3764
3531
|
|
|
3765
3532
|
// src/executor/postInteractionSettle.ts
|
|
@@ -3771,14 +3538,45 @@ function targetIdFromPayload(payload) {
|
|
|
3771
3538
|
if (typeof targetId === "string" && targetId.trim()) return targetId.trim();
|
|
3772
3539
|
return "";
|
|
3773
3540
|
}
|
|
3541
|
+
function hashFieldValue(value) {
|
|
3542
|
+
let hash = 2166136261;
|
|
3543
|
+
for (let i = 0; i < value.length; i++) {
|
|
3544
|
+
hash ^= value.charCodeAt(i);
|
|
3545
|
+
hash = Math.imul(hash, 16777619);
|
|
3546
|
+
}
|
|
3547
|
+
return (hash >>> 0).toString(36);
|
|
3548
|
+
}
|
|
3549
|
+
var DISCRETE_FIELD_TYPES = /* @__PURE__ */ new Set(["select", "date"]);
|
|
3550
|
+
function discreteFieldSignature() {
|
|
3551
|
+
const sigs = [];
|
|
3552
|
+
for (const { id, kind } of componentRegistry.snapshot()) {
|
|
3553
|
+
if (kind !== "field") continue;
|
|
3554
|
+
const entry = componentRegistry.getField(id);
|
|
3555
|
+
if (!entry || !DISCRETE_FIELD_TYPES.has(entry.fieldType ?? "")) continue;
|
|
3556
|
+
try {
|
|
3557
|
+
sigs.push(`${id}:${hashFieldValue(String(entry.getValue() ?? ""))}`);
|
|
3558
|
+
} catch {
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
return sigs.sort().join(",");
|
|
3562
|
+
}
|
|
3774
3563
|
async function waitForPostInteractionSettle(payload) {
|
|
3775
3564
|
const targetId = targetIdFromPayload(payload);
|
|
3776
3565
|
const fromScreen = getCurrentScreen();
|
|
3777
3566
|
const baseline = probeLoadingState(targetId || null, fromScreen);
|
|
3567
|
+
const baselineFields = discreteFieldSignature();
|
|
3568
|
+
const readable = baseline.fingerprint !== "";
|
|
3778
3569
|
await sleepMs(100);
|
|
3779
3570
|
const afterPress = probeLoadingState(targetId || null, getCurrentScreen());
|
|
3780
3571
|
const modalOrLoading = afterPress.modalOpen || afterPress.loading || afterPress.pressedDisabled;
|
|
3781
3572
|
const fingerprintChanged = afterPress.fingerprint !== baseline.fingerprint;
|
|
3573
|
+
const verdict = () => {
|
|
3574
|
+
if (!readable) return { effect: "unknown" };
|
|
3575
|
+
const settled = probeLoadingState(targetId || null, getCurrentScreen());
|
|
3576
|
+
if (settled.fingerprint === "") return { effect: "unknown" };
|
|
3577
|
+
if (discreteFieldSignature() !== baselineFields) return { effect: "changed" };
|
|
3578
|
+
return { effect: settled.fingerprint !== baseline.fingerprint ? "changed" : "none" };
|
|
3579
|
+
};
|
|
3782
3580
|
const capMs = isListItemPressTarget(targetId) ? 5e3 : 1e4;
|
|
3783
3581
|
if (modalOrLoading || fingerprintChanged) {
|
|
3784
3582
|
await waitForLoadingSettle({
|
|
@@ -3786,7 +3584,7 @@ async function waitForPostInteractionSettle(payload) {
|
|
|
3786
3584
|
fromScreen,
|
|
3787
3585
|
maxMs: capMs
|
|
3788
3586
|
});
|
|
3789
|
-
return;
|
|
3587
|
+
return verdict();
|
|
3790
3588
|
}
|
|
3791
3589
|
if (isListItemPressTarget(targetId)) {
|
|
3792
3590
|
await waitForLoadingSettle({
|
|
@@ -3794,6 +3592,7 @@ async function waitForPostInteractionSettle(payload) {
|
|
|
3794
3592
|
maxMs: 3e3
|
|
3795
3593
|
});
|
|
3796
3594
|
}
|
|
3595
|
+
return verdict();
|
|
3797
3596
|
}
|
|
3798
3597
|
|
|
3799
3598
|
// src/executor/ActionExecutor.ts
|
|
@@ -3848,10 +3647,9 @@ function validateActionPayload(action) {
|
|
|
3848
3647
|
const hasIndex = typeof payload.toIndex === "number";
|
|
3849
3648
|
const hasDirection = payload.direction === "up" || payload.direction === "down";
|
|
3850
3649
|
if (!hasIndex && !hasDirection) {
|
|
3851
|
-
return invalid(
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
);
|
|
3650
|
+
return invalid('scroll_list action needs toIndex (1-based) or direction ("up"/"down")', {
|
|
3651
|
+
targetId: typeof payload.listId === "string" ? payload.listId : void 0
|
|
3652
|
+
});
|
|
3855
3653
|
}
|
|
3856
3654
|
return null;
|
|
3857
3655
|
}
|
|
@@ -3859,20 +3657,44 @@ function validateActionPayload(action) {
|
|
|
3859
3657
|
return null;
|
|
3860
3658
|
}
|
|
3861
3659
|
}
|
|
3660
|
+
function effectFor(payload, observed) {
|
|
3661
|
+
if (observed === "none" && isDestructivePayload(payload)) return "unknown";
|
|
3662
|
+
return observed;
|
|
3663
|
+
}
|
|
3664
|
+
function isDestructivePayload(payload) {
|
|
3665
|
+
if (!payload || typeof payload !== "object") return false;
|
|
3666
|
+
const p = payload;
|
|
3667
|
+
if (p.destructive === true) return true;
|
|
3668
|
+
const params = p.params;
|
|
3669
|
+
return !!params && typeof params === "object" && params.destructive === true;
|
|
3670
|
+
}
|
|
3862
3671
|
async function executeAction(action, context) {
|
|
3863
|
-
|
|
3672
|
+
appilotsDebugLog(
|
|
3673
|
+
`executeAction called: type="${action.type}", id="${action.id}", payload=${JSON.stringify(action.payload)}`
|
|
3674
|
+
);
|
|
3864
3675
|
const permissions = context.permissions ?? DEFAULT_PERMISSIONS;
|
|
3865
|
-
if (permissions.allowedActions && permissions.allowedActions.length > 0 && !permissions.allowedActions.includes(action.type)) {
|
|
3866
|
-
return {
|
|
3867
|
-
success: false,
|
|
3868
|
-
error: `Action type "${action.type}" is not in the allowed actions list`
|
|
3869
|
-
};
|
|
3870
|
-
}
|
|
3871
3676
|
context.emit({
|
|
3872
3677
|
type: "agent:action:start",
|
|
3873
3678
|
timestamp: Date.now(),
|
|
3874
3679
|
data: { actionId: action.id, actionType: action.type }
|
|
3875
3680
|
});
|
|
3681
|
+
if (permissions.allowedActions && permissions.allowedActions.length > 0 && !permissions.allowedActions.includes(action.type)) {
|
|
3682
|
+
const result2 = {
|
|
3683
|
+
success: false,
|
|
3684
|
+
error: `Action type "${action.type}" is not in the allowed actions list`
|
|
3685
|
+
};
|
|
3686
|
+
context.emit({
|
|
3687
|
+
type: "agent:action:error",
|
|
3688
|
+
timestamp: Date.now(),
|
|
3689
|
+
data: {
|
|
3690
|
+
actionId: action.id,
|
|
3691
|
+
actionType: action.type,
|
|
3692
|
+
success: false,
|
|
3693
|
+
error: result2.error
|
|
3694
|
+
}
|
|
3695
|
+
});
|
|
3696
|
+
return result2;
|
|
3697
|
+
}
|
|
3876
3698
|
let result;
|
|
3877
3699
|
const validationFailure = validateActionPayload(action);
|
|
3878
3700
|
if (validationFailure) {
|
|
@@ -3906,25 +3728,25 @@ async function executeAction(action, context) {
|
|
|
3906
3728
|
registry: context.registry ?? getDefaultRegistry()
|
|
3907
3729
|
});
|
|
3908
3730
|
break;
|
|
3909
|
-
case "ui_interaction":
|
|
3910
|
-
|
|
3731
|
+
case "ui_interaction": {
|
|
3732
|
+
const payload = action.payload;
|
|
3733
|
+
result = uiInteractionHandler(payload, {
|
|
3911
3734
|
permissions,
|
|
3912
3735
|
registry: context.registry ?? getDefaultRegistry()
|
|
3913
3736
|
});
|
|
3914
3737
|
if (result.success) {
|
|
3915
|
-
await waitForPostInteractionSettle(
|
|
3738
|
+
const settle = await waitForPostInteractionSettle(payload);
|
|
3739
|
+
result.effect = result.effect ?? effectFor(payload, settle.effect);
|
|
3916
3740
|
}
|
|
3917
3741
|
break;
|
|
3742
|
+
}
|
|
3918
3743
|
case "scroll_list":
|
|
3919
3744
|
result = await scrollListHandler(action.payload, {
|
|
3920
3745
|
permissions
|
|
3921
3746
|
});
|
|
3922
3747
|
break;
|
|
3923
3748
|
case "confirm":
|
|
3924
|
-
result = await confirmHandler(
|
|
3925
|
-
action.id,
|
|
3926
|
-
action.payload
|
|
3927
|
-
);
|
|
3749
|
+
result = await confirmHandler(action.id, action.payload);
|
|
3928
3750
|
break;
|
|
3929
3751
|
case "custom":
|
|
3930
3752
|
result = {
|
|
@@ -3947,7 +3769,9 @@ async function executeAction(action, context) {
|
|
|
3947
3769
|
error: err?.message ?? "Action execution failed unexpectedly"
|
|
3948
3770
|
};
|
|
3949
3771
|
}
|
|
3950
|
-
|
|
3772
|
+
appilotsDebugLog(
|
|
3773
|
+
`executeAction result: type="${action.type}", id="${action.id}", success=${result.success}, error=${result.error ?? "none"}`
|
|
3774
|
+
);
|
|
3951
3775
|
context.emit({
|
|
3952
3776
|
type: result.success ? "agent:action:complete" : "agent:action:error",
|
|
3953
3777
|
timestamp: Date.now(),
|
|
@@ -3963,6 +3787,93 @@ async function executeAction(action, context) {
|
|
|
3963
3787
|
});
|
|
3964
3788
|
return result;
|
|
3965
3789
|
}
|
|
3790
|
+
|
|
3791
|
+
// src/platform/confirmedDestructiveAlert.ts
|
|
3792
|
+
var SUPPRESSION_WINDOW_MS = 1e3;
|
|
3793
|
+
var _active = null;
|
|
3794
|
+
function resolveConfirmButton(buttons) {
|
|
3795
|
+
if (!buttons || buttons.length === 0) return void 0;
|
|
3796
|
+
const destructive = buttons.find(
|
|
3797
|
+
(button) => button.style === "destructive" && typeof button.onPress === "function"
|
|
3798
|
+
);
|
|
3799
|
+
if (destructive) return destructive;
|
|
3800
|
+
return buttons.find(
|
|
3801
|
+
(button) => button.style !== "cancel" && typeof button.onPress === "function"
|
|
3802
|
+
);
|
|
3803
|
+
}
|
|
3804
|
+
function restore(suppression) {
|
|
3805
|
+
if (suppression.timer) {
|
|
3806
|
+
clearTimeout(suppression.timer);
|
|
3807
|
+
suppression.timer = null;
|
|
3808
|
+
}
|
|
3809
|
+
if (_active !== suppression) return;
|
|
3810
|
+
if (suppression.host.alert === _patchedAlert) {
|
|
3811
|
+
suppression.host.alert = suppression.originalAlert;
|
|
3812
|
+
}
|
|
3813
|
+
_active = null;
|
|
3814
|
+
}
|
|
3815
|
+
var _patchedAlert = (title, message, buttons, options) => {
|
|
3816
|
+
const suppression = _active;
|
|
3817
|
+
if (!suppression) return;
|
|
3818
|
+
if (suppression.consumed) {
|
|
3819
|
+
return suppression.originalAlert(title, message, buttons, options);
|
|
3820
|
+
}
|
|
3821
|
+
const button = resolveConfirmButton(buttons);
|
|
3822
|
+
if (!button?.onPress) {
|
|
3823
|
+
return suppression.originalAlert(title, message, buttons, options);
|
|
3824
|
+
}
|
|
3825
|
+
suppression.consumed = true;
|
|
3826
|
+
appilotsDebugLog("Suppressing native Alert during already-confirmed destructive agent action");
|
|
3827
|
+
try {
|
|
3828
|
+
suppression.pending = Promise.resolve(button.onPress()).then(() => void 0).catch((err) => reportHandlerFailure(err));
|
|
3829
|
+
} catch (err) {
|
|
3830
|
+
reportHandlerFailure(err);
|
|
3831
|
+
suppression.pending = null;
|
|
3832
|
+
}
|
|
3833
|
+
};
|
|
3834
|
+
function reportHandlerFailure(err) {
|
|
3835
|
+
console.error("[Appilots] The app\u2019s Alert handler threw while being auto-confirmed", err);
|
|
3836
|
+
}
|
|
3837
|
+
async function runWithConfirmedDestructiveContext(enabled, host, fn) {
|
|
3838
|
+
if (!enabled) return fn();
|
|
3839
|
+
if (_active) {
|
|
3840
|
+
_active.depth += 1;
|
|
3841
|
+
const joined = _active;
|
|
3842
|
+
try {
|
|
3843
|
+
return await fn();
|
|
3844
|
+
} finally {
|
|
3845
|
+
joined.depth -= 1;
|
|
3846
|
+
if (joined.depth === 0) {
|
|
3847
|
+
if (joined.pending) await joined.pending.catch(() => void 0);
|
|
3848
|
+
restore(joined);
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
const suppression = {
|
|
3853
|
+
host,
|
|
3854
|
+
originalAlert: host.alert,
|
|
3855
|
+
depth: 1,
|
|
3856
|
+
consumed: false,
|
|
3857
|
+
pending: null,
|
|
3858
|
+
timer: null
|
|
3859
|
+
};
|
|
3860
|
+
_active = suppression;
|
|
3861
|
+
host.alert = _patchedAlert;
|
|
3862
|
+
suppression.timer = setTimeout(() => {
|
|
3863
|
+
if (!suppression.consumed) restore(suppression);
|
|
3864
|
+
}, SUPPRESSION_WINDOW_MS);
|
|
3865
|
+
suppression.timer?.unref?.();
|
|
3866
|
+
try {
|
|
3867
|
+
const result = await fn();
|
|
3868
|
+
if (suppression.pending) await suppression.pending.catch(() => void 0);
|
|
3869
|
+
return result;
|
|
3870
|
+
} finally {
|
|
3871
|
+
suppression.depth -= 1;
|
|
3872
|
+
if (suppression.depth === 0) restore(suppression);
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
|
|
3876
|
+
// src/platform/reactNativeAdapter.ts
|
|
3966
3877
|
var POST_ACTION_LOADING_MAX_MS = 6e3;
|
|
3967
3878
|
function resolveScreenMetadata() {
|
|
3968
3879
|
const activePath = getActiveRouteNames();
|
|
@@ -4037,13 +3948,10 @@ async function settleTurn(input) {
|
|
|
4037
3948
|
targetScreen: navigateTargetScreen,
|
|
4038
3949
|
maxMs: 3500
|
|
4039
3950
|
});
|
|
4040
|
-
|
|
4041
|
-
`
|
|
3951
|
+
appilotsDebugLog(
|
|
3952
|
+
`settleTurn: post-navigate settle done \u2014 screen="${settle.screen}" transitioned=${settle.transitioned} timedOut=${settle.timedOut} waitedMs=${settle.waitedMs}`
|
|
4042
3953
|
);
|
|
4043
|
-
const navigationMoved = settle.transitioned || screenDepartedBaseline(
|
|
4044
|
-
input.preNavigateScreen ?? null,
|
|
4045
|
-
input.preNavigateSignature ?? null
|
|
4046
|
-
) || !!navigateTargetScreen && !!settle.screen && routesBelongToSameFeature(settle.screen, navigateTargetScreen);
|
|
3954
|
+
const navigationMoved = settle.transitioned || screenDepartedBaseline(input.preNavigateScreen ?? null, input.preNavigateSignature ?? null) || !!navigateTargetScreen && !!settle.screen && routesBelongToSameFeature(settle.screen, navigateTargetScreen);
|
|
4047
3955
|
if (!navigationMoved) {
|
|
4048
3956
|
appilotsDebugWarn(
|
|
4049
3957
|
`tool-without-effect: navigate emitted but screen did not change (stayed at "${input.preNavigateScreen ?? "unknown"}", waitedMs=${settle.waitedMs}). Likely causes: target screenName missing from MCP doc, nested-nav path absent, or React Navigation rejected the route.`
|
|
@@ -4056,8 +3964,8 @@ async function settleTurn(input) {
|
|
|
4056
3964
|
fromScreen: settle.screen ?? null,
|
|
4057
3965
|
maxMs: 3e3
|
|
4058
3966
|
});
|
|
4059
|
-
|
|
4060
|
-
`
|
|
3967
|
+
appilotsDebugLog(
|
|
3968
|
+
`settleTurn: post-navigate loading settle \u2014 settled=${loadSettle.settled} loadingPending=${loadSettle.loadingPending} probes=${loadSettle.probeCount} waitedMs=${loadSettle.waitedMs}`
|
|
4061
3969
|
);
|
|
4062
3970
|
if (loadSettle.loadingPending) loadingPending = true;
|
|
4063
3971
|
} else {
|
|
@@ -4071,8 +3979,8 @@ async function settleTurn(input) {
|
|
|
4071
3979
|
fromScreen: getCurrentScreen(),
|
|
4072
3980
|
maxMs: settleCapMs
|
|
4073
3981
|
});
|
|
4074
|
-
|
|
4075
|
-
`
|
|
3982
|
+
appilotsDebugLog(
|
|
3983
|
+
`settleTurn: post-action loading settle \u2014 pressedId="${pressedId ?? ""}" settled=${loadSettle.settled} transitioned=${loadSettle.transitioned} loadingPending=${loadSettle.loadingPending} probes=${loadSettle.probeCount} waitedMs=${loadSettle.waitedMs} cap=${settleCapMs}${hints?.isAsyncTrigger ? " (mcp-inferred)" : ""}`
|
|
4076
3984
|
);
|
|
4077
3985
|
if (loadSettle.loadingPending) loadingPending = true;
|
|
4078
3986
|
const listPressActions = turnActions.filter((action) => {
|
|
@@ -4093,8 +4001,8 @@ async function settleTurn(input) {
|
|
|
4093
4001
|
maxMs: 4e3
|
|
4094
4002
|
});
|
|
4095
4003
|
const afterFingerprint = probeLoadingState(pressedId, getCurrentScreen()).fingerprint;
|
|
4096
|
-
|
|
4097
|
-
`
|
|
4004
|
+
appilotsDebugLog(
|
|
4005
|
+
`settleTurn: post-list-press settle \u2014 screen="${postPressSettle.screen}" transitioned=${postPressSettle.transitioned} timedOut=${postPressSettle.timedOut} waitedMs=${postPressSettle.waitedMs}`
|
|
4098
4006
|
);
|
|
4099
4007
|
if (!postPressSettle.transitioned && baselineFingerprint === afterFingerprint) {
|
|
4100
4008
|
for (const action of listPressActions) {
|
|
@@ -4113,42 +4021,14 @@ var reactNativeChatAdapter = {
|
|
|
4113
4021
|
getCurrentScreenSignature,
|
|
4114
4022
|
settleTurn
|
|
4115
4023
|
};
|
|
4116
|
-
async function runWithConfirmedDestructiveContext(enabled, fn) {
|
|
4117
|
-
if (!enabled) return fn();
|
|
4118
|
-
const originalAlert = Alert.alert;
|
|
4119
|
-
let consumed = false;
|
|
4120
|
-
let alertWork = null;
|
|
4121
|
-
Alert.alert = (title, message, buttons, options) => {
|
|
4122
|
-
if (consumed) {
|
|
4123
|
-
return originalAlert(title, message, buttons, options);
|
|
4124
|
-
}
|
|
4125
|
-
const destructive = buttons?.find((button) => button.style === "destructive");
|
|
4126
|
-
const actionable = destructive ?? buttons?.find((button) => button.style !== "cancel" && typeof button.onPress === "function");
|
|
4127
|
-
if (actionable?.onPress) {
|
|
4128
|
-
consumed = true;
|
|
4129
|
-
console.log(
|
|
4130
|
-
"[Appilots] Suppressing native Alert during already-confirmed destructive agent action"
|
|
4131
|
-
);
|
|
4132
|
-
alertWork = Promise.resolve(actionable.onPress()).then(() => void 0);
|
|
4133
|
-
return;
|
|
4134
|
-
}
|
|
4135
|
-
return originalAlert(title, message, buttons, options);
|
|
4136
|
-
};
|
|
4137
|
-
try {
|
|
4138
|
-
const result = await fn();
|
|
4139
|
-
if (alertWork) await alertWork;
|
|
4140
|
-
return result;
|
|
4141
|
-
} finally {
|
|
4142
|
-
Alert.alert = originalAlert;
|
|
4143
|
-
}
|
|
4144
|
-
}
|
|
4145
4024
|
function createReactNativeActionRunner(getOptions) {
|
|
4146
4025
|
return {
|
|
4147
4026
|
async execute(action, { confirmedDestructive }) {
|
|
4148
|
-
const { permissions, emit, navigationRef } = getOptions();
|
|
4027
|
+
const { permissions, emit, navigationRef, suppressNativeConfirm } = getOptions();
|
|
4149
4028
|
const navRef = navigationRef?.current ? navigationRef : { current: getNavigationRef() };
|
|
4150
4029
|
return runWithConfirmedDestructiveContext(
|
|
4151
|
-
confirmedDestructive,
|
|
4030
|
+
confirmedDestructive && suppressNativeConfirm !== false,
|
|
4031
|
+
Alert,
|
|
4152
4032
|
() => executeAction(action, {
|
|
4153
4033
|
navigationRef: navRef,
|
|
4154
4034
|
permissions,
|
|
@@ -4218,12 +4098,14 @@ function useAppilotsActions(options = {}) {
|
|
|
4218
4098
|
const runnerOptionsRef = useRef({
|
|
4219
4099
|
permissions: config.permissions ?? DEFAULT_PERMISSIONS2,
|
|
4220
4100
|
emit,
|
|
4221
|
-
navigationRef
|
|
4101
|
+
navigationRef,
|
|
4102
|
+
suppressNativeConfirm: config.suppressNativeConfirm
|
|
4222
4103
|
});
|
|
4223
4104
|
runnerOptionsRef.current = {
|
|
4224
4105
|
permissions: config.permissions ?? DEFAULT_PERMISSIONS2,
|
|
4225
4106
|
emit,
|
|
4226
|
-
navigationRef
|
|
4107
|
+
navigationRef,
|
|
4108
|
+
suppressNativeConfirm: config.suppressNativeConfirm
|
|
4227
4109
|
};
|
|
4228
4110
|
const machine = useMemo(
|
|
4229
4111
|
() => new ActionQueueMachine(
|
|
@@ -4313,7 +4195,7 @@ function useSuggestedPrompts(options) {
|
|
|
4313
4195
|
if (cancelled || requestIdRef.current !== requestId) return;
|
|
4314
4196
|
setPrompts(mergePromptSources(localDev, data, limit));
|
|
4315
4197
|
}).catch((err) => {
|
|
4316
|
-
|
|
4198
|
+
appilotsDebugWarn("useSuggestedPrompts: fetch failed", err);
|
|
4317
4199
|
}).finally(() => {
|
|
4318
4200
|
if (cancelled || requestIdRef.current !== requestId) return;
|
|
4319
4201
|
setIsLoading(false);
|
|
@@ -4511,4 +4393,4 @@ function useAppilotsSlider(id, options) {
|
|
|
4511
4393
|
}, [id, registry, getValue, setValue, min, max, step, label, screen]);
|
|
4512
4394
|
}
|
|
4513
4395
|
|
|
4514
|
-
export { captureSnapshot, executeAction, shouldShowSuggestedPrompts, useAppilots, useAppilotsActions, useAppilotsChat, useAppilotsField, useAppilotsNavigation, useAppilotsSlider, useAppilotsTarget, useAppilotsToggle, useSuggestedPrompts };
|
|
4396
|
+
export { appilotsSelfCheck, captureSnapshot, executeAction, shouldShowSuggestedPrompts, useAppilots, useAppilotsActions, useAppilotsChat, useAppilotsField, useAppilotsNavigation, useAppilotsSlider, useAppilotsTarget, useAppilotsToggle, useSuggestedPrompts };
|