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/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 = 20000;
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: 5000,
1019
- accessibility: 3000,
1020
- ocr: 5000,
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 7000;
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
- const screenshot = shouldScreenshot
1185
- ? {
1186
- image: screenshotToBase64(afterWithMarker),
1187
- width: after.width,
1188
- height: after.height,
1189
- scaleFactor: after.scaleFactor
1190
- }
1191
- : undefined;
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, afterBuffer: after.buffer, afterWithMarkerBuffer: afterWithMarker };
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 for verification
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
- if (canVerify) {
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
- attempted.push({ strategy: strat, reason });
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, afterBuffer, afterWithMarkerBuffer } = await burstCaptureAndVerify(platform, beforeBuffer, targetUdid, beforeScaleFactor, strategyMarker));
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 — frames are still in
1987
- // imageBuffer (accessible via get_images(groupId=verification.burstGroupId)).
1988
- if (!shouldScreenshot)
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, afterBuffer, afterWithMarkerBuffer } = await verifyAndCapture(platform, canVerify, shouldScreenshot, beforeBuffer, targetUdid, beforeScaleFactor, strategyMarker));
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
- if (verification && !verification.skipped && (verification.changeRate ?? 1) < 0.001) {
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
- attempted.push({ strategy: strat, reason: result.reason });
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, shouldScreenshot, beforeBuffer, targetUdid, beforeScaleFactor, fiberMarker));
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
- const suggestion = buildSuggestion(query, strategies, platform);
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: hitTimeout ? `Tap timed out after ${elapsed}ms (budget ${TAP_TIMEOUT_MS}ms)` : undefined,
2230
+ error: errorOverride,
2169
2231
  suggestion,
2170
2232
  device: deviceName,
2171
2233
  screenshot: failScreenshot