execbro 0.0.1-reserve → 1.10.12

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.
Files changed (475) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +496 -0
  3. package/build/__tests__/helpers/fake-cdp-server.d.ts +56 -0
  4. package/build/__tests__/helpers/fake-cdp-server.d.ts.map +1 -0
  5. package/build/__tests__/helpers/fake-cdp-server.js +120 -0
  6. package/build/__tests__/helpers/fake-cdp-server.js.map +1 -0
  7. package/build/__tests__/helpers/tap-test-helpers.d.ts +36 -0
  8. package/build/__tests__/helpers/tap-test-helpers.d.ts.map +1 -0
  9. package/build/__tests__/helpers/tap-test-helpers.js +130 -0
  10. package/build/__tests__/helpers/tap-test-helpers.js.map +1 -0
  11. package/build/__tests__/integration/auto-reconnect.test.d.ts +2 -0
  12. package/build/__tests__/integration/auto-reconnect.test.d.ts.map +1 -0
  13. package/build/__tests__/integration/auto-reconnect.test.js +118 -0
  14. package/build/__tests__/integration/auto-reconnect.test.js.map +1 -0
  15. package/build/__tests__/integration/connection-health.test.d.ts +2 -0
  16. package/build/__tests__/integration/connection-health.test.d.ts.map +1 -0
  17. package/build/__tests__/integration/connection-health.test.js +157 -0
  18. package/build/__tests__/integration/connection-health.test.js.map +1 -0
  19. package/build/__tests__/integration/execute-in-app.test.d.ts +2 -0
  20. package/build/__tests__/integration/execute-in-app.test.d.ts.map +1 -0
  21. package/build/__tests__/integration/execute-in-app.test.js +124 -0
  22. package/build/__tests__/integration/execute-in-app.test.js.map +1 -0
  23. package/build/__tests__/integration/per-call-timeout.test.d.ts +2 -0
  24. package/build/__tests__/integration/per-call-timeout.test.d.ts.map +1 -0
  25. package/build/__tests__/integration/per-call-timeout.test.js +103 -0
  26. package/build/__tests__/integration/per-call-timeout.test.js.map +1 -0
  27. package/build/__tests__/integration/tap-strategies.test.d.ts +2 -0
  28. package/build/__tests__/integration/tap-strategies.test.d.ts.map +1 -0
  29. package/build/__tests__/integration/tap-strategies.test.js +330 -0
  30. package/build/__tests__/integration/tap-strategies.test.js.map +1 -0
  31. package/build/__tests__/integration/tools.test.d.ts +2 -0
  32. package/build/__tests__/integration/tools.test.d.ts.map +1 -0
  33. package/build/__tests__/integration/tools.test.js +227 -0
  34. package/build/__tests__/integration/tools.test.js.map +1 -0
  35. package/build/__tests__/setup.d.ts +2 -0
  36. package/build/__tests__/setup.d.ts.map +1 -0
  37. package/build/__tests__/setup.js +15 -0
  38. package/build/__tests__/setup.js.map +1 -0
  39. package/build/__tests__/unit/androidEmulatorAvds.test.d.ts +2 -0
  40. package/build/__tests__/unit/androidEmulatorAvds.test.d.ts.map +1 -0
  41. package/build/__tests__/unit/androidEmulatorAvds.test.js +98 -0
  42. package/build/__tests__/unit/androidEmulatorAvds.test.js.map +1 -0
  43. package/build/__tests__/unit/buildToken.test.d.ts +2 -0
  44. package/build/__tests__/unit/buildToken.test.d.ts.map +1 -0
  45. package/build/__tests__/unit/buildToken.test.js +22 -0
  46. package/build/__tests__/unit/buildToken.test.js.map +1 -0
  47. package/build/__tests__/unit/bundle.test.d.ts +2 -0
  48. package/build/__tests__/unit/bundle.test.d.ts.map +1 -0
  49. package/build/__tests__/unit/bundle.test.js +53 -0
  50. package/build/__tests__/unit/bundle.test.js.map +1 -0
  51. package/build/__tests__/unit/burst.test.d.ts +2 -0
  52. package/build/__tests__/unit/burst.test.d.ts.map +1 -0
  53. package/build/__tests__/unit/burst.test.js +54 -0
  54. package/build/__tests__/unit/burst.test.js.map +1 -0
  55. package/build/__tests__/unit/connection-health.test.d.ts +2 -0
  56. package/build/__tests__/unit/connection-health.test.d.ts.map +1 -0
  57. package/build/__tests__/unit/connection-health.test.js +38 -0
  58. package/build/__tests__/unit/connection-health.test.js.map +1 -0
  59. package/build/__tests__/unit/debugGlobals.test.d.ts +2 -0
  60. package/build/__tests__/unit/debugGlobals.test.d.ts.map +1 -0
  61. package/build/__tests__/unit/debugGlobals.test.js +14 -0
  62. package/build/__tests__/unit/debugGlobals.test.js.map +1 -0
  63. package/build/__tests__/unit/deviceDiscovery.test.d.ts +2 -0
  64. package/build/__tests__/unit/deviceDiscovery.test.d.ts.map +1 -0
  65. package/build/__tests__/unit/deviceDiscovery.test.js +119 -0
  66. package/build/__tests__/unit/deviceDiscovery.test.js.map +1 -0
  67. package/build/__tests__/unit/deviceResolver.test.d.ts +2 -0
  68. package/build/__tests__/unit/deviceResolver.test.d.ts.map +1 -0
  69. package/build/__tests__/unit/deviceResolver.test.js +261 -0
  70. package/build/__tests__/unit/deviceResolver.test.js.map +1 -0
  71. package/build/__tests__/unit/escapeNonAscii.test.d.ts +2 -0
  72. package/build/__tests__/unit/escapeNonAscii.test.d.ts.map +1 -0
  73. package/build/__tests__/unit/escapeNonAscii.test.js +76 -0
  74. package/build/__tests__/unit/escapeNonAscii.test.js.map +1 -0
  75. package/build/__tests__/unit/exec.test.d.ts +2 -0
  76. package/build/__tests__/unit/exec.test.d.ts.map +1 -0
  77. package/build/__tests__/unit/exec.test.js +54 -0
  78. package/build/__tests__/unit/exec.test.js.map +1 -0
  79. package/build/__tests__/unit/executor.test.d.ts +2 -0
  80. package/build/__tests__/unit/executor.test.d.ts.map +1 -0
  81. package/build/__tests__/unit/executor.test.js +220 -0
  82. package/build/__tests__/unit/executor.test.js.map +1 -0
  83. package/build/__tests__/unit/failureArtifact.test.d.ts +2 -0
  84. package/build/__tests__/unit/failureArtifact.test.d.ts.map +1 -0
  85. package/build/__tests__/unit/failureArtifact.test.js +207 -0
  86. package/build/__tests__/unit/failureArtifact.test.js.map +1 -0
  87. package/build/__tests__/unit/fastRefreshRecorder.test.d.ts +2 -0
  88. package/build/__tests__/unit/fastRefreshRecorder.test.d.ts.map +1 -0
  89. package/build/__tests__/unit/fastRefreshRecorder.test.js +68 -0
  90. package/build/__tests__/unit/fastRefreshRecorder.test.js.map +1 -0
  91. package/build/__tests__/unit/fastRefreshTools.test.d.ts +2 -0
  92. package/build/__tests__/unit/fastRefreshTools.test.d.ts.map +1 -0
  93. package/build/__tests__/unit/fastRefreshTools.test.js +112 -0
  94. package/build/__tests__/unit/fastRefreshTools.test.js.map +1 -0
  95. package/build/__tests__/unit/fiberMatch.test.d.ts +2 -0
  96. package/build/__tests__/unit/fiberMatch.test.d.ts.map +1 -0
  97. package/build/__tests__/unit/fiberMatch.test.js +114 -0
  98. package/build/__tests__/unit/fiberMatch.test.js.map +1 -0
  99. package/build/__tests__/unit/fingerprint.test.d.ts +2 -0
  100. package/build/__tests__/unit/fingerprint.test.d.ts.map +1 -0
  101. package/build/__tests__/unit/fingerprint.test.js +99 -0
  102. package/build/__tests__/unit/fingerprint.test.js.map +1 -0
  103. package/build/__tests__/unit/focusedInput.test.d.ts +2 -0
  104. package/build/__tests__/unit/focusedInput.test.d.ts.map +1 -0
  105. package/build/__tests__/unit/focusedInput.test.js +58 -0
  106. package/build/__tests__/unit/focusedInput.test.js.map +1 -0
  107. package/build/__tests__/unit/focusedInputTools.test.d.ts +2 -0
  108. package/build/__tests__/unit/focusedInputTools.test.d.ts.map +1 -0
  109. package/build/__tests__/unit/focusedInputTools.test.js +79 -0
  110. package/build/__tests__/unit/focusedInputTools.test.js.map +1 -0
  111. package/build/__tests__/unit/iconSemantics.test.d.ts +2 -0
  112. package/build/__tests__/unit/iconSemantics.test.d.ts.map +1 -0
  113. package/build/__tests__/unit/iconSemantics.test.js +80 -0
  114. package/build/__tests__/unit/iconSemantics.test.js.map +1 -0
  115. package/build/__tests__/unit/imageBuffer.test.d.ts +2 -0
  116. package/build/__tests__/unit/imageBuffer.test.d.ts.map +1 -0
  117. package/build/__tests__/unit/imageBuffer.test.js +128 -0
  118. package/build/__tests__/unit/imageBuffer.test.js.map +1 -0
  119. package/build/__tests__/unit/logs.test.d.ts +2 -0
  120. package/build/__tests__/unit/logs.test.d.ts.map +1 -0
  121. package/build/__tests__/unit/logs.test.js +90 -0
  122. package/build/__tests__/unit/logs.test.js.map +1 -0
  123. package/build/__tests__/unit/measureComponent.test.d.ts +2 -0
  124. package/build/__tests__/unit/measureComponent.test.d.ts.map +1 -0
  125. package/build/__tests__/unit/measureComponent.test.js +66 -0
  126. package/build/__tests__/unit/measureComponent.test.js.map +1 -0
  127. package/build/__tests__/unit/measureComponentTools.test.d.ts +2 -0
  128. package/build/__tests__/unit/measureComponentTools.test.d.ts.map +1 -0
  129. package/build/__tests__/unit/measureComponentTools.test.js +94 -0
  130. package/build/__tests__/unit/measureComponentTools.test.js.map +1 -0
  131. package/build/__tests__/unit/metro.test.d.ts +2 -0
  132. package/build/__tests__/unit/metro.test.d.ts.map +1 -0
  133. package/build/__tests__/unit/metro.test.js +61 -0
  134. package/build/__tests__/unit/metro.test.js.map +1 -0
  135. package/build/__tests__/unit/network.test.d.ts +2 -0
  136. package/build/__tests__/unit/network.test.d.ts.map +1 -0
  137. package/build/__tests__/unit/network.test.js +102 -0
  138. package/build/__tests__/unit/network.test.js.map +1 -0
  139. package/build/__tests__/unit/networkInterceptor.test.d.ts +2 -0
  140. package/build/__tests__/unit/networkInterceptor.test.d.ts.map +1 -0
  141. package/build/__tests__/unit/networkInterceptor.test.js +104 -0
  142. package/build/__tests__/unit/networkInterceptor.test.js.map +1 -0
  143. package/build/__tests__/unit/probe.test.d.ts +2 -0
  144. package/build/__tests__/unit/probe.test.d.ts.map +1 -0
  145. package/build/__tests__/unit/probe.test.js +62 -0
  146. package/build/__tests__/unit/probe.test.js.map +1 -0
  147. package/build/__tests__/unit/rnGlobalsBootstrap.test.d.ts +2 -0
  148. package/build/__tests__/unit/rnGlobalsBootstrap.test.d.ts.map +1 -0
  149. package/build/__tests__/unit/rnGlobalsBootstrap.test.js +29 -0
  150. package/build/__tests__/unit/rnGlobalsBootstrap.test.js.map +1 -0
  151. package/build/__tests__/unit/screenState.test.d.ts +2 -0
  152. package/build/__tests__/unit/screenState.test.d.ts.map +1 -0
  153. package/build/__tests__/unit/screenState.test.js +186 -0
  154. package/build/__tests__/unit/screenState.test.js.map +1 -0
  155. package/build/__tests__/unit/screenshot-diff.test.d.ts +2 -0
  156. package/build/__tests__/unit/screenshot-diff.test.d.ts.map +1 -0
  157. package/build/__tests__/unit/screenshot-diff.test.js +97 -0
  158. package/build/__tests__/unit/screenshot-diff.test.js.map +1 -0
  159. package/build/__tests__/unit/tap-evidence.test.d.ts +2 -0
  160. package/build/__tests__/unit/tap-evidence.test.d.ts.map +1 -0
  161. package/build/__tests__/unit/tap-evidence.test.js +89 -0
  162. package/build/__tests__/unit/tap-evidence.test.js.map +1 -0
  163. package/build/__tests__/unit/tap.test.d.ts +2 -0
  164. package/build/__tests__/unit/tap.test.d.ts.map +1 -0
  165. package/build/__tests__/unit/tap.test.js +769 -0
  166. package/build/__tests__/unit/tap.test.js.map +1 -0
  167. package/build/__tests__/unit/timeoutClamp.test.d.ts +2 -0
  168. package/build/__tests__/unit/timeoutClamp.test.d.ts.map +1 -0
  169. package/build/__tests__/unit/timeoutClamp.test.js +17 -0
  170. package/build/__tests__/unit/timeoutClamp.test.js.map +1 -0
  171. package/build/__tests__/unit/toolDescriptions.test.d.ts +2 -0
  172. package/build/__tests__/unit/toolDescriptions.test.d.ts.map +1 -0
  173. package/build/__tests__/unit/toolDescriptions.test.js +145 -0
  174. package/build/__tests__/unit/toolDescriptions.test.js.map +1 -0
  175. package/build/__tests__/unit/transportClassify.test.d.ts +2 -0
  176. package/build/__tests__/unit/transportClassify.test.d.ts.map +1 -0
  177. package/build/__tests__/unit/transportClassify.test.js +60 -0
  178. package/build/__tests__/unit/transportClassify.test.js.map +1 -0
  179. package/build/__tests__/unit/verifyAction.test.d.ts +2 -0
  180. package/build/__tests__/unit/verifyAction.test.d.ts.map +1 -0
  181. package/build/__tests__/unit/verifyAction.test.js +26 -0
  182. package/build/__tests__/unit/verifyAction.test.js.map +1 -0
  183. package/build/core/android.d.ts +299 -0
  184. package/build/core/android.d.ts.map +1 -0
  185. package/build/core/android.js +1582 -0
  186. package/build/core/android.js.map +1 -0
  187. package/build/core/appDetection.d.ts +20 -0
  188. package/build/core/appDetection.d.ts.map +1 -0
  189. package/build/core/appDetection.js +167 -0
  190. package/build/core/appDetection.js.map +1 -0
  191. package/build/core/bundle.d.ts +49 -0
  192. package/build/core/bundle.d.ts.map +1 -0
  193. package/build/core/bundle.js +356 -0
  194. package/build/core/bundle.js.map +1 -0
  195. package/build/core/componentSearch.d.ts +41 -0
  196. package/build/core/componentSearch.d.ts.map +1 -0
  197. package/build/core/componentSearch.js +588 -0
  198. package/build/core/componentSearch.js.map +1 -0
  199. package/build/core/componentTree.d.ts +13 -0
  200. package/build/core/componentTree.d.ts.map +1 -0
  201. package/build/core/componentTree.js +275 -0
  202. package/build/core/componentTree.js.map +1 -0
  203. package/build/core/config.d.ts +9 -0
  204. package/build/core/config.d.ts.map +1 -0
  205. package/build/core/config.js +28 -0
  206. package/build/core/config.js.map +1 -0
  207. package/build/core/connection.d.ts +102 -0
  208. package/build/core/connection.d.ts.map +1 -0
  209. package/build/core/connection.js +1710 -0
  210. package/build/core/connection.js.map +1 -0
  211. package/build/core/connectionState.d.ts +122 -0
  212. package/build/core/connectionState.d.ts.map +1 -0
  213. package/build/core/connectionState.js +313 -0
  214. package/build/core/connectionState.js.map +1 -0
  215. package/build/core/debugGlobals.d.ts +6 -0
  216. package/build/core/debugGlobals.d.ts.map +1 -0
  217. package/build/core/debugGlobals.js +370 -0
  218. package/build/core/debugGlobals.js.map +1 -0
  219. package/build/core/deviceDiscovery.d.ts +52 -0
  220. package/build/core/deviceDiscovery.d.ts.map +1 -0
  221. package/build/core/deviceDiscovery.js +74 -0
  222. package/build/core/deviceDiscovery.js.map +1 -0
  223. package/build/core/deviceResolver.d.ts +44 -0
  224. package/build/core/deviceResolver.d.ts.map +1 -0
  225. package/build/core/deviceResolver.js +225 -0
  226. package/build/core/deviceResolver.js.map +1 -0
  227. package/build/core/errorScreenParser.d.ts +30 -0
  228. package/build/core/errorScreenParser.d.ts.map +1 -0
  229. package/build/core/errorScreenParser.js +198 -0
  230. package/build/core/errorScreenParser.js.map +1 -0
  231. package/build/core/errors.d.ts +14 -0
  232. package/build/core/errors.d.ts.map +1 -0
  233. package/build/core/errors.js +17 -0
  234. package/build/core/errors.js.map +1 -0
  235. package/build/core/escapeNonAscii.d.ts +18 -0
  236. package/build/core/escapeNonAscii.d.ts.map +1 -0
  237. package/build/core/escapeNonAscii.js +352 -0
  238. package/build/core/escapeNonAscii.js.map +1 -0
  239. package/build/core/exec.d.ts +22 -0
  240. package/build/core/exec.d.ts.map +1 -0
  241. package/build/core/exec.js +54 -0
  242. package/build/core/exec.js.map +1 -0
  243. package/build/core/executor.d.ts +9 -0
  244. package/build/core/executor.d.ts.map +1 -0
  245. package/build/core/executor.js +12 -0
  246. package/build/core/executor.js.map +1 -0
  247. package/build/core/failureArtifact.d.ts +120 -0
  248. package/build/core/failureArtifact.d.ts.map +1 -0
  249. package/build/core/failureArtifact.js +113 -0
  250. package/build/core/failureArtifact.js.map +1 -0
  251. package/build/core/fastRefreshRecorder.d.ts +23 -0
  252. package/build/core/fastRefreshRecorder.d.ts.map +1 -0
  253. package/build/core/fastRefreshRecorder.js +83 -0
  254. package/build/core/fastRefreshRecorder.js.map +1 -0
  255. package/build/core/fastRefreshTools.d.ts +19 -0
  256. package/build/core/fastRefreshTools.d.ts.map +1 -0
  257. package/build/core/fastRefreshTools.js +71 -0
  258. package/build/core/fastRefreshTools.js.map +1 -0
  259. package/build/core/feedback.d.ts +18 -0
  260. package/build/core/feedback.d.ts.map +1 -0
  261. package/build/core/feedback.js +73 -0
  262. package/build/core/feedback.js.map +1 -0
  263. package/build/core/fiberMatch.d.ts +25 -0
  264. package/build/core/fiberMatch.d.ts.map +1 -0
  265. package/build/core/fiberMatch.js +35 -0
  266. package/build/core/fiberMatch.js.map +1 -0
  267. package/build/core/fingerprint.d.ts +4 -0
  268. package/build/core/fingerprint.d.ts.map +1 -0
  269. package/build/core/fingerprint.js +47 -0
  270. package/build/core/fingerprint.js.map +1 -0
  271. package/build/core/focusedInput.d.ts +27 -0
  272. package/build/core/focusedInput.d.ts.map +1 -0
  273. package/build/core/focusedInput.js +110 -0
  274. package/build/core/focusedInput.js.map +1 -0
  275. package/build/core/focusedInputTools.d.ts +21 -0
  276. package/build/core/focusedInputTools.d.ts.map +1 -0
  277. package/build/core/focusedInputTools.js +51 -0
  278. package/build/core/focusedInputTools.js.map +1 -0
  279. package/build/core/format.d.ts +8 -0
  280. package/build/core/format.d.ts.map +1 -0
  281. package/build/core/format.js +34 -0
  282. package/build/core/format.js.map +1 -0
  283. package/build/core/guides.d.ts +21 -0
  284. package/build/core/guides.d.ts.map +1 -0
  285. package/build/core/guides.js +397 -0
  286. package/build/core/guides.js.map +1 -0
  287. package/build/core/iconSemantics.d.ts +18 -0
  288. package/build/core/iconSemantics.d.ts.map +1 -0
  289. package/build/core/iconSemantics.js +114 -0
  290. package/build/core/iconSemantics.js.map +1 -0
  291. package/build/core/imageBuffer.d.ts +37 -0
  292. package/build/core/imageBuffer.d.ts.map +1 -0
  293. package/build/core/imageBuffer.js +58 -0
  294. package/build/core/imageBuffer.js.map +1 -0
  295. package/build/core/index.d.ts +37 -0
  296. package/build/core/index.d.ts.map +1 -0
  297. package/build/core/index.js +66 -0
  298. package/build/core/index.js.map +1 -0
  299. package/build/core/inspector.d.ts +55 -0
  300. package/build/core/inspector.d.ts.map +1 -0
  301. package/build/core/inspector.js +603 -0
  302. package/build/core/inspector.js.map +1 -0
  303. package/build/core/ios.d.ts +263 -0
  304. package/build/core/ios.d.ts.map +1 -0
  305. package/build/core/ios.js +1705 -0
  306. package/build/core/ios.js.map +1 -0
  307. package/build/core/jsExecute.d.ts +62 -0
  308. package/build/core/jsExecute.d.ts.map +1 -0
  309. package/build/core/jsExecute.js +696 -0
  310. package/build/core/jsExecute.js.map +1 -0
  311. package/build/core/license.d.ts +48 -0
  312. package/build/core/license.d.ts.map +1 -0
  313. package/build/core/license.js +267 -0
  314. package/build/core/license.js.map +1 -0
  315. package/build/core/logbox.d.ts +58 -0
  316. package/build/core/logbox.d.ts.map +1 -0
  317. package/build/core/logbox.js +330 -0
  318. package/build/core/logbox.js.map +1 -0
  319. package/build/core/logs.d.ts +44 -0
  320. package/build/core/logs.d.ts.map +1 -0
  321. package/build/core/logs.js +149 -0
  322. package/build/core/logs.js.map +1 -0
  323. package/build/core/measureComponent.d.ts +19 -0
  324. package/build/core/measureComponent.d.ts.map +1 -0
  325. package/build/core/measureComponent.js +94 -0
  326. package/build/core/measureComponent.js.map +1 -0
  327. package/build/core/measureComponentTools.d.ts +6 -0
  328. package/build/core/measureComponentTools.d.ts.map +1 -0
  329. package/build/core/measureComponentTools.js +43 -0
  330. package/build/core/measureComponentTools.js.map +1 -0
  331. package/build/core/metro.d.ts +31 -0
  332. package/build/core/metro.d.ts.map +1 -0
  333. package/build/core/metro.js +124 -0
  334. package/build/core/metro.js.map +1 -0
  335. package/build/core/nativeOnlyHints.d.ts +14 -0
  336. package/build/core/nativeOnlyHints.d.ts.map +1 -0
  337. package/build/core/nativeOnlyHints.js +58 -0
  338. package/build/core/nativeOnlyHints.js.map +1 -0
  339. package/build/core/network.d.ts +43 -0
  340. package/build/core/network.d.ts.map +1 -0
  341. package/build/core/network.js +217 -0
  342. package/build/core/network.js.map +1 -0
  343. package/build/core/networkInterceptor.d.ts +30 -0
  344. package/build/core/networkInterceptor.d.ts.map +1 -0
  345. package/build/core/networkInterceptor.js +211 -0
  346. package/build/core/networkInterceptor.js.map +1 -0
  347. package/build/core/ocr.d.ts +87 -0
  348. package/build/core/ocr.d.ts.map +1 -0
  349. package/build/core/ocr.js +335 -0
  350. package/build/core/ocr.js.map +1 -0
  351. package/build/core/paths.d.ts +2 -0
  352. package/build/core/paths.d.ts.map +1 -0
  353. package/build/core/paths.js +28 -0
  354. package/build/core/paths.js.map +1 -0
  355. package/build/core/posthog.d.ts +5 -0
  356. package/build/core/posthog.d.ts.map +1 -0
  357. package/build/core/posthog.js +52 -0
  358. package/build/core/posthog.js.map +1 -0
  359. package/build/core/pressables.d.ts +46 -0
  360. package/build/core/pressables.d.ts.map +1 -0
  361. package/build/core/pressables.js +1719 -0
  362. package/build/core/pressables.js.map +1 -0
  363. package/build/core/probe.d.ts +12 -0
  364. package/build/core/probe.d.ts.map +1 -0
  365. package/build/core/probe.js +56 -0
  366. package/build/core/probe.js.map +1 -0
  367. package/build/core/redux.d.ts +22 -0
  368. package/build/core/redux.d.ts.map +1 -0
  369. package/build/core/redux.js +116 -0
  370. package/build/core/redux.js.map +1 -0
  371. package/build/core/register.d.ts +8 -0
  372. package/build/core/register.d.ts.map +1 -0
  373. package/build/core/register.js +202 -0
  374. package/build/core/register.js.map +1 -0
  375. package/build/core/rnGlobalsBootstrap.d.ts +16 -0
  376. package/build/core/rnGlobalsBootstrap.d.ts.map +1 -0
  377. package/build/core/rnGlobalsBootstrap.js +116 -0
  378. package/build/core/rnGlobalsBootstrap.js.map +1 -0
  379. package/build/core/screenLayout.d.ts +53 -0
  380. package/build/core/screenLayout.d.ts.map +1 -0
  381. package/build/core/screenLayout.js +878 -0
  382. package/build/core/screenLayout.js.map +1 -0
  383. package/build/core/screenState.d.ts +98 -0
  384. package/build/core/screenState.d.ts.map +1 -0
  385. package/build/core/screenState.js +1121 -0
  386. package/build/core/screenState.js.map +1 -0
  387. package/build/core/sdkBridge.d.ts +67 -0
  388. package/build/core/sdkBridge.d.ts.map +1 -0
  389. package/build/core/sdkBridge.js +148 -0
  390. package/build/core/sdkBridge.js.map +1 -0
  391. package/build/core/state.d.ts +37 -0
  392. package/build/core/state.d.ts.map +1 -0
  393. package/build/core/state.js +124 -0
  394. package/build/core/state.js.map +1 -0
  395. package/build/core/telemetry.d.ts +50 -0
  396. package/build/core/telemetry.d.ts.map +1 -0
  397. package/build/core/telemetry.js +653 -0
  398. package/build/core/telemetry.js.map +1 -0
  399. package/build/core/toolHelpers.d.ts +24 -0
  400. package/build/core/toolHelpers.d.ts.map +1 -0
  401. package/build/core/toolHelpers.js +118 -0
  402. package/build/core/toolHelpers.js.map +1 -0
  403. package/build/core/types.d.ts +162 -0
  404. package/build/core/types.d.ts.map +1 -0
  405. package/build/core/types.js +2 -0
  406. package/build/core/types.js.map +1 -0
  407. package/build/index.d.ts +4 -0
  408. package/build/index.d.ts.map +1 -0
  409. package/build/index.js +132 -0
  410. package/build/index.js.map +1 -0
  411. package/build/pro/screenshot-diff.d.ts +10 -0
  412. package/build/pro/screenshot-diff.d.ts.map +1 -0
  413. package/build/pro/screenshot-diff.js +38 -0
  414. package/build/pro/screenshot-diff.js.map +1 -0
  415. package/build/pro/tap.d.ts +295 -0
  416. package/build/pro/tap.d.ts.map +1 -0
  417. package/build/pro/tap.js +1937 -0
  418. package/build/pro/tap.js.map +1 -0
  419. package/build/pro/verifyAction.d.ts +50 -0
  420. package/build/pro/verifyAction.d.ts.map +1 -0
  421. package/build/pro/verifyAction.js +285 -0
  422. package/build/pro/verifyAction.js.map +1 -0
  423. package/build/tools/_deviceArg.d.ts +45 -0
  424. package/build/tools/_deviceArg.d.ts.map +1 -0
  425. package/build/tools/_deviceArg.js +58 -0
  426. package/build/tools/_deviceArg.js.map +1 -0
  427. package/build/tools/accountTools.d.ts +46 -0
  428. package/build/tools/accountTools.d.ts.map +1 -0
  429. package/build/tools/accountTools.js +223 -0
  430. package/build/tools/accountTools.js.map +1 -0
  431. package/build/tools/bundleTools.d.ts +3 -0
  432. package/build/tools/bundleTools.d.ts.map +1 -0
  433. package/build/tools/bundleTools.js +375 -0
  434. package/build/tools/bundleTools.js.map +1 -0
  435. package/build/tools/componentTools.d.ts +3 -0
  436. package/build/tools/componentTools.d.ts.map +1 -0
  437. package/build/tools/componentTools.js +745 -0
  438. package/build/tools/componentTools.js.map +1 -0
  439. package/build/tools/connectionTools.d.ts +3 -0
  440. package/build/tools/connectionTools.d.ts.map +1 -0
  441. package/build/tools/connectionTools.js +625 -0
  442. package/build/tools/connectionTools.js.map +1 -0
  443. package/build/tools/deviceTools.d.ts +3 -0
  444. package/build/tools/deviceTools.d.ts.map +1 -0
  445. package/build/tools/deviceTools.js +423 -0
  446. package/build/tools/deviceTools.js.map +1 -0
  447. package/build/tools/executionTools.d.ts +3 -0
  448. package/build/tools/executionTools.d.ts.map +1 -0
  449. package/build/tools/executionTools.js +243 -0
  450. package/build/tools/executionTools.js.map +1 -0
  451. package/build/tools/interactionTools.d.ts +3 -0
  452. package/build/tools/interactionTools.d.ts.map +1 -0
  453. package/build/tools/interactionTools.js +539 -0
  454. package/build/tools/interactionTools.js.map +1 -0
  455. package/build/tools/logTools.d.ts +3 -0
  456. package/build/tools/logTools.d.ts.map +1 -0
  457. package/build/tools/logTools.js +371 -0
  458. package/build/tools/logTools.js.map +1 -0
  459. package/build/tools/metaTools.d.ts +7 -0
  460. package/build/tools/metaTools.d.ts.map +1 -0
  461. package/build/tools/metaTools.js +186 -0
  462. package/build/tools/metaTools.js.map +1 -0
  463. package/build/tools/networkTools.d.ts +3 -0
  464. package/build/tools/networkTools.d.ts.map +1 -0
  465. package/build/tools/networkTools.js +478 -0
  466. package/build/tools/networkTools.js.map +1 -0
  467. package/build/tools/reduxTools.d.ts +3 -0
  468. package/build/tools/reduxTools.d.ts.map +1 -0
  469. package/build/tools/reduxTools.js +83 -0
  470. package/build/tools/reduxTools.js.map +1 -0
  471. package/build/tools/screenshotTools.d.ts +3 -0
  472. package/build/tools/screenshotTools.d.ts.map +1 -0
  473. package/build/tools/screenshotTools.js +703 -0
  474. package/build/tools/screenshotTools.js.map +1 -0
  475. package/package.json +63 -2
