react-native-ai-debugger 1.4.41 → 1.5.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/build/__tests__/integration/tap-strategies.test.js +4 -3
- package/build/__tests__/integration/tap-strategies.test.js.map +1 -1
- package/build/core/connection.d.ts +14 -0
- package/build/core/connection.d.ts.map +1 -1
- package/build/core/connection.js +49 -0
- package/build/core/connection.js.map +1 -1
- package/build/core/executor.d.ts +3 -1
- package/build/core/executor.d.ts.map +1 -1
- package/build/core/executor.js +269 -24
- package/build/core/executor.js.map +1 -1
- package/build/core/index.d.ts +2 -2
- package/build/core/index.d.ts.map +1 -1
- package/build/core/index.js +2 -2
- package/build/core/index.js.map +1 -1
- package/build/core/ios.d.ts +1 -0
- package/build/core/ios.d.ts.map +1 -1
- package/build/core/ios.js +41 -0
- package/build/core/ios.js.map +1 -1
- package/build/index.js +206 -74
- package/build/index.js.map +1 -1
- package/build/pro/tap.d.ts +8 -0
- package/build/pro/tap.d.ts.map +1 -1
- package/build/pro/tap.js +188 -21
- package/build/pro/tap.js.map +1 -1
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import { getInstallationId, getServerVersion, getPackageName, isDevMode, TELEMET
|
|
|
13
13
|
import { isSDKInstalled, querySDKNetwork, getSDKNetworkEntry, getSDKNetworkStats, clearSDKNetwork, querySDKConsole, getSDKConsoleStats, clearSDKConsole } from "./core/sdkBridge.js";
|
|
14
14
|
import { tap, convertScreenshotToTapCoords } from "./pro/tap.js";
|
|
15
15
|
import { getActivateLicenseConfig, handleActivateLicense, getDeleteAccountConfig, handleDeleteAccount, } from "./tools/accountTools.js";
|
|
16
|
-
import { logBuffers, networkBuffers, getLogBuffer, getNetworkBuffer, getTotalLogCount, getConnectedAppByDevice, LogBuffer, NetworkBuffer, bundleErrorBuffer, imageBuffer, connectedApps, scanMetroPorts, fetchDevices, filterDebuggableDevices, connectToDevice, getConnectedApps, executeInApp, listDebugGlobals, inspectGlobal, reloadApp,
|
|
16
|
+
import { logBuffers, networkBuffers, getLogBuffer, getNetworkBuffer, getTotalLogCount, getConnectedAppByDevice, getConnectedAppBySimulatorUdid, getConnectedAppByAndroidDeviceId, LogBuffer, NetworkBuffer, bundleErrorBuffer, imageBuffer, connectedApps, getActiveOrBootedSimulatorUdid, scanMetroPorts, fetchDevices, filterDebuggableDevices, connectToDevice, getConnectedApps, executeInApp, listDebugGlobals, inspectGlobal, reloadApp,
|
|
17
17
|
// React Component Inspection
|
|
18
18
|
getComponentTree, getScreenLayout, getPressableElements, inspectComponent, findComponents, inspectAtPoint, toggleElementInspector, isInspectorActive, getInspectorSelection, getFirstConnectedApp, getLogs, searchLogs, getLogSummary, getNetworkRequests, searchNetworkRequests, getNetworkStats, formatRequestDetails,
|
|
19
19
|
// Connection state
|
|
@@ -27,7 +27,7 @@ connectMetroBuildEvents, disconnectMetroBuildEvents, getBundleErrors, getBundleS
|
|
|
27
27
|
// Error screen parsing (OCR fallback)
|
|
28
28
|
parseErrorScreenText, formatParsedError,
|
|
29
29
|
// OCR
|
|
30
|
-
recognizeText, inferIOSDevicePixelRatio,
|
|
30
|
+
recognizeText, inferIOSDevicePixelRatio,
|
|
31
31
|
// Android
|
|
32
32
|
listAndroidDevices, androidScreenshot, androidInstallApp, androidLaunchApp, androidListPackages,
|
|
33
33
|
// Android UI Input (Phase 2)
|
|
@@ -41,7 +41,7 @@ listIOSSimulators, iosScreenshot, iosInstallApp, iosLaunchApp, iosOpenUrl, iosTe
|
|
|
41
41
|
// iOS UI driver tools
|
|
42
42
|
isUiDriverAvailable, iosTap, iosSwipe, iosInputText, iosButton, iosKeyEvent, iosKeySequence, iosDescribeAll, iosDescribePoint, IOS_BUTTON_TYPES,
|
|
43
43
|
// iOS Element Finding (no screenshots)
|
|
44
|
-
iosFindElement, iosWaitForElement, getDevicePixelRatio,
|
|
44
|
+
iosFindElement, iosWaitForElement, getDevicePixelRatio, getIOSSafeAreaTop,
|
|
45
45
|
// Telemetry
|
|
46
46
|
initTelemetry, trackToolInvocation, getTargetPlatform,
|
|
47
47
|
// Format utilities (TONL)
|
|
@@ -1494,7 +1494,7 @@ registerToolWithTelemetry("get_screen_layout", {
|
|
|
1494
1494
|
});
|
|
1495
1495
|
// Tool: Get all pressable elements on screen
|
|
1496
1496
|
registerToolWithTelemetry("get_pressable_elements", {
|
|
1497
|
-
description: "Find all pressable (onPress) and input (TextInput) elements currently visible on screen. Returns component names, tap
|
|
1497
|
+
description: "Find all pressable (onPress) and input (TextInput) elements currently visible on screen. Returns component names, ready-to-tap center coordinates in SCREENSHOT PIXELS (same space as ios_screenshot/android_screenshot — pass directly to tap(x, y)), text labels, testID, accessibilityLabel, and a spatial nearbyText hint for icon-only buttons. Each element includes hasLabel (true if it contains text) and isInput (true for TextInput fields).",
|
|
1498
1498
|
inputSchema: {
|
|
1499
1499
|
device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
|
|
1500
1500
|
}
|
|
@@ -1511,15 +1511,130 @@ registerToolWithTelemetry("get_pressable_elements", {
|
|
|
1511
1511
|
isError: true
|
|
1512
1512
|
};
|
|
1513
1513
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1514
|
+
const elements = result.parsedElements;
|
|
1515
|
+
if (!elements || elements.length === 0) {
|
|
1516
|
+
return {
|
|
1517
|
+
content: [{ type: "text", text: result.result || "No pressable elements found." }]
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
// Resolve target app so we can pick the right simulator / android device
|
|
1521
|
+
const targetApp = device ? getConnectedAppByDevice(device) : getFirstConnectedApp();
|
|
1522
|
+
if (!targetApp) {
|
|
1523
|
+
// No connected app — return raw (points) output; better than nothing
|
|
1524
|
+
return {
|
|
1525
|
+
content: [{ type: "text", text: result.result || "No pressable elements found." }]
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
// Capture a lightweight screenshot to learn scaleFactor (downscale) and dimensions.
|
|
1529
|
+
// Conversion: screenshot_px = native_coord * devicePixelRatio / screenshotScale
|
|
1530
|
+
let lines = [];
|
|
1531
|
+
try {
|
|
1532
|
+
if (targetApp.platform === "ios") {
|
|
1533
|
+
const udid = targetApp.simulatorUdid;
|
|
1534
|
+
const shot = await iosScreenshot(undefined, udid);
|
|
1535
|
+
const screenshotScale = shot.scaleFactor || 1;
|
|
1536
|
+
const devicePixelRatio = (shot.originalWidth && shot.originalHeight
|
|
1537
|
+
? inferIOSDevicePixelRatio(shot.originalWidth, shot.originalHeight)
|
|
1538
|
+
: null) ?? (await getDevicePixelRatio(udid)) ?? 3;
|
|
1539
|
+
// Fallback to 59pt (iPhone typical) when the UI driver preflight can't
|
|
1540
|
+
// resolve the true inset — matches ios_screenshot's default. Without this
|
|
1541
|
+
// shift, react-native-screens modal-presented screens report y relative
|
|
1542
|
+
// to content origin and taps land in the status bar instead of the button.
|
|
1543
|
+
const safeAreaTop = (await getIOSSafeAreaTop(udid).catch(() => 0)) || 59;
|
|
1544
|
+
// Keep the app's lastScreenshot metadata in sync so tap(x, y) uses the
|
|
1545
|
+
// same scaleFactor when converting our pixel coords back to points.
|
|
1546
|
+
if (shot.originalWidth && shot.originalHeight) {
|
|
1547
|
+
targetApp.lastScreenshot = {
|
|
1548
|
+
originalWidth: shot.originalWidth,
|
|
1549
|
+
originalHeight: shot.originalHeight,
|
|
1550
|
+
scaleFactor: screenshotScale
|
|
1551
|
+
};
|
|
1519
1552
|
}
|
|
1520
|
-
|
|
1553
|
+
lines = formatPressablesInPixels(elements, {
|
|
1554
|
+
platform: "ios",
|
|
1555
|
+
devicePixelRatio,
|
|
1556
|
+
screenshotScale,
|
|
1557
|
+
safeAreaTop
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
else {
|
|
1561
|
+
const deviceId = targetApp.deviceInfo?.deviceName;
|
|
1562
|
+
const shot = await androidScreenshot(undefined, deviceId);
|
|
1563
|
+
const screenshotScale = shot.scaleFactor || 1;
|
|
1564
|
+
const density = await androidGetDensity(deviceId).catch(() => ({ density: 160 }));
|
|
1565
|
+
const devicePixelRatio = (density.density || 160) / 160;
|
|
1566
|
+
if (shot.originalWidth && shot.originalHeight) {
|
|
1567
|
+
targetApp.lastScreenshot = {
|
|
1568
|
+
originalWidth: shot.originalWidth,
|
|
1569
|
+
originalHeight: shot.originalHeight,
|
|
1570
|
+
scaleFactor: screenshotScale
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
lines = formatPressablesInPixels(elements, {
|
|
1574
|
+
platform: "android",
|
|
1575
|
+
devicePixelRatio,
|
|
1576
|
+
screenshotScale,
|
|
1577
|
+
safeAreaTop: 0
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
catch {
|
|
1582
|
+
// Fallback to points if screenshot/metadata unavailable
|
|
1583
|
+
return {
|
|
1584
|
+
content: [{ type: "text", text: result.result || "No pressable elements found." }]
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
const iconCount = elements.filter((e) => !e.hasLabel).length;
|
|
1588
|
+
const labeledCount = elements.length - iconCount;
|
|
1589
|
+
const summary = `Found ${elements.length} pressable elements (${iconCount} icon-only, ${labeledCount} with text labels)`;
|
|
1590
|
+
const text = [summary, "", ...lines].join("\n");
|
|
1591
|
+
return {
|
|
1592
|
+
content: [{ type: "text", text }]
|
|
1521
1593
|
};
|
|
1522
1594
|
});
|
|
1595
|
+
function formatPressablesInPixels(elements, opts) {
|
|
1596
|
+
const { platform, devicePixelRatio, screenshotScale, safeAreaTop } = opts;
|
|
1597
|
+
const out = [];
|
|
1598
|
+
for (let i = 0; i < elements.length; i++) {
|
|
1599
|
+
const el = elements[i];
|
|
1600
|
+
// react-native-screens modal presentations report y relative to content origin;
|
|
1601
|
+
// shift into the window frame when the measurement falls inside the safe-area band.
|
|
1602
|
+
let cy = el.center.y;
|
|
1603
|
+
let fy = el.frame.y;
|
|
1604
|
+
if (platform === "ios" && safeAreaTop > 0) {
|
|
1605
|
+
if (cy < safeAreaTop)
|
|
1606
|
+
cy += safeAreaTop;
|
|
1607
|
+
if (fy < safeAreaTop)
|
|
1608
|
+
fy += safeAreaTop;
|
|
1609
|
+
}
|
|
1610
|
+
const toPx = (v) => Math.round((v * devicePixelRatio) / screenshotScale);
|
|
1611
|
+
const cx = toPx(el.center.x);
|
|
1612
|
+
const cyPx = toPx(cy);
|
|
1613
|
+
const fx = toPx(el.frame.x);
|
|
1614
|
+
const fyPx = toPx(fy);
|
|
1615
|
+
const fw = toPx(el.frame.width);
|
|
1616
|
+
const fh = toPx(el.frame.height);
|
|
1617
|
+
const num = i + 1;
|
|
1618
|
+
const label = el.hasLabel
|
|
1619
|
+
? `"${el.text}"`
|
|
1620
|
+
: el.intent
|
|
1621
|
+
? `(${el.intent} icon)`
|
|
1622
|
+
: "(icon/image)";
|
|
1623
|
+
const ids = [];
|
|
1624
|
+
if (el.testID)
|
|
1625
|
+
ids.push(`testID="${el.testID}"`);
|
|
1626
|
+
if (el.accessibilityLabel)
|
|
1627
|
+
ids.push(`a11y="${el.accessibilityLabel}"`);
|
|
1628
|
+
const idStr = ids.length > 0 ? ` [${ids.join(", ")}]` : "";
|
|
1629
|
+
const inputStr = el.isInput ? " (input)" : "";
|
|
1630
|
+
const wrapperStr = el.isWrapper ? " [wrapper — skip unless dismissing keyboard]" : "";
|
|
1631
|
+
const nearPart = el.nearbyText ? ` near "${el.nearbyText}"` : "";
|
|
1632
|
+
out.push(`${num}. ${el.component} ${label}${nearPart} — center:(${cx},${cyPx}) frame:(${fx},${fyPx} ${fw}x${fh})${idStr}${inputStr}${wrapperStr}`);
|
|
1633
|
+
if (el.path)
|
|
1634
|
+
out.push(` path: ${el.path}`);
|
|
1635
|
+
}
|
|
1636
|
+
return out;
|
|
1637
|
+
}
|
|
1523
1638
|
// Tool: Inspect a specific component by name
|
|
1524
1639
|
registerToolWithTelemetry("inspect_component", {
|
|
1525
1640
|
description: "Inspect a specific React component by name. **DRILL-DOWN TOOL**: Use after get_screen_layout or find_components to identify which component to inspect. Returns props, style, state (hooks), and optionally children tree. Use childrenDepth to control how deep nested children go.",
|
|
@@ -1745,8 +1860,8 @@ registerToolWithTelemetry("tap", {
|
|
|
1745
1860
|
}
|
|
1746
1861
|
return {
|
|
1747
1862
|
content,
|
|
1748
|
-
isError: !result.success,
|
|
1749
|
-
_errorMessage: !result.success
|
|
1863
|
+
isError: !result.success && !result.ambiguous,
|
|
1864
|
+
_errorMessage: !result.success && !result.ambiguous
|
|
1750
1865
|
? `${JSON.stringify(result.query)}|${result.error || ""}`
|
|
1751
1866
|
: undefined,
|
|
1752
1867
|
_errorContext: errorContext,
|
|
@@ -2871,10 +2986,13 @@ registerToolWithTelemetry("android_screenshot", {
|
|
|
2871
2986
|
// Build info text with coordinate conversion guidance
|
|
2872
2987
|
const pixelWidth = result.originalWidth || 0;
|
|
2873
2988
|
const pixelHeight = result.originalHeight || 0;
|
|
2874
|
-
//
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2989
|
+
// Resolve the RN app running on THIS Android device so enrichment
|
|
2990
|
+
// pulls data from the right app (not whichever app is "first").
|
|
2991
|
+
const targetApp = getConnectedAppByAndroidDeviceId(deviceId);
|
|
2992
|
+
const targetDeviceName = targetApp?.deviceInfo.deviceName;
|
|
2993
|
+
// Store screenshot metadata on the matching app (not an arbitrary one)
|
|
2994
|
+
if (targetApp) {
|
|
2995
|
+
targetApp.lastScreenshot = {
|
|
2878
2996
|
originalWidth: pixelWidth,
|
|
2879
2997
|
originalHeight: pixelHeight,
|
|
2880
2998
|
scaleFactor: result.scaleFactor || 1,
|
|
@@ -2902,27 +3020,28 @@ registerToolWithTelemetry("android_screenshot", {
|
|
|
2902
3020
|
// Enrich with screen layout data (component names + tap coordinates)
|
|
2903
3021
|
// Android: density ratio = densityDpi / 160 (160dpi = 1dp = 1px baseline)
|
|
2904
3022
|
const androidPixelRatio = densityDpi / 160;
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
}
|
|
2909
|
-
catch {
|
|
2910
|
-
// Non-fatal: screenshot works without layout enrichment
|
|
2911
|
-
}
|
|
2912
|
-
// Enrich with pressable elements (filtered list of tappable components, converted to screenshot pixel coords)
|
|
3023
|
+
// Skip enrichment when no RN app is connected to this Android device —
|
|
3024
|
+
// otherwise we would pull fiber data from a different device's app.
|
|
3025
|
+
const canEnrich = !!targetApp;
|
|
2913
3026
|
let pressablesText = null;
|
|
3027
|
+
// Screen Layout tree previously appended here was dropped — it was noisy
|
|
3028
|
+
// (nested Svg/G/Path duplicates). Use get_screen_layout when the tree is needed.
|
|
3029
|
+
// Enrich with pressable elements (filtered list of tappable components, converted to screenshot pixel coords)
|
|
2914
3030
|
try {
|
|
2915
|
-
|
|
3031
|
+
if (!canEnrich)
|
|
3032
|
+
throw new Error("skip");
|
|
3033
|
+
const pressables = await getPressableElements({ device: targetDeviceName });
|
|
2916
3034
|
if (pressables.success && pressables.parsedElements && pressables.parsedElements.length > 0) {
|
|
2917
3035
|
const screenshotScale = result.scaleFactor || 1;
|
|
2918
3036
|
pressablesText = pressables.parsedElements.map((el) => {
|
|
2919
3037
|
const px = Math.round((el.center.x * androidPixelRatio) / screenshotScale);
|
|
2920
3038
|
const py = Math.round((el.center.y * androidPixelRatio) / screenshotScale);
|
|
2921
|
-
const label = el.accessibilityLabel || el.text || el.testID || el.component;
|
|
3039
|
+
const label = el.accessibilityLabel || el.text || el.testID || (el.intent ? `${el.intent} icon` : el.component);
|
|
2922
3040
|
const idPart = el.testID ? ` testID="${el.testID}"` : "";
|
|
2923
3041
|
const kindPart = el.isInput ? " [input]" : "";
|
|
2924
3042
|
const wrapPart = el.isWrapper ? " [wrapper — skip]" : "";
|
|
2925
|
-
|
|
3043
|
+
const nearPart = !el.text && !el.accessibilityLabel && el.nearbyText ? ` near "${el.nearbyText}"` : "";
|
|
3044
|
+
return ` (${px}, ${py}) ${el.component}: "${label}"${nearPart}${idPart}${kindPart}${wrapPart}`;
|
|
2926
3045
|
}).join("\n");
|
|
2927
3046
|
}
|
|
2928
3047
|
}
|
|
@@ -2942,34 +3061,35 @@ registerToolWithTelemetry("android_screenshot", {
|
|
|
2942
3061
|
if (pressablesText) {
|
|
2943
3062
|
infoText += `\n\n🎯 Pressable elements (ready-to-tap, coordinates in screenshot pixels):`;
|
|
2944
3063
|
infoText += `\n${pressablesText}`;
|
|
2945
|
-
}
|
|
2946
|
-
if (layoutText) {
|
|
2947
|
-
infoText += `\n\n📋 Screen Layout (all components, tap coordinates in pixels):`;
|
|
2948
|
-
infoText += `\n${layoutText}`;
|
|
2949
|
-
}
|
|
2950
|
-
if (pressablesText || layoutText) {
|
|
2951
3064
|
infoText += `\n\n💡 Next steps:`;
|
|
2952
3065
|
infoText += `\n • tap(text="Button Label") — when text is exact and unique`;
|
|
2953
3066
|
infoText += `\n • tap(testID="id") or tap(component="Name") — when you know the identifier`;
|
|
2954
3067
|
infoText += `\n • tap(x=<px>, y=<px>) — use coordinates from the pressable elements list above (reliable for icons and ambiguous elements)`;
|
|
2955
|
-
infoText += `\n •
|
|
3068
|
+
infoText += `\n • get_screen_layout — full component tree when you need more than pressables`;
|
|
2956
3069
|
}
|
|
2957
3070
|
else {
|
|
3071
|
+
if (!canEnrich && connectedApps.size > 0) {
|
|
3072
|
+
infoText += `\n\nℹ️ Pressable enrichment skipped: no RN app is connected to device ${deviceId ?? "(default)"}.`;
|
|
3073
|
+
infoText += ` ${connectedApps.size} other app(s) are connected on different device(s) — their fiber data was intentionally not used to avoid mismatched output.`;
|
|
3074
|
+
}
|
|
2958
3075
|
infoText += `\n\n💡 Next steps:`;
|
|
2959
3076
|
infoText += `\n • tap(text="Button Label") — tap element by visible text`;
|
|
2960
3077
|
infoText += `\n • tap(x=<px>, y=<px>) — tap at coordinates from this screenshot`;
|
|
2961
3078
|
infoText += `\n • android_describe_all — get full UI tree with exact tap coordinates`;
|
|
2962
3079
|
infoText += `\n • android_find_element(text="...") — find element coordinates without tapping`;
|
|
2963
3080
|
}
|
|
2964
|
-
// Check for LogBox overlay
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
3081
|
+
// Check for LogBox overlay — only on the matching RN app; skip otherwise
|
|
3082
|
+
// to avoid surfacing warnings from a different device's app.
|
|
3083
|
+
if (canEnrich) {
|
|
3084
|
+
try {
|
|
3085
|
+
const logBoxState = await detectLogBox(targetDeviceName);
|
|
3086
|
+
if (logBoxState && logBoxState.total > 0) {
|
|
3087
|
+
infoText += formatLogBoxWarning(logBoxState);
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
catch {
|
|
3091
|
+
// Non-fatal: LogBox detection failure should not break screenshot
|
|
2969
3092
|
}
|
|
2970
|
-
}
|
|
2971
|
-
catch {
|
|
2972
|
-
// Non-fatal: LogBox detection failure should not break screenshot
|
|
2973
3093
|
}
|
|
2974
3094
|
imageBuffer.add({
|
|
2975
3095
|
id: `android-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
@@ -3459,10 +3579,15 @@ registerToolWithTelemetry("ios_screenshot", {
|
|
|
3459
3579
|
// Build info text with coordinate guidance for iOS
|
|
3460
3580
|
const pixelWidth = result.originalWidth || 0;
|
|
3461
3581
|
const pixelHeight = result.originalHeight || 0;
|
|
3462
|
-
//
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3582
|
+
// Resolve the RN app running on THIS simulator so enrichment pulls
|
|
3583
|
+
// fiber/layout data from the right app (not the first-connected one,
|
|
3584
|
+
// which may belong to a different simulator).
|
|
3585
|
+
const resolvedUdid = udid || (await getActiveOrBootedSimulatorUdid());
|
|
3586
|
+
const targetApp = resolvedUdid ? getConnectedAppBySimulatorUdid(resolvedUdid) : null;
|
|
3587
|
+
const targetDeviceName = targetApp?.deviceInfo.deviceName;
|
|
3588
|
+
// Store screenshot metadata on the matching app (not an arbitrary one)
|
|
3589
|
+
if (targetApp) {
|
|
3590
|
+
targetApp.lastScreenshot = {
|
|
3466
3591
|
originalWidth: pixelWidth,
|
|
3467
3592
|
originalHeight: pixelHeight,
|
|
3468
3593
|
scaleFactor: result.scaleFactor || 1,
|
|
@@ -3514,28 +3639,34 @@ registerToolWithTelemetry("ios_screenshot", {
|
|
|
3514
3639
|
pointHeight = Math.round(pixelHeight / scaleFactor);
|
|
3515
3640
|
}
|
|
3516
3641
|
const safeAreaOffsetPixels = safeAreaTop * scaleFactor;
|
|
3517
|
-
//
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
}
|
|
3522
|
-
catch {
|
|
3523
|
-
// Non-fatal: screenshot works without layout enrichment
|
|
3524
|
-
}
|
|
3525
|
-
// Enrich with pressable elements (filtered list of tappable components, converted to screenshot pixel coords)
|
|
3642
|
+
// The Screen Layout tree was previously appended here but produced huge noisy
|
|
3643
|
+
// output (nested Svg/G/Path duplicates). Agents should use get_screen_layout
|
|
3644
|
+
// explicitly when they need the tree. The Pressable elements block below is
|
|
3645
|
+
// the signal most consumers actually want.
|
|
3526
3646
|
let pressablesText = null;
|
|
3647
|
+
const canEnrich = !!targetApp;
|
|
3648
|
+
// Enrich with pressable elements (filtered list of tappable components, converted to screenshot pixel coords)
|
|
3527
3649
|
try {
|
|
3528
|
-
|
|
3650
|
+
if (!canEnrich)
|
|
3651
|
+
throw new Error("skip");
|
|
3652
|
+
const pressables = await getPressableElements({ device: targetDeviceName });
|
|
3529
3653
|
if (pressables.success && pressables.parsedElements && pressables.parsedElements.length > 0) {
|
|
3530
3654
|
const screenshotScale = result.scaleFactor || 1;
|
|
3531
3655
|
pressablesText = pressables.parsedElements.map((el) => {
|
|
3656
|
+
// See enrichScreenshotWithLayout: shift y when fiber reports the element
|
|
3657
|
+
// inside the safe-area band (a react-native-screens modal artifact).
|
|
3658
|
+
let centerYPoints = el.center.y;
|
|
3659
|
+
if (safeAreaTop > 0 && centerYPoints < safeAreaTop) {
|
|
3660
|
+
centerYPoints += safeAreaTop;
|
|
3661
|
+
}
|
|
3532
3662
|
const px = Math.round((el.center.x * scaleFactor) / screenshotScale);
|
|
3533
|
-
const py = Math.round((
|
|
3534
|
-
const label = el.accessibilityLabel || el.text || el.testID || el.component;
|
|
3663
|
+
const py = Math.round((centerYPoints * scaleFactor) / screenshotScale);
|
|
3664
|
+
const label = el.accessibilityLabel || el.text || el.testID || (el.intent ? `${el.intent} icon` : el.component);
|
|
3535
3665
|
const idPart = el.testID ? ` testID="${el.testID}"` : "";
|
|
3536
3666
|
const kindPart = el.isInput ? " [input]" : "";
|
|
3537
3667
|
const wrapPart = el.isWrapper ? " [wrapper — skip]" : "";
|
|
3538
|
-
|
|
3668
|
+
const nearPart = !el.text && !el.accessibilityLabel && el.nearbyText ? ` near "${el.nearbyText}"` : "";
|
|
3669
|
+
return ` (${px}, ${py}) ${el.component}: "${label}"${nearPart}${idPart}${kindPart}${wrapPart}`;
|
|
3539
3670
|
}).join("\n");
|
|
3540
3671
|
}
|
|
3541
3672
|
}
|
|
@@ -3552,34 +3683,35 @@ registerToolWithTelemetry("ios_screenshot", {
|
|
|
3552
3683
|
if (pressablesText) {
|
|
3553
3684
|
infoText += `\n\n🎯 Pressable elements (ready-to-tap, coordinates in screenshot pixels):`;
|
|
3554
3685
|
infoText += `\n${pressablesText}`;
|
|
3555
|
-
}
|
|
3556
|
-
if (layoutText) {
|
|
3557
|
-
infoText += `\n\n📋 Screen Layout (all components, tap coordinates in pixels):`;
|
|
3558
|
-
infoText += `\n${layoutText}`;
|
|
3559
|
-
}
|
|
3560
|
-
if (pressablesText || layoutText) {
|
|
3561
3686
|
infoText += `\n\n💡 Next steps:`;
|
|
3562
3687
|
infoText += `\n • tap(text="Button Label") — when text is exact and unique`;
|
|
3563
3688
|
infoText += `\n • tap(testID="id") or tap(component="Name") — when you know the identifier`;
|
|
3564
3689
|
infoText += `\n • tap(x=<px>, y=<px>) — use coordinates from the pressable elements list above (reliable for icons and ambiguous elements)`;
|
|
3565
|
-
infoText += `\n •
|
|
3690
|
+
infoText += `\n • get_screen_layout — full component tree when you need more than pressables`;
|
|
3566
3691
|
}
|
|
3567
3692
|
else {
|
|
3693
|
+
if (!canEnrich && connectedApps.size > 0) {
|
|
3694
|
+
infoText += `\n\nℹ️ Pressable enrichment skipped: no RN app is connected to simulator ${resolvedUdid}.`;
|
|
3695
|
+
infoText += ` ${connectedApps.size} other app(s) are connected on different device(s) — their fiber data was intentionally not used to avoid mismatched output.`;
|
|
3696
|
+
}
|
|
3568
3697
|
infoText += `\n\n💡 Next steps:`;
|
|
3569
3698
|
infoText += `\n • tap(text="Button Label") — tap element by visible text`;
|
|
3570
3699
|
infoText += `\n • tap(x=<px>, y=<px>) — tap at coordinates from this screenshot`;
|
|
3571
3700
|
infoText += `\n • ios_describe_all — get full UI tree with exact tap coordinates`;
|
|
3572
3701
|
infoText += `\n • ios_find_element(label="...") — find element coordinates without tapping`;
|
|
3573
3702
|
}
|
|
3574
|
-
// Check for LogBox overlay
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3703
|
+
// Check for LogBox overlay — only on the matching RN app; skip otherwise
|
|
3704
|
+
// to avoid surfacing warnings from a different simulator's app.
|
|
3705
|
+
if (canEnrich) {
|
|
3706
|
+
try {
|
|
3707
|
+
const logBoxState = await detectLogBox(targetDeviceName);
|
|
3708
|
+
if (logBoxState && logBoxState.total > 0) {
|
|
3709
|
+
infoText += formatLogBoxWarning(logBoxState);
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
catch {
|
|
3713
|
+
// Non-fatal: LogBox detection failure should not break screenshot
|
|
3579
3714
|
}
|
|
3580
|
-
}
|
|
3581
|
-
catch {
|
|
3582
|
-
// Non-fatal: LogBox detection failure should not break screenshot
|
|
3583
3715
|
}
|
|
3584
3716
|
imageBuffer.add({
|
|
3585
3717
|
id: `ios-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|