react-native-ai-debugger 1.9.0 → 1.9.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/build/__tests__/integration/execute-in-app.test.js +11 -3
- package/build/__tests__/integration/execute-in-app.test.js.map +1 -1
- package/build/__tests__/unit/failureArtifact.test.js +3 -3
- package/build/__tests__/unit/failureArtifact.test.js.map +1 -1
- package/build/core/failureArtifact.d.ts +0 -2
- package/build/core/failureArtifact.d.ts.map +1 -1
- package/build/core/failureArtifact.js +0 -4
- package/build/core/failureArtifact.js.map +1 -1
- package/build/core/pressables.d.ts.map +1 -1
- package/build/core/pressables.js +43 -8
- package/build/core/pressables.js.map +1 -1
- package/build/core/rnGlobalsBootstrap.d.ts.map +1 -1
- package/build/core/rnGlobalsBootstrap.js +16 -1
- package/build/core/rnGlobalsBootstrap.js.map +1 -1
- package/build/pro/screenshot-diff.js +1 -1
- package/build/pro/screenshot-diff.js.map +1 -1
- package/build/pro/tap.d.ts +3 -0
- package/build/pro/tap.d.ts.map +1 -1
- package/build/pro/tap.js +97 -35
- package/build/pro/tap.js.map +1 -1
- package/build/tools/componentTools.d.ts.map +1 -1
- package/build/tools/componentTools.js +14 -18
- package/build/tools/componentTools.js.map +1 -1
- package/build/tools/connectionTools.d.ts.map +1 -1
- package/build/tools/connectionTools.js +5 -3
- package/build/tools/connectionTools.js.map +1 -1
- package/build/tools/executionTools.d.ts.map +1 -1
- package/build/tools/executionTools.js +7 -9
- package/build/tools/executionTools.js.map +1 -1
- package/package.json +1 -1
package/build/pro/tap.js
CHANGED
|
@@ -1009,20 +1009,24 @@ async function tryCoordinateStrategy(pixelX, pixelY, platform, lastScreenshot, u
|
|
|
1009
1009
|
}
|
|
1010
1010
|
}
|
|
1011
1011
|
const SETTLE_DELAY_MS = 800;
|
|
1012
|
-
const TAP_TIMEOUT_MS =
|
|
1012
|
+
const TAP_TIMEOUT_MS = 25000;
|
|
1013
1013
|
const MIN_STRATEGY_BUDGET_MS = 500;
|
|
1014
1014
|
// Per-strategy budget. OCR cap on Android is bumped via maxStrategyMs() because
|
|
1015
1015
|
// the ADB screencap+pull leg has ~2s variance on real devices; iOS stays at 5s
|
|
1016
1016
|
// where xcrun simctl screenshot is consistent.
|
|
1017
|
+
// Heavy strategies (fiber on deep trees with multi-depth retries, axe accessibility
|
|
1018
|
+
// dumps on dense iOS screens) need more headroom — previous caps produced spurious
|
|
1019
|
+
// timeouts that the agent read as "element missing" when the strategy simply didn't
|
|
1020
|
+
// get to finish. The overall TAP_TIMEOUT_MS budget still bounds the worst case.
|
|
1017
1021
|
const MAX_STRATEGY_MS = {
|
|
1018
|
-
fiber:
|
|
1019
|
-
accessibility:
|
|
1020
|
-
ocr:
|
|
1022
|
+
fiber: 8000,
|
|
1023
|
+
accessibility: 6000,
|
|
1024
|
+
ocr: 6000,
|
|
1021
1025
|
coordinate: 3000
|
|
1022
1026
|
};
|
|
1023
1027
|
function maxStrategyMs(strategy, platform) {
|
|
1024
1028
|
if (strategy === "ocr" && platform === "android")
|
|
1025
|
-
return
|
|
1029
|
+
return 9000;
|
|
1026
1030
|
return MAX_STRATEGY_MS[strategy] ?? 5000;
|
|
1027
1031
|
}
|
|
1028
1032
|
// Matches only the outer withTimeout wrapper message for a tap strategy.
|
|
@@ -1030,7 +1034,7 @@ function maxStrategyMs(strategy, platform) {
|
|
|
1030
1034
|
// must NOT be classified as a tap-level timeout.
|
|
1031
1035
|
const STRATEGY_TIMEOUT_RE = /^(fiber|accessibility|ocr|coordinate) timed out after \d+ms$/;
|
|
1032
1036
|
export function isTapTimeout(attempted) {
|
|
1033
|
-
return attempted.some((a) => STRATEGY_TIMEOUT_RE.test(a.reason) || a.reason.startsWith("Skipped —"));
|
|
1037
|
+
return attempted.some((a) => a.outcome === "timeout" || a.outcome === "skipped" || STRATEGY_TIMEOUT_RE.test(a.reason) || a.reason.startsWith("Skipped —"));
|
|
1034
1038
|
}
|
|
1035
1039
|
function withTimeout(promise, ms, label) {
|
|
1036
1040
|
return new Promise((resolve, reject) => {
|
|
@@ -1181,14 +1185,16 @@ async function verifyAndCapture(platform, shouldVerify, shouldScreenshot, before
|
|
|
1181
1185
|
const afterWithMarker = markerPx
|
|
1182
1186
|
? await drawTapMarker(after.buffer, markerPx.x, markerPx.y)
|
|
1183
1187
|
: after.buffer;
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
:
|
|
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
|
+
};
|
|
1192
1198
|
let verification;
|
|
1193
1199
|
if (!shouldVerify) {
|
|
1194
1200
|
verification = {
|
|
@@ -1252,7 +1258,7 @@ async function verifyAndCapture(platform, shouldVerify, shouldScreenshot, before
|
|
|
1252
1258
|
groupId: verifyGroupId,
|
|
1253
1259
|
metadata: { phase: "after", changeRate: verification?.changeRate }
|
|
1254
1260
|
});
|
|
1255
|
-
return { screenshot, verification,
|
|
1261
|
+
return { screenshot, verification, afterWithMarkerBuffer: afterWithMarker };
|
|
1256
1262
|
}
|
|
1257
1263
|
// --- Burst Capture ---
|
|
1258
1264
|
const BURST_FRAME_COUNT = 4;
|
|
@@ -1348,7 +1354,6 @@ async function burstCaptureAndVerify(platform, beforeBuffer, udid, beforeScaleFa
|
|
|
1348
1354
|
return {
|
|
1349
1355
|
screenshot,
|
|
1350
1356
|
verification,
|
|
1351
|
-
afterBuffer: frames[lastFrameIdx],
|
|
1352
1357
|
afterWithMarkerBuffer: markedFrames[lastFrameIdx]
|
|
1353
1358
|
};
|
|
1354
1359
|
}
|
|
@@ -1478,7 +1483,6 @@ async function captureTapArtifact(ctx) {
|
|
|
1478
1483
|
chosenElement: null,
|
|
1479
1484
|
screenshots: {
|
|
1480
1485
|
before: ctx.screenshotBuffer ?? null,
|
|
1481
|
-
after: ctx.afterBuffer ?? null,
|
|
1482
1486
|
afterWithMarker: ctx.afterWithMarker ?? null
|
|
1483
1487
|
},
|
|
1484
1488
|
deviceMeta: {
|
|
@@ -1813,14 +1817,16 @@ export async function tap(options) {
|
|
|
1813
1817
|
if (strat === "fiber" && !hasMetro) {
|
|
1814
1818
|
attempted.push({
|
|
1815
1819
|
strategy: "fiber",
|
|
1816
|
-
reason: "Skipped — no Metro connection (required for fiber)"
|
|
1820
|
+
reason: "Skipped — no Metro connection (required for fiber)",
|
|
1821
|
+
outcome: "skipped"
|
|
1817
1822
|
});
|
|
1818
1823
|
return false;
|
|
1819
1824
|
}
|
|
1820
1825
|
if (uiDriverMissing && UI_DRIVER_REQUIRED_STRATEGIES.includes(strat)) {
|
|
1821
1826
|
attempted.push({
|
|
1822
1827
|
strategy: strat,
|
|
1823
|
-
reason: "Skipped — iOS UI driver is not installed (required for iOS tap/accessibility/OCR)"
|
|
1828
|
+
reason: "Skipped — iOS UI driver is not installed (required for iOS tap/accessibility/OCR)",
|
|
1829
|
+
outcome: "skipped"
|
|
1824
1830
|
});
|
|
1825
1831
|
return false;
|
|
1826
1832
|
}
|
|
@@ -1845,10 +1851,12 @@ export async function tap(options) {
|
|
|
1845
1851
|
// capture cost is paid either way; bandwidth cost is what `screenshot` toggles.
|
|
1846
1852
|
const shouldScreenshot = options.screenshot !== false;
|
|
1847
1853
|
const canVerify = options.verify !== false;
|
|
1848
|
-
// Capture "before" screenshot
|
|
1854
|
+
// Capture "before" screenshot. Always attempted (independent of canVerify)
|
|
1855
|
+
// so failure artifacts always carry a before.png — the diagnostic value of
|
|
1856
|
+
// the before frame doesn't depend on whether we run the post-tap diff.
|
|
1849
1857
|
let beforeBuffer = null;
|
|
1850
1858
|
let beforeScaleFactor;
|
|
1851
|
-
|
|
1859
|
+
{
|
|
1852
1860
|
const before = await captureScreenshot(platform, targetUdid);
|
|
1853
1861
|
beforeBuffer = before?.buffer || null;
|
|
1854
1862
|
beforeScaleFactor = before?.scaleFactor;
|
|
@@ -1876,7 +1884,8 @@ export async function tap(options) {
|
|
|
1876
1884
|
if (remaining < MIN_STRATEGY_BUDGET_MS) {
|
|
1877
1885
|
attempted.push({
|
|
1878
1886
|
strategy: strat,
|
|
1879
|
-
reason: `Skipped — only ${remaining}ms remaining (budget ${TAP_TIMEOUT_MS}ms)
|
|
1887
|
+
reason: `Skipped — only ${remaining}ms remaining (budget ${TAP_TIMEOUT_MS}ms)`,
|
|
1888
|
+
outcome: "skipped"
|
|
1880
1889
|
});
|
|
1881
1890
|
continue;
|
|
1882
1891
|
}
|
|
@@ -1949,7 +1958,13 @@ export async function tap(options) {
|
|
|
1949
1958
|
// Fall through past the catch block to the success handler.
|
|
1950
1959
|
}
|
|
1951
1960
|
else {
|
|
1952
|
-
|
|
1961
|
+
// Classify the throw: a strategy-level timeout means "result UNKNOWN"
|
|
1962
|
+
// (the strategy didn't finish), distinct from "ran clean and found
|
|
1963
|
+
// nothing". Agents must not infer absence from timeouts. The
|
|
1964
|
+
// `outcome` field carries that distinction; `reason` stays the raw
|
|
1965
|
+
// message so existing log-matchers / classifiers keep working.
|
|
1966
|
+
const outcome = STRATEGY_TIMEOUT_RE.test(reason) ? "timeout" : "error";
|
|
1967
|
+
attempted.push({ strategy: strat, reason, outcome });
|
|
1953
1968
|
continue;
|
|
1954
1969
|
}
|
|
1955
1970
|
}
|
|
@@ -1960,7 +1975,6 @@ export async function tap(options) {
|
|
|
1960
1975
|
ocrPrewarmCtrl?.abort();
|
|
1961
1976
|
let screenshot;
|
|
1962
1977
|
let verification;
|
|
1963
|
-
let afterBuffer;
|
|
1964
1978
|
let afterWithMarkerBuffer;
|
|
1965
1979
|
let dprForMarker;
|
|
1966
1980
|
if (platform === "ios") {
|
|
@@ -1981,15 +1995,22 @@ export async function tap(options) {
|
|
|
1981
1995
|
devicePixelRatio: dprForMarker
|
|
1982
1996
|
});
|
|
1983
1997
|
if (options.burst && canVerify && beforeBuffer) {
|
|
1984
|
-
({ screenshot, verification,
|
|
1998
|
+
({ screenshot, verification, afterWithMarkerBuffer } = await burstCaptureAndVerify(platform, beforeBuffer, targetUdid, beforeScaleFactor, strategyMarker));
|
|
1985
1999
|
// Burst always produces a screenshot for diff visualization. Drop the
|
|
1986
|
-
// bytes when the caller didn't ask for them
|
|
1987
|
-
//
|
|
1988
|
-
|
|
2000
|
+
// bytes when the caller didn't ask for them AND the tap was meaningful
|
|
2001
|
+
// — keeping the screenshot on unmeaningful taps saves the agent a
|
|
2002
|
+
// round-trip when it needs to diagnose. Frames remain in imageBuffer
|
|
2003
|
+
// either way (accessible via get_images(groupId=verification.burstGroupId)).
|
|
2004
|
+
if (!shouldScreenshot && verification?.meaningful !== false)
|
|
1989
2005
|
screenshot = undefined;
|
|
1990
2006
|
}
|
|
1991
2007
|
else {
|
|
1992
|
-
({ screenshot, verification,
|
|
2008
|
+
({ screenshot, verification, afterWithMarkerBuffer } = await verifyAndCapture(platform, canVerify,
|
|
2009
|
+
/* shouldScreenshot */ true, beforeBuffer, targetUdid, beforeScaleFactor, strategyMarker));
|
|
2010
|
+
// Mirror the burst-path gate: drop the screenshot only when the
|
|
2011
|
+
// caller didn't ask for it AND the tap was meaningful.
|
|
2012
|
+
if (!shouldScreenshot && verification?.meaningful !== false)
|
|
2013
|
+
screenshot = undefined;
|
|
1993
2014
|
}
|
|
1994
2015
|
if (screenshot && app) {
|
|
1995
2016
|
app.lastScreenshot = {
|
|
@@ -2014,7 +2035,9 @@ export async function tap(options) {
|
|
|
2014
2035
|
});
|
|
2015
2036
|
// Capture an artifact for "successful but unmeaningful" taps so we can
|
|
2016
2037
|
// diagnose taps that landed wrong or hit non-responsive elements.
|
|
2017
|
-
|
|
2038
|
+
// Uses the same `meaningful` flag the agent sees so dashboard outcomes
|
|
2039
|
+
// stay consistent with what the caller observed.
|
|
2040
|
+
if (verification && !verification.skipped && verification.meaningful === false) {
|
|
2018
2041
|
const unmeaningfulSignals = await captureTapArtifact({
|
|
2019
2042
|
query,
|
|
2020
2043
|
outcome: "unmeaningful",
|
|
@@ -2024,7 +2047,6 @@ export async function tap(options) {
|
|
|
2024
2047
|
deviceName,
|
|
2025
2048
|
screenshotMeta: screenshot ? { width: screenshot.width, height: screenshot.height } : undefined,
|
|
2026
2049
|
screenshotBuffer: beforeBuffer,
|
|
2027
|
-
afterBuffer: afterBuffer ?? null,
|
|
2028
2050
|
afterWithMarker: afterWithMarkerBuffer ?? null,
|
|
2029
2051
|
chosenTapPoint: strategyMarker ? { x: strategyMarker.x, y: strategyMarker.y } : null,
|
|
2030
2052
|
verification,
|
|
@@ -2034,7 +2056,19 @@ export async function tap(options) {
|
|
|
2034
2056
|
}
|
|
2035
2057
|
return successResult;
|
|
2036
2058
|
}
|
|
2037
|
-
|
|
2059
|
+
// Classify the strategy's non-success result. The reason string carries
|
|
2060
|
+
// the diagnostic from the strategy; outcome lets agents branch on category
|
|
2061
|
+
// without parsing prose. "invisible" / "ambiguous" are pinned by substring
|
|
2062
|
+
// matches the fiber strategy emits via pressables.ts.
|
|
2063
|
+
const reasonStr = result.reason || "";
|
|
2064
|
+
let attemptOutcome = "not-found";
|
|
2065
|
+
if (result.ambiguous)
|
|
2066
|
+
attemptOutcome = "ambiguous";
|
|
2067
|
+
else if (reasonStr.indexOf("but none are visible") !== -1)
|
|
2068
|
+
attemptOutcome = "invisible";
|
|
2069
|
+
else if (STRATEGY_TIMEOUT_RE.test(reasonStr))
|
|
2070
|
+
attemptOutcome = "timeout";
|
|
2071
|
+
attempted.push({ strategy: strat, reason: reasonStr, outcome: attemptOutcome });
|
|
2038
2072
|
// If fiber found an element with measured coordinates, do a native tap directly
|
|
2039
2073
|
if (strat === "fiber" && result.convertedTo && result.pressed) {
|
|
2040
2074
|
try {
|
|
@@ -2092,8 +2126,11 @@ export async function tap(options) {
|
|
|
2092
2126
|
({ screenshot, verification } = await burstCaptureAndVerify(platform, beforeBuffer, targetUdid, beforeScaleFactor, fiberMarker));
|
|
2093
2127
|
}
|
|
2094
2128
|
else {
|
|
2095
|
-
({ screenshot, verification } = await verifyAndCapture(platform, canVerify,
|
|
2129
|
+
({ screenshot, verification } = await verifyAndCapture(platform, canVerify,
|
|
2130
|
+
/* shouldScreenshot */ true, beforeBuffer, targetUdid, beforeScaleFactor, fiberMarker));
|
|
2096
2131
|
}
|
|
2132
|
+
if (!shouldScreenshot && verification?.meaningful !== false)
|
|
2133
|
+
screenshot = undefined;
|
|
2097
2134
|
if (screenshot && app) {
|
|
2098
2135
|
app.lastScreenshot = {
|
|
2099
2136
|
originalWidth: screenshot.width,
|
|
@@ -2151,7 +2188,20 @@ export async function tap(options) {
|
|
|
2151
2188
|
// itself ran out of time.
|
|
2152
2189
|
const hitTimeout = isTapTimeout(attempted);
|
|
2153
2190
|
const elapsed = TAP_TIMEOUT_MS - remainingMs();
|
|
2154
|
-
|
|
2191
|
+
// If fiber located the element but visibility filtered it out, prepend a
|
|
2192
|
+
// scroll-or-dismiss hint so the agent doesn't waste a turn assuming the
|
|
2193
|
+
// testID/component is missing.
|
|
2194
|
+
const fiberSawInvisible = attempted.some(a => a.strategy === "fiber" && a.outcome === "invisible");
|
|
2195
|
+
const allTimedOut = attempted.length > 0 && attempted.every(a => a.outcome === "timeout" || a.outcome === "skipped");
|
|
2196
|
+
let suggestion = buildSuggestion(query, strategies, platform);
|
|
2197
|
+
if (fiberSawInvisible) {
|
|
2198
|
+
suggestion = `Element exists in the React tree but is not on screen. Try scrolling it into view (${platform === "ios" ? "ios_swipe / arrow keys" : "android_swipe"}), dismiss any overlay covering it, or wait for layout to settle. ` + suggestion;
|
|
2199
|
+
}
|
|
2200
|
+
else if (allTimedOut) {
|
|
2201
|
+
// No strategy got to a definitive answer — don't let the agent conclude
|
|
2202
|
+
// the element is missing. Steer toward retry or a different strategy.
|
|
2203
|
+
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
|
+
}
|
|
2155
2205
|
const { screenshot: failScreenshot } = shouldScreenshot
|
|
2156
2206
|
? await verifyAndCapture(platform, false, true, null, targetUdid)
|
|
2157
2207
|
: { screenshot: undefined };
|
|
@@ -2162,10 +2212,22 @@ export async function tap(options) {
|
|
|
2162
2212
|
scaleFactor: failScreenshot.scaleFactor
|
|
2163
2213
|
};
|
|
2164
2214
|
}
|
|
2215
|
+
// Pick the right error framing: a definitive "not found" is different from
|
|
2216
|
+
// "all strategies timed out — we don't know whether it exists".
|
|
2217
|
+
let errorOverride;
|
|
2218
|
+
if (hitTimeout) {
|
|
2219
|
+
errorOverride = `Tap timed out after ${elapsed}ms (budget ${TAP_TIMEOUT_MS}ms)`;
|
|
2220
|
+
}
|
|
2221
|
+
else if (allTimedOut) {
|
|
2222
|
+
errorOverride = `All tap strategies timed out before completing — element presence is UNKNOWN`;
|
|
2223
|
+
}
|
|
2224
|
+
else if (fiberSawInvisible) {
|
|
2225
|
+
errorOverride = `Element matches the query but is not visible on screen`;
|
|
2226
|
+
}
|
|
2165
2227
|
const failureResult = formatTapFailure({
|
|
2166
2228
|
query,
|
|
2167
2229
|
attempted,
|
|
2168
|
-
error:
|
|
2230
|
+
error: errorOverride,
|
|
2169
2231
|
suggestion,
|
|
2170
2232
|
device: deviceName,
|
|
2171
2233
|
screenshot: failScreenshot
|