@@ -0,0 +1,1937 @@
1
+ import { connectedApps } from "../core/state.js";
2
+ import { executeInApp } from "../core/executor.js";
3
+ import { pressElement } from "../core/executor.js";
4
+ import { iosTap, iosFindElement, iosScreenshot, isUiDriverAvailable, getUiDriverInstallHint, getIOSSafeAreaTop } from "../core/ios.js";
5
+ import { androidTap, androidFindElement } from "../core/android.js";
6
+ import { compareScreenshots } from "./screenshot-diff.js";
7
+ import { scanMetroPorts, fetchDevices, selectMainDevice } from "../core/metro.js";
8
+ import { connectToDevice, clearReconnectionSuppression, getConnectedAppByDevice } from "../core/connection.js";
9
+ import { resolveDeviceTarget, formatResolverError } from "../core/deviceResolver.js";
10
+ import { notifyDriverMissing } from "../core/logbox.js";
11
+ import { captureFailureArtifact } from "../core/failureArtifact.js";
12
+ import { captureScreenshot, verifyAndCapture, burstCaptureAndVerify, } from "./verifyAction.js";
13
+ export function buildVerificationExplanation(v) {
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";
18
+ // Burst path with typed verdict
19
+ if (v.kind === "settled_elsewhere") {
20
+ return `${Action} caused a visible UI change (${pct(v.changeRate)} pixel diff). The screen updated as expected.`;
21
+ }
22
+ if (v.kind === "snap_back") {
23
+ if (action === "swipe") {
24
+ return (`Snap-back detected: content moved during the drag (frame ${v.peakFrame} peak ${pct(v.peakChangeRate || 0)} diff) ` +
25
+ `but returned to the starting position. Classic 'content fits inside the viewport' pattern — ` +
26
+ `check contentSize vs layoutSize on the ScrollView, not gesture handling.`);
27
+ }
28
+ return (`Transient visual feedback detected (frame ${v.peakFrame} peak ${pct(v.peakChangeRate || 0)} diff) ` +
29
+ `but no persistent change. ${Action} triggered a momentary animation that settled back.`);
30
+ }
31
+ if (v.kind === "missed") {
32
+ return (`No visual change detected — neither persistent nor transient across burst frames. ` +
33
+ `The ${target} may not respond visually or the ${action} may have missed its target.`);
34
+ }
35
+ // Legacy non-burst path
36
+ if (v.meaningful) {
37
+ return `${Action} caused a visible UI change (${pct(v.changeRate)} pixel diff). The screen updated as expected.`;
38
+ }
39
+ return (`No visual change detected between before and after screenshots. ` +
40
+ `The ${target} may not respond visually or the ${action} may have missed.`);
41
+ }
42
+ const BURST_CHANGE_THRESHOLD = 0.005;
43
+ export async function analyzeBurstFrames(frames, options) {
44
+ if (frames.length < 2) {
45
+ return {
46
+ meaningful: false,
47
+ persistentChangeRate: 0,
48
+ transientChangeDetected: false,
49
+ peakChangeRate: 0,
50
+ peakFrame: 0,
51
+ framesWithChange: [],
52
+ kind: "missed"
53
+ };
54
+ }
55
+ let peakChangeRate = 0;
56
+ let peakFrame = 0;
57
+ const framesWithChange = [];
58
+ for (let i = 1; i < frames.length; i++) {
59
+ const diff = await compareScreenshots(frames[i - 1], frames[i], options);
60
+ if (diff.changeRate > BURST_CHANGE_THRESHOLD) {
61
+ framesWithChange.push(i);
62
+ }
63
+ if (diff.changeRate > peakChangeRate) {
64
+ peakChangeRate = diff.changeRate;
65
+ peakFrame = i;
66
+ }
67
+ }
68
+ const persistentDiff = await compareScreenshots(frames[0], frames[frames.length - 1], options);
69
+ const persistentChangeRate = persistentDiff.changeRate;
70
+ const transientChangeDetected = !persistentDiff.changed && framesWithChange.length > 0;
71
+ // Snap-back (transient motion that reverts) is NOT a successful gesture —
72
+ // the content moved during the drag but returned to its starting position.
73
+ // Callers diagnosing scroll failures need this to read as "didn't work".
74
+ const meaningful = persistentDiff.changed;
75
+ let kind;
76
+ if (persistentDiff.changed) {
77
+ kind = "settled_elsewhere";
78
+ }
79
+ else if (transientChangeDetected) {
80
+ kind = "snap_back";
81
+ }
82
+ else {
83
+ kind = "missed";
84
+ }
85
+ return {
86
+ meaningful,
87
+ persistentChangeRate,
88
+ transientChangeDetected,
89
+ peakChangeRate,
90
+ peakFrame,
91
+ framesWithChange,
92
+ kind
93
+ };
94
+ }
95
+ // --- Helpers ---
96
+ export function buildQuery(options) {
97
+ const query = {};
98
+ if (options.text !== undefined)
99
+ query.text = options.text;
100
+ if (options.testID !== undefined)
101
+ query.testID = options.testID;
102
+ if (options.component !== undefined)
103
+ query.component = options.component;
104
+ if (options.x !== undefined)
105
+ query.x = options.x;
106
+ if (options.y !== undefined)
107
+ query.y = options.y;
108
+ return query;
109
+ }
110
+ /**
111
+ * Check if text contains characters that break Hermes Runtime.evaluate.
112
+ * Standard accented Latin characters (Polish, Vietnamese, French, German, etc.)
113
+ * and Cyrillic work fine in Hermes. Only emoji and special Unicode ranges cause issues.
114
+ */
115
+ export function hasProblematicUnicode(text) {
116
+ const emojiPattern = /[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{FE00}-\u{FE0F}]|[\u{1F900}-\u{1F9FF}]|[\u{200D}]|[\u{20E3}]|[\u{E0020}-\u{E007F}]/u;
117
+ return emojiPattern.test(text);
118
+ }
119
+ function normalizeForMatch(text) {
120
+ return text.trim().replace(/\s+/g, " ").toLowerCase();
121
+ }
122
+ export function reconstructLinesFromWords(words) {
123
+ if (!words.length)
124
+ return [];
125
+ const sorted = [...words].sort((a, b) => a.bbox.y0 - b.bbox.y0);
126
+ const groups = [];
127
+ let current = [];
128
+ let runningMidY = -Infinity;
129
+ let runningHeight = 0;
130
+ for (const w of sorted) {
131
+ const wMid = (w.bbox.y0 + w.bbox.y1) / 2;
132
+ const wHeight = Math.max(1, w.bbox.y1 - w.bbox.y0);
133
+ const tolerance = Math.min(wHeight, runningHeight || wHeight) * 0.5;
134
+ if (current.length === 0 || Math.abs(wMid - runningMidY) <= tolerance) {
135
+ current.push(w);
136
+ const n = current.length;
137
+ runningMidY = runningMidY === -Infinity ? wMid : (runningMidY * (n - 1) + wMid) / n;
138
+ runningHeight = (runningHeight * (n - 1) + wHeight) / n;
139
+ }
140
+ else {
141
+ groups.push(current);
142
+ current = [w];
143
+ runningMidY = wMid;
144
+ runningHeight = wHeight;
145
+ }
146
+ }
147
+ if (current.length > 0)
148
+ groups.push(current);
149
+ return groups.map(group => {
150
+ const sortedWords = [...group].sort((a, b) => a.bbox.x0 - b.bbox.x0);
151
+ const text = sortedWords.map(w => w.text).join(" ");
152
+ const x0 = Math.min(...sortedWords.map(w => w.bbox.x0));
153
+ const y0 = Math.min(...sortedWords.map(w => w.bbox.y0));
154
+ const x1 = Math.max(...sortedWords.map(w => w.bbox.x1));
155
+ const y1 = Math.max(...sortedWords.map(w => w.bbox.y1));
156
+ const tapX = sortedWords.reduce((s, w) => s + w.tapCenter.x, 0) / sortedWords.length;
157
+ const tapY = sortedWords.reduce((s, w) => s + w.tapCenter.y, 0) / sortedWords.length;
158
+ return { text, bbox: { x0, y0, x1, y1 }, tapCenter: { x: tapX, y: tapY }, words: sortedWords };
159
+ });
160
+ }
161
+ /**
162
+ * Narrow the tap point to just the words that produced the substring match.
163
+ * Without this, a query for "Remotely" against a reconstructed line "Yes Remotely No"
164
+ * would tap at the line's centroid (middle of "Remotely" in this case, but easily wrong
165
+ * for asymmetric layouts). Tracks character offsets through the joined text so we know
166
+ * which constituent words contributed to the matched range.
167
+ */
168
+ function refineTapCenterToMatchingWords(line, normalizedNeedle) {
169
+ const normalizedFull = normalizeForMatch(line.text);
170
+ const startChar = normalizedFull.indexOf(normalizedNeedle);
171
+ if (startChar < 0)
172
+ return line.tapCenter;
173
+ const endChar = startChar + normalizedNeedle.length;
174
+ let offset = 0;
175
+ const covered = [];
176
+ for (const w of line.words) {
177
+ const wNorm = normalizeForMatch(w.text);
178
+ const wordStart = offset;
179
+ const wordEnd = offset + wNorm.length;
180
+ // Word overlaps the matched range
181
+ if (wordEnd > startChar && wordStart < endChar) {
182
+ covered.push(w);
183
+ }
184
+ offset = wordEnd + 1; // +1 for the joining space
185
+ }
186
+ if (covered.length === 0)
187
+ return line.tapCenter;
188
+ const tapX = covered.reduce((s, w) => s + w.tapCenter.x, 0) / covered.length;
189
+ const tapY = covered.reduce((s, w) => s + w.tapCenter.y, 0) / covered.length;
190
+ return { x: tapX, y: tapY };
191
+ }
192
+ export function findOcrMatch(ocrResult, query) {
193
+ const needle = normalizeForMatch(query);
194
+ if (!needle)
195
+ return null;
196
+ const words = ocrResult.words ?? [];
197
+ const lines = ocrResult.lines ?? [];
198
+ const exactWord = words.find((w) => normalizeForMatch(w.text) === needle);
199
+ if (exactWord)
200
+ return { text: exactWord.text, tapCenter: exactWord.tapCenter };
201
+ const exactLine = lines.find((l) => normalizeForMatch(l.text) === needle);
202
+ if (exactLine)
203
+ return { text: exactLine.text, tapCenter: exactLine.tapCenter };
204
+ const substringLine = lines.find((l) => normalizeForMatch(l.text).includes(needle));
205
+ if (substringLine)
206
+ return { text: substringLine.text, tapCenter: substringLine.tapCenter };
207
+ const substringWord = words.find((w) => normalizeForMatch(w.text).includes(needle));
208
+ if (substringWord)
209
+ return { text: substringWord.text, tapCenter: substringWord.tapCenter };
210
+ // Fall through to phrases reconstructed from word detections. Tap point is
211
+ // refined to only the words covering the matched substring, so a query for
212
+ // "Continue with Google" inside "[icon] Continue with Google" lands on the
213
+ // text run instead of the line centroid (and won't drift onto the icon).
214
+ const reconstructed = reconstructLinesFromWords(words);
215
+ const exactRecon = reconstructed.find((l) => normalizeForMatch(l.text) === needle);
216
+ if (exactRecon) {
217
+ return { text: exactRecon.text, tapCenter: refineTapCenterToMatchingWords(exactRecon, needle) };
218
+ }
219
+ const substringRecon = reconstructed.find((l) => normalizeForMatch(l.text).includes(needle));
220
+ if (substringRecon) {
221
+ return { text: substringRecon.text, tapCenter: refineTapCenterToMatchingWords(substringRecon, needle) };
222
+ }
223
+ return null;
224
+ }
225
+ export function getAvailableStrategies(query, strategy) {
226
+ if (query.x !== undefined && query.y !== undefined) {
227
+ return ["coordinate"];
228
+ }
229
+ if (strategy !== "auto") {
230
+ // Always fallback to OCR for text queries — explicit strategy may miss visible text
231
+ if (query.text && strategy !== "ocr" && strategy !== "coordinate") {
232
+ return [strategy, "ocr"];
233
+ }
234
+ return [strategy];
235
+ }
236
+ if (query.component && !query.text && !query.testID) {
237
+ return ["fiber"];
238
+ }
239
+ if (query.testID && !query.text) {
240
+ return ["accessibility", "fiber"];
241
+ }
242
+ if (query.text) {
243
+ const strategies = [];
244
+ strategies.push("accessibility");
245
+ if (!hasProblematicUnicode(query.text)) {
246
+ strategies.push("fiber");
247
+ }
248
+ strategies.push("ocr");
249
+ return strategies;
250
+ }
251
+ return ["fiber", "accessibility", "ocr"];
252
+ }
253
+ /**
254
+ * Convert screenshot image coordinates to platform-native tap coordinates.
255
+ *
256
+ * For iOS: screenshot pixels → device pixels (undo downscale) → points (÷ DPR)
257
+ * For Android: screenshot pixels → device pixels (undo downscale)
258
+ *
259
+ * IMPORTANT: Only use this for EXTERNAL coordinates from screenshots.
260
+ * Internal strategies (OCR, accessibility, fiber) produce tap-ready coordinates
261
+ * and call iosTap/androidTap directly — they must NOT go through this function.
262
+ */
263
+ export function convertScreenshotToTapCoords(pixelX, pixelY, platform, devicePixelRatio, scaleFactor = 1) {
264
+ const deviceX = pixelX * scaleFactor;
265
+ const deviceY = pixelY * scaleFactor;
266
+ if (platform === "android") {
267
+ return { x: Math.round(deviceX), y: Math.round(deviceY) };
268
+ }
269
+ return {
270
+ x: Math.round(deviceX / devicePixelRatio),
271
+ y: Math.round(deviceY / devicePixelRatio)
272
+ };
273
+ }
274
+ /** @deprecated Use convertScreenshotToTapCoords instead */
275
+ export const convertPixelsToPoints = convertScreenshotToTapCoords;
276
+ export async function getCurrentScreen() {
277
+ try {
278
+ // Note: Uses var instead of let/const because Hermes Runtime.evaluate
279
+ // sometimes has issues with block-scoped declarations
280
+ const expression = `(function() {
281
+ var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
282
+ if (!hook) return null;
283
+ var roots = [];
284
+ if (hook.getFiberRoots) {
285
+ hook.renderers.forEach(function(r, id) {
286
+ var fiberRoots = hook.getFiberRoots(id);
287
+ if (fiberRoots) fiberRoots.forEach(function(root) { roots.push(root); });
288
+ });
289
+ }
290
+ if (roots.length === 0) return null;
291
+
292
+ function findScreen(fiber, depth) {
293
+ if (!fiber || depth > 5000) return null;
294
+ var name = fiber.type && (fiber.type.displayName || fiber.type.name || (typeof fiber.type === 'string' ? fiber.type : null));
295
+
296
+ if (name === 'RNSScreen') {
297
+ var props = fiber.memoizedProps || {};
298
+ if (props['aria-hidden'] === true) return null;
299
+ var child = fiber.child;
300
+ while (child) {
301
+ var childName = child.type && (child.type.displayName || child.type.name);
302
+ if (childName && typeof child.type !== 'string' && childName !== 'RNSScreenContentWrapper') {
303
+ return childName;
304
+ }
305
+ child = child.child;
306
+ }
307
+ }
308
+
309
+ var child = fiber.child;
310
+ while (child) {
311
+ var found = findScreen(child, depth + 1);
312
+ if (found) return found;
313
+ child = child.sibling;
314
+ }
315
+ return null;
316
+ }
317
+
318
+ for (var i = 0; i < roots.length; i++) {
319
+ var root = roots[i].current;
320
+ var screen = findScreen(root, 0);
321
+ if (screen) return screen;
322
+ }
323
+
324
+ function findFirstUserComponent(fiber, depth) {
325
+ if (!fiber || depth > 5000) return null;
326
+ var name = fiber.type && (fiber.type.displayName || fiber.type.name);
327
+ if (name && typeof fiber.type !== 'string') return name;
328
+ var child = fiber.child;
329
+ while (child) {
330
+ var found = findFirstUserComponent(child, depth + 1);
331
+ if (found) return found;
332
+ child = child.sibling;
333
+ }
334
+ return null;
335
+ }
336
+
337
+ for (var i = 0; i < roots.length; i++) {
338
+ var fallback = findFirstUserComponent(roots[i].current, 0);
339
+ if (fallback) return fallback;
340
+ }
341
+ return null;
342
+ })()`;
343
+ const result = await executeInApp(expression, false, { originatingToolName: "tap" });
344
+ if (result.success && result.result && result.result !== "null" && result.result !== "undefined") {
345
+ return result.result.replace(/^"|"$/g, "");
346
+ }
347
+ return null;
348
+ }
349
+ catch {
350
+ return null;
351
+ }
352
+ }
353
+ export function formatTapSuccess(data) {
354
+ const { screenshot, verification, ...rest } = data;
355
+ // I3 (2026-05-16): when the tap landed on a TextInput-like element and the diff
356
+ // reports no visual change, that does NOT mean the focus failed. iOS simulators with
357
+ // the hardware keyboard connected (Cmd+K) suppress the on-screen keyboard, so the
358
+ // input is focused but the screen looks identical. Surface this so the agent doesn't
359
+ // assume the tap missed and retry the wrong thing.
360
+ const isTextInputComponent = (() => {
361
+ const name = data.component ?? data.pressed ?? "";
362
+ return /textfield|textinput|edittext|searchfield/i.test(name);
363
+ })();
364
+ const note = verification && !verification.skipped && verification.meaningful === false && isTextInputComponent
365
+ ? "TextInput focused but no visual change detected. If the simulator has a hardware keyboard connected (Cmd+K), the software keyboard is suppressed even though the input is focused — proceed with text entry via ios_input_text / android_input_text."
366
+ : undefined;
367
+ return {
368
+ success: true,
369
+ ...rest,
370
+ ...(verification && { verification }),
371
+ ...(screenshot && { screenshot }),
372
+ ...(note && { warning: note })
373
+ };
374
+ }
375
+ export function formatTapFailure(data) {
376
+ const errorMsg = data.error || buildErrorMessage(data.query);
377
+ const warning = data.verification && !data.verification.skipped && data.verification.meaningful === false
378
+ ? "Tap executed but no visual change detected. The element may not exist at these coordinates. Examine the screenshot to verify and retry with adjusted coordinates."
379
+ : undefined;
380
+ const lastStrategy = data.attempted.length > 0 ? data.attempted[data.attempted.length - 1].strategy : undefined;
381
+ return {
382
+ success: false,
383
+ method: lastStrategy,
384
+ query: data.query,
385
+ screen: data.screen,
386
+ error: errorMsg,
387
+ attempted: data.attempted,
388
+ suggestion: data.suggestion,
389
+ matches: data.matches,
390
+ ...(data.ambiguous && { ambiguous: true }),
391
+ ...(data.device && { device: data.device }),
392
+ ...(data.verification && { verification: data.verification }),
393
+ ...(data.screenshot && { screenshot: data.screenshot }),
394
+ ...(warning && { warning })
395
+ };
396
+ }
397
+ function buildErrorMessage(query) {
398
+ const parts = [];
399
+ if (query.text)
400
+ parts.push(`text="${query.text}"`);
401
+ if (query.testID)
402
+ parts.push(`testID="${query.testID}"`);
403
+ if (query.component)
404
+ parts.push(`component="${query.component}"`);
405
+ return `No element found matching ${parts.join(", ")}`;
406
+ }
407
+ // I2 (2026-05-16): RN component composition routinely produces N matches at the same
408
+ // coordinates for the same testID/text (e.g. ThemedButton → TouchableOpacity → TouchableOpacity).
409
+ // The C1 ambiguity guard refuses all such taps and demands `index=`. That's correct in the
410
+ // spatial-ambiguity case (two distinct buttons share a label) but wrong in the
411
+ // wrapper-ambiguity case — there's only one logical button, just stacked components.
412
+ //
413
+ // Collapse matches that share the same (testID, text) and overlap geometrically
414
+ // (centers within TOLERANCE_PX). Keeps the first occurrence — fiber/a11y walks parent-first,
415
+ // so the outermost wrapper survives (its onPress almost always proxies to inner handlers).
416
+ // Spatial ambiguity (different x/y) is preserved and continues to surface the C1 refusal.
417
+ function collapseGeometricallyEquivalentMatches(matches, getCenter, getKey) {
418
+ if (matches.length <= 1)
419
+ return matches;
420
+ const TOLERANCE_PX = 2;
421
+ const out = [];
422
+ for (const m of matches) {
423
+ const mc = getCenter(m);
424
+ const mk = getKey(m);
425
+ const isDuplicate = out.some(o => {
426
+ if (getKey(o) !== mk)
427
+ return false;
428
+ const oc = getCenter(o);
429
+ return Math.abs((mc.x ?? 0) - (oc.x ?? 0)) <= TOLERANCE_PX &&
430
+ Math.abs((mc.y ?? 0) - (oc.y ?? 0)) <= TOLERANCE_PX;
431
+ });
432
+ if (!isDuplicate)
433
+ out.push(m);
434
+ }
435
+ return out;
436
+ }
437
+ export function makeEmptyEvidenceSink() {
438
+ return {
439
+ fiber: { ran: false, durationMs: 0, metroConnected: false, pressables: [] },
440
+ accessibility: { ran: false, durationMs: 0, elements: [] },
441
+ ocr: { ran: false, durationMs: 0, detections: [], closestMatch: null, bestCandidate: null }
442
+ };
443
+ }
444
+ function ocrSimilarity(a, b) {
445
+ if (!a || !b)
446
+ return 0;
447
+ if (a === b)
448
+ return 1;
449
+ // Dice coefficient over character bigrams (case/space-insensitive)
450
+ const bigrams = (s) => {
451
+ const m = new Map();
452
+ if (s.length < 2) {
453
+ if (s)
454
+ m.set(s, 1);
455
+ return m;
456
+ }
457
+ for (let i = 0; i < s.length - 1; i++) {
458
+ const bg = s.slice(i, i + 2);
459
+ m.set(bg, (m.get(bg) ?? 0) + 1);
460
+ }
461
+ return m;
462
+ };
463
+ const ag = bigrams(a);
464
+ const bg = bigrams(b);
465
+ let intersection = 0;
466
+ for (const [k, va] of ag) {
467
+ const vb = bg.get(k);
468
+ if (vb)
469
+ intersection += Math.min(va, vb);
470
+ }
471
+ const total = Array.from(ag.values()).reduce((s, v) => s + v, 0)
472
+ + Array.from(bg.values()).reduce((s, v) => s + v, 0);
473
+ if (!total)
474
+ return 0;
475
+ return (2 * intersection) / total;
476
+ }
477
+ export function findClosestOcrText(ocrResult, query) {
478
+ if (!query)
479
+ return null;
480
+ const needle = normalizeForMatch(query);
481
+ if (!needle)
482
+ return null;
483
+ const candidates = [];
484
+ if (ocrResult?.words?.length)
485
+ candidates.push(...ocrResult.words.map(w => ({ text: w.text })));
486
+ if (ocrResult?.lines?.length)
487
+ candidates.push(...ocrResult.lines.map(l => ({ text: l.text })));
488
+ // Include reconstructed phrases so the diagnostic surfaces "Continue with Google@1.00"
489
+ // instead of "Continue@0.54" when the engine fragmented a multi-word phrase into
490
+ // separate word detections.
491
+ if (ocrResult?.words?.length) {
492
+ candidates.push(...reconstructLinesFromWords(ocrResult.words).map(l => ({ text: l.text })));
493
+ }
494
+ if (!candidates.length)
495
+ return null;
496
+ let best = null;
497
+ for (const c of candidates) {
498
+ const norm = normalizeForMatch(c.text);
499
+ if (!norm)
500
+ continue;
501
+ const score = ocrSimilarity(needle, norm);
502
+ if (!best || score > best.score)
503
+ best = { text: c.text, score };
504
+ }
505
+ return best;
506
+ }
507
+ // --- Strategy Functions ---
508
+ async function tryFiberStrategy(query, index, maxTraversalDepth, sink, device) {
509
+ if (sink) {
510
+ sink.fiber.ran = true;
511
+ sink.fiber.metroConnected = connectedApps.size > 0;
512
+ }
513
+ const startedAt = Date.now();
514
+ try {
515
+ // Retry with increasing depth if the initial traversal finds nothing
516
+ const baseDepth = maxTraversalDepth ?? 15;
517
+ const depthAttempts = [baseDepth];
518
+ // Only add deeper retries if user didn't explicitly set a high depth
519
+ if (baseDepth <= 15) {
520
+ depthAttempts.push(30, 45);
521
+ }
522
+ else if (baseDepth <= 30) {
523
+ depthAttempts.push(baseDepth * 2);
524
+ }
525
+ let lastResult = null;
526
+ for (const depth of depthAttempts) {
527
+ const result = await tryFiberAtDepth(query, index, depth, device);
528
+ if (sink && result.matches?.length) {
529
+ sink.fiber.pressables = result.matches.slice(0, 50).map(m => ({
530
+ label: m.text || undefined,
531
+ testID: m.testID ?? undefined,
532
+ componentName: m.component,
533
+ bounds: (m.x !== undefined && m.y !== undefined)
534
+ ? { x: m.x, y: m.y, width: 0, height: 0 }
535
+ : undefined
536
+ }));
537
+ }
538
+ if (result.success || result.matches) {
539
+ return result;
540
+ }
541
+ lastResult = result;
542
+ }
543
+ return lastResult;
544
+ }
545
+ finally {
546
+ if (sink)
547
+ sink.fiber.durationMs = Date.now() - startedAt;
548
+ }
549
+ }
550
+ async function tryFiberAtDepth(query, index, maxTraversalDepth, device) {
551
+ try {
552
+ const result = await pressElement({
553
+ text: query.text,
554
+ testID: query.testID,
555
+ component: query.component,
556
+ index,
557
+ maxTraversalDepth,
558
+ device
559
+ });
560
+ if (!result.success) {
561
+ return { success: false, reason: result.error || "pressElement failed" };
562
+ }
563
+ if (!result.result) {
564
+ return { success: false, reason: "No result from pressElement" };
565
+ }
566
+ const parsed = JSON.parse(result.result);
567
+ if (parsed.error) {
568
+ const strategyResult = {
569
+ success: false,
570
+ reason: parsed.error
571
+ };
572
+ if (parsed.matches) {
573
+ strategyResult.matches = parsed.matches;
574
+ }
575
+ return strategyResult;
576
+ }
577
+ // Fiber finds the element by text/testID/component, then measures its
578
+ // host component's screen position for a native tap. This ensures the tap
579
+ // goes through React's event pipeline, executing any onPress wrappers
580
+ // (analytics, debouncing, state tracking) inside the component.
581
+ if (parsed.needsNativeTap) {
582
+ // Ambiguity guard: if multiple elements match and the caller didn't
583
+ // specify an explicit index, refuse to tap and surface the full list
584
+ // so the agent can pick the right one.
585
+ if ((parsed.totalMatches ?? 1) > 1 && index === undefined) {
586
+ // I2: collapse geometrically-identical wrapper matches before refusing.
587
+ const collapsed = collapseGeometricallyEquivalentMatches(parsed.allMatches ?? [], (m) => ({ x: m.x, y: m.y }), (m) => `${m.testID ?? ""}::${m.text ?? ""}`);
588
+ if (collapsed.length > 1) {
589
+ return {
590
+ success: false,
591
+ reason: `Ambiguous: ${collapsed.length} elements match this query — use index= to pick one`,
592
+ matches: collapsed,
593
+ ambiguous: true
594
+ };
595
+ }
596
+ // Collapsed to one logical element — fall through and tap. The JS-side
597
+ // search already picked allMatches[0] (the outermost wrapper) for
598
+ // parsed.pressed/parsed.nativeTapTarget.
599
+ }
600
+ const elementType = parsed.isInput ? "input element" : "pressable element";
601
+ if (parsed.nativeTapTarget && parsed.nativeTapTarget.x && parsed.nativeTapTarget.y) {
602
+ return {
603
+ success: false,
604
+ reason: `Found ${parsed.pressed} (${elementType}) — measured coordinates for native tap`,
605
+ pressed: parsed.pressed,
606
+ text: parsed.text,
607
+ path: parsed.path || null,
608
+ component: parsed.pressed || null,
609
+ convertedTo: {
610
+ x: parsed.nativeTapTarget.x,
611
+ y: parsed.nativeTapTarget.y,
612
+ unit: parsed.nativeTapTarget.unit || "points"
613
+ }
614
+ };
615
+ }
616
+ return {
617
+ success: false,
618
+ reason: `Found ${parsed.pressed} (${elementType}) but could not measure coordinates — falling through to next strategy`
619
+ };
620
+ }
621
+ // All elements now use needsNativeTap — this shouldn't be reached
622
+ return {
623
+ success: false,
624
+ reason: "Unexpected: element did not request native tap"
625
+ };
626
+ }
627
+ catch (err) {
628
+ return {
629
+ success: false,
630
+ reason: `Fiber strategy error: ${err instanceof Error ? err.message : String(err)}`
631
+ };
632
+ }
633
+ }
634
+ async function tryAccessibilityStrategy(query, index, platform, udid, sink, signal) {
635
+ if (sink)
636
+ sink.accessibility.ran = true;
637
+ const startedAt = Date.now();
638
+ try {
639
+ const hasTestID = !!query.testID;
640
+ const hasText = !!query.text;
641
+ if (!hasTestID && !hasText) {
642
+ return {
643
+ success: false,
644
+ reason: "No text or testID for accessibility search"
645
+ };
646
+ }
647
+ if (platform === "ios") {
648
+ // iOS: testID maps to accessibilityIdentifier — search by identifier first,
649
+ // then fall back to labelContains for text-based searches
650
+ let result;
651
+ if (hasTestID && !hasText) {
652
+ // Try exact identifier match first (testID → accessibilityIdentifier)
653
+ result = await iosFindElement({
654
+ identifier: query.testID,
655
+ index
656
+ }, udid);
657
+ // Fall back to identifierContains if exact match fails
658
+ if (!result.success || !result.allMatches || result.allMatches.length === 0) {
659
+ result = await iosFindElement({
660
+ identifierContains: query.testID,
661
+ index
662
+ }, udid);
663
+ }
664
+ // Last resort: try labelContains in case testID is reflected in label
665
+ if (!result.success || !result.allMatches || result.allMatches.length === 0) {
666
+ result = await iosFindElement({
667
+ labelContains: query.testID,
668
+ index
669
+ }, udid);
670
+ }
671
+ }
672
+ else {
673
+ const searchText = query.text;
674
+ // Exact label match first to avoid matching dialog titles like "Clear All"
675
+ // when the query is a single word that happens to be a substring ("Clear").
676
+ result = await iosFindElement({
677
+ label: searchText,
678
+ index
679
+ }, udid);
680
+ if (!result.success || !result.allMatches || result.allMatches.length === 0) {
681
+ result = await iosFindElement({
682
+ labelContains: searchText,
683
+ index
684
+ }, udid);
685
+ }
686
+ }
687
+ if (sink && result.allMatches?.length) {
688
+ sink.accessibility.elements = result.allMatches.slice(0, 50).map(el => ({
689
+ label: el.label || undefined,
690
+ testID: el.identifier || undefined,
691
+ frame: el.frame
692
+ ? { x: el.frame.x, y: el.frame.y, width: el.frame.width, height: el.frame.height }
693
+ : undefined
694
+ }));
695
+ }
696
+ if (!result.success || !result.allMatches || result.allMatches.length === 0) {
697
+ return {
698
+ success: false,
699
+ reason: result.error ?? "No iOS accessibility match"
700
+ };
701
+ }
702
+ // Ambiguity guard: when the predicate matches multiple a11y elements
703
+ // (e.g. text="My Events" hits both the header title AND the bottom tab),
704
+ // refuse to tap without an explicit index — picking allMatches[0] silently
705
+ // landed on non-interactive header elements in production telemetry.
706
+ // Mirrors the fiber-strategy guard further up the file.
707
+ // I2: collapse geometrically-identical wrapper matches before refusing.
708
+ const iosEffectiveMatches = result.allMatches.length > 1
709
+ ? collapseGeometricallyEquivalentMatches(result.allMatches, (m) => ({ x: m.center?.x, y: m.center?.y }), (m) => `${m.identifier ?? ""}::${m.label ?? ""}`)
710
+ : result.allMatches;
711
+ if (iosEffectiveMatches.length > 1 && index === undefined) {
712
+ return {
713
+ success: false,
714
+ reason: `Ambiguous: ${iosEffectiveMatches.length} elements match this query — use index= to pick one`,
715
+ matches: iosEffectiveMatches.map((m, i) => ({
716
+ index: i,
717
+ component: m.type ?? "",
718
+ text: m.label ?? "",
719
+ testID: m.identifier ?? null,
720
+ x: m.center?.x,
721
+ y: m.center?.y
722
+ })),
723
+ ambiguous: true
724
+ };
725
+ }
726
+ const match = iosEffectiveMatches[index ?? 0];
727
+ if (!match) {
728
+ return {
729
+ success: false,
730
+ reason: `Index ${index} out of bounds (${iosEffectiveMatches.length} matches)`
731
+ };
732
+ }
733
+ await iosTap(match.center.x, match.center.y, { udid });
734
+ return {
735
+ success: true,
736
+ reason: "Tapped via iOS accessibility",
737
+ pressed: match.label || match.type,
738
+ text: match.label || undefined,
739
+ component: match.type || null,
740
+ convertedTo: { x: match.center.x, y: match.center.y, unit: "points" }
741
+ };
742
+ }
743
+ else {
744
+ // Android: testID maps to resource-id, text maps to text content
745
+ const searchOptions = { index };
746
+ if (hasTestID && !hasText) {
747
+ searchOptions.resourceId = query.testID;
748
+ }
749
+ else if (hasText) {
750
+ searchOptions.textContains = query.text;
751
+ }
752
+ let result = await androidFindElement(searchOptions, undefined, signal);
753
+ // If testID search via resourceId failed, try contentDescContains
754
+ // (older RN versions map testID to content-description)
755
+ if (hasTestID && !hasText && (!result.success || !result.allMatches || result.allMatches.length === 0)) {
756
+ result = await androidFindElement({
757
+ contentDescContains: query.testID,
758
+ index
759
+ }, undefined, signal);
760
+ }
761
+ if (sink && result.allMatches?.length) {
762
+ sink.accessibility.elements = result.allMatches.slice(0, 50).map(el => ({
763
+ label: el.text || el.contentDesc || undefined,
764
+ testID: el.resourceId || undefined,
765
+ frame: el.bounds
766
+ ? { x: el.bounds.left, y: el.bounds.top, width: el.bounds.width, height: el.bounds.height }
767
+ : undefined
768
+ }));
769
+ }
770
+ if (!result.success || !result.allMatches || result.allMatches.length === 0) {
771
+ return {
772
+ success: false,
773
+ reason: result.error ?? "No Android accessibility match"
774
+ };
775
+ }
776
+ // Ambiguity guard — see iOS branch above for rationale.
777
+ // I2: collapse geometrically-identical wrapper matches before refusing.
778
+ const androidEffectiveMatches = result.allMatches.length > 1
779
+ ? collapseGeometricallyEquivalentMatches(result.allMatches, (m) => ({ x: m.center?.x, y: m.center?.y }), (m) => `${m.resourceId ?? ""}::${m.text ?? m.contentDesc ?? ""}`)
780
+ : result.allMatches;
781
+ if (androidEffectiveMatches.length > 1 && index === undefined) {
782
+ return {
783
+ success: false,
784
+ reason: `Ambiguous: ${androidEffectiveMatches.length} elements match this query — use index= to pick one`,
785
+ matches: androidEffectiveMatches.map((m, i) => ({
786
+ index: i,
787
+ component: m.className ?? "",
788
+ text: m.text ?? m.contentDesc ?? "",
789
+ testID: m.resourceId ?? null,
790
+ x: m.center?.x,
791
+ y: m.center?.y
792
+ })),
793
+ ambiguous: true
794
+ };
795
+ }
796
+ const match = androidEffectiveMatches[index ?? 0];
797
+ if (!match) {
798
+ return {
799
+ success: false,
800
+ reason: `Index ${index} out of bounds (${androidEffectiveMatches.length} matches)`
801
+ };
802
+ }
803
+ await androidTap(match.center.x, match.center.y);
804
+ return {
805
+ success: true,
806
+ reason: "Tapped via Android accessibility",
807
+ pressed: match.text || match.className || undefined,
808
+ text: match.text || undefined,
809
+ component: match.className || undefined,
810
+ convertedTo: { x: match.center.x, y: match.center.y, unit: "pixels" }
811
+ };
812
+ }
813
+ }
814
+ catch (err) {
815
+ return {
816
+ success: false,
817
+ reason: `Accessibility strategy error: ${err instanceof Error ? err.message : String(err)}`
818
+ };
819
+ }
820
+ finally {
821
+ if (sink)
822
+ sink.accessibility.durationMs = Date.now() - startedAt;
823
+ }
824
+ }
825
+ /**
826
+ * Run OCR sense (capture + recognize + match) and tap the match. Also records
827
+ * `sink.ocr.bestCandidate` (matched text + tap coords) into the evidence sink,
828
+ * which is serialized into the R2 failure bundle for diagnostics.
829
+ */
830
+ async function tryOcrStrategy(query, platform, udid, sink, signal) {
831
+ if (sink)
832
+ sink.ocr.ran = true;
833
+ const ocrStartedAt = Date.now();
834
+ try {
835
+ const searchText = query.text;
836
+ if (!searchText) {
837
+ return { success: false, reason: "OCR strategy requires text query" };
838
+ }
839
+ let imageBuffer;
840
+ let scaleFactor = 1;
841
+ if (platform === "ios") {
842
+ const screenshot = await iosScreenshot(undefined, udid);
843
+ if (!screenshot.success || !screenshot.data) {
844
+ return {
845
+ success: false,
846
+ reason: "Failed to capture iOS screenshot for OCR"
847
+ };
848
+ }
849
+ imageBuffer = screenshot.data;
850
+ scaleFactor = screenshot.scaleFactor ?? 1;
851
+ }
852
+ else {
853
+ const { androidScreenshot } = await import("../core/android.js");
854
+ const screenshot = await androidScreenshot(undefined, undefined, signal);
855
+ if (!screenshot.success || !screenshot.data) {
856
+ return {
857
+ success: false,
858
+ reason: "Failed to capture Android screenshot for OCR"
859
+ };
860
+ }
861
+ imageBuffer = screenshot.data;
862
+ scaleFactor = screenshot.scaleFactor ?? 1;
863
+ }
864
+ const { recognizeText } = await import("../core/ocr.js");
865
+ const ocrResult = await recognizeText(imageBuffer, {
866
+ scaleFactor,
867
+ platform,
868
+ signal
869
+ });
870
+ if (sink && ocrResult) {
871
+ const allDetections = [
872
+ ...(ocrResult.words ?? []),
873
+ ...(ocrResult.lines ?? [])
874
+ ];
875
+ sink.ocr.detections = allDetections.slice(0, 100).map(r => ({
876
+ text: r.text,
877
+ bbox: [r.bbox.x0, r.bbox.y0, r.bbox.x1 - r.bbox.x0, r.bbox.y1 - r.bbox.y0],
878
+ conf: r.confidence ?? 0
879
+ }));
880
+ sink.ocr.closestMatch = findClosestOcrText(ocrResult, searchText);
881
+ }
882
+ const match = findOcrMatch(ocrResult, searchText);
883
+ // Record the best candidate (matched text + tap coords) into the
884
+ // evidence sink. This is serialized into the R2 failure bundle and is
885
+ // useful when diagnosing OCR taps (e.g. OCR found the match at these
886
+ // coords but the tap didn't register a visual change).
887
+ if (sink && match) {
888
+ const closest = sink.ocr.closestMatch;
889
+ sink.ocr.bestCandidate = {
890
+ text: match.text,
891
+ score: closest && closest.text === match.text ? closest.score : 1,
892
+ tapCenter: { x: match.tapCenter.x, y: match.tapCenter.y },
893
+ scaleFactor
894
+ };
895
+ }
896
+ if (!match) {
897
+ return {
898
+ success: false,
899
+ reason: `OCR did not find text "${searchText}" on screen`
900
+ };
901
+ }
902
+ if (platform === "ios") {
903
+ // tapCenter is in image-pixel space (downscaled) — convert to points
904
+ const { getDevicePixelRatio } = await import("../core/ios.js");
905
+ const dpr = await getDevicePixelRatio(udid);
906
+ const tapResult = await iosTap(Math.round((match.tapCenter.x * scaleFactor) / dpr), Math.round((match.tapCenter.y * scaleFactor) / dpr), { udid });
907
+ if (!tapResult.success) {
908
+ return {
909
+ success: false,
910
+ reason: `OCR found "${match.text}" but tap failed: ${tapResult.error}`
911
+ };
912
+ }
913
+ }
914
+ else {
915
+ // Android: image-pixel → device-pixel (undo downscale), ADB accepts pixels
916
+ await androidTap(Math.round(match.tapCenter.x * scaleFactor), Math.round(match.tapCenter.y * scaleFactor));
917
+ }
918
+ return {
919
+ success: true,
920
+ reason: "Tapped via OCR text recognition",
921
+ text: match.text,
922
+ convertedTo: {
923
+ x: match.tapCenter.x,
924
+ y: match.tapCenter.y,
925
+ unit: "pixels"
926
+ }
927
+ };
928
+ }
929
+ catch (err) {
930
+ return {
931
+ success: false,
932
+ reason: `OCR strategy error: ${err instanceof Error ? err.message : String(err)}`
933
+ };
934
+ }
935
+ finally {
936
+ if (sink)
937
+ sink.ocr.durationMs = Date.now() - ocrStartedAt;
938
+ }
939
+ }
940
+ async function tryCoordinateStrategy(pixelX, pixelY, platform, lastScreenshot, udid) {
941
+ try {
942
+ if (platform === "ios") {
943
+ const scaleFactor = lastScreenshot?.scaleFactor ?? 1;
944
+ const { getDevicePixelRatio } = await import("../core/ios.js");
945
+ const devicePixelRatio = await getDevicePixelRatio(udid);
946
+ const converted = convertScreenshotToTapCoords(pixelX, pixelY, "ios", devicePixelRatio, scaleFactor);
947
+ const tapResult = await iosTap(converted.x, converted.y, { udid });
948
+ if (!tapResult.success) {
949
+ return {
950
+ success: false,
951
+ reason: `Coordinate tap failed: ${tapResult.error}`
952
+ };
953
+ }
954
+ // Best-effort: identify what was tapped via fiber tree
955
+ let screen = null;
956
+ try {
957
+ screen = await getCurrentScreen();
958
+ }
959
+ catch {
960
+ // Inspection failure is non-fatal
961
+ }
962
+ return {
963
+ success: true,
964
+ reason: "Tapped at coordinates (iOS)",
965
+ screen,
966
+ convertedTo: { x: converted.x, y: converted.y, unit: "points" }
967
+ };
968
+ }
969
+ else {
970
+ const scaleFactor = lastScreenshot?.scaleFactor ?? 1;
971
+ const converted = convertScreenshotToTapCoords(pixelX, pixelY, "android", 1, scaleFactor);
972
+ await androidTap(converted.x, converted.y);
973
+ // Best-effort: identify what was tapped via fiber tree
974
+ let screen = null;
975
+ try {
976
+ screen = await getCurrentScreen();
977
+ }
978
+ catch {
979
+ // Inspection failure is non-fatal
980
+ }
981
+ return {
982
+ success: true,
983
+ reason: "Tapped at coordinates (Android)",
984
+ screen,
985
+ convertedTo: { x: converted.x, y: converted.y, unit: "pixels" }
986
+ };
987
+ }
988
+ }
989
+ catch (err) {
990
+ return {
991
+ success: false,
992
+ reason: `Coordinate strategy error: ${err instanceof Error ? err.message : String(err)}`
993
+ };
994
+ }
995
+ }
996
+ const TAP_TIMEOUT_MS = 25000;
997
+ const MIN_STRATEGY_BUDGET_MS = 500;
998
+ // Per-strategy budget. OCR cap on Android is bumped via maxStrategyMs() because
999
+ // the ADB screencap+pull leg has ~2s variance on real devices; iOS stays at 5s
1000
+ // where xcrun simctl screenshot is consistent.
1001
+ // Heavy strategies (fiber on deep trees with multi-depth retries, axe accessibility
1002
+ // dumps on dense iOS screens) need more headroom — previous caps produced spurious
1003
+ // timeouts that the agent read as "element missing" when the strategy simply didn't
1004
+ // get to finish. The overall TAP_TIMEOUT_MS budget still bounds the worst case.
1005
+ // Coordinate looks "light" (one tap subprocess) but on iOS it pairs an axe/idb
1006
+ // invocation with a CDP getDevicePixelRatio + best-effort fiber inspection;
1007
+ // the 3000ms cap surfaced as spurious "coordinate timed out" failures while
1008
+ // 20+ seconds of the overall budget were still unused. Align with the other
1009
+ // strategies — the global TAP_TIMEOUT_MS still bounds the worst case.
1010
+ const MAX_STRATEGY_MS = {
1011
+ fiber: 8000,
1012
+ accessibility: 6000,
1013
+ ocr: 6000,
1014
+ coordinate: 8000
1015
+ };
1016
+ function maxStrategyMs(strategy, platform) {
1017
+ if (strategy === "ocr" && platform === "android")
1018
+ return 9000;
1019
+ return MAX_STRATEGY_MS[strategy] ?? 5000;
1020
+ }
1021
+ // Matches only the outer withTimeout wrapper message for a tap strategy.
1022
+ // Nested sub-operation errors inside a strategy (e.g. "CDP getProperties timed out after 150ms")
1023
+ // must NOT be classified as a tap-level timeout.
1024
+ const STRATEGY_TIMEOUT_RE = /^(fiber|accessibility|ocr|coordinate) timed out after \d+ms$/;
1025
+ export function isTapTimeout(attempted) {
1026
+ return attempted.some((a) => a.outcome === "timeout" || a.outcome === "skipped" || STRATEGY_TIMEOUT_RE.test(a.reason) || a.reason.startsWith("Skipped —"));
1027
+ }
1028
+ function withTimeout(promise, ms, label) {
1029
+ return new Promise((resolve, reject) => {
1030
+ const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
1031
+ promise.then((val) => {
1032
+ clearTimeout(timer);
1033
+ resolve(val);
1034
+ }, (err) => {
1035
+ clearTimeout(timer);
1036
+ reject(err);
1037
+ });
1038
+ });
1039
+ }
1040
+ /**
1041
+ * Like withTimeout, but creates an AbortController whose signal is handed to
1042
+ * the inner factory. On timeout the signal aborts so subprocess work
1043
+ * (uiautomator dump, OCR fetch, screencap) can be killed instead of running
1044
+ * past the strategy cap and bleeding into total tap duration.
1045
+ */
1046
+ function withCancelableTimeout(make, ms, label) {
1047
+ const ctrl = new AbortController();
1048
+ const timer = setTimeout(() => ctrl.abort(), ms);
1049
+ return make(ctrl.signal).then((val) => { clearTimeout(timer); return val; }, (err) => {
1050
+ clearTimeout(timer);
1051
+ if (ctrl.signal.aborted)
1052
+ throw new Error(`${label} timed out after ${ms}ms`);
1053
+ throw err;
1054
+ });
1055
+ }
1056
+ /**
1057
+ * Compute marker coordinates in screenshot-pixel space (what the returned PNG uses).
1058
+ * Unit rules by strategy, assuming screenshotScale = downscale factor:
1059
+ * coordinate : input is already screenshot pixels → pass through
1060
+ * ocr : match.tapCenter is image-pixel (= screenshot-pixel) → pass through
1061
+ * iOS points : point * DPR / screenshotScale
1062
+ * android dp : dp * densityScale / screenshotScale
1063
+ * android devicePx : devicePx / screenshotScale
1064
+ */
1065
+ function computeMarkerPx(args) {
1066
+ const { strategy, input, convertedTo, platform, screenshotScale, devicePixelRatio, androidDensityScale } = args;
1067
+ const scale = screenshotScale || 1;
1068
+ if (strategy === "coordinate" || strategy === "native-coordinate") {
1069
+ return input ? { x: input.x, y: input.y } : undefined;
1070
+ }
1071
+ if (!convertedTo)
1072
+ return undefined;
1073
+ if (strategy === "ocr") {
1074
+ return { x: Math.round(convertedTo.x), y: Math.round(convertedTo.y) };
1075
+ }
1076
+ if (platform === "ios") {
1077
+ // fiber/accessibility on iOS return points
1078
+ const dpr = devicePixelRatio || 3;
1079
+ return {
1080
+ x: Math.round((convertedTo.x * dpr) / scale),
1081
+ y: Math.round((convertedTo.y * dpr) / scale)
1082
+ };
1083
+ }
1084
+ // Android
1085
+ if (strategy === "fiber+native" && androidDensityScale) {
1086
+ // Fabric fiber returns dp; fiber+native path scales to device pixels before tap
1087
+ return {
1088
+ x: Math.round((convertedTo.x * androidDensityScale) / scale),
1089
+ y: Math.round((convertedTo.y * androidDensityScale) / scale)
1090
+ };
1091
+ }
1092
+ // accessibility android: convertedTo is device pixels
1093
+ return {
1094
+ x: Math.round(convertedTo.x / scale),
1095
+ y: Math.round(convertedTo.y / scale)
1096
+ };
1097
+ }
1098
+ // D2 (Step 6): in-memory ring buffer of recent coord-strategy artifact keys
1099
+ // `${round(x/20)}:${round(y/20)}`. Entries expire after 60s. The first
1100
+ // failed coord tap at a grid cell uploads its artifact; retries within the
1101
+ // window get suppressed.
1102
+ const recentCoordArtifacts = new Map();
1103
+ const COORD_DEDUP_WINDOW_MS = 60_000;
1104
+ const COORD_DEDUP_BUCKET = 20;
1105
+ function shouldDedupArtifact(ctx) {
1106
+ // Only applies to coordinate-strategy artifacts where we have x,y coords.
1107
+ const winningStrat = ctx.attempted.find(a => a.reason === "success")?.strategy
1108
+ ?? ctx.attempted[ctx.attempted.length - 1]?.strategy;
1109
+ if (winningStrat !== "coordinate")
1110
+ return false;
1111
+ const x = ctx.chosenTapPoint?.x ?? ctx.query.x;
1112
+ const y = ctx.chosenTapPoint?.y ?? ctx.query.y;
1113
+ if (typeof x !== "number" || typeof y !== "number")
1114
+ return false;
1115
+ const key = `${Math.round(x / COORD_DEDUP_BUCKET)}:${Math.round(y / COORD_DEDUP_BUCKET)}`;
1116
+ const now = Date.now();
1117
+ // Sweep stale entries opportunistically — keeps the map from growing.
1118
+ for (const [k, ts] of recentCoordArtifacts) {
1119
+ if (now - ts > COORD_DEDUP_WINDOW_MS)
1120
+ recentCoordArtifacts.delete(k);
1121
+ }
1122
+ const last = recentCoordArtifacts.get(key);
1123
+ if (last !== undefined && (now - last) < COORD_DEDUP_WINDOW_MS)
1124
+ return true;
1125
+ recentCoordArtifacts.set(key, now);
1126
+ return false;
1127
+ }
1128
+ /** Test-only: drop the dedup state so a fresh tap always uploads. */
1129
+ export function resetCoordArtifactDedup() {
1130
+ recentCoordArtifacts.clear();
1131
+ }
1132
+ async function captureTapArtifact(ctx) {
1133
+ try {
1134
+ const { getServerVersion, categorizeError } = await import("../core/telemetry.js");
1135
+ const strategyChain = ctx.attempted.map(a => `${a.strategy}:${a.reason.slice(0, 40)}`).join("|");
1136
+ // Resolve error category up-front so we can short-circuit driver-missing failures.
1137
+ // These aren't tap-tool bugs — they're host setup problems (no idb/axe/adb on PATH).
1138
+ // Skipping the artifact upload saves R2 storage and stops these from polluting the
1139
+ // failure dashboard, which is the signal we use to triage genuine tap regressions.
1140
+ const errorCategory = ctx.errorCategory
1141
+ ?? categorizeError(ctx.errorMessage ?? "", strategyChain);
1142
+ if (errorCategory === "driver_missing") {
1143
+ return undefined;
1144
+ }
1145
+ // D1 (Step 6): no-metro unmeaningful taps have no diagnostic signal
1146
+ // beyond a screenshot diff that's already below threshold by definition.
1147
+ // ~50% of recent failure-artifact rows were these — they crowd out real
1148
+ // failures in the dashboard. Telemetry row still flows; just no R2 bundle.
1149
+ const noMetro = ctx.evidence ? !ctx.evidence.fiber.metroConnected : connectedApps.size === 0;
1150
+ if (ctx.outcome === "unmeaningful" && noMetro) {
1151
+ return undefined;
1152
+ }
1153
+ // D2 (Step 6): retry deduplication — coord-strategy taps at the same
1154
+ // grid cell within 60s upload the same near-identical artifact. The
1155
+ // first capture is enough; subsequent ones are noise.
1156
+ if (shouldDedupArtifact(ctx)) {
1157
+ return undefined;
1158
+ }
1159
+ const result = await captureFailureArtifact({
1160
+ outcome: ctx.outcome,
1161
+ predicate: ctx.query,
1162
+ errorMessage: ctx.errorMessage,
1163
+ errorCategory,
1164
+ strategyChain,
1165
+ sessionId: "",
1166
+ version: getServerVersion(),
1167
+ changeRate: ctx.verification?.changeRate,
1168
+ meaningful: ctx.verification?.meaningful,
1169
+ senses: ctx.evidence
1170
+ ? {
1171
+ ocr: {
1172
+ ran: ctx.evidence.ocr.ran,
1173
+ durationMs: ctx.evidence.ocr.durationMs,
1174
+ detections: ctx.evidence.ocr.detections,
1175
+ closestMatch: ctx.evidence.ocr.closestMatch
1176
+ },
1177
+ fiber: {
1178
+ ran: ctx.evidence.fiber.ran,
1179
+ durationMs: ctx.evidence.fiber.durationMs,
1180
+ metroConnected: ctx.evidence.fiber.metroConnected,
1181
+ pressables: ctx.evidence.fiber.pressables.map(p => ({
1182
+ label: p.label,
1183
+ testID: p.testID,
1184
+ componentName: p.componentName,
1185
+ bounds: p.bounds
1186
+ ? [p.bounds.x, p.bounds.y, p.bounds.width, p.bounds.height]
1187
+ : undefined
1188
+ }))
1189
+ },
1190
+ accessibility: {
1191
+ ran: ctx.evidence.accessibility.ran,
1192
+ durationMs: ctx.evidence.accessibility.durationMs,
1193
+ elements: ctx.evidence.accessibility.elements.map(el => ({
1194
+ label: el.label,
1195
+ testID: el.testID,
1196
+ frame: el.frame
1197
+ ? [el.frame.x, el.frame.y, el.frame.width, el.frame.height]
1198
+ : undefined
1199
+ }))
1200
+ }
1201
+ }
1202
+ : {
1203
+ ocr: { ran: false, durationMs: 0, detections: [], closestMatch: null },
1204
+ fiber: {
1205
+ ran: ctx.attempted.some(a => a.strategy === "fiber"),
1206
+ durationMs: 0,
1207
+ metroConnected: connectedApps.size > 0,
1208
+ pressables: (ctx.fiberMatches || []).slice(0, 10).map(m => ({
1209
+ label: m.text || undefined,
1210
+ testID: m.testID || undefined,
1211
+ componentName: m.component
1212
+ }))
1213
+ },
1214
+ accessibility: {
1215
+ ran: ctx.attempted.some(a => a.strategy === "accessibility"),
1216
+ durationMs: 0,
1217
+ elements: []
1218
+ }
1219
+ },
1220
+ chosenTapPoint: ctx.chosenTapPoint ?? null,
1221
+ chosenElement: null,
1222
+ screenshots: {
1223
+ before: ctx.screenshotBuffer ?? null,
1224
+ afterWithMarker: ctx.afterWithMarker ?? null
1225
+ },
1226
+ deviceMeta: {
1227
+ platform: ctx.platform,
1228
+ driver: ctx.iosDriver,
1229
+ screenSize: { w: ctx.screenshotMeta?.width || 0, h: ctx.screenshotMeta?.height || 0 }
1230
+ }
1231
+ });
1232
+ return result.signals;
1233
+ }
1234
+ catch {
1235
+ return undefined;
1236
+ }
1237
+ }
1238
+ function attachArtifactSignals(result, signals) {
1239
+ if (!signals)
1240
+ return result;
1241
+ if (signals.artifactKey)
1242
+ result.artifactKey = signals.artifactKey;
1243
+ if (signals.ocrClosestMatch)
1244
+ result.ocrClosestMatch = signals.ocrClosestMatch;
1245
+ if (signals.fiberPressableCount)
1246
+ result.fiberPressableCount = signals.fiberPressableCount;
1247
+ if (signals.accessibilityMatchCount)
1248
+ result.accessibilityMatchCount = signals.accessibilityMatchCount;
1249
+ if (signals.appRoute)
1250
+ result.appRoute = signals.appRoute;
1251
+ // Append agent-facing hints to error message
1252
+ if (result.error) {
1253
+ if (signals.ocrClosestMatch) {
1254
+ result.error = `${result.error}\nClosest OCR match: ${signals.ocrClosestMatch}`;
1255
+ }
1256
+ if (signals.nearbyPressables.length > 0) {
1257
+ const labels = signals.nearbyPressables
1258
+ .map(p => p.testID || p.label)
1259
+ .filter(Boolean)
1260
+ .slice(0, 3)
1261
+ .join(", ");
1262
+ if (labels)
1263
+ result.error = `${result.error}\nNearby pressables: ${labels}`;
1264
+ }
1265
+ }
1266
+ return result;
1267
+ }
1268
+ // --- Orchestrator ---
1269
+ export async function tap(options) {
1270
+ const query = buildQuery(options);
1271
+ const strategy = options.strategy || "auto";
1272
+ const index = options.index;
1273
+ const maxTraversalDepth = options.maxTraversalDepth;
1274
+ const deadline = Date.now() + TAP_TIMEOUT_MS;
1275
+ const remainingMs = () => Math.max(0, deadline - Date.now());
1276
+ // Validate inputs
1277
+ const hasSearchParam = query.text || query.testID || query.component;
1278
+ const hasCoordinates = query.x !== undefined || query.y !== undefined;
1279
+ if (!hasSearchParam && !hasCoordinates) {
1280
+ return {
1281
+ success: false,
1282
+ query,
1283
+ error: "Must provide at least one of: text, testID, component, or x/y coordinates"
1284
+ };
1285
+ }
1286
+ if (hasCoordinates && (query.x === undefined || query.y === undefined)) {
1287
+ return {
1288
+ success: false,
1289
+ query,
1290
+ error: "Both x and y coordinates must be provided"
1291
+ };
1292
+ }
1293
+ // Native mode: bypass React Native connection, tap directly via ADB/simctl
1294
+ if (options.native && hasCoordinates) {
1295
+ const nativeResolved = await resolveDeviceTarget(options.device);
1296
+ if (!nativeResolved.ok) {
1297
+ return {
1298
+ success: false,
1299
+ query,
1300
+ error: formatResolverError(nativeResolved.error)
1301
+ };
1302
+ }
1303
+ const platform = nativeResolved.target.platform;
1304
+ const nativeUdid = nativeResolved.target.iosUdid;
1305
+ const nativeShouldScreenshot = options.screenshot !== false;
1306
+ // Decoupled (I5, 2026-05-16): verify runs even when image bytes aren't returned.
1307
+ const nativeShouldVerify = options.verify !== false;
1308
+ let nativeBeforeBuffer = null;
1309
+ let nativeScreenshotMeta;
1310
+ if (nativeShouldVerify) {
1311
+ const before = await captureScreenshot(platform, nativeUdid);
1312
+ nativeBeforeBuffer = before?.buffer || null;
1313
+ if (before) {
1314
+ nativeScreenshotMeta = {
1315
+ originalWidth: before.width,
1316
+ originalHeight: before.height,
1317
+ scaleFactor: before.scaleFactor
1318
+ };
1319
+ }
1320
+ }
1321
+ // If no screenshot was taken for verification, take one just for scaleFactor
1322
+ if (!nativeScreenshotMeta) {
1323
+ const ref = await captureScreenshot(platform, nativeUdid);
1324
+ if (ref) {
1325
+ nativeScreenshotMeta = {
1326
+ originalWidth: ref.width,
1327
+ originalHeight: ref.height,
1328
+ scaleFactor: ref.scaleFactor
1329
+ };
1330
+ // Also use it for verification if buffer is needed
1331
+ if (!nativeBeforeBuffer) {
1332
+ nativeBeforeBuffer = ref.buffer;
1333
+ }
1334
+ }
1335
+ }
1336
+ let result;
1337
+ try {
1338
+ result = await withTimeout(tryCoordinateStrategy(query.x, query.y, platform, nativeScreenshotMeta, nativeUdid), remainingMs(), "native-coordinate");
1339
+ }
1340
+ catch (err) {
1341
+ return formatTapFailure({
1342
+ query,
1343
+ attempted: [
1344
+ {
1345
+ strategy: "native-coordinate",
1346
+ reason: err instanceof Error ? err.message : String(err)
1347
+ }
1348
+ ],
1349
+ suggestion: `Tap timed out. Take a screenshot (${platform === "ios" ? "ios_screenshot" : "android_screenshot"}) and retry with coordinates.`
1350
+ });
1351
+ }
1352
+ if (result.success) {
1353
+ let screenshot;
1354
+ let verification;
1355
+ const nativeMarker = computeMarkerPx({
1356
+ strategy: "native-coordinate",
1357
+ input: { x: query.x, y: query.y },
1358
+ platform,
1359
+ screenshotScale: nativeScreenshotMeta?.scaleFactor || 1
1360
+ });
1361
+ if (options.burst && nativeShouldVerify && nativeBeforeBuffer) {
1362
+ ({ screenshot, verification } = await burstCaptureAndVerify({
1363
+ platform,
1364
+ beforeBuffer: nativeBeforeBuffer,
1365
+ udid: nativeUdid,
1366
+ beforeScaleFactor: nativeScreenshotMeta?.scaleFactor,
1367
+ markerPx: nativeMarker
1368
+ }));
1369
+ if (!nativeShouldScreenshot)
1370
+ screenshot = undefined;
1371
+ }
1372
+ else {
1373
+ ({ screenshot, verification } = await verifyAndCapture({
1374
+ platform,
1375
+ shouldVerify: nativeShouldVerify,
1376
+ shouldScreenshot: nativeShouldScreenshot,
1377
+ beforeBuffer: nativeBeforeBuffer,
1378
+ udid: nativeUdid,
1379
+ beforeScaleFactor: nativeScreenshotMeta?.scaleFactor,
1380
+ markerPx: nativeMarker
1381
+ }));
1382
+ }
1383
+ return formatTapSuccess({
1384
+ method: "native-coordinate",
1385
+ query,
1386
+ pressed: result.pressed,
1387
+ convertedTo: result.convertedTo,
1388
+ platform,
1389
+ screenshot,
1390
+ verification
1391
+ });
1392
+ }
1393
+ return formatTapFailure({
1394
+ query,
1395
+ attempted: [{ strategy: "native-coordinate", reason: result.reason }],
1396
+ suggestion: `Take a screenshot (${platform === "ios" ? "ios_screenshot" : "android_screenshot"}) to verify coordinates.`
1397
+ });
1398
+ }
1399
+ // Resolve device target via the unified resolver. Replaces the prior chain
1400
+ // of (udid → connectedApp → findSimulatorByName) lookups and the standalone
1401
+ // "no platform → probe OS for devices" branch. Returns a structured error
1402
+ // when ambiguous; surface that to the caller so they can disambiguate.
1403
+ const resolved = await resolveDeviceTarget(options.device);
1404
+ if (!resolved.ok) {
1405
+ return {
1406
+ success: false,
1407
+ query,
1408
+ error: formatResolverError(resolved.error)
1409
+ };
1410
+ }
1411
+ const platform = resolved.target.platform;
1412
+ let targetUdid = resolved.target.iosUdid;
1413
+ // Pick the connected app to bias strategy selection. Prefer the registry
1414
+ // entry whose identifier matches the resolved target; fall back to a
1415
+ // device-name match (handy when registry entries lack identifiers yet);
1416
+ // last resort is the first connected app on the matching platform.
1417
+ const allApps = Array.from(connectedApps.values());
1418
+ let hasMetro = allApps.length > 0;
1419
+ let app;
1420
+ if (resolved.target.iosUdid) {
1421
+ app = allApps.find((a) => a.platform === "ios" && a.simulatorUdid === resolved.target.iosUdid);
1422
+ }
1423
+ else if (resolved.target.androidSerial) {
1424
+ app = allApps.find((a) => a.platform === "android" && a.adbSerial === resolved.target.androidSerial);
1425
+ }
1426
+ if (!app && options.device) {
1427
+ try {
1428
+ app = getConnectedAppByDevice(options.device) ?? undefined;
1429
+ }
1430
+ catch {
1431
+ // getConnectedAppByDevice throws on ambiguous matches; the resolver
1432
+ // would have already returned that error, so reaching here means
1433
+ // the resolved target's identifier just doesn't appear in the
1434
+ // registry yet — fall through to the platform-first pick below.
1435
+ }
1436
+ }
1437
+ if (!app) {
1438
+ app = allApps.find((a) => a.platform === platform) ?? allApps[0];
1439
+ }
1440
+ // Try to auto-connect to Metro (for fiber strategy), but don't fail if it doesn't work.
1441
+ if (!hasMetro) {
1442
+ try {
1443
+ await withTimeout((async () => {
1444
+ clearReconnectionSuppression();
1445
+ const openPorts = await scanMetroPorts();
1446
+ for (const port of openPorts) {
1447
+ const devices = await fetchDevices(port);
1448
+ const mainDevice = selectMainDevice(devices);
1449
+ if (mainDevice) {
1450
+ await connectToDevice(mainDevice, port);
1451
+ break;
1452
+ }
1453
+ }
1454
+ })(), Math.min(remainingMs(), 3000), "auto-connect");
1455
+ const apps = Array.from(connectedApps.values());
1456
+ hasMetro = apps.length > 0;
1457
+ app = apps.find((a) => a.platform === platform) ?? apps[0];
1458
+ if (platform === "ios" && !targetUdid) {
1459
+ targetUdid = app?.simulatorUdid ?? targetUdid;
1460
+ }
1461
+ }
1462
+ catch {
1463
+ // Auto-connect failed — Metro-dependent strategies will be skipped
1464
+ }
1465
+ }
1466
+ // The response's `device` label must reflect the RESOLVED target, not the
1467
+ // registry-app fallback. When the registry lookup couldn't find a matching
1468
+ // app (e.g. simulatorUdid hadn't been backfilled yet), `app` lands on the
1469
+ // first iOS entry — which on a multi-sim setup is the WRONG device. The
1470
+ // resolver already knows the right deviceName; trust it. Bug #5 (2026-05-20).
1471
+ const deviceName = resolved.target.deviceName || app?.deviceInfo?.deviceName;
1472
+ // Determine strategies
1473
+ const strategies = getAvailableStrategies(query, strategy);
1474
+ const attempted = [];
1475
+ const evidence = makeEmptyEvidenceSink();
1476
+ // Early UI driver check for iOS — fail fast instead of falling through every strategy
1477
+ const UI_DRIVER_REQUIRED_STRATEGIES = ["accessibility", "ocr", "coordinate"];
1478
+ let uiDriverMissing = false;
1479
+ if (platform === "ios") {
1480
+ uiDriverMissing = !(await isUiDriverAvailable());
1481
+ }
1482
+ // Filter strategies by available capabilities
1483
+ const filteredStrategies = strategies.filter((strat) => {
1484
+ if (strat === "fiber" && !hasMetro) {
1485
+ attempted.push({
1486
+ strategy: "fiber",
1487
+ reason: "Skipped — no Metro connection (required for fiber)",
1488
+ outcome: "skipped"
1489
+ });
1490
+ return false;
1491
+ }
1492
+ if (uiDriverMissing && UI_DRIVER_REQUIRED_STRATEGIES.includes(strat)) {
1493
+ attempted.push({
1494
+ strategy: strat,
1495
+ reason: "Skipped — iOS UI driver is not installed (required for iOS tap/accessibility/OCR)",
1496
+ outcome: "skipped"
1497
+ });
1498
+ return false;
1499
+ }
1500
+ return true;
1501
+ });
1502
+ if (filteredStrategies.length === 0) {
1503
+ if (uiDriverMissing) {
1504
+ notifyDriverMissing("ios");
1505
+ }
1506
+ const errorMessage = uiDriverMissing
1507
+ ? `Cannot tap on iOS Simulator — ${getUiDriverInstallHint()}\n\nThe iOS UI driver is required for tapping, swiping, text input, and accessibility queries on iOS Simulators.\n\nAfter installing, retry the tap.`
1508
+ : "All strategies require Metro connection, which is unavailable.\n\nTo fix:\n1. Make sure your React Native app is running\n2. Run scan_metro to connect\n3. Or use tap(x, y, native=true) for coordinate-based taps";
1509
+ return {
1510
+ success: false,
1511
+ query,
1512
+ attempted,
1513
+ error: errorMessage
1514
+ };
1515
+ }
1516
+ // Determine screenshot and verification behavior.
1517
+ // Decoupled (I5, 2026-05-16): verify can run without returning image bytes —
1518
+ // capture cost is paid either way; bandwidth cost is what `screenshot` toggles.
1519
+ const shouldScreenshot = options.screenshot !== false;
1520
+ const canVerify = options.verify !== false;
1521
+ // Capture "before" screenshot. Always attempted (independent of canVerify)
1522
+ // so failure artifacts always carry a before.png — the diagnostic value of
1523
+ // the before frame doesn't depend on whether we run the post-tap diff.
1524
+ let beforeBuffer = null;
1525
+ let beforeScaleFactor;
1526
+ {
1527
+ const before = await captureScreenshot(platform, targetUdid);
1528
+ beforeBuffer = before?.buffer || null;
1529
+ beforeScaleFactor = before?.scaleFactor;
1530
+ }
1531
+ // OCR runs lazily when the loop reaches it (see the `case "ocr"` branch).
1532
+ // A concurrent pre-warm probe used to fire here to shave ~5s off OCR-win
1533
+ // rows, but it dispatched a paid Google Vision request at t=0 on every
1534
+ // text-predicate tap — and since cloud OCR (~200ms) finishes well before
1535
+ // the higher-priority strategy that usually wins, the post-win abort came
1536
+ // too late to cancel the billed request. OCR wins only ~1.9% of taps, so
1537
+ // the pre-warm paid for cloud Vision on ~42% of eligible taps to help a
1538
+ // tiny minority. Removed 2026-06-02; the timeout-recovery path below still
1539
+ // salvages the perfect-match-past-cap case.
1540
+ // Execute strategies in order with per-strategy caps and overall budget
1541
+ for (const strat of filteredStrategies) {
1542
+ const remaining = remainingMs();
1543
+ if (remaining < MIN_STRATEGY_BUDGET_MS) {
1544
+ attempted.push({
1545
+ strategy: strat,
1546
+ reason: `Skipped — only ${remaining}ms remaining (budget ${TAP_TIMEOUT_MS}ms)`,
1547
+ outcome: "skipped"
1548
+ });
1549
+ continue;
1550
+ }
1551
+ const cap = maxStrategyMs(strat, platform);
1552
+ const budget = Math.min(cap, remaining);
1553
+ let result;
1554
+ try {
1555
+ switch (strat) {
1556
+ case "fiber":
1557
+ // Fiber is JS-only against a CDP target — no subprocess to cancel,
1558
+ // so the cheaper non-cancellable wrapper is fine.
1559
+ result = await withTimeout(tryFiberStrategy(query, index, maxTraversalDepth, evidence, deviceName), budget, `fiber`);
1560
+ break;
1561
+ case "accessibility":
1562
+ result = await withCancelableTimeout((signal) => tryAccessibilityStrategy(query, index, platform, targetUdid, evidence, signal), budget, `accessibility`);
1563
+ break;
1564
+ case "ocr":
1565
+ result = await withCancelableTimeout((signal) => tryOcrStrategy(query, platform, targetUdid, evidence, signal), budget, `ocr`);
1566
+ break;
1567
+ case "coordinate":
1568
+ // Prefer `beforeScaleFactor` (captured against `targetUdid` this turn)
1569
+ // over `app.lastScreenshot.scaleFactor` (stale and may belong to a
1570
+ // different device on a multi-sim setup). Bug #5 (2026-05-20).
1571
+ result = await withTimeout(tryCoordinateStrategy(query.x, query.y, platform, beforeScaleFactor != null
1572
+ ? { originalWidth: 0, originalHeight: 0, scaleFactor: beforeScaleFactor }
1573
+ : app?.lastScreenshot, targetUdid), budget, `coordinate`);
1574
+ break;
1575
+ default:
1576
+ result = { success: false, reason: `Unknown strategy: ${strat}` };
1577
+ }
1578
+ }
1579
+ catch (err) {
1580
+ const reason = err instanceof Error ? err.message : String(err);
1581
+ // Classify the throw: a strategy-level timeout means "result UNKNOWN"
1582
+ // (the strategy didn't finish), distinct from "ran clean and found
1583
+ // nothing". Agents must not infer absence from timeouts. The
1584
+ // `outcome` field carries that distinction; `reason` stays the raw
1585
+ // message so existing log-matchers / classifiers keep working.
1586
+ const outcome = STRATEGY_TIMEOUT_RE.test(reason) ? "timeout" : "error";
1587
+ attempted.push({ strategy: strat, reason, outcome });
1588
+ continue;
1589
+ }
1590
+ if (result.success) {
1591
+ let screenshot;
1592
+ let verification;
1593
+ let afterWithMarkerBuffer;
1594
+ let dprForMarker;
1595
+ if (platform === "ios") {
1596
+ try {
1597
+ const { getDevicePixelRatio } = await import("../core/ios.js");
1598
+ dprForMarker = await getDevicePixelRatio(targetUdid);
1599
+ }
1600
+ catch {
1601
+ dprForMarker = 3;
1602
+ }
1603
+ }
1604
+ const strategyMarker = computeMarkerPx({
1605
+ strategy: strat,
1606
+ input: strat === "coordinate" ? { x: query.x, y: query.y } : undefined,
1607
+ convertedTo: result.convertedTo,
1608
+ platform,
1609
+ screenshotScale: beforeScaleFactor || app?.lastScreenshot?.scaleFactor || 1,
1610
+ devicePixelRatio: dprForMarker
1611
+ });
1612
+ if (options.burst && canVerify && beforeBuffer) {
1613
+ ({ screenshot, verification, afterWithMarkerBuffer } = await burstCaptureAndVerify({
1614
+ platform,
1615
+ beforeBuffer,
1616
+ udid: targetUdid,
1617
+ beforeScaleFactor,
1618
+ markerPx: strategyMarker
1619
+ }));
1620
+ // Burst always produces a screenshot for diff visualization. Drop the
1621
+ // bytes when the caller didn't ask for them AND the tap was meaningful
1622
+ // — keeping the screenshot on unmeaningful taps saves the agent a
1623
+ // round-trip when it needs to diagnose. Frames remain in imageBuffer
1624
+ // either way (accessible via get_images(groupId=verification.burstGroupId)).
1625
+ if (!shouldScreenshot && verification?.meaningful !== false)
1626
+ screenshot = undefined;
1627
+ }
1628
+ else {
1629
+ ({ screenshot, verification, afterWithMarkerBuffer } = await verifyAndCapture({
1630
+ platform,
1631
+ shouldVerify: canVerify,
1632
+ shouldScreenshot: true,
1633
+ beforeBuffer,
1634
+ udid: targetUdid,
1635
+ beforeScaleFactor,
1636
+ markerPx: strategyMarker
1637
+ }));
1638
+ // Mirror the burst-path gate: drop the screenshot only when the
1639
+ // caller didn't ask for it AND the tap was meaningful.
1640
+ if (!shouldScreenshot && verification?.meaningful !== false)
1641
+ screenshot = undefined;
1642
+ }
1643
+ if (screenshot && app) {
1644
+ app.lastScreenshot = {
1645
+ originalWidth: screenshot.width,
1646
+ originalHeight: screenshot.height,
1647
+ scaleFactor: screenshot.scaleFactor
1648
+ };
1649
+ }
1650
+ const successResult = formatTapSuccess({
1651
+ method: strat,
1652
+ query,
1653
+ pressed: result.pressed,
1654
+ text: result.text,
1655
+ screen: result.screen,
1656
+ path: result.path,
1657
+ component: result.component,
1658
+ convertedTo: result.convertedTo,
1659
+ platform,
1660
+ device: deviceName,
1661
+ screenshot,
1662
+ verification
1663
+ });
1664
+ // Capture an artifact for "successful but unmeaningful" taps so we can
1665
+ // diagnose taps that landed wrong or hit non-responsive elements.
1666
+ // Uses the same `meaningful` flag the agent sees so dashboard outcomes
1667
+ // stay consistent with what the caller observed.
1668
+ if (verification && !verification.skipped && verification.meaningful === false) {
1669
+ const unmeaningfulSignals = await captureTapArtifact({
1670
+ query,
1671
+ outcome: "unmeaningful",
1672
+ attempted: [...attempted, { strategy: strat, reason: "success" }],
1673
+ platform,
1674
+ iosDriver: platform === "ios" ? (process.env.IOS_DRIVER?.toLowerCase() || "axe") : undefined,
1675
+ deviceName,
1676
+ screenshotMeta: screenshot ? { width: screenshot.width, height: screenshot.height } : undefined,
1677
+ screenshotBuffer: beforeBuffer,
1678
+ afterWithMarker: afterWithMarkerBuffer ?? null,
1679
+ chosenTapPoint: strategyMarker ? { x: strategyMarker.x, y: strategyMarker.y } : null,
1680
+ verification,
1681
+ evidence
1682
+ });
1683
+ attachArtifactSignals(successResult, unmeaningfulSignals);
1684
+ }
1685
+ return successResult;
1686
+ }
1687
+ // Classify the strategy's non-success result. The reason string carries
1688
+ // the diagnostic from the strategy; outcome lets agents branch on category
1689
+ // without parsing prose. "invisible" / "ambiguous" are pinned by substring
1690
+ // matches the fiber strategy emits via pressables.ts.
1691
+ const reasonStr = result.reason || "";
1692
+ let attemptOutcome = "not-found";
1693
+ if (result.ambiguous)
1694
+ attemptOutcome = "ambiguous";
1695
+ else if (reasonStr.indexOf("but none are visible") !== -1)
1696
+ attemptOutcome = "invisible";
1697
+ else if (STRATEGY_TIMEOUT_RE.test(reasonStr))
1698
+ attemptOutcome = "timeout";
1699
+ attempted.push({ strategy: strat, reason: reasonStr, outcome: attemptOutcome });
1700
+ // If fiber found an element with measured coordinates, do a native tap directly
1701
+ if (strat === "fiber" && result.convertedTo && result.pressed) {
1702
+ try {
1703
+ const coords = result.convertedTo;
1704
+ if (platform === "ios") {
1705
+ // react-native-screens modal/sheet presentations cause measureInWindow to
1706
+ // return y relative to the screen's content origin, not the window. If the
1707
+ // measured y falls inside the safe-area band, shift it down by the inset.
1708
+ const safeAreaTop = await getIOSSafeAreaTop(targetUdid);
1709
+ const tapY = (safeAreaTop > 0 && coords.y < safeAreaTop) ? coords.y + safeAreaTop : coords.y;
1710
+ await iosTap(coords.x, tapY, { udid: targetUdid });
1711
+ coords.y = tapY;
1712
+ }
1713
+ else {
1714
+ // Fabric returns dp — androidTap expects pixels
1715
+ // Convert dp to pixels using device density
1716
+ const { androidGetDensity } = await import("../core/android.js");
1717
+ const densityResult = await androidGetDensity();
1718
+ const densityScale = (densityResult.density || 420) / 160;
1719
+ const pxX = Math.round(coords.x * densityScale);
1720
+ const pxY = Math.round(coords.y * densityScale);
1721
+ await androidTap(pxX, pxY);
1722
+ // Report the actual tap coords (pixels) the caller can pass straight
1723
+ // to coordinate tools / verification — not the raw dp from fiber.
1724
+ // Matches every other Android coord report in the tool (OB1, 2026-05-20).
1725
+ coords.x = pxX;
1726
+ coords.y = pxY;
1727
+ coords.unit = "pixels";
1728
+ }
1729
+ // fiber+native uses native tap — always verify
1730
+ let screenshot;
1731
+ let verification;
1732
+ let fnDpr;
1733
+ let fnDensity;
1734
+ if (platform === "ios") {
1735
+ try {
1736
+ const { getDevicePixelRatio } = await import("../core/ios.js");
1737
+ fnDpr = await getDevicePixelRatio(targetUdid);
1738
+ }
1739
+ catch {
1740
+ fnDpr = 3;
1741
+ }
1742
+ }
1743
+ else {
1744
+ try {
1745
+ const { androidGetDensity } = await import("../core/android.js");
1746
+ const d = await androidGetDensity();
1747
+ fnDensity = (d.density || 420) / 160;
1748
+ }
1749
+ catch {
1750
+ fnDensity = undefined;
1751
+ }
1752
+ }
1753
+ const fiberMarker = computeMarkerPx({
1754
+ strategy: "fiber+native",
1755
+ convertedTo: coords,
1756
+ platform,
1757
+ screenshotScale: beforeScaleFactor || app?.lastScreenshot?.scaleFactor || 1,
1758
+ devicePixelRatio: fnDpr,
1759
+ androidDensityScale: fnDensity
1760
+ });
1761
+ if (options.burst && canVerify && beforeBuffer) {
1762
+ ({ screenshot, verification } = await burstCaptureAndVerify({
1763
+ platform,
1764
+ beforeBuffer,
1765
+ udid: targetUdid,
1766
+ beforeScaleFactor,
1767
+ markerPx: fiberMarker
1768
+ }));
1769
+ }
1770
+ else {
1771
+ ({ screenshot, verification } = await verifyAndCapture({
1772
+ platform,
1773
+ shouldVerify: canVerify,
1774
+ shouldScreenshot: true,
1775
+ beforeBuffer,
1776
+ udid: targetUdid,
1777
+ beforeScaleFactor,
1778
+ markerPx: fiberMarker
1779
+ }));
1780
+ }
1781
+ if (!shouldScreenshot && verification?.meaningful !== false)
1782
+ screenshot = undefined;
1783
+ if (screenshot && app) {
1784
+ app.lastScreenshot = {
1785
+ originalWidth: screenshot.width,
1786
+ originalHeight: screenshot.height,
1787
+ scaleFactor: screenshot.scaleFactor
1788
+ };
1789
+ }
1790
+ return formatTapSuccess({
1791
+ method: "fiber+native",
1792
+ query,
1793
+ pressed: result.pressed,
1794
+ text: result.text,
1795
+ path: result.path,
1796
+ component: result.component,
1797
+ convertedTo: coords,
1798
+ platform,
1799
+ device: deviceName,
1800
+ screenshot,
1801
+ verification
1802
+ });
1803
+ }
1804
+ catch {
1805
+ // Native tap at fiber coordinates failed — continue to next strategy
1806
+ }
1807
+ }
1808
+ // Ambiguous fiber result — multiple elements matched, no index given.
1809
+ // Return immediately with the full list so the agent can decide.
1810
+ // Do NOT fall through to other strategies (they can't resolve ambiguity).
1811
+ if (result.matches && result.ambiguous) {
1812
+ const { screenshot: matchScreenshot } = shouldScreenshot
1813
+ ? await verifyAndCapture({ platform, shouldVerify: false, shouldScreenshot: true, beforeBuffer: null, udid: targetUdid })
1814
+ : { screenshot: undefined };
1815
+ if (matchScreenshot && app) {
1816
+ app.lastScreenshot = {
1817
+ originalWidth: matchScreenshot.width,
1818
+ originalHeight: matchScreenshot.height,
1819
+ scaleFactor: matchScreenshot.scaleFactor
1820
+ };
1821
+ }
1822
+ return formatTapFailure({
1823
+ query,
1824
+ attempted,
1825
+ error: `Ambiguous: ${result.matches.length} elements match this query. Tap did not execute.`,
1826
+ suggestion: `Specify index= (0–${result.matches.length - 1}) or add text= to narrow down. See matches[] for position and text of each element.`,
1827
+ device: deviceName,
1828
+ matches: result.matches,
1829
+ ambiguous: true,
1830
+ screenshot: matchScreenshot
1831
+ });
1832
+ }
1833
+ }
1834
+ // All strategies failed — check if the tap budget was the cause.
1835
+ // Only outer withTimeout wrapper messages or Skipped entries count; nested
1836
+ // sub-op errors that happen to contain "timed out" do not imply the tap
1837
+ // itself ran out of time.
1838
+ const hitTimeout = isTapTimeout(attempted);
1839
+ const elapsed = TAP_TIMEOUT_MS - remainingMs();
1840
+ // If fiber located the element but visibility filtered it out, prepend a
1841
+ // scroll-or-dismiss hint so the agent doesn't waste a turn assuming the
1842
+ // testID/component is missing.
1843
+ const fiberSawInvisible = attempted.some(a => a.strategy === "fiber" && a.outcome === "invisible");
1844
+ const allTimedOut = attempted.length > 0 && attempted.every(a => a.outcome === "timeout" || a.outcome === "skipped");
1845
+ let suggestion = buildSuggestion(query, strategies, platform);
1846
+ if (fiberSawInvisible) {
1847
+ 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;
1848
+ }
1849
+ else if (allTimedOut) {
1850
+ // No strategy got to a definitive answer — don't let the agent conclude
1851
+ // the element is missing. Steer toward retry or a different strategy.
1852
+ 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;
1853
+ }
1854
+ const { screenshot: failScreenshot } = shouldScreenshot
1855
+ ? await verifyAndCapture({ platform, shouldVerify: false, shouldScreenshot: true, beforeBuffer: null, udid: targetUdid })
1856
+ : { screenshot: undefined };
1857
+ if (failScreenshot && app) {
1858
+ app.lastScreenshot = {
1859
+ originalWidth: failScreenshot.width,
1860
+ originalHeight: failScreenshot.height,
1861
+ scaleFactor: failScreenshot.scaleFactor
1862
+ };
1863
+ }
1864
+ // Pick the right error framing: a definitive "not found" is different from
1865
+ // "all strategies timed out — we don't know whether it exists".
1866
+ let errorOverride;
1867
+ if (hitTimeout) {
1868
+ errorOverride = `Tap timed out after ${elapsed}ms (budget ${TAP_TIMEOUT_MS}ms)`;
1869
+ }
1870
+ else if (allTimedOut) {
1871
+ errorOverride = `All tap strategies timed out before completing — element presence is UNKNOWN`;
1872
+ }
1873
+ else if (fiberSawInvisible) {
1874
+ errorOverride = `Element matches the query but is not visible on screen`;
1875
+ }
1876
+ const failureResult = formatTapFailure({
1877
+ query,
1878
+ attempted,
1879
+ error: errorOverride,
1880
+ suggestion,
1881
+ device: deviceName,
1882
+ screenshot: failScreenshot
1883
+ });
1884
+ const failSignals = await captureTapArtifact({
1885
+ query,
1886
+ outcome: "failure",
1887
+ errorMessage: failureResult.error,
1888
+ attempted,
1889
+ platform,
1890
+ iosDriver: platform === "ios" ? (process.env.IOS_DRIVER?.toLowerCase() || "axe") : undefined,
1891
+ deviceName,
1892
+ screenshotMeta: failScreenshot ? { width: failScreenshot.width, height: failScreenshot.height } : undefined,
1893
+ screenshotBuffer: beforeBuffer,
1894
+ afterWithMarker: null,
1895
+ chosenTapPoint: null,
1896
+ evidence
1897
+ });
1898
+ return attachArtifactSignals(failureResult, failSignals);
1899
+ }
1900
+ // Matches testIDs typical of virtualized list items: trailing `-<digits>` (e.g. `store-item-0`,
1901
+ // `route-item-6`) or containing a UUID v4 fragment (e.g. `group-card-e4566bc6-9164-4fd0-...`).
1902
+ // Used by buildSuggestion() to tell the agent to scroll first when a list-item testID
1903
+ // can't be found in the visible fiber tree.
1904
+ const VIRTUALIZED_TESTID_RE = /(-\d+$)|([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i;
1905
+ function looksLikeVirtualizedListItem(testID) {
1906
+ return VIRTUALIZED_TESTID_RE.test(testID);
1907
+ }
1908
+ function buildSuggestion(query, triedStrategies, platform) {
1909
+ const suggestions = [];
1910
+ if (!triedStrategies.includes("ocr") && query.text) {
1911
+ suggestions.push("Try strategy='ocr' to find text visually on screen");
1912
+ }
1913
+ if (query.text && query.text.length <= 2) {
1914
+ suggestions.push("Very short text is unreliable for OCR — use testID or coordinates instead");
1915
+ }
1916
+ if (query.text && hasProblematicUnicode(query.text)) {
1917
+ suggestions.push("Emoji text cannot use fiber strategy — use testID or coordinates instead");
1918
+ }
1919
+ if (query.component && triedStrategies.includes("fiber")) {
1920
+ suggestions.push("Component not found or has no onPress handler — use find_components to discover exact component names, or use text/coordinates instead");
1921
+ }
1922
+ if (query.testID && !triedStrategies.includes("ocr")) {
1923
+ suggestions.push("testID not found in fiber/accessibility tree — verify the element is on the current screen with a screenshot");
1924
+ }
1925
+ // List-item testIDs (suffix `-N` or containing a UUID) are typically rendered inside a
1926
+ // virtualized list — items beyond the viewport are unmounted, so the fiber tree genuinely
1927
+ // doesn't contain them. Adding the hint as a separate suggestion (rather than rewriting
1928
+ // the error) keeps the existing message stable for tooling that parses it.
1929
+ if (query.testID && looksLikeVirtualizedListItem(query.testID)) {
1930
+ suggestions.push("testID looks like a virtualized list item — scroll it on-screen first " +
1931
+ `(swipe()${platform === "ios" ? " or ios_button arrow keys" : ""}) before tapping`);
1932
+ }
1933
+ suggestions.push(`Take a screenshot (${platform === "ios" ? "ios_screenshot" : "android_screenshot"}) ` +
1934
+ "to verify the element is visible, then use x/y coordinates");
1935
+ return suggestions.join(". ");
1936
+ }
1937
+ //# sourceMappingURL=tap.js.map