mobile-debug-mcp 0.26.5 → 0.27.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/dist/interact/index.js +352 -185
- package/dist/server/common.js +39 -0
- package/dist/server-core.js +1 -1
- package/docs/CHANGELOG.md +3 -0
- package/docs/ROADMAP.md +109 -11
- package/docs/rfcs/010-verification-stabilization-and-temporal-convergence.md +265 -0
- package/docs/rfcs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows.md +321 -0
- package/docs/rfcs/011.1-recovery-contract-types-and-runtime-wiring-spec.md +253 -0
- package/docs/rfcs/012.md +203 -0
- package/docs/specs/mcp-tooling-spec-v1.md +12 -0
- package/docs/tools/interact.md +10 -0
- package/package.json +1 -1
- package/src/interact/index.ts +393 -186
- package/src/server/common.ts +44 -1
- package/src/server-core.ts +1 -1
- package/src/types.ts +36 -0
- package/test/unit/interact/adjust_control.test.ts +77 -1
- package/test/unit/interact/verification_stabilization.test.ts +94 -0
- package/test/unit/server/common.test.ts +36 -1
package/dist/interact/index.js
CHANGED
|
@@ -291,6 +291,38 @@ export class ToolsInteract {
|
|
|
291
291
|
const safeRatio = Math.min(1 - (endpointMargin * 0.25), Math.max(endpointMargin, targetRatio + centerBias + directionBias + edgeBias));
|
|
292
292
|
return ToolsInteract._buildControlPoint(bounds, safeRatio, axis);
|
|
293
293
|
}
|
|
294
|
+
static _buildAdjustmentProbePoints(bounds, targetValue, currentValue, min, max, axis) {
|
|
295
|
+
const targetPoint = ToolsInteract._buildConservativeControlPoint(bounds, targetValue, currentValue, min, max, axis);
|
|
296
|
+
const currentPoint = currentValue !== null
|
|
297
|
+
? ToolsInteract._buildControlPoint(bounds, (currentValue - min) / (max - min), axis)
|
|
298
|
+
: ToolsInteract._buildControlPoint(bounds, 0.5, axis);
|
|
299
|
+
const [left, top, right, bottom] = bounds;
|
|
300
|
+
const width = Math.max(1, right - left);
|
|
301
|
+
const height = Math.max(1, bottom - top);
|
|
302
|
+
const crossAxisBumps = axis === 'horizontal'
|
|
303
|
+
? [Math.max(24, Math.floor(height * 0.75)), Math.max(40, Math.floor(height * 1.5))]
|
|
304
|
+
: [Math.max(24, Math.floor(width * 0.75)), Math.max(40, Math.floor(width * 1.5))];
|
|
305
|
+
const clampPoint = (point) => ({
|
|
306
|
+
x: axis === 'horizontal'
|
|
307
|
+
? Math.max(left, Math.min(right, point.x))
|
|
308
|
+
: Math.max(left, Math.min(right + Math.max(width, height), point.x)),
|
|
309
|
+
y: axis === 'vertical'
|
|
310
|
+
? Math.max(top, Math.min(bottom, point.y))
|
|
311
|
+
: Math.max(top, Math.min(bottom + Math.max(height, width), point.y))
|
|
312
|
+
});
|
|
313
|
+
const probes = [targetPoint, currentPoint];
|
|
314
|
+
for (const bump of crossAxisBumps) {
|
|
315
|
+
if (axis === 'horizontal') {
|
|
316
|
+
probes.push({ x: targetPoint.x, y: bottom + bump }, { x: currentPoint.x, y: bottom + bump });
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
probes.push({ x: right + bump, y: targetPoint.y }, { x: right + bump, y: currentPoint.y });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return Array.from(new Map(probes
|
|
323
|
+
.map(clampPoint)
|
|
324
|
+
.map((point) => [`${point.x}:${point.y}`, point])).values());
|
|
325
|
+
}
|
|
294
326
|
static _controlAxis(el, bounds) {
|
|
295
327
|
const type = ToolsInteract._normalize(el.type ?? el.class ?? '');
|
|
296
328
|
const role = ToolsInteract._normalize(el.role ?? '');
|
|
@@ -743,6 +775,7 @@ export class ToolsInteract {
|
|
|
743
775
|
const currentPoint = currentValue !== null
|
|
744
776
|
? ToolsInteract._buildControlPoint(bounds, (currentValue - min) / (max - min), axis)
|
|
745
777
|
: ToolsInteract._buildControlPoint(bounds, 0.5, axis);
|
|
778
|
+
const probePoints = ToolsInteract._buildAdjustmentProbePoints(bounds, targetValue, currentValue, min, max, axis);
|
|
746
779
|
const runVerification = async () => {
|
|
747
780
|
const verification = await ToolsInteract.expectStateHandler({
|
|
748
781
|
element_id: resolvedTarget?.elementId ?? element_id,
|
|
@@ -770,36 +803,66 @@ export class ToolsInteract {
|
|
|
770
803
|
withinTolerance: observedValue !== null && Math.abs(observedValue - targetValue) <= normalizedTolerance
|
|
771
804
|
};
|
|
772
805
|
};
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
attemptCount++;
|
|
782
|
-
if (!primaryActionResult.success) {
|
|
783
|
-
lastAdjustmentMode = 'gesture';
|
|
784
|
-
const fallbackActionResult = await ToolsInteract.swipeHandler({
|
|
806
|
+
let actionDevice = currentDevice;
|
|
807
|
+
let observedState = actualState;
|
|
808
|
+
let verification = null;
|
|
809
|
+
let verificationResult = { verification: null, observedState: actualState, withinTolerance: false };
|
|
810
|
+
for (let i = 0; i < probePoints.length; i++) {
|
|
811
|
+
const probePoint = probePoints[i];
|
|
812
|
+
lastAdjustmentMode = 'coordinate';
|
|
813
|
+
const actionResult = await ToolsInteract.tapHandler({
|
|
785
814
|
platform: resolvedPlatform,
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
x2: targetPoint.x,
|
|
789
|
-
y2: targetPoint.y,
|
|
790
|
-
duration: 220,
|
|
815
|
+
x: probePoint.x,
|
|
816
|
+
y: probePoint.y,
|
|
791
817
|
deviceId: resolvedDeviceId
|
|
792
818
|
});
|
|
793
819
|
attemptCount++;
|
|
794
|
-
|
|
795
|
-
|
|
820
|
+
actionDevice = actionResult.device ?? actionDevice;
|
|
821
|
+
if (!actionResult.success) {
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
verificationResult = await runVerification();
|
|
825
|
+
observedState = verificationResult.observedState;
|
|
826
|
+
lastObservedState = observedState;
|
|
827
|
+
if (verificationResult.withinTolerance) {
|
|
828
|
+
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
|
|
829
|
+
const base = buildActionExecutionResult({
|
|
830
|
+
actionType,
|
|
831
|
+
sourceModule: 'interact',
|
|
832
|
+
device: actionDevice ?? currentDevice,
|
|
833
|
+
selector: targetSelector,
|
|
834
|
+
resolved: resolvedTarget,
|
|
835
|
+
success: true,
|
|
836
|
+
uiFingerprintBefore: fingerprintBefore,
|
|
837
|
+
uiFingerprintAfter,
|
|
838
|
+
details: {
|
|
839
|
+
target_value: targetValue,
|
|
840
|
+
tolerance: normalizedTolerance,
|
|
841
|
+
property,
|
|
842
|
+
attempts: attemptCount,
|
|
843
|
+
adjustment_mode: lastAdjustmentMode,
|
|
844
|
+
actual_state: observedState,
|
|
845
|
+
converged: true,
|
|
846
|
+
within_tolerance: true,
|
|
847
|
+
reason: verificationResult.verification?.reason ?? 'control converged to target value'
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
return {
|
|
851
|
+
...base,
|
|
852
|
+
target_state: {
|
|
853
|
+
property,
|
|
854
|
+
target_value: targetValue,
|
|
855
|
+
tolerance: normalizedTolerance
|
|
856
|
+
},
|
|
857
|
+
actual_state: observedState,
|
|
858
|
+
within_tolerance: true,
|
|
859
|
+
converged: true,
|
|
860
|
+
attempts: attemptCount,
|
|
861
|
+
adjustment_mode: lastAdjustmentMode
|
|
862
|
+
};
|
|
796
863
|
}
|
|
797
|
-
actionDevice = fallbackActionResult.device ?? actionDevice;
|
|
798
864
|
}
|
|
799
|
-
|
|
800
|
-
let observedState = verificationResult.observedState;
|
|
801
|
-
lastObservedState = observedState;
|
|
802
|
-
if (!verificationResult.withinTolerance && currentValue !== null) {
|
|
865
|
+
if (currentValue !== null) {
|
|
803
866
|
lastAdjustmentMode = 'gesture';
|
|
804
867
|
const fallbackActionResult = await ToolsInteract.swipeHandler({
|
|
805
868
|
platform: resolvedPlatform,
|
|
@@ -812,12 +875,51 @@ export class ToolsInteract {
|
|
|
812
875
|
});
|
|
813
876
|
attemptCount++;
|
|
814
877
|
if (!fallbackActionResult.success) {
|
|
815
|
-
return buildFailure('UNKNOWN', fallbackActionResult.error ?? 'adjustment gesture failed', resolvedTarget, fallbackActionResult.device, observedState ?? actualState, attemptCount, lastAdjustmentMode, false);
|
|
878
|
+
return buildFailure('UNKNOWN', fallbackActionResult.error ?? 'adjustment gesture failed', resolvedTarget, fallbackActionResult.device ?? actionDevice, observedState ?? actualState, attemptCount, lastAdjustmentMode, false);
|
|
816
879
|
}
|
|
880
|
+
actionDevice = fallbackActionResult.device ?? actionDevice;
|
|
817
881
|
verificationResult = await runVerification();
|
|
818
882
|
observedState = verificationResult.observedState;
|
|
883
|
+
lastObservedState = observedState;
|
|
884
|
+
if (verificationResult.withinTolerance) {
|
|
885
|
+
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
|
|
886
|
+
const base = buildActionExecutionResult({
|
|
887
|
+
actionType,
|
|
888
|
+
sourceModule: 'interact',
|
|
889
|
+
device: actionDevice ?? currentDevice,
|
|
890
|
+
selector: targetSelector,
|
|
891
|
+
resolved: resolvedTarget,
|
|
892
|
+
success: true,
|
|
893
|
+
uiFingerprintBefore: fingerprintBefore,
|
|
894
|
+
uiFingerprintAfter,
|
|
895
|
+
details: {
|
|
896
|
+
target_value: targetValue,
|
|
897
|
+
tolerance: normalizedTolerance,
|
|
898
|
+
property,
|
|
899
|
+
attempts: attemptCount,
|
|
900
|
+
adjustment_mode: lastAdjustmentMode,
|
|
901
|
+
actual_state: observedState,
|
|
902
|
+
converged: true,
|
|
903
|
+
within_tolerance: true,
|
|
904
|
+
reason: verificationResult.verification?.reason ?? 'control converged to target value'
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
return {
|
|
908
|
+
...base,
|
|
909
|
+
target_state: {
|
|
910
|
+
property,
|
|
911
|
+
target_value: targetValue,
|
|
912
|
+
tolerance: normalizedTolerance
|
|
913
|
+
},
|
|
914
|
+
actual_state: observedState,
|
|
915
|
+
within_tolerance: true,
|
|
916
|
+
converged: true,
|
|
917
|
+
attempts: attemptCount,
|
|
918
|
+
adjustment_mode: lastAdjustmentMode
|
|
919
|
+
};
|
|
920
|
+
}
|
|
819
921
|
}
|
|
820
|
-
|
|
922
|
+
verification = verificationResult.verification;
|
|
821
923
|
lastObservedState = observedState;
|
|
822
924
|
if (verificationResult.withinTolerance) {
|
|
823
925
|
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
|
|
@@ -1224,6 +1326,10 @@ export class ToolsInteract {
|
|
|
1224
1326
|
let lastMatchedCount = 0;
|
|
1225
1327
|
let lastMatchedElement = null;
|
|
1226
1328
|
let lastConditionSatisfied = false;
|
|
1329
|
+
let matchedAt = null;
|
|
1330
|
+
let stableMatchCount = 0;
|
|
1331
|
+
const stableObservationCount = 2;
|
|
1332
|
+
const snapshotStaleThresholdMs = 500;
|
|
1227
1333
|
// Precompute normalized selector values and helpers (constant across polls)
|
|
1228
1334
|
const normalize = ToolsInteract._normalize;
|
|
1229
1335
|
const containsFlag = !!selector?.contains;
|
|
@@ -1338,23 +1444,34 @@ export class ToolsInteract {
|
|
|
1338
1444
|
lastMatchedCount = matchedCount;
|
|
1339
1445
|
lastConditionSatisfied = conditionMet;
|
|
1340
1446
|
lastMatchedElement = matchedElement ? ToolsInteract._buildResolvedElement(resolvedPlatform, resolvedDeviceId, matchedElement.el, matchedElement.idx) : null;
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1447
|
+
const now = Date.now();
|
|
1448
|
+
const snapshotAgeMs = typeof tree?.captured_at_ms === 'number' ? now - tree.captured_at_ms : null;
|
|
1449
|
+
const snapshotFresh = snapshotAgeMs === null || snapshotAgeMs <= snapshotStaleThresholdMs;
|
|
1450
|
+
if (conditionMet && snapshotFresh) {
|
|
1451
|
+
if (matchedAt === null)
|
|
1452
|
+
matchedAt = now;
|
|
1453
|
+
stableMatchCount++;
|
|
1454
|
+
if (stableMatchCount >= stableObservationCount) {
|
|
1455
|
+
const latency_ms = now - overallStart;
|
|
1456
|
+
const outEl = lastMatchedElement;
|
|
1457
|
+
return {
|
|
1458
|
+
status: 'success',
|
|
1459
|
+
matched: matchedCount,
|
|
1460
|
+
element: outEl,
|
|
1461
|
+
metrics: { latency_ms, poll_count: totalPollCount, attempts },
|
|
1462
|
+
requested,
|
|
1463
|
+
observed: {
|
|
1464
|
+
matched_count: matchedCount,
|
|
1465
|
+
condition_satisfied: true,
|
|
1466
|
+
selected_index: outEl?.index ?? null,
|
|
1467
|
+
last_matched_element: outEl
|
|
1468
|
+
}
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
else {
|
|
1473
|
+
stableMatchCount = 0;
|
|
1474
|
+
matchedAt = null;
|
|
1358
1475
|
}
|
|
1359
1476
|
}
|
|
1360
1477
|
catch (e) {
|
|
@@ -1674,174 +1791,224 @@ export class ToolsInteract {
|
|
|
1674
1791
|
retryable: errorCode === 'TIMEOUT'
|
|
1675
1792
|
};
|
|
1676
1793
|
}
|
|
1677
|
-
static async expectStateHandler({ selector, element_id, property, expected, platform, deviceId }) {
|
|
1678
|
-
const tree = await ToolsObserve.getUITreeHandler({ platform, deviceId });
|
|
1679
|
-
const elements = Array.isArray(tree?.elements) ? tree.elements : [];
|
|
1680
|
-
const treePlatform = tree?.device?.platform === 'ios' ? 'ios' : (platform || 'android');
|
|
1681
|
-
const treeDeviceId = tree?.device?.id || deviceId;
|
|
1682
|
-
let matched = null;
|
|
1683
|
-
if (element_id) {
|
|
1684
|
-
const resolved = ToolsInteract._resolvedUiElements.get(element_id);
|
|
1685
|
-
if (resolved) {
|
|
1686
|
-
const current = ToolsInteract._findCurrentResolvedElement(elements, treePlatform, treeDeviceId, resolved);
|
|
1687
|
-
if (current)
|
|
1688
|
-
matched = { el: current.el, idx: current.index };
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1691
|
-
if (!matched && selector) {
|
|
1692
|
-
matched = ToolsInteract._findFirstMatchingElement(elements, selector);
|
|
1693
|
-
}
|
|
1694
|
-
if (!matched) {
|
|
1695
|
-
return {
|
|
1696
|
-
success: false,
|
|
1697
|
-
selector,
|
|
1698
|
-
element_id: element_id ?? null,
|
|
1699
|
-
expected_state: { property, expected },
|
|
1700
|
-
reason: 'element not found',
|
|
1701
|
-
failure_code: 'ELEMENT_NOT_FOUND',
|
|
1702
|
-
retryable: true
|
|
1703
|
-
};
|
|
1704
|
-
}
|
|
1705
|
-
const resolvedElement = ToolsInteract._resolvedTargetFromElement(ToolsInteract._computeElementId(treePlatform, treeDeviceId, matched.el, matched.idx), matched.el, matched.idx);
|
|
1706
|
-
const observedState = matched.el.state ?? null;
|
|
1707
|
-
const actual = observedState?.[property] ?? null;
|
|
1794
|
+
static async expectStateHandler({ selector, element_id, property, expected, platform, deviceId, stabilization_window_ms = 1000, stable_observation_count = 2, snapshot_stale_threshold_ms = 500, poll_interval_ms = 150 }) {
|
|
1708
1795
|
const compareBoolean = (value) => typeof value === 'boolean' ? value : null;
|
|
1709
1796
|
const compareString = (value) => typeof value === 'string' ? value : null;
|
|
1710
1797
|
const compareNumber = (value) => typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1798
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
1799
|
+
const start = Date.now();
|
|
1800
|
+
const deadline = start + Math.max(500, stabilization_window_ms);
|
|
1801
|
+
const stableTarget = Math.max(1, Math.floor(stable_observation_count || 2));
|
|
1802
|
+
const pollDelay = Math.max(100, Math.min(poll_interval_ms || 150, 200));
|
|
1803
|
+
const staleThreshold = Math.max(300, Math.min(snapshot_stale_threshold_ms || 500, 800));
|
|
1804
|
+
let attempts = 0;
|
|
1805
|
+
let stableCount = 0;
|
|
1806
|
+
let lastReason = 'element not found';
|
|
1807
|
+
let lastFailureCode = 'ELEMENT_NOT_FOUND';
|
|
1808
|
+
let lastObservedElement = null;
|
|
1809
|
+
let lastObservedValue = null;
|
|
1810
|
+
let lastRawValue = null;
|
|
1811
|
+
let lastResolvedElementId = element_id ?? null;
|
|
1812
|
+
while (Date.now() <= deadline) {
|
|
1813
|
+
attempts++;
|
|
1814
|
+
const tree = await ToolsObserve.getUITreeHandler({ platform, deviceId });
|
|
1815
|
+
const elements = Array.isArray(tree?.elements) ? tree.elements : [];
|
|
1816
|
+
const treePlatform = tree?.device?.platform === 'ios' ? 'ios' : (platform || 'android');
|
|
1817
|
+
const treeDeviceId = tree?.device?.id || deviceId;
|
|
1818
|
+
const treeAgeMs = typeof tree?.captured_at_ms === 'number' ? Date.now() - tree.captured_at_ms : null;
|
|
1819
|
+
let matched = null;
|
|
1820
|
+
if (element_id) {
|
|
1821
|
+
const resolved = ToolsInteract._resolvedUiElements.get(element_id);
|
|
1822
|
+
if (resolved) {
|
|
1823
|
+
const current = ToolsInteract._findCurrentResolvedElement(elements, treePlatform, treeDeviceId, resolved);
|
|
1824
|
+
if (current)
|
|
1825
|
+
matched = { el: current.el, idx: current.index };
|
|
1732
1826
|
}
|
|
1733
|
-
observedValue = actualBool;
|
|
1734
|
-
break;
|
|
1735
1827
|
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
const expectedNumber = compareNumber(expected);
|
|
1739
|
-
const actualNumber = compareNumber(actual);
|
|
1740
|
-
if (expectedNumber !== null && actualNumber !== null) {
|
|
1741
|
-
success = actualNumber === expectedNumber;
|
|
1742
|
-
rawValue = actualNumber;
|
|
1743
|
-
observedValue = actualNumber;
|
|
1744
|
-
reason = success ? 'value matches expected value' : `expected value=${expectedNumber} but observed ${actualNumber}`;
|
|
1745
|
-
break;
|
|
1746
|
-
}
|
|
1747
|
-
const expectedString = typeof expected === 'string' ? expected : null;
|
|
1748
|
-
const actualString = compareString(actual);
|
|
1749
|
-
if (expectedString !== null && actualString !== null) {
|
|
1750
|
-
success = actualString === expectedString;
|
|
1751
|
-
rawValue = actualString;
|
|
1752
|
-
observedValue = actualString;
|
|
1753
|
-
reason = success ? 'value matches expected value' : `expected value=${expectedString} but observed ${actualString}`;
|
|
1754
|
-
}
|
|
1755
|
-
else {
|
|
1756
|
-
reason = 'value state unavailable';
|
|
1757
|
-
}
|
|
1758
|
-
break;
|
|
1828
|
+
if (!matched && selector) {
|
|
1829
|
+
matched = ToolsInteract._findFirstMatchingElement(elements, selector);
|
|
1759
1830
|
}
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1831
|
+
if (!matched) {
|
|
1832
|
+
lastReason = 'element not found';
|
|
1833
|
+
lastFailureCode = 'ELEMENT_NOT_FOUND';
|
|
1834
|
+
stableCount = 0;
|
|
1835
|
+
await sleep(pollDelay);
|
|
1836
|
+
continue;
|
|
1837
|
+
}
|
|
1838
|
+
const resolvedElement = ToolsInteract._resolvedTargetFromElement(ToolsInteract._computeElementId(treePlatform, treeDeviceId, matched.el, matched.idx), matched.el, matched.idx);
|
|
1839
|
+
lastResolvedElementId = resolvedElement.elementId;
|
|
1840
|
+
lastObservedElement = { ...resolvedElement, state: matched.el.state ?? null };
|
|
1841
|
+
if (treeAgeMs !== null && treeAgeMs > staleThreshold) {
|
|
1842
|
+
lastReason = 'stale snapshot';
|
|
1843
|
+
lastFailureCode = 'UNKNOWN';
|
|
1844
|
+
stableCount = 0;
|
|
1845
|
+
await sleep(pollDelay);
|
|
1846
|
+
continue;
|
|
1847
|
+
}
|
|
1848
|
+
const observedState = matched.el.state ?? null;
|
|
1849
|
+
const actual = observedState?.[property] ?? null;
|
|
1850
|
+
let success = false;
|
|
1851
|
+
let reason = '';
|
|
1852
|
+
let rawValue = null;
|
|
1853
|
+
let observedValue = actual;
|
|
1854
|
+
switch (property) {
|
|
1855
|
+
case 'checked':
|
|
1856
|
+
case 'focused':
|
|
1857
|
+
case 'expanded':
|
|
1858
|
+
case 'enabled': {
|
|
1859
|
+
const expectedBool = compareBoolean(expected);
|
|
1860
|
+
const actualBool = compareBoolean(actual);
|
|
1861
|
+
if (expectedBool === null) {
|
|
1862
|
+
reason = `expected ${property} must be boolean`;
|
|
1863
|
+
}
|
|
1864
|
+
else if (actualBool === null) {
|
|
1865
|
+
reason = `${property} state unavailable`;
|
|
1866
|
+
}
|
|
1867
|
+
else {
|
|
1868
|
+
rawValue = actualBool;
|
|
1869
|
+
success = actualBool === expectedBool;
|
|
1870
|
+
reason = success ? `${property} matches expected value` : `expected ${property}=${expectedBool} but observed ${actualBool}`;
|
|
1871
|
+
}
|
|
1872
|
+
observedValue = actualBool;
|
|
1769
1873
|
break;
|
|
1770
1874
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1875
|
+
case 'value':
|
|
1876
|
+
case 'raw_value': {
|
|
1877
|
+
const expectedNumber = compareNumber(expected);
|
|
1878
|
+
const actualNumber = compareNumber(actual);
|
|
1879
|
+
if (expectedNumber !== null && actualNumber !== null) {
|
|
1880
|
+
success = actualNumber === expectedNumber;
|
|
1881
|
+
rawValue = actualNumber;
|
|
1882
|
+
observedValue = actualNumber;
|
|
1883
|
+
reason = success ? 'value matches expected value' : `expected value=${expectedNumber} but observed ${actualNumber}`;
|
|
1775
1884
|
break;
|
|
1776
1885
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1886
|
+
const expectedString = typeof expected === 'string' ? expected : null;
|
|
1887
|
+
const actualString = compareString(actual);
|
|
1888
|
+
if (expectedString !== null && actualString !== null) {
|
|
1889
|
+
success = actualString === expectedString;
|
|
1890
|
+
rawValue = actualString;
|
|
1891
|
+
observedValue = actualString;
|
|
1892
|
+
reason = success ? 'value matches expected value' : `expected value=${expectedString} but observed ${actualString}`;
|
|
1893
|
+
}
|
|
1894
|
+
else {
|
|
1895
|
+
reason = 'value state unavailable';
|
|
1896
|
+
}
|
|
1781
1897
|
break;
|
|
1782
1898
|
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1899
|
+
case 'selected': {
|
|
1900
|
+
const expectedBool = typeof expected === 'boolean' ? expected : null;
|
|
1901
|
+
const expectedString = typeof expected === 'string'
|
|
1902
|
+
? expected
|
|
1903
|
+
: expected && typeof expected === 'object'
|
|
1904
|
+
? String(expected.id ?? expected.label ?? '')
|
|
1905
|
+
: null;
|
|
1906
|
+
if (!observedState || observedState.selected === undefined || observedState.selected === null) {
|
|
1907
|
+
reason = 'selected state unavailable';
|
|
1908
|
+
break;
|
|
1909
|
+
}
|
|
1910
|
+
if (expectedBool !== null) {
|
|
1911
|
+
const actualBool = typeof observedState.selected === 'boolean' ? observedState.selected : null;
|
|
1912
|
+
if (actualBool === null) {
|
|
1913
|
+
reason = 'selected state is not boolean';
|
|
1914
|
+
break;
|
|
1915
|
+
}
|
|
1916
|
+
rawValue = actualBool;
|
|
1917
|
+
observedValue = actualBool;
|
|
1918
|
+
success = actualBool === expectedBool;
|
|
1919
|
+
reason = success ? 'selected matches expected value' : `expected selected=${expectedBool} but observed ${actualBool}`;
|
|
1920
|
+
break;
|
|
1921
|
+
}
|
|
1922
|
+
const actualSelected = typeof observedState.selected === 'object' && observedState.selected !== null
|
|
1923
|
+
? String(observedState.selected.id ?? observedState.selected.label ?? '')
|
|
1924
|
+
: String(observedState.selected);
|
|
1925
|
+
const actualString = actualSelected.trim();
|
|
1926
|
+
if (!expectedString) {
|
|
1927
|
+
reason = 'expected selected must be boolean, string, or object with id/label';
|
|
1928
|
+
break;
|
|
1929
|
+
}
|
|
1930
|
+
rawValue = actualString;
|
|
1931
|
+
observedValue = actualString;
|
|
1932
|
+
success = actualString === expectedString;
|
|
1933
|
+
reason = success ? 'selected matches expected value' : `expected selected=${expectedString} but observed ${actualString}`;
|
|
1789
1934
|
break;
|
|
1790
1935
|
}
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1936
|
+
case 'text_value': {
|
|
1937
|
+
const expectedString = typeof expected === 'string' ? expected : null;
|
|
1938
|
+
const actualString = compareString(actual);
|
|
1939
|
+
if (!expectedString) {
|
|
1940
|
+
reason = 'expected text_value must be string';
|
|
1941
|
+
}
|
|
1942
|
+
else if (!actualString) {
|
|
1943
|
+
reason = 'text_value state unavailable';
|
|
1944
|
+
}
|
|
1945
|
+
else {
|
|
1946
|
+
success = actualString === expectedString;
|
|
1947
|
+
rawValue = actualString;
|
|
1948
|
+
observedValue = actualString;
|
|
1949
|
+
reason = success ? 'text_value matches expected value' : `expected text_value=${expectedString} but observed ${actualString}`;
|
|
1950
|
+
}
|
|
1951
|
+
break;
|
|
1805
1952
|
}
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1953
|
+
default: {
|
|
1954
|
+
if (actual !== null && actual !== undefined) {
|
|
1955
|
+
success = actual === expected;
|
|
1956
|
+
observedValue = actual;
|
|
1957
|
+
rawValue = typeof actual === 'string' || typeof actual === 'number' || typeof actual === 'boolean' ? actual : null;
|
|
1958
|
+
reason = success ? `${property} matches expected value` : `expected ${property} to match but observed ${String(actual)}`;
|
|
1959
|
+
}
|
|
1960
|
+
else {
|
|
1961
|
+
reason = `unsupported or unavailable state property: ${property}`;
|
|
1962
|
+
}
|
|
1811
1963
|
}
|
|
1812
|
-
break;
|
|
1813
1964
|
}
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1965
|
+
if (success) {
|
|
1966
|
+
stableCount++;
|
|
1967
|
+
if (stableCount >= stableTarget) {
|
|
1968
|
+
return {
|
|
1969
|
+
success: true,
|
|
1970
|
+
selector,
|
|
1971
|
+
element_id: lastResolvedElementId,
|
|
1972
|
+
expected_state: { property, expected },
|
|
1973
|
+
element: lastObservedElement,
|
|
1974
|
+
observed_state: {
|
|
1975
|
+
property,
|
|
1976
|
+
value: observedValue,
|
|
1977
|
+
...(rawValue !== null ? { raw_value: rawValue } : {})
|
|
1978
|
+
},
|
|
1979
|
+
reason,
|
|
1980
|
+
stabilization_attempts: attempts,
|
|
1981
|
+
stabilization_window_ms: Date.now() - start,
|
|
1982
|
+
stable_observation_count: stableCount,
|
|
1983
|
+
snapshot_freshness_ms: treeAgeMs ?? undefined
|
|
1984
|
+
};
|
|
1823
1985
|
}
|
|
1824
1986
|
}
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1987
|
+
else {
|
|
1988
|
+
stableCount = 0;
|
|
1989
|
+
lastReason = reason || lastReason;
|
|
1990
|
+
lastFailureCode = 'UNKNOWN';
|
|
1991
|
+
}
|
|
1992
|
+
if (!success) {
|
|
1993
|
+
lastObservedValue = observedValue;
|
|
1994
|
+
lastRawValue = rawValue;
|
|
1995
|
+
}
|
|
1996
|
+
await sleep(pollDelay);
|
|
1828
1997
|
}
|
|
1829
1998
|
return {
|
|
1830
|
-
success,
|
|
1999
|
+
success: false,
|
|
1831
2000
|
selector,
|
|
1832
|
-
element_id:
|
|
2001
|
+
element_id: lastResolvedElementId,
|
|
1833
2002
|
expected_state: { property, expected },
|
|
1834
|
-
element:
|
|
1835
|
-
...resolvedElement,
|
|
1836
|
-
state: observedState
|
|
1837
|
-
},
|
|
2003
|
+
element: lastObservedElement,
|
|
1838
2004
|
observed_state: {
|
|
1839
2005
|
property,
|
|
1840
|
-
value:
|
|
1841
|
-
...(
|
|
2006
|
+
value: lastObservedValue,
|
|
2007
|
+
...(lastRawValue !== null ? { raw_value: lastRawValue } : {})
|
|
1842
2008
|
},
|
|
1843
|
-
reason,
|
|
1844
|
-
|
|
2009
|
+
reason: lastReason,
|
|
2010
|
+
failure_code: lastFailureCode,
|
|
2011
|
+
retryable: true
|
|
1845
2012
|
};
|
|
1846
2013
|
}
|
|
1847
2014
|
static async waitForUICore({ type = 'ui', query, timeoutMs = 30000, pollIntervalMs = 300, includeSnapshotOnFailure = true, match = 'present', stability_ms = 700, observationDelayMs = 0, platform, deviceId }) {
|