react-native-ai-debugger 1.9.1 → 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/build/__tests__/integration/tap-strategies.test.js +1 -1
- package/build/__tests__/integration/tap-strategies.test.js.map +1 -1
- package/build/__tests__/unit/androidEmulatorAvds.test.d.ts +2 -0
- package/build/__tests__/unit/androidEmulatorAvds.test.d.ts.map +1 -0
- package/build/__tests__/unit/androidEmulatorAvds.test.js +98 -0
- package/build/__tests__/unit/androidEmulatorAvds.test.js.map +1 -0
- package/build/__tests__/unit/deviceDiscovery.test.d.ts +2 -0
- package/build/__tests__/unit/deviceDiscovery.test.d.ts.map +1 -0
- package/build/__tests__/unit/deviceDiscovery.test.js +119 -0
- package/build/__tests__/unit/deviceDiscovery.test.js.map +1 -0
- package/build/__tests__/unit/deviceResolver.test.d.ts +2 -0
- package/build/__tests__/unit/deviceResolver.test.d.ts.map +1 -0
- package/build/__tests__/unit/deviceResolver.test.js +226 -0
- package/build/__tests__/unit/deviceResolver.test.js.map +1 -0
- package/build/__tests__/unit/tap.test.js +23 -34
- package/build/__tests__/unit/tap.test.js.map +1 -1
- package/build/__tests__/unit/verifyAction.test.d.ts +2 -0
- package/build/__tests__/unit/verifyAction.test.d.ts.map +1 -0
- package/build/__tests__/unit/verifyAction.test.js +26 -0
- package/build/__tests__/unit/verifyAction.test.js.map +1 -0
- package/build/core/android.d.ts +20 -0
- package/build/core/android.d.ts.map +1 -1
- package/build/core/android.js +98 -0
- package/build/core/android.js.map +1 -1
- package/build/core/connection.d.ts.map +1 -1
- package/build/core/connection.js +116 -55
- package/build/core/connection.js.map +1 -1
- package/build/core/connectionState.d.ts +14 -0
- package/build/core/connectionState.d.ts.map +1 -1
- package/build/core/connectionState.js +29 -0
- package/build/core/connectionState.js.map +1 -1
- package/build/core/deviceDiscovery.d.ts +52 -0
- package/build/core/deviceDiscovery.d.ts.map +1 -0
- package/build/core/deviceDiscovery.js +74 -0
- package/build/core/deviceDiscovery.js.map +1 -0
- package/build/core/deviceResolver.d.ts +44 -0
- package/build/core/deviceResolver.d.ts.map +1 -0
- package/build/core/deviceResolver.js +215 -0
- package/build/core/deviceResolver.js.map +1 -0
- package/build/core/guides.d.ts.map +1 -1
- package/build/core/guides.js +3 -4
- package/build/core/guides.js.map +1 -1
- package/build/core/index.d.ts +1 -1
- package/build/core/index.d.ts.map +1 -1
- package/build/core/index.js +3 -1
- package/build/core/index.js.map +1 -1
- package/build/core/types.d.ts +1 -0
- package/build/core/types.d.ts.map +1 -1
- package/build/pro/tap.d.ts +5 -10
- package/build/pro/tap.d.ts.map +1 -1
- package/build/pro/tap.js +125 -403
- package/build/pro/tap.js.map +1 -1
- package/build/pro/verifyAction.d.ts +50 -0
- package/build/pro/verifyAction.d.ts.map +1 -0
- package/build/pro/verifyAction.js +283 -0
- package/build/pro/verifyAction.js.map +1 -0
- package/build/tools/componentTools.d.ts.map +1 -1
- package/build/tools/componentTools.js +8 -6
- package/build/tools/componentTools.js.map +1 -1
- package/build/tools/connectionTools.d.ts.map +1 -1
- package/build/tools/connectionTools.js +64 -5
- package/build/tools/connectionTools.js.map +1 -1
- package/build/tools/deviceTools.d.ts.map +1 -1
- package/build/tools/deviceTools.js +104 -2
- package/build/tools/deviceTools.js.map +1 -1
- package/build/tools/interactionTools.d.ts.map +1 -1
- package/build/tools/interactionTools.js +113 -151
- package/build/tools/interactionTools.js.map +1 -1
- package/build/tools/screenshotTools.d.ts.map +1 -1
- package/build/tools/screenshotTools.js +22 -6
- package/build/tools/screenshotTools.js.map +1 -1
- package/package.json +1 -1
package/build/pro/tap.js
CHANGED
|
@@ -1,29 +1,34 @@
|
|
|
1
|
-
import { connectedApps
|
|
1
|
+
import { connectedApps } from "../core/state.js";
|
|
2
2
|
import { executeInApp } from "../core/executor.js";
|
|
3
3
|
import { pressElement } from "../core/executor.js";
|
|
4
|
-
import { iosTap, iosFindElement, iosScreenshot,
|
|
5
|
-
import { androidTap, androidFindElement
|
|
4
|
+
import { iosTap, iosFindElement, iosScreenshot, isUiDriverAvailable, getUiDriverInstallHint, getIOSSafeAreaTop } from "../core/ios.js";
|
|
5
|
+
import { androidTap, androidFindElement } from "../core/android.js";
|
|
6
6
|
import { compareScreenshots } from "./screenshot-diff.js";
|
|
7
7
|
import { scanMetroPorts, fetchDevices, selectMainDevice } from "../core/metro.js";
|
|
8
8
|
import { connectToDevice, clearReconnectionSuppression, getConnectedAppByDevice } from "../core/connection.js";
|
|
9
|
+
import { resolveDeviceTarget, formatResolverError } from "../core/deviceResolver.js";
|
|
9
10
|
import { notifyDriverMissing } from "../core/logbox.js";
|
|
10
11
|
import { captureFailureArtifact } from "../core/failureArtifact.js";
|
|
12
|
+
import { captureScreenshot, verifyAndCapture, burstCaptureAndVerify, } from "./verifyAction.js";
|
|
11
13
|
export function buildVerificationExplanation(v) {
|
|
12
14
|
const pct = (rate) => (rate * 100).toFixed(1) + "%";
|
|
15
|
+
const action = v.action ?? "tap";
|
|
16
|
+
const Action = action[0].toUpperCase() + action.slice(1);
|
|
17
|
+
const target = action === "swipe" ? "scroll surface" : "element";
|
|
13
18
|
if (v.meaningful && !v.transientChangeDetected) {
|
|
14
|
-
return
|
|
19
|
+
return `${Action} caused a visible UI change (${pct(v.changeRate)} pixel diff). The screen updated as expected.`;
|
|
15
20
|
}
|
|
16
21
|
if (v.meaningful && v.transientChangeDetected) {
|
|
17
22
|
return (`No persistent change, but transient visual feedback detected ` +
|
|
18
23
|
`(frame ${v.peakFrame} peak ${pct(v.peakChangeRate || 0)} diff). ` +
|
|
19
|
-
|
|
24
|
+
`${Action} triggered a momentary animation that settled back to original state.`);
|
|
20
25
|
}
|
|
21
26
|
if (v.transientChangeDetected === false) {
|
|
22
27
|
return (`No visual change detected — neither persistent nor transient across burst frames. ` +
|
|
23
|
-
`The
|
|
28
|
+
`The ${target} may not respond visually or the ${action} may have missed.`);
|
|
24
29
|
}
|
|
25
30
|
return (`No visual change detected between before and after screenshots. ` +
|
|
26
|
-
`The
|
|
31
|
+
`The ${target} may not respond visually or the ${action} may have missed.`);
|
|
27
32
|
}
|
|
28
33
|
const BURST_CHANGE_THRESHOLD = 0.005;
|
|
29
34
|
export async function analyzeBurstFrames(frames, options) {
|
|
@@ -476,7 +481,7 @@ export function findClosestOcrText(ocrResult, query) {
|
|
|
476
481
|
return best;
|
|
477
482
|
}
|
|
478
483
|
// --- Strategy Functions ---
|
|
479
|
-
async function tryFiberStrategy(query, index, maxTraversalDepth, sink) {
|
|
484
|
+
async function tryFiberStrategy(query, index, maxTraversalDepth, sink, device) {
|
|
480
485
|
if (sink) {
|
|
481
486
|
sink.fiber.ran = true;
|
|
482
487
|
sink.fiber.metroConnected = connectedApps.size > 0;
|
|
@@ -495,7 +500,7 @@ async function tryFiberStrategy(query, index, maxTraversalDepth, sink) {
|
|
|
495
500
|
}
|
|
496
501
|
let lastResult = null;
|
|
497
502
|
for (const depth of depthAttempts) {
|
|
498
|
-
const result = await tryFiberAtDepth(query, index, depth);
|
|
503
|
+
const result = await tryFiberAtDepth(query, index, depth, device);
|
|
499
504
|
if (sink && result.matches?.length) {
|
|
500
505
|
sink.fiber.pressables = result.matches.slice(0, 50).map(m => ({
|
|
501
506
|
label: m.text || undefined,
|
|
@@ -518,14 +523,15 @@ async function tryFiberStrategy(query, index, maxTraversalDepth, sink) {
|
|
|
518
523
|
sink.fiber.durationMs = Date.now() - startedAt;
|
|
519
524
|
}
|
|
520
525
|
}
|
|
521
|
-
async function tryFiberAtDepth(query, index, maxTraversalDepth) {
|
|
526
|
+
async function tryFiberAtDepth(query, index, maxTraversalDepth, device) {
|
|
522
527
|
try {
|
|
523
528
|
const result = await pressElement({
|
|
524
529
|
text: query.text,
|
|
525
530
|
testID: query.testID,
|
|
526
531
|
component: query.component,
|
|
527
532
|
index,
|
|
528
|
-
maxTraversalDepth
|
|
533
|
+
maxTraversalDepth,
|
|
534
|
+
device
|
|
529
535
|
});
|
|
530
536
|
if (!result.success) {
|
|
531
537
|
return { success: false, reason: result.error || "pressElement failed" };
|
|
@@ -1008,7 +1014,6 @@ async function tryCoordinateStrategy(pixelX, pixelY, platform, lastScreenshot, u
|
|
|
1008
1014
|
};
|
|
1009
1015
|
}
|
|
1010
1016
|
}
|
|
1011
|
-
const SETTLE_DELAY_MS = 800;
|
|
1012
1017
|
const TAP_TIMEOUT_MS = 25000;
|
|
1013
1018
|
const MIN_STRATEGY_BUDGET_MS = 500;
|
|
1014
1019
|
// Per-strategy budget. OCR cap on Android is bumped via maxStrategyMs() because
|
|
@@ -1064,54 +1069,6 @@ function withCancelableTimeout(make, ms, label) {
|
|
|
1064
1069
|
throw err;
|
|
1065
1070
|
});
|
|
1066
1071
|
}
|
|
1067
|
-
async function captureScreenshot(platform, udid) {
|
|
1068
|
-
try {
|
|
1069
|
-
const result = platform === "ios" ? await iosScreenshot(undefined, udid) : await androidScreenshot();
|
|
1070
|
-
if (!result.success || !result.data)
|
|
1071
|
-
return null;
|
|
1072
|
-
return {
|
|
1073
|
-
buffer: result.data,
|
|
1074
|
-
width: result.originalWidth || 0,
|
|
1075
|
-
height: result.originalHeight || 0,
|
|
1076
|
-
scaleFactor: result.scaleFactor || 1
|
|
1077
|
-
};
|
|
1078
|
-
}
|
|
1079
|
-
catch {
|
|
1080
|
-
return null;
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
function screenshotToBase64(buffer) {
|
|
1084
|
-
return buffer.toString("base64");
|
|
1085
|
-
}
|
|
1086
|
-
/**
|
|
1087
|
-
* Composite a red crosshair marker onto a screenshot at the given pixel coordinates.
|
|
1088
|
-
* Uses sharp + SVG so we don't need to inject into the app. Coordinates are in the
|
|
1089
|
-
* screenshot's own pixel space (i.e. the space the returned image uses).
|
|
1090
|
-
*/
|
|
1091
|
-
async function drawTapMarker(input, x, y) {
|
|
1092
|
-
try {
|
|
1093
|
-
const sharp = (await import("sharp")).default;
|
|
1094
|
-
const size = 72;
|
|
1095
|
-
const half = size / 2;
|
|
1096
|
-
const svg = `<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
|
|
1097
|
-
<circle cx="${half}" cy="${half}" r="${half - 8}" fill="none" stroke="white" stroke-width="6" opacity="0.85"/>
|
|
1098
|
-
<circle cx="${half}" cy="${half}" r="${half - 8}" fill="none" stroke="#FF2D55" stroke-width="3" opacity="1"/>
|
|
1099
|
-
<line x1="${half}" y1="8" x2="${half}" y2="${size - 8}" stroke="white" stroke-width="6" opacity="0.85"/>
|
|
1100
|
-
<line x1="8" y1="${half}" x2="${size - 8}" y2="${half}" stroke="white" stroke-width="6" opacity="0.85"/>
|
|
1101
|
-
<line x1="${half}" y1="8" x2="${half}" y2="${size - 8}" stroke="#FF2D55" stroke-width="2.5"/>
|
|
1102
|
-
<line x1="8" y1="${half}" x2="${size - 8}" y2="${half}" stroke="#FF2D55" stroke-width="2.5"/>
|
|
1103
|
-
<circle cx="${half}" cy="${half}" r="3" fill="#FF2D55"/>
|
|
1104
|
-
</svg>`;
|
|
1105
|
-
const left = Math.round(x - half);
|
|
1106
|
-
const top = Math.round(y - half);
|
|
1107
|
-
return await sharp(input)
|
|
1108
|
-
.composite([{ input: Buffer.from(svg), left, top }])
|
|
1109
|
-
.toBuffer();
|
|
1110
|
-
}
|
|
1111
|
-
catch {
|
|
1112
|
-
return input;
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
1072
|
/**
|
|
1116
1073
|
* Compute marker coordinates in screenshot-pixel space (what the returned PNG uses).
|
|
1117
1074
|
* Unit rules by strategy, assuming screenshotScale = downscale factor:
|
|
@@ -1154,209 +1111,6 @@ function computeMarkerPx(args) {
|
|
|
1154
1111
|
y: Math.round(convertedTo.y / scale)
|
|
1155
1112
|
};
|
|
1156
1113
|
}
|
|
1157
|
-
async function verifyAndCapture(platform, shouldVerify, shouldScreenshot, beforeBuffer, udid, beforeScaleFactor, markerPx) {
|
|
1158
|
-
// Decoupled 2026-05-16 (I5): screenshot and verify are independent knobs.
|
|
1159
|
-
// - shouldVerify=true: capture after-screenshot internally, diff against beforeBuffer, return verification.
|
|
1160
|
-
// - shouldScreenshot=true: also return the (marker-overlaid) image bytes in the response.
|
|
1161
|
-
// - both false: fastest path, return only a `skipped` verification stub so the agent can tell apart "ran clean" from "skipped intentionally".
|
|
1162
|
-
if (!shouldVerify && !shouldScreenshot) {
|
|
1163
|
-
return {
|
|
1164
|
-
verification: {
|
|
1165
|
-
skipped: true,
|
|
1166
|
-
skippedReason: "screenshot=false, verify=false",
|
|
1167
|
-
explanation: "Verification skipped (screenshot=false and verify=false)."
|
|
1168
|
-
}
|
|
1169
|
-
};
|
|
1170
|
-
}
|
|
1171
|
-
await new Promise((resolve) => setTimeout(resolve, SETTLE_DELAY_MS));
|
|
1172
|
-
const after = await captureScreenshot(platform, udid);
|
|
1173
|
-
if (!after) {
|
|
1174
|
-
if (shouldVerify) {
|
|
1175
|
-
return {
|
|
1176
|
-
verification: {
|
|
1177
|
-
skipped: true,
|
|
1178
|
-
skippedReason: "after-screenshot capture failed",
|
|
1179
|
-
explanation: "Verification skipped — could not capture post-tap screenshot."
|
|
1180
|
-
}
|
|
1181
|
-
};
|
|
1182
|
-
}
|
|
1183
|
-
return {};
|
|
1184
|
-
}
|
|
1185
|
-
const afterWithMarker = markerPx
|
|
1186
|
-
? await drawTapMarker(after.buffer, markerPx.x, markerPx.y)
|
|
1187
|
-
: after.buffer;
|
|
1188
|
-
// Construct the screenshot eagerly. Caller decides whether to drop it
|
|
1189
|
-
// based on `shouldScreenshot` AND the verification outcome — when a tap
|
|
1190
|
-
// is unmeaningful we surface the image regardless so the agent can
|
|
1191
|
-
// diagnose without an extra round-trip.
|
|
1192
|
-
const screenshot = {
|
|
1193
|
-
image: screenshotToBase64(afterWithMarker),
|
|
1194
|
-
width: after.width,
|
|
1195
|
-
height: after.height,
|
|
1196
|
-
scaleFactor: after.scaleFactor
|
|
1197
|
-
};
|
|
1198
|
-
let verification;
|
|
1199
|
-
if (!shouldVerify) {
|
|
1200
|
-
verification = {
|
|
1201
|
-
skipped: true,
|
|
1202
|
-
skippedReason: "verify=false",
|
|
1203
|
-
explanation: "Verification skipped (verify=false)."
|
|
1204
|
-
};
|
|
1205
|
-
}
|
|
1206
|
-
else if (!beforeBuffer) {
|
|
1207
|
-
verification = {
|
|
1208
|
-
skipped: true,
|
|
1209
|
-
skippedReason: "before-screenshot unavailable",
|
|
1210
|
-
explanation: "Verification skipped — could not capture pre-tap screenshot."
|
|
1211
|
-
};
|
|
1212
|
-
}
|
|
1213
|
-
else {
|
|
1214
|
-
try {
|
|
1215
|
-
const rawStatusBar = platform === "ios" ? 177 : 142; // pixels in original screenshot space
|
|
1216
|
-
const scale = beforeScaleFactor || after.scaleFactor || 1;
|
|
1217
|
-
const statusBarHeight = Math.round(rawStatusBar / scale);
|
|
1218
|
-
const diff = await compareScreenshots(beforeBuffer, after.buffer, {
|
|
1219
|
-
statusBarHeight
|
|
1220
|
-
});
|
|
1221
|
-
verification = {
|
|
1222
|
-
meaningful: diff.changed,
|
|
1223
|
-
changeRate: diff.changeRate,
|
|
1224
|
-
changedPixels: diff.changedPixels,
|
|
1225
|
-
totalPixels: diff.totalPixels,
|
|
1226
|
-
explanation: buildVerificationExplanation({
|
|
1227
|
-
meaningful: diff.changed,
|
|
1228
|
-
changeRate: diff.changeRate,
|
|
1229
|
-
changedPixels: diff.changedPixels,
|
|
1230
|
-
totalPixels: diff.totalPixels
|
|
1231
|
-
})
|
|
1232
|
-
};
|
|
1233
|
-
}
|
|
1234
|
-
catch (err) {
|
|
1235
|
-
verification = {
|
|
1236
|
-
skipped: true,
|
|
1237
|
-
skippedReason: `diff failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1238
|
-
explanation: "Verification skipped — pixel diff threw."
|
|
1239
|
-
};
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
const verifyGroupId = `verify-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1243
|
-
if (beforeBuffer) {
|
|
1244
|
-
imageBuffer.add({
|
|
1245
|
-
id: `${verifyGroupId}-before`,
|
|
1246
|
-
image: beforeBuffer,
|
|
1247
|
-
timestamp: Date.now(),
|
|
1248
|
-
source: "tap-verify",
|
|
1249
|
-
groupId: verifyGroupId,
|
|
1250
|
-
metadata: { phase: "before" }
|
|
1251
|
-
});
|
|
1252
|
-
}
|
|
1253
|
-
imageBuffer.add({
|
|
1254
|
-
id: `${verifyGroupId}-after`,
|
|
1255
|
-
image: afterWithMarker,
|
|
1256
|
-
timestamp: Date.now(),
|
|
1257
|
-
source: "tap-verify",
|
|
1258
|
-
groupId: verifyGroupId,
|
|
1259
|
-
metadata: { phase: "after", changeRate: verification?.changeRate }
|
|
1260
|
-
});
|
|
1261
|
-
return { screenshot, verification, afterWithMarkerBuffer: afterWithMarker };
|
|
1262
|
-
}
|
|
1263
|
-
// --- Burst Capture ---
|
|
1264
|
-
const BURST_FRAME_COUNT = 4;
|
|
1265
|
-
const BURST_FRAME_INTERVAL_MS = 150;
|
|
1266
|
-
async function burstCaptureAndVerify(platform, beforeBuffer, udid, beforeScaleFactor, markerPx) {
|
|
1267
|
-
if (!beforeBuffer)
|
|
1268
|
-
return {};
|
|
1269
|
-
const frames = [beforeBuffer];
|
|
1270
|
-
let capturedScaleFactor = beforeScaleFactor || 1;
|
|
1271
|
-
for (let i = 0; i < BURST_FRAME_COUNT; i++) {
|
|
1272
|
-
await new Promise((resolve) => setTimeout(resolve, BURST_FRAME_INTERVAL_MS));
|
|
1273
|
-
const capture = await captureScreenshot(platform, udid);
|
|
1274
|
-
if (capture) {
|
|
1275
|
-
frames.push(capture.buffer);
|
|
1276
|
-
if (i === 0)
|
|
1277
|
-
capturedScaleFactor = capture.scaleFactor || capturedScaleFactor;
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
if (frames.length < 2)
|
|
1281
|
-
return {};
|
|
1282
|
-
const rawStatusBar = platform === "ios" ? 177 : 142; // pixels in original screenshot space
|
|
1283
|
-
const statusBarHeight = Math.round(rawStatusBar / capturedScaleFactor);
|
|
1284
|
-
// Run the diff on clean frames (no marker) so the cross doesn't register as change.
|
|
1285
|
-
const analysis = await analyzeBurstFrames(frames, { statusBarHeight });
|
|
1286
|
-
// Apply marker to post-tap frames only (frames[0] is the before state).
|
|
1287
|
-
const markedFrames = [];
|
|
1288
|
-
for (let i = 0; i < frames.length; i++) {
|
|
1289
|
-
if (markerPx && i > 0) {
|
|
1290
|
-
markedFrames.push(await drawTapMarker(frames[i], markerPx.x, markerPx.y));
|
|
1291
|
-
}
|
|
1292
|
-
else {
|
|
1293
|
-
markedFrames.push(frames[i]);
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
const groupId = `burst-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1297
|
-
for (let i = 0; i < markedFrames.length; i++) {
|
|
1298
|
-
imageBuffer.add({
|
|
1299
|
-
id: `${groupId}-f${i}`,
|
|
1300
|
-
image: markedFrames[i],
|
|
1301
|
-
timestamp: Date.now(),
|
|
1302
|
-
source: "tap-burst",
|
|
1303
|
-
groupId,
|
|
1304
|
-
metadata: {
|
|
1305
|
-
frameIndex: i,
|
|
1306
|
-
isBefore: i === 0,
|
|
1307
|
-
changeRate: i === 0 ? 0 : analysis.framesWithChange.includes(i) ? analysis.peakChangeRate : 0
|
|
1308
|
-
}
|
|
1309
|
-
});
|
|
1310
|
-
}
|
|
1311
|
-
imageBuffer.addGroup({
|
|
1312
|
-
groupId,
|
|
1313
|
-
intent: "tap-verification",
|
|
1314
|
-
source: "tap-burst",
|
|
1315
|
-
timestamp: Date.now(),
|
|
1316
|
-
frameCount: frames.length,
|
|
1317
|
-
summary: {
|
|
1318
|
-
peakChangeRate: analysis.peakChangeRate,
|
|
1319
|
-
peakFrame: analysis.peakFrame,
|
|
1320
|
-
framesWithChange: analysis.framesWithChange,
|
|
1321
|
-
transientChangeDetected: analysis.transientChangeDetected,
|
|
1322
|
-
persistentChangeRate: analysis.persistentChangeRate
|
|
1323
|
-
}
|
|
1324
|
-
});
|
|
1325
|
-
// Get dimensions from the last frame using sharp
|
|
1326
|
-
const sharp = (await import("sharp")).default;
|
|
1327
|
-
const meta = await sharp(markedFrames[markedFrames.length - 1]).metadata();
|
|
1328
|
-
const screenshot = {
|
|
1329
|
-
image: screenshotToBase64(markedFrames[markedFrames.length - 1]),
|
|
1330
|
-
width: meta.width || 0,
|
|
1331
|
-
height: meta.height || 0,
|
|
1332
|
-
scaleFactor: 1
|
|
1333
|
-
};
|
|
1334
|
-
const verification = {
|
|
1335
|
-
meaningful: analysis.meaningful,
|
|
1336
|
-
changeRate: analysis.persistentChangeRate,
|
|
1337
|
-
changedPixels: 0,
|
|
1338
|
-
totalPixels: 0,
|
|
1339
|
-
transientChangeDetected: analysis.transientChangeDetected,
|
|
1340
|
-
peakChangeRate: analysis.peakChangeRate,
|
|
1341
|
-
peakFrame: analysis.peakFrame,
|
|
1342
|
-
burstGroupId: groupId,
|
|
1343
|
-
explanation: buildVerificationExplanation({
|
|
1344
|
-
meaningful: analysis.meaningful,
|
|
1345
|
-
changeRate: analysis.persistentChangeRate,
|
|
1346
|
-
changedPixels: 0,
|
|
1347
|
-
totalPixels: 0,
|
|
1348
|
-
transientChangeDetected: analysis.transientChangeDetected,
|
|
1349
|
-
peakChangeRate: analysis.peakChangeRate,
|
|
1350
|
-
peakFrame: analysis.peakFrame
|
|
1351
|
-
})
|
|
1352
|
-
};
|
|
1353
|
-
const lastFrameIdx = frames.length - 1;
|
|
1354
|
-
return {
|
|
1355
|
-
screenshot,
|
|
1356
|
-
verification,
|
|
1357
|
-
afterWithMarkerBuffer: markedFrames[lastFrameIdx]
|
|
1358
|
-
};
|
|
1359
|
-
}
|
|
1360
1114
|
// D2 (Step 6): in-memory ring buffer of recent coord-strategy artifact keys
|
|
1361
1115
|
// `${round(x/20)}:${round(y/20)}`. Entries expire after 60s. The first
|
|
1362
1116
|
// failed coord tap at a grid cell uploads its artifact; retries within the
|
|
@@ -1554,67 +1308,16 @@ export async function tap(options) {
|
|
|
1554
1308
|
}
|
|
1555
1309
|
// Native mode: bypass React Native connection, tap directly via ADB/simctl
|
|
1556
1310
|
if (options.native && hasCoordinates) {
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
// surface a clear error rather than silently overriding.
|
|
1560
|
-
if (options.udid && platform === "android") {
|
|
1311
|
+
const nativeResolved = await resolveDeviceTarget(options.device);
|
|
1312
|
+
if (!nativeResolved.ok) {
|
|
1561
1313
|
return {
|
|
1562
1314
|
success: false,
|
|
1563
1315
|
query,
|
|
1564
|
-
error:
|
|
1316
|
+
error: formatResolverError(nativeResolved.error)
|
|
1565
1317
|
};
|
|
1566
1318
|
}
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
if (options.udid) {
|
|
1570
|
-
platform = "ios";
|
|
1571
|
-
nativeUdid = options.udid;
|
|
1572
|
-
}
|
|
1573
|
-
else if (!platform) {
|
|
1574
|
-
// If `device` is provided, try resolving it as an iOS simulator name first —
|
|
1575
|
-
// otherwise the auto-detect would fall through to getActiveOrBootedSimulatorUdid()
|
|
1576
|
-
// and ignore the caller's targeting hint when multiple simulators are booted.
|
|
1577
|
-
const deviceMatchedUdid = options.device
|
|
1578
|
-
? await findSimulatorByName(options.device).catch(() => null)
|
|
1579
|
-
: null;
|
|
1580
|
-
if (deviceMatchedUdid) {
|
|
1581
|
-
platform = "ios";
|
|
1582
|
-
nativeUdid = deviceMatchedUdid;
|
|
1583
|
-
}
|
|
1584
|
-
else {
|
|
1585
|
-
const [androidDevice, iosSimulator] = await Promise.all([
|
|
1586
|
-
getDefaultAndroidDevice().catch(() => null),
|
|
1587
|
-
getActiveOrBootedSimulatorUdid().catch(() => null)
|
|
1588
|
-
]);
|
|
1589
|
-
if (androidDevice && iosSimulator) {
|
|
1590
|
-
return {
|
|
1591
|
-
success: false,
|
|
1592
|
-
query,
|
|
1593
|
-
error: 'Multiple platforms detected (both Android and iOS). Specify platform: "android" or platform: "ios" to target the correct device.'
|
|
1594
|
-
};
|
|
1595
|
-
}
|
|
1596
|
-
if (androidDevice) {
|
|
1597
|
-
platform = "android";
|
|
1598
|
-
}
|
|
1599
|
-
else if (iosSimulator) {
|
|
1600
|
-
platform = "ios";
|
|
1601
|
-
nativeUdid = iosSimulator;
|
|
1602
|
-
}
|
|
1603
|
-
else {
|
|
1604
|
-
return {
|
|
1605
|
-
success: false,
|
|
1606
|
-
query,
|
|
1607
|
-
error: "No Android device or iOS simulator found. Connect a device or start a simulator."
|
|
1608
|
-
};
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
else if (platform === "ios") {
|
|
1613
|
-
nativeUdid =
|
|
1614
|
-
(options.device ? await findSimulatorByName(options.device) : null) ??
|
|
1615
|
-
(await getActiveOrBootedSimulatorUdid()) ??
|
|
1616
|
-
undefined;
|
|
1617
|
-
}
|
|
1319
|
+
const platform = nativeResolved.target.platform;
|
|
1320
|
+
const nativeUdid = nativeResolved.target.iosUdid;
|
|
1618
1321
|
const nativeShouldScreenshot = options.screenshot !== false;
|
|
1619
1322
|
// Decoupled (I5, 2026-05-16): verify runs even when image bytes aren't returned.
|
|
1620
1323
|
const nativeShouldVerify = options.verify !== false;
|
|
@@ -1672,12 +1375,26 @@ export async function tap(options) {
|
|
|
1672
1375
|
screenshotScale: nativeScreenshotMeta?.scaleFactor || 1
|
|
1673
1376
|
});
|
|
1674
1377
|
if (options.burst && nativeShouldVerify && nativeBeforeBuffer) {
|
|
1675
|
-
({ screenshot, verification } = await burstCaptureAndVerify(
|
|
1378
|
+
({ screenshot, verification } = await burstCaptureAndVerify({
|
|
1379
|
+
platform,
|
|
1380
|
+
beforeBuffer: nativeBeforeBuffer,
|
|
1381
|
+
udid: nativeUdid,
|
|
1382
|
+
beforeScaleFactor: nativeScreenshotMeta?.scaleFactor,
|
|
1383
|
+
markerPx: nativeMarker
|
|
1384
|
+
}));
|
|
1676
1385
|
if (!nativeShouldScreenshot)
|
|
1677
1386
|
screenshot = undefined;
|
|
1678
1387
|
}
|
|
1679
1388
|
else {
|
|
1680
|
-
({ screenshot, verification } = await verifyAndCapture(
|
|
1389
|
+
({ screenshot, verification } = await verifyAndCapture({
|
|
1390
|
+
platform,
|
|
1391
|
+
shouldVerify: nativeShouldVerify,
|
|
1392
|
+
shouldScreenshot: nativeShouldScreenshot,
|
|
1393
|
+
beforeBuffer: nativeBeforeBuffer,
|
|
1394
|
+
udid: nativeUdid,
|
|
1395
|
+
beforeScaleFactor: nativeScreenshotMeta?.scaleFactor,
|
|
1396
|
+
markerPx: nativeMarker
|
|
1397
|
+
}));
|
|
1681
1398
|
}
|
|
1682
1399
|
return formatTapSuccess({
|
|
1683
1400
|
method: "native-coordinate",
|
|
@@ -1695,46 +1412,48 @@ export async function tap(options) {
|
|
|
1695
1412
|
suggestion: `Take a screenshot (${platform === "ios" ? "ios_screenshot" : "android_screenshot"}) to verify coordinates.`
|
|
1696
1413
|
});
|
|
1697
1414
|
}
|
|
1698
|
-
//
|
|
1699
|
-
|
|
1415
|
+
// Resolve device target via the unified resolver. Replaces the prior chain
|
|
1416
|
+
// of (udid → connectedApp → findSimulatorByName) lookups and the standalone
|
|
1417
|
+
// "no platform → probe OS for devices" branch. Returns a structured error
|
|
1418
|
+
// when ambiguous; surface that to the caller so they can disambiguate.
|
|
1419
|
+
const resolved = await resolveDeviceTarget(options.device);
|
|
1420
|
+
if (!resolved.ok) {
|
|
1700
1421
|
return {
|
|
1701
1422
|
success: false,
|
|
1702
1423
|
query,
|
|
1703
|
-
error:
|
|
1424
|
+
error: formatResolverError(resolved.error)
|
|
1704
1425
|
};
|
|
1705
1426
|
}
|
|
1706
|
-
|
|
1427
|
+
const platform = resolved.target.platform;
|
|
1428
|
+
let targetUdid = resolved.target.iosUdid;
|
|
1429
|
+
// Pick the connected app to bias strategy selection. Prefer the registry
|
|
1430
|
+
// entry whose identifier matches the resolved target; fall back to a
|
|
1431
|
+
// device-name match (handy when registry entries lack identifiers yet);
|
|
1432
|
+
// last resort is the first connected app on the matching platform.
|
|
1707
1433
|
const allApps = Array.from(connectedApps.values());
|
|
1708
1434
|
let hasMetro = allApps.length > 0;
|
|
1709
|
-
// Pick the connected app to bias platform/strategy selection.
|
|
1710
|
-
// Precedence:
|
|
1711
|
-
// 1. udid → match the iOS app whose simulatorUdid equals udid (if any)
|
|
1712
|
-
// 2. device → substring match against connected app's deviceName/title
|
|
1713
|
-
// 3. platform → first app on that platform
|
|
1714
|
-
// 4. first connected app
|
|
1715
1435
|
let app;
|
|
1716
|
-
if (
|
|
1717
|
-
app = allApps.find((a) => a.platform === "ios" && a.simulatorUdid ===
|
|
1436
|
+
if (resolved.target.iosUdid) {
|
|
1437
|
+
app = allApps.find((a) => a.platform === "ios" && a.simulatorUdid === resolved.target.iosUdid);
|
|
1438
|
+
}
|
|
1439
|
+
else if (resolved.target.androidSerial) {
|
|
1440
|
+
app = allApps.find((a) => a.platform === "android" && a.adbSerial === resolved.target.androidSerial);
|
|
1718
1441
|
}
|
|
1719
1442
|
if (!app && options.device) {
|
|
1720
1443
|
try {
|
|
1721
1444
|
app = getConnectedAppByDevice(options.device) ?? undefined;
|
|
1722
1445
|
}
|
|
1723
|
-
catch
|
|
1724
|
-
// getConnectedAppByDevice throws on ambiguous matches
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1729
|
-
};
|
|
1446
|
+
catch {
|
|
1447
|
+
// getConnectedAppByDevice throws on ambiguous matches; the resolver
|
|
1448
|
+
// would have already returned that error, so reaching here means
|
|
1449
|
+
// the resolved target's identifier just doesn't appear in the
|
|
1450
|
+
// registry yet — fall through to the platform-first pick below.
|
|
1730
1451
|
}
|
|
1731
1452
|
}
|
|
1732
1453
|
if (!app) {
|
|
1733
|
-
app =
|
|
1734
|
-
? (allApps.find((a) => a.platform === options.platform) ?? allApps[0])
|
|
1735
|
-
: allApps[0];
|
|
1454
|
+
app = allApps.find((a) => a.platform === platform) ?? allApps[0];
|
|
1736
1455
|
}
|
|
1737
|
-
// Try to auto-connect to Metro (for fiber strategy), but don't fail if it doesn't work
|
|
1456
|
+
// Try to auto-connect to Metro (for fiber strategy), but don't fail if it doesn't work.
|
|
1738
1457
|
if (!hasMetro) {
|
|
1739
1458
|
try {
|
|
1740
1459
|
await withTimeout((async () => {
|
|
@@ -1751,57 +1470,21 @@ export async function tap(options) {
|
|
|
1751
1470
|
})(), Math.min(remainingMs(), 3000), "auto-connect");
|
|
1752
1471
|
const apps = Array.from(connectedApps.values());
|
|
1753
1472
|
hasMetro = apps.length > 0;
|
|
1754
|
-
app = apps[0];
|
|
1473
|
+
app = apps.find((a) => a.platform === platform) ?? apps[0];
|
|
1474
|
+
if (platform === "ios" && !targetUdid) {
|
|
1475
|
+
targetUdid = app?.simulatorUdid ?? targetUdid;
|
|
1476
|
+
}
|
|
1755
1477
|
}
|
|
1756
1478
|
catch {
|
|
1757
1479
|
// Auto-connect failed — Metro-dependent strategies will be skipped
|
|
1758
1480
|
}
|
|
1759
1481
|
}
|
|
1760
|
-
//
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
else {
|
|
1767
|
-
// No Metro — detect from available devices
|
|
1768
|
-
const [androidDevice, iosSimulator] = await Promise.all([
|
|
1769
|
-
getDefaultAndroidDevice().catch(() => null),
|
|
1770
|
-
getActiveOrBootedSimulatorUdid().catch(() => null)
|
|
1771
|
-
]);
|
|
1772
|
-
if (androidDevice && iosSimulator) {
|
|
1773
|
-
return {
|
|
1774
|
-
success: false,
|
|
1775
|
-
query,
|
|
1776
|
-
error: 'Both iOS and Android devices detected but no Metro connection. Specify platform="ios" or platform="android".'
|
|
1777
|
-
};
|
|
1778
|
-
}
|
|
1779
|
-
if (androidDevice)
|
|
1780
|
-
platform = "android";
|
|
1781
|
-
else if (iosSimulator)
|
|
1782
|
-
platform = "ios";
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
if (!platform) {
|
|
1786
|
-
return {
|
|
1787
|
-
success: false,
|
|
1788
|
-
query,
|
|
1789
|
-
error: "No devices found. Boot a simulator or connect an Android device."
|
|
1790
|
-
};
|
|
1791
|
-
}
|
|
1792
|
-
// Resolve target iOS simulator UDID.
|
|
1793
|
-
// Precedence: explicit udid → connected app's simulatorUdid → findSimulatorByName(device or app deviceName)
|
|
1794
|
-
// This ensures taps go to the correct device when multiple simulators are booted.
|
|
1795
|
-
let targetUdid;
|
|
1796
|
-
if (platform === "ios") {
|
|
1797
|
-
targetUdid =
|
|
1798
|
-
options.udid ??
|
|
1799
|
-
app?.simulatorUdid ??
|
|
1800
|
-
(options.device ? await findSimulatorByName(options.device) : null) ??
|
|
1801
|
-
(app?.deviceInfo?.deviceName ? await findSimulatorByName(app.deviceInfo.deviceName) : null) ??
|
|
1802
|
-
undefined;
|
|
1803
|
-
}
|
|
1804
|
-
const deviceName = app?.deviceInfo?.deviceName;
|
|
1482
|
+
// The response's `device` label must reflect the RESOLVED target, not the
|
|
1483
|
+
// registry-app fallback. When the registry lookup couldn't find a matching
|
|
1484
|
+
// app (e.g. simulatorUdid hadn't been backfilled yet), `app` lands on the
|
|
1485
|
+
// first iOS entry — which on a multi-sim setup is the WRONG device. The
|
|
1486
|
+
// resolver already knows the right deviceName; trust it. Bug #5 (2026-05-20).
|
|
1487
|
+
const deviceName = resolved.target.deviceName || app?.deviceInfo?.deviceName;
|
|
1805
1488
|
// Determine strategies
|
|
1806
1489
|
const strategies = getAvailableStrategies(query, strategy);
|
|
1807
1490
|
const attempted = [];
|
|
@@ -1898,7 +1581,7 @@ export async function tap(options) {
|
|
|
1898
1581
|
case "fiber":
|
|
1899
1582
|
// Fiber is JS-only against a CDP target — no subprocess to cancel,
|
|
1900
1583
|
// so the cheaper non-cancellable wrapper is fine.
|
|
1901
|
-
result = await withTimeout(tryFiberStrategy(query, index, maxTraversalDepth, evidence), budget, `fiber`);
|
|
1584
|
+
result = await withTimeout(tryFiberStrategy(query, index, maxTraversalDepth, evidence, deviceName), budget, `fiber`);
|
|
1902
1585
|
break;
|
|
1903
1586
|
case "accessibility":
|
|
1904
1587
|
result = await withCancelableTimeout((signal) => tryAccessibilityStrategy(query, index, platform, targetUdid, evidence, signal), budget, `accessibility`);
|
|
@@ -1935,7 +1618,12 @@ export async function tap(options) {
|
|
|
1935
1618
|
}
|
|
1936
1619
|
break;
|
|
1937
1620
|
case "coordinate":
|
|
1938
|
-
|
|
1621
|
+
// Prefer `beforeScaleFactor` (captured against `targetUdid` this turn)
|
|
1622
|
+
// over `app.lastScreenshot.scaleFactor` (stale and may belong to a
|
|
1623
|
+
// different device on a multi-sim setup). Bug #5 (2026-05-20).
|
|
1624
|
+
result = await withTimeout(tryCoordinateStrategy(query.x, query.y, platform, beforeScaleFactor != null
|
|
1625
|
+
? { originalWidth: 0, originalHeight: 0, scaleFactor: beforeScaleFactor }
|
|
1626
|
+
: app?.lastScreenshot, targetUdid), budget, `coordinate`);
|
|
1939
1627
|
break;
|
|
1940
1628
|
default:
|
|
1941
1629
|
result = { success: false, reason: `Unknown strategy: ${strat}` };
|
|
@@ -1995,7 +1683,13 @@ export async function tap(options) {
|
|
|
1995
1683
|
devicePixelRatio: dprForMarker
|
|
1996
1684
|
});
|
|
1997
1685
|
if (options.burst && canVerify && beforeBuffer) {
|
|
1998
|
-
({ screenshot, verification, afterWithMarkerBuffer } = await burstCaptureAndVerify(
|
|
1686
|
+
({ screenshot, verification, afterWithMarkerBuffer } = await burstCaptureAndVerify({
|
|
1687
|
+
platform,
|
|
1688
|
+
beforeBuffer,
|
|
1689
|
+
udid: targetUdid,
|
|
1690
|
+
beforeScaleFactor,
|
|
1691
|
+
markerPx: strategyMarker
|
|
1692
|
+
}));
|
|
1999
1693
|
// Burst always produces a screenshot for diff visualization. Drop the
|
|
2000
1694
|
// bytes when the caller didn't ask for them AND the tap was meaningful
|
|
2001
1695
|
// — keeping the screenshot on unmeaningful taps saves the agent a
|
|
@@ -2005,8 +1699,15 @@ export async function tap(options) {
|
|
|
2005
1699
|
screenshot = undefined;
|
|
2006
1700
|
}
|
|
2007
1701
|
else {
|
|
2008
|
-
({ screenshot, verification, afterWithMarkerBuffer } = await verifyAndCapture(
|
|
2009
|
-
|
|
1702
|
+
({ screenshot, verification, afterWithMarkerBuffer } = await verifyAndCapture({
|
|
1703
|
+
platform,
|
|
1704
|
+
shouldVerify: canVerify,
|
|
1705
|
+
shouldScreenshot: true,
|
|
1706
|
+
beforeBuffer,
|
|
1707
|
+
udid: targetUdid,
|
|
1708
|
+
beforeScaleFactor,
|
|
1709
|
+
markerPx: strategyMarker
|
|
1710
|
+
}));
|
|
2010
1711
|
// Mirror the burst-path gate: drop the screenshot only when the
|
|
2011
1712
|
// caller didn't ask for it AND the tap was meaningful.
|
|
2012
1713
|
if (!shouldScreenshot && verification?.meaningful !== false)
|
|
@@ -2088,7 +1789,15 @@ export async function tap(options) {
|
|
|
2088
1789
|
const { androidGetDensity } = await import("../core/android.js");
|
|
2089
1790
|
const densityResult = await androidGetDensity();
|
|
2090
1791
|
const densityScale = (densityResult.density || 420) / 160;
|
|
2091
|
-
|
|
1792
|
+
const pxX = Math.round(coords.x * densityScale);
|
|
1793
|
+
const pxY = Math.round(coords.y * densityScale);
|
|
1794
|
+
await androidTap(pxX, pxY);
|
|
1795
|
+
// Report the actual tap coords (pixels) the caller can pass straight
|
|
1796
|
+
// to coordinate tools / verification — not the raw dp from fiber.
|
|
1797
|
+
// Matches every other Android coord report in the tool (OB1, 2026-05-20).
|
|
1798
|
+
coords.x = pxX;
|
|
1799
|
+
coords.y = pxY;
|
|
1800
|
+
coords.unit = "pixels";
|
|
2092
1801
|
}
|
|
2093
1802
|
// fiber+native uses native tap — always verify
|
|
2094
1803
|
let screenshot;
|
|
@@ -2123,11 +1832,24 @@ export async function tap(options) {
|
|
|
2123
1832
|
androidDensityScale: fnDensity
|
|
2124
1833
|
});
|
|
2125
1834
|
if (options.burst && canVerify && beforeBuffer) {
|
|
2126
|
-
({ screenshot, verification } = await burstCaptureAndVerify(
|
|
1835
|
+
({ screenshot, verification } = await burstCaptureAndVerify({
|
|
1836
|
+
platform,
|
|
1837
|
+
beforeBuffer,
|
|
1838
|
+
udid: targetUdid,
|
|
1839
|
+
beforeScaleFactor,
|
|
1840
|
+
markerPx: fiberMarker
|
|
1841
|
+
}));
|
|
2127
1842
|
}
|
|
2128
1843
|
else {
|
|
2129
|
-
({ screenshot, verification } = await verifyAndCapture(
|
|
2130
|
-
|
|
1844
|
+
({ screenshot, verification } = await verifyAndCapture({
|
|
1845
|
+
platform,
|
|
1846
|
+
shouldVerify: canVerify,
|
|
1847
|
+
shouldScreenshot: true,
|
|
1848
|
+
beforeBuffer,
|
|
1849
|
+
udid: targetUdid,
|
|
1850
|
+
beforeScaleFactor,
|
|
1851
|
+
markerPx: fiberMarker
|
|
1852
|
+
}));
|
|
2131
1853
|
}
|
|
2132
1854
|
if (!shouldScreenshot && verification?.meaningful !== false)
|
|
2133
1855
|
screenshot = undefined;
|
|
@@ -2161,7 +1883,7 @@ export async function tap(options) {
|
|
|
2161
1883
|
// Do NOT fall through to other strategies (they can't resolve ambiguity).
|
|
2162
1884
|
if (result.matches && result.ambiguous) {
|
|
2163
1885
|
const { screenshot: matchScreenshot } = shouldScreenshot
|
|
2164
|
-
? await verifyAndCapture(platform, false, true, null, targetUdid)
|
|
1886
|
+
? await verifyAndCapture({ platform, shouldVerify: false, shouldScreenshot: true, beforeBuffer: null, udid: targetUdid })
|
|
2165
1887
|
: { screenshot: undefined };
|
|
2166
1888
|
if (matchScreenshot && app) {
|
|
2167
1889
|
app.lastScreenshot = {
|
|
@@ -2195,7 +1917,7 @@ export async function tap(options) {
|
|
|
2195
1917
|
const allTimedOut = attempted.length > 0 && attempted.every(a => a.outcome === "timeout" || a.outcome === "skipped");
|
|
2196
1918
|
let suggestion = buildSuggestion(query, strategies, platform);
|
|
2197
1919
|
if (fiberSawInvisible) {
|
|
2198
|
-
suggestion = `Element exists in the React tree but is not on screen. Try scrolling it into view (
|
|
1920
|
+
suggestion = `Element exists in the React tree but is not on screen. Try scrolling it into view with swipe(), dismiss any overlay covering it, or wait for layout to settle. ` + suggestion;
|
|
2199
1921
|
}
|
|
2200
1922
|
else if (allTimedOut) {
|
|
2201
1923
|
// No strategy got to a definitive answer — don't let the agent conclude
|
|
@@ -2203,7 +1925,7 @@ export async function tap(options) {
|
|
|
2203
1925
|
suggestion = `All strategies timed out — the element's presence is UNKNOWN. Retry the tap (transient slowness is common on dense screens), or try a different strategy explicitly (e.g. strategy='fiber' if accessibility timed out). ` + suggestion;
|
|
2204
1926
|
}
|
|
2205
1927
|
const { screenshot: failScreenshot } = shouldScreenshot
|
|
2206
|
-
? await verifyAndCapture(platform, false, true, null, targetUdid)
|
|
1928
|
+
? await verifyAndCapture({ platform, shouldVerify: false, shouldScreenshot: true, beforeBuffer: null, udid: targetUdid })
|
|
2207
1929
|
: { screenshot: undefined };
|
|
2208
1930
|
if (failScreenshot && app) {
|
|
2209
1931
|
app.lastScreenshot = {
|
|
@@ -2279,7 +2001,7 @@ function buildSuggestion(query, triedStrategies, platform) {
|
|
|
2279
2001
|
// the error) keeps the existing message stable for tooling that parses it.
|
|
2280
2002
|
if (query.testID && looksLikeVirtualizedListItem(query.testID)) {
|
|
2281
2003
|
suggestions.push("testID looks like a virtualized list item — scroll it on-screen first " +
|
|
2282
|
-
`(${platform === "ios" ? "ios_button arrow keys
|
|
2004
|
+
`(swipe()${platform === "ios" ? " or ios_button arrow keys" : ""}) before tapping`);
|
|
2283
2005
|
}
|
|
2284
2006
|
suggestions.push(`Take a screenshot (${platform === "ios" ? "ios_screenshot" : "android_screenshot"}) ` +
|
|
2285
2007
|
"to verify the element is visible, then use x/y coordinates");
|