mobile-debug-mcp 0.30.0 → 0.30.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/interact/index.js +876 -155
- package/dist/observe/index.js +1 -0
- package/dist/observe/snapshot-metadata.js +62 -1
- package/dist/server/tool-definitions.js +20 -11
- package/dist/server/tool-handlers.js +7 -5
- package/dist/server-core.js +1 -1
- package/docs/CHANGELOG.md +9 -0
- package/docs/ROADMAP.md +85 -209
- package/docs/{rfcs → specs-deltas}/013-wait-and-synchronization-reliability.md +15 -35
- package/docs/{rfcs → specs-deltas}/014-actionability-resolution.md +32 -30
- package/docs/specs-deltas/015-adjustable-control-support.md +541 -0
- package/package.json +1 -1
- package/skills/spec-review/SKILL.md +52 -0
- package/skills/spec-review/references/spec-review-checklist.md +13 -0
- package/skills/spec-review/references/spec-review-template.md +28 -0
- package/src/interact/index.ts +1008 -171
- package/src/observe/index.ts +1 -0
- package/src/observe/snapshot-metadata.ts +69 -2
- package/src/server/tool-definitions.ts +20 -11
- package/src/server/tool-handlers.ts +7 -5
- package/src/server-core.ts +1 -1
- package/src/types.ts +26 -0
- package/test/unit/interact/adjust_control.test.ts +690 -33
- package/test/unit/interact/subtree_collection.test.ts +24 -0
- package/test/unit/interact/tap_element.test.ts +71 -0
- package/test/unit/interact/wait_for_ui_change.test.ts +92 -1
- package/test/unit/observe/snapshot_metadata.test.ts +67 -0
- package/test/unit/server/contract.test.ts +4 -0
- package/skills/rfc-review/SKILL.md +0 -52
- package/skills/rfc-review/references/rfc-review-checklist.md +0 -12
- package/skills/rfc-review/references/rfc-review-template.md +0 -28
- /package/docs/{rfcs → specs-deltas}/001-state-verification.md +0 -0
- /package/docs/{rfcs → specs-deltas}/002-richer-element-identity +0 -0
- /package/docs/{rfcs → specs-deltas}/003-wait-and-synchronization-reliability.md +0 -0
- /package/docs/{rfcs → specs-deltas}/004-action-verification-routing.md +0 -0
- /package/docs/{rfcs → specs-deltas}/005-unified-action-execution-and-verification-model.md +0 -0
- /package/docs/{rfcs → specs-deltas}/006-runtime-action-instrumentation-and-binding-layer.md +0 -0
- /package/docs/{rfcs → specs-deltas}/007-actionability-resolution-and-executable-target-selection.md +0 -0
- /package/docs/{rfcs → specs-deltas}/008-adjustable-control-support-and-semantic-value-manipulation.md +0 -0
- /package/docs/{rfcs → specs-deltas}/009-semantic-control-modeling-for-custom-and-composite-controls.md +0 -0
- /package/docs/{rfcs → specs-deltas}/010-verification-stabilization-and-temporal-convergence.md +0 -0
- /package/docs/{rfcs → specs-deltas}/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows.md +0 -0
- /package/docs/{rfcs → specs-deltas}/011.1-recovery-contract-types-and-runtime-wiring-spec.md +0 -0
- /package/docs/{rfcs → specs-deltas}/012-action-trace-and-xecution-observability.md +0 -0
package/dist/interact/index.js
CHANGED
|
@@ -95,10 +95,68 @@ export class ToolsInteract {
|
|
|
95
95
|
}
|
|
96
96
|
return null;
|
|
97
97
|
}
|
|
98
|
+
static _resolveParentIndex(elements, parentId) {
|
|
99
|
+
if (parentId === undefined || parentId === null)
|
|
100
|
+
return null;
|
|
101
|
+
if (typeof parentId === 'number' && Number.isInteger(parentId) && parentId >= 0 && parentId < elements.length) {
|
|
102
|
+
return parentId;
|
|
103
|
+
}
|
|
104
|
+
if (typeof parentId === 'string') {
|
|
105
|
+
const normalized = ToolsInteract._normalize(parentId);
|
|
106
|
+
if (!normalized)
|
|
107
|
+
return null;
|
|
108
|
+
if (/^\d+$/.test(normalized)) {
|
|
109
|
+
const index = Number(normalized);
|
|
110
|
+
if (index >= 0 && index < elements.length)
|
|
111
|
+
return index;
|
|
112
|
+
}
|
|
113
|
+
const foundIndex = elements.findIndex((el) => {
|
|
114
|
+
if (!el)
|
|
115
|
+
return false;
|
|
116
|
+
return ToolsInteract._normalize(el.resourceId ?? el.resourceID ?? el.id ?? '') === normalized ||
|
|
117
|
+
ToolsInteract._normalize(el.stable_id ?? '') === normalized;
|
|
118
|
+
});
|
|
119
|
+
return foundIndex >= 0 ? foundIndex : null;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
98
123
|
static _isVisibleElement(el) {
|
|
99
124
|
const bounds = ToolsInteract._normalizeBounds(el.bounds);
|
|
100
125
|
return !!el.visible && !!bounds && bounds[2] > bounds[0] && bounds[3] > bounds[1];
|
|
101
126
|
}
|
|
127
|
+
static _isTapActionable(el, storedStableId, platform) {
|
|
128
|
+
if (!ToolsInteract._isVisibleElement(el)) {
|
|
129
|
+
return { actionable: false, failureCode: 'ELEMENT_NOT_INTERACTABLE', reason: 'element is not visible' };
|
|
130
|
+
}
|
|
131
|
+
if (el.enabled === false) {
|
|
132
|
+
return { actionable: false, failureCode: 'ELEMENT_NOT_INTERACTABLE', reason: 'element is disabled' };
|
|
133
|
+
}
|
|
134
|
+
const semanticTapActionable = !!el.semantic && (el.semantic.is_clickable ||
|
|
135
|
+
(Array.isArray(el.semantic.supported_actions) && el.semantic.supported_actions.some((action) => ToolsInteract._normalize(action) === 'tap')));
|
|
136
|
+
if (!el.clickable && !(platform === 'ios' && semanticTapActionable)) {
|
|
137
|
+
return { actionable: false, failureCode: 'ELEMENT_NOT_INTERACTABLE', reason: 'element is not clickable' };
|
|
138
|
+
}
|
|
139
|
+
if (storedStableId) {
|
|
140
|
+
if (!el.stable_id || el.stable_id !== storedStableId) {
|
|
141
|
+
return { actionable: false, failureCode: 'STALE_REFERENCE', reason: 'element stable_id changed' };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { actionable: true };
|
|
145
|
+
}
|
|
146
|
+
static _isAdjustableActionable(el, storedStableId) {
|
|
147
|
+
if (!ToolsInteract._isVisibleElement(el)) {
|
|
148
|
+
return { actionable: false, failureCode: 'ELEMENT_NOT_INTERACTABLE', reason: 'element is not visible' };
|
|
149
|
+
}
|
|
150
|
+
if (el.enabled === false) {
|
|
151
|
+
return { actionable: false, failureCode: 'ELEMENT_NOT_INTERACTABLE', reason: 'element is disabled' };
|
|
152
|
+
}
|
|
153
|
+
if (storedStableId) {
|
|
154
|
+
if (!el.stable_id || el.stable_id !== storedStableId) {
|
|
155
|
+
return { actionable: false, failureCode: 'STALE_REFERENCE', reason: 'element stable_id changed' };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { actionable: true };
|
|
159
|
+
}
|
|
102
160
|
static _computeElementId(platform, deviceId, el, index) {
|
|
103
161
|
const identity = {
|
|
104
162
|
platform,
|
|
@@ -120,7 +178,8 @@ export class ToolsInteract {
|
|
|
120
178
|
platform,
|
|
121
179
|
deviceId,
|
|
122
180
|
bounds,
|
|
123
|
-
index
|
|
181
|
+
index,
|
|
182
|
+
stable_id: el.stable_id ?? null
|
|
124
183
|
});
|
|
125
184
|
return {
|
|
126
185
|
text: el.text ?? null,
|
|
@@ -138,6 +197,214 @@ export class ToolsInteract {
|
|
|
138
197
|
semantic: el.semantic ?? null
|
|
139
198
|
};
|
|
140
199
|
}
|
|
200
|
+
static _resolveUiChangeScope(tree, scope, target) {
|
|
201
|
+
const elements = Array.isArray(tree?.elements) ? tree.elements : [];
|
|
202
|
+
const normalizedScope = scope === 'subtree' ? 'subtree' : 'screen';
|
|
203
|
+
if (normalizedScope === 'screen') {
|
|
204
|
+
return {
|
|
205
|
+
elements,
|
|
206
|
+
resolution: {
|
|
207
|
+
scope: 'screen',
|
|
208
|
+
target: null,
|
|
209
|
+
resolved: true,
|
|
210
|
+
resolvedIndex: null,
|
|
211
|
+
resolvedStableId: null,
|
|
212
|
+
reason: 'screen scope'
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const requestedTarget = typeof target === 'string' && target.trim().length > 0 ? target.trim() : null;
|
|
217
|
+
if (!requestedTarget) {
|
|
218
|
+
return {
|
|
219
|
+
elements: [],
|
|
220
|
+
resolution: {
|
|
221
|
+
scope: 'subtree',
|
|
222
|
+
target: null,
|
|
223
|
+
resolved: false,
|
|
224
|
+
resolvedIndex: null,
|
|
225
|
+
resolvedStableId: null,
|
|
226
|
+
reason: 'subtree scope requires a target element id'
|
|
227
|
+
},
|
|
228
|
+
error: {
|
|
229
|
+
code: 'INVALID_SCOPE',
|
|
230
|
+
message: 'scope=subtree requires a target element_id'
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
const resolved = ToolsInteract._findScopedElement(tree, requestedTarget);
|
|
235
|
+
if (!resolved) {
|
|
236
|
+
return {
|
|
237
|
+
elements: [],
|
|
238
|
+
resolution: {
|
|
239
|
+
scope: 'subtree',
|
|
240
|
+
target: requestedTarget,
|
|
241
|
+
resolved: false,
|
|
242
|
+
resolvedIndex: null,
|
|
243
|
+
resolvedStableId: null,
|
|
244
|
+
reason: 'target element could not be resolved'
|
|
245
|
+
},
|
|
246
|
+
error: {
|
|
247
|
+
code: 'ELEMENT_NOT_FOUND',
|
|
248
|
+
message: `Target element ${requestedTarget} could not be resolved for subtree scope`
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const subtreeIndices = ToolsInteract._collectSubtreeIndices(elements, resolved.index);
|
|
253
|
+
const scopedElements = subtreeIndices.map((index) => elements[index]).filter((element) => !!element);
|
|
254
|
+
return {
|
|
255
|
+
elements: scopedElements,
|
|
256
|
+
resolution: {
|
|
257
|
+
scope: 'subtree',
|
|
258
|
+
target: requestedTarget,
|
|
259
|
+
resolved: true,
|
|
260
|
+
resolvedIndex: resolved.index,
|
|
261
|
+
resolvedStableId: resolved.stableId,
|
|
262
|
+
reason: resolved.reason
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
static _findScopedElement(tree, targetElementId) {
|
|
267
|
+
const elements = Array.isArray(tree?.elements) ? tree.elements : [];
|
|
268
|
+
const platform = tree?.device?.platform === 'ios' ? 'ios' : 'android';
|
|
269
|
+
const deviceId = tree?.device?.id ?? undefined;
|
|
270
|
+
const normalizedTarget = ToolsInteract._normalize(targetElementId);
|
|
271
|
+
for (let i = 0; i < elements.length; i++) {
|
|
272
|
+
const el = elements[i];
|
|
273
|
+
if (!el)
|
|
274
|
+
continue;
|
|
275
|
+
const computedElementId = ToolsInteract._computeElementId(platform, deviceId, el, i);
|
|
276
|
+
if (computedElementId === targetElementId) {
|
|
277
|
+
return {
|
|
278
|
+
index: i,
|
|
279
|
+
stableId: el.stable_id ?? null,
|
|
280
|
+
reason: 'element_id_match'
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
for (let i = 0; i < elements.length; i++) {
|
|
285
|
+
const el = elements[i];
|
|
286
|
+
if (!el)
|
|
287
|
+
continue;
|
|
288
|
+
if (el.stable_id && ToolsInteract._normalize(el.stable_id) === normalizedTarget) {
|
|
289
|
+
return {
|
|
290
|
+
index: i,
|
|
291
|
+
stableId: el.stable_id,
|
|
292
|
+
reason: 'stable_id_match'
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const storedContext = ToolsInteract._resolvedUiElements.get(targetElementId);
|
|
297
|
+
if (storedContext?.stable_id) {
|
|
298
|
+
const normalizedStoredStableId = ToolsInteract._normalize(storedContext.stable_id);
|
|
299
|
+
for (let i = 0; i < elements.length; i++) {
|
|
300
|
+
const el = elements[i];
|
|
301
|
+
if (!el?.stable_id)
|
|
302
|
+
continue;
|
|
303
|
+
if (ToolsInteract._normalize(el.stable_id) === normalizedStoredStableId) {
|
|
304
|
+
return {
|
|
305
|
+
index: i,
|
|
306
|
+
stableId: el.stable_id,
|
|
307
|
+
reason: 'stored_stable_id_match'
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
static _collectSubtreeIndices(elements, rootIndex) {
|
|
315
|
+
if (!Array.isArray(elements) || rootIndex < 0 || rootIndex >= elements.length)
|
|
316
|
+
return [];
|
|
317
|
+
const visited = new Set();
|
|
318
|
+
const stack = [rootIndex];
|
|
319
|
+
const result = [];
|
|
320
|
+
while (stack.length > 0) {
|
|
321
|
+
const index = stack.pop();
|
|
322
|
+
if (index === undefined || visited.has(index) || index < 0 || index >= elements.length)
|
|
323
|
+
continue;
|
|
324
|
+
visited.add(index);
|
|
325
|
+
result.push(index);
|
|
326
|
+
const element = elements[index];
|
|
327
|
+
if (!element)
|
|
328
|
+
continue;
|
|
329
|
+
const directChildren = new Set();
|
|
330
|
+
if (Array.isArray(element.children)) {
|
|
331
|
+
for (const childIndex of element.children) {
|
|
332
|
+
if (typeof childIndex === 'number' && Number.isInteger(childIndex) && childIndex >= 0 && childIndex < elements.length) {
|
|
333
|
+
directChildren.add(childIndex);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
for (let i = 0; i < elements.length; i++) {
|
|
338
|
+
if (ToolsInteract._resolveParentIndex(elements, elements[i]?.parentId) === index) {
|
|
339
|
+
directChildren.add(i);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
for (const childIndex of directChildren) {
|
|
343
|
+
if (!visited.has(childIndex))
|
|
344
|
+
stack.push(childIndex);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return result.sort((left, right) => left - right);
|
|
348
|
+
}
|
|
349
|
+
static _changeIdentityForElement(el, index) {
|
|
350
|
+
const stableId = ToolsInteract._normalize(el.stable_id);
|
|
351
|
+
if (stableId)
|
|
352
|
+
return `stable:${stableId}`;
|
|
353
|
+
return `fallback:${ToolsInteract._hash({
|
|
354
|
+
text: ToolsInteract._normalize(el.text ?? el.label ?? el.value ?? ''),
|
|
355
|
+
contentDescription: ToolsInteract._normalize(el.contentDescription ?? el.contentDesc ?? el.accessibilityLabel ?? ''),
|
|
356
|
+
resourceId: ToolsInteract._normalize(el.resourceId ?? el.resourceID ?? el.id ?? ''),
|
|
357
|
+
type: ToolsInteract._normalize(el.type ?? el.class ?? ''),
|
|
358
|
+
bounds: ToolsInteract._normalizeBounds(el.bounds) ?? [0, 0, 0, 0],
|
|
359
|
+
index
|
|
360
|
+
})}`;
|
|
361
|
+
}
|
|
362
|
+
static _summarizeUiChangeDelta(initialElements, currentElements) {
|
|
363
|
+
const buildMap = (elements) => {
|
|
364
|
+
const map = new Map();
|
|
365
|
+
for (let i = 0; i < elements.length; i++) {
|
|
366
|
+
const element = elements[i];
|
|
367
|
+
if (!element)
|
|
368
|
+
continue;
|
|
369
|
+
const key = ToolsInteract._changeIdentityForElement(element, i);
|
|
370
|
+
map.set(key, ToolsInteract._hash({
|
|
371
|
+
text: ToolsInteract._normalize(element.text ?? element.label ?? element.value ?? ''),
|
|
372
|
+
contentDescription: ToolsInteract._normalize(element.contentDescription ?? element.contentDesc ?? element.accessibilityLabel ?? ''),
|
|
373
|
+
resourceId: ToolsInteract._normalize(element.resourceId ?? element.resourceID ?? element.id ?? ''),
|
|
374
|
+
type: ToolsInteract._normalize(element.type ?? element.class ?? ''),
|
|
375
|
+
bounds: ToolsInteract._normalizeBounds(element.bounds) ?? [0, 0, 0, 0],
|
|
376
|
+
state: element.state ?? null,
|
|
377
|
+
visible: !!element.visible,
|
|
378
|
+
enabled: !!element.enabled,
|
|
379
|
+
clickable: !!element.clickable
|
|
380
|
+
}));
|
|
381
|
+
}
|
|
382
|
+
return map;
|
|
383
|
+
};
|
|
384
|
+
const initialMap = buildMap(initialElements);
|
|
385
|
+
const currentMap = buildMap(currentElements);
|
|
386
|
+
let added = 0;
|
|
387
|
+
let removed = 0;
|
|
388
|
+
let mutated = 0;
|
|
389
|
+
for (const [key, value] of currentMap.entries()) {
|
|
390
|
+
if (!initialMap.has(key)) {
|
|
391
|
+
added++;
|
|
392
|
+
}
|
|
393
|
+
else if (initialMap.get(key) !== value) {
|
|
394
|
+
mutated++;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
for (const key of initialMap.keys()) {
|
|
398
|
+
if (!currentMap.has(key))
|
|
399
|
+
removed++;
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
total_elements: currentElements.length,
|
|
403
|
+
added_elements: added,
|
|
404
|
+
removed_elements: removed,
|
|
405
|
+
mutated_elements: mutated
|
|
406
|
+
};
|
|
407
|
+
}
|
|
141
408
|
static _rememberResolvedElement(elementId, context) {
|
|
142
409
|
if (ToolsInteract._resolvedUiElements.has(elementId)) {
|
|
143
410
|
ToolsInteract._resolvedUiElements.delete(elementId);
|
|
@@ -261,13 +528,214 @@ export class ToolsInteract {
|
|
|
261
528
|
const stateValue = el.state[property];
|
|
262
529
|
if (typeof stateValue === 'number' && Number.isFinite(stateValue))
|
|
263
530
|
return stateValue;
|
|
531
|
+
const parsedStateValue = ToolsInteract._parseNumericControlValue(stateValue);
|
|
532
|
+
if (parsedStateValue !== null)
|
|
533
|
+
return parsedStateValue;
|
|
264
534
|
if (property === 'value' || property === 'raw_value') {
|
|
265
535
|
const fallback = el.state.raw_value ?? el.state.value;
|
|
266
536
|
if (typeof fallback === 'number' && Number.isFinite(fallback))
|
|
267
537
|
return fallback;
|
|
538
|
+
return ToolsInteract._parseNumericControlValue(fallback);
|
|
539
|
+
}
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
static _parseNumericControlValue(value) {
|
|
543
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
544
|
+
return value;
|
|
545
|
+
if (typeof value !== 'string')
|
|
546
|
+
return null;
|
|
547
|
+
const trimmed = value.trim();
|
|
548
|
+
if (!trimmed)
|
|
549
|
+
return null;
|
|
550
|
+
const percent = trimmed.match(/^([-+]?\d+(?:\.\d+)?)\s*%$/);
|
|
551
|
+
if (percent)
|
|
552
|
+
return Number(percent[1]);
|
|
553
|
+
const fraction = trimmed.match(/^([-+]?\d+(?:\.\d+)?)\s+of\s+[-+]?\d+(?:\.\d+)?$/i);
|
|
554
|
+
if (fraction)
|
|
555
|
+
return Number(fraction[1]);
|
|
556
|
+
const plain = Number(trimmed);
|
|
557
|
+
if (Number.isFinite(plain))
|
|
558
|
+
return plain;
|
|
559
|
+
const trailing = trimmed.match(/,\s*([-+]?\d+(?:\.\d+)?)$/);
|
|
560
|
+
if (trailing)
|
|
561
|
+
return Number(trailing[1]);
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
static _readNumericControlRange(el) {
|
|
565
|
+
const min = typeof el?.state?.value_range?.min === 'number' ? el.state.value_range.min : null;
|
|
566
|
+
const max = typeof el?.state?.value_range?.max === 'number' ? el.state.value_range.max : null;
|
|
567
|
+
if (min !== null && max !== null && Number.isFinite(min) && Number.isFinite(max) && max > min) {
|
|
568
|
+
return { min, max };
|
|
569
|
+
}
|
|
570
|
+
const value = el?.state?.value ?? el?.state?.raw_value;
|
|
571
|
+
if (typeof value === 'string') {
|
|
572
|
+
const fraction = value.trim().match(/^[-+]?\d+(?:\.\d+)?\s+of\s+([-+]?\d+(?:\.\d+)?)$/i);
|
|
573
|
+
if (fraction) {
|
|
574
|
+
const parsedMax = Number(fraction[1]);
|
|
575
|
+
if (Number.isFinite(parsedMax) && parsedMax > 0)
|
|
576
|
+
return { min: 0, max: parsedMax };
|
|
577
|
+
}
|
|
268
578
|
}
|
|
269
579
|
return null;
|
|
270
580
|
}
|
|
581
|
+
static _readControlStep(el) {
|
|
582
|
+
const candidates = [
|
|
583
|
+
el?.state?.step,
|
|
584
|
+
el?.state?.value_range?.step
|
|
585
|
+
];
|
|
586
|
+
for (const candidate of candidates) {
|
|
587
|
+
if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate > 0)
|
|
588
|
+
return candidate;
|
|
589
|
+
}
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
static _quantizeTargetValue(targetValue, min, max, step) {
|
|
593
|
+
if (!step)
|
|
594
|
+
return targetValue;
|
|
595
|
+
const quantized = min + (Math.round((targetValue - min) / step) * step);
|
|
596
|
+
const bounded = Math.max(min, Math.min(max, quantized));
|
|
597
|
+
const precision = ToolsInteract._decimalPrecisionForStep(step);
|
|
598
|
+
return Number(bounded.toFixed(precision));
|
|
599
|
+
}
|
|
600
|
+
static _decimalPrecisionForStep(step) {
|
|
601
|
+
const normalized = String(step).toLowerCase();
|
|
602
|
+
const [coefficient, exponentPart] = normalized.split('e');
|
|
603
|
+
const coefficientDecimals = coefficient.includes('.')
|
|
604
|
+
? coefficient.split('.')[1]?.length ?? 0
|
|
605
|
+
: 0;
|
|
606
|
+
const exponent = exponentPart !== undefined ? Number(exponentPart) : 0;
|
|
607
|
+
const precision = exponent < 0
|
|
608
|
+
? coefficientDecimals + Math.abs(exponent)
|
|
609
|
+
: Math.max(0, coefficientDecimals - exponent);
|
|
610
|
+
return Math.max(0, Math.min(12, precision));
|
|
611
|
+
}
|
|
612
|
+
static _defaultAdjustTolerance(range, step) {
|
|
613
|
+
if (step)
|
|
614
|
+
return 0;
|
|
615
|
+
if (!range)
|
|
616
|
+
return 0;
|
|
617
|
+
return Math.max(0, 0.01 * Math.abs(range.max - range.min));
|
|
618
|
+
}
|
|
619
|
+
static _runtimeActionSet(el) {
|
|
620
|
+
const values = [
|
|
621
|
+
...(Array.isArray(el?.actions) ? el.actions : []),
|
|
622
|
+
...(Array.isArray(el?.accessibilityActions) ? el.accessibilityActions : []),
|
|
623
|
+
...(Array.isArray(el?.runtimeActions) ? el.runtimeActions : []),
|
|
624
|
+
...(Array.isArray(el?.platformActions) ? el.platformActions : [])
|
|
625
|
+
];
|
|
626
|
+
return new Set(values.map((value) => ToolsInteract._normalize(value)).filter(Boolean));
|
|
627
|
+
}
|
|
628
|
+
static _hasDirectAdjustmentAction(el) {
|
|
629
|
+
const actions = ToolsInteract._runtimeActionSet(el);
|
|
630
|
+
return [
|
|
631
|
+
'action_set_progress',
|
|
632
|
+
'set_progress',
|
|
633
|
+
'setprogress',
|
|
634
|
+
'semanticsactions.setprogress',
|
|
635
|
+
'set_value',
|
|
636
|
+
'setvalue',
|
|
637
|
+
'direct_set_value'
|
|
638
|
+
].some((action) => actions.has(action));
|
|
639
|
+
}
|
|
640
|
+
static _adjustmentDirections(el) {
|
|
641
|
+
const actions = ToolsInteract._runtimeActionSet(el);
|
|
642
|
+
return {
|
|
643
|
+
increment: ['increment', 'accessibility_increment', 'action_increment'].some((action) => actions.has(action)),
|
|
644
|
+
decrement: ['decrement', 'accessibility_decrement', 'action_decrement'].some((action) => actions.has(action))
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
static _textFromElement(el) {
|
|
648
|
+
return String(el?.text ?? el?.contentDescription ?? el?.contentDesc ?? el?.accessibilityLabel ?? el?.value ?? '').trim();
|
|
649
|
+
}
|
|
650
|
+
static _parseLeadingNumber(text) {
|
|
651
|
+
const match = text.trim().match(/^([-+]?\d+(?:\.\d+)?)(?:\s*[a-zA-Z%]+)?$/);
|
|
652
|
+
if (!match)
|
|
653
|
+
return null;
|
|
654
|
+
const value = Number(match[1]);
|
|
655
|
+
return Number.isFinite(value) ? value : null;
|
|
656
|
+
}
|
|
657
|
+
static _inferLabeledSliderState(elements, track) {
|
|
658
|
+
const trackBounds = ToolsInteract._normalizeBounds(track?.bounds);
|
|
659
|
+
if (!track || !trackBounds)
|
|
660
|
+
return null;
|
|
661
|
+
const type = ToolsInteract._normalize(track.type ?? track.class ?? '');
|
|
662
|
+
const role = ToolsInteract._normalize(track.role ?? '');
|
|
663
|
+
const width = trackBounds[2] - trackBounds[0];
|
|
664
|
+
const height = trackBounds[3] - trackBounds[1];
|
|
665
|
+
const wideTrackLike = width >= 240 && height <= 220;
|
|
666
|
+
const knownTrackLike = /slider|seekbar|range/.test(type) || /slider|range/.test(role);
|
|
667
|
+
if (!wideTrackLike && !knownTrackLike)
|
|
668
|
+
return null;
|
|
669
|
+
const trackTop = trackBounds[1];
|
|
670
|
+
const trackBottom = trackBounds[3];
|
|
671
|
+
const trackCenterX = (trackBounds[0] + trackBounds[2]) / 2;
|
|
672
|
+
const labels = elements
|
|
673
|
+
.map((el, index) => {
|
|
674
|
+
const bounds = ToolsInteract._normalizeBounds(el.bounds);
|
|
675
|
+
const text = ToolsInteract._textFromElement(el);
|
|
676
|
+
const value = ToolsInteract._parseLeadingNumber(text);
|
|
677
|
+
return bounds && text && value !== null
|
|
678
|
+
? {
|
|
679
|
+
index,
|
|
680
|
+
el,
|
|
681
|
+
text,
|
|
682
|
+
value,
|
|
683
|
+
bounds,
|
|
684
|
+
centerX: (bounds[0] + bounds[2]) / 2,
|
|
685
|
+
centerY: (bounds[1] + bounds[3]) / 2,
|
|
686
|
+
hasUnit: /[a-zA-Z%]/.test(text)
|
|
687
|
+
}
|
|
688
|
+
: null;
|
|
689
|
+
})
|
|
690
|
+
.filter((label) => !!label);
|
|
691
|
+
const rangeLabels = labels
|
|
692
|
+
.filter((label) => {
|
|
693
|
+
const verticallyNearTrack = label.centerY >= trackTop - 520 && label.centerY <= trackBottom + 260;
|
|
694
|
+
const horizontallyNearTrack = label.centerX >= trackBounds[0] - 120 && label.centerX <= trackBounds[2] + 120;
|
|
695
|
+
return verticallyNearTrack && horizontallyNearTrack && label.hasUnit;
|
|
696
|
+
})
|
|
697
|
+
.sort((a, b) => a.centerX - b.centerX);
|
|
698
|
+
if (rangeLabels.length < 2)
|
|
699
|
+
return null;
|
|
700
|
+
const minLabel = rangeLabels[0];
|
|
701
|
+
const maxLabel = rangeLabels[rangeLabels.length - 1];
|
|
702
|
+
if (!minLabel || !maxLabel || maxLabel.value <= minLabel.value)
|
|
703
|
+
return null;
|
|
704
|
+
const currentCandidates = labels
|
|
705
|
+
.filter((label) => {
|
|
706
|
+
if (label.index === minLabel.index || label.index === maxLabel.index)
|
|
707
|
+
return false;
|
|
708
|
+
if (label.value < minLabel.value || label.value > maxLabel.value)
|
|
709
|
+
return false;
|
|
710
|
+
if (label.centerY < trackTop - 950 || label.centerY > minLabel.centerY - 20)
|
|
711
|
+
return false;
|
|
712
|
+
return label.centerX >= trackCenterX - 80;
|
|
713
|
+
})
|
|
714
|
+
.sort((a, b) => {
|
|
715
|
+
const yDistance = Math.abs((trackTop - 420) - a.centerY) - Math.abs((trackTop - 420) - b.centerY);
|
|
716
|
+
if (yDistance !== 0)
|
|
717
|
+
return yDistance;
|
|
718
|
+
return b.centerX - a.centerX;
|
|
719
|
+
});
|
|
720
|
+
const currentLabel = currentCandidates[0];
|
|
721
|
+
if (!currentLabel)
|
|
722
|
+
return null;
|
|
723
|
+
const candidateBelowRangeLabels = minLabel.centerY < trackTop && maxLabel.centerY < trackTop;
|
|
724
|
+
const labelDerivedBounds = [
|
|
725
|
+
Math.min(minLabel.bounds[0], trackBounds[0]),
|
|
726
|
+
Math.max(currentLabel.bounds[3] + 12, Math.min(minLabel.bounds[1], maxLabel.bounds[1]) - 160),
|
|
727
|
+
Math.max(maxLabel.bounds[2], trackBounds[2]),
|
|
728
|
+
Math.max(currentLabel.bounds[3] + 80, Math.min(minLabel.bounds[1], maxLabel.bounds[1]) - 32)
|
|
729
|
+
];
|
|
730
|
+
return {
|
|
731
|
+
current: currentLabel.value,
|
|
732
|
+
min: minLabel.value,
|
|
733
|
+
max: maxLabel.value,
|
|
734
|
+
value_range: { min: minLabel.value, max: maxLabel.value },
|
|
735
|
+
touch_bounds: candidateBelowRangeLabels ? labelDerivedBounds : trackBounds,
|
|
736
|
+
source: 'nearby_labels'
|
|
737
|
+
};
|
|
738
|
+
}
|
|
271
739
|
static _buildControlPoint(bounds, ratio, axis) {
|
|
272
740
|
const clampedRatio = Math.max(0, Math.min(1, ratio));
|
|
273
741
|
const [left, top, right, bottom] = bounds;
|
|
@@ -384,25 +852,12 @@ export class ToolsInteract {
|
|
|
384
852
|
let current = chosen;
|
|
385
853
|
let safety = 0;
|
|
386
854
|
while (safety < 20 && current.el && !(current.el.clickable || current.el.focusable || ToolsInteract._isSemanticActionable(current.el)) && current.el.parentId !== undefined && current.el.parentId !== null) {
|
|
387
|
-
const
|
|
388
|
-
let parentIndex = null;
|
|
389
|
-
if (typeof parentId === 'number')
|
|
390
|
-
parentIndex = parentId;
|
|
391
|
-
else if (typeof parentId === 'string' && /^\d+$/.test(parentId))
|
|
392
|
-
parentIndex = Number(parentId);
|
|
855
|
+
const parentIndex = ToolsInteract._resolveParentIndex(elements, current.el.parentId);
|
|
393
856
|
if (parentIndex !== null && elements[parentIndex]) {
|
|
394
857
|
current = { el: elements[parentIndex], idx: parentIndex };
|
|
395
858
|
if (current.el.clickable || current.el.focusable || ToolsInteract._isSemanticActionable(current.el))
|
|
396
859
|
return current;
|
|
397
860
|
}
|
|
398
|
-
else if (typeof parentId === 'string') {
|
|
399
|
-
const foundIndex = elements.findIndex((el) => el.resourceId === parentId || el.id === parentId);
|
|
400
|
-
if (foundIndex === -1)
|
|
401
|
-
break;
|
|
402
|
-
current = { el: elements[foundIndex], idx: foundIndex };
|
|
403
|
-
if (current.el.clickable || current.el.focusable || ToolsInteract._isSemanticActionable(current.el))
|
|
404
|
-
return current;
|
|
405
|
-
}
|
|
406
861
|
else {
|
|
407
862
|
break;
|
|
408
863
|
}
|
|
@@ -516,11 +971,9 @@ export class ToolsInteract {
|
|
|
516
971
|
return ToolsInteract._actionFailure(actionType, selector, null, 'STALE_REFERENCE', true, fingerprintBefore);
|
|
517
972
|
}
|
|
518
973
|
const resolvedTarget = ToolsInteract._resolvedTargetFromElement(resolved.elementId, currentMatch.el, currentMatch.index);
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if (currentMatch.el.enabled === false) {
|
|
523
|
-
return ToolsInteract._actionFailure(actionType, selector, resolvedTarget, 'ELEMENT_NOT_INTERACTABLE', true, fingerprintBefore);
|
|
974
|
+
const tapActionability = ToolsInteract._isTapActionable(currentMatch.el, resolved.stable_id, resolved.platform);
|
|
975
|
+
if (!tapActionability.actionable) {
|
|
976
|
+
return ToolsInteract._actionFailure(actionType, selector, resolvedTarget, tapActionability.failureCode ?? 'ELEMENT_NOT_INTERACTABLE', true, fingerprintBefore);
|
|
524
977
|
}
|
|
525
978
|
const bounds = ToolsInteract._normalizeBounds(currentMatch.el.bounds) ?? resolved.bounds;
|
|
526
979
|
if (!bounds || bounds[2] <= bounds[0] || bounds[3] <= bounds[1]) {
|
|
@@ -545,14 +998,17 @@ export class ToolsInteract {
|
|
|
545
998
|
sourceModule: 'interact'
|
|
546
999
|
});
|
|
547
1000
|
}
|
|
548
|
-
static async adjustControlHandler({ selector, element_id, property = 'value', targetValue, tolerance
|
|
1001
|
+
static async adjustControlHandler({ selector, element_id, property = 'value', targetValue, tolerance, maxAttempts, platform, deviceId }) {
|
|
549
1002
|
const actionType = 'adjust_control';
|
|
550
1003
|
const targetSelector = selector ?? (element_id ? { elementId: element_id } : null);
|
|
551
|
-
const
|
|
552
|
-
const attemptsLimit =
|
|
1004
|
+
const requestedTolerance = typeof tolerance === 'number' && Number.isFinite(tolerance) ? Math.max(0, tolerance) : null;
|
|
1005
|
+
const attemptsLimit = maxAttempts === undefined || maxAttempts === null
|
|
1006
|
+
? 5
|
|
1007
|
+
: Math.max(1, Math.floor(Number(maxAttempts) || 1));
|
|
553
1008
|
const sourcePlatform = platform || 'android';
|
|
554
1009
|
let resolvedPlatform = sourcePlatform;
|
|
555
1010
|
let resolvedDeviceId = deviceId;
|
|
1011
|
+
const storedResolvedTarget = element_id ? ToolsInteract._resolvedUiElements.get(element_id) ?? null : null;
|
|
556
1012
|
const fingerprintBefore = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
|
|
557
1013
|
let semanticFallbackElement = null;
|
|
558
1014
|
const traceSteps = [];
|
|
@@ -582,6 +1038,7 @@ export class ToolsInteract {
|
|
|
582
1038
|
retry_depth: attempts
|
|
583
1039
|
});
|
|
584
1040
|
}
|
|
1041
|
+
const responseTolerance = requestedTolerance ?? 0;
|
|
585
1042
|
const base = buildActionExecutionResult({
|
|
586
1043
|
actionType,
|
|
587
1044
|
sourceModule: 'interact',
|
|
@@ -594,7 +1051,7 @@ export class ToolsInteract {
|
|
|
594
1051
|
failure: { failureCode, retryable },
|
|
595
1052
|
details: {
|
|
596
1053
|
target_value: targetValue,
|
|
597
|
-
tolerance:
|
|
1054
|
+
tolerance: responseTolerance,
|
|
598
1055
|
property,
|
|
599
1056
|
attempts,
|
|
600
1057
|
adjustment_mode: adjustmentMode,
|
|
@@ -610,7 +1067,7 @@ export class ToolsInteract {
|
|
|
610
1067
|
target_state: {
|
|
611
1068
|
property,
|
|
612
1069
|
target_value: targetValue,
|
|
613
|
-
tolerance:
|
|
1070
|
+
tolerance: responseTolerance
|
|
614
1071
|
},
|
|
615
1072
|
actual_state: actualState,
|
|
616
1073
|
within_tolerance: false,
|
|
@@ -702,8 +1159,8 @@ export class ToolsInteract {
|
|
|
702
1159
|
}
|
|
703
1160
|
return null;
|
|
704
1161
|
};
|
|
705
|
-
if (!selector && !element_id) {
|
|
706
|
-
return buildFailure('ELEMENT_NOT_FOUND', 'selector or element_id is required', null, undefined, null, 0, 'gesture', false);
|
|
1162
|
+
if ((!selector && !element_id) || (selector && element_id)) {
|
|
1163
|
+
return buildFailure('ELEMENT_NOT_FOUND', 'exactly one of selector or element_id is required', null, undefined, null, 0, 'gesture', false);
|
|
707
1164
|
}
|
|
708
1165
|
if (selector && !element_id) {
|
|
709
1166
|
const waitResult = await ToolsInteract.waitForUIHandler({
|
|
@@ -742,7 +1199,8 @@ export class ToolsInteract {
|
|
|
742
1199
|
let currentDevice = undefined;
|
|
743
1200
|
let attemptCount = 0;
|
|
744
1201
|
let cachedResolvedMatch = null;
|
|
745
|
-
|
|
1202
|
+
let coordinateTapsAwaitingFreshGesture = false;
|
|
1203
|
+
adjustmentAttempts: for (let attempt = 0; attempt < attemptsLimit && attemptCount < attemptsLimit; attempt++) {
|
|
746
1204
|
const resolved = cachedResolvedMatch
|
|
747
1205
|
? {
|
|
748
1206
|
tree: null,
|
|
@@ -758,11 +1216,33 @@ export class ToolsInteract {
|
|
|
758
1216
|
resolvedTarget = resolved.resolvedTarget;
|
|
759
1217
|
const currentEl = resolved.match.el;
|
|
760
1218
|
cachedResolvedMatch = resolved.match;
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
1219
|
+
const adjustableActionability = ToolsInteract._isAdjustableActionable(currentEl, storedResolvedTarget?.stable_id);
|
|
1220
|
+
if (!adjustableActionability.actionable) {
|
|
1221
|
+
return buildFailure(adjustableActionability.failureCode ?? 'ELEMENT_NOT_INTERACTABLE', adjustableActionability.reason ?? 'adjustable control is not actionable', resolvedTarget, currentDevice, lastObservedState, attemptCount, lastAdjustmentMode, true);
|
|
1222
|
+
}
|
|
1223
|
+
const elementsForInference = Array.isArray(resolved.tree?.elements)
|
|
1224
|
+
? resolved.tree.elements
|
|
1225
|
+
: cachedResolvedMatch
|
|
1226
|
+
? [cachedResolvedMatch.el]
|
|
1227
|
+
: [currentEl];
|
|
1228
|
+
const inferredSliderState = ToolsInteract._inferLabeledSliderState(elementsForInference, currentEl);
|
|
1229
|
+
const nativeBounds = ToolsInteract._normalizeBounds(currentEl.bounds);
|
|
1230
|
+
const bounds = inferredSliderState?.touch_bounds ?? nativeBounds;
|
|
1231
|
+
const valueRange = ToolsInteract._readNumericControlRange(currentEl) ?? inferredSliderState?.value_range ?? null;
|
|
1232
|
+
const step = ToolsInteract._readControlStep(currentEl);
|
|
1233
|
+
const normalizedTargetValue = valueRange
|
|
1234
|
+
? ToolsInteract._quantizeTargetValue(targetValue, valueRange.min, valueRange.max, step)
|
|
1235
|
+
: targetValue;
|
|
1236
|
+
const effectiveTolerance = requestedTolerance ?? ToolsInteract._defaultAdjustTolerance(valueRange, step);
|
|
1237
|
+
const currentValue = ToolsInteract._readNumericControlValue(currentEl, property) ?? inferredSliderState?.current ?? null;
|
|
764
1238
|
const actualState = currentValue !== null
|
|
765
|
-
? {
|
|
1239
|
+
? {
|
|
1240
|
+
property,
|
|
1241
|
+
value: currentValue,
|
|
1242
|
+
raw_value: typeof currentEl.state?.raw_value === 'number'
|
|
1243
|
+
? currentEl.state.raw_value
|
|
1244
|
+
: inferredSliderState?.current
|
|
1245
|
+
}
|
|
766
1246
|
: null;
|
|
767
1247
|
lastObservedState = actualState;
|
|
768
1248
|
if (!traceSteps.some((step) => step.stage === 'resolve')) {
|
|
@@ -775,10 +1255,20 @@ export class ToolsInteract {
|
|
|
775
1255
|
if (property !== 'value' && property !== 'raw_value') {
|
|
776
1256
|
return buildFailure('ELEMENT_NOT_INTERACTABLE', 'adjust_control currently supports numeric value and raw_value properties only', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
|
|
777
1257
|
}
|
|
778
|
-
|
|
1258
|
+
const isAdjustableControl = ToolsInteract._isAdjustableControl(currentEl) || !!inferredSliderState;
|
|
1259
|
+
if (!isAdjustableControl) {
|
|
1260
|
+
return buildFailure('ELEMENT_NOT_INTERACTABLE', 'target is not an adjustable control', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
|
|
1261
|
+
}
|
|
1262
|
+
if (currentValue === null) {
|
|
1263
|
+
return buildFailure('UNKNOWN', 'current numeric value is unavailable', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
|
|
1264
|
+
}
|
|
1265
|
+
if (valueRange && (targetValue < valueRange.min || targetValue > valueRange.max)) {
|
|
1266
|
+
return buildFailure('CONTROL_CONVERGENCE_FAILED', `targetValue ${targetValue} is outside the control range ${valueRange.min}..${valueRange.max}`, resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
|
|
1267
|
+
}
|
|
1268
|
+
if (currentValue !== null && Math.abs(currentValue - normalizedTargetValue) <= effectiveTolerance) {
|
|
779
1269
|
recordTraceStep('verify', 'success', {
|
|
780
1270
|
property,
|
|
781
|
-
target_value:
|
|
1271
|
+
target_value: normalizedTargetValue,
|
|
782
1272
|
actual_state: actualState,
|
|
783
1273
|
reason: 'control already within tolerance'
|
|
784
1274
|
});
|
|
@@ -793,8 +1283,8 @@ export class ToolsInteract {
|
|
|
793
1283
|
uiFingerprintBefore: fingerprintBefore,
|
|
794
1284
|
uiFingerprintAfter,
|
|
795
1285
|
details: {
|
|
796
|
-
target_value:
|
|
797
|
-
tolerance:
|
|
1286
|
+
target_value: normalizedTargetValue,
|
|
1287
|
+
tolerance: effectiveTolerance,
|
|
798
1288
|
property,
|
|
799
1289
|
attempts: attemptCount,
|
|
800
1290
|
adjustment_mode: 'semantic',
|
|
@@ -809,8 +1299,8 @@ export class ToolsInteract {
|
|
|
809
1299
|
...base,
|
|
810
1300
|
target_state: {
|
|
811
1301
|
property,
|
|
812
|
-
target_value:
|
|
813
|
-
tolerance:
|
|
1302
|
+
target_value: normalizedTargetValue,
|
|
1303
|
+
tolerance: effectiveTolerance
|
|
814
1304
|
},
|
|
815
1305
|
actual_state: actualState,
|
|
816
1306
|
within_tolerance: true,
|
|
@@ -819,40 +1309,40 @@ export class ToolsInteract {
|
|
|
819
1309
|
adjustment_mode: 'semantic'
|
|
820
1310
|
};
|
|
821
1311
|
}
|
|
822
|
-
if (!ToolsInteract._isAdjustableControl(currentEl)) {
|
|
823
|
-
return buildFailure('ELEMENT_NOT_INTERACTABLE', 'target is not an adjustable control', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
|
|
824
|
-
}
|
|
825
1312
|
if (!bounds) {
|
|
826
1313
|
return buildFailure('ELEMENT_NOT_INTERACTABLE', 'adjustable control has no bounds', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
|
|
827
1314
|
}
|
|
828
|
-
const min = typeof valueRange?.min === 'number' ? valueRange.min : null;
|
|
829
|
-
const max = typeof valueRange?.max === 'number' ? valueRange.max : null;
|
|
830
|
-
if (min === null || max === null || !Number.isFinite(min) || !Number.isFinite(max) || max <= min) {
|
|
831
|
-
return buildFailure('ELEMENT_NOT_INTERACTABLE', 'value_range unavailable', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
|
|
832
|
-
}
|
|
833
|
-
if (targetValue < min || targetValue > max) {
|
|
834
|
-
return buildFailure('UNKNOWN', `targetValue ${targetValue} is outside the control range ${min}..${max}`, resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
|
|
835
|
-
}
|
|
836
|
-
const axis = ToolsInteract._controlAxis(currentEl, bounds);
|
|
837
|
-
const targetPoint = ToolsInteract._buildConservativeControlPoint(bounds, targetValue, currentValue, min, max, axis);
|
|
838
|
-
const currentPoint = currentValue !== null
|
|
839
|
-
? ToolsInteract._buildControlPoint(bounds, (currentValue - min) / (max - min), axis)
|
|
840
|
-
: ToolsInteract._buildControlPoint(bounds, 0.5, axis);
|
|
841
|
-
const probePoints = ToolsInteract._buildAdjustmentProbePoints(bounds, targetValue, currentValue, min, max, axis);
|
|
842
1315
|
const runVerification = async () => {
|
|
1316
|
+
if (inferredSliderState) {
|
|
1317
|
+
const tree = await ToolsObserve.getUITreeHandler({ platform: resolvedPlatform, deviceId: resolvedDeviceId });
|
|
1318
|
+
const elements = Array.isArray(tree?.elements) ? tree.elements : [];
|
|
1319
|
+
const refreshed = ToolsInteract._inferLabeledSliderState(elements, currentEl);
|
|
1320
|
+
const observedValue = refreshed?.current ?? null;
|
|
1321
|
+
const observedState = observedValue !== null
|
|
1322
|
+
? { property, value: observedValue, raw_value: observedValue }
|
|
1323
|
+
: actualState;
|
|
1324
|
+
return {
|
|
1325
|
+
verification: {
|
|
1326
|
+
success: observedValue !== null && Math.abs(observedValue - normalizedTargetValue) <= effectiveTolerance,
|
|
1327
|
+
observed_state: observedState,
|
|
1328
|
+
reason: observedValue !== null
|
|
1329
|
+
? 'visible labeled value read from nearby slider labels'
|
|
1330
|
+
: 'visible labeled value unavailable'
|
|
1331
|
+
},
|
|
1332
|
+
observedState,
|
|
1333
|
+
withinTolerance: observedValue !== null && Math.abs(observedValue - normalizedTargetValue) <= effectiveTolerance
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
843
1336
|
const verification = await ToolsInteract.expectStateHandler({
|
|
844
1337
|
element_id: resolvedTarget?.elementId ?? element_id,
|
|
845
1338
|
selector: selector ?? undefined,
|
|
846
1339
|
property,
|
|
847
|
-
expected:
|
|
1340
|
+
expected: normalizedTargetValue,
|
|
848
1341
|
platform: resolvedPlatform,
|
|
849
1342
|
deviceId: resolvedDeviceId
|
|
850
1343
|
});
|
|
851
|
-
const observedValue =
|
|
852
|
-
|
|
853
|
-
: typeof verification?.observed_state?.raw_value === 'number'
|
|
854
|
-
? verification.observed_state.raw_value
|
|
855
|
-
: null;
|
|
1344
|
+
const observedValue = ToolsInteract._parseNumericControlValue(verification?.observed_state?.value) ??
|
|
1345
|
+
ToolsInteract._parseNumericControlValue(verification?.observed_state?.raw_value);
|
|
856
1346
|
const observedState = observedValue !== null
|
|
857
1347
|
? {
|
|
858
1348
|
property,
|
|
@@ -863,88 +1353,266 @@ export class ToolsInteract {
|
|
|
863
1353
|
return {
|
|
864
1354
|
verification,
|
|
865
1355
|
observedState,
|
|
866
|
-
withinTolerance: observedValue !== null && Math.abs(observedValue -
|
|
1356
|
+
withinTolerance: observedValue !== null && Math.abs(observedValue - normalizedTargetValue) <= effectiveTolerance
|
|
867
1357
|
};
|
|
868
1358
|
};
|
|
869
1359
|
let actionDevice = currentDevice;
|
|
870
1360
|
let observedState = actualState;
|
|
871
1361
|
let verification = null;
|
|
872
1362
|
let verificationResult = { verification: null, observedState: actualState, withinTolerance: false };
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
mode: 'coordinate',
|
|
879
|
-
point: probePoint
|
|
880
|
-
});
|
|
881
|
-
const actionResult = await ToolsInteract.tapHandler({
|
|
882
|
-
platform: resolvedPlatform,
|
|
883
|
-
x: probePoint.x,
|
|
884
|
-
y: probePoint.y,
|
|
885
|
-
deviceId: resolvedDeviceId
|
|
886
|
-
});
|
|
887
|
-
attemptCount++;
|
|
888
|
-
actionDevice = actionResult.device ?? actionDevice;
|
|
889
|
-
if (!actionResult.success) {
|
|
1363
|
+
const directActionSupported = ToolsInteract._hasDirectAdjustmentAction(currentEl);
|
|
1364
|
+
if (directActionSupported && attemptCount < attemptsLimit) {
|
|
1365
|
+
const { interact, resolved: actionResolved } = await ToolsInteract.getInteractionService(resolvedPlatform, resolvedDeviceId);
|
|
1366
|
+
if (typeof interact.setAdjustableValue === 'function') {
|
|
1367
|
+
lastAdjustmentMode = 'semantic';
|
|
890
1368
|
recordTraceStep('execute', 'retry', {
|
|
891
|
-
attempt: attemptCount,
|
|
892
|
-
mode: '
|
|
893
|
-
|
|
894
|
-
|
|
1369
|
+
attempt: attemptCount + 1,
|
|
1370
|
+
mode: 'semantic',
|
|
1371
|
+
strategy: 'direct_set_value',
|
|
1372
|
+
target_value: normalizedTargetValue
|
|
895
1373
|
});
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
const base = buildActionExecutionResult({
|
|
911
|
-
actionType,
|
|
912
|
-
sourceModule: 'interact',
|
|
913
|
-
device: actionDevice ?? currentDevice,
|
|
914
|
-
selector: targetSelector,
|
|
915
|
-
resolved: resolvedTarget,
|
|
916
|
-
success: true,
|
|
917
|
-
uiFingerprintBefore: fingerprintBefore,
|
|
918
|
-
uiFingerprintAfter,
|
|
919
|
-
details: {
|
|
920
|
-
target_value: targetValue,
|
|
921
|
-
tolerance: normalizedTolerance,
|
|
1374
|
+
const actionResult = await interact.setAdjustableValue({
|
|
1375
|
+
deviceId: actionResolved.id,
|
|
1376
|
+
target: resolvedTarget,
|
|
1377
|
+
property,
|
|
1378
|
+
value: normalizedTargetValue
|
|
1379
|
+
});
|
|
1380
|
+
attemptCount++;
|
|
1381
|
+
actionDevice = actionResult?.device ?? actionDevice;
|
|
1382
|
+
if (actionResult?.success !== false) {
|
|
1383
|
+
verificationResult = await runVerification();
|
|
1384
|
+
observedState = verificationResult.observedState;
|
|
1385
|
+
lastObservedState = observedState;
|
|
1386
|
+
recordTraceStep('verify', verificationResult.withinTolerance ? 'success' : 'retry', {
|
|
1387
|
+
attempt: attemptCount,
|
|
922
1388
|
property,
|
|
923
|
-
|
|
924
|
-
|
|
1389
|
+
target_value: normalizedTargetValue,
|
|
1390
|
+
actual_state: observedState,
|
|
1391
|
+
reason: verificationResult.verification?.reason ?? 'direct adjustment did not converge yet'
|
|
1392
|
+
});
|
|
1393
|
+
if (verificationResult.withinTolerance) {
|
|
1394
|
+
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
|
|
1395
|
+
const base = buildActionExecutionResult({
|
|
1396
|
+
actionType,
|
|
1397
|
+
sourceModule: 'interact',
|
|
1398
|
+
device: actionDevice ?? currentDevice,
|
|
1399
|
+
selector: targetSelector,
|
|
1400
|
+
resolved: resolvedTarget,
|
|
1401
|
+
success: true,
|
|
1402
|
+
uiFingerprintBefore: fingerprintBefore,
|
|
1403
|
+
uiFingerprintAfter,
|
|
1404
|
+
details: {
|
|
1405
|
+
target_value: normalizedTargetValue,
|
|
1406
|
+
tolerance: effectiveTolerance,
|
|
1407
|
+
property,
|
|
1408
|
+
attempts: attemptCount,
|
|
1409
|
+
adjustment_mode: lastAdjustmentMode,
|
|
1410
|
+
actual_state: observedState,
|
|
1411
|
+
converged: true,
|
|
1412
|
+
within_tolerance: true,
|
|
1413
|
+
reason: verificationResult.verification?.reason ?? 'control converged to target value'
|
|
1414
|
+
},
|
|
1415
|
+
traceSteps
|
|
1416
|
+
});
|
|
1417
|
+
return {
|
|
1418
|
+
...base,
|
|
1419
|
+
target_state: {
|
|
1420
|
+
property,
|
|
1421
|
+
target_value: normalizedTargetValue,
|
|
1422
|
+
tolerance: effectiveTolerance
|
|
1423
|
+
},
|
|
1424
|
+
actual_state: observedState,
|
|
1425
|
+
within_tolerance: true,
|
|
1426
|
+
converged: true,
|
|
1427
|
+
attempts: attemptCount,
|
|
1428
|
+
adjustment_mode: lastAdjustmentMode
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
cachedResolvedMatch = null;
|
|
1432
|
+
if (attemptCount < attemptsLimit) {
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
const directions = ToolsInteract._adjustmentDirections(currentEl);
|
|
1439
|
+
if ((directions.increment || directions.decrement) && attemptCount < attemptsLimit) {
|
|
1440
|
+
const { interact, resolved: actionResolved } = await ToolsInteract.getInteractionService(resolvedPlatform, resolvedDeviceId);
|
|
1441
|
+
const adjustByDirection = typeof interact.adjustAccessibleValue === 'function'
|
|
1442
|
+
? interact.adjustAccessibleValue.bind(interact)
|
|
1443
|
+
: null;
|
|
1444
|
+
while (adjustByDirection && attemptCount < attemptsLimit) {
|
|
1445
|
+
const direction = normalizedTargetValue > (observedState?.value ?? currentValue) ? 'increment' : 'decrement';
|
|
1446
|
+
if ((direction === 'increment' && !directions.increment) || (direction === 'decrement' && !directions.decrement))
|
|
1447
|
+
break;
|
|
1448
|
+
lastAdjustmentMode = 'semantic';
|
|
1449
|
+
recordTraceStep('execute', 'retry', {
|
|
1450
|
+
attempt: attemptCount + 1,
|
|
1451
|
+
mode: 'semantic',
|
|
1452
|
+
strategy: 'accessibility_increment_decrement',
|
|
1453
|
+
direction
|
|
1454
|
+
});
|
|
1455
|
+
const actionResult = await adjustByDirection({
|
|
1456
|
+
deviceId: actionResolved.id,
|
|
1457
|
+
target: resolvedTarget,
|
|
1458
|
+
property,
|
|
1459
|
+
direction
|
|
1460
|
+
});
|
|
1461
|
+
attemptCount++;
|
|
1462
|
+
actionDevice = actionResult?.device ?? actionDevice;
|
|
1463
|
+
if (actionResult?.success === false)
|
|
1464
|
+
break;
|
|
1465
|
+
verificationResult = await runVerification();
|
|
1466
|
+
observedState = verificationResult.observedState;
|
|
1467
|
+
lastObservedState = observedState;
|
|
1468
|
+
recordTraceStep('verify', verificationResult.withinTolerance ? 'success' : 'retry', {
|
|
1469
|
+
attempt: attemptCount,
|
|
1470
|
+
property,
|
|
1471
|
+
target_value: normalizedTargetValue,
|
|
1472
|
+
actual_state: observedState,
|
|
1473
|
+
reason: verificationResult.verification?.reason ?? 'incremental adjustment did not converge yet'
|
|
1474
|
+
});
|
|
1475
|
+
if (verificationResult.withinTolerance) {
|
|
1476
|
+
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
|
|
1477
|
+
const base = buildActionExecutionResult({
|
|
1478
|
+
actionType,
|
|
1479
|
+
sourceModule: 'interact',
|
|
1480
|
+
device: actionDevice ?? currentDevice,
|
|
1481
|
+
selector: targetSelector,
|
|
1482
|
+
resolved: resolvedTarget,
|
|
1483
|
+
success: true,
|
|
1484
|
+
uiFingerprintBefore: fingerprintBefore,
|
|
1485
|
+
uiFingerprintAfter,
|
|
1486
|
+
details: {
|
|
1487
|
+
target_value: normalizedTargetValue,
|
|
1488
|
+
tolerance: effectiveTolerance,
|
|
1489
|
+
property,
|
|
1490
|
+
attempts: attemptCount,
|
|
1491
|
+
adjustment_mode: lastAdjustmentMode,
|
|
1492
|
+
actual_state: observedState,
|
|
1493
|
+
converged: true,
|
|
1494
|
+
within_tolerance: true,
|
|
1495
|
+
reason: verificationResult.verification?.reason ?? 'control converged to target value'
|
|
1496
|
+
},
|
|
1497
|
+
traceSteps
|
|
1498
|
+
});
|
|
1499
|
+
return {
|
|
1500
|
+
...base,
|
|
1501
|
+
target_state: {
|
|
1502
|
+
property,
|
|
1503
|
+
target_value: normalizedTargetValue,
|
|
1504
|
+
tolerance: effectiveTolerance
|
|
1505
|
+
},
|
|
925
1506
|
actual_state: observedState,
|
|
926
|
-
converged: true,
|
|
927
1507
|
within_tolerance: true,
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1508
|
+
converged: true,
|
|
1509
|
+
attempts: attemptCount,
|
|
1510
|
+
adjustment_mode: lastAdjustmentMode
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
cachedResolvedMatch = null;
|
|
1514
|
+
if (attemptCount < attemptsLimit) {
|
|
1515
|
+
continue adjustmentAttempts;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
if (!bounds) {
|
|
1520
|
+
return buildFailure('ELEMENT_NOT_INTERACTABLE', 'adjustable control has no bounds', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
|
|
1521
|
+
}
|
|
1522
|
+
if (!valueRange) {
|
|
1523
|
+
return buildFailure('UNKNOWN', 'value_range unavailable', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
|
|
1524
|
+
}
|
|
1525
|
+
const min = valueRange.min;
|
|
1526
|
+
const max = valueRange.max;
|
|
1527
|
+
const axis = ToolsInteract._controlAxis(currentEl, bounds);
|
|
1528
|
+
const targetPoint = ToolsInteract._buildConservativeControlPoint(bounds, normalizedTargetValue, currentValue, min, max, axis);
|
|
1529
|
+
const currentPoint = ToolsInteract._buildControlPoint(bounds, (currentValue - min) / (max - min), axis);
|
|
1530
|
+
if (!coordinateTapsAwaitingFreshGesture) {
|
|
1531
|
+
const probePoints = inferredSliderState
|
|
1532
|
+
? [targetPoint]
|
|
1533
|
+
: ToolsInteract._buildAdjustmentProbePoints(bounds, normalizedTargetValue, currentValue, min, max, axis);
|
|
1534
|
+
for (let i = 0; i < probePoints.length && attemptCount < attemptsLimit; i++) {
|
|
1535
|
+
const probePoint = probePoints[i];
|
|
1536
|
+
lastAdjustmentMode = 'coordinate';
|
|
1537
|
+
recordTraceStep('execute', 'retry', {
|
|
1538
|
+
attempt: attemptCount + 1,
|
|
1539
|
+
mode: 'coordinate',
|
|
1540
|
+
point: probePoint
|
|
931
1541
|
});
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1542
|
+
const actionResult = await ToolsInteract.tapHandler({
|
|
1543
|
+
platform: resolvedPlatform,
|
|
1544
|
+
x: probePoint.x,
|
|
1545
|
+
y: probePoint.y,
|
|
1546
|
+
deviceId: resolvedDeviceId
|
|
1547
|
+
});
|
|
1548
|
+
attemptCount++;
|
|
1549
|
+
actionDevice = actionResult.device ?? actionDevice;
|
|
1550
|
+
if (!actionResult.success) {
|
|
1551
|
+
recordTraceStep('execute', 'retry', {
|
|
1552
|
+
attempt: attemptCount,
|
|
1553
|
+
mode: 'coordinate',
|
|
1554
|
+
point: probePoint,
|
|
1555
|
+
success: false
|
|
1556
|
+
});
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
verificationResult = await runVerification();
|
|
1560
|
+
observedState = verificationResult.observedState;
|
|
1561
|
+
lastObservedState = observedState;
|
|
1562
|
+
recordTraceStep('verify', verificationResult.withinTolerance ? 'success' : 'retry', {
|
|
1563
|
+
attempt: attemptCount,
|
|
1564
|
+
property,
|
|
1565
|
+
target_value: normalizedTargetValue,
|
|
939
1566
|
actual_state: observedState,
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1567
|
+
reason: verificationResult.verification?.reason ?? 'control did not converge yet'
|
|
1568
|
+
});
|
|
1569
|
+
if (verificationResult.withinTolerance) {
|
|
1570
|
+
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
|
|
1571
|
+
const base = buildActionExecutionResult({
|
|
1572
|
+
actionType,
|
|
1573
|
+
sourceModule: 'interact',
|
|
1574
|
+
device: actionDevice ?? currentDevice,
|
|
1575
|
+
selector: targetSelector,
|
|
1576
|
+
resolved: resolvedTarget,
|
|
1577
|
+
success: true,
|
|
1578
|
+
uiFingerprintBefore: fingerprintBefore,
|
|
1579
|
+
uiFingerprintAfter,
|
|
1580
|
+
details: {
|
|
1581
|
+
target_value: normalizedTargetValue,
|
|
1582
|
+
tolerance: effectiveTolerance,
|
|
1583
|
+
property,
|
|
1584
|
+
attempts: attemptCount,
|
|
1585
|
+
adjustment_mode: lastAdjustmentMode,
|
|
1586
|
+
actual_state: observedState,
|
|
1587
|
+
converged: true,
|
|
1588
|
+
within_tolerance: true,
|
|
1589
|
+
reason: verificationResult.verification?.reason ?? 'control converged to target value'
|
|
1590
|
+
},
|
|
1591
|
+
traceSteps
|
|
1592
|
+
});
|
|
1593
|
+
return {
|
|
1594
|
+
...base,
|
|
1595
|
+
target_state: {
|
|
1596
|
+
property,
|
|
1597
|
+
target_value: normalizedTargetValue,
|
|
1598
|
+
tolerance: effectiveTolerance
|
|
1599
|
+
},
|
|
1600
|
+
actual_state: observedState,
|
|
1601
|
+
within_tolerance: true,
|
|
1602
|
+
converged: true,
|
|
1603
|
+
attempts: attemptCount,
|
|
1604
|
+
adjustment_mode: lastAdjustmentMode
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
coordinateTapsAwaitingFreshGesture = true;
|
|
1608
|
+
cachedResolvedMatch = null;
|
|
1609
|
+
if (attemptCount < attemptsLimit) {
|
|
1610
|
+
continue adjustmentAttempts;
|
|
1611
|
+
}
|
|
945
1612
|
}
|
|
946
1613
|
}
|
|
947
|
-
if (
|
|
1614
|
+
if (attemptCount < attemptsLimit) {
|
|
1615
|
+
coordinateTapsAwaitingFreshGesture = false;
|
|
948
1616
|
lastAdjustmentMode = 'gesture';
|
|
949
1617
|
recordTraceStep('execute', 'retry', {
|
|
950
1618
|
attempt: attemptCount + 1,
|
|
@@ -958,7 +1626,7 @@ export class ToolsInteract {
|
|
|
958
1626
|
y1: currentPoint.y,
|
|
959
1627
|
x2: targetPoint.x,
|
|
960
1628
|
y2: targetPoint.y,
|
|
961
|
-
duration: 220,
|
|
1629
|
+
duration: inferredSliderState ? 520 : 220,
|
|
962
1630
|
deviceId: resolvedDeviceId
|
|
963
1631
|
});
|
|
964
1632
|
attemptCount++;
|
|
@@ -979,7 +1647,7 @@ export class ToolsInteract {
|
|
|
979
1647
|
recordTraceStep('verify', verificationResult.withinTolerance ? 'success' : 'retry', {
|
|
980
1648
|
attempt: attemptCount,
|
|
981
1649
|
property,
|
|
982
|
-
target_value:
|
|
1650
|
+
target_value: normalizedTargetValue,
|
|
983
1651
|
actual_state: observedState,
|
|
984
1652
|
reason: verificationResult.verification?.reason ?? 'gesture adjustment did not converge yet'
|
|
985
1653
|
});
|
|
@@ -995,8 +1663,8 @@ export class ToolsInteract {
|
|
|
995
1663
|
uiFingerprintBefore: fingerprintBefore,
|
|
996
1664
|
uiFingerprintAfter,
|
|
997
1665
|
details: {
|
|
998
|
-
target_value:
|
|
999
|
-
tolerance:
|
|
1666
|
+
target_value: normalizedTargetValue,
|
|
1667
|
+
tolerance: effectiveTolerance,
|
|
1000
1668
|
property,
|
|
1001
1669
|
attempts: attemptCount,
|
|
1002
1670
|
adjustment_mode: lastAdjustmentMode,
|
|
@@ -1011,8 +1679,8 @@ export class ToolsInteract {
|
|
|
1011
1679
|
...base,
|
|
1012
1680
|
target_state: {
|
|
1013
1681
|
property,
|
|
1014
|
-
target_value:
|
|
1015
|
-
tolerance:
|
|
1682
|
+
target_value: normalizedTargetValue,
|
|
1683
|
+
tolerance: effectiveTolerance
|
|
1016
1684
|
},
|
|
1017
1685
|
actual_state: observedState,
|
|
1018
1686
|
within_tolerance: true,
|
|
@@ -1028,7 +1696,7 @@ export class ToolsInteract {
|
|
|
1028
1696
|
recordTraceStep('verify', 'success', {
|
|
1029
1697
|
attempt: attemptCount,
|
|
1030
1698
|
property,
|
|
1031
|
-
target_value:
|
|
1699
|
+
target_value: normalizedTargetValue,
|
|
1032
1700
|
actual_state: observedState,
|
|
1033
1701
|
reason: verification?.reason ?? 'control converged to target value'
|
|
1034
1702
|
});
|
|
@@ -1043,8 +1711,8 @@ export class ToolsInteract {
|
|
|
1043
1711
|
uiFingerprintBefore: fingerprintBefore,
|
|
1044
1712
|
uiFingerprintAfter,
|
|
1045
1713
|
details: {
|
|
1046
|
-
target_value:
|
|
1047
|
-
tolerance:
|
|
1714
|
+
target_value: normalizedTargetValue,
|
|
1715
|
+
tolerance: effectiveTolerance,
|
|
1048
1716
|
property,
|
|
1049
1717
|
attempts: attemptCount,
|
|
1050
1718
|
adjustment_mode: lastAdjustmentMode,
|
|
@@ -1059,8 +1727,8 @@ export class ToolsInteract {
|
|
|
1059
1727
|
...base,
|
|
1060
1728
|
target_state: {
|
|
1061
1729
|
property,
|
|
1062
|
-
target_value:
|
|
1063
|
-
tolerance:
|
|
1730
|
+
target_value: normalizedTargetValue,
|
|
1731
|
+
tolerance: effectiveTolerance
|
|
1064
1732
|
},
|
|
1065
1733
|
actual_state: observedState,
|
|
1066
1734
|
within_tolerance: true,
|
|
@@ -1069,22 +1737,24 @@ export class ToolsInteract {
|
|
|
1069
1737
|
adjustment_mode: lastAdjustmentMode
|
|
1070
1738
|
};
|
|
1071
1739
|
}
|
|
1072
|
-
cachedResolvedMatch =
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
...
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1740
|
+
cachedResolvedMatch = inferredSliderState
|
|
1741
|
+
? null
|
|
1742
|
+
: {
|
|
1743
|
+
el: {
|
|
1744
|
+
...currentEl,
|
|
1745
|
+
state: {
|
|
1746
|
+
...(currentEl.state ?? null),
|
|
1747
|
+
...(observedState ? {
|
|
1748
|
+
[observedState.property]: observedState.value,
|
|
1749
|
+
raw_value: observedState.raw_value ?? observedState.value
|
|
1750
|
+
} : {})
|
|
1751
|
+
}
|
|
1752
|
+
},
|
|
1753
|
+
idx: resolved.match.idx
|
|
1754
|
+
};
|
|
1085
1755
|
}
|
|
1086
1756
|
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
|
|
1087
|
-
return buildFailure('
|
|
1757
|
+
return buildFailure('CONTROL_CONVERGENCE_FAILED', 'control did not converge within the allotted attempts', resolvedTarget, currentDevice, lastObservedState, attemptCount, lastAdjustmentMode, true, uiFingerprintAfter);
|
|
1088
1758
|
}
|
|
1089
1759
|
static async swipeHandler({ platform = 'android', x1, y1, x2, y2, duration, deviceId }) {
|
|
1090
1760
|
const { interact, resolved } = await ToolsInteract.getInteractionService(platform, deviceId);
|
|
@@ -1711,24 +2381,65 @@ export class ToolsInteract {
|
|
|
1711
2381
|
}
|
|
1712
2382
|
};
|
|
1713
2383
|
}
|
|
1714
|
-
static async waitForUIChangeHandler({ platform, deviceId, timeout_ms = 60000, stability_window_ms = 300, expected_change }) {
|
|
2384
|
+
static async waitForUIChangeHandler({ platform, deviceId, timeout_ms = 60000, stability_window_ms = 300, expected_change, scope = 'screen', target = null }) {
|
|
1715
2385
|
const start = Date.now();
|
|
1716
2386
|
const pollIntervalMs = 300;
|
|
1717
2387
|
const stabilityWindow = Math.max(0, typeof stability_window_ms === 'number' ? stability_window_ms : 300);
|
|
1718
2388
|
let baseline = null;
|
|
2389
|
+
let baselineScope = null;
|
|
1719
2390
|
let lastObservedRevision = null;
|
|
1720
2391
|
let lastLoadingState = null;
|
|
2392
|
+
let lastSnapshotFreshnessMs = null;
|
|
1721
2393
|
let candidateSignatures = null;
|
|
1722
2394
|
let candidateObservedChange = null;
|
|
1723
2395
|
let candidateSinceMs = null;
|
|
2396
|
+
let lastChangeSummary = null;
|
|
2397
|
+
let lastScopeResolution = {
|
|
2398
|
+
scope: scope === 'subtree' ? 'subtree' : 'screen',
|
|
2399
|
+
target: target && typeof target === 'string' ? target : null,
|
|
2400
|
+
resolved: scope !== 'subtree',
|
|
2401
|
+
resolvedIndex: null,
|
|
2402
|
+
resolvedStableId: null,
|
|
2403
|
+
reason: scope === 'subtree' ? 'target not resolved yet' : 'screen scope'
|
|
2404
|
+
};
|
|
1724
2405
|
while (Date.now() - start < timeout_ms) {
|
|
1725
2406
|
try {
|
|
1726
2407
|
const tree = await ToolsObserve.getUITreeHandler({ platform, deviceId });
|
|
1727
|
-
const
|
|
2408
|
+
const scopedTree = ToolsInteract._resolveUiChangeScope(tree, scope, target);
|
|
2409
|
+
if (scopedTree.error) {
|
|
2410
|
+
lastScopeResolution = scopedTree.resolution;
|
|
2411
|
+
return {
|
|
2412
|
+
success: false,
|
|
2413
|
+
observed_change: null,
|
|
2414
|
+
snapshot_revision: typeof tree?.snapshot_revision === 'number' ? tree.snapshot_revision : lastObservedRevision ?? undefined,
|
|
2415
|
+
snapshot_freshness_ms: typeof tree?.captured_at_ms === 'number' ? Math.max(0, Date.now() - tree.captured_at_ms) : lastSnapshotFreshnessMs ?? null,
|
|
2416
|
+
timeout: true,
|
|
2417
|
+
elapsed_ms: Date.now() - start,
|
|
2418
|
+
expected_change,
|
|
2419
|
+
loading_state: tree?.loading_state ?? lastLoadingState ?? null,
|
|
2420
|
+
scope: scopedTree.resolution.scope,
|
|
2421
|
+
target: scopedTree.resolution.target,
|
|
2422
|
+
stability_state: 'transient',
|
|
2423
|
+
change_summary: lastChangeSummary,
|
|
2424
|
+
reason: scopedTree.error.message,
|
|
2425
|
+
error: scopedTree.error
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
const scopedElements = scopedTree.elements;
|
|
2429
|
+
const scopedSignatureTree = {
|
|
2430
|
+
...tree,
|
|
2431
|
+
elements: scopedElements
|
|
2432
|
+
};
|
|
2433
|
+
const signatures = ToolsInteract._buildUiChangeSignatures(scopedSignatureTree);
|
|
1728
2434
|
lastObservedRevision = typeof tree?.snapshot_revision === 'number' ? tree.snapshot_revision : lastObservedRevision;
|
|
1729
2435
|
lastLoadingState = tree?.loading_state ?? lastLoadingState;
|
|
2436
|
+
lastSnapshotFreshnessMs = typeof tree?.captured_at_ms === 'number' ? Math.max(0, Date.now() - tree.captured_at_ms) : lastSnapshotFreshnessMs;
|
|
2437
|
+
lastChangeSummary = baseline ? ToolsInteract._summarizeUiChangeDelta((baselineScope?.elements ?? []), scopedElements) : lastChangeSummary;
|
|
2438
|
+
lastScopeResolution = scopedTree.resolution;
|
|
2439
|
+
baselineScope = baselineScope ?? scopedTree;
|
|
1730
2440
|
if (!baseline) {
|
|
1731
2441
|
baseline = signatures;
|
|
2442
|
+
baselineScope = scopedTree;
|
|
1732
2443
|
}
|
|
1733
2444
|
else {
|
|
1734
2445
|
const observedChange = ToolsInteract._matchesUiChange(expected_change, baseline, signatures);
|
|
@@ -1744,10 +2455,15 @@ export class ToolsInteract {
|
|
|
1744
2455
|
success: true,
|
|
1745
2456
|
observed_change: candidateObservedChange ?? observedChange,
|
|
1746
2457
|
snapshot_revision: lastObservedRevision ?? undefined,
|
|
2458
|
+
snapshot_freshness_ms: lastSnapshotFreshnessMs ?? null,
|
|
1747
2459
|
timeout: false,
|
|
1748
2460
|
elapsed_ms: Date.now() - start,
|
|
1749
2461
|
expected_change,
|
|
1750
2462
|
loading_state: lastLoadingState ?? null,
|
|
2463
|
+
scope: lastScopeResolution.scope,
|
|
2464
|
+
target: lastScopeResolution.target,
|
|
2465
|
+
stability_state: 'stable',
|
|
2466
|
+
change_summary: lastChangeSummary,
|
|
1751
2467
|
reason: 'UI change observed'
|
|
1752
2468
|
};
|
|
1753
2469
|
}
|
|
@@ -1768,10 +2484,15 @@ export class ToolsInteract {
|
|
|
1768
2484
|
success: false,
|
|
1769
2485
|
observed_change: null,
|
|
1770
2486
|
snapshot_revision: lastObservedRevision ?? undefined,
|
|
2487
|
+
snapshot_freshness_ms: lastSnapshotFreshnessMs ?? null,
|
|
1771
2488
|
timeout: true,
|
|
1772
2489
|
elapsed_ms: Date.now() - start,
|
|
1773
2490
|
expected_change,
|
|
1774
2491
|
loading_state: lastLoadingState ?? null,
|
|
2492
|
+
scope: lastScopeResolution.scope,
|
|
2493
|
+
target: lastScopeResolution.target,
|
|
2494
|
+
stability_state: 'transient',
|
|
2495
|
+
change_summary: lastChangeSummary,
|
|
1775
2496
|
reason: 'timeout'
|
|
1776
2497
|
};
|
|
1777
2498
|
}
|