@appilots/sdk 0.8.0 → 0.11.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 +5 -4
- package/dist/.build-meta.json +5 -0
- package/dist/{chunk-N7EAOLBP.js → chunk-2YPEQTL2.js} +956 -1134
- package/dist/{chunk-ZSEACJKU.mjs → chunk-3K7ORC4X.mjs} +16 -13
- package/dist/{chunk-AYWMBMSN.js → chunk-55Y2AQE7.js} +1992 -384
- package/dist/{chunk-C2KVVJ5K.mjs → chunk-G6KI7KVH.mjs} +1968 -386
- package/dist/{chunk-H7BMQTXH.js → chunk-IZEWWAA7.js} +19 -16
- package/dist/{chunk-MHWVRZYF.mjs → chunk-NA5H4IHH.mjs} +736 -919
- 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-BdYY-dvG.d.mts → index-BPZ0j71S.d.mts} +254 -6
- package/dist/{index-BdYY-dvG.d.ts → index-BPZ0j71S.d.ts} +254 -6
- package/dist/index.d.mts +306 -21
- package/dist/index.d.ts +306 -21
- package/dist/index.js +337 -229
- package/dist/index.mjs +269 -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 +4 -2
- 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, clampSnapshotToWireLimits, 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-G6KI7KVH.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,36 +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);
|
|
203
|
+
canonicalizeRegisteredFields(snapshot);
|
|
773
204
|
clampSnapshotToWireLimits(snapshot);
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
snapshot.elements
|
|
205
|
+
const entries = deriveInteractionElements(snapshot, { rectOf: rectForNode });
|
|
206
|
+
const rectById = new Map(
|
|
207
|
+
entries.flatMap((entry) => entry.rect ? [[entry.id, entry.rect]] : [])
|
|
778
208
|
);
|
|
209
|
+
snapshot.elements = entries.map(toWireElement);
|
|
210
|
+
snapshot.choiceGroups = attachElementIdsToChoiceGroups(snapshot.choiceGroups, snapshot.elements);
|
|
779
211
|
clampSnapshotToWireLimits(snapshot);
|
|
780
|
-
elementRegistry.replaceAll(
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
212
|
+
elementRegistry.replaceAll(
|
|
213
|
+
snapshot.elements.map((element) => {
|
|
214
|
+
const rect = rectById.get(element.id);
|
|
215
|
+
return rect ? { ...element, rect } : element;
|
|
216
|
+
})
|
|
785
217
|
);
|
|
218
|
+
const elapsed = Date.now() - start;
|
|
219
|
+
const totalListItems = snapshot.lists.reduce((acc, l) => acc + l.items.length, 0);
|
|
786
220
|
const totalListDataItems = snapshot.lists.reduce(
|
|
787
221
|
(acc, l) => acc + (typeof l.itemCount === "number" ? l.itemCount : 0),
|
|
788
222
|
0
|
|
789
223
|
);
|
|
790
|
-
|
|
791
|
-
`
|
|
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)`
|
|
792
226
|
);
|
|
793
|
-
|
|
794
|
-
`
|
|
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(", ")}]`
|
|
795
229
|
);
|
|
796
230
|
return snapshot;
|
|
797
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
|
+
}
|
|
798
272
|
|
|
799
273
|
// src/introspection/findInteractive.ts
|
|
800
274
|
function getTypeName2(type) {
|
|
@@ -808,7 +282,7 @@ function getTypeName2(type) {
|
|
|
808
282
|
}
|
|
809
283
|
return null;
|
|
810
284
|
}
|
|
811
|
-
function
|
|
285
|
+
function matchesName(fiber, name) {
|
|
812
286
|
return getTypeName2(fiber.type) === name || getTypeName2(fiber.elementType) === name;
|
|
813
287
|
}
|
|
814
288
|
var TOUCHABLE_NAMES2 = /* @__PURE__ */ new Set([
|
|
@@ -821,33 +295,33 @@ var TOUCHABLE_NAMES2 = /* @__PURE__ */ new Set([
|
|
|
821
295
|
function isTouchable(fiber) {
|
|
822
296
|
return TOUCHABLE_NAMES2.has(getTypeName2(fiber.type) ?? "") || TOUCHABLE_NAMES2.has(getTypeName2(fiber.elementType) ?? "");
|
|
823
297
|
}
|
|
824
|
-
function
|
|
298
|
+
function hasPressHandler(fiber) {
|
|
825
299
|
const props = fiber?.memoizedProps ?? {};
|
|
826
300
|
return typeof props.onPress === "function" || typeof props.onPressIn === "function";
|
|
827
301
|
}
|
|
828
|
-
function
|
|
829
|
-
return isTouchable(fiber) ||
|
|
302
|
+
function isPressableTarget(fiber) {
|
|
303
|
+
return isTouchable(fiber) || hasPressHandler(fiber);
|
|
830
304
|
}
|
|
831
|
-
var
|
|
305
|
+
var SELF_REFERENTIAL_COMPONENTS = /* @__PURE__ */ new Set([
|
|
832
306
|
"AppilotsChat",
|
|
833
307
|
"AppilotsChatInner",
|
|
834
308
|
"ActionBreadcrumb",
|
|
835
309
|
"ConfirmDialog"
|
|
836
310
|
]);
|
|
837
|
-
function
|
|
311
|
+
function isAppilotsSkipMarked(props) {
|
|
838
312
|
if (!props) return false;
|
|
839
313
|
return props.__appilotsSkip === true || props["data-appilots-skip"] === true || props.appilotsSkip === true;
|
|
840
314
|
}
|
|
841
|
-
function
|
|
315
|
+
function extractTextFromChildren(children) {
|
|
842
316
|
if (children == null) return "";
|
|
843
317
|
if (typeof children === "string") return children;
|
|
844
318
|
if (typeof children === "number") return String(children);
|
|
845
319
|
if (Array.isArray(children)) {
|
|
846
|
-
return children.map(
|
|
320
|
+
return children.map(extractTextFromChildren).filter(Boolean).join("");
|
|
847
321
|
}
|
|
848
322
|
return "";
|
|
849
323
|
}
|
|
850
|
-
function
|
|
324
|
+
function findChildText(fiber) {
|
|
851
325
|
const stack = [];
|
|
852
326
|
if (fiber.child) stack.push(fiber.child);
|
|
853
327
|
const visited = /* @__PURE__ */ new WeakSet();
|
|
@@ -855,8 +329,8 @@ function findChildText2(fiber) {
|
|
|
855
329
|
const node = stack.pop();
|
|
856
330
|
if (!node || visited.has(node)) continue;
|
|
857
331
|
visited.add(node);
|
|
858
|
-
if (
|
|
859
|
-
const text =
|
|
332
|
+
if (matchesName(node, "Text")) {
|
|
333
|
+
const text = extractTextFromChildren(node.memoizedProps?.children);
|
|
860
334
|
if (text && text.trim().length > 0) return text.trim();
|
|
861
335
|
}
|
|
862
336
|
if (node.sibling) stack.push(node.sibling);
|
|
@@ -868,10 +342,10 @@ function shouldSkipSubtree2(fiber) {
|
|
|
868
342
|
const props = fiber.memoizedProps;
|
|
869
343
|
if (!props) return false;
|
|
870
344
|
const typeName = getTypeName2(fiber.type) ?? getTypeName2(fiber.elementType);
|
|
871
|
-
if (typeName &&
|
|
872
|
-
if (
|
|
873
|
-
if (
|
|
874
|
-
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;
|
|
875
349
|
const style = props.style;
|
|
876
350
|
if (style) {
|
|
877
351
|
const flat = Array.isArray(style) ? Object.assign({}, ...style.filter(Boolean)) : style;
|
|
@@ -880,20 +354,19 @@ function shouldSkipSubtree2(fiber) {
|
|
|
880
354
|
}
|
|
881
355
|
return false;
|
|
882
356
|
}
|
|
883
|
-
|
|
884
|
-
if (!s) return "";
|
|
885
|
-
return s.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
|
|
886
|
-
}
|
|
357
|
+
var normalize = foldForCompare;
|
|
887
358
|
function scoreMatch(fiber, queryNorm, queryRaw) {
|
|
888
359
|
const props = fiber.memoizedProps ?? {};
|
|
889
360
|
const candidates = [];
|
|
890
361
|
if (typeof props.testID === "string") candidates.push({ val: props.testID, weight: 100 });
|
|
891
|
-
if (typeof props.accessibilityLabel === "string")
|
|
362
|
+
if (typeof props.accessibilityLabel === "string")
|
|
363
|
+
candidates.push({ val: props.accessibilityLabel, weight: 95 });
|
|
892
364
|
if (typeof props.label === "string") candidates.push({ val: props.label, weight: 90 });
|
|
893
|
-
if (typeof props.placeholder === "string")
|
|
365
|
+
if (typeof props.placeholder === "string")
|
|
366
|
+
candidates.push({ val: props.placeholder, weight: 70 });
|
|
894
367
|
if (typeof props.title === "string") candidates.push({ val: props.title, weight: 85 });
|
|
895
|
-
if (
|
|
896
|
-
const childText =
|
|
368
|
+
if (isPressableTarget(fiber)) {
|
|
369
|
+
const childText = findChildText(fiber);
|
|
897
370
|
if (childText) candidates.push({ val: childText, weight: 80 });
|
|
898
371
|
}
|
|
899
372
|
let best = 0;
|
|
@@ -902,7 +375,8 @@ function scoreMatch(fiber, queryNorm, queryRaw) {
|
|
|
902
375
|
if (!vNorm) continue;
|
|
903
376
|
if (val === queryRaw) best = Math.max(best, weight + 20);
|
|
904
377
|
else if (vNorm === queryNorm) best = Math.max(best, weight + 10);
|
|
905
|
-
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);
|
|
906
380
|
}
|
|
907
381
|
if (best > 0 && props.disabled === true) {
|
|
908
382
|
best = Math.max(1, best - 60);
|
|
@@ -933,6 +407,12 @@ function wrapTextInput(fiber) {
|
|
|
933
407
|
if (node && typeof node.focus === "function") {
|
|
934
408
|
node.focus();
|
|
935
409
|
}
|
|
410
|
+
},
|
|
411
|
+
submitFromKeyboard: () => {
|
|
412
|
+
const { onSubmitEditing, value } = getProps();
|
|
413
|
+
if (typeof onSubmitEditing !== "function") return false;
|
|
414
|
+
onSubmitEditing({ nativeEvent: { text: value ?? "" } });
|
|
415
|
+
return true;
|
|
936
416
|
}
|
|
937
417
|
};
|
|
938
418
|
}
|
|
@@ -972,14 +452,14 @@ function wrapTouchable(fiber, label) {
|
|
|
972
452
|
function findInteractiveByIdentifier(query, options = {}) {
|
|
973
453
|
const root = getFiberRoot();
|
|
974
454
|
if (!root) {
|
|
975
|
-
|
|
455
|
+
appilotsDebugWarn("findInteractiveByIdentifier: fiber root not captured yet");
|
|
976
456
|
return null;
|
|
977
457
|
}
|
|
978
458
|
const queryNorm = normalize(query);
|
|
979
459
|
const queryRaw = query;
|
|
980
460
|
let bestScore = 0;
|
|
981
461
|
let bestHandle = null;
|
|
982
|
-
let
|
|
462
|
+
let bestId;
|
|
983
463
|
const visited = /* @__PURE__ */ new WeakSet();
|
|
984
464
|
const stack = [];
|
|
985
465
|
if (root.child) stack.push(root.child);
|
|
@@ -990,40 +470,50 @@ function findInteractiveByIdentifier(query, options = {}) {
|
|
|
990
470
|
if (fiber.sibling) stack.push(fiber.sibling);
|
|
991
471
|
if (shouldSkipSubtree2(fiber)) continue;
|
|
992
472
|
let kind = null;
|
|
993
|
-
if (
|
|
994
|
-
else if (
|
|
995
|
-
else if (
|
|
473
|
+
if (matchesName(fiber, "TextInput")) kind = "field";
|
|
474
|
+
else if (matchesName(fiber, "Switch")) kind = "toggle";
|
|
475
|
+
else if (isPressableTarget(fiber)) kind = "target";
|
|
996
476
|
if (kind && (!options.kind || options.kind === kind)) {
|
|
997
477
|
const score = scoreMatch(fiber, queryNorm, queryRaw);
|
|
998
478
|
if (score > bestScore) {
|
|
999
479
|
bestScore = score;
|
|
1000
480
|
const props = fiber.memoizedProps ?? {};
|
|
1001
|
-
|
|
481
|
+
bestId = props.testID ?? props.accessibilityLabel ?? props.placeholder ?? (kind === "target" ? findChildText(fiber) : void 0);
|
|
1002
482
|
if (kind === "field") bestHandle = wrapTextInput(fiber);
|
|
1003
483
|
else if (kind === "toggle") bestHandle = wrapSwitch(fiber);
|
|
1004
|
-
else bestHandle = wrapTouchable(fiber,
|
|
484
|
+
else bestHandle = wrapTouchable(fiber, findChildText(fiber));
|
|
1005
485
|
}
|
|
1006
486
|
}
|
|
1007
487
|
if (fiber.child) stack.push(fiber.child);
|
|
1008
488
|
}
|
|
1009
489
|
if (bestHandle) {
|
|
1010
|
-
|
|
1011
|
-
`
|
|
490
|
+
appilotsDebugLog(
|
|
491
|
+
`findInteractiveByIdentifier: query="${query}" \u2192 kind=${bestHandle.kind} label="${bestHandle.label ?? bestId}" score=${bestScore}`
|
|
1012
492
|
);
|
|
1013
493
|
} else {
|
|
1014
|
-
|
|
494
|
+
appilotsDebugLog(`findInteractiveByIdentifier: query="${query}" \u2192 no match`);
|
|
1015
495
|
}
|
|
1016
496
|
return bestHandle;
|
|
1017
497
|
}
|
|
1018
498
|
|
|
1019
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
|
+
}
|
|
1020
508
|
function categorizeNavigationError(message) {
|
|
1021
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(
|
|
1022
510
|
message
|
|
1023
511
|
)) {
|
|
1024
512
|
return "component-not-found";
|
|
1025
513
|
}
|
|
1026
|
-
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
|
+
)) {
|
|
1027
517
|
return "screen-timeout";
|
|
1028
518
|
}
|
|
1029
519
|
return "unknown";
|
|
@@ -1050,7 +540,9 @@ function screenExistsAnywhere(state, screenName) {
|
|
|
1050
540
|
function safeNavigate(nav, screenName, params, path) {
|
|
1051
541
|
if (path && path.length > 0) {
|
|
1052
542
|
const { root, nestedParams } = buildNestedParams(path, screenName, params);
|
|
1053
|
-
|
|
543
|
+
appilotsDebugLog(
|
|
544
|
+
`safeNavigate: using path-based navigation \u2014 root="${root}", nested=${JSON.stringify(nestedParams)}`
|
|
545
|
+
);
|
|
1054
546
|
nav.navigate(root, nestedParams);
|
|
1055
547
|
return;
|
|
1056
548
|
}
|
|
@@ -1065,11 +557,17 @@ function safeNavigate(nav, screenName, params, path) {
|
|
|
1065
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.`
|
|
1066
558
|
);
|
|
1067
559
|
}
|
|
1068
|
-
|
|
560
|
+
appilotsDebugLog(
|
|
561
|
+
`safeNavigate: screen exists in nested navigator, attempting direct navigation to "${screenName}"`
|
|
562
|
+
);
|
|
1069
563
|
nav.navigate(screenName, params);
|
|
1070
564
|
}
|
|
1071
565
|
function navigateHandler(payload, context) {
|
|
1072
|
-
|
|
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
|
+
);
|
|
1073
571
|
const { navigationRef, permissions } = context;
|
|
1074
572
|
const nav = navigationRef.current;
|
|
1075
573
|
if (!permissions.canNavigate) {
|
|
@@ -1079,14 +577,15 @@ function navigateHandler(payload, context) {
|
|
|
1079
577
|
diagnose: { category: "unknown", screen: payload.screenName }
|
|
1080
578
|
};
|
|
1081
579
|
}
|
|
1082
|
-
|
|
580
|
+
const isGoBack = payload.navigationAction === "goBack";
|
|
581
|
+
if (!isGoBack && permissions.blockedScreens?.includes(payload.screenName)) {
|
|
1083
582
|
return {
|
|
1084
583
|
success: false,
|
|
1085
584
|
error: `Navigation to "${payload.screenName}" is blocked`,
|
|
1086
585
|
diagnose: { category: "unknown", screen: payload.screenName }
|
|
1087
586
|
};
|
|
1088
587
|
}
|
|
1089
|
-
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)) {
|
|
1090
589
|
return {
|
|
1091
590
|
success: false,
|
|
1092
591
|
error: `Navigation to "${payload.screenName}" is not in allowed screens`,
|
|
@@ -1094,10 +593,11 @@ function navigateHandler(payload, context) {
|
|
|
1094
593
|
};
|
|
1095
594
|
}
|
|
1096
595
|
if (!nav) {
|
|
596
|
+
reportMissingNavigationRef();
|
|
1097
597
|
return {
|
|
1098
598
|
success: false,
|
|
1099
|
-
error: "Navigation
|
|
1100
|
-
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 }
|
|
1101
601
|
};
|
|
1102
602
|
}
|
|
1103
603
|
if (!nav.isReady || typeof nav.isReady === "function" && !nav.isReady()) {
|
|
@@ -1108,7 +608,15 @@ function navigateHandler(payload, context) {
|
|
|
1108
608
|
};
|
|
1109
609
|
}
|
|
1110
610
|
try {
|
|
1111
|
-
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;
|
|
1112
620
|
switch (navigationAction) {
|
|
1113
621
|
case "navigate":
|
|
1114
622
|
safeNavigate(nav, screenName, params ?? {}, path);
|
|
@@ -1151,10 +659,14 @@ function navigateHandler(payload, context) {
|
|
|
1151
659
|
diagnose: { category: "unknown", screen: payload.screenName }
|
|
1152
660
|
};
|
|
1153
661
|
}
|
|
1154
|
-
|
|
662
|
+
appilotsDebugLog(
|
|
663
|
+
`navigateHandler: navigation succeeded for "${payload.screenName}" (action="${payload.navigationAction}")`
|
|
664
|
+
);
|
|
1155
665
|
return { success: true };
|
|
1156
666
|
} catch (err) {
|
|
1157
|
-
|
|
667
|
+
appilotsDebugWarn(
|
|
668
|
+
`navigateHandler: navigation failed for "${payload.screenName}": ${err?.message}`
|
|
669
|
+
);
|
|
1158
670
|
const msg = String(err?.message ?? "Navigation failed");
|
|
1159
671
|
const category = categorizeNavigationError(msg);
|
|
1160
672
|
return {
|
|
@@ -1177,7 +689,7 @@ function parseSyntheticListItemId(id) {
|
|
|
1177
689
|
if (listIndex < 0 || itemIndex < 1) return null;
|
|
1178
690
|
return { listIndex, itemIndex };
|
|
1179
691
|
}
|
|
1180
|
-
function
|
|
692
|
+
function stringProp(props, key) {
|
|
1181
693
|
const value = props?.[key];
|
|
1182
694
|
if (typeof value === "string" && value.trim()) return value.trim();
|
|
1183
695
|
if (typeof value === "number") return String(value);
|
|
@@ -1185,30 +697,30 @@ function stringProp2(props, key) {
|
|
|
1185
697
|
}
|
|
1186
698
|
function listIdFromContainer(container) {
|
|
1187
699
|
const props = container?.memoizedProps ?? {};
|
|
1188
|
-
return
|
|
700
|
+
return stringProp(props, "__appilotsListId") ?? stringProp(props, "appilotsListId") ?? stringProp(props, "testID") ?? stringProp(props, "accessibilityLabel");
|
|
1189
701
|
}
|
|
1190
702
|
function listLabelFromContainer(container) {
|
|
1191
703
|
const props = container?.memoizedProps ?? {};
|
|
1192
|
-
return
|
|
704
|
+
return stringProp(props, "__appilotsListLabel") ?? stringProp(props, "accessibilityLabel") ?? stringProp(props, "aria-label") ?? stringProp(props, "label");
|
|
1193
705
|
}
|
|
1194
706
|
function itemKeyFromFiber(fiber) {
|
|
1195
707
|
const props = fiber?.memoizedProps ?? {};
|
|
1196
|
-
return
|
|
708
|
+
return stringProp(props, "__appilotsItemKey") ?? (fiber?.key !== void 0 && fiber?.key !== null ? String(fiber.key) : void 0);
|
|
1197
709
|
}
|
|
1198
710
|
function markedItemIndexFromFiber(fiber) {
|
|
1199
711
|
const value = fiber?.memoizedProps?.__appilotsItemIndex;
|
|
1200
712
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1201
713
|
}
|
|
1202
714
|
function normalize2(value) {
|
|
1203
|
-
return String(value ?? "")
|
|
715
|
+
return foldForCompare(String(value ?? ""));
|
|
1204
716
|
}
|
|
1205
|
-
function
|
|
717
|
+
function hasPressHandler2(fiber) {
|
|
1206
718
|
const props = fiber?.memoizedProps ?? {};
|
|
1207
719
|
return typeof props.onPress === "function" || typeof props.onPressIn === "function";
|
|
1208
720
|
}
|
|
1209
721
|
function isPressableFiber(fiber) {
|
|
1210
722
|
const typeName = getTypeName(fiber.type) ?? getTypeName(fiber.elementType) ?? "";
|
|
1211
|
-
return TOUCHABLE_NAMES.has(typeName) ||
|
|
723
|
+
return TOUCHABLE_NAMES.has(typeName) || hasPressHandler2(fiber);
|
|
1212
724
|
}
|
|
1213
725
|
function findListContainers(options = {}) {
|
|
1214
726
|
const root = getFiberRoot();
|
|
@@ -1353,13 +865,10 @@ function collectTexts(fiber) {
|
|
|
1353
865
|
}
|
|
1354
866
|
function hasUsefulTouchableIdentity(fiber) {
|
|
1355
867
|
const props = fiber?.memoizedProps ?? {};
|
|
1356
|
-
if (
|
|
1357
|
-
if (
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
if (typeof props.label === "string" && /[a-z0-9]/i.test(props.label)) return true;
|
|
1361
|
-
const label = findFirstText(fiber);
|
|
1362
|
-
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));
|
|
1363
872
|
}
|
|
1364
873
|
function describeTouchable(fiber) {
|
|
1365
874
|
const props = fiber?.memoizedProps ?? {};
|
|
@@ -1372,12 +881,7 @@ function touchableLooksDestructive(fiber) {
|
|
|
1372
881
|
const descriptor = describeTouchable(fiber);
|
|
1373
882
|
if (descriptor && looksDestructiveActionLabel(descriptor)) return true;
|
|
1374
883
|
const props = fiber?.memoizedProps ?? {};
|
|
1375
|
-
const haystack = [
|
|
1376
|
-
props.testID,
|
|
1377
|
-
props.accessibilityLabel,
|
|
1378
|
-
props.accessibilityHint,
|
|
1379
|
-
props.label
|
|
1380
|
-
].filter((value) => typeof value === "string").join(" ");
|
|
884
|
+
const haystack = [props.testID, props.accessibilityLabel, props.accessibilityHint, props.label].filter((value) => typeof value === "string").join(" ");
|
|
1381
885
|
return looksDestructiveActionLabel(haystack);
|
|
1382
886
|
}
|
|
1383
887
|
function describeItemCandidate(listIndex, zeroBasedIndex, item) {
|
|
@@ -1411,8 +915,8 @@ function pressResolvedListItem(list, itemFiber, listIndex, itemIndex) {
|
|
|
1411
915
|
(candidate) => candidate !== touchable && containsFiber(touchable, candidate)
|
|
1412
916
|
);
|
|
1413
917
|
if (primaryWrapsNestedTargets) {
|
|
1414
|
-
|
|
1415
|
-
`
|
|
918
|
+
appilotsDebugLog(
|
|
919
|
+
`listItemDispatch: item "list-${listIndex}-item-${itemIndex}" has nested pressables; pressing outer row target`
|
|
1416
920
|
);
|
|
1417
921
|
} else {
|
|
1418
922
|
const candidates = safeIdentifiableTouchables.map(describeTouchable).filter((d) => !!d).slice(0, 8);
|
|
@@ -1522,14 +1026,14 @@ function pressListItemAtOrdinal(listIndex, itemIndex) {
|
|
|
1522
1026
|
}
|
|
1523
1027
|
return pressResolvedListItem(list, items[zeroBased], listIndex, itemIndex);
|
|
1524
1028
|
}
|
|
1525
|
-
function
|
|
1526
|
-
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" } };
|
|
1527
1031
|
const parsed = locator.syntheticId ? parseSyntheticListItemId(locator.syntheticId) : null;
|
|
1528
1032
|
const effective = parsed ? { ...locator, listIndex: parsed.listIndex, itemIndex: parsed.itemIndex } : locator;
|
|
1529
1033
|
const containers = findListContainers({ allowSingleHeuristic: true });
|
|
1530
1034
|
const lists = candidateLists(containers, effective);
|
|
1531
1035
|
if (lists.length === 0) {
|
|
1532
|
-
return { ok: false, reason: "list-not-found" };
|
|
1036
|
+
return { ok: false, result: { ok: false, reason: "list-not-found" } };
|
|
1533
1037
|
}
|
|
1534
1038
|
const scoredCandidates = [];
|
|
1535
1039
|
for (const { list, listIndex } of lists) {
|
|
@@ -1540,7 +1044,14 @@ function pressListItemByIdentity(locator) {
|
|
|
1540
1044
|
}
|
|
1541
1045
|
}
|
|
1542
1046
|
if (scoredCandidates.length === 0) {
|
|
1543
|
-
return {
|
|
1047
|
+
return {
|
|
1048
|
+
ok: false,
|
|
1049
|
+
result: {
|
|
1050
|
+
ok: false,
|
|
1051
|
+
reason: "item-not-found",
|
|
1052
|
+
containerType: lists[0]?.list.containerType
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1544
1055
|
}
|
|
1545
1056
|
scoredCandidates.sort((a, b) => b.score - a.score);
|
|
1546
1057
|
const best = scoredCandidates[0];
|
|
@@ -1549,13 +1060,104 @@ function pressListItemByIdentity(locator) {
|
|
|
1549
1060
|
const candidates = tiedCandidates.map((c) => describeItemCandidate(c.listIndex, c.itemIndex - 1, c.item)).slice(0, 8);
|
|
1550
1061
|
return {
|
|
1551
1062
|
ok: false,
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
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,
|
|
1555
1122
|
candidates
|
|
1556
1123
|
};
|
|
1557
1124
|
}
|
|
1558
|
-
|
|
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
|
+
);
|
|
1559
1161
|
}
|
|
1560
1162
|
|
|
1561
1163
|
// src/executor/handlers/elementDispatch.ts
|
|
@@ -1784,6 +1386,46 @@ function dispatchElement(element, registry, action) {
|
|
|
1784
1386
|
};
|
|
1785
1387
|
}
|
|
1786
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
|
+
}
|
|
1787
1429
|
const targetId = element.targetId ?? element.label ?? element.id;
|
|
1788
1430
|
return dispatchLegacyTarget(targetId, registry, "toggle");
|
|
1789
1431
|
}
|
|
@@ -1813,8 +1455,8 @@ function dispatchStructuredElementId(identifier, action) {
|
|
|
1813
1455
|
texts: parsed.itemKey ? [parsed.itemKey] : void 0
|
|
1814
1456
|
});
|
|
1815
1457
|
if (pressed.ok) {
|
|
1816
|
-
|
|
1817
|
-
`
|
|
1458
|
+
appilotsDebugLog(
|
|
1459
|
+
`elementDispatch: resolved stale el id "${identifier}" via structural identity (listId="${parsed.listId}", itemKey="${parsed.itemKey}")`
|
|
1818
1460
|
);
|
|
1819
1461
|
return { success: true };
|
|
1820
1462
|
}
|
|
@@ -1838,7 +1480,6 @@ function pressElementOrTarget(identifier, registry) {
|
|
|
1838
1480
|
|
|
1839
1481
|
// src/executor/formFieldRewrite.ts
|
|
1840
1482
|
var LIST_ORDINAL_RE = /^list-\d+-item-\d+$/i;
|
|
1841
|
-
var PLATE_RE = /^[A-Z]{3}-?\d[A-Z0-9]\d{2}$/i;
|
|
1842
1483
|
function normalize4(value) {
|
|
1843
1484
|
return value.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
|
|
1844
1485
|
}
|
|
@@ -1878,9 +1519,6 @@ function collectKnownFormFields(registry) {
|
|
|
1878
1519
|
}
|
|
1879
1520
|
return registryFields;
|
|
1880
1521
|
}
|
|
1881
|
-
function fieldHaystack(field) {
|
|
1882
|
-
return normalize4(`${field.id} ${field.label ?? ""}`);
|
|
1883
|
-
}
|
|
1884
1522
|
function isMisplacedFieldId(fieldId, knownIds) {
|
|
1885
1523
|
const trimmed = fieldId.trim();
|
|
1886
1524
|
if (!trimmed) return true;
|
|
@@ -1904,50 +1542,68 @@ function valueMatchesOption(field, rawValue) {
|
|
|
1904
1542
|
return label === value || optionValue === value || value.includes(label) || value.includes(optionValue);
|
|
1905
1543
|
});
|
|
1906
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
|
+
}
|
|
1907
1596
|
function inferFieldIdForValue(rawValue, fields, used) {
|
|
1908
1597
|
const value = String(rawValue ?? "").trim();
|
|
1909
1598
|
if (!value) return void 0;
|
|
1910
|
-
const
|
|
1911
|
-
for (const field of
|
|
1912
|
-
if (used.has(field.id)) continue;
|
|
1599
|
+
const available = fields.filter((field) => field.id && !used.has(field.id));
|
|
1600
|
+
for (const field of available) {
|
|
1913
1601
|
if (valueMatchesOption(field, value)) return field.id;
|
|
1914
1602
|
}
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
if (/^(19|20)\d{2}$/.test(value)) {
|
|
1920
|
-
const field = fields.find((f) => /ano|year/.test(fieldHaystack(f)) && !used.has(f.id));
|
|
1921
|
-
if (field) return field.id;
|
|
1922
|
-
}
|
|
1923
|
-
if (/^\d+$/.test(value)) {
|
|
1924
|
-
const mileage = fields.find((f) => /quilometragem|mileage|km/.test(fieldHaystack(f)) && !used.has(f.id));
|
|
1925
|
-
if (mileage) return mileage.id;
|
|
1926
|
-
const year = fields.find((f) => /ano|year/.test(fieldHaystack(f)) && !used.has(f.id));
|
|
1927
|
-
if (year) return year.id;
|
|
1928
|
-
}
|
|
1929
|
-
if (/preto|branco|azul|vermelho|prata|black|white|red|blue|silver|gray|grey/.test(normalizedValue)) {
|
|
1930
|
-
const cor = fields.find((f) => /cor|color/.test(fieldHaystack(f)) && !used.has(f.id));
|
|
1931
|
-
if (cor) return cor.id;
|
|
1932
|
-
}
|
|
1933
|
-
const marca = fields.find((f) => /marca|brand|make/.test(fieldHaystack(f)) && !used.has(f.id));
|
|
1934
|
-
if (marca && !/model/i.test(normalizedValue)) return marca.id;
|
|
1935
|
-
const modelo = fields.find((f) => /modelo|model/.test(fieldHaystack(f)) && !used.has(f.id));
|
|
1936
|
-
if (modelo) return modelo.id;
|
|
1937
|
-
const tipo = fields.find((f) => /\btipo\b|type|vehicletype/.test(fieldHaystack(f)) && !used.has(f.id));
|
|
1938
|
-
if (tipo && /carro|car|moto|truck|van|flex|gasoline|diesel|hybrid|electric|el[eé]trico/.test(normalizedValue)) {
|
|
1939
|
-
return tipo.id;
|
|
1940
|
-
}
|
|
1941
|
-
const combustivel = fields.find((f) => /combustivel|fuel/.test(fieldHaystack(f)) && !used.has(f.id));
|
|
1942
|
-
if (combustivel && /flex|gasoline|diesel|hybrid|electric|el[eé]trico/.test(normalizedValue)) {
|
|
1943
|
-
return combustivel.id;
|
|
1944
|
-
}
|
|
1945
|
-
const nextText = fields.find((field) => {
|
|
1946
|
-
if (used.has(field.id)) return false;
|
|
1947
|
-
const type = normalize4(field.type ?? "text");
|
|
1948
|
-
return type === "text" || type === "input" || type === "number" || type === "";
|
|
1949
|
-
});
|
|
1950
|
-
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;
|
|
1951
1607
|
}
|
|
1952
1608
|
function rewriteMisplacedFormFillFields(fields, registry) {
|
|
1953
1609
|
const knownFields = collectKnownFormFields(registry);
|
|
@@ -1972,28 +1628,36 @@ function rewriteMisplacedFormFillFields(fields, registry) {
|
|
|
1972
1628
|
}
|
|
1973
1629
|
|
|
1974
1630
|
// src/executor/formValidation.ts
|
|
1975
|
-
var
|
|
1976
|
-
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;
|
|
1977
1633
|
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
]
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
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
|
+
}
|
|
1989
1647
|
}
|
|
1990
|
-
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} `);
|
|
1991
1655
|
}
|
|
1992
1656
|
function readVisibleFormValidation(snap) {
|
|
1993
1657
|
if (!snap) return void 0;
|
|
1994
1658
|
for (const text of snap.texts ?? []) {
|
|
1995
|
-
if (!text?.trim() || !
|
|
1996
|
-
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 };
|
|
1997
1661
|
}
|
|
1998
1662
|
return void 0;
|
|
1999
1663
|
}
|
|
@@ -2215,7 +1879,9 @@ function resolveComponentId(componentId, registry, action) {
|
|
|
2215
1879
|
const snapshot = registry.snapshot();
|
|
2216
1880
|
for (const comp of snapshot) {
|
|
2217
1881
|
if (comp.id.toLowerCase() === lower) {
|
|
2218
|
-
|
|
1882
|
+
appilotsDebugLog(
|
|
1883
|
+
`resolveComponentId: case-insensitive match "${componentId}" \u2192 "${comp.id}"`
|
|
1884
|
+
);
|
|
2219
1885
|
return comp.id;
|
|
2220
1886
|
}
|
|
2221
1887
|
}
|
|
@@ -2230,12 +1896,16 @@ function resolveComponentId(componentId, registry, action) {
|
|
|
2230
1896
|
if (metaAction && typeof metaAction === "object" && metaAction.label) {
|
|
2231
1897
|
const metaLabel = metaAction.label;
|
|
2232
1898
|
const normalizedMetaLabel = metaLabel.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
|
|
2233
|
-
|
|
1899
|
+
appilotsDebugLog(
|
|
1900
|
+
`resolveComponentId: found screen metadata for "${componentId}" \u2192 label="${metaLabel}"`
|
|
1901
|
+
);
|
|
2234
1902
|
for (const comp of snapshot) {
|
|
2235
1903
|
if (!comp.label) continue;
|
|
2236
1904
|
const compLabel = comp.label.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
|
|
2237
1905
|
if (compLabel === normalizedMetaLabel || compLabel.includes(normalizedMetaLabel) || normalizedMetaLabel.includes(compLabel)) {
|
|
2238
|
-
|
|
1906
|
+
appilotsDebugLog(
|
|
1907
|
+
`resolveComponentId: FOUND via metadata: "${componentId}" \u2192 "${comp.id}" (label="${comp.label}")`
|
|
1908
|
+
);
|
|
2239
1909
|
return comp.id;
|
|
2240
1910
|
}
|
|
2241
1911
|
}
|
|
@@ -2250,8 +1920,8 @@ function resolveComponentId(componentId, registry, action) {
|
|
|
2250
1920
|
}));
|
|
2251
1921
|
const match = resolveTarget(componentId, candidates, { action });
|
|
2252
1922
|
if (match.status === "matched") {
|
|
2253
|
-
|
|
2254
|
-
`
|
|
1923
|
+
appilotsDebugLog(
|
|
1924
|
+
`resolveComponentId: matched "${componentId}" -> "${match.id}" (tier=${match.tier})`
|
|
2255
1925
|
);
|
|
2256
1926
|
return match.id;
|
|
2257
1927
|
}
|
|
@@ -2340,8 +2010,8 @@ function setValueHandler(rawComponentId, requested, registry) {
|
|
|
2340
2010
|
try {
|
|
2341
2011
|
slider.setValue(applied);
|
|
2342
2012
|
if (applied !== requestedValue) {
|
|
2343
|
-
|
|
2344
|
-
`
|
|
2013
|
+
appilotsDebugLog(
|
|
2014
|
+
`uiInteractionHandler: set_value snapped ${requestedValue} \u2192 ${applied} for "${componentId}" (step=${step})`
|
|
2345
2015
|
);
|
|
2346
2016
|
}
|
|
2347
2017
|
return { success: true, effect: "changed" };
|
|
@@ -2375,6 +2045,21 @@ function checkModalOcclusion(rawComponentId, action) {
|
|
|
2375
2045
|
return null;
|
|
2376
2046
|
}
|
|
2377
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
|
+
}
|
|
2378
2063
|
const fiberRoot = getFiberRoot();
|
|
2379
2064
|
if (!fiberRoot) return null;
|
|
2380
2065
|
let snap;
|
|
@@ -2406,9 +2091,12 @@ function checkModalOcclusion(rawComponentId, action) {
|
|
|
2406
2091
|
modalOptionLabels.push(button.label ?? button.id);
|
|
2407
2092
|
}
|
|
2408
2093
|
}
|
|
2409
|
-
for (const toggle of snap.toggles ?? [])
|
|
2410
|
-
|
|
2411
|
-
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);
|
|
2412
2100
|
for (const list of snap.lists ?? []) {
|
|
2413
2101
|
for (const item of list.items ?? []) {
|
|
2414
2102
|
const itemInModal = (item.buttons ?? []).some((b) => b.inModal) || (item.inputs ?? []).some((i) => i.inModal) || (item.toggles ?? []).some((t) => t.inModal);
|
|
@@ -2443,7 +2131,10 @@ function uiInteractionHandler(payload, context) {
|
|
|
2443
2131
|
return {
|
|
2444
2132
|
success: false,
|
|
2445
2133
|
error: "UI interaction is not permitted",
|
|
2446
|
-
diagnose: buildUiDiagnose(
|
|
2134
|
+
diagnose: buildUiDiagnose(
|
|
2135
|
+
payload.componentId ?? payload.targetId ?? "",
|
|
2136
|
+
"unknown"
|
|
2137
|
+
)
|
|
2447
2138
|
};
|
|
2448
2139
|
}
|
|
2449
2140
|
const { action } = payload;
|
|
@@ -2457,8 +2148,8 @@ function uiInteractionHandler(payload, context) {
|
|
|
2457
2148
|
if (sliderId) {
|
|
2458
2149
|
const smuggled = payload.value ?? payload.params?.value;
|
|
2459
2150
|
if (smuggled !== void 0) {
|
|
2460
|
-
|
|
2461
|
-
`
|
|
2151
|
+
appilotsDebugLog(
|
|
2152
|
+
`uiInteractionHandler: "${actionStr}" on slider "${sliderId}" carried value=${JSON.stringify(smuggled)} \u2014 routing to set_value`
|
|
2462
2153
|
);
|
|
2463
2154
|
return setValueHandler(rawComponentId, smuggled, registry);
|
|
2464
2155
|
}
|
|
@@ -2468,18 +2159,53 @@ function uiInteractionHandler(payload, context) {
|
|
|
2468
2159
|
diagnose: { ...buildUiDiagnose(sliderId, "unknown"), recoverable: true }
|
|
2469
2160
|
};
|
|
2470
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
|
+
}
|
|
2471
2197
|
const occluded = checkModalOcclusion(rawComponentId, actionStr);
|
|
2472
2198
|
if (occluded) {
|
|
2473
|
-
|
|
2474
|
-
`
|
|
2199
|
+
appilotsDebugLog(
|
|
2200
|
+
`uiInteractionHandler: blocked "${actionStr}" on "${rawComponentId}" \u2014 target is behind an open modal`
|
|
2475
2201
|
);
|
|
2476
2202
|
return occluded;
|
|
2477
2203
|
}
|
|
2478
2204
|
if (typeof rawComponentId === "string" && rawComponentId.trim().toLowerCase().startsWith("el:")) {
|
|
2479
2205
|
const result = dispatchElementOrTarget(rawComponentId, registry, actionStr);
|
|
2480
2206
|
if (result.success) {
|
|
2481
|
-
|
|
2482
|
-
`
|
|
2207
|
+
appilotsDebugLog(
|
|
2208
|
+
`uiInteractionHandler: interaction element dispatch succeeded for "${rawComponentId}"`
|
|
2483
2209
|
);
|
|
2484
2210
|
return { success: true };
|
|
2485
2211
|
}
|
|
@@ -2487,13 +2213,13 @@ function uiInteractionHandler(payload, context) {
|
|
|
2487
2213
|
}
|
|
2488
2214
|
const ordinal = parseSyntheticListItemId(rawComponentId);
|
|
2489
2215
|
if (ordinal && (action === "press" || actionStr === "tap" || actionStr === "click" || actionStr === "longPress")) {
|
|
2490
|
-
|
|
2491
|
-
`
|
|
2216
|
+
appilotsDebugLog(
|
|
2217
|
+
`uiInteractionHandler: dispatching synthetic list ordinal \u2014 componentId="${rawComponentId}" list=${ordinal.listIndex} item=${ordinal.itemIndex}`
|
|
2492
2218
|
);
|
|
2493
2219
|
const result = pressListItemAtOrdinal(ordinal.listIndex, ordinal.itemIndex);
|
|
2494
2220
|
if (result.ok) {
|
|
2495
|
-
|
|
2496
|
-
`
|
|
2221
|
+
appilotsDebugLog(
|
|
2222
|
+
`uiInteractionHandler: synthetic ordinal press succeeded (container=${result.containerType ?? "List"})`
|
|
2497
2223
|
);
|
|
2498
2224
|
return { success: true };
|
|
2499
2225
|
}
|
|
@@ -2538,7 +2264,9 @@ function uiInteractionHandler(payload, context) {
|
|
|
2538
2264
|
};
|
|
2539
2265
|
}
|
|
2540
2266
|
const componentId = resolveComponentId(rawComponentId, registry, actionStr);
|
|
2541
|
-
|
|
2267
|
+
appilotsDebugLog(
|
|
2268
|
+
`uiInteractionHandler: resolved componentId="${payload.componentId}" \u2192 "${componentId}"`
|
|
2269
|
+
);
|
|
2542
2270
|
const target = registry.getTarget(componentId);
|
|
2543
2271
|
if (target) {
|
|
2544
2272
|
try {
|
|
@@ -2589,7 +2317,9 @@ function uiInteractionHandler(payload, context) {
|
|
|
2589
2317
|
return { success: false, error: err?.message ?? "Focus failed" };
|
|
2590
2318
|
}
|
|
2591
2319
|
}
|
|
2592
|
-
|
|
2320
|
+
appilotsDebugLog(
|
|
2321
|
+
`uiInteractionHandler: registry miss for "${componentId}", falling back to fiber tree`
|
|
2322
|
+
);
|
|
2593
2323
|
const handle = findInteractiveByIdentifier(componentId, {
|
|
2594
2324
|
kind: actionStr === "toggle" ? "toggle" : actionStr === "focus" ? "field" : "target"
|
|
2595
2325
|
});
|
|
@@ -2597,7 +2327,9 @@ function uiInteractionHandler(payload, context) {
|
|
|
2597
2327
|
try {
|
|
2598
2328
|
if (actionStr === "press" && handle.kind === "target" && handle.press) {
|
|
2599
2329
|
handle.press();
|
|
2600
|
-
|
|
2330
|
+
appilotsDebugLog(
|
|
2331
|
+
`uiInteractionHandler: fiber fallback press succeeded for "${componentId}"`
|
|
2332
|
+
);
|
|
2601
2333
|
return { success: true };
|
|
2602
2334
|
}
|
|
2603
2335
|
if (actionStr === "press" && handle.kind === "toggle" && handle.setValue) {
|
|
@@ -2743,12 +2475,10 @@ function readVisibleValidation(fieldId, label) {
|
|
|
2743
2475
|
try {
|
|
2744
2476
|
const snap = captureSnapshot();
|
|
2745
2477
|
const texts = snap.texts ?? [];
|
|
2746
|
-
const needles = [fieldId, label].filter(
|
|
2747
|
-
(s) => !!s && s.length > 0
|
|
2748
|
-
);
|
|
2478
|
+
const needles = [fieldId, label].filter((s) => !!s && s.length > 0);
|
|
2749
2479
|
for (const text of texts) {
|
|
2750
2480
|
const lower = text.toLowerCase();
|
|
2751
|
-
if (
|
|
2481
|
+
if (FORM_VALIDATION_TEXT_RE2.test(lower)) {
|
|
2752
2482
|
for (const needle of needles) {
|
|
2753
2483
|
if (lower.includes(needle.toLowerCase())) return text;
|
|
2754
2484
|
}
|
|
@@ -2869,8 +2599,10 @@ async function tryPressSelectOption(fieldId, value, registry, options = {}) {
|
|
|
2869
2599
|
function normalizeFieldType(type) {
|
|
2870
2600
|
if (typeof type !== "string") return void 0;
|
|
2871
2601
|
const normalized = normalize3(type);
|
|
2872
|
-
if (normalized === "select" || normalized === "picker" || normalized === "dropdown")
|
|
2873
|
-
|
|
2602
|
+
if (normalized === "select" || normalized === "picker" || normalized === "dropdown")
|
|
2603
|
+
return "select";
|
|
2604
|
+
if (normalized === "toggle" || normalized === "switch" || normalized === "checkbox")
|
|
2605
|
+
return "toggle";
|
|
2874
2606
|
if (normalized === "date") return "date";
|
|
2875
2607
|
if (normalized === "number" || normalized === "numeric") return "number";
|
|
2876
2608
|
if (normalized === "custom") return "custom";
|
|
@@ -2904,10 +2636,7 @@ function fieldCueTerms(fieldId, label) {
|
|
|
2904
2636
|
function snapshotHasFieldCue(snap, fieldId, label) {
|
|
2905
2637
|
const terms = fieldCueTerms(fieldId, label);
|
|
2906
2638
|
if (terms.length === 0) return false;
|
|
2907
|
-
const visible = [
|
|
2908
|
-
...snap.texts ?? [],
|
|
2909
|
-
...(snap.buttons ?? []).map((b) => b.label ?? b.id ?? "")
|
|
2910
|
-
].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);
|
|
2911
2640
|
return visible.some((text) => terms.some((term) => text.includes(term) || term.includes(text)));
|
|
2912
2641
|
}
|
|
2913
2642
|
function scoreCandidateText(value, texts) {
|
|
@@ -2981,9 +2710,7 @@ function buildChoiceCandidates(snap, fieldId, value, allowFirstVisibleWhenNoCue,
|
|
|
2981
2710
|
fieldCueVisible && elements.length <= 12 ? 50 : 0,
|
|
2982
2711
|
...cueTexts.map((text) => {
|
|
2983
2712
|
const normalized = normalize3(text);
|
|
2984
|
-
return fieldTerms.some(
|
|
2985
|
-
(term) => normalized.includes(term) || term.includes(normalized)
|
|
2986
|
-
) ? 80 : 0;
|
|
2713
|
+
return fieldTerms.some((term) => normalized.includes(term) || term.includes(normalized)) ? 80 : 0;
|
|
2987
2714
|
})
|
|
2988
2715
|
);
|
|
2989
2716
|
const valueScore = genericValue ? 0 : scoreCandidateText(value, texts);
|
|
@@ -3055,7 +2782,11 @@ async function selectVisibleChoiceAsync(fieldId, value, registry, options) {
|
|
|
3055
2782
|
return {
|
|
3056
2783
|
success: false,
|
|
3057
2784
|
error: `No visible option found for select field "${fieldId}"`,
|
|
3058
|
-
diagnose: {
|
|
2785
|
+
diagnose: {
|
|
2786
|
+
category: "component-not-found",
|
|
2787
|
+
fieldId,
|
|
2788
|
+
screen: getCurrentScreen() ?? void 0
|
|
2789
|
+
}
|
|
3059
2790
|
};
|
|
3060
2791
|
}
|
|
3061
2792
|
const pressed = pressTargetByIdOrLabel(candidate.targetId, registry);
|
|
@@ -3087,17 +2818,19 @@ function pressContinuationButton(registry) {
|
|
|
3087
2818
|
return pressTargetByIdOrLabel(id, registry);
|
|
3088
2819
|
}
|
|
3089
2820
|
function findField(fieldId, registry) {
|
|
3090
|
-
|
|
2821
|
+
appilotsDebugLog(`findField: searching for fieldId="${fieldId}"`);
|
|
3091
2822
|
const exact = registry.getField(fieldId);
|
|
3092
2823
|
if (exact) {
|
|
3093
|
-
|
|
2824
|
+
appilotsDebugLog(`findField: FOUND exact match for fieldId="${fieldId}"`);
|
|
3094
2825
|
return exact;
|
|
3095
2826
|
}
|
|
3096
2827
|
const lower = fieldId.toLowerCase();
|
|
3097
2828
|
const snapshot = registry.snapshot();
|
|
3098
2829
|
for (const comp of snapshot) {
|
|
3099
2830
|
if (comp.kind === "field" && comp.id.toLowerCase() === lower) {
|
|
3100
|
-
|
|
2831
|
+
appilotsDebugLog(
|
|
2832
|
+
`findField: FOUND case-insensitive match: registered="${comp.id}" for searched="${fieldId}"`
|
|
2833
|
+
);
|
|
3101
2834
|
return registry.getField(comp.id);
|
|
3102
2835
|
}
|
|
3103
2836
|
}
|
|
@@ -3125,8 +2858,8 @@ function findField(fieldId, registry) {
|
|
|
3125
2858
|
}
|
|
3126
2859
|
}
|
|
3127
2860
|
if (bestComp && bestScore >= 75) {
|
|
3128
|
-
|
|
3129
|
-
`
|
|
2861
|
+
appilotsDebugLog(
|
|
2862
|
+
`findField: scored match "${fieldId}" -> "${bestComp.id}" (label="${bestComp.label}", score=${bestScore})`
|
|
3130
2863
|
);
|
|
3131
2864
|
return registry.getField(bestComp.id);
|
|
3132
2865
|
}
|
|
@@ -3152,8 +2885,8 @@ function findField(fieldId, registry) {
|
|
|
3152
2885
|
}
|
|
3153
2886
|
}
|
|
3154
2887
|
if (mBest && mBestScore >= 60) {
|
|
3155
|
-
|
|
3156
|
-
`
|
|
2888
|
+
appilotsDebugLog(
|
|
2889
|
+
`findField: FOUND via screen metadata translation: fieldId="${fieldId}" -> metaLabel="${metaLabel}" -> registered="${mBest.id}" (score=${mBestScore})`
|
|
3157
2890
|
);
|
|
3158
2891
|
return registry.getField(mBest.id);
|
|
3159
2892
|
}
|
|
@@ -3161,8 +2894,8 @@ function findField(fieldId, registry) {
|
|
|
3161
2894
|
}
|
|
3162
2895
|
}
|
|
3163
2896
|
if (bestComp && bestScore >= 40) {
|
|
3164
|
-
|
|
3165
|
-
`
|
|
2897
|
+
appilotsDebugLog(
|
|
2898
|
+
`findField: weak substring match "${fieldId}" -> "${bestComp.id}" (label="${bestComp.label}", score=${bestScore})`
|
|
3166
2899
|
);
|
|
3167
2900
|
return registry.getField(bestComp.id);
|
|
3168
2901
|
}
|
|
@@ -3172,17 +2905,19 @@ function findField(fieldId, registry) {
|
|
|
3172
2905
|
return void 0;
|
|
3173
2906
|
}
|
|
3174
2907
|
function findToggle(fieldId, registry) {
|
|
3175
|
-
|
|
2908
|
+
appilotsDebugLog(`findToggle: searching for fieldId="${fieldId}"`);
|
|
3176
2909
|
const exact = registry.getToggle(fieldId);
|
|
3177
2910
|
if (exact) {
|
|
3178
|
-
|
|
2911
|
+
appilotsDebugLog(`findToggle: FOUND exact match for "${fieldId}"`);
|
|
3179
2912
|
return exact;
|
|
3180
2913
|
}
|
|
3181
2914
|
const lower = fieldId.toLowerCase();
|
|
3182
2915
|
const snapshot = registry.snapshot();
|
|
3183
2916
|
for (const comp of snapshot) {
|
|
3184
2917
|
if (comp.kind === "toggle" && comp.id.toLowerCase() === lower) {
|
|
3185
|
-
|
|
2918
|
+
appilotsDebugLog(
|
|
2919
|
+
`findToggle: FOUND case-insensitive match: registered="${comp.id}" for searched="${fieldId}"`
|
|
2920
|
+
);
|
|
3186
2921
|
return registry.getToggle(comp.id);
|
|
3187
2922
|
}
|
|
3188
2923
|
}
|
|
@@ -3197,7 +2932,9 @@ function findSlider(fieldId, registry) {
|
|
|
3197
2932
|
const lower = fieldId.toLowerCase();
|
|
3198
2933
|
for (const comp of registry.snapshot()) {
|
|
3199
2934
|
if (comp.kind === "slider" && comp.id.toLowerCase() === lower) {
|
|
3200
|
-
|
|
2935
|
+
appilotsDebugLog(
|
|
2936
|
+
`findSlider: FOUND case-insensitive match: registered="${comp.id}" for searched="${fieldId}"`
|
|
2937
|
+
);
|
|
3201
2938
|
return registry.getSlider(comp.id);
|
|
3202
2939
|
}
|
|
3203
2940
|
}
|
|
@@ -3229,7 +2966,7 @@ async function formFillHandler(payload, context) {
|
|
|
3229
2966
|
"formFillHandler: registry is EMPTY \u2014 auto-tracking may not be active. Ensure enableAppilotsAutoTracking() runs before components mount."
|
|
3230
2967
|
);
|
|
3231
2968
|
} else {
|
|
3232
|
-
|
|
2969
|
+
appilotsDebugLog(
|
|
3233
2970
|
`[Appilots formFillHandler] Registry has ${registrySnapshot.length} components:`,
|
|
3234
2971
|
registrySnapshot.map((c) => `${c.kind}:${c.id}`).join(", ")
|
|
3235
2972
|
);
|
|
@@ -3237,7 +2974,7 @@ async function formFillHandler(payload, context) {
|
|
|
3237
2974
|
const fieldResults = [];
|
|
3238
2975
|
let allSuccess = true;
|
|
3239
2976
|
let selectedChoiceThisBatch = false;
|
|
3240
|
-
|
|
2977
|
+
appilotsDebugLog(`formFillHandler: processing ${payload.fields.length} fields`);
|
|
3241
2978
|
const availableFieldIds = () => registry.snapshot().filter((comp) => comp.kind === "field").map((comp) => `${comp.id}${comp.label ? ` (${comp.label})` : ""}`).slice(0, 8);
|
|
3242
2979
|
const isElementIdField = (field) => typeof field.fieldId === "string" && field.fieldId.trim().toLowerCase().startsWith("el:");
|
|
3243
2980
|
for (const field of payload.fields.filter(isElementIdField)) {
|
|
@@ -3264,8 +3001,8 @@ async function formFillHandler(payload, context) {
|
|
|
3264
3001
|
const { fieldId, fieldType, value } = field;
|
|
3265
3002
|
const effectiveFieldType = getEffectiveFieldType(fieldId, fieldType, registry);
|
|
3266
3003
|
let selectFailure;
|
|
3267
|
-
|
|
3268
|
-
`
|
|
3004
|
+
appilotsDebugLog(
|
|
3005
|
+
`formFillHandler: looking up fieldId="${fieldId}" fieldType="${fieldType}" effectiveFieldType="${effectiveFieldType}" value=${describeValue(value)}`
|
|
3269
3006
|
);
|
|
3270
3007
|
const fieldEntry = findField(fieldId, registry);
|
|
3271
3008
|
if (fieldEntry) {
|
|
@@ -3316,7 +3053,11 @@ async function formFillHandler(payload, context) {
|
|
|
3316
3053
|
fieldId,
|
|
3317
3054
|
success: false,
|
|
3318
3055
|
error: selectResult.error ?? `Select value "${String(value)}" was not applied for "${fieldId}"`,
|
|
3319
|
-
diagnose: selectResult.diagnose ?? {
|
|
3056
|
+
diagnose: selectResult.diagnose ?? {
|
|
3057
|
+
category: "component-not-found",
|
|
3058
|
+
fieldId,
|
|
3059
|
+
screen: screenPath
|
|
3060
|
+
}
|
|
3320
3061
|
});
|
|
3321
3062
|
allSuccess = false;
|
|
3322
3063
|
continue;
|
|
@@ -3371,7 +3112,9 @@ async function formFillHandler(payload, context) {
|
|
|
3371
3112
|
continue;
|
|
3372
3113
|
}
|
|
3373
3114
|
try {
|
|
3374
|
-
sliderEntry.setValue(
|
|
3115
|
+
sliderEntry.setValue(
|
|
3116
|
+
snapSliderValue(numeric, sliderEntry.min, sliderEntry.max, sliderEntry.step)
|
|
3117
|
+
);
|
|
3375
3118
|
fieldResults.push({ fieldId, success: true });
|
|
3376
3119
|
} catch (err) {
|
|
3377
3120
|
fieldResults.push({
|
|
@@ -3420,8 +3163,8 @@ async function formFillHandler(payload, context) {
|
|
|
3420
3163
|
}
|
|
3421
3164
|
if (selectResult.success) {
|
|
3422
3165
|
selectedChoiceThisBatch = true;
|
|
3423
|
-
|
|
3424
|
-
`
|
|
3166
|
+
appilotsDebugLog(
|
|
3167
|
+
`formFillHandler: selected visible option for "${fieldId}"` + (selectResult.selectedLabel ? ` \u2192 "${selectResult.selectedLabel}"` : "")
|
|
3425
3168
|
);
|
|
3426
3169
|
await sleep2(120);
|
|
3427
3170
|
fieldResults.push({ fieldId, success: true });
|
|
@@ -3429,7 +3172,7 @@ async function formFillHandler(payload, context) {
|
|
|
3429
3172
|
}
|
|
3430
3173
|
selectFailure = selectResult;
|
|
3431
3174
|
}
|
|
3432
|
-
|
|
3175
|
+
appilotsDebugLog(`formFillHandler: registry miss for "${fieldId}", falling back to fiber tree`);
|
|
3433
3176
|
const handle = findInteractiveByIdentifier(fieldId, {
|
|
3434
3177
|
kind: effectiveFieldType === "toggle" ? "toggle" : "field"
|
|
3435
3178
|
});
|
|
@@ -3437,7 +3180,9 @@ async function formFillHandler(payload, context) {
|
|
|
3437
3180
|
try {
|
|
3438
3181
|
handle.focus?.();
|
|
3439
3182
|
handle.setValue(value);
|
|
3440
|
-
|
|
3183
|
+
appilotsDebugLog(
|
|
3184
|
+
`formFillHandler: fiber fallback succeeded for "${fieldId}" \u2192 label="${handle.label}"`
|
|
3185
|
+
);
|
|
3441
3186
|
fieldResults.push({ fieldId, success: true });
|
|
3442
3187
|
continue;
|
|
3443
3188
|
} catch (err) {
|
|
@@ -3479,7 +3224,15 @@ async function formFillHandler(payload, context) {
|
|
|
3479
3224
|
);
|
|
3480
3225
|
} else {
|
|
3481
3226
|
await sleep2(120);
|
|
3482
|
-
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
|
+
);
|
|
3483
3236
|
if (!submitResult.success) {
|
|
3484
3237
|
appilotsDebugWarn(`formFillHandler: submitAfterFill failed \u2014 ${submitResult.error}`);
|
|
3485
3238
|
return {
|
|
@@ -3517,7 +3270,7 @@ async function formFillHandler(payload, context) {
|
|
|
3517
3270
|
}
|
|
3518
3271
|
};
|
|
3519
3272
|
}
|
|
3520
|
-
|
|
3273
|
+
appilotsDebugLog("formFillHandler: submitAfterFill \u2014 submit completed");
|
|
3521
3274
|
}
|
|
3522
3275
|
}
|
|
3523
3276
|
const aggregatedDiagnose = aggregateFieldDiagnoses(fieldResults, screenPath);
|
|
@@ -3539,8 +3292,8 @@ function pressSubmitButton(context, registry) {
|
|
|
3539
3292
|
if (target) {
|
|
3540
3293
|
try {
|
|
3541
3294
|
target.press();
|
|
3542
|
-
|
|
3543
|
-
`
|
|
3295
|
+
appilotsDebugLog(
|
|
3296
|
+
`formFillHandler: submitAfterFill \u2014 pressed registry target "${candidate}"` + (resolvedScreen ? ` on screen "${resolvedScreen}"` : "")
|
|
3544
3297
|
);
|
|
3545
3298
|
return { success: true, pressedTargetId: candidate };
|
|
3546
3299
|
} catch (err) {
|
|
@@ -3564,9 +3317,7 @@ function pressSubmitButton(context, registry) {
|
|
|
3564
3317
|
if (handle?.press) {
|
|
3565
3318
|
try {
|
|
3566
3319
|
handle.press();
|
|
3567
|
-
|
|
3568
|
-
`[Appilots] formFillHandler: submitAfterFill \u2014 fiber fallback "${submitLabel}"`
|
|
3569
|
-
);
|
|
3320
|
+
appilotsDebugLog(`formFillHandler: submitAfterFill \u2014 fiber fallback "${submitLabel}"`);
|
|
3570
3321
|
return { success: true, pressedTargetId: submitId ?? submitLabel };
|
|
3571
3322
|
} catch (err) {
|
|
3572
3323
|
return {
|
|
@@ -3588,9 +3339,7 @@ function pressSubmitButton(context, registry) {
|
|
|
3588
3339
|
if (id) {
|
|
3589
3340
|
const pressed = pressTargetByIdOrLabel(id, registry);
|
|
3590
3341
|
if (pressed.success) {
|
|
3591
|
-
|
|
3592
|
-
`[Appilots] formFillHandler: submitAfterFill \u2014 pressed visible heuristic "${id}"`
|
|
3593
|
-
);
|
|
3342
|
+
appilotsDebugLog(`formFillHandler: submitAfterFill \u2014 pressed visible heuristic "${id}"`);
|
|
3594
3343
|
return { success: true, pressedTargetId: id };
|
|
3595
3344
|
}
|
|
3596
3345
|
}
|
|
@@ -3604,8 +3353,8 @@ function pressSubmitButton(context, registry) {
|
|
|
3604
3353
|
if (target) {
|
|
3605
3354
|
try {
|
|
3606
3355
|
target.press();
|
|
3607
|
-
|
|
3608
|
-
`
|
|
3356
|
+
appilotsDebugLog(
|
|
3357
|
+
`formFillHandler: submitAfterFill \u2014 pressed by label heuristic "${comp.label}"`
|
|
3609
3358
|
);
|
|
3610
3359
|
return { success: true, pressedTargetId: comp.id };
|
|
3611
3360
|
} catch (err) {
|
|
@@ -3617,9 +3366,16 @@ function pressSubmitButton(context, registry) {
|
|
|
3617
3366
|
}
|
|
3618
3367
|
}
|
|
3619
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
|
+
}
|
|
3620
3376
|
return {
|
|
3621
3377
|
success: false,
|
|
3622
|
-
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)',
|
|
3623
3379
|
diagnose: { category: "component-not-found", screen: resolvedScreen }
|
|
3624
3380
|
};
|
|
3625
3381
|
}
|
|
@@ -3673,6 +3429,8 @@ function resolveListEntry(listId) {
|
|
|
3673
3429
|
return { candidates: entries.map((e) => e.id) };
|
|
3674
3430
|
}
|
|
3675
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] };
|
|
3676
3434
|
return { candidates: entries.map((e) => e.id) };
|
|
3677
3435
|
}
|
|
3678
3436
|
function pageOffset(metrics, direction) {
|
|
@@ -3712,7 +3470,7 @@ async function scrollListHandler(payload, context) {
|
|
|
3712
3470
|
}
|
|
3713
3471
|
const { entry, candidates } = resolveListEntry(payload.listId);
|
|
3714
3472
|
if (!entry) {
|
|
3715
|
-
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).";
|
|
3716
3474
|
return {
|
|
3717
3475
|
success: false,
|
|
3718
3476
|
error: detail,
|
|
@@ -3720,6 +3478,13 @@ async function scrollListHandler(payload, context) {
|
|
|
3720
3478
|
};
|
|
3721
3479
|
}
|
|
3722
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
|
+
}
|
|
3723
3488
|
if (typeof entry.itemCount === "number" && toIndex > entry.itemCount) {
|
|
3724
3489
|
return {
|
|
3725
3490
|
success: false,
|
|
@@ -3754,14 +3519,14 @@ async function scrollListHandler(payload, context) {
|
|
|
3754
3519
|
}
|
|
3755
3520
|
return {
|
|
3756
3521
|
success: false,
|
|
3757
|
-
error: `
|
|
3522
|
+
error: `"${entry.id}" does not expose a scrollable handle`,
|
|
3758
3523
|
diagnose: buildDiagnose("unknown", entry.id)
|
|
3759
3524
|
};
|
|
3760
3525
|
}
|
|
3761
3526
|
|
|
3762
3527
|
// src/executor/handlers/confirmHandler.ts
|
|
3763
3528
|
async function confirmHandler(_actionId, _payload) {
|
|
3764
|
-
return { success: true };
|
|
3529
|
+
return { success: true, effect: "unknown" };
|
|
3765
3530
|
}
|
|
3766
3531
|
|
|
3767
3532
|
// src/executor/postInteractionSettle.ts
|
|
@@ -3773,14 +3538,45 @@ function targetIdFromPayload(payload) {
|
|
|
3773
3538
|
if (typeof targetId === "string" && targetId.trim()) return targetId.trim();
|
|
3774
3539
|
return "";
|
|
3775
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
|
+
}
|
|
3776
3563
|
async function waitForPostInteractionSettle(payload) {
|
|
3777
3564
|
const targetId = targetIdFromPayload(payload);
|
|
3778
3565
|
const fromScreen = getCurrentScreen();
|
|
3779
3566
|
const baseline = probeLoadingState(targetId || null, fromScreen);
|
|
3567
|
+
const baselineFields = discreteFieldSignature();
|
|
3568
|
+
const readable = baseline.fingerprint !== "";
|
|
3780
3569
|
await sleepMs(100);
|
|
3781
3570
|
const afterPress = probeLoadingState(targetId || null, getCurrentScreen());
|
|
3782
3571
|
const modalOrLoading = afterPress.modalOpen || afterPress.loading || afterPress.pressedDisabled;
|
|
3783
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
|
+
};
|
|
3784
3580
|
const capMs = isListItemPressTarget(targetId) ? 5e3 : 1e4;
|
|
3785
3581
|
if (modalOrLoading || fingerprintChanged) {
|
|
3786
3582
|
await waitForLoadingSettle({
|
|
@@ -3788,7 +3584,7 @@ async function waitForPostInteractionSettle(payload) {
|
|
|
3788
3584
|
fromScreen,
|
|
3789
3585
|
maxMs: capMs
|
|
3790
3586
|
});
|
|
3791
|
-
return;
|
|
3587
|
+
return verdict();
|
|
3792
3588
|
}
|
|
3793
3589
|
if (isListItemPressTarget(targetId)) {
|
|
3794
3590
|
await waitForLoadingSettle({
|
|
@@ -3796,6 +3592,7 @@ async function waitForPostInteractionSettle(payload) {
|
|
|
3796
3592
|
maxMs: 3e3
|
|
3797
3593
|
});
|
|
3798
3594
|
}
|
|
3595
|
+
return verdict();
|
|
3799
3596
|
}
|
|
3800
3597
|
|
|
3801
3598
|
// src/executor/ActionExecutor.ts
|
|
@@ -3850,10 +3647,9 @@ function validateActionPayload(action) {
|
|
|
3850
3647
|
const hasIndex = typeof payload.toIndex === "number";
|
|
3851
3648
|
const hasDirection = payload.direction === "up" || payload.direction === "down";
|
|
3852
3649
|
if (!hasIndex && !hasDirection) {
|
|
3853
|
-
return invalid(
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
);
|
|
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
|
+
});
|
|
3857
3653
|
}
|
|
3858
3654
|
return null;
|
|
3859
3655
|
}
|
|
@@ -3861,20 +3657,44 @@ function validateActionPayload(action) {
|
|
|
3861
3657
|
return null;
|
|
3862
3658
|
}
|
|
3863
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
|
+
}
|
|
3864
3671
|
async function executeAction(action, context) {
|
|
3865
|
-
|
|
3672
|
+
appilotsDebugLog(
|
|
3673
|
+
`executeAction called: type="${action.type}", id="${action.id}", payload=${JSON.stringify(action.payload)}`
|
|
3674
|
+
);
|
|
3866
3675
|
const permissions = context.permissions ?? DEFAULT_PERMISSIONS;
|
|
3867
|
-
if (permissions.allowedActions && permissions.allowedActions.length > 0 && !permissions.allowedActions.includes(action.type)) {
|
|
3868
|
-
return {
|
|
3869
|
-
success: false,
|
|
3870
|
-
error: `Action type "${action.type}" is not in the allowed actions list`
|
|
3871
|
-
};
|
|
3872
|
-
}
|
|
3873
3676
|
context.emit({
|
|
3874
3677
|
type: "agent:action:start",
|
|
3875
3678
|
timestamp: Date.now(),
|
|
3876
3679
|
data: { actionId: action.id, actionType: action.type }
|
|
3877
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
|
+
}
|
|
3878
3698
|
let result;
|
|
3879
3699
|
const validationFailure = validateActionPayload(action);
|
|
3880
3700
|
if (validationFailure) {
|
|
@@ -3908,25 +3728,25 @@ async function executeAction(action, context) {
|
|
|
3908
3728
|
registry: context.registry ?? getDefaultRegistry()
|
|
3909
3729
|
});
|
|
3910
3730
|
break;
|
|
3911
|
-
case "ui_interaction":
|
|
3912
|
-
|
|
3731
|
+
case "ui_interaction": {
|
|
3732
|
+
const payload = action.payload;
|
|
3733
|
+
result = uiInteractionHandler(payload, {
|
|
3913
3734
|
permissions,
|
|
3914
3735
|
registry: context.registry ?? getDefaultRegistry()
|
|
3915
3736
|
});
|
|
3916
3737
|
if (result.success) {
|
|
3917
|
-
await waitForPostInteractionSettle(
|
|
3738
|
+
const settle = await waitForPostInteractionSettle(payload);
|
|
3739
|
+
result.effect = result.effect ?? effectFor(payload, settle.effect);
|
|
3918
3740
|
}
|
|
3919
3741
|
break;
|
|
3742
|
+
}
|
|
3920
3743
|
case "scroll_list":
|
|
3921
3744
|
result = await scrollListHandler(action.payload, {
|
|
3922
3745
|
permissions
|
|
3923
3746
|
});
|
|
3924
3747
|
break;
|
|
3925
3748
|
case "confirm":
|
|
3926
|
-
result = await confirmHandler(
|
|
3927
|
-
action.id,
|
|
3928
|
-
action.payload
|
|
3929
|
-
);
|
|
3749
|
+
result = await confirmHandler(action.id, action.payload);
|
|
3930
3750
|
break;
|
|
3931
3751
|
case "custom":
|
|
3932
3752
|
result = {
|
|
@@ -3949,7 +3769,9 @@ async function executeAction(action, context) {
|
|
|
3949
3769
|
error: err?.message ?? "Action execution failed unexpectedly"
|
|
3950
3770
|
};
|
|
3951
3771
|
}
|
|
3952
|
-
|
|
3772
|
+
appilotsDebugLog(
|
|
3773
|
+
`executeAction result: type="${action.type}", id="${action.id}", success=${result.success}, error=${result.error ?? "none"}`
|
|
3774
|
+
);
|
|
3953
3775
|
context.emit({
|
|
3954
3776
|
type: result.success ? "agent:action:complete" : "agent:action:error",
|
|
3955
3777
|
timestamp: Date.now(),
|
|
@@ -4001,9 +3823,7 @@ var _patchedAlert = (title, message, buttons, options) => {
|
|
|
4001
3823
|
return suppression.originalAlert(title, message, buttons, options);
|
|
4002
3824
|
}
|
|
4003
3825
|
suppression.consumed = true;
|
|
4004
|
-
|
|
4005
|
-
"[Appilots] Suppressing native Alert during already-confirmed destructive agent action"
|
|
4006
|
-
);
|
|
3826
|
+
appilotsDebugLog("Suppressing native Alert during already-confirmed destructive agent action");
|
|
4007
3827
|
try {
|
|
4008
3828
|
suppression.pending = Promise.resolve(button.onPress()).then(() => void 0).catch((err) => reportHandlerFailure(err));
|
|
4009
3829
|
} catch (err) {
|
|
@@ -4128,13 +3948,10 @@ async function settleTurn(input) {
|
|
|
4128
3948
|
targetScreen: navigateTargetScreen,
|
|
4129
3949
|
maxMs: 3500
|
|
4130
3950
|
});
|
|
4131
|
-
|
|
4132
|
-
`
|
|
3951
|
+
appilotsDebugLog(
|
|
3952
|
+
`settleTurn: post-navigate settle done \u2014 screen="${settle.screen}" transitioned=${settle.transitioned} timedOut=${settle.timedOut} waitedMs=${settle.waitedMs}`
|
|
4133
3953
|
);
|
|
4134
|
-
const navigationMoved = settle.transitioned || screenDepartedBaseline(
|
|
4135
|
-
input.preNavigateScreen ?? null,
|
|
4136
|
-
input.preNavigateSignature ?? null
|
|
4137
|
-
) || !!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);
|
|
4138
3955
|
if (!navigationMoved) {
|
|
4139
3956
|
appilotsDebugWarn(
|
|
4140
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.`
|
|
@@ -4147,8 +3964,8 @@ async function settleTurn(input) {
|
|
|
4147
3964
|
fromScreen: settle.screen ?? null,
|
|
4148
3965
|
maxMs: 3e3
|
|
4149
3966
|
});
|
|
4150
|
-
|
|
4151
|
-
`
|
|
3967
|
+
appilotsDebugLog(
|
|
3968
|
+
`settleTurn: post-navigate loading settle \u2014 settled=${loadSettle.settled} loadingPending=${loadSettle.loadingPending} probes=${loadSettle.probeCount} waitedMs=${loadSettle.waitedMs}`
|
|
4152
3969
|
);
|
|
4153
3970
|
if (loadSettle.loadingPending) loadingPending = true;
|
|
4154
3971
|
} else {
|
|
@@ -4162,8 +3979,8 @@ async function settleTurn(input) {
|
|
|
4162
3979
|
fromScreen: getCurrentScreen(),
|
|
4163
3980
|
maxMs: settleCapMs
|
|
4164
3981
|
});
|
|
4165
|
-
|
|
4166
|
-
`
|
|
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)" : ""}`
|
|
4167
3984
|
);
|
|
4168
3985
|
if (loadSettle.loadingPending) loadingPending = true;
|
|
4169
3986
|
const listPressActions = turnActions.filter((action) => {
|
|
@@ -4184,8 +4001,8 @@ async function settleTurn(input) {
|
|
|
4184
4001
|
maxMs: 4e3
|
|
4185
4002
|
});
|
|
4186
4003
|
const afterFingerprint = probeLoadingState(pressedId, getCurrentScreen()).fingerprint;
|
|
4187
|
-
|
|
4188
|
-
`
|
|
4004
|
+
appilotsDebugLog(
|
|
4005
|
+
`settleTurn: post-list-press settle \u2014 screen="${postPressSettle.screen}" transitioned=${postPressSettle.transitioned} timedOut=${postPressSettle.timedOut} waitedMs=${postPressSettle.waitedMs}`
|
|
4189
4006
|
);
|
|
4190
4007
|
if (!postPressSettle.transitioned && baselineFingerprint === afterFingerprint) {
|
|
4191
4008
|
for (const action of listPressActions) {
|
|
@@ -4378,7 +4195,7 @@ function useSuggestedPrompts(options) {
|
|
|
4378
4195
|
if (cancelled || requestIdRef.current !== requestId) return;
|
|
4379
4196
|
setPrompts(mergePromptSources(localDev, data, limit));
|
|
4380
4197
|
}).catch((err) => {
|
|
4381
|
-
|
|
4198
|
+
appilotsDebugWarn("useSuggestedPrompts: fetch failed", err);
|
|
4382
4199
|
}).finally(() => {
|
|
4383
4200
|
if (cancelled || requestIdRef.current !== requestId) return;
|
|
4384
4201
|
setIsLoading(false);
|
|
@@ -4576,4 +4393,4 @@ function useAppilotsSlider(id, options) {
|
|
|
4576
4393
|
}, [id, registry, getValue, setValue, min, max, step, label, screen]);
|
|
4577
4394
|
}
|
|
4578
4395
|
|
|
4579
|
-
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 };
|