rnxsim 0.1.313 → 0.1.314

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 (297) hide show
  1. package/LICENSE +11 -10
  2. package/README.md +5 -0
  3. package/cli/app-config.ts +65 -0
  4. package/cli/app-fonts.ts +408 -0
  5. package/cli/app-project.ts +231 -0
  6. package/cli/app-splash.ts +185 -0
  7. package/cli/app-state-reset.ts +24 -0
  8. package/cli/auth.ts +155 -0
  9. package/cli/bin.ts +594 -0
  10. package/cli/bridge-diagnostics.ts +226 -0
  11. package/cli/bridge-flow-runner.ts +2830 -0
  12. package/cli/browser-evals.ts +96 -0
  13. package/cli/commands/agent-wrapper.ts +986 -0
  14. package/cli/commands/agent.ts +423 -0
  15. package/cli/commands/app-fonts.ts +98 -0
  16. package/cli/commands/assert.ts +541 -0
  17. package/cli/commands/auth.ts +59 -0
  18. package/cli/commands/camera.ts +266 -0
  19. package/cli/commands/cleanup.ts +169 -0
  20. package/cli/commands/compat.ts +87 -0
  21. package/cli/commands/config.ts +32 -0
  22. package/cli/commands/control.ts +2142 -0
  23. package/cli/commands/cpu-profile.ts +269 -0
  24. package/cli/commands/daemon-mac-app.ts +169 -0
  25. package/cli/commands/daemon.ts +874 -0
  26. package/cli/commands/debug.ts +719 -0
  27. package/cli/commands/desktop.ts +39 -0
  28. package/cli/commands/detect.ts +197 -0
  29. package/cli/commands/detox.ts +385 -0
  30. package/cli/commands/device.ts +133 -0
  31. package/cli/commands/diagnose.ts +589 -0
  32. package/cli/commands/electron.ts +95 -0
  33. package/cli/commands/film.ts +379 -0
  34. package/cli/commands/flow.ts +1124 -0
  35. package/cli/commands/inspect/actions.ts +622 -0
  36. package/cli/commands/inspect/core.ts +2405 -0
  37. package/cli/commands/inspect/count.ts +17 -0
  38. package/cli/commands/inspect/describe.ts +192 -0
  39. package/cli/commands/inspect/env.ts +23 -0
  40. package/cli/commands/inspect/find.ts +171 -0
  41. package/cli/commands/inspect/get-layout.ts +39 -0
  42. package/cli/commands/inspect/keyboard.ts +52 -0
  43. package/cli/commands/inspect/list.ts +58 -0
  44. package/cli/commands/inspect/memory.ts +215 -0
  45. package/cli/commands/inspect/redaction.ts +39 -0
  46. package/cli/commands/inspect/resolve-target.ts +82 -0
  47. package/cli/commands/inspect/screens.ts +78 -0
  48. package/cli/commands/inspect/settle.ts +22 -0
  49. package/cli/commands/inspect/settling.ts +158 -0
  50. package/cli/commands/inspect/shared.ts +353 -0
  51. package/cli/commands/inspect/sleep.ts +14 -0
  52. package/cli/commands/inspect/tree.ts +32 -0
  53. package/cli/commands/inspect/url.ts +17 -0
  54. package/cli/commands/inspect/wait-event.ts +210 -0
  55. package/cli/commands/inspect/wait-idle.ts +24 -0
  56. package/cli/commands/inspect/wait-ready.ts +74 -0
  57. package/cli/commands/inspect/wait-selector.ts +54 -0
  58. package/cli/commands/inspect/wait.ts +31 -0
  59. package/cli/commands/inspect.ts +4519 -0
  60. package/cli/commands/install-desktop.ts +351 -0
  61. package/cli/commands/login.ts +331 -0
  62. package/cli/commands/logout.ts +31 -0
  63. package/cli/commands/maestro-generate.ts +361 -0
  64. package/cli/commands/maestro.ts +453 -0
  65. package/cli/commands/mode.ts +57 -0
  66. package/cli/commands/no-bridge-hint.ts +80 -0
  67. package/cli/commands/perf.ts +66 -0
  68. package/cli/commands/permissions.ts +203 -0
  69. package/cli/commands/profile.ts +108 -0
  70. package/cli/commands/react.ts +353 -0
  71. package/cli/commands/record.ts +1434 -0
  72. package/cli/commands/report-issue.ts +305 -0
  73. package/cli/commands/reset.ts +85 -0
  74. package/cli/commands/runtime.ts +351 -0
  75. package/cli/commands/screenshot-command.ts +106 -0
  76. package/cli/commands/screenshot-layers.ts +143 -0
  77. package/cli/commands/screenshot-mode.ts +37 -0
  78. package/cli/commands/screenshot.ts +488 -0
  79. package/cli/commands/screenshots-capture.ts +607 -0
  80. package/cli/commands/screenshots.ts +127 -0
  81. package/cli/commands/serve.ts +168 -0
  82. package/cli/commands/setup.ts +545 -0
  83. package/cli/commands/shell-boolean-mode.ts +81 -0
  84. package/cli/commands/skills.ts +467 -0
  85. package/cli/commands/slides.ts +361 -0
  86. package/cli/commands/state.ts +87 -0
  87. package/cli/commands/storage.ts +58 -0
  88. package/cli/commands/telemetry.ts +54 -0
  89. package/cli/commands/three-mode.ts +763 -0
  90. package/cli/commands/timeline.ts +122 -0
  91. package/cli/commands/upgrade.ts +208 -0
  92. package/cli/commands/upload.ts +1225 -0
  93. package/cli/commands/version.ts +54 -0
  94. package/cli/commands/what-happened.ts +327 -0
  95. package/cli/current-sim.ts +204 -0
  96. package/cli/desktop-companion.ts +300 -0
  97. package/cli/drivers/electron.ts +70 -0
  98. package/cli/drivers/index.ts +20 -0
  99. package/cli/drivers/playwright-provisioning.ts +180 -0
  100. package/cli/drivers/playwright.ts +698 -0
  101. package/cli/drivers/registry.ts +65 -0
  102. package/cli/drivers/types.ts +102 -0
  103. package/cli/flow-file.ts +142 -0
  104. package/cli/flow-live-status.ts +120 -0
  105. package/cli/flow-session.ts +187 -0
  106. package/cli/help.ts +80 -0
  107. package/cli/hidden-runtime-alias.ts +19 -0
  108. package/cli/hints.ts +216 -0
  109. package/cli/inspect-notice-state.ts +114 -0
  110. package/cli/maestro-js.ts +334 -0
  111. package/cli/open-url.ts +8 -0
  112. package/cli/parent-pid.ts +204 -0
  113. package/cli/parse-args.ts +211 -0
  114. package/cli/prompt.ts +51 -0
  115. package/cli/recording-access.ts +107 -0
  116. package/cli/registry.ts +1 -0
  117. package/cli/resolve-assets.ts +63 -0
  118. package/cli/run-registry.ts +226 -0
  119. package/cli/runtime-notes.ts +66 -0
  120. package/cli/runtime-summary.ts +25 -0
  121. package/cli/setup-repository.ts +187 -0
  122. package/cli/telemetry.ts +187 -0
  123. package/cli/ws-bridge.ts +798 -0
  124. package/dist-cli/bin.js +5 -5
  125. package/dist-cli/chunks/{agent-XZ2KTPCU.js → agent-7YBDCYMA.js} +2 -2
  126. package/dist-cli/chunks/{agent-wrapper-JJYYW2WH.js → agent-wrapper-2GHFBHCR.js} +2 -2
  127. package/dist-cli/chunks/{app-fonts-IXRNQG6B.js → app-fonts-RSNVPQSU.js} +2 -2
  128. package/dist-cli/chunks/{assert-54T5SK5F.js → assert-XCMX3XJX.js} +2 -2
  129. package/dist-cli/chunks/{auth-FI5UDI45.js → auth-B442HRAX.js} +2 -2
  130. package/dist-cli/chunks/{beta-JV6UKADW.js → beta-XJ55JK3M.js} +2 -2
  131. package/dist-cli/chunks/camera-UGYSSLIK.js +33 -0
  132. package/dist-cli/chunks/{chunk-3NV2NCNX.js → chunk-277AEQZX.js} +2 -2
  133. package/dist-cli/chunks/{chunk-2YR5BGA5.js → chunk-2JNSK774.js} +2 -2
  134. package/dist-cli/chunks/{chunk-RSZWCKNT.js → chunk-32WOTSTR.js} +3 -3
  135. package/dist-cli/chunks/{chunk-WEXDAC74.js → chunk-3S753SNQ.js} +2 -2
  136. package/dist-cli/chunks/{chunk-IJO63TDP.js → chunk-46ZOLOYA.js} +2 -2
  137. package/dist-cli/chunks/{chunk-WUSWBCWA.js → chunk-5L7ELDQL.js} +8 -9
  138. package/dist-cli/chunks/{chunk-WF3T4SVI.js → chunk-7OOPFSQS.js} +2 -2
  139. package/dist-cli/chunks/{chunk-TZFFR3SD.js → chunk-7SV3RPRW.js} +2 -2
  140. package/dist-cli/chunks/{chunk-WWZIXIRD.js → chunk-7ZC35MOU.js} +1 -1
  141. package/dist-cli/chunks/chunk-AMG5E6CC.js +9 -0
  142. package/dist-cli/chunks/{chunk-WINYQ44O.js → chunk-APWNH3A4.js} +1 -1
  143. package/dist-cli/chunks/chunk-B57XUKY3.js +4 -0
  144. package/dist-cli/chunks/{chunk-YDGQTMQL.js → chunk-C2NL26TD.js} +1 -1
  145. package/dist-cli/chunks/{chunk-NMF2ZMZQ.js → chunk-D2FNUWAB.js} +4 -4
  146. package/dist-cli/chunks/{chunk-YIFT42WN.js → chunk-E5T4XSJ3.js} +2 -2
  147. package/dist-cli/chunks/{chunk-46EUUFJ5.js → chunk-EAC34EQS.js} +1 -1
  148. package/dist-cli/chunks/{chunk-OVFJFXUD.js → chunk-EG32ML36.js} +2 -2
  149. package/dist-cli/chunks/{chunk-5YJCOWCH.js → chunk-FXUAC6D5.js} +1 -1
  150. package/dist-cli/chunks/chunk-GQSL4USA.js +6 -0
  151. package/dist-cli/chunks/{chunk-VFCMSYZK.js → chunk-HAXW27SS.js} +2 -2
  152. package/dist-cli/chunks/{chunk-2D2UPBBR.js → chunk-IZAHPAN6.js} +1 -1
  153. package/dist-cli/chunks/{chunk-7GN3LVWB.js → chunk-J62KM5TB.js} +2 -2
  154. package/dist-cli/chunks/{chunk-BTWORNNG.js → chunk-JEMCD5E4.js} +1 -1
  155. package/dist-cli/chunks/{chunk-DCEMHR2Y.js → chunk-JZS3Q37N.js} +2 -2
  156. package/dist-cli/chunks/{chunk-D4FFVGI5.js → chunk-KR4JON7D.js} +1 -1
  157. package/dist-cli/chunks/chunk-KWCYKQIQ.js +4 -0
  158. package/dist-cli/chunks/{chunk-VNQEB4L7.js → chunk-MCWPL644.js} +2 -2
  159. package/dist-cli/chunks/chunk-MJYK3N2I.js +4 -0
  160. package/dist-cli/chunks/{chunk-5TEF3ET3.js → chunk-O6TRIZNS.js} +2 -2
  161. package/dist-cli/chunks/{chunk-QKDWYITG.js → chunk-P7XL2E73.js} +3 -3
  162. package/dist-cli/chunks/{chunk-BBULZ7CG.js → chunk-PFQTUKQ4.js} +62 -87
  163. package/dist-cli/chunks/{chunk-GGRX24GF.js → chunk-PG5RZCTN.js} +2 -2
  164. package/dist-cli/chunks/{chunk-W6K4EFPH.js → chunk-QGRI2Z4M.js} +2 -2
  165. package/dist-cli/chunks/{chunk-ZMJD5GEC.js → chunk-QLXXE7GE.js} +1 -1
  166. package/dist-cli/chunks/{chunk-VZXWHRUZ.js → chunk-QOJJJWJE.js} +89 -133
  167. package/dist-cli/chunks/{chunk-OZSSI4WN.js → chunk-RWZY5427.js} +2 -2
  168. package/dist-cli/chunks/{chunk-UC6U3MML.js → chunk-RZKU2K3J.js} +2 -2
  169. package/dist-cli/chunks/{chunk-IJ5CAZZC.js → chunk-SBV4IK4H.js} +1 -1
  170. package/dist-cli/chunks/{chunk-DZS6WPUI.js → chunk-SGMVFFMK.js} +1 -1
  171. package/dist-cli/chunks/{chunk-OHAZNXLK.js → chunk-TAX4UT2N.js} +1 -1
  172. package/dist-cli/chunks/{chunk-5DHC6KHQ.js → chunk-TUOFAWXT.js} +1 -1
  173. package/dist-cli/chunks/{chunk-GASE6UBA.js → chunk-UHZLOHGP.js} +1 -1
  174. package/dist-cli/chunks/{chunk-F5ZRSS3C.js → chunk-VC7V76U3.js} +1 -1
  175. package/dist-cli/chunks/{chunk-RTN5C5RL.js → chunk-VQVMLW4U.js} +1 -1
  176. package/dist-cli/chunks/{chunk-WMIIKMGK.js → chunk-VUKKYPZN.js} +2 -2
  177. package/dist-cli/chunks/{chunk-5TPRP5QT.js → chunk-WGGRJDRE.js} +1 -1
  178. package/dist-cli/chunks/chunk-X6H76EKP.js +15 -0
  179. package/dist-cli/chunks/chunk-XLZ5FNRT.js +27 -0
  180. package/dist-cli/chunks/{chunk-HI5TFJWN.js → chunk-XULEACM4.js} +2 -2
  181. package/dist-cli/chunks/{chunk-MJRLLB4R.js → chunk-YFSDM7AX.js} +4 -4
  182. package/dist-cli/chunks/chunk-YWI3UEVX.js +5 -0
  183. package/dist-cli/chunks/{chunk-XEVZYVIW.js → chunk-ZBSJO4NB.js} +10 -9
  184. package/dist-cli/chunks/{cleanup-P27PA6JI.js → cleanup-EYLGCA6Z.js} +2 -2
  185. package/dist-cli/chunks/cli-version-LL2UGIHE.js +4 -0
  186. package/dist-cli/chunks/{compat-ZD65FED3.js → compat-TLJYHB4E.js} +2 -2
  187. package/dist-cli/chunks/{config-XMJRNM2A.js → config-4JWUOEXK.js} +2 -2
  188. package/dist-cli/chunks/{control-KMIQT3QP.js → control-HPAOYF4N.js} +2 -2
  189. package/dist-cli/chunks/daemon-OYLASXLE.js +4 -0
  190. package/dist-cli/chunks/{debug-PT4HOP7N.js → debug-4BKXF6KI.js} +5 -5
  191. package/dist-cli/chunks/{desktop-S3FG72AK.js → desktop-ZQ6ZD2S6.js} +3 -3
  192. package/dist-cli/chunks/{detox-B3D4IFCN.js → detox-WNPASSS3.js} +2 -2
  193. package/dist-cli/chunks/{device-XBNDSB2R.js → device-BXLXVG3V.js} +2 -2
  194. package/dist-cli/chunks/{diagnose-HMQXJE5N.js → diagnose-3QX7W5RW.js} +2 -2
  195. package/dist-cli/chunks/{disk-cleanup-BLCZ5BSZ.js → disk-cleanup-KSWKI7WB.js} +2 -2
  196. package/dist-cli/chunks/drivers-EPIEFF7P.js +4 -0
  197. package/dist-cli/chunks/{film-BJGTBYZB.js → film-GDMR33VO.js} +3 -3
  198. package/dist-cli/chunks/flow-NKMPBYCJ.js +4 -0
  199. package/dist-cli/chunks/help-OGCBHOKA.js +4 -0
  200. package/dist-cli/chunks/{hidden-runtime-alias-ANOYADHM.js → hidden-runtime-alias-S2GTDX3T.js} +2 -2
  201. package/dist-cli/chunks/home-paths-QRCDLTTV.js +4 -0
  202. package/dist-cli/chunks/inspect-FUYMOZPY.js +4 -0
  203. package/dist-cli/chunks/install-desktop-FR6YKW7Y.js +4 -0
  204. package/dist-cli/chunks/{login-FJ737MWG.js → login-IJAPUZHI.js} +4 -4
  205. package/dist-cli/chunks/{logout-ZCNMMHMY.js → logout-QMXDFU2X.js} +2 -2
  206. package/dist-cli/chunks/{maestro-SZTNKLDF.js → maestro-ZXU3YCVX.js} +3 -3
  207. package/dist-cli/chunks/{maestro-generate-DCFAIZ4H.js → maestro-generate-PYB5QY7K.js} +3 -3
  208. package/dist-cli/chunks/{mode-GRMQCRXR.js → mode-WGUCL5FZ.js} +2 -2
  209. package/dist-cli/chunks/{optional-demo-registry-W36EWFFB.js → optional-demo-registry-WH2O6H36.js} +2 -2
  210. package/dist-cli/chunks/{perf-QYBAAUZG.js → perf-TOD3UFAH.js} +2 -2
  211. package/dist-cli/chunks/{permissions-3QCQ6VF4.js → permissions-I5BRJGTB.js} +2 -2
  212. package/dist-cli/chunks/{record-QPPC2S4E.js → record-ZYL2FYSK.js} +3 -3
  213. package/dist-cli/chunks/{report-issue-7NMFP4HK.js → report-issue-TAI6DYZO.js} +2 -2
  214. package/dist-cli/chunks/reset-7YGYKQPQ.js +4 -0
  215. package/dist-cli/chunks/runtime-B4JO6QRI.js +4 -0
  216. package/dist-cli/chunks/{screenshot-command-67AECJFB.js → screenshot-command-7ANLODZY.js} +7 -7
  217. package/dist-cli/chunks/{screenshot-layers-ASWBYPJL.js → screenshot-layers-A7FYXSVU.js} +3 -3
  218. package/dist-cli/chunks/{screenshots-capture-PXA3HFQK.js → screenshots-capture-WT2ZY6CB.js} +2 -2
  219. package/dist-cli/chunks/serve-TG5WKA4V.js +44 -0
  220. package/dist-cli/chunks/{setup-7DWPMRSB.js → setup-PCWLD22V.js} +2 -2
  221. package/dist-cli/chunks/{skills-S3Y22TUA.js → skills-27ZHWCQS.js} +2 -2
  222. package/dist-cli/chunks/state-ZQVY46ZO.js +14 -0
  223. package/dist-cli/chunks/{storage-XUIMJWAJ.js → storage-OYC57C4X.js} +6 -6
  224. package/dist-cli/chunks/store-32HSPZHI.js +4 -0
  225. package/dist-cli/chunks/telemetry-PBJR7XHR.js +4 -0
  226. package/dist-cli/chunks/{timeline-TMPLQPSP.js → timeline-RAJJFQT6.js} +2 -2
  227. package/dist-cli/chunks/{upgrade-7HDSIM7K.js → upgrade-OEFNZRIP.js} +2 -2
  228. package/dist-cli/chunks/upload-LQJITLKK.js +4 -0
  229. package/dist-cli/chunks/version-NIXTY6PL.js +6 -0
  230. package/dist-cli/chunks/{web-DG3WBYD3.js → web-XBUTBVGR.js} +2 -2
  231. package/dist-cli/chunks/{what-happened-XFVUTZR7.js → what-happened-YNHRFUDX.js} +3 -3
  232. package/dist-lib/agent-daemon-client.cjs +1 -1
  233. package/dist-lib/agent-events.cjs +1 -1
  234. package/dist-lib/agent-identity.cjs +1 -1
  235. package/dist-lib/agent-sessions.cjs +1 -1
  236. package/dist-lib/attached-projects.cjs +1 -1
  237. package/dist-lib/auth/shared-session.cjs +1 -1
  238. package/dist-lib/backend-origin.cjs +1 -1
  239. package/dist-lib/beta.cjs +1 -1
  240. package/dist-lib/beta.mjs +1 -1
  241. package/dist-lib/bridge-constants.cjs +1 -1
  242. package/dist-lib/bridge-contract.cjs +20 -0
  243. package/dist-lib/cli-constants.cjs +1 -1
  244. package/dist-lib/config.cjs +1 -1
  245. package/dist-lib/detox/index.cjs +1 -1
  246. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  247. package/dist-lib/home-paths.cjs +67 -28
  248. package/dist-lib/host/bridge-host.cjs +140 -12
  249. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  250. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  251. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  252. package/dist-lib/host/websocket-proxy.cjs +1 -1
  253. package/dist-lib/index.cjs +2815 -40
  254. package/dist-lib/jump-to-source-babel.cjs +1 -1
  255. package/dist-lib/menu.cjs +1 -1
  256. package/dist-lib/menu.mjs +1 -1
  257. package/dist-lib/metro.cjs +1 -1
  258. package/dist-lib/profiles.cjs +1 -1
  259. package/dist-lib/public-brand.cjs +1 -1
  260. package/dist-lib/render-mode.cjs +1 -1
  261. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  262. package/dist-lib/sdk.cjs +2549 -2061
  263. package/dist-lib/sdk.mjs +2543 -2061
  264. package/dist-lib/skills.cjs +480 -280
  265. package/dist-lib/vite.cjs +1 -1
  266. package/package.json +8 -2
  267. package/src/bridge-constants.ts +3 -4
  268. package/src/bridge-contract.ts +251 -0
  269. package/src/connect.ts +83 -0
  270. package/src/disk-cleanup.ts +30 -0
  271. package/src/home-paths.ts +81 -38
  272. package/src/host/bridge-host.ts +134 -6
  273. package/src/index.ts +27 -1
  274. package/src/sdk.ts +8 -0
  275. package/src/sim-client.ts +660 -0
  276. package/dist-cli/chunks/camera-VL73YIKP.js +0 -22
  277. package/dist-cli/chunks/chunk-4NPPOV2N.js +0 -5
  278. package/dist-cli/chunks/chunk-FSUYIVJ6.js +0 -9
  279. package/dist-cli/chunks/chunk-G2WW6L2C.js +0 -23
  280. package/dist-cli/chunks/chunk-KTHV3RUS.js +0 -26
  281. package/dist-cli/chunks/chunk-LF2ZVT7O.js +0 -6
  282. package/dist-cli/chunks/chunk-NFK7T35W.js +0 -4
  283. package/dist-cli/chunks/chunk-TIVZIMMW.js +0 -4
  284. package/dist-cli/chunks/cli-version-WWLPBDQ7.js +0 -4
  285. package/dist-cli/chunks/daemon-G2ME7NLB.js +0 -4
  286. package/dist-cli/chunks/drivers-LDECZGP2.js +0 -4
  287. package/dist-cli/chunks/flow-UEQNVTU7.js +0 -4
  288. package/dist-cli/chunks/help-T5FYSVGB.js +0 -4
  289. package/dist-cli/chunks/home-paths-GT3LFNOR.js +0 -4
  290. package/dist-cli/chunks/inspect-ZA6XF5LD.js +0 -4
  291. package/dist-cli/chunks/install-desktop-TIMUDHPL.js +0 -4
  292. package/dist-cli/chunks/runtime-XOAXMSTU.js +0 -4
  293. package/dist-cli/chunks/serve-BI2NBAXG.js +0 -44
  294. package/dist-cli/chunks/store-JTHEJLAZ.js +0 -4
  295. package/dist-cli/chunks/telemetry-ZYJGD2DB.js +0 -4
  296. package/dist-cli/chunks/upload-GMSZPWM6.js +0 -4
  297. package/dist-cli/chunks/version-HOCHZ37L.js +0 -6
package/dist-lib/sdk.mjs CHANGED
@@ -1,2174 +1,2647 @@
1
- /*! rnx v0.1.313 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.314 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
+ var __defProp = Object.defineProperty;
3
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
4
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
2
5
 
3
- // cli/commands/inspect/core.ts
4
- function resolveMaxMsFlag(args, fallbackMs) {
5
- const aliases = ["--max-ms", "--maxMs", "--maxms", "--max_ms"];
6
- for (const flag of aliases) {
7
- const i = args.indexOf(flag);
8
- if (i >= 0 && args[i + 1]) {
9
- const n = Number(args[i + 1]);
10
- if (Number.isFinite(n)) return Math.max(100, n);
11
- }
12
- }
13
- return fallbackMs;
14
- }
15
- var INSPECT_PICK_COMMANDS = /* @__PURE__ */ new Set([
16
- "tap",
17
- "double-tap",
18
- "tap-text",
19
- "tap-id",
20
- "long-press",
21
- "touch"
22
- ]);
23
- function isInspectPickCommand(subcommand) {
24
- return typeof subcommand === "string" && INSPECT_PICK_COMMANDS.has(subcommand);
25
- }
26
- async function isInspectModeActive(bridge) {
27
- const active = await bridge.send({
28
- type: "evaluate",
29
- code: "window.__sootsimEngineState?.inspectActive === true"
30
- });
31
- return active === true;
32
- }
33
- async function shouldSkipAutoSettleForInspectPick(bridge, subcommand) {
34
- if (!isInspectPickCommand(subcommand)) return false;
35
- try {
36
- return await isInspectModeActive(bridge);
37
- } catch {
38
- return false;
39
- }
40
- }
41
- async function inspectNodeCount(bridge) {
42
- const count = await bridge.send({
43
- type: "evaluate",
44
- code: "(async () => await window.__sootsimTest.getNodeCount())()"
45
- });
46
- return { nodes: typeof count === "number" ? count : 0 };
47
- }
48
- async function inspectTree(bridge, depth = 5, opts) {
49
- const optsArg = opts?.format ? `, ${JSON.stringify(opts)}` : "";
50
- const tree = await bridge.send({
51
- type: "evaluate",
52
- code: `(async () => await window.__sootsimTest.dumpTree(${depth}${optsArg}))()`
53
- });
54
- if (opts?.format === "boxes" && typeof tree === "string") {
55
- try {
56
- return { depth, tree: JSON.parse(tree) };
57
- } catch {
58
- return { depth, tree };
59
- }
60
- }
61
- return { depth, tree };
62
- }
63
- async function inspectUrl(bridge) {
64
- const url = await bridge.send({ type: "evaluate", code: "window.location.href" });
65
- return { url: typeof url === "string" ? url : "" };
66
- }
67
- async function inspectDescribe(bridge, dumpOpts) {
68
- const code = `(async () => {
69
- const t = window.__sootsimTest
70
- const mainShell = window.SootSim?.bridges?.mainShell
71
- const kb = window.__sootsimKeyboard
72
- if (!t) return { error: 'no test bridge' }
6
+ // cli/commands/inspect/settling.ts
7
+ async function waitForSootsimIdle({
8
+ bridge,
9
+ simId,
10
+ maxMs,
11
+ pollMs = 50,
12
+ stablePolls = 3,
13
+ strict = false
14
+ }) {
15
+ const result = await bridge.send(
16
+ {
17
+ type: "evaluate",
18
+ simId,
19
+ code: `(async () => {
20
+ const start = Date.now()
21
+ const deadline = start + ${Math.max(0, Math.round(maxMs))}
22
+ const pollMs = ${Math.max(1, Math.round(pollMs))}
23
+ const requiredStablePolls = ${Math.max(1, Math.round(stablePolls))}
24
+ const strict = ${strict ? "true" : "false"}
25
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
73
26
 
74
- let shell = null
27
+ // route-aware settle: a tap that pushes/pops a screen is async \u2014 the
28
+ // old screen still renders for a moment, then the new one mounts and
29
+ // its content loads. a pure layout-hash check reports "idle" on the
30
+ // outgoing screen or on the incoming skeleton, so a driver re-taps
31
+ // and stacks duplicate navigations. drain any in-flight screen
32
+ // transition FIRST, bounded by the overall budget. when no
33
+ // transition is happening this returns in ~80ms (startWindowMs), so
34
+ // a plain button tap barely pays for it.
75
35
  try {
76
- shell = typeof mainShell?.getState === 'function' ? await mainShell.getState() : null
36
+ const wfst = window.__sootsimTest?.waitForScreenTransitions
37
+ if (typeof wfst === 'function') {
38
+ const remaining = deadline - Date.now()
39
+ if (remaining > 120) {
40
+ await wfst({
41
+ timeoutMs: Math.min(remaining - 80, 4000),
42
+ settleMs: 64,
43
+ startWindowMs: 80,
44
+ })
45
+ }
46
+ }
77
47
  } catch {}
78
48
 
79
- const opts = ${JSON.stringify(dumpOpts)}
80
- const needsRecursiveDump = opts.verbose || opts.subtreeRoot || opts.includeOccluded
81
- const globToRegex = (glob) => {
82
- const escaped = String(glob).replace(/[.+^$(){}|[\\]\\\\]/g, '\\\\$&')
83
- const body = escaped.replace(/\\*/g, '.*').replace(/\\?/g, '.')
84
- return new RegExp('^' + body + '$', 'i')
85
- }
86
- // clipping a describe line is a DISPLAY decision. opts.fullText (set by
87
- // \`describe --json\`) keeps the real string, so a bundle-eval stack or a
88
- // red-box message reaches the caller whole instead of ending in an
89
- // ellipsis nothing downstream can undo.
90
- const clipForDisplay = (value, max) => {
91
- const text = typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''
92
- if (opts.fullText || text.length <= max) return text
93
- return text.slice(0, max - 1).trimEnd() + '\u2026'
94
- }
95
- const formatInspectableTree = async () => {
96
- if (typeof t.listInspectable !== 'function') return null
97
- const list = await t.listInspectable({})
98
- if (!Array.isArray(list)) return null
99
- const filter = typeof opts.filter === 'string' ? opts.filter.toLowerCase() : ''
100
- const testIdRegex = opts.testIdLike ? globToRegex(opts.testIdLike) : null
101
- const onlyRegex = opts.onlyGlob ? globToRegex(opts.onlyGlob) : null
102
- const lines = []
103
- let matched = 0
104
- for (const node of list) {
105
- const testID = node?.testID || node?.testId || ''
106
- const role = node?.role || ''
107
- const label = node?.accessibilityLabel || node?.label || ''
108
- const text = node?.text || ''
109
- if (testIdRegex && !testIdRegex.test(testID)) continue
110
- const joined = [role, label, text, testID].filter(Boolean).join(' ')
111
- if (onlyRegex && !onlyRegex.test(joined)) continue
112
- if (filter && !joined.toLowerCase().includes(filter)) continue
113
- matched++
114
-
115
- const type = String(node?.type || 'node').toLowerCase()
116
- const handlers = Array.isArray(node?.handlers) ? node.handlers : []
117
- const parts = []
118
- if (role) parts.push('[' + role + ']')
119
- else if (handlers.length > 0) parts.push('[tap]')
120
- else parts.push('<' + type + '>')
121
- const visibleText = clipForDisplay(text, 50)
122
- if (visibleText) parts.push(JSON.stringify(visibleText))
123
- if (testID) parts.push('#' + testID)
124
- if (label && label !== text) {
125
- parts.push('label=' + JSON.stringify(clipForDisplay(label, 50)))
49
+ const readSnapshot = async () => {
50
+ let animating = false
51
+ let pendingFetches = 0
52
+ let requestTotal = 0
53
+ let requestInFlight = 0
54
+ try {
55
+ const stats = await window.__sootsimRenderHost?.queryStats?.()
56
+ if (stats) {
57
+ animating =
58
+ stats.hasActiveAnims === true ||
59
+ stats.hasActiveNativeAnimations === true ||
60
+ stats.hasPendingAnimationFrames === true
61
+ // a freshly-pushed screen showing a skeleton is layout-stable
62
+ // but not actually settled \u2014 its data/images are still in
63
+ // flight. treat bounded image-loader fetches as not-idle so
64
+ // settle waits for real content, capped by the budget.
65
+ const pf = stats.memory && stats.memory.imageLoader
66
+ ? stats.memory.imageLoader.pendingFetches
67
+ : 0
68
+ pendingFetches = typeof pf === 'number' ? pf : 0
126
69
  }
127
- if (!opts.hideXy) {
128
- const abs = node?.absolute || {}
129
- const layout = node?.layout || {}
130
- const x = Math.round(Number(abs.x) || 0)
131
- const y = Math.round(Number(abs.y) || 0)
132
- const w = Math.round(Number(layout.width) || 0)
133
- const h = Math.round(Number(layout.height) || 0)
134
- parts.push('@(' + x + ',' + y + ') ' + w + 'x' + h)
70
+ } catch {}
71
+ try {
72
+ const counts = await window.__sootsimTest?.getRequestCounts?.()
73
+ requestTotal = typeof counts?.total === 'number' ? counts.total : 0
74
+ requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
75
+ } catch {}
76
+ const root = window.__sootsimRoot
77
+ const nodes = []
78
+ if (root) {
79
+ const walk = (n) => {
80
+ if (n.layout && n.layout.width > 0) {
81
+ nodes.push(Math.round(n.layout.x) + ',' + Math.round(n.layout.y) + ',' + Math.round(n.layout.width))
82
+ }
83
+ for (const c of n.children || []) walk(c)
135
84
  }
136
- if (handlers.length > 0) parts.push('(tap)')
137
- lines.push(' ' + parts.join(' '))
85
+ walk(root)
138
86
  }
139
- return { tree: lines.join('\\n'), matched }
87
+ return { layout: nodes.join(';'), animating, pendingFetches, requestTotal, requestInFlight }
140
88
  }
141
89
 
142
- let tree = ''
143
- const compactResult = needsRecursiveDump ? null : await formatInspectableTree()
144
- if (compactResult) {
145
- tree = compactResult.tree
146
- } else {
147
- tree = await t.dumpTree(12, opts)
90
+ // how long generic background network is allowed to keep us waiting AFTER
91
+ // the screen is otherwise visually settled. apps with continuous traffic
92
+ // (polling, realtime sockets, a tap that fires 20-50 fetches) never reach
93
+ // requestInFlight===0 / a stable requestTotal, so requiring full network
94
+ // quiet burns the ENTIRE budget on every command \u2014 the leading idle that
95
+ // failed PR-preview recordings for network-heavy apps (3pc fight-pulse).
96
+ // layout-stability + the image-loader already prove visible content
97
+ // loaded (a data load changes layout; an image load shows in the loader),
98
+ // so the generic request counters are a courtesy, not a gate: honor them
99
+ // only within this grace once the screen is visually still.
100
+ const networkQuietGraceMs = 1200
101
+ let lastLayout = ''
102
+ let lastRequestTotal = -1
103
+ let stable = 0
104
+ let visuallyStillSince = 0
105
+ while (Date.now() < deadline) {
106
+ const snapshot = await readSnapshot()
107
+ const strictOk = !strict || !snapshot.animating
108
+ // visually settled: no animations, no image content loading, layout
109
+ // unchanged since the last poll.
110
+ const visuallyStill =
111
+ strictOk && snapshot.pendingFetches === 0 && snapshot.layout === lastLayout
112
+ // generic network quiet: nothing in flight and no new requests issued.
113
+ const networkQuiet =
114
+ snapshot.requestInFlight === 0 && snapshot.requestTotal === lastRequestTotal
115
+ if (visuallyStill) {
116
+ stable++
117
+ if (visuallyStillSince === 0) visuallyStillSince = Date.now()
118
+ // settle once the screen has held still long enough AND either the
119
+ // network is genuinely quiet OR it has refused to quiet within the
120
+ // grace (continuous background traffic must not block forever).
121
+ if (
122
+ stable >= requiredStablePolls &&
123
+ (networkQuiet || Date.now() - visuallyStillSince >= networkQuietGraceMs)
124
+ ) {
125
+ return { settled: true, elapsed: Date.now() - start }
126
+ }
127
+ } else {
128
+ stable = 0
129
+ visuallyStillSince = 0
130
+ }
131
+ lastLayout = snapshot.layout
132
+ lastRequestTotal = snapshot.requestTotal
133
+ await sleep(pollMs)
148
134
  }
149
- const nodeCount = (await t.getNodeCount?.()) || 0
150
- const keyboard = kb && typeof kb.getLayout === 'function' ? kb.getLayout() : null
151
- return { tree, shell, nodeCount, keyboard }
152
- })()`;
153
- const result = await bridge.send({ type: "evaluate", code });
154
- return result ?? {};
155
- }
156
- function scoreLayoutTextContrast(input) {
157
- const required = input.fontSize >= 18 || input.fontSize >= 14 && (input.fontWeight ?? 400) >= 700 ? 3 : 4.5;
158
- const { normal, background } = input;
159
- if (normal.width <= 0 || normal.height <= 0 || normal.width !== background.width || normal.height !== background.height) {
160
- return { ratio: null, required, reason: "capture dimensions do not match" };
161
- }
162
- const left = Math.max(0, Math.floor(input.box.x));
163
- const top = Math.max(0, Math.floor(input.box.y));
164
- const right = Math.min(normal.width, Math.ceil(input.box.x + input.box.w));
165
- const bottom = Math.min(normal.height, Math.ceil(input.box.y + input.box.h));
166
- if (right <= left || bottom <= top) {
167
- return { ratio: null, required, reason: "text is outside the captured frame" };
168
- }
169
- const linear = (value) => {
170
- const channel = value / 255;
171
- return channel <= 0.04045 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
172
- };
173
- const ratioOf = (foreground, behind) => {
174
- const foregroundLuminance = 0.2126 * linear(foreground[0]) + 0.7152 * linear(foreground[1]) + 0.0722 * linear(foreground[2]);
175
- const backgroundLuminance = 0.2126 * linear(behind[0]) + 0.7152 * linear(behind[1]) + 0.0722 * linear(behind[2]);
176
- return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05) / (Math.min(foregroundLuminance, backgroundLuminance) + 0.05);
177
- };
178
- const hex = (color) => `#${color.slice(0, 3).map((value) => Math.round(value).toString(16).padStart(2, "0")).join("")}`;
179
- const area = (right - left) * (bottom - top);
180
- const step = Math.max(1, Math.floor(Math.sqrt(area / 12e3)));
181
- const offsetAt = (x, y) => (y * normal.width + x) * 4;
182
- const deltaAt = (offset) => Math.max(
183
- Math.abs(normal.data[offset] - background.data[offset]),
184
- Math.abs(normal.data[offset + 1] - background.data[offset + 1]),
185
- Math.abs(normal.data[offset + 2] - background.data[offset + 2])
186
- );
187
- let strongestDelta = 0;
188
- let strongestForeground = null;
189
- let opaqueBackground = null;
190
- for (let y = top; y < bottom; y += step) {
191
- for (let x = left; x < right; x += step) {
192
- const offset = offsetAt(x, y);
193
- if (background.data[offset + 3] < 250) continue;
194
- const behind = [
195
- background.data[offset],
196
- background.data[offset + 1],
197
- background.data[offset + 2]
198
- ];
199
- opaqueBackground ?? (opaqueBackground = behind);
200
- const delta = deltaAt(offset);
201
- if (delta <= strongestDelta) continue;
202
- strongestDelta = delta;
203
- strongestForeground = [
204
- normal.data[offset],
205
- normal.data[offset + 1],
206
- normal.data[offset + 2]
207
- ];
135
+ return { settled: false, elapsed: Date.now() - start }
136
+ })()`
137
+ },
138
+ {
139
+ timeoutMs: maxMs + 1e3
208
140
  }
209
- }
210
- let bestRatio = Infinity;
211
- let bestForeground = null;
212
- let bestBackground = null;
213
- const consider = (foreground, behind) => {
214
- const ratio = ratioOf(foreground, behind);
215
- if (ratio >= bestRatio) return;
216
- bestRatio = ratio;
217
- bestForeground = foreground;
218
- bestBackground = behind;
141
+ );
142
+ const { elapsed, settled } = result ?? {};
143
+ return {
144
+ elapsed: typeof elapsed === "number" ? elapsed : maxMs,
145
+ settled: settled === true
219
146
  };
220
- if (strongestDelta >= 2 && strongestForeground) {
221
- for (let y = top; y < bottom; y += step) {
222
- for (let x = left; x < right; x += step) {
223
- const offset = offsetAt(x, y);
224
- if (background.data[offset + 3] < 250 || deltaAt(offset) < 2) {
225
- continue;
226
- }
227
- consider(strongestForeground, [
228
- background.data[offset],
229
- background.data[offset + 1],
230
- background.data[offset + 2]
231
- ]);
232
- }
233
- }
234
- } else if (opaqueBackground) {
235
- consider(opaqueBackground, opaqueBackground);
236
- }
237
- if (!bestForeground || !bestBackground || !Number.isFinite(bestRatio)) {
238
- return {
239
- ratio: null,
240
- required,
241
- reason: "could not isolate opaque rendered text pixels"
242
- };
147
+ }
148
+
149
+ // cli/commands/inspect/actions.ts
150
+ var SCREEN_EVAL = `(() => {
151
+ const spec = window.SootSim?.state?.engineSnapshot?.windowState?.deviceSpec
152
+ return spec && spec.width > 0 && spec.height > 0
153
+ ? { width: spec.width, height: spec.height }
154
+ : null
155
+ })()`;
156
+ var DEFAULT_AGENT_TIMING = {
157
+ initialWaitMs: 3e3,
158
+ deadlineMs: 5e3,
159
+ retryWaitMs: 700
160
+ };
161
+ var DEFAULT_INTERACTIVE_TIMING = {
162
+ initialWaitMs: 1200,
163
+ deadlineMs: 2500,
164
+ retryWaitMs: 700
165
+ };
166
+ function tapTiming(agent, opts = {}) {
167
+ return {
168
+ ...agent ? DEFAULT_AGENT_TIMING : DEFAULT_INTERACTIVE_TIMING,
169
+ ...opts
170
+ };
171
+ }
172
+ function isTapSuccess(result) {
173
+ if (!result || result.hit === false || result.ok === false) return false;
174
+ if (result.requestedTargetMatched === false) return false;
175
+ if (result.pointerTapHandled === false && !result.keyboardOpened && !result.isTextInput) {
176
+ return false;
243
177
  }
178
+ return true;
179
+ }
180
+ function tapTargetFromPayload(payload, textFallback) {
244
181
  return {
245
- ratio: Math.round(bestRatio * 100) / 100,
246
- required,
247
- foreground: hex(bestForeground),
248
- background: hex(bestBackground)
182
+ nodeId: payload.target?.nodeId ?? payload.match?.nodeId ?? payload.node?.nodeId ?? null,
183
+ id: payload.target?.id ?? payload.match?.id ?? payload.node?.id ?? null,
184
+ testID: payload.target?.testID ?? payload.match?.testID ?? payload.node?.testID ?? null,
185
+ text: payload.target?.text ?? payload.target?.accessibilityLabel ?? payload.match?.text ?? payload.match?.accessibilityLabel ?? payload.node?.text ?? textFallback ?? null,
186
+ type: payload.target?.type ?? payload.match?.type ?? payload.node?.type ?? null
249
187
  };
250
188
  }
251
- function layoutGetNativeEval(opts = {}) {
252
- const includeStyle = opts.styling === true;
253
- const filter = opts.filter?.trim().toLowerCase() ?? "";
254
- return `(async () => {
255
- const t = window.__sootsimTest
256
- if (!t || typeof t.listInspectable !== 'function') return []
257
- const list = await t.listInspectable({})
258
- if (!Array.isArray(list)) return []
259
- const includeStyle = ${JSON.stringify(includeStyle)}
260
- const scoreContrast = ${scoreLayoutTextContrast.toString()}
261
- // narrows by ELEMENT, not by style field: fewer rows, full detail on each.
262
- const filter = ${JSON.stringify(filter)}
263
- const matchesFilter = (n, text) => {
264
- if (!filter) return true
265
- const fields = [text, n.testID || n.testId, n.accessibilityRole || n.role, n.type]
266
- return fields.some((f) => typeof f === 'string' && f.toLowerCase().includes(filter))
267
- }
268
-
269
- const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0)
270
- const round = (v) => Math.round(num(v))
271
-
272
- // borderRadius can be authored as a string ("12px") or split per-corner;
273
- // prefer the uniform borderRadius, else the largest corner radius present.
274
- const radiusOf = (style) => {
275
- if (!style) return 0
276
- const keys = ['borderRadius','borderTopLeftRadius','borderTopRightRadius','borderBottomLeftRadius','borderBottomRightRadius']
277
- let max = 0
278
- for (const k of keys) {
279
- const raw = style[k]
280
- const n = typeof raw === 'number' ? raw : (typeof raw === 'string' ? parseFloat(raw) : NaN)
281
- if (Number.isFinite(n)) max = Math.max(max, n)
282
- }
283
- return Math.round(max)
284
- }
285
- const fontSizeOf = (style) => {
286
- if (!style) return 0
287
- const raw = style.fontSize
288
- const n = typeof raw === 'number' ? raw : (typeof raw === 'string' ? parseFloat(raw) : NaN)
289
- return Number.isFinite(n) ? Math.round(n) : 0
290
- }
291
- // styling extractors \u2014 omit boring defaults so the agent only sees notable
292
- // styling (low opacity, struck text, clipping, a real border/color).
293
- const colorStr = (v) => {
294
- if (v == null) return undefined
295
- const s = String(v).split(' ').join('')
296
- return s && s !== 'transparent' && s !== 'rgba(0,0,0,0)' ? s : undefined
297
- }
298
- const opacityOf = (style) => {
299
- const o = style && style.opacity
300
- const n = typeof o === 'number' ? o : (typeof o === 'string' ? parseFloat(o) : NaN)
301
- return Number.isFinite(n) && n < 1 ? Math.round(n * 100) / 100 : undefined
302
- }
303
- const weightOf = (style) => {
304
- const w = style && style.fontWeight
305
- const n = w === 'bold' ? 700 : (typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN))
306
- return Number.isFinite(n) && n >= 600 ? n : undefined
307
- }
308
- const decoOf = (style) => {
309
- const d = style && (style.textDecorationLine || style.textDecoration)
310
- return d && d !== 'none' && d !== 'normal' ? String(d).split(' ')[0] : undefined
311
- }
312
- const overflowOf = (style) => (style && style.overflow === 'hidden' ? 'hidden' : undefined)
313
- const borderWOf = (style) => {
314
- if (!style) return undefined
315
- const keys = ['borderWidth','borderTopWidth','borderBottomWidth','borderLeftWidth','borderRightWidth']
316
- let max = 0
317
- for (const k of keys) {
318
- const r = style[k]
319
- const n = typeof r === 'number' ? r : (typeof r === 'string' ? parseFloat(r) : NaN)
320
- if (Number.isFinite(n)) max = Math.max(max, n)
189
+ async function tapResolvedTarget(bridge, args) {
190
+ const timing = tapTiming(!!args.agent, args.timing);
191
+ let attempts = 0;
192
+ let lastPayload = null;
193
+ let lastResult = null;
194
+ try {
195
+ await waitForSootsimIdle({
196
+ bridge,
197
+ maxMs: timing.initialWaitMs,
198
+ pollMs: 32,
199
+ stablePolls: 2
200
+ });
201
+ } catch {
202
+ }
203
+ const deadline = Date.now() + timing.deadlineMs;
204
+ let lastOffscreenAt = null;
205
+ while (Date.now() <= deadline || attempts === 0) {
206
+ attempts++;
207
+ const payload = await args.resolve();
208
+ lastPayload = payload;
209
+ if (payload?.error === "bridge-not-ready" || payload?.ambiguous || payload?.nthOutOfRange) {
210
+ return { payload, result: null, attempts, failure: "special" };
211
+ }
212
+ if (payload && typeof payload.cx === "number" && typeof payload.cy === "number") {
213
+ if (payload.offscreen) {
214
+ lastResult = {
215
+ hit: false,
216
+ reason: "offscreen",
217
+ x: payload.cx,
218
+ y: payload.cy,
219
+ screen: payload.screen
220
+ };
221
+ const at = `${Math.round(payload.cx)},${Math.round(payload.cy)}`;
222
+ if (lastOffscreenAt === at) {
223
+ return { payload, result: lastResult, attempts, failure: "missed" };
321
224
  }
322
- return max > 0 ? Math.round(max) : undefined
323
- }
324
- // only meaningful alongside a real border width, so it is resolved by the
325
- // caller below rather than on its own.
326
- const borderColorOf = (style) => {
327
- if (!style) return undefined
328
- const keys = ['borderColor','borderTopColor','borderBottomColor','borderLeftColor','borderRightColor']
329
- for (const k of keys) {
330
- const c = colorStr(style[k])
331
- if (c) return c
332
- }
333
- return undefined
334
- }
335
-
336
- const isVisible = (n) => {
337
- const layout = n && n.layout
338
- if (!layout || layout.width <= 0 || layout.height <= 0) return false
339
- const abs = (n && (n.absolute || n.absolutePosition)) || null
340
- if (!abs) return true
341
- const device = (n && n.device) || {}
342
- const screenW = Number(device.width) || window.innerWidth || 0
343
- const screenH = Number(device.height) || window.innerHeight || 0
344
- if (!screenW || !screenH) return true
345
- return abs.x + layout.width > 0 && abs.y + layout.height > 0 && abs.x < screenW && abs.y < screenH
346
- }
347
-
348
- const elements = list.filter(isVisible).filter((n) => {
349
- const t = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
350
- return matchesFilter(n, t)
351
- }).map((n) => {
352
- const layout = n.layout || { x: 0, y: 0, width: 0, height: 0 }
353
- const abs = n.absolute || n.absolutePosition || { x: layout.x, y: layout.y }
354
- const text = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
355
- const base = {
356
- ...(typeof n.nodeId === 'number'
357
- ? {
358
- nodeId: n.nodeId,
359
- ancestorIds: Array.isArray(n.ancestors)
360
- ? n.ancestors.map((ancestor) => ancestor && ancestor.nodeId).filter((id) => typeof id === 'number')
361
- : [],
225
+ lastOffscreenAt = at;
226
+ } else {
227
+ lastOffscreenAt = null;
228
+ const requestedTarget = tapTargetFromPayload(payload, args.textFallback);
229
+ const verification = await bridge.send({
230
+ type: "evaluate",
231
+ code: `(async () => {
232
+ const t = window.__sootsimTest
233
+ if (
234
+ !t ||
235
+ typeof t.inspectAt !== 'function' ||
236
+ typeof t.resolveTapTarget !== 'function'
237
+ ) {
238
+ return { error: 'tap-target-inspection-unavailable' }
239
+ }
240
+ const requestedTarget = ${JSON.stringify(requestedTarget)}
241
+ const inspected = await t.inspectAt(${payload.cx}, ${payload.cy})
242
+ const resolved =
243
+ inspected && typeof inspected.nodeId === 'number'
244
+ ? await t.resolveTapTarget(inspected.nodeId)
245
+ : null
246
+ const node = (resolved && resolved.target) || inspected
247
+ const coordinateTarget = node
248
+ ? {
249
+ nodeId: node.nodeId ?? null,
250
+ id: node.id ?? null,
251
+ testID: node.testID ?? null,
252
+ text: node.text ?? node.accessibilityLabel ?? null,
253
+ type: node.type ?? null,
254
+ }
255
+ : null
256
+ const coordinateAncestors = Array.isArray(inspected?.ancestors)
257
+ ? inspected.ancestors
258
+ : []
259
+ const ancestorMatchesRequested = coordinateAncestors.some(
260
+ (ancestor) =>
261
+ (typeof requestedTarget.nodeId === 'number' &&
262
+ ancestor?.nodeId === requestedTarget.nodeId) ||
263
+ (requestedTarget.testID &&
264
+ ancestor?.testID === requestedTarget.testID) ||
265
+ (requestedTarget.id && ancestor?.id === requestedTarget.id),
266
+ )
267
+ const requestedTargetMatched =
268
+ coordinateTarget === null
269
+ ? false
270
+ : typeof requestedTarget.nodeId === 'number' &&
271
+ typeof coordinateTarget.nodeId === 'number'
272
+ ? requestedTarget.nodeId === coordinateTarget.nodeId ||
273
+ ancestorMatchesRequested
274
+ : requestedTarget.testID && coordinateTarget.testID
275
+ ? requestedTarget.testID === coordinateTarget.testID
276
+ || ancestorMatchesRequested
277
+ : requestedTarget.id && coordinateTarget.id
278
+ ? requestedTarget.id === coordinateTarget.id
279
+ || ancestorMatchesRequested
280
+ : ancestorMatchesRequested
281
+ if (!requestedTargetMatched) {
282
+ return {
283
+ requestedTargetMatched,
284
+ requestedTarget,
285
+ coordinateTarget,
362
286
  }
363
- : {}),
364
- testID: n.testID || n.testId || undefined,
365
- role: n.accessibilityRole || n.role || undefined,
366
- type: typeof n.type === 'string' ? n.type : 'node',
367
- text,
368
- box: { x: round(abs.x), y: round(abs.y), w: round(layout.width), h: round(layout.height) },
369
- borderRadius: 0,
370
- padding: { t: 0, r: 0, b: 0, l: 0 },
371
- fontSize: 0,
287
+ }
288
+ if (typeof t.activatePressAt !== 'function') {
289
+ return { error: 'tap-target-activation-unavailable' }
290
+ }
291
+ const activation = await t.activatePressAt(
292
+ ${payload.cx},
293
+ ${payload.cy},
294
+ requestedTarget.nodeId,
295
+ )
296
+ if (activation?.ok) {
297
+ window.dispatchEvent(
298
+ new CustomEvent('sootsim:agentAction', {
299
+ detail: {
300
+ type: 'tap',
301
+ x: ${payload.cx},
302
+ y: ${payload.cy},
303
+ target: requestedTarget,
304
+ },
305
+ }),
306
+ )
307
+ }
308
+ return {
309
+ activation,
310
+ requestedTargetMatched,
311
+ requestedTarget,
312
+ coordinateTarget,
313
+ }
314
+ })()`
315
+ });
316
+ if (verification?.error) {
317
+ return {
318
+ payload,
319
+ result: {
320
+ hit: false,
321
+ reason: verification.error,
322
+ requestedTarget
323
+ },
324
+ attempts,
325
+ failure: "special"
326
+ };
372
327
  }
373
- if (!includeStyle) return base
374
- // computedStyle has the resolved values; style is the curated subset \u2014
375
- // fall back across both so radius/fontSize survive either shape.
376
- const style = Object.assign({}, n.style || {}, n.computedStyle || {})
377
- const pad = (n.boxModel && n.boxModel.padding) || {}
378
- const borderWidth = borderWOf(style)
379
- return {
380
- ...base,
381
- borderRadius: radiusOf(style),
382
- padding: { t: round(pad.top), r: round(pad.right), b: round(pad.bottom), l: round(pad.left) },
383
- fontSize: text ? (fontSizeOf(style) || 14) : 0,
384
- color: text ? colorStr(style.color) : undefined,
385
- bg: colorStr(style.backgroundColor),
386
- opacity: opacityOf(style),
387
- fontWeight: weightOf(style),
388
- textDecoration: decoOf(style),
389
- overflow: overflowOf(style),
390
- borderWidth,
391
- borderColor: borderWidth ? borderColorOf(style) : undefined,
328
+ if (verification?.requestedTargetMatched === false) {
329
+ return {
330
+ payload,
331
+ result: {
332
+ hit: false,
333
+ reason: "target-covered",
334
+ ...verification
335
+ },
336
+ attempts,
337
+ failure: "missed"
338
+ };
392
339
  }
393
- })
394
- const textElements = elements.filter((element) => element.text && element.fontSize > 0)
395
- if (!includeStyle || textElements.length === 0) return elements
396
-
397
- const screenshot = window.SootSim && window.SootSim.bridges && window.SootSim.bridges.screenshot
398
- if (typeof screenshot !== 'function') {
399
- throw new Error('styled native layout requires the rnx capture bridge')
400
- }
401
- const device = list.find((node) => node && node.device)?.device || {}
402
- const width = Math.round(Number(device.width) || window.innerWidth || 0)
403
- const height = Math.round(Number(device.height) || window.innerHeight || 0)
404
- if (!width || !height) throw new Error('native layout has no capture dimensions')
405
- const capture = async (suppressText) => {
406
- const dataUrl = await screenshot({
407
- format: 'png',
408
- outputWidth: width,
409
- outputHeight: height,
410
- ...(suppressText ? { suppressText: true } : {}),
411
- })
412
- const image = await new Promise((resolve, reject) => {
413
- const next = new Image()
414
- next.onload = () => resolve(next)
415
- next.onerror = () => reject(new Error('could not decode native layout pixels'))
416
- next.src = dataUrl
417
- })
418
- const canvas = document.createElement('canvas')
419
- canvas.width = image.naturalWidth
420
- canvas.height = image.naturalHeight
421
- const context = canvas.getContext('2d', { willReadFrequently: true })
422
- if (!context) throw new Error('could not read native layout pixels')
423
- context.drawImage(image, 0, 0)
424
- return context.getImageData(0, 0, canvas.width, canvas.height)
425
- }
426
- const normal = await capture(false)
427
- const background = await capture(true)
428
- for (const element of textElements) {
429
- element.contrast = scoreContrast({
430
- normal,
431
- background,
432
- box: element.box,
433
- fontSize: element.fontSize,
434
- fontWeight: element.fontWeight,
435
- })
340
+ const dispatched = verification?.activation?.tap ?? verification?.activation;
341
+ const result = dispatched && typeof dispatched === "object" ? { ...dispatched, ...verification?.activation, ...verification } : { hit: !!dispatched, ...verification };
342
+ lastResult = result;
343
+ if (isTapSuccess(result)) return { payload, result, attempts };
436
344
  }
437
- return elements
438
- })()`;
345
+ }
346
+ const remaining = deadline - Date.now();
347
+ if (remaining <= 0) break;
348
+ try {
349
+ await waitForSootsimIdle({
350
+ bridge,
351
+ maxMs: Math.min(remaining, timing.retryWaitMs),
352
+ pollMs: 32,
353
+ stablePolls: 2
354
+ });
355
+ } catch {
356
+ await new Promise((resolve) => setTimeout(resolve, Math.min(120, remaining)));
357
+ }
358
+ }
359
+ return {
360
+ payload: lastPayload,
361
+ result: lastResult,
362
+ attempts,
363
+ failure: lastPayload && typeof lastPayload.cx === "number" ? "missed" : "not-found"
364
+ };
439
365
  }
440
- var LAYOUT_GET_NATIVE_EVAL = layoutGetNativeEval();
441
- async function inspectGetLayout(bridge, opts = {}) {
442
- const raw = await bridge.send({
443
- type: "evaluate",
444
- code: layoutGetNativeEval(opts)
366
+ async function tapCoordinates(bridge, x, y, target) {
367
+ return bridge.send({
368
+ type: "tap",
369
+ x,
370
+ y,
371
+ ...target ? { target } : {}
445
372
  });
446
- return Array.isArray(raw) ? raw : [];
447
373
  }
448
- function formatLayoutElements(elements, opts) {
449
- if (elements.length === 0) return " no visible elements found";
450
- const lines = elements.map((el) => {
451
- const ident = el.testID ? `#${el.testID}` : el.selector ? el.selector : el.role ? `[${el.role}]` : `<${el.type}>`;
452
- const parts = [ident, `@(${el.box.x},${el.box.y})`, `${el.box.w}x${el.box.h}`];
453
- if (el.borderRadius > 0) parts.push(`radius:${el.borderRadius}`);
454
- const p = el.padding;
455
- if (p.t || p.r || p.b || p.l) {
456
- const uniform = p.t === p.r && p.r === p.b && p.b === p.l;
457
- parts.push(uniform ? `pad:${p.t}` : `pad:${p.t},${p.r},${p.b},${p.l}`);
458
- }
459
- if (el.fontSize > 0) parts.push(`font:${el.fontSize}`);
460
- if (opts?.styling) {
461
- if (el.fontWeight) parts.push(`weight:${el.fontWeight}`);
462
- if (el.color) parts.push(`color:${el.color}`);
463
- if (el.bg) parts.push(`bg:${el.bg}`);
464
- if (el.contrast) {
465
- const score = el.contrast.ratio == null ? `?/${el.contrast.required}` : `${el.contrast.ratio}/${el.contrast.required}${el.contrast.ratio < el.contrast.required ? " FAIL" : ""}`;
466
- parts.push(`contrast:${score}`);
467
- if (el.contrast.background) parts.push(`on:${el.contrast.background}`);
468
- }
469
- if (el.opacity != null) parts.push(`opacity:${el.opacity}`);
470
- if (el.textDecoration) parts.push(`deco:${el.textDecoration}`);
471
- if (el.overflow) parts.push(`overflow:${el.overflow}`);
472
- if (el.borderWidth) {
473
- parts.push(
474
- el.borderColor ? `border:${el.borderWidth}/${el.borderColor}` : `border:${el.borderWidth}`
475
- );
476
- }
477
- }
478
- if (el.text) {
479
- const text = el.text.length > 40 ? `${el.text.slice(0, 39)}\u2026` : el.text;
480
- parts.push(`"${text}"`);
481
- }
482
- return ` ${parts.join(" ")}`;
374
+ async function tapById(bridge, query, opts = {}) {
375
+ const arg = JSON.stringify(query);
376
+ return tapResolvedTarget(bridge, {
377
+ agent: opts.agent,
378
+ timing: opts.timing,
379
+ resolve: () => bridge.send({
380
+ type: "evaluate",
381
+ code: `(async () => {
382
+ const t = window.__sootsimTest
383
+ if (!t) return null
384
+ const n = (await t.findByTestId(${arg})) || (await t.findById(${arg}))
385
+ if (!n || !n.absolutePosition || !n.layout) return { cx: null }
386
+ const resolved =
387
+ typeof n.nodeId === 'number' && typeof t.resolveTapTarget === 'function'
388
+ ? await t.resolveTapTarget(n.nodeId)
389
+ : null
390
+ const target = (resolved && resolved.target) || n
391
+ const cx =
392
+ resolved && typeof resolved.cx === 'number'
393
+ ? resolved.cx
394
+ : n.absolutePosition.x + (n.layout.width || 0) / 2
395
+ const cy =
396
+ resolved && typeof resolved.cy === 'number'
397
+ ? resolved.cy
398
+ : n.absolutePosition.y + (n.layout.height || 0) / 2
399
+ const scr = ${SCREEN_EVAL}
400
+ const offscreen =
401
+ !!scr && (cx < 0 || cy < 0 || cx > scr.width || cy > scr.height)
402
+ return {
403
+ cx,
404
+ cy,
405
+ ...(offscreen ? { offscreen: true, screen: scr } : {}),
406
+ match: {
407
+ nodeId: n.nodeId ?? null,
408
+ id: n.id,
409
+ testID: n.testID,
410
+ text: n.text ?? n.accessibilityLabel ?? null,
411
+ type: n.type,
412
+ },
413
+ target: {
414
+ nodeId: target.nodeId ?? null,
415
+ id: target.id,
416
+ testID: target.testID,
417
+ text:
418
+ target.text ??
419
+ target.accessibilityLabel ??
420
+ n.text ??
421
+ n.accessibilityLabel ??
422
+ null,
423
+ type: target.type,
424
+ },
425
+ strategy: (resolved && resolved.strategy) || 'matched-node',
426
+ }
427
+ })()`
428
+ })
483
429
  });
484
- return lines.join("\n");
485
430
  }
486
- var INSPECT_A11Y_CODE = `(async () => {
487
- const t = window.__sootsimTest
488
- if (!t) return []
431
+ async function tapByText(bridge, query, options = {}, opts = {}) {
432
+ const filterArg = JSON.stringify({
433
+ query,
434
+ exact: !!options.exact,
435
+ role: options.role ?? null,
436
+ within: options.within ?? null,
437
+ minX: options.minX ?? null,
438
+ maxX: options.maxX ?? null,
439
+ minY: options.minY ?? null,
440
+ maxY: options.maxY ?? null,
441
+ near: options.near ?? null,
442
+ nth: options.nth ?? null,
443
+ first: !!options.first
444
+ });
445
+ return tapResolvedTarget(bridge, {
446
+ agent: opts.agent,
447
+ timing: opts.timing,
448
+ textFallback: query,
449
+ resolve: () => bridge.send({
450
+ type: "evaluate",
451
+ code: `(async () => {
452
+ const t = window.__sootsimTest
453
+ if (!t) return { error: 'bridge-not-ready' }
454
+ const F = ${filterArg}
489
455
 
490
- const readLayout = (n) => n?.layout || null
491
- const readAbs = (n) => n?.absolutePosition || n?.absolute || null
492
- const readRole = (n) => n?.accessibilityRole || n?.role || null
493
- const readLabel = (n) => n?.accessibilityLabel || n?.label || n?.text || null
494
- const readHint = (n) => n?.accessibilityHint || n?.hint || null
495
- const readState = (n) => n?.accessibilityState || n?.state || null
496
- const readTestId = (n) => n?.testID || n?.testId || null
497
- const readType = (n) => typeof n?.type === 'string' ? n.type.toLowerCase() : n?.type
498
- const readPressable = (n) => {
499
- if (n?.pressable) return true
500
- const handlers = Array.isArray(n?.handlers) ? n.handlers : []
501
- const role = readRole(n)
502
- return handlers.length > 0 || role === 'button' || role === 'link' || role === 'tab'
503
- }
504
- const isTextInput = (n) => n?.isTextInput === true
505
- const isTextNode = (n) => readType(n) === 'text'
506
- const isVisibleTarget = (n) => {
507
- const layout = readLayout(n)
508
- if (!layout || layout.width <= 0 || layout.height <= 0) return false
509
- const abs = readAbs(n)
510
- if (!abs) return true
511
- const device = n?.device || {}
512
- const screenW = Number(device.width) || window.innerWidth || 0
513
- const screenH = Number(device.height) || window.innerHeight || 0
514
- return (
515
- abs.x + layout.width > 0 &&
516
- abs.y + layout.height > 0 &&
517
- abs.x < screenW &&
518
- abs.y < screenH
519
- )
520
- }
521
- const hasAccessibleSignal = (n) => {
522
- const role = readRole(n)
523
- const label = readLabel(n)
524
- const hint = readHint(n)
525
- if (role) return true
526
- if (hint) return true
527
- if (isTextInput(n)) return true
528
- if (readPressable(n)) return true
529
- if (isTextNode(n) && n?.text) return true
530
- if (typeof label === 'string' && label.length <= 30 && label !== n?.text) return true
531
- return false
532
- }
533
- const normalize = (n) => {
534
- const layout = readLayout(n)
535
- const abs = readAbs(n)
536
- const role = readRole(n) || (readPressable(n) ? 'button' : isTextInput(n) ? 'textfield' : isTextNode(n) ? 'statictext' : 'none')
537
- return {
538
- role,
539
- label: readLabel(n),
540
- hint: readHint(n),
541
- state: readState(n),
542
- testID: readTestId(n),
543
- position: abs ? { x: Math.round(abs.x), y: Math.round(abs.y) } : null,
544
- size: layout ? { w: Math.round(layout.width), h: Math.round(layout.height) } : null,
545
- }
546
- }
456
+ const res = await t.queryTextCandidates({
457
+ query: F.query,
458
+ exact: !!F.exact,
459
+ ...(F.role ? { role: F.role } : {}),
460
+ })
547
461
 
548
- if (typeof t.listInspectable === 'function') {
549
- const list = await t.listInspectable({})
550
- if (Array.isArray(list)) {
551
- return list.filter(n => isVisibleTarget(n) && hasAccessibleSignal(n)).map(normalize)
552
- }
553
- }
462
+ let candidates = res.candidates || []
554
463
 
555
- const all = await t.queryAll({ pruneHidden: true })
556
- return all
557
- .filter(n => isVisibleTarget(n) && hasAccessibleSignal(n))
558
- .map(normalize)
559
- })()`;
560
- async function inspectAccessibilityTree(bridge) {
561
- const nodes = await bridge.send({ type: "evaluate", code: INSPECT_A11Y_CODE });
562
- return Array.isArray(nodes) ? nodes : [];
563
- }
564
- var FIND_INSPECTABLE_TARGETS = `
565
- const fromInspectable = async () => {
566
- if (typeof t.listInspectable !== 'function') return null
567
- const list = await t.listInspectable({})
568
- if (!Array.isArray(list)) return null
569
- return list.map((n) => {
570
- const role = n.accessibilityRole ?? n.role
571
- const label = n.accessibilityLabel ?? n.label
572
- const layout = n.layout
573
- const absolutePosition = n.absolutePosition ?? n.absolute
574
- const screenW = Number(n.device?.width) || window.innerWidth || 0
575
- const screenH = Number(n.device?.height) || window.innerHeight || 0
576
- const visibleFrame = layout && absolutePosition
577
- ? {
578
- x: Math.max(0, absolutePosition.x),
579
- y: Math.max(0, absolutePosition.y),
580
- width: Math.max(
581
- 0,
582
- Math.min(screenW, absolutePosition.x + layout.width) -
583
- Math.max(0, absolutePosition.x),
584
- ),
585
- height: Math.max(
586
- 0,
587
- Math.min(screenH, absolutePosition.y + layout.height) -
588
- Math.max(0, absolutePosition.y),
589
- ),
590
- }
591
- : null
592
- const handlers = Array.isArray(n.handlers) ? n.handlers : []
593
- const pressable =
594
- handlers.length > 0 ||
595
- role === 'button' ||
596
- role === 'link' ||
597
- role === 'tab'
598
- return {
599
- type: typeof n.type === 'string' ? n.type.toLowerCase() : n.type,
600
- id: n.id ?? n.nodeId,
601
- nodeId: n.nodeId,
602
- testID: n.testID ?? n.testId,
603
- text: n.text,
604
- layout,
605
- absolutePosition,
606
- visibleFrame,
607
- style: n.computedStyle ?? n.style ?? {},
608
- childCount: n.childCount ?? 0,
609
- pressed: n.pressed ?? false,
610
- pressable,
611
- isTextInput: n.isTextInput ?? false,
612
- accessible: n.accessible ?? true,
613
- accessibilityLabel: label,
614
- accessibilityRole: role,
615
- accessibilityHint: n.accessibilityHint,
616
- accessibilityState: n.accessibilityState,
617
- accessibilityValue: n.accessibilityValue,
618
- }
619
- })
620
- }
621
- const isVisibleTarget = (n) => {
622
- if (!n?.layout) return false
623
- const frame = n.visibleFrame
624
- if (frame) return frame.width > 0 && frame.height > 0
625
- const abs = n.absolutePosition
626
- if (!abs) return n.layout.width > 0 && n.layout.height > 0
627
- const screenW = window.innerWidth || 0
628
- const screenH = window.innerHeight || 0
629
- return (
630
- abs.x + n.layout.width > 0 &&
631
- abs.y + n.layout.height > 0 &&
632
- abs.x < screenW &&
633
- abs.y < screenH
634
- )
635
- }
636
- `;
637
- function resolveFindMode(q) {
638
- if (q.testId) {
639
- return {
640
- mode: "testid",
641
- code: `(async () => {
642
- const t = window.__sootsimTest
643
- if (!t) return null
644
- return (await t.findByTestId(${JSON.stringify(q.testId)})) || (await t.findById(${JSON.stringify(q.testId)}))
645
- })()`
646
- };
647
- }
648
- if (q.role) {
649
- return {
650
- mode: "role",
651
- code: `(async () => {
652
- const t = window.__sootsimTest
653
- if (!t) return []
654
- return await t.queryAll({ hasRole: ${JSON.stringify(q.role)}, pruneHidden: true })
655
- })()`
656
- };
657
- }
658
- if (q.type) {
659
- return {
660
- mode: "type",
661
- code: `(async () => {
662
- const t = window.__sootsimTest
663
- if (!t) return []
664
- return await t.queryAll({ type: ${JSON.stringify(q.type)}, pruneHidden: true })
665
- })()`
666
- };
667
- }
668
- if (q.pressable) {
669
- return {
670
- mode: "pressable",
671
- code: `(async () => {
672
- const t = window.__sootsimTest
673
- if (!t) return []
674
- ${FIND_INSPECTABLE_TARGETS}
675
- const inspectable = await fromInspectable()
676
- if (inspectable) return inspectable.filter(n => n.pressable && isVisibleTarget(n))
677
- const all = await t.queryAll({ pruneHidden: true })
678
- return all.filter(n => n.pressable && isVisibleTarget(n))
679
- })()`
680
- };
681
- }
682
- if (q.interactive) {
683
- return {
684
- mode: "interactive-targets",
685
- code: `(async () => {
686
- const t = window.__sootsimTest
687
- if (!t) return []
688
- ${FIND_INSPECTABLE_TARGETS}
689
- const inspectable = await fromInspectable()
690
- if (inspectable) {
691
- return inspectable.filter(n => n.pressable && isVisibleTarget(n))
692
- }
693
- const all = await t.queryAll({ pruneHidden: true })
694
- return all.filter(n => n.pressable && isVisibleTarget(n))
695
- })()`
696
- };
697
- }
698
- if (q.visible) {
699
- return {
700
- mode: "visible",
701
- code: `(async () => {
702
- const t = window.__sootsimTest
703
- if (!t) return []
704
- const all = await t.queryAll({ pruneHidden: true })
705
- return all.filter(n => n.layout && n.layout.width > 0 && n.layout.height > 0)
706
- })()`
707
- };
708
- }
709
- if (q.text) {
710
- return {
711
- mode: "text",
712
- code: `(async () => {
713
- const t = window.__sootsimTest
714
- if (!t) return null
715
- return await t.findByText(${JSON.stringify(q.text)})
716
- })()`
717
- };
718
- }
719
- return null;
720
- }
721
- async function inspectFind(bridge, q) {
722
- const resolved = resolveFindMode(q);
723
- if (!resolved) return null;
724
- const result = await bridge.send({ type: "evaluate", code: resolved.code });
725
- return { mode: resolved.mode, result };
726
- }
727
- function rankInteractive(nodes) {
728
- return [...nodes].sort((a, b) => scoreInteractive(b) - scoreInteractive(a));
729
- }
730
- function scoreInteractive(n) {
731
- let score = 0;
732
- if (n.testID) score += 100;
733
- if (typeof n.text === "string" && n.text.trim().length > 0) score += 60;
734
- if (typeof n.accessibilityLabel === "string" && n.accessibilityLabel.trim().length > 0) {
735
- score += 30;
736
- }
737
- if (n.accessibilityRole) score += 15;
738
- const w = n.layout?.width ?? 0;
739
- const h = n.layout?.height ?? 0;
740
- const area = w * h;
741
- if (area >= 400 && area <= 6e4) score += 25;
742
- else if (area > 6e4) score -= 20;
743
- const y = n.absolutePosition?.y ?? 0;
744
- if (y < 0) score -= 30;
745
- return score;
746
- }
747
- function tapCommandForNode(n) {
748
- if (n.testID) return `rnxsim do tap-id ${shellEscape(n.testID)}`;
749
- const text = typeof n.text === "string" ? n.text.trim() : "";
750
- if (text.length > 0 && text.length <= 80)
751
- return `rnxsim do tap-text ${shellEscape(text)}`;
752
- const x = Math.round(((n.absolutePosition?.x ?? 0) + (n.layout?.width ?? 0) / 2) * 10) / 10;
753
- const y = Math.round(((n.absolutePosition?.y ?? 0) + (n.layout?.height ?? 0) / 2) * 10) / 10;
754
- return `rnxsim do tap ${x} ${y}`;
755
- }
756
- function shellEscape(value) {
757
- if (/^[A-Za-z0-9_./@:-]+$/.test(value)) return value;
758
- return `'${value.replace(/'/g, `'\\''`)}'`;
759
- }
760
- var WAIT_READY_PROBE = `(async () => {
761
- const t = window.__sootsimTest
762
- let nodes = 0
763
- try { nodes = (await t?.getNodeCount?.()) || 0 } catch {}
764
- let targets = 0
765
- let liveFrameActive = false
766
- let liveFrameChannels = 0
767
- let liveFramePublishes = 0
768
- let loadingText = ''
769
- const cleanText = (value) =>
770
- typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''
771
- const loadingSignal = (value) => {
772
- const text = cleanText(value)
773
- if (!text) return ''
774
- return /^(opening app\\b|looking for your app\\b|building bundle\\b|preparing rnxsim runtime\\b|loading native bundle\\b|loading rnx stubs\\b|loading bundle loader\\b|loading native stubs\\b|loading compat bridge\\b|loading react-native shim\\b|loading react bridge\\b|loading reconciler\\b|loading replacement modules\\b|injecting rnx modules\\b|warming sqlite engine\\b|capturing root component\\b|reloading app\\b|loading(?:[.!\u2026]*)?$|connecting(?:[.!\u2026]*)?$)/i.test(text)
775
- ? text
776
- : ''
777
- }
778
- try {
779
- const loading = window.__sootsimConnectRNLoading
780
- if (loading && loading.active === true) {
781
- loadingText = [loading.title, loading.detail].map(cleanText).filter(Boolean).join(' ')
782
- }
783
- } catch {}
784
- try {
785
- if (typeof t?.listInspectable === 'function') {
786
- const list = await t.listInspectable({})
787
- if (Array.isArray(list)) {
788
- const screenW = window.innerWidth || 0
789
- const screenH = window.innerHeight || 0
790
- const isVisible = (n) => {
791
- const layout = n?.layout
792
- if (!layout || layout.width <= 0 || layout.height <= 0) return false
793
- const abs = n?.absolutePosition || n?.absolute
794
- if (!abs) return true
795
- const device = n?.device || {}
796
- const w = Number(device.width) || screenW
797
- const h = Number(device.height) || screenH
798
- return (
799
- abs.x + layout.width > 0 &&
800
- abs.y + layout.height > 0 &&
801
- abs.x < w &&
802
- abs.y < h
803
- )
804
- }
805
- const hasContentSignal = (n) => {
806
- const role = n?.accessibilityRole || n?.role
807
- const label = cleanText(n?.accessibilityLabel || n?.label)
808
- const handlers = Array.isArray(n?.handlers) ? n.handlers : []
809
- const text = cleanText(n?.text)
810
- const testID = cleanText(n?.testID || n?.testId)
811
- const loading = loadingSignal(text) || loadingSignal(label) || loadingSignal(testID)
812
- if (!loadingText && loading) {
813
- loadingText = loading
814
- }
815
- if (handlers.length > 0) return true
816
- if (role) return true
817
- if (n?.isTextInput) return true
818
- if (text) return true
819
- if (label) return true
820
- if (testID && !/^splash|loading/i.test(testID)) return true
821
- return false
822
- }
823
- targets = list.filter(n => isVisible(n) && hasContentSignal(n)).length
824
- }
825
- }
826
- } catch {}
827
- try {
828
- const stats = await t?.getImageLoaderStats?.()
829
- const presentation = stats?.liveFramePresentation
830
- liveFrameChannels = Number(presentation?.activeChannels) || 0
831
- liveFramePublishes = Number(presentation?.publishes) || 0
832
- const previous = window.__sootsimWaitReadyLiveFramePublishes
833
- liveFrameActive =
834
- liveFrameChannels > 0 &&
835
- typeof previous === 'number' &&
836
- liveFramePublishes > previous
837
- window.__sootsimWaitReadyLiveFramePublishes = liveFramePublishes
838
- } catch {}
839
- let externalReady = null
840
- let externalStatus = ''
841
- let externalError = ''
842
- let suppressedEntryError = ''
843
- try {
844
- const getExternalAppState = t?.getExternalAppState
845
- const external = typeof getExternalAppState === 'function'
846
- ? await Promise.race([
847
- getExternalAppState(),
848
- new Promise((resolve) => setTimeout(
849
- () => resolve({
850
- state: {
851
- ready: false,
852
- loading: true,
853
- status: 'waiting for tenant worker...',
854
- },
855
- }),
856
- 1000,
857
- )),
858
- ])
859
- : null
860
- const state = external?.state
861
- if (state && typeof state === 'object') {
862
- externalReady = state.ready === true
863
- if (typeof state.status === 'string') externalStatus = state.status
864
- if (typeof state.error === 'string') externalError = state.error
865
- }
866
- if (!externalError && Array.isArray(external?.entryErrors)) {
867
- // suppressed-and-continued entry errors (bundle-loader's patched __r
868
- // swallows entry throws and moves on) must NOT gate readiness \u2014 the
869
- // app frequently runs fine after one (e.g. metro's "Unknown named
870
- // module" probing noise). they surface separately as a diagnostic
871
- // for the timeout path.
872
- const isReal = (item) => item && typeof item.message === 'string' && item.message.trim()
873
- const fatal = external.entryErrors.find((item) => isReal(item) && item.suppressed !== true)
874
- if (fatal) externalError = fatal.message
875
- const suppressed = external.entryErrors.find((item) => isReal(item) && item.suppressed === true)
876
- if (suppressed) suppressedEntryError = suppressed.message
877
- }
878
- // once the app has errored its last status ("injecting sootsim
879
- // modules...") is stale \u2014 reporting it makes a dead app read as one that
880
- // is still booting.
881
- if (externalError) loadingText = ''
882
- else if (externalReady === false && externalStatus) loadingText = externalStatus
883
- } catch {}
884
- let errors = 0
885
- try { errors = window.__sootsimConsole?.count?.()?.errors ?? 0 } catch {}
886
- return {
887
- flag: (window).__sootsimExternalAppReady,
888
- at: (window).__sootsimExternalAppReadyAt || 0,
889
- nodes,
890
- targets,
891
- liveFrameActive,
892
- liveFrameChannels,
893
- liveFramePublishes,
894
- errors,
895
- loadingText,
896
- externalReady,
897
- externalStatus,
898
- externalError,
899
- suppressedEntryError,
900
- }
901
- })()`;
902
- var READY_CONTENT_NODE_FLOOR = 100;
903
- var READY_NODE_STABLE_MS = 750;
904
- var WAIT_READY_PROGRESS_INTERVAL_MS = 2e3;
905
- var READY_PROBE_POLL_MS = 150;
906
- function readyProbeHasContent(probe) {
907
- return probe.liveFrameActive || probe.targets > 0 || probe.nodes >= READY_CONTENT_NODE_FLOOR;
908
- }
909
- function readyProbeHasTargetContent(probe) {
910
- return probe.liveFrameActive || probe.targets > 0;
911
- }
912
- function waitReadyReason(status) {
913
- if (status.bridgeError) {
914
- return `sim unreachable: ${status.bridgeError} \u2014 the target tab is closed or no sim is connected (check \`rnxsim list\`, reopen with \`rnxsim open\`)`;
915
- }
916
- const base = status.externalError ? `guest app errored: ${status.externalError}` : status.loadingText ? `still showing "${status.loadingText}"` : status.externalReady === false ? "guest app is still loading" : status.flag !== true && status.targets > 0 ? "native content is rendered but the ready signal has not settled" : status.flag !== true ? "guest app has not emitted sootsim:externalAppReady" : status.targets <= 0 ? "ready flag emitted but no visible app content is inspectable yet" : "node tree is still changing";
917
- return !status.externalError && status.suppressedEntryError ? `${base} (suppressed entry error: ${status.suppressedEntryError})` : base;
918
- }
919
- async function inspectWaitReady(bridge, timeoutMs = 2e4, options = {}) {
920
- const start = Date.now();
921
- const deadline = start + timeoutMs;
922
- const progressIntervalMs = options.progressIntervalMs ?? WAIT_READY_PROGRESS_INTERVAL_MS;
923
- let nextProgressAt = start + progressIntervalMs;
924
- let lastNodes = -1;
925
- let nodeStableSince = start;
926
- let bridgeError = "";
927
- let last = {
928
- flag: void 0,
929
- at: 0,
930
- nodes: 0,
931
- targets: 0,
932
- liveFrameActive: false,
933
- liveFrameChannels: 0,
934
- liveFramePublishes: 0,
935
- errors: 0,
936
- loadingText: "",
937
- externalReady: null,
938
- externalStatus: "",
939
- externalError: "",
940
- suppressedEntryError: ""
941
- };
942
- while (Date.now() < deadline) {
943
- try {
944
- last = await bridge.send({ type: "evaluate", code: WAIT_READY_PROBE }) ?? last;
945
- bridgeError = "";
946
- } catch (error) {
947
- bridgeError = error instanceof Error ? error.message : String(error);
948
- if (bridgeError.startsWith("multiple sims are connected:") || bridgeError.startsWith("saved sim ")) {
949
- throw error;
950
- }
951
- }
952
- const now = Date.now();
953
- const status2 = {
954
- ready: false,
955
- elapsedMs: now - start,
956
- nodes: last.nodes,
957
- targets: last.targets,
958
- liveFrameActive: last.liveFrameActive,
959
- liveFrameChannels: last.liveFrameChannels,
960
- liveFramePublishes: last.liveFramePublishes,
961
- flag: last.flag,
962
- loadingText: last.loadingText,
963
- externalReady: last.externalReady,
964
- externalStatus: last.externalStatus,
965
- externalError: last.externalError,
966
- suppressedEntryError: last.suppressedEntryError,
967
- errors: last.errors,
968
- bridgeError
969
- };
970
- if (last.nodes !== lastNodes) {
971
- lastNodes = last.nodes;
972
- nodeStableSince = now;
973
- }
974
- const hasReadySignal = last.flag === true && readyProbeHasContent(last);
975
- const hasRenderedContentWithoutReadySignal = last.flag !== true && readyProbeHasTargetContent(last);
976
- if ((hasReadySignal || hasRenderedContentWithoutReadySignal) && last.externalReady !== false && !last.externalError && !last.loadingText) {
977
- if (now - nodeStableSince >= READY_NODE_STABLE_MS) {
978
- return {
979
- ...status2,
980
- ready: true
981
- };
982
- }
983
- }
984
- if (last.externalError) return status2;
985
- if (options.onProgress && progressIntervalMs > 0 && now >= nextProgressAt) {
986
- options.onProgress(status2);
987
- do {
988
- nextProgressAt += progressIntervalMs;
989
- } while (now >= nextProgressAt);
990
- }
991
- await new Promise((r) => setTimeout(r, READY_PROBE_POLL_MS));
992
- }
993
- const status = {
994
- ready: false,
995
- elapsedMs: Date.now() - start,
996
- nodes: last.nodes,
997
- targets: last.targets,
998
- liveFrameActive: last.liveFrameActive,
999
- liveFrameChannels: last.liveFrameChannels,
1000
- liveFramePublishes: last.liveFramePublishes,
1001
- flag: last.flag,
1002
- loadingText: last.loadingText,
1003
- externalReady: last.externalReady,
1004
- externalStatus: last.externalStatus,
1005
- externalError: last.externalError,
1006
- suppressedEntryError: last.suppressedEntryError,
1007
- errors: last.errors,
1008
- bridgeError
1009
- };
1010
- if (options.onProgress && progressIntervalMs > 0 && Date.now() >= nextProgressAt) {
1011
- options.onProgress(status);
1012
- }
1013
- return status;
1014
- }
1015
- async function inspectWaitSelector(bridge, testId, timeoutMs = 5e3, opts = {}) {
1016
- const gone = opts.gone === true;
1017
- const result = await bridge.send(
1018
- {
1019
- type: "evaluate",
1020
- code: `(async () => {
1021
- const start = Date.now()
1022
- const deadline = start + ${timeoutMs}
1023
- const gone = ${gone}
1024
- const ID = ${JSON.stringify(testId)}
1025
- const present = (node) => !!(node && node.layout && node.layout.width > 0 && node.layout.height > 0)
1026
- const find = async () => {
1027
- const t = window.__sootsimTest
1028
- if (!t) return undefined // bridge not ready \u2014 indeterminate, not "gone"
1029
- try {
1030
- // gone-mode confirms a gate/screen LEFT \u2014 it only needs structural
1031
- // existence + layout, NOT the full-tree occlusion pass (pruneHidden).
1032
- // that pass (computeOccludedInspectNodeIds) walks the whole tree and
1033
- // does O(n\xB2)-ish rect-occlusion testing EVERY poll; on a large
1034
- // post-login app tree it costs seconds per poll, so --gone can never
1035
- // get two clean "absent" polls inside budget and falsely reports the
1036
- // gate as still blocked \u2014 the root cause of the 3pc PR-preview outage.
1037
- // presence-mode keeps pruneHidden so "appeared" still means
1038
- // visibly-reachable. a departed gate is unmounted or zero-sized, so the
1039
- // cheap structural+layout check is correct here and ~O(n) cheap.
1040
- const matches = await t.queryAll?.(gone ? { hasId: ID } : { hasId: ID, pruneHidden: true })
1041
- if (!Array.isArray(matches)) return undefined
1042
- return matches.find(present) || null
1043
- } catch {
1044
- return undefined
1045
- }
1046
- }
1047
- let goneStreak = 0
1048
- while (Date.now() < deadline) {
1049
- const node = await find()
1050
- if (gone) {
1051
- if (node === null) {
1052
- goneStreak += 1
1053
- if (goneStreak >= 2) return { found: true, elapsed: Date.now() - start }
464
+ candidates = candidates.filter((c) => {
465
+ const ax = c.info.absolutePosition && c.info.absolutePosition.x
466
+ const ay = c.info.absolutePosition && c.info.absolutePosition.y
467
+ if (F.minX !== null && !(ax >= F.minX)) return false
468
+ if (F.maxX !== null && !(ax <= F.maxX)) return false
469
+ if (F.minY !== null && !(ay >= F.minY)) return false
470
+ if (F.maxY !== null && !(ay <= F.maxY)) return false
471
+ if (F.within && !c.ancestorTestIDs.includes(F.within)) return false
472
+ return true
473
+ })
474
+
475
+ if (F.near) {
476
+ candidates.sort((a, b) => {
477
+ const ax = (a.info.absolutePosition && a.info.absolutePosition.x) || 0
478
+ const ay = (a.info.absolutePosition && a.info.absolutePosition.y) || 0
479
+ const bx = (b.info.absolutePosition && b.info.absolutePosition.x) || 0
480
+ const by = (b.info.absolutePosition && b.info.absolutePosition.y) || 0
481
+ return (
482
+ Math.hypot(ax - F.near.x, ay - F.near.y) -
483
+ Math.hypot(bx - F.near.x, by - F.near.y)
484
+ )
485
+ })
1054
486
  } else {
1055
- goneStreak = 0
487
+ candidates.sort((a, b) => {
488
+ const ay = (a.info.absolutePosition && a.info.absolutePosition.y) || 0
489
+ const by = (b.info.absolutePosition && b.info.absolutePosition.y) || 0
490
+ if (Math.abs(ay - by) > 2) return ay - by
491
+ const ax = (a.info.absolutePosition && a.info.absolutePosition.x) || 0
492
+ const bx = (b.info.absolutePosition && b.info.absolutePosition.x) || 0
493
+ return ax - bx
494
+ })
1056
495
  }
1057
- } else if (present(node)) {
1058
- return { found: true, node, elapsed: Date.now() - start }
1059
- }
1060
- await new Promise((r) => setTimeout(r, 80))
1061
- }
1062
- return { found: false, elapsed: Date.now() - start }
1063
- })()`
1064
- },
1065
- {
1066
- timeoutMs: timeoutMs + 1e3
1067
- }
1068
- );
1069
- return result ?? { found: false, elapsed: timeoutMs };
1070
- }
1071
- function consoleEntriesEval(level, limit) {
1072
- const getter = level === "error" ? "getErrors" : "getWarnings";
1073
- return `(() => {
1074
- const out = []
1075
- const seen = new Set()
1076
- const norm = (s) => (typeof s === 'string' ? s : (() => { try { return JSON.stringify(s) } catch { return String(s) } })())
1077
- const key = (ts, args) => Math.round((ts || 0) / 250) + '|' + (Array.isArray(args) ? args.map(norm).join(' ') : norm(args))
1078
- const push = (e) => {
1079
- if (!e) return
1080
- const ts = typeof e.timestamp === 'number' ? e.timestamp : (typeof e.ts === 'number' ? e.ts : 0)
1081
- const k = key(ts, e.args)
1082
- if (seen.has(k)) return
1083
- seen.add(k)
1084
- out.push({ timestamp: ts, args: Array.isArray(e.args) ? e.args : [e.args], stack: e.stack || undefined, source: e.source || undefined })
1085
- }
1086
- // page-realm ws-bridge buffer first (richest stacks for page-origin errors)
1087
- try { for (const e of (window.__sootsimConsole?.${getter}(${limit}) || [])) push(e) } catch {}
1088
- // engine observability store \u2014 the always-on render-worker / forwarded path
1089
- try {
1090
- const obs = window.__sootsimObservability
1091
- const snap = obs && obs.logs && typeof obs.logs.getSnapshot === 'function' ? obs.logs.getSnapshot() : []
1092
- for (const e of snap) if (e && e.level === ${JSON.stringify(level)}) push(e)
1093
- } catch {}
1094
- return out.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)).slice(-${limit})
1095
- })()`;
1096
- }
1097
- async function inspectErrors(bridge, limit = 20) {
1098
- const result = await bridge.send({
1099
- type: "evaluate",
1100
- code: consoleEntriesEval("error", limit)
1101
- });
1102
- return Array.isArray(result) ? result : [];
1103
- }
1104
- async function inspectWarnings(bridge, limit = 20) {
1105
- const result = await bridge.send({
1106
- type: "evaluate",
1107
- code: consoleEntriesEval("warn", limit)
496
+
497
+ const total = candidates.length
498
+ if (total === 0) return { matched: 0, total: 0 }
499
+
500
+ let idx = 0
501
+ if (F.nth !== null) {
502
+ idx = F.nth < 0 ? total + F.nth : F.nth
503
+ if (idx < 0 || idx >= total) {
504
+ return { matched: 0, total, nthOutOfRange: true, nth: F.nth }
505
+ }
506
+ } else if (total > 1 && !F.first && !F.near) {
507
+ return {
508
+ ambiguous: true,
509
+ total,
510
+ candidates: candidates.slice(0, 10).map((c, i) => ({
511
+ idx: i,
512
+ nodeId: c.info.nodeId,
513
+ type: c.info.type,
514
+ testID: c.info.testID,
515
+ text: (c.info.text || '').slice(0, 60),
516
+ abs: c.info.absolutePosition,
517
+ layout: c.info.layout
518
+ ? {
519
+ width: Math.round(c.info.layout.width || 0),
520
+ height: Math.round(c.info.layout.height || 0),
521
+ }
522
+ : null,
523
+ ancestorTestIDs: (c.ancestorTestIDs || []).slice(0, 5),
524
+ })),
525
+ }
526
+ }
527
+
528
+ const picked = candidates[idx]
529
+ const n = picked.info
530
+ if (!n.absolutePosition || !n.layout) return { matched: 0, total }
531
+
532
+ const resolved =
533
+ typeof n.nodeId === 'number' &&
534
+ typeof t.resolveTapTarget === 'function'
535
+ ? await t.resolveTapTarget(n.nodeId)
536
+ : null
537
+ const target = (resolved && resolved.target) || n
538
+ const cx =
539
+ resolved && typeof resolved.cx === 'number'
540
+ ? resolved.cx
541
+ : n.absolutePosition.x + (n.layout.width || 0) / 2
542
+ const cy =
543
+ resolved && typeof resolved.cy === 'number'
544
+ ? resolved.cy
545
+ : n.absolutePosition.y + (n.layout.height || 0) / 2
546
+ const scr = ${SCREEN_EVAL}
547
+ const offscreen =
548
+ !!scr && (cx < 0 || cy < 0 || cx > scr.width || cy > scr.height)
549
+ return {
550
+ cx,
551
+ cy,
552
+ ...(offscreen ? { offscreen: true, screen: scr } : {}),
553
+ match: {
554
+ nodeId: n.nodeId ?? null,
555
+ id: n.id,
556
+ testID: n.testID,
557
+ type: n.type,
558
+ },
559
+ target: {
560
+ nodeId: target.nodeId ?? null,
561
+ id: target.id,
562
+ testID: target.testID,
563
+ text:
564
+ target.text ??
565
+ target.accessibilityLabel ??
566
+ n.text ??
567
+ n.accessibilityLabel ??
568
+ null,
569
+ type: target.type,
570
+ },
571
+ strategy: (resolved && resolved.strategy) || 'matched-node',
572
+ total,
573
+ idx,
574
+ }
575
+ })()`
576
+ })
1108
577
  });
1109
- return Array.isArray(result) ? result : [];
1110
578
  }
1111
- var MERGED_CONSOLE_COUNT_EVAL = `(() => {
1112
- const norm = (s) => (typeof s === 'string' ? s : (() => { try { return JSON.stringify(s) } catch { return String(s) } })())
1113
- const key = (lvl, ts, args) => lvl + '|' + Math.round((ts || 0) / 250) + '|' + (Array.isArray(args) ? args.map(norm).join(' ') : norm(args))
1114
- const seen = new Set()
1115
- let errors = 0
1116
- let warnings = 0
1117
- const add = (lvl, ts, args) => {
1118
- if (lvl !== 'error' && lvl !== 'warn') return
1119
- const k = key(lvl, ts, args)
1120
- if (seen.has(k)) return
1121
- seen.add(k)
1122
- if (lvl === 'error') errors++
1123
- else warnings++
579
+ async function tapBest(bridge, query, opts = {}) {
580
+ const arg = JSON.stringify(query);
581
+ return tapResolvedTarget(bridge, {
582
+ agent: opts.agent,
583
+ timing: opts.timing,
584
+ textFallback: query,
585
+ resolve: async () => {
586
+ const payload = await bridge.send({
587
+ type: "evaluate",
588
+ code: `(async () => {
589
+ const t = window.__sootsimTest
590
+ if (!t) return { error: 'bridge-not-ready' }
591
+ const byTestId =
592
+ (await t.findByTestId(${arg})) || (await t.findById(${arg}))
593
+ if (byTestId && byTestId.absolutePosition && byTestId.layout) {
594
+ return {
595
+ strategy: 'testid',
596
+ node: {
597
+ nodeId: byTestId.nodeId ?? null,
598
+ id: byTestId.id,
599
+ testID: byTestId.testID,
600
+ type: byTestId.type,
601
+ text: byTestId.text,
602
+ absolutePosition: byTestId.absolutePosition,
603
+ layout: byTestId.layout,
604
+ },
605
+ screen: ${SCREEN_EVAL},
606
+ }
607
+ }
608
+ const byText = await t.findByText(${arg})
609
+ if (byText && byText.absolutePosition && byText.layout) {
610
+ return {
611
+ strategy: 'text',
612
+ node: {
613
+ nodeId: byText.nodeId ?? null,
614
+ id: byText.id,
615
+ testID: byText.testID,
616
+ type: byText.type,
617
+ text: byText.text,
618
+ absolutePosition: byText.absolutePosition,
619
+ layout: byText.layout,
620
+ },
621
+ screen: ${SCREEN_EVAL},
622
+ }
623
+ }
624
+ return { strategy: 'none' }
625
+ })()`
626
+ });
627
+ if (!payload || !("node" in payload)) return payload;
628
+ const node = payload.node;
629
+ const cx = node.absolutePosition.x + node.layout.width / 2;
630
+ const cy = node.absolutePosition.y + node.layout.height / 2;
631
+ const screen = payload.screen ?? null;
632
+ const offscreen = !!screen && (cx < 0 || cy < 0 || cx > screen.width || cy > screen.height);
633
+ return {
634
+ ...payload,
635
+ cx,
636
+ cy,
637
+ ...offscreen ? { offscreen: true, screen } : {},
638
+ target: {
639
+ id: node.id,
640
+ testID: node.testID,
641
+ text: payload.strategy === "text" ? query : node.text,
642
+ type: node.type
643
+ }
644
+ };
1124
645
  }
1125
- try {
1126
- const c = window.__sootsimConsole
1127
- for (const e of (c?.getErrors?.(200) || [])) add('error', e?.timestamp ?? e?.ts, e?.args)
1128
- for (const e of (c?.getWarnings?.(200) || [])) add('warn', e?.timestamp ?? e?.ts, e?.args)
1129
- } catch {}
1130
- try {
1131
- const obs = window.__sootsimObservability
1132
- const snap = obs && obs.logs && typeof obs.logs.getSnapshot === 'function' ? obs.logs.getSnapshot() : []
1133
- for (const e of snap) if (e) add(e.level, e.ts, e.args)
1134
- } catch {}
1135
- return { errors, warnings, total: errors + warnings }
1136
- })()`;
1137
- async function clearConsole(bridge) {
1138
- await bridge.send({
1139
- type: "evaluate",
1140
- code: 'window.__sootsimConsole?.clear(); window.__sootsimObservability?.logs?.clear?.(); "cleared"'
1141
646
  });
1142
647
  }
1143
- async function inspectRequests(bridge, opts = {}) {
1144
- const limit = opts.limit ?? 20;
1145
- const method = opts.failed === false ? "getRequests" : "getFailedRequests";
1146
- const result = await bridge.send({
1147
- type: "call",
1148
- path: `__sootsimTest.${method}`,
1149
- args: [limit]
1150
- });
1151
- return Array.isArray(result) ? result : [];
648
+
649
+ // cli/commands/inspect/core.ts
650
+ function resolveMaxMsFlag(args, fallbackMs) {
651
+ const aliases = ["--max-ms", "--maxMs", "--maxms", "--max_ms"];
652
+ for (const flag of aliases) {
653
+ const i = args.indexOf(flag);
654
+ if (i >= 0 && args[i + 1]) {
655
+ const n = Number(args[i + 1]);
656
+ if (Number.isFinite(n)) return Math.max(100, n);
657
+ }
658
+ }
659
+ return fallbackMs;
1152
660
  }
1153
- async function clearRequests(bridge) {
1154
- await bridge.send({ type: "call", path: "__sootsimTest.clearRequests", args: [] });
661
+ var INSPECT_PICK_COMMANDS = /* @__PURE__ */ new Set([
662
+ "tap",
663
+ "double-tap",
664
+ "tap-text",
665
+ "tap-id",
666
+ "long-press",
667
+ "touch"
668
+ ]);
669
+ function isInspectPickCommand(subcommand) {
670
+ return typeof subcommand === "string" && INSPECT_PICK_COMMANDS.has(subcommand);
1155
671
  }
1156
- async function inspectScrollStateAt(bridge, x, y) {
1157
- const result = await bridge.send({
1158
- type: "call",
1159
- path: "__sootsimTest.getScrollStateAt",
1160
- args: [x, y]
672
+ async function isInspectModeActive(bridge) {
673
+ const active = await bridge.send({
674
+ type: "evaluate",
675
+ code: "window.__sootsimEngineState?.inspectActive === true"
1161
676
  });
1162
- if (!result || typeof result !== "object") return null;
1163
- const state = result;
1164
- return {
1165
- ...state,
1166
- scrollOffsetX: typeof state.scrollOffsetX === "number" ? state.scrollOffsetX : state.offsetX,
1167
- scrollOffsetY: typeof state.scrollOffsetY === "number" ? state.scrollOffsetY : state.offsetY
1168
- };
677
+ return active === true;
1169
678
  }
1170
- async function inspectLogs(bridge) {
1171
- const res = await bridge.send({
679
+ async function shouldSkipAutoSettleForInspectPick(bridge, subcommand) {
680
+ if (!isInspectPickCommand(subcommand)) return false;
681
+ try {
682
+ return await isInspectModeActive(bridge);
683
+ } catch {
684
+ return false;
685
+ }
686
+ }
687
+ async function inspectNodeCount(bridge) {
688
+ const count = await bridge.send({
1172
689
  type: "evaluate",
1173
- code: `(() => {
1174
- const obs = window.__sootsimObservability;
1175
- if (!obs) return { ok: false };
1176
- return { ok: true, entries: obs.logs.getSnapshot() };
1177
- })()`
690
+ code: "(async () => await window.__sootsimTest.getNodeCount())()"
1178
691
  });
1179
- if (!res || !res.ok) return [];
1180
- return res.entries ?? [];
692
+ return { nodes: typeof count === "number" ? count : 0 };
1181
693
  }
1182
- async function clearLogs(bridge) {
1183
- await bridge.send({
694
+ async function inspectTree(bridge, depth = 5, opts) {
695
+ const optsArg = opts?.format ? `, ${JSON.stringify(opts)}` : "";
696
+ const tree = await bridge.send({
1184
697
  type: "evaluate",
1185
- code: 'window.__sootsimObservability?.logs.clear(); "cleared"'
698
+ code: `(async () => await window.__sootsimTest.dumpTree(${depth}${optsArg}))()`
1186
699
  });
1187
- }
1188
- function isForwardedWorkerLogTwin(a, b) {
1189
- if (a.source === b.source) return false;
1190
- if (a.level !== b.level) return false;
1191
- if (Math.abs(a.ts - b.ts) > 1e3) return false;
1192
- if (a.args.length !== b.args.length) return false;
1193
- const sources = /* @__PURE__ */ new Set([a.source, b.source]);
1194
- if (!sources.has("sootsim-worker")) return false;
1195
- if (!sources.has("render-worker") && !sources.has("forwarded-render-worker")) {
1196
- return false;
700
+ if (opts?.format === "boxes" && typeof tree === "string") {
701
+ try {
702
+ return { depth, tree: JSON.parse(tree) };
703
+ } catch {
704
+ return { depth, tree };
705
+ }
1197
706
  }
1198
- return a.args.every((arg, index) => arg === b.args[index]);
707
+ return { depth, tree };
1199
708
  }
1200
- function filterLogEntries(entries, opts = {}) {
1201
- let out = [];
1202
- for (const entry of entries) {
1203
- if (out.some((candidate) => isForwardedWorkerLogTwin(candidate, entry))) {
1204
- continue;
709
+ async function inspectUrl(bridge) {
710
+ const url = await bridge.send({ type: "evaluate", code: "window.location.href" });
711
+ return { url: typeof url === "string" ? url : "" };
712
+ }
713
+ async function inspectDescribe(bridge, dumpOpts) {
714
+ const dumpDepth = dumpOpts.maxDepth ?? 12;
715
+ const code = `(async () => {
716
+ const t = window.__sootsimTest
717
+ const mainShell = window.SootSim?.bridges?.mainShell
718
+ const kb = window.__sootsimKeyboard
719
+ if (!t) return { error: 'no test bridge' }
720
+
721
+ let shell = null
722
+ try {
723
+ shell = typeof mainShell?.getState === 'function' ? await mainShell.getState() : null
724
+ } catch {}
725
+
726
+ const opts = ${JSON.stringify(dumpOpts)}
727
+ const needsRecursiveDump =
728
+ opts.verbose || opts.subtreeRoot || opts.includeOccluded || opts.maxDepth != null
729
+ const globToRegex = (glob) => {
730
+ const escaped = String(glob).replace(/[.+^$(){}|[\\]\\\\]/g, '\\\\$&')
731
+ const body = escaped.replace(/\\*/g, '.*').replace(/\\?/g, '.')
732
+ return new RegExp('^' + body + '$', 'i')
733
+ }
734
+ // clipping a describe line is a DISPLAY decision. opts.fullText (set by
735
+ // \`describe --json\`) keeps the real string, so a bundle-eval stack or a
736
+ // red-box message reaches the caller whole instead of ending in an
737
+ // ellipsis nothing downstream can undo.
738
+ const clipForDisplay = (value, max) => {
739
+ const text = typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''
740
+ if (opts.fullText || text.length <= max) return text
741
+ return text.slice(0, max - 1).trimEnd() + '\u2026'
742
+ }
743
+ const formatInspectableTree = async () => {
744
+ if (typeof t.listInspectable !== 'function') return null
745
+ const list = await t.listInspectable({})
746
+ if (!Array.isArray(list)) return null
747
+ const filter = typeof opts.filter === 'string' ? opts.filter.toLowerCase() : ''
748
+ const testIdRegex = opts.testIdLike ? globToRegex(opts.testIdLike) : null
749
+ const onlyRegex = opts.onlyGlob ? globToRegex(opts.onlyGlob) : null
750
+ const lines = []
751
+ let matched = 0
752
+ for (const node of list) {
753
+ const testID = node?.testID || node?.testId || ''
754
+ const role = node?.role || ''
755
+ const label = node?.accessibilityLabel || node?.label || ''
756
+ const text = node?.text || ''
757
+ if (testIdRegex && !testIdRegex.test(testID)) continue
758
+ const joined = [role, label, text, testID].filter(Boolean).join(' ')
759
+ if (onlyRegex && !onlyRegex.test(joined)) continue
760
+ if (filter && !joined.toLowerCase().includes(filter)) continue
761
+ matched++
762
+
763
+ const type = String(node?.type || 'node').toLowerCase()
764
+ const handlers = Array.isArray(node?.handlers) ? node.handlers : []
765
+ const parts = []
766
+ if (role) parts.push('[' + role + ']')
767
+ else if (handlers.length > 0) parts.push('[tap]')
768
+ else parts.push('<' + type + '>')
769
+ const visibleText = clipForDisplay(text, 50)
770
+ if (visibleText) parts.push(JSON.stringify(visibleText))
771
+ if (testID) parts.push('#' + testID)
772
+ if (label && label !== text) {
773
+ parts.push('label=' + JSON.stringify(clipForDisplay(label, 50)))
774
+ }
775
+ if (!opts.hideXy) {
776
+ const abs = node?.absolute || {}
777
+ const layout = node?.layout || {}
778
+ const x = Math.round(Number(abs.x) || 0)
779
+ const y = Math.round(Number(abs.y) || 0)
780
+ const w = Math.round(Number(layout.width) || 0)
781
+ const h = Math.round(Number(layout.height) || 0)
782
+ parts.push('@(' + x + ',' + y + ') ' + w + 'x' + h)
783
+ }
784
+ if (handlers.length > 0) parts.push('(tap)')
785
+ lines.push(' ' + parts.join(' '))
786
+ }
787
+ return { tree: lines.join('\\n'), matched }
788
+ }
789
+
790
+ let tree = ''
791
+ const compactResult = needsRecursiveDump ? null : await formatInspectableTree()
792
+ if (compactResult) {
793
+ tree = compactResult.tree
794
+ } else {
795
+ tree = await t.dumpTree(${dumpDepth}, opts)
796
+ }
797
+ const nodeCount = (await t.getNodeCount?.()) || 0
798
+ const keyboard = kb && typeof kb.getLayout === 'function' ? kb.getLayout() : null
799
+ return { tree, shell, nodeCount, keyboard }
800
+ })()`;
801
+ const result = await bridge.send({ type: "evaluate", code });
802
+ return result ?? {};
803
+ }
804
+ function scoreLayoutTextContrast(input) {
805
+ const required = input.fontSize >= 18 || input.fontSize >= 14 && (input.fontWeight ?? 400) >= 700 ? 3 : 4.5;
806
+ const { normal, background } = input;
807
+ if (normal.width <= 0 || normal.height <= 0 || normal.width !== background.width || normal.height !== background.height) {
808
+ return { ratio: null, required, reason: "capture dimensions do not match" };
809
+ }
810
+ const left = Math.max(0, Math.floor(input.box.x));
811
+ const top = Math.max(0, Math.floor(input.box.y));
812
+ const right = Math.min(normal.width, Math.ceil(input.box.x + input.box.w));
813
+ const bottom = Math.min(normal.height, Math.ceil(input.box.y + input.box.h));
814
+ if (right <= left || bottom <= top) {
815
+ return { ratio: null, required, reason: "text is outside the captured frame" };
816
+ }
817
+ const linear = (value) => {
818
+ const channel = value / 255;
819
+ return channel <= 0.04045 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
820
+ };
821
+ const ratioOf = (foreground, behind) => {
822
+ const foregroundLuminance = 0.2126 * linear(foreground[0]) + 0.7152 * linear(foreground[1]) + 0.0722 * linear(foreground[2]);
823
+ const backgroundLuminance = 0.2126 * linear(behind[0]) + 0.7152 * linear(behind[1]) + 0.0722 * linear(behind[2]);
824
+ return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05) / (Math.min(foregroundLuminance, backgroundLuminance) + 0.05);
825
+ };
826
+ const hex = (color) => `#${color.slice(0, 3).map((value) => Math.round(value).toString(16).padStart(2, "0")).join("")}`;
827
+ const area = (right - left) * (bottom - top);
828
+ const step = Math.max(1, Math.floor(Math.sqrt(area / 12e3)));
829
+ const offsetAt = (x, y) => (y * normal.width + x) * 4;
830
+ const deltaAt = (offset) => Math.max(
831
+ Math.abs(normal.data[offset] - background.data[offset]),
832
+ Math.abs(normal.data[offset + 1] - background.data[offset + 1]),
833
+ Math.abs(normal.data[offset + 2] - background.data[offset + 2])
834
+ );
835
+ let strongestDelta = 0;
836
+ let strongestForeground = null;
837
+ let opaqueBackground = null;
838
+ for (let y = top; y < bottom; y += step) {
839
+ for (let x = left; x < right; x += step) {
840
+ const offset = offsetAt(x, y);
841
+ if (background.data[offset + 3] < 250) continue;
842
+ const behind = [
843
+ background.data[offset],
844
+ background.data[offset + 1],
845
+ background.data[offset + 2]
846
+ ];
847
+ opaqueBackground ?? (opaqueBackground = behind);
848
+ const delta = deltaAt(offset);
849
+ if (delta <= strongestDelta) continue;
850
+ strongestDelta = delta;
851
+ strongestForeground = [
852
+ normal.data[offset],
853
+ normal.data[offset + 1],
854
+ normal.data[offset + 2]
855
+ ];
1205
856
  }
1206
- out.push(entry);
1207
857
  }
1208
- if (!opts.showInternal) {
1209
- out = out.filter((e) => {
1210
- const first = e.args[0];
1211
- return !(typeof first === "string" && first.startsWith("[rnx]"));
1212
- });
858
+ let bestRatio = Infinity;
859
+ let bestForeground = null;
860
+ let bestBackground = null;
861
+ const consider = (foreground, behind) => {
862
+ const ratio = ratioOf(foreground, behind);
863
+ if (ratio >= bestRatio) return;
864
+ bestRatio = ratio;
865
+ bestForeground = foreground;
866
+ bestBackground = behind;
867
+ };
868
+ if (strongestDelta >= 2 && strongestForeground) {
869
+ for (let y = top; y < bottom; y += step) {
870
+ for (let x = left; x < right; x += step) {
871
+ const offset = offsetAt(x, y);
872
+ if (background.data[offset + 3] < 250 || deltaAt(offset) < 2) {
873
+ continue;
874
+ }
875
+ consider(strongestForeground, [
876
+ background.data[offset],
877
+ background.data[offset + 1],
878
+ background.data[offset + 2]
879
+ ]);
880
+ }
881
+ }
882
+ } else if (opaqueBackground) {
883
+ consider(opaqueBackground, opaqueBackground);
1213
884
  }
1214
- if (opts.level) out = out.filter((e) => opts.level.has(e.level));
1215
- if (opts.filter) {
1216
- const lf = opts.filter.toLowerCase();
1217
- out = out.filter((e) => e.args.join(" ").toLowerCase().includes(lf));
885
+ if (!bestForeground || !bestBackground || !Number.isFinite(bestRatio)) {
886
+ return {
887
+ ratio: null,
888
+ required,
889
+ reason: "could not isolate opaque rendered text pixels"
890
+ };
1218
891
  }
1219
- return out;
892
+ return {
893
+ ratio: Math.round(bestRatio * 100) / 100,
894
+ required,
895
+ foreground: hex(bestForeground),
896
+ background: hex(bestBackground)
897
+ };
898
+ }
899
+ function layoutGetNativeEval(opts = {}) {
900
+ const includeStyle = opts.styling === true;
901
+ const filter = opts.filter?.trim().toLowerCase() ?? "";
902
+ return `(async () => {
903
+ const t = window.__sootsimTest
904
+ if (!t || typeof t.listInspectable !== 'function') return []
905
+ const list = await t.listInspectable({})
906
+ if (!Array.isArray(list)) return []
907
+ const includeStyle = ${JSON.stringify(includeStyle)}
908
+ const scoreContrast = ${scoreLayoutTextContrast.toString()}
909
+ // narrows by ELEMENT, not by style field: fewer rows, full detail on each.
910
+ const filter = ${JSON.stringify(filter)}
911
+ const matchesFilter = (n, text) => {
912
+ if (!filter) return true
913
+ const fields = [text, n.testID || n.testId, n.accessibilityRole || n.role, n.type]
914
+ return fields.some((f) => typeof f === 'string' && f.toLowerCase().includes(filter))
915
+ }
916
+
917
+ const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0)
918
+ const round = (v) => Math.round(num(v))
919
+
920
+ // borderRadius can be authored as a string ("12px") or split per-corner;
921
+ // prefer the uniform borderRadius, else the largest corner radius present.
922
+ const radiusOf = (style) => {
923
+ if (!style) return 0
924
+ const keys = ['borderRadius','borderTopLeftRadius','borderTopRightRadius','borderBottomLeftRadius','borderBottomRightRadius']
925
+ let max = 0
926
+ for (const k of keys) {
927
+ const raw = style[k]
928
+ const n = typeof raw === 'number' ? raw : (typeof raw === 'string' ? parseFloat(raw) : NaN)
929
+ if (Number.isFinite(n)) max = Math.max(max, n)
930
+ }
931
+ return Math.round(max)
932
+ }
933
+ const fontSizeOf = (style) => {
934
+ if (!style) return 0
935
+ const raw = style.fontSize
936
+ const n = typeof raw === 'number' ? raw : (typeof raw === 'string' ? parseFloat(raw) : NaN)
937
+ return Number.isFinite(n) ? Math.round(n) : 0
938
+ }
939
+ // styling extractors \u2014 omit boring defaults so the agent only sees notable
940
+ // styling (low opacity, struck text, clipping, a real border/color).
941
+ const colorStr = (v) => {
942
+ if (v == null) return undefined
943
+ const s = String(v).split(' ').join('')
944
+ return s && s !== 'transparent' && s !== 'rgba(0,0,0,0)' ? s : undefined
945
+ }
946
+ const opacityOf = (style) => {
947
+ const o = style && style.opacity
948
+ const n = typeof o === 'number' ? o : (typeof o === 'string' ? parseFloat(o) : NaN)
949
+ return Number.isFinite(n) && n < 1 ? Math.round(n * 100) / 100 : undefined
950
+ }
951
+ const weightOf = (style) => {
952
+ const w = style && style.fontWeight
953
+ const n = w === 'bold' ? 700 : (typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN))
954
+ return Number.isFinite(n) && n >= 600 ? n : undefined
955
+ }
956
+ const decoOf = (style) => {
957
+ const d = style && (style.textDecorationLine || style.textDecoration)
958
+ return d && d !== 'none' && d !== 'normal' ? String(d).split(' ')[0] : undefined
959
+ }
960
+ const overflowOf = (style) => (style && style.overflow === 'hidden' ? 'hidden' : undefined)
961
+ const borderWOf = (style) => {
962
+ if (!style) return undefined
963
+ const keys = ['borderWidth','borderTopWidth','borderBottomWidth','borderLeftWidth','borderRightWidth']
964
+ let max = 0
965
+ for (const k of keys) {
966
+ const r = style[k]
967
+ const n = typeof r === 'number' ? r : (typeof r === 'string' ? parseFloat(r) : NaN)
968
+ if (Number.isFinite(n)) max = Math.max(max, n)
969
+ }
970
+ return max > 0 ? Math.round(max) : undefined
971
+ }
972
+ // only meaningful alongside a real border width, so it is resolved by the
973
+ // caller below rather than on its own.
974
+ const borderColorOf = (style) => {
975
+ if (!style) return undefined
976
+ const keys = ['borderColor','borderTopColor','borderBottomColor','borderLeftColor','borderRightColor']
977
+ for (const k of keys) {
978
+ const c = colorStr(style[k])
979
+ if (c) return c
980
+ }
981
+ return undefined
982
+ }
983
+
984
+ const isVisible = (n) => {
985
+ const layout = n && n.layout
986
+ if (!layout || layout.width <= 0 || layout.height <= 0) return false
987
+ const abs = (n && (n.absolute || n.absolutePosition)) || null
988
+ if (!abs) return true
989
+ const device = (n && n.device) || {}
990
+ const screenW = Number(device.width) || window.innerWidth || 0
991
+ const screenH = Number(device.height) || window.innerHeight || 0
992
+ if (!screenW || !screenH) return true
993
+ return abs.x + layout.width > 0 && abs.y + layout.height > 0 && abs.x < screenW && abs.y < screenH
994
+ }
995
+
996
+ const elements = list.filter(isVisible).filter((n) => {
997
+ const t = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
998
+ return matchesFilter(n, t)
999
+ }).map((n) => {
1000
+ const layout = n.layout || { x: 0, y: 0, width: 0, height: 0 }
1001
+ const abs = n.absolute || n.absolutePosition || { x: layout.x, y: layout.y }
1002
+ const text = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
1003
+ const base = {
1004
+ ...(typeof n.nodeId === 'number'
1005
+ ? {
1006
+ nodeId: n.nodeId,
1007
+ ancestorIds: Array.isArray(n.ancestors)
1008
+ ? n.ancestors.map((ancestor) => ancestor && ancestor.nodeId).filter((id) => typeof id === 'number')
1009
+ : [],
1010
+ }
1011
+ : {}),
1012
+ testID: n.testID || n.testId || undefined,
1013
+ role: n.accessibilityRole || n.role || undefined,
1014
+ type: typeof n.type === 'string' ? n.type : 'node',
1015
+ text,
1016
+ box: { x: round(abs.x), y: round(abs.y), w: round(layout.width), h: round(layout.height) },
1017
+ borderRadius: 0,
1018
+ padding: { t: 0, r: 0, b: 0, l: 0 },
1019
+ fontSize: 0,
1020
+ }
1021
+ if (!includeStyle) return base
1022
+ // computedStyle has the resolved values; style is the curated subset \u2014
1023
+ // fall back across both so radius/fontSize survive either shape.
1024
+ const style = Object.assign({}, n.style || {}, n.computedStyle || {})
1025
+ const pad = (n.boxModel && n.boxModel.padding) || {}
1026
+ const borderWidth = borderWOf(style)
1027
+ return {
1028
+ ...base,
1029
+ borderRadius: radiusOf(style),
1030
+ padding: { t: round(pad.top), r: round(pad.right), b: round(pad.bottom), l: round(pad.left) },
1031
+ fontSize: text ? (fontSizeOf(style) || 14) : 0,
1032
+ color: text ? colorStr(style.color) : undefined,
1033
+ bg: colorStr(style.backgroundColor),
1034
+ opacity: opacityOf(style),
1035
+ fontWeight: weightOf(style),
1036
+ textDecoration: decoOf(style),
1037
+ overflow: overflowOf(style),
1038
+ borderWidth,
1039
+ borderColor: borderWidth ? borderColorOf(style) : undefined,
1040
+ }
1041
+ })
1042
+ const textElements = elements.filter((element) => element.text && element.fontSize > 0)
1043
+ if (!includeStyle || textElements.length === 0) return elements
1044
+
1045
+ const screenshot = window.SootSim && window.SootSim.bridges && window.SootSim.bridges.screenshot
1046
+ if (typeof screenshot !== 'function') {
1047
+ throw new Error('styled native layout requires the rnx capture bridge')
1048
+ }
1049
+ const device = list.find((node) => node && node.device)?.device || {}
1050
+ const width = Math.round(Number(device.width) || window.innerWidth || 0)
1051
+ const height = Math.round(Number(device.height) || window.innerHeight || 0)
1052
+ if (!width || !height) throw new Error('native layout has no capture dimensions')
1053
+ const capture = async (suppressText) => {
1054
+ const dataUrl = await screenshot({
1055
+ format: 'png',
1056
+ outputWidth: width,
1057
+ outputHeight: height,
1058
+ ...(suppressText ? { suppressText: true } : {}),
1059
+ })
1060
+ const image = await new Promise((resolve, reject) => {
1061
+ const next = new Image()
1062
+ next.onload = () => resolve(next)
1063
+ next.onerror = () => reject(new Error('could not decode native layout pixels'))
1064
+ next.src = dataUrl
1065
+ })
1066
+ const canvas = document.createElement('canvas')
1067
+ canvas.width = image.naturalWidth
1068
+ canvas.height = image.naturalHeight
1069
+ const context = canvas.getContext('2d', { willReadFrequently: true })
1070
+ if (!context) throw new Error('could not read native layout pixels')
1071
+ context.drawImage(image, 0, 0)
1072
+ return context.getImageData(0, 0, canvas.width, canvas.height)
1073
+ }
1074
+ const normal = await capture(false)
1075
+ const background = await capture(true)
1076
+ for (const element of textElements) {
1077
+ element.contrast = scoreContrast({
1078
+ normal,
1079
+ background,
1080
+ box: element.box,
1081
+ fontSize: element.fontSize,
1082
+ fontWeight: element.fontWeight,
1083
+ })
1084
+ }
1085
+ return elements
1086
+ })()`;
1220
1087
  }
1221
- async function inspectTimelineSummary(bridge, query) {
1222
- return await bridge.send({
1223
- type: "call",
1224
- path: "SootSim.bridges.timeline.summary",
1225
- args: [query]
1088
+ var LAYOUT_GET_NATIVE_EVAL = layoutGetNativeEval();
1089
+ async function inspectGetLayout(bridge, opts = {}) {
1090
+ const raw = await bridge.send({
1091
+ type: "evaluate",
1092
+ code: layoutGetNativeEval(opts)
1226
1093
  });
1094
+ return Array.isArray(raw) ? raw : [];
1227
1095
  }
1228
- async function inspectTimelineRecent(bridge, query) {
1229
- return await bridge.send({
1230
- type: "call",
1231
- path: "SootSim.bridges.timeline.recent",
1232
- args: [query]
1096
+ function formatLayoutElements(elements, opts) {
1097
+ if (elements.length === 0) return " no visible elements found";
1098
+ const lines = elements.map((el) => {
1099
+ const ident = el.testID ? `#${el.testID}` : el.selector ? el.selector : el.role ? `[${el.role}]` : `<${el.type}>`;
1100
+ const parts = [ident, `@(${el.box.x},${el.box.y})`, `${el.box.w}x${el.box.h}`];
1101
+ if (el.borderRadius > 0) parts.push(`radius:${el.borderRadius}`);
1102
+ const p = el.padding;
1103
+ if (p.t || p.r || p.b || p.l) {
1104
+ const uniform = p.t === p.r && p.r === p.b && p.b === p.l;
1105
+ parts.push(uniform ? `pad:${p.t}` : `pad:${p.t},${p.r},${p.b},${p.l}`);
1106
+ }
1107
+ if (el.fontSize > 0) parts.push(`font:${el.fontSize}`);
1108
+ if (opts?.styling) {
1109
+ if (el.fontWeight) parts.push(`weight:${el.fontWeight}`);
1110
+ if (el.color) parts.push(`color:${el.color}`);
1111
+ if (el.bg) parts.push(`bg:${el.bg}`);
1112
+ if (el.contrast) {
1113
+ const score = el.contrast.ratio == null ? `?/${el.contrast.required}` : `${el.contrast.ratio}/${el.contrast.required}${el.contrast.ratio < el.contrast.required ? " FAIL" : ""}`;
1114
+ parts.push(`contrast:${score}`);
1115
+ if (el.contrast.background) parts.push(`on:${el.contrast.background}`);
1116
+ }
1117
+ if (el.opacity != null) parts.push(`opacity:${el.opacity}`);
1118
+ if (el.textDecoration) parts.push(`deco:${el.textDecoration}`);
1119
+ if (el.overflow) parts.push(`overflow:${el.overflow}`);
1120
+ if (el.borderWidth) {
1121
+ parts.push(
1122
+ el.borderColor ? `border:${el.borderWidth}/${el.borderColor}` : `border:${el.borderWidth}`
1123
+ );
1124
+ }
1125
+ }
1126
+ if (el.text) {
1127
+ const text = el.text.length > 40 ? `${el.text.slice(0, 39)}\u2026` : el.text;
1128
+ parts.push(`"${text}"`);
1129
+ }
1130
+ return ` ${parts.join(" ")}`;
1233
1131
  });
1132
+ return lines.join("\n");
1234
1133
  }
1235
- async function detectOpenNativeUI(bridge) {
1236
- let events;
1237
- try {
1238
- const res = await inspectTimelineRecent(bridge, {
1239
- kinds: ["alert", "actionsheet"],
1240
- limit: 80
1241
- });
1242
- events = res.events;
1243
- } catch {
1244
- return [];
1245
- }
1246
- const latest = /* @__PURE__ */ new Map();
1247
- for (const ev of events) latest.set(ev.kind, ev);
1248
- const out = [];
1249
- const check = (kind, label) => {
1250
- const ev = latest.get(kind);
1251
- if (!ev || !ev.data || typeof ev.data !== "object") return;
1252
- const d = ev.data;
1253
- if (d.phase !== "show") return;
1254
- out.push({ label, title: typeof d.title === "string" ? d.title : null });
1255
- };
1256
- check("alert", "Alert");
1257
- check("actionsheet", "ActionSheet");
1258
- return out;
1259
- }
1260
- async function inspectTimelineAdvanceCursor(bridge, cursorKey, watermark) {
1261
- await bridge.send({
1262
- type: "call",
1263
- path: "SootSim.bridges.timeline.cursorAdvance",
1264
- args: [cursorKey, watermark]
1265
- });
1134
+ var INSPECT_A11Y_CODE = `(async () => {
1135
+ const t = window.__sootsimTest
1136
+ if (!t) return []
1137
+
1138
+ const readLayout = (n) => n?.layout || null
1139
+ const readAbs = (n) => n?.absolutePosition || n?.absolute || null
1140
+ const readRole = (n) => n?.accessibilityRole || n?.role || null
1141
+ const readLabel = (n) => n?.accessibilityLabel || n?.label || n?.text || null
1142
+ const readHint = (n) => n?.accessibilityHint || n?.hint || null
1143
+ const readState = (n) => n?.accessibilityState || n?.state || null
1144
+ const readTestId = (n) => n?.testID || n?.testId || null
1145
+ const readType = (n) => typeof n?.type === 'string' ? n.type.toLowerCase() : n?.type
1146
+ const readPressable = (n) => {
1147
+ if (n?.pressable) return true
1148
+ const handlers = Array.isArray(n?.handlers) ? n.handlers : []
1149
+ const role = readRole(n)
1150
+ return handlers.length > 0 || role === 'button' || role === 'link' || role === 'tab'
1151
+ }
1152
+ const isTextInput = (n) => n?.isTextInput === true
1153
+ const isTextNode = (n) => readType(n) === 'text'
1154
+ const isVisibleTarget = (n) => {
1155
+ const layout = readLayout(n)
1156
+ if (!layout || layout.width <= 0 || layout.height <= 0) return false
1157
+ const abs = readAbs(n)
1158
+ if (!abs) return true
1159
+ const device = n?.device || {}
1160
+ const screenW = Number(device.width) || window.innerWidth || 0
1161
+ const screenH = Number(device.height) || window.innerHeight || 0
1162
+ return (
1163
+ abs.x + layout.width > 0 &&
1164
+ abs.y + layout.height > 0 &&
1165
+ abs.x < screenW &&
1166
+ abs.y < screenH
1167
+ )
1168
+ }
1169
+ const hasAccessibleSignal = (n) => {
1170
+ const role = readRole(n)
1171
+ const label = readLabel(n)
1172
+ const hint = readHint(n)
1173
+ if (role) return true
1174
+ if (hint) return true
1175
+ if (isTextInput(n)) return true
1176
+ if (readPressable(n)) return true
1177
+ if (isTextNode(n) && n?.text) return true
1178
+ if (typeof label === 'string' && label.length <= 30 && label !== n?.text) return true
1179
+ return false
1180
+ }
1181
+ const normalize = (n) => {
1182
+ const layout = readLayout(n)
1183
+ const abs = readAbs(n)
1184
+ const role = readRole(n) || (readPressable(n) ? 'button' : isTextInput(n) ? 'textfield' : isTextNode(n) ? 'statictext' : 'none')
1185
+ return {
1186
+ role,
1187
+ label: readLabel(n),
1188
+ hint: readHint(n),
1189
+ state: readState(n),
1190
+ testID: readTestId(n),
1191
+ position: abs ? { x: Math.round(abs.x), y: Math.round(abs.y) } : null,
1192
+ size: layout ? { w: Math.round(layout.width), h: Math.round(layout.height) } : null,
1193
+ }
1194
+ }
1195
+
1196
+ if (typeof t.listInspectable === 'function') {
1197
+ const list = await t.listInspectable({})
1198
+ if (Array.isArray(list)) {
1199
+ return list.filter(n => isVisibleTarget(n) && hasAccessibleSignal(n)).map(normalize)
1200
+ }
1201
+ }
1202
+
1203
+ const all = await t.queryAll({ pruneHidden: true })
1204
+ return all
1205
+ .filter(n => isVisibleTarget(n) && hasAccessibleSignal(n))
1206
+ .map(normalize)
1207
+ })()`;
1208
+ async function inspectAccessibilityTree(bridge) {
1209
+ const nodes = await bridge.send({ type: "evaluate", code: INSPECT_A11Y_CODE });
1210
+ return Array.isArray(nodes) ? nodes : [];
1266
1211
  }
1267
- async function inspectKeyboard(bridge) {
1268
- const result = await bridge.send({
1269
- type: "evaluate",
1270
- code: `(() => {
1271
- const kb = window.__sootsimKeyboard
1272
- if (!kb || typeof kb.getLayout !== 'function') {
1273
- return { error: 'keyboard bridge getLayout() not available' }
1212
+ var FIND_INSPECTABLE_TARGETS = `
1213
+ const fromInspectable = async () => {
1214
+ if (typeof t.listInspectable !== 'function') return null
1215
+ const list = await t.listInspectable({})
1216
+ if (!Array.isArray(list)) return null
1217
+ return list.map((n) => {
1218
+ const role = n.accessibilityRole ?? n.role
1219
+ const label = n.accessibilityLabel ?? n.label
1220
+ const layout = n.layout
1221
+ const absolutePosition = n.absolutePosition ?? n.absolute
1222
+ const screenW = Number(n.device?.width) || window.innerWidth || 0
1223
+ const screenH = Number(n.device?.height) || window.innerHeight || 0
1224
+ const visibleFrame = layout && absolutePosition
1225
+ ? {
1226
+ x: Math.max(0, absolutePosition.x),
1227
+ y: Math.max(0, absolutePosition.y),
1228
+ width: Math.max(
1229
+ 0,
1230
+ Math.min(screenW, absolutePosition.x + layout.width) -
1231
+ Math.max(0, absolutePosition.x),
1232
+ ),
1233
+ height: Math.max(
1234
+ 0,
1235
+ Math.min(screenH, absolutePosition.y + layout.height) -
1236
+ Math.max(0, absolutePosition.y),
1237
+ ),
1238
+ }
1239
+ : null
1240
+ const handlers = Array.isArray(n.handlers) ? n.handlers : []
1241
+ const pressable =
1242
+ handlers.length > 0 ||
1243
+ role === 'button' ||
1244
+ role === 'link' ||
1245
+ role === 'tab'
1246
+ return {
1247
+ type: typeof n.type === 'string' ? n.type.toLowerCase() : n.type,
1248
+ id: n.id ?? n.nodeId,
1249
+ nodeId: n.nodeId,
1250
+ testID: n.testID ?? n.testId,
1251
+ text: n.text,
1252
+ layout,
1253
+ absolutePosition,
1254
+ visibleFrame,
1255
+ style: n.computedStyle ?? n.style ?? {},
1256
+ childCount: n.childCount ?? 0,
1257
+ pressed: n.pressed ?? false,
1258
+ pressable,
1259
+ isTextInput: n.isTextInput ?? false,
1260
+ accessible: n.accessible ?? true,
1261
+ accessibilityLabel: label,
1262
+ accessibilityRole: role,
1263
+ accessibilityHint: n.accessibilityHint,
1264
+ accessibilityState: n.accessibilityState,
1265
+ accessibilityValue: n.accessibilityValue,
1266
+ }
1267
+ })
1274
1268
  }
1275
- return kb.getLayout()
1269
+ const isVisibleTarget = (n) => {
1270
+ if (!n?.layout) return false
1271
+ const frame = n.visibleFrame
1272
+ if (frame) return frame.width > 0 && frame.height > 0
1273
+ const abs = n.absolutePosition
1274
+ if (!abs) return n.layout.width > 0 && n.layout.height > 0
1275
+ const screenW = window.innerWidth || 0
1276
+ const screenH = window.innerHeight || 0
1277
+ return (
1278
+ abs.x + n.layout.width > 0 &&
1279
+ abs.y + n.layout.height > 0 &&
1280
+ abs.x < screenW &&
1281
+ abs.y < screenH
1282
+ )
1283
+ }
1284
+ `;
1285
+ function resolveFindMode(q) {
1286
+ if (q.testId) {
1287
+ return {
1288
+ mode: "testid",
1289
+ code: `(async () => {
1290
+ const t = window.__sootsimTest
1291
+ if (!t) return null
1292
+ return (await t.findByTestId(${JSON.stringify(q.testId)})) || (await t.findById(${JSON.stringify(q.testId)}))
1276
1293
  })()`
1277
- });
1278
- return result ?? { error: "keyboard bridge returned no result" };
1279
- }
1280
- function isShellCommandUnavailable(error) {
1281
- const message = error instanceof Error ? error.message : String(error);
1282
- return message.includes("call target not found: SootSim.bridges.mainShell") || message.includes("test bridge unavailable before app-in-worker boot");
1283
- }
1284
- async function getShellState(bridge, readyTimeoutMs = 0) {
1285
- const deadline = Date.now() + Math.max(0, readyTimeoutMs);
1286
- while (true) {
1287
- try {
1288
- return await bridge.send({
1289
- type: "call",
1290
- path: "SootSim.bridges.mainShell.getState",
1291
- args: []
1292
- });
1293
- } catch (error) {
1294
- if (!isShellCommandUnavailable(error) || Date.now() >= deadline) throw error;
1295
- await new Promise((r) => setTimeout(r, 50));
1296
- }
1294
+ };
1297
1295
  }
1298
- }
1299
- async function inspectScreens(bridge) {
1300
- const payload = await bridge.send({
1301
- type: "evaluate",
1302
- code: `(async () => {
1303
- const test = window.__sootsimTest
1304
- const kb = window.__sootsimKeyboard
1305
- const navSnap =
1306
- test && typeof test.getNavigationSnapshot === 'function'
1307
- ? await test.getNavigationSnapshot()
1308
- : null
1309
- const route =
1310
- test && typeof test.getRouteInfo === 'function'
1311
- ? await test.getRouteInfo()
1312
- : null
1313
- const keyboard =
1314
- kb && typeof kb.getLayout === 'function'
1315
- ? (() => {
1316
- const layout = kb.getLayout()
1317
- return layout ? {
1318
- visible: layout.visible,
1319
- mode: layout.mode,
1320
- spec: layout.spec
1321
- ? {
1322
- keyboardType: layout.spec.keyboardType,
1323
- returnKeyType: layout.spec.returnKeyType,
1324
- }
1325
- : null,
1326
- } : null
1327
- })()
1328
- : null
1329
- return { nav: navSnap, route, keyboard }
1296
+ if (q.role) {
1297
+ return {
1298
+ mode: "role",
1299
+ code: `(async () => {
1300
+ const t = window.__sootsimTest
1301
+ if (!t) return []
1302
+ return await t.queryAll({ hasRole: ${JSON.stringify(q.role)}, pruneHidden: true })
1330
1303
  })()`
1331
- });
1332
- const shell = await getShellState(bridge, 500).catch(() => null);
1333
- return {
1334
- shell,
1335
- nav: payload?.nav ?? null,
1336
- route: payload?.route ?? null,
1337
- keyboard: payload?.keyboard ?? null
1338
- };
1339
- }
1340
- var DEBUG_CHANNELS = [
1341
- "portals",
1342
- "sheets",
1343
- "layout",
1344
- "onlayout",
1345
- "animated",
1346
- "render",
1347
- "touch",
1348
- "yoga"
1349
- ];
1350
- async function inspectDebugStatus(bridge) {
1351
- return bridge.send({ type: "evaluate", code: "window.__sootsimDebug.status()" });
1352
- }
1353
- async function inspectDebugFlags(bridge) {
1354
- return bridge.send({ type: "evaluate", code: "window.__sootsimDebug.flags()" });
1355
- }
1356
- async function inspectDebugFind(bridge, target) {
1357
- return bridge.send({
1358
- type: "evaluate",
1359
- code: `window.__sootsimTest.debugFind(${JSON.stringify(target)})`
1360
- });
1361
- }
1362
- async function inspectDebugRecent(bridge, channel, limit = 50) {
1363
- const code = channel && channel !== "all" ? `window.__sootsimDebug.recent(${JSON.stringify(channel)}, ${limit})` : `window.__sootsimDebug.recent(undefined, ${limit})`;
1364
- return bridge.send({ type: "evaluate", code });
1365
- }
1366
- async function setDebugChannels(bridge, action, channels) {
1367
- const args = channels.length > 0 ? channels.map((c) => JSON.stringify(c)).join(", ") : action === "disable" ? "'all'" : "";
1368
- return bridge.send({
1369
- type: "evaluate",
1370
- code: `window.__sootsimDebug.${action}(${args})`
1371
- });
1372
- }
1373
- async function inspectMemory(bridge) {
1374
- const result = await bridge.send({
1375
- type: "evaluate",
1376
- code: `(async () => {
1377
- const host = window.__sootsimRenderHost
1378
- const stats = host?.queryStats ? await host.queryStats() : null
1379
- const shellRaw = window.__sootsimShellMemory
1380
- ? await window.__sootsimShellMemory()
1381
- : null
1382
- const shell = shellRaw && !shellRaw.error ? shellRaw : null
1383
- // the compositor paints home + app:one + app:two, so every app-surface
1384
- // picture and raster image is counted here and nowhere else.
1385
- let compositor = null
1386
- try {
1387
- const cs = await window.__sootsimCompositor?.getStats?.(false, true)
1388
- compositor = cs?.memory ?? null
1389
- } catch {}
1390
- const normalize = (m) =>
1391
- m
1392
- ? {
1393
- nodesRegistered: m.nodesRegistered ?? null,
1394
- nodesDetached: m.nodesDetached ?? null,
1395
- detachedTypes: m.detachedTypes ?? null,
1396
- objects: m.objects ?? null,
1397
- activeNativeAnimations: m.activeNativeAnimations ?? null,
1398
- imageLoader: m.imageLoader ?? null,
1399
- workerHeap: m.workerHeap ?? null,
1400
- wasmHeapBytes: m.wasmHeapBytes ?? null,
1401
- }
1402
- : null
1403
- const hostMem = performance.memory
1404
- ? {
1405
- usedJSHeapSize: performance.memory.usedJSHeapSize,
1406
- totalJSHeapSize: performance.memory.totalJSHeapSize,
1407
- jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
1408
- }
1409
- : null
1410
- return {
1411
- tenant: normalize(stats?.memory),
1412
- shell: normalize(shell),
1413
- compositor: normalize(compositor),
1414
- hostHeap: hostMem,
1304
+ };
1305
+ }
1306
+ if (q.type) {
1307
+ return {
1308
+ mode: "type",
1309
+ code: `(async () => {
1310
+ const t = window.__sootsimTest
1311
+ if (!t) return []
1312
+ return await t.queryAll({ type: ${JSON.stringify(q.type)}, pruneHidden: true })
1313
+ })()`
1314
+ };
1315
+ }
1316
+ if (q.pressable) {
1317
+ return {
1318
+ mode: "pressable",
1319
+ code: `(async () => {
1320
+ const t = window.__sootsimTest
1321
+ if (!t) return []
1322
+ ${FIND_INSPECTABLE_TARGETS}
1323
+ const inspectable = await fromInspectable()
1324
+ if (inspectable) return inspectable.filter(n => n.pressable && isVisibleTarget(n))
1325
+ const all = await t.queryAll({ pruneHidden: true })
1326
+ return all.filter(n => n.pressable && isVisibleTarget(n))
1327
+ })()`
1328
+ };
1329
+ }
1330
+ if (q.interactive) {
1331
+ return {
1332
+ mode: "interactive-targets",
1333
+ code: `(async () => {
1334
+ const t = window.__sootsimTest
1335
+ if (!t) return []
1336
+ ${FIND_INSPECTABLE_TARGETS}
1337
+ const inspectable = await fromInspectable()
1338
+ if (inspectable) {
1339
+ return inspectable.filter(n => n.pressable && isVisibleTarget(n))
1415
1340
  }
1341
+ const all = await t.queryAll({ pruneHidden: true })
1342
+ return all.filter(n => n.pressable && isVisibleTarget(n))
1416
1343
  })()`
1417
- });
1418
- return result ?? { tenant: null, shell: null, compositor: null, hostHeap: null };
1419
- }
1420
- function formatTimelineSummary(summary) {
1421
- if (summary.total === 0) return "nothing recorded";
1422
- const parts = [];
1423
- const ORDER = [
1424
- "error",
1425
- "warning",
1426
- "console",
1427
- "fetch",
1428
- "toast",
1429
- "alert",
1430
- "actionsheet",
1431
- "picker",
1432
- "notification",
1433
- "screen",
1434
- "route",
1435
- "keyboard",
1436
- "app-launch",
1437
- "shell",
1438
- "scroll",
1439
- "gesture",
1440
- "text-input",
1441
- "react-commit",
1442
- "animation",
1443
- "reanimated"
1444
- ];
1445
- const seen = /* @__PURE__ */ new Set();
1446
- for (const k of ORDER) {
1447
- const n = summary.byKind[k];
1448
- if (n) {
1449
- parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
1450
- seen.add(k);
1451
- }
1344
+ };
1452
1345
  }
1453
- for (const [k, n] of Object.entries(summary.byKind)) {
1454
- if (!seen.has(k) && n) parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
1346
+ if (q.visible) {
1347
+ return {
1348
+ mode: "visible",
1349
+ code: `(async () => {
1350
+ const t = window.__sootsimTest
1351
+ if (!t) return []
1352
+ const all = await t.queryAll({ pruneHidden: true })
1353
+ return all.filter(n => n.layout && n.layout.width > 0 && n.layout.height > 0)
1354
+ })()`
1355
+ };
1455
1356
  }
1456
- return parts.join(" \xB7 ");
1357
+ if (q.text) {
1358
+ return {
1359
+ mode: "text",
1360
+ code: `(async () => {
1361
+ const t = window.__sootsimTest
1362
+ if (!t) return null
1363
+ return await t.findByText(${JSON.stringify(q.text)})
1364
+ })()`
1365
+ };
1366
+ }
1367
+ return null;
1457
1368
  }
1458
- var NOISY_TIMELINE_KINDS = /* @__PURE__ */ new Set([
1459
- "react-commit",
1460
- "layout",
1461
- "scroll"
1462
- ]);
1463
- function formatTimelineRelativeTime(t, anchor) {
1464
- if (anchor === null) return new Date(t).toLocaleTimeString();
1465
- const dt = (t - anchor) / 1e3;
1466
- const sign = dt >= 0 ? "+" : "";
1467
- return `${sign}${dt.toFixed(2)}s`;
1369
+ async function inspectFind(bridge, q) {
1370
+ const resolved = resolveFindMode(q);
1371
+ if (!resolved) return null;
1372
+ const result = await bridge.send({ type: "evaluate", code: resolved.code });
1373
+ return { mode: resolved.mode, result };
1468
1374
  }
1469
- function formatTimelinePayload(kind, d) {
1470
- switch (kind) {
1471
- case "app-launch":
1472
- return d.phase === "launch" ? `launch ${d.appName ?? d.toAppId ?? ""}` : `dismiss ${d.appName ?? d.fromAppId ?? ""} \u2192 ${d.toAppId ?? ""}`;
1473
- case "toast":
1474
- return `"${d.text ?? ""}"${d.durationMs ? ` (${d.durationMs}ms)` : ""}`;
1475
- case "keyboard":
1476
- return `${d.phase ?? "?"}${d.heightPx ? ` h=${d.heightPx}` : ""}${d.mode ? ` ${d.mode}` : ""}`;
1477
- case "screen":
1478
- return `${d.phase ?? "?"} ${d.name ?? d.activeName ?? ""}`;
1479
- case "flow-step":
1480
- return `${d.status ?? "?"} ${d.stepName ?? "step"}${d.durationMs ? ` ${d.durationMs}ms` : ""}`;
1481
- case "route":
1482
- return `${d.phase ?? "?"} ${d.path ?? d.pathname ?? ""}`;
1483
- case "alert":
1484
- case "actionsheet":
1485
- case "picker":
1486
- return `${d.phase ?? "?"} ${d.title ?? d.message ?? ""}`;
1487
- case "notification":
1488
- return `${d.title ?? ""}${d.body ? ` \u2014 ${d.body}` : ""}`;
1489
- case "fetch":
1490
- return `${d.method ?? "GET"} ${d.url ?? ""}${d.status ? ` -> ${d.status}` : ""}`;
1491
- case "console":
1492
- case "console-log":
1493
- return `${d.level ?? "log"}: ${(d.message ?? "").toString().slice(0, 120)}`;
1494
- case "shell":
1495
- return `${d.event ?? d.type ?? d.phase ?? ""}`;
1496
- case "scroll":
1497
- return `${d.phase ?? "?"} ${d.target ?? ""}`;
1498
- case "gesture":
1499
- return `${d.phase ?? "?"} ${d.type ?? ""}`;
1500
- case "text-input":
1501
- return `${d.phase ?? "?"}${d.value !== void 0 ? ` "${String(d.value).slice(0, 40)}"` : ""}`;
1502
- case "layout":
1503
- return `${d.kind ?? "?"} ${d.testID ?? d.type ?? ""}${d.skipped ? ` skipped:${d.reason ?? "unknown"}` : ""}`;
1504
- case "react-commit": {
1505
- const slowest = d.slowest;
1506
- return `${d.fiberCount ?? "?"} fibers ${d.durationMs ?? "?"}ms${slowest?.displayName ? ` \xB7 ${slowest.displayName} ${slowest.durationMs ?? "?"}ms` : ""}`;
1507
- }
1508
- case "frame":
1509
- return `${d.totalMs ?? "?"}ms${d.renderMs ? ` \xB7 render ${d.renderMs}ms` : ""}${d.layoutMs ? ` \xB7 layout ${d.layoutMs}ms` : ""}${d.copyMs ? ` \xB7 copy ${d.copyMs}ms` : ""}`;
1510
- case "reanimated":
1511
- case "animation":
1512
- return `${d.kind ?? ""} ${d.target ?? ""}${d.durationMs ? ` ${d.durationMs}ms` : ""}`;
1375
+ function rankInteractive(nodes) {
1376
+ return [...nodes].sort((a, b) => scoreInteractive(b) - scoreInteractive(a));
1377
+ }
1378
+ function scoreInteractive(n) {
1379
+ let score = 0;
1380
+ if (n.testID) score += 100;
1381
+ if (typeof n.text === "string" && n.text.trim().length > 0) score += 60;
1382
+ if (typeof n.accessibilityLabel === "string" && n.accessibilityLabel.trim().length > 0) {
1383
+ score += 30;
1513
1384
  }
1514
- return "";
1385
+ if (n.accessibilityRole) score += 15;
1386
+ const w = n.layout?.width ?? 0;
1387
+ const h = n.layout?.height ?? 0;
1388
+ const area = w * h;
1389
+ if (area >= 400 && area <= 6e4) score += 25;
1390
+ else if (area > 6e4) score -= 20;
1391
+ const y = n.absolutePosition?.y ?? 0;
1392
+ if (y < 0) score -= 30;
1393
+ return score;
1515
1394
  }
1516
- function formatTimelineEvent(event, anchor) {
1517
- const ts = formatTimelineRelativeTime(event.t, anchor).padStart(8);
1518
- const ctx = event.context.padEnd(6);
1519
- const kind = `[${event.kind}]`.padEnd(15);
1520
- const d = event.data;
1521
- const payload = d && typeof d === "object" ? formatTimelinePayload(event.kind, d) : "";
1522
- return ` ${ts} ${ctx} ${kind} ${payload}`;
1395
+ function tapCommandForNode(n) {
1396
+ if (n.testID) return `rnxsim do tap-id ${shellEscape(n.testID)}`;
1397
+ const text = typeof n.text === "string" ? n.text.trim() : "";
1398
+ if (text.length > 0 && text.length <= 80)
1399
+ return `rnxsim do tap-text ${shellEscape(text)}`;
1400
+ const x = Math.round(((n.absolutePosition?.x ?? 0) + (n.layout?.width ?? 0) / 2) * 10) / 10;
1401
+ const y = Math.round(((n.absolutePosition?.y ?? 0) + (n.layout?.height ?? 0) / 2) * 10) / 10;
1402
+ return `rnxsim do tap ${x} ${y}`;
1523
1403
  }
1524
- function formatTimelineEvents(events) {
1525
- if (!events.length) return "";
1526
- const anchor = events[0]?.t ?? null;
1527
- return events.map((e) => formatTimelineEvent(e, anchor)).join("\n");
1404
+ function shellEscape(value) {
1405
+ if (/^[A-Za-z0-9_./@:-]+$/.test(value)) return value;
1406
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1528
1407
  }
1529
-
1530
- // cli/commands/inspect/settling.ts
1531
- async function waitForSootsimIdle({
1532
- bridge,
1533
- simId,
1534
- maxMs,
1535
- pollMs = 50,
1536
- stablePolls = 3,
1537
- strict = false
1538
- }) {
1539
- const result = await bridge.send(
1540
- {
1541
- type: "evaluate",
1542
- simId,
1543
- code: `(async () => {
1544
- const start = Date.now()
1545
- const deadline = start + ${Math.max(0, Math.round(maxMs))}
1546
- const pollMs = ${Math.max(1, Math.round(pollMs))}
1547
- const requiredStablePolls = ${Math.max(1, Math.round(stablePolls))}
1548
- const strict = ${strict ? "true" : "false"}
1549
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
1550
-
1551
- // route-aware settle: a tap that pushes/pops a screen is async \u2014 the
1552
- // old screen still renders for a moment, then the new one mounts and
1553
- // its content loads. a pure layout-hash check reports "idle" on the
1554
- // outgoing screen or on the incoming skeleton, so a driver re-taps
1555
- // and stacks duplicate navigations. drain any in-flight screen
1556
- // transition FIRST, bounded by the overall budget. when no
1557
- // transition is happening this returns in ~80ms (startWindowMs), so
1558
- // a plain button tap barely pays for it.
1559
- try {
1560
- const wfst = window.__sootsimTest?.waitForScreenTransitions
1561
- if (typeof wfst === 'function') {
1562
- const remaining = deadline - Date.now()
1563
- if (remaining > 120) {
1564
- await wfst({
1565
- timeoutMs: Math.min(remaining - 80, 4000),
1566
- settleMs: 64,
1567
- startWindowMs: 80,
1568
- })
1569
- }
1570
- }
1571
- } catch {}
1572
-
1573
- const readSnapshot = async () => {
1574
- let animating = false
1575
- let pendingFetches = 0
1576
- let requestTotal = 0
1577
- let requestInFlight = 0
1578
- try {
1579
- const stats = await window.__sootsimRenderHost?.queryStats?.()
1580
- if (stats) {
1581
- animating =
1582
- stats.hasActiveAnims === true ||
1583
- stats.hasActiveNativeAnimations === true ||
1584
- stats.hasPendingAnimationFrames === true
1585
- // a freshly-pushed screen showing a skeleton is layout-stable
1586
- // but not actually settled \u2014 its data/images are still in
1587
- // flight. treat bounded image-loader fetches as not-idle so
1588
- // settle waits for real content, capped by the budget.
1589
- const pf = stats.memory && stats.memory.imageLoader
1590
- ? stats.memory.imageLoader.pendingFetches
1591
- : 0
1592
- pendingFetches = typeof pf === 'number' ? pf : 0
1593
- }
1594
- } catch {}
1595
- try {
1596
- const counts = await window.__sootsimTest?.getRequestCounts?.()
1597
- requestTotal = typeof counts?.total === 'number' ? counts.total : 0
1598
- requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
1599
- } catch {}
1600
- const root = window.__sootsimRoot
1601
- const nodes = []
1602
- if (root) {
1603
- const walk = (n) => {
1604
- if (n.layout && n.layout.width > 0) {
1605
- nodes.push(Math.round(n.layout.x) + ',' + Math.round(n.layout.y) + ',' + Math.round(n.layout.width))
1606
- }
1607
- for (const c of n.children || []) walk(c)
1608
- }
1609
- walk(root)
1610
- }
1611
- return { layout: nodes.join(';'), animating, pendingFetches, requestTotal, requestInFlight }
1408
+ var WAIT_READY_PROBE = `(async () => {
1409
+ const t = window.__sootsimTest
1410
+ let nodes = 0
1411
+ try { nodes = (await t?.getNodeCount?.()) || 0 } catch {}
1412
+ let targets = 0
1413
+ let liveFrameActive = false
1414
+ let liveFrameChannels = 0
1415
+ let liveFramePublishes = 0
1416
+ let loadingText = ''
1417
+ const cleanText = (value) =>
1418
+ typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''
1419
+ const loadingSignal = (value) => {
1420
+ const text = cleanText(value)
1421
+ if (!text) return ''
1422
+ return /^(opening app\\b|looking for your app\\b|building bundle\\b|preparing rnxsim runtime\\b|loading native bundle\\b|loading rnx stubs\\b|loading bundle loader\\b|loading native stubs\\b|loading compat bridge\\b|loading react-native shim\\b|loading react bridge\\b|loading reconciler\\b|loading replacement modules\\b|injecting rnx modules\\b|warming sqlite engine\\b|capturing root component\\b|reloading app\\b|loading(?:[.!\u2026]*)?$|connecting(?:[.!\u2026]*)?$)/i.test(text)
1423
+ ? text
1424
+ : ''
1425
+ }
1426
+ try {
1427
+ const loading = window.__sootsimConnectRNLoading
1428
+ if (loading && loading.active === true) {
1429
+ loadingText = [loading.title, loading.detail].map(cleanText).filter(Boolean).join(' ')
1612
1430
  }
1613
-
1614
- // how long generic background network is allowed to keep us waiting AFTER
1615
- // the screen is otherwise visually settled. apps with continuous traffic
1616
- // (polling, realtime sockets, a tap that fires 20-50 fetches) never reach
1617
- // requestInFlight===0 / a stable requestTotal, so requiring full network
1618
- // quiet burns the ENTIRE budget on every command \u2014 the leading idle that
1619
- // failed PR-preview recordings for network-heavy apps (3pc fight-pulse).
1620
- // layout-stability + the image-loader already prove visible content
1621
- // loaded (a data load changes layout; an image load shows in the loader),
1622
- // so the generic request counters are a courtesy, not a gate: honor them
1623
- // only within this grace once the screen is visually still.
1624
- const networkQuietGraceMs = 1200
1625
- let lastLayout = ''
1626
- let lastRequestTotal = -1
1627
- let stable = 0
1628
- let visuallyStillSince = 0
1629
- while (Date.now() < deadline) {
1630
- const snapshot = await readSnapshot()
1631
- const strictOk = !strict || !snapshot.animating
1632
- // visually settled: no animations, no image content loading, layout
1633
- // unchanged since the last poll.
1634
- const visuallyStill =
1635
- strictOk && snapshot.pendingFetches === 0 && snapshot.layout === lastLayout
1636
- // generic network quiet: nothing in flight and no new requests issued.
1637
- const networkQuiet =
1638
- snapshot.requestInFlight === 0 && snapshot.requestTotal === lastRequestTotal
1639
- if (visuallyStill) {
1640
- stable++
1641
- if (visuallyStillSince === 0) visuallyStillSince = Date.now()
1642
- // settle once the screen has held still long enough AND either the
1643
- // network is genuinely quiet OR it has refused to quiet within the
1644
- // grace (continuous background traffic must not block forever).
1645
- if (
1646
- stable >= requiredStablePolls &&
1647
- (networkQuiet || Date.now() - visuallyStillSince >= networkQuietGraceMs)
1648
- ) {
1649
- return { settled: true, elapsed: Date.now() - start }
1431
+ } catch {}
1432
+ try {
1433
+ if (typeof t?.listInspectable === 'function') {
1434
+ const list = await t.listInspectable({})
1435
+ if (Array.isArray(list)) {
1436
+ const screenW = window.innerWidth || 0
1437
+ const screenH = window.innerHeight || 0
1438
+ const isVisible = (n) => {
1439
+ const layout = n?.layout
1440
+ if (!layout || layout.width <= 0 || layout.height <= 0) return false
1441
+ const abs = n?.absolutePosition || n?.absolute
1442
+ if (!abs) return true
1443
+ const device = n?.device || {}
1444
+ const w = Number(device.width) || screenW
1445
+ const h = Number(device.height) || screenH
1446
+ return (
1447
+ abs.x + layout.width > 0 &&
1448
+ abs.y + layout.height > 0 &&
1449
+ abs.x < w &&
1450
+ abs.y < h
1451
+ )
1452
+ }
1453
+ const hasContentSignal = (n) => {
1454
+ const role = n?.accessibilityRole || n?.role
1455
+ const label = cleanText(n?.accessibilityLabel || n?.label)
1456
+ const handlers = Array.isArray(n?.handlers) ? n.handlers : []
1457
+ const text = cleanText(n?.text)
1458
+ const testID = cleanText(n?.testID || n?.testId)
1459
+ const loading = loadingSignal(text) || loadingSignal(label) || loadingSignal(testID)
1460
+ if (!loadingText && loading) {
1461
+ loadingText = loading
1462
+ }
1463
+ if (handlers.length > 0) return true
1464
+ if (role) return true
1465
+ if (n?.isTextInput) return true
1466
+ if (text) return true
1467
+ if (label) return true
1468
+ if (testID && !/^splash|loading/i.test(testID)) return true
1469
+ return false
1650
1470
  }
1651
- } else {
1652
- stable = 0
1653
- visuallyStillSince = 0
1471
+ targets = list.filter(n => isVisible(n) && hasContentSignal(n)).length
1654
1472
  }
1655
- lastLayout = snapshot.layout
1656
- lastRequestTotal = snapshot.requestTotal
1657
- await sleep(pollMs)
1658
1473
  }
1659
- return { settled: false, elapsed: Date.now() - start }
1660
- })()`
1661
- },
1662
- {
1663
- timeoutMs: maxMs + 1e3
1474
+ } catch {}
1475
+ try {
1476
+ const stats = await t?.getImageLoaderStats?.()
1477
+ const presentation = stats?.liveFramePresentation
1478
+ liveFrameChannels = Number(presentation?.activeChannels) || 0
1479
+ liveFramePublishes = Number(presentation?.publishes) || 0
1480
+ const previous = window.__sootsimWaitReadyLiveFramePublishes
1481
+ liveFrameActive =
1482
+ liveFrameChannels > 0 &&
1483
+ typeof previous === 'number' &&
1484
+ liveFramePublishes > previous
1485
+ window.__sootsimWaitReadyLiveFramePublishes = liveFramePublishes
1486
+ } catch {}
1487
+ let externalReady = null
1488
+ let externalStatus = ''
1489
+ let externalError = ''
1490
+ let suppressedEntryError = ''
1491
+ try {
1492
+ const getExternalAppState = t?.getExternalAppState
1493
+ const external = typeof getExternalAppState === 'function'
1494
+ ? await Promise.race([
1495
+ getExternalAppState(),
1496
+ new Promise((resolve) => setTimeout(
1497
+ () => resolve({
1498
+ state: {
1499
+ ready: false,
1500
+ loading: true,
1501
+ status: 'waiting for tenant worker...',
1502
+ },
1503
+ }),
1504
+ 1000,
1505
+ )),
1506
+ ])
1507
+ : null
1508
+ const state = external?.state
1509
+ if (state && typeof state === 'object') {
1510
+ externalReady = state.ready === true
1511
+ if (typeof state.status === 'string') externalStatus = state.status
1512
+ if (typeof state.error === 'string') externalError = state.error
1513
+ }
1514
+ if (!externalError && Array.isArray(external?.entryErrors)) {
1515
+ // suppressed-and-continued entry errors (bundle-loader's patched __r
1516
+ // swallows entry throws and moves on) must NOT gate readiness \u2014 the
1517
+ // app frequently runs fine after one (e.g. metro's "Unknown named
1518
+ // module" probing noise). they surface separately as a diagnostic
1519
+ // for the timeout path.
1520
+ const isReal = (item) => item && typeof item.message === 'string' && item.message.trim()
1521
+ const fatal = external.entryErrors.find((item) => isReal(item) && item.suppressed !== true)
1522
+ if (fatal) externalError = fatal.message
1523
+ const suppressed = external.entryErrors.find((item) => isReal(item) && item.suppressed === true)
1524
+ if (suppressed) suppressedEntryError = suppressed.message
1525
+ }
1526
+ // once the app has errored its last status ("injecting sootsim
1527
+ // modules...") is stale \u2014 reporting it makes a dead app read as one that
1528
+ // is still booting.
1529
+ if (externalError) loadingText = ''
1530
+ else if (externalReady === false && externalStatus) loadingText = externalStatus
1531
+ } catch {}
1532
+ let errors = 0
1533
+ try { errors = window.__sootsimConsole?.count?.()?.errors ?? 0 } catch {}
1534
+ return {
1535
+ flag: (window).__sootsimExternalAppReady,
1536
+ at: (window).__sootsimExternalAppReadyAt || 0,
1537
+ nodes,
1538
+ targets,
1539
+ liveFrameActive,
1540
+ liveFrameChannels,
1541
+ liveFramePublishes,
1542
+ errors,
1543
+ loadingText,
1544
+ externalReady,
1545
+ externalStatus,
1546
+ externalError,
1547
+ suppressedEntryError,
1664
1548
  }
1665
- );
1666
- const { elapsed, settled } = result ?? {};
1667
- return {
1668
- elapsed: typeof elapsed === "number" ? elapsed : maxMs,
1669
- settled: settled === true
1670
- };
1549
+ })()`;
1550
+ var READY_CONTENT_NODE_FLOOR = 100;
1551
+ var READY_NODE_STABLE_MS = 750;
1552
+ var WAIT_READY_PROGRESS_INTERVAL_MS = 2e3;
1553
+ var READY_PROBE_POLL_MS = 150;
1554
+ function readyProbeHasContent(probe) {
1555
+ return probe.liveFrameActive || probe.targets > 0 || probe.nodes >= READY_CONTENT_NODE_FLOOR;
1671
1556
  }
1672
-
1673
- // cli/commands/inspect/actions.ts
1674
- var SCREEN_EVAL = `(() => {
1675
- const spec = window.SootSim?.state?.engineSnapshot?.windowState?.deviceSpec
1676
- return spec && spec.width > 0 && spec.height > 0
1677
- ? { width: spec.width, height: spec.height }
1678
- : null
1679
- })()`;
1680
- var DEFAULT_AGENT_TIMING = {
1681
- initialWaitMs: 3e3,
1682
- deadlineMs: 5e3,
1683
- retryWaitMs: 700
1684
- };
1685
- var DEFAULT_INTERACTIVE_TIMING = {
1686
- initialWaitMs: 1200,
1687
- deadlineMs: 2500,
1688
- retryWaitMs: 700
1689
- };
1690
- function tapTiming(agent, opts = {}) {
1691
- return {
1692
- ...agent ? DEFAULT_AGENT_TIMING : DEFAULT_INTERACTIVE_TIMING,
1693
- ...opts
1694
- };
1557
+ function readyProbeHasTargetContent(probe) {
1558
+ return probe.liveFrameActive || probe.targets > 0;
1695
1559
  }
1696
- function isTapSuccess(result) {
1697
- if (!result || result.hit === false || result.ok === false) return false;
1698
- if (result.requestedTargetMatched === false) return false;
1699
- if (result.pointerTapHandled === false && !result.keyboardOpened && !result.isTextInput) {
1700
- return false;
1560
+ function waitReadyReason(status) {
1561
+ if (status.bridgeError) {
1562
+ return `sim unreachable: ${status.bridgeError} \u2014 the target tab is closed or no sim is connected (check \`rnxsim list\`, reopen with \`rnxsim open\`)`;
1701
1563
  }
1702
- return true;
1564
+ const base = status.externalError ? `guest app errored: ${status.externalError}` : status.loadingText ? `still showing "${status.loadingText}"` : status.externalReady === false ? "guest app is still loading" : status.flag !== true && status.targets > 0 ? "native content is rendered but the ready signal has not settled" : status.flag !== true ? "guest app has not emitted sootsim:externalAppReady" : status.targets <= 0 ? "ready flag emitted but no visible app content is inspectable yet" : "node tree is still changing";
1565
+ return !status.externalError && status.suppressedEntryError ? `${base} (suppressed entry error: ${status.suppressedEntryError})` : base;
1703
1566
  }
1704
- function tapTargetFromPayload(payload, textFallback) {
1705
- return {
1706
- nodeId: payload.target?.nodeId ?? payload.match?.nodeId ?? payload.node?.nodeId ?? null,
1707
- id: payload.target?.id ?? payload.match?.id ?? payload.node?.id ?? null,
1708
- testID: payload.target?.testID ?? payload.match?.testID ?? payload.node?.testID ?? null,
1709
- text: payload.target?.text ?? payload.target?.accessibilityLabel ?? payload.match?.text ?? payload.match?.accessibilityLabel ?? payload.node?.text ?? textFallback ?? null,
1710
- type: payload.target?.type ?? payload.match?.type ?? payload.node?.type ?? null
1567
+ async function inspectWaitReady(bridge, timeoutMs = 2e4, options = {}) {
1568
+ const start = Date.now();
1569
+ const deadline = start + timeoutMs;
1570
+ const progressIntervalMs = options.progressIntervalMs ?? WAIT_READY_PROGRESS_INTERVAL_MS;
1571
+ let nextProgressAt = start + progressIntervalMs;
1572
+ let lastNodes = -1;
1573
+ let nodeStableSince = start;
1574
+ let bridgeError = "";
1575
+ let last = {
1576
+ flag: void 0,
1577
+ at: 0,
1578
+ nodes: 0,
1579
+ targets: 0,
1580
+ liveFrameActive: false,
1581
+ liveFrameChannels: 0,
1582
+ liveFramePublishes: 0,
1583
+ errors: 0,
1584
+ loadingText: "",
1585
+ externalReady: null,
1586
+ externalStatus: "",
1587
+ externalError: "",
1588
+ suppressedEntryError: ""
1711
1589
  };
1712
- }
1713
- async function tapResolvedTarget(bridge, args) {
1714
- const timing = tapTiming(!!args.agent, args.timing);
1715
- let attempts = 0;
1716
- let lastPayload = null;
1717
- let lastResult = null;
1718
- try {
1719
- await waitForSootsimIdle({
1720
- bridge,
1721
- maxMs: timing.initialWaitMs,
1722
- pollMs: 32,
1723
- stablePolls: 2
1724
- });
1725
- } catch {
1590
+ while (Date.now() < deadline) {
1591
+ try {
1592
+ last = await bridge.send({ type: "evaluate", code: WAIT_READY_PROBE }) ?? last;
1593
+ bridgeError = "";
1594
+ } catch (error) {
1595
+ bridgeError = error instanceof Error ? error.message : String(error);
1596
+ if (bridgeError.startsWith("multiple sims are connected:") || bridgeError.startsWith("saved sim ")) {
1597
+ throw error;
1598
+ }
1599
+ }
1600
+ const now = Date.now();
1601
+ const status2 = {
1602
+ ready: false,
1603
+ elapsedMs: now - start,
1604
+ nodes: last.nodes,
1605
+ targets: last.targets,
1606
+ liveFrameActive: last.liveFrameActive,
1607
+ liveFrameChannels: last.liveFrameChannels,
1608
+ liveFramePublishes: last.liveFramePublishes,
1609
+ flag: last.flag,
1610
+ loadingText: last.loadingText,
1611
+ externalReady: last.externalReady,
1612
+ externalStatus: last.externalStatus,
1613
+ externalError: last.externalError,
1614
+ suppressedEntryError: last.suppressedEntryError,
1615
+ errors: last.errors,
1616
+ bridgeError
1617
+ };
1618
+ if (last.nodes !== lastNodes) {
1619
+ lastNodes = last.nodes;
1620
+ nodeStableSince = now;
1621
+ }
1622
+ const hasReadySignal = last.flag === true && readyProbeHasContent(last);
1623
+ const hasRenderedContentWithoutReadySignal = last.flag !== true && readyProbeHasTargetContent(last);
1624
+ if ((hasReadySignal || hasRenderedContentWithoutReadySignal) && last.externalReady !== false && !last.externalError && !last.loadingText) {
1625
+ if (now - nodeStableSince >= READY_NODE_STABLE_MS) {
1626
+ return {
1627
+ ...status2,
1628
+ ready: true
1629
+ };
1630
+ }
1631
+ }
1632
+ if (last.externalError) return status2;
1633
+ if (options.onProgress && progressIntervalMs > 0 && now >= nextProgressAt) {
1634
+ options.onProgress(status2);
1635
+ do {
1636
+ nextProgressAt += progressIntervalMs;
1637
+ } while (now >= nextProgressAt);
1638
+ }
1639
+ await new Promise((r) => setTimeout(r, READY_PROBE_POLL_MS));
1640
+ }
1641
+ const status = {
1642
+ ready: false,
1643
+ elapsedMs: Date.now() - start,
1644
+ nodes: last.nodes,
1645
+ targets: last.targets,
1646
+ liveFrameActive: last.liveFrameActive,
1647
+ liveFrameChannels: last.liveFrameChannels,
1648
+ liveFramePublishes: last.liveFramePublishes,
1649
+ flag: last.flag,
1650
+ loadingText: last.loadingText,
1651
+ externalReady: last.externalReady,
1652
+ externalStatus: last.externalStatus,
1653
+ externalError: last.externalError,
1654
+ suppressedEntryError: last.suppressedEntryError,
1655
+ errors: last.errors,
1656
+ bridgeError
1657
+ };
1658
+ if (options.onProgress && progressIntervalMs > 0 && Date.now() >= nextProgressAt) {
1659
+ options.onProgress(status);
1726
1660
  }
1727
- const deadline = Date.now() + timing.deadlineMs;
1728
- let lastOffscreenAt = null;
1729
- while (Date.now() <= deadline || attempts === 0) {
1730
- attempts++;
1731
- const payload = await args.resolve();
1732
- lastPayload = payload;
1733
- if (payload?.error === "bridge-not-ready" || payload?.ambiguous || payload?.nthOutOfRange) {
1734
- return { payload, result: null, attempts, failure: "special" };
1735
- }
1736
- if (payload && typeof payload.cx === "number" && typeof payload.cy === "number") {
1737
- if (payload.offscreen) {
1738
- lastResult = {
1739
- hit: false,
1740
- reason: "offscreen",
1741
- x: payload.cx,
1742
- y: payload.cy,
1743
- screen: payload.screen
1744
- };
1745
- const at = `${Math.round(payload.cx)},${Math.round(payload.cy)}`;
1746
- if (lastOffscreenAt === at) {
1747
- return { payload, result: lastResult, attempts, failure: "missed" };
1748
- }
1749
- lastOffscreenAt = at;
1750
- } else {
1751
- lastOffscreenAt = null;
1752
- const requestedTarget = tapTargetFromPayload(payload, args.textFallback);
1753
- const verification = await bridge.send({
1754
- type: "evaluate",
1755
- code: `(async () => {
1756
- const t = window.__sootsimTest
1757
- if (
1758
- !t ||
1759
- typeof t.inspectAt !== 'function' ||
1760
- typeof t.resolveTapTarget !== 'function'
1761
- ) {
1762
- return { error: 'tap-target-inspection-unavailable' }
1763
- }
1764
- const requestedTarget = ${JSON.stringify(requestedTarget)}
1765
- const inspected = await t.inspectAt(${payload.cx}, ${payload.cy})
1766
- const resolved =
1767
- inspected && typeof inspected.nodeId === 'number'
1768
- ? await t.resolveTapTarget(inspected.nodeId)
1769
- : null
1770
- const node = (resolved && resolved.target) || inspected
1771
- const coordinateTarget = node
1772
- ? {
1773
- nodeId: node.nodeId ?? null,
1774
- id: node.id ?? null,
1775
- testID: node.testID ?? null,
1776
- text: node.text ?? node.accessibilityLabel ?? null,
1777
- type: node.type ?? null,
1778
- }
1779
- : null
1780
- const coordinateAncestors = Array.isArray(inspected?.ancestors)
1781
- ? inspected.ancestors
1782
- : []
1783
- const ancestorMatchesRequested = coordinateAncestors.some(
1784
- (ancestor) =>
1785
- (typeof requestedTarget.nodeId === 'number' &&
1786
- ancestor?.nodeId === requestedTarget.nodeId) ||
1787
- (requestedTarget.testID &&
1788
- ancestor?.testID === requestedTarget.testID) ||
1789
- (requestedTarget.id && ancestor?.id === requestedTarget.id),
1790
- )
1791
- const requestedTargetMatched =
1792
- coordinateTarget === null
1793
- ? false
1794
- : typeof requestedTarget.nodeId === 'number' &&
1795
- typeof coordinateTarget.nodeId === 'number'
1796
- ? requestedTarget.nodeId === coordinateTarget.nodeId ||
1797
- ancestorMatchesRequested
1798
- : requestedTarget.testID && coordinateTarget.testID
1799
- ? requestedTarget.testID === coordinateTarget.testID
1800
- || ancestorMatchesRequested
1801
- : requestedTarget.id && coordinateTarget.id
1802
- ? requestedTarget.id === coordinateTarget.id
1803
- || ancestorMatchesRequested
1804
- : ancestorMatchesRequested
1805
- if (!requestedTargetMatched) {
1806
- return {
1807
- requestedTargetMatched,
1808
- requestedTarget,
1809
- coordinateTarget,
1810
- }
1811
- }
1812
- if (typeof t.activatePressAt !== 'function') {
1813
- return { error: 'tap-target-activation-unavailable' }
1814
- }
1815
- const activation = await t.activatePressAt(
1816
- ${payload.cx},
1817
- ${payload.cy},
1818
- requestedTarget.nodeId,
1819
- )
1820
- if (activation?.ok) {
1821
- window.dispatchEvent(
1822
- new CustomEvent('sootsim:agentAction', {
1823
- detail: {
1824
- type: 'tap',
1825
- x: ${payload.cx},
1826
- y: ${payload.cy},
1827
- target: requestedTarget,
1828
- },
1829
- }),
1830
- )
1831
- }
1832
- return {
1833
- activation,
1834
- requestedTargetMatched,
1835
- requestedTarget,
1836
- coordinateTarget,
1837
- }
1838
- })()`
1839
- });
1840
- if (verification?.error) {
1841
- return {
1842
- payload,
1843
- result: {
1844
- hit: false,
1845
- reason: verification.error,
1846
- requestedTarget
1847
- },
1848
- attempts,
1849
- failure: "special"
1850
- };
1661
+ return status;
1662
+ }
1663
+ async function inspectWaitSelector(bridge, testId, timeoutMs = 5e3, opts = {}) {
1664
+ const gone = opts.gone === true;
1665
+ const result = await bridge.send(
1666
+ {
1667
+ type: "evaluate",
1668
+ code: `(async () => {
1669
+ const start = Date.now()
1670
+ const deadline = start + ${timeoutMs}
1671
+ const gone = ${gone}
1672
+ const ID = ${JSON.stringify(testId)}
1673
+ const present = (node) => !!(node && node.layout && node.layout.width > 0 && node.layout.height > 0)
1674
+ const find = async () => {
1675
+ const t = window.__sootsimTest
1676
+ if (!t) return undefined // bridge not ready \u2014 indeterminate, not "gone"
1677
+ try {
1678
+ // gone-mode confirms a gate/screen LEFT \u2014 it only needs structural
1679
+ // existence + layout, NOT the full-tree occlusion pass (pruneHidden).
1680
+ // that pass (computeOccludedInspectNodeIds) walks the whole tree and
1681
+ // does O(n\xB2)-ish rect-occlusion testing EVERY poll; on a large
1682
+ // post-login app tree it costs seconds per poll, so --gone can never
1683
+ // get two clean "absent" polls inside budget and falsely reports the
1684
+ // gate as still blocked \u2014 the root cause of the 3pc PR-preview outage.
1685
+ // presence-mode keeps pruneHidden so "appeared" still means
1686
+ // visibly-reachable. a departed gate is unmounted or zero-sized, so the
1687
+ // cheap structural+layout check is correct here and ~O(n) cheap.
1688
+ const matches = await t.queryAll?.(gone ? { hasId: ID } : { hasId: ID, pruneHidden: true })
1689
+ if (!Array.isArray(matches)) return undefined
1690
+ return matches.find(present) || null
1691
+ } catch {
1692
+ return undefined
1851
1693
  }
1852
- if (verification?.requestedTargetMatched === false) {
1853
- return {
1854
- payload,
1855
- result: {
1856
- hit: false,
1857
- reason: "target-covered",
1858
- ...verification
1859
- },
1860
- attempts,
1861
- failure: "missed"
1862
- };
1694
+ }
1695
+ let goneStreak = 0
1696
+ while (Date.now() < deadline) {
1697
+ const node = await find()
1698
+ if (gone) {
1699
+ if (node === null) {
1700
+ goneStreak += 1
1701
+ if (goneStreak >= 2) return { found: true, elapsed: Date.now() - start }
1702
+ } else {
1703
+ goneStreak = 0
1704
+ }
1705
+ } else if (present(node)) {
1706
+ return { found: true, node, elapsed: Date.now() - start }
1863
1707
  }
1864
- const dispatched = verification?.activation?.tap ?? verification?.activation;
1865
- const result = dispatched && typeof dispatched === "object" ? { ...dispatched, ...verification?.activation, ...verification } : { hit: !!dispatched, ...verification };
1866
- lastResult = result;
1867
- if (isTapSuccess(result)) return { payload, result, attempts };
1708
+ await new Promise((r) => setTimeout(r, 80))
1868
1709
  }
1710
+ return { found: false, elapsed: Date.now() - start }
1711
+ })()`
1712
+ },
1713
+ {
1714
+ timeoutMs: timeoutMs + 1e3
1869
1715
  }
1870
- const remaining = deadline - Date.now();
1871
- if (remaining <= 0) break;
1716
+ );
1717
+ return result ?? { found: false, elapsed: timeoutMs };
1718
+ }
1719
+ function consoleEntriesEval(level, limit) {
1720
+ const getter = level === "error" ? "getErrors" : "getWarnings";
1721
+ return `(() => {
1722
+ const out = []
1723
+ const seen = new Set()
1724
+ const norm = (s) => (typeof s === 'string' ? s : (() => { try { return JSON.stringify(s) } catch { return String(s) } })())
1725
+ const key = (ts, args) => Math.round((ts || 0) / 250) + '|' + (Array.isArray(args) ? args.map(norm).join(' ') : norm(args))
1726
+ const push = (e) => {
1727
+ if (!e) return
1728
+ const ts = typeof e.timestamp === 'number' ? e.timestamp : (typeof e.ts === 'number' ? e.ts : 0)
1729
+ const k = key(ts, e.args)
1730
+ if (seen.has(k)) return
1731
+ seen.add(k)
1732
+ out.push({ timestamp: ts, args: Array.isArray(e.args) ? e.args : [e.args], stack: e.stack || undefined, source: e.source || undefined })
1733
+ }
1734
+ // page-realm ws-bridge buffer first (richest stacks for page-origin errors)
1735
+ try { for (const e of (window.__sootsimConsole?.${getter}(${limit}) || [])) push(e) } catch {}
1736
+ // engine observability store \u2014 the always-on render-worker / forwarded path
1872
1737
  try {
1873
- await waitForSootsimIdle({
1874
- bridge,
1875
- maxMs: Math.min(remaining, timing.retryWaitMs),
1876
- pollMs: 32,
1877
- stablePolls: 2
1878
- });
1879
- } catch {
1880
- await new Promise((resolve) => setTimeout(resolve, Math.min(120, remaining)));
1738
+ const obs = window.__sootsimObservability
1739
+ const snap = obs && obs.logs && typeof obs.logs.getSnapshot === 'function' ? obs.logs.getSnapshot() : []
1740
+ for (const e of snap) if (e && e.level === ${JSON.stringify(level)}) push(e)
1741
+ } catch {}
1742
+ return out.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)).slice(-${limit})
1743
+ })()`;
1744
+ }
1745
+ async function inspectErrors(bridge, limit = 20) {
1746
+ const result = await bridge.send({
1747
+ type: "evaluate",
1748
+ code: consoleEntriesEval("error", limit)
1749
+ });
1750
+ return Array.isArray(result) ? result : [];
1751
+ }
1752
+ async function inspectWarnings(bridge, limit = 20) {
1753
+ const result = await bridge.send({
1754
+ type: "evaluate",
1755
+ code: consoleEntriesEval("warn", limit)
1756
+ });
1757
+ return Array.isArray(result) ? result : [];
1758
+ }
1759
+ var MERGED_CONSOLE_COUNT_EVAL = `(() => {
1760
+ const norm = (s) => (typeof s === 'string' ? s : (() => { try { return JSON.stringify(s) } catch { return String(s) } })())
1761
+ const key = (lvl, ts, args) => lvl + '|' + Math.round((ts || 0) / 250) + '|' + (Array.isArray(args) ? args.map(norm).join(' ') : norm(args))
1762
+ const seen = new Set()
1763
+ let errors = 0
1764
+ let warnings = 0
1765
+ const add = (lvl, ts, args) => {
1766
+ if (lvl !== 'error' && lvl !== 'warn') return
1767
+ const k = key(lvl, ts, args)
1768
+ if (seen.has(k)) return
1769
+ seen.add(k)
1770
+ if (lvl === 'error') errors++
1771
+ else warnings++
1881
1772
  }
1882
- }
1773
+ try {
1774
+ const c = window.__sootsimConsole
1775
+ for (const e of (c?.getErrors?.(200) || [])) add('error', e?.timestamp ?? e?.ts, e?.args)
1776
+ for (const e of (c?.getWarnings?.(200) || [])) add('warn', e?.timestamp ?? e?.ts, e?.args)
1777
+ } catch {}
1778
+ try {
1779
+ const obs = window.__sootsimObservability
1780
+ const snap = obs && obs.logs && typeof obs.logs.getSnapshot === 'function' ? obs.logs.getSnapshot() : []
1781
+ for (const e of snap) if (e) add(e.level, e.ts, e.args)
1782
+ } catch {}
1783
+ return { errors, warnings, total: errors + warnings }
1784
+ })()`;
1785
+ async function clearConsole(bridge) {
1786
+ await bridge.send({
1787
+ type: "evaluate",
1788
+ code: 'window.__sootsimConsole?.clear(); window.__sootsimObservability?.logs?.clear?.(); "cleared"'
1789
+ });
1790
+ }
1791
+ async function inspectRequests(bridge, opts = {}) {
1792
+ const limit = opts.limit ?? 20;
1793
+ const method = opts.failed === false ? "getRequests" : "getFailedRequests";
1794
+ const result = await bridge.send({
1795
+ type: "call",
1796
+ path: `__sootsimTest.${method}`,
1797
+ args: [limit]
1798
+ });
1799
+ return Array.isArray(result) ? result : [];
1800
+ }
1801
+ async function clearRequests(bridge) {
1802
+ await bridge.send({ type: "call", path: "__sootsimTest.clearRequests", args: [] });
1803
+ }
1804
+ async function inspectScrollStateAt(bridge, x, y) {
1805
+ const result = await bridge.send({
1806
+ type: "call",
1807
+ path: "__sootsimTest.getScrollStateAt",
1808
+ args: [x, y]
1809
+ });
1810
+ if (!result || typeof result !== "object") return null;
1811
+ const state = result;
1883
1812
  return {
1884
- payload: lastPayload,
1885
- result: lastResult,
1886
- attempts,
1887
- failure: lastPayload && typeof lastPayload.cx === "number" ? "missed" : "not-found"
1813
+ ...state,
1814
+ scrollOffsetX: typeof state.scrollOffsetX === "number" ? state.scrollOffsetX : state.offsetX,
1815
+ scrollOffsetY: typeof state.scrollOffsetY === "number" ? state.scrollOffsetY : state.offsetY
1888
1816
  };
1889
1817
  }
1890
- async function tapCoordinates(bridge, x, y, target) {
1891
- return bridge.send({
1892
- type: "tap",
1893
- x,
1894
- y,
1895
- ...target ? { target } : {}
1818
+ async function inspectLogs(bridge) {
1819
+ const res = await bridge.send({
1820
+ type: "evaluate",
1821
+ code: `(() => {
1822
+ const obs = window.__sootsimObservability;
1823
+ if (!obs) return { ok: false };
1824
+ return { ok: true, entries: obs.logs.getSnapshot() };
1825
+ })()`
1896
1826
  });
1827
+ if (!res || !res.ok) return [];
1828
+ return res.entries ?? [];
1897
1829
  }
1898
- async function tapById(bridge, query, opts = {}) {
1899
- const arg = JSON.stringify(query);
1900
- return tapResolvedTarget(bridge, {
1901
- agent: opts.agent,
1902
- timing: opts.timing,
1903
- resolve: () => bridge.send({
1904
- type: "evaluate",
1905
- code: `(async () => {
1906
- const t = window.__sootsimTest
1907
- if (!t) return null
1908
- const n = (await t.findByTestId(${arg})) || (await t.findById(${arg}))
1909
- if (!n || !n.absolutePosition || !n.layout) return { cx: null }
1910
- const resolved =
1911
- typeof n.nodeId === 'number' && typeof t.resolveTapTarget === 'function'
1912
- ? await t.resolveTapTarget(n.nodeId)
1913
- : null
1914
- const target = (resolved && resolved.target) || n
1915
- const cx =
1916
- resolved && typeof resolved.cx === 'number'
1917
- ? resolved.cx
1918
- : n.absolutePosition.x + (n.layout.width || 0) / 2
1919
- const cy =
1920
- resolved && typeof resolved.cy === 'number'
1921
- ? resolved.cy
1922
- : n.absolutePosition.y + (n.layout.height || 0) / 2
1923
- const scr = ${SCREEN_EVAL}
1924
- const offscreen =
1925
- !!scr && (cx < 0 || cy < 0 || cx > scr.width || cy > scr.height)
1926
- return {
1927
- cx,
1928
- cy,
1929
- ...(offscreen ? { offscreen: true, screen: scr } : {}),
1930
- match: {
1931
- nodeId: n.nodeId ?? null,
1932
- id: n.id,
1933
- testID: n.testID,
1934
- text: n.text ?? n.accessibilityLabel ?? null,
1935
- type: n.type,
1936
- },
1937
- target: {
1938
- nodeId: target.nodeId ?? null,
1939
- id: target.id,
1940
- testID: target.testID,
1941
- text:
1942
- target.text ??
1943
- target.accessibilityLabel ??
1944
- n.text ??
1945
- n.accessibilityLabel ??
1946
- null,
1947
- type: target.type,
1948
- },
1949
- strategy: (resolved && resolved.strategy) || 'matched-node',
1950
- }
1951
- })()`
1952
- })
1830
+ async function clearLogs(bridge) {
1831
+ await bridge.send({
1832
+ type: "evaluate",
1833
+ code: 'window.__sootsimObservability?.logs.clear(); "cleared"'
1953
1834
  });
1954
1835
  }
1955
- async function tapByText(bridge, query, options = {}, opts = {}) {
1956
- const filterArg = JSON.stringify({
1957
- query,
1958
- exact: !!options.exact,
1959
- role: options.role ?? null,
1960
- within: options.within ?? null,
1961
- minX: options.minX ?? null,
1962
- maxX: options.maxX ?? null,
1963
- minY: options.minY ?? null,
1964
- maxY: options.maxY ?? null,
1965
- near: options.near ?? null,
1966
- nth: options.nth ?? null,
1967
- first: !!options.first
1836
+ function isForwardedWorkerLogTwin(a, b) {
1837
+ if (a.source === b.source) return false;
1838
+ if (a.level !== b.level) return false;
1839
+ if (Math.abs(a.ts - b.ts) > 1e3) return false;
1840
+ if (a.args.length !== b.args.length) return false;
1841
+ const sources = /* @__PURE__ */ new Set([a.source, b.source]);
1842
+ if (!sources.has("sootsim-worker")) return false;
1843
+ if (!sources.has("render-worker") && !sources.has("forwarded-render-worker")) {
1844
+ return false;
1845
+ }
1846
+ return a.args.every((arg, index) => arg === b.args[index]);
1847
+ }
1848
+ function filterLogEntries(entries, opts = {}) {
1849
+ let out = [];
1850
+ for (const entry of entries) {
1851
+ if (out.some((candidate) => isForwardedWorkerLogTwin(candidate, entry))) {
1852
+ continue;
1853
+ }
1854
+ out.push(entry);
1855
+ }
1856
+ if (!opts.showInternal) {
1857
+ out = out.filter((e) => {
1858
+ const first = e.args[0];
1859
+ return !(typeof first === "string" && first.startsWith("[rnx]"));
1860
+ });
1861
+ }
1862
+ if (opts.level) out = out.filter((e) => opts.level.has(e.level));
1863
+ if (opts.filter) {
1864
+ const lf = opts.filter.toLowerCase();
1865
+ out = out.filter((e) => e.args.join(" ").toLowerCase().includes(lf));
1866
+ }
1867
+ return out;
1868
+ }
1869
+ async function inspectTimelineSummary(bridge, query) {
1870
+ return await bridge.send({
1871
+ type: "call",
1872
+ path: "SootSim.bridges.timeline.summary",
1873
+ args: [query]
1968
1874
  });
1969
- return tapResolvedTarget(bridge, {
1970
- agent: opts.agent,
1971
- timing: opts.timing,
1972
- textFallback: query,
1973
- resolve: () => bridge.send({
1974
- type: "evaluate",
1975
- code: `(async () => {
1976
- const t = window.__sootsimTest
1977
- if (!t) return { error: 'bridge-not-ready' }
1978
- const F = ${filterArg}
1979
-
1980
- const res = await t.queryTextCandidates({
1981
- query: F.query,
1982
- exact: !!F.exact,
1983
- ...(F.role ? { role: F.role } : {}),
1984
- })
1985
-
1986
- let candidates = res.candidates || []
1987
-
1988
- candidates = candidates.filter((c) => {
1989
- const ax = c.info.absolutePosition && c.info.absolutePosition.x
1990
- const ay = c.info.absolutePosition && c.info.absolutePosition.y
1991
- if (F.minX !== null && !(ax >= F.minX)) return false
1992
- if (F.maxX !== null && !(ax <= F.maxX)) return false
1993
- if (F.minY !== null && !(ay >= F.minY)) return false
1994
- if (F.maxY !== null && !(ay <= F.maxY)) return false
1995
- if (F.within && !c.ancestorTestIDs.includes(F.within)) return false
1996
- return true
1997
- })
1998
-
1999
- if (F.near) {
2000
- candidates.sort((a, b) => {
2001
- const ax = (a.info.absolutePosition && a.info.absolutePosition.x) || 0
2002
- const ay = (a.info.absolutePosition && a.info.absolutePosition.y) || 0
2003
- const bx = (b.info.absolutePosition && b.info.absolutePosition.x) || 0
2004
- const by = (b.info.absolutePosition && b.info.absolutePosition.y) || 0
2005
- return (
2006
- Math.hypot(ax - F.near.x, ay - F.near.y) -
2007
- Math.hypot(bx - F.near.x, by - F.near.y)
2008
- )
2009
- })
2010
- } else {
2011
- candidates.sort((a, b) => {
2012
- const ay = (a.info.absolutePosition && a.info.absolutePosition.y) || 0
2013
- const by = (b.info.absolutePosition && b.info.absolutePosition.y) || 0
2014
- if (Math.abs(ay - by) > 2) return ay - by
2015
- const ax = (a.info.absolutePosition && a.info.absolutePosition.x) || 0
2016
- const bx = (b.info.absolutePosition && b.info.absolutePosition.x) || 0
2017
- return ax - bx
2018
- })
2019
- }
2020
-
2021
- const total = candidates.length
2022
- if (total === 0) return { matched: 0, total: 0 }
2023
-
2024
- let idx = 0
2025
- if (F.nth !== null) {
2026
- idx = F.nth < 0 ? total + F.nth : F.nth
2027
- if (idx < 0 || idx >= total) {
2028
- return { matched: 0, total, nthOutOfRange: true, nth: F.nth }
2029
- }
2030
- } else if (total > 1 && !F.first && !F.near) {
2031
- return {
2032
- ambiguous: true,
2033
- total,
2034
- candidates: candidates.slice(0, 10).map((c, i) => ({
2035
- idx: i,
2036
- nodeId: c.info.nodeId,
2037
- type: c.info.type,
2038
- testID: c.info.testID,
2039
- text: (c.info.text || '').slice(0, 60),
2040
- abs: c.info.absolutePosition,
2041
- layout: c.info.layout
1875
+ }
1876
+ async function inspectTimelineRecent(bridge, query) {
1877
+ return await bridge.send({
1878
+ type: "call",
1879
+ path: "SootSim.bridges.timeline.recent",
1880
+ args: [query]
1881
+ });
1882
+ }
1883
+ async function detectOpenNativeUI(bridge) {
1884
+ let events;
1885
+ try {
1886
+ const res = await inspectTimelineRecent(bridge, {
1887
+ kinds: ["alert", "actionsheet"],
1888
+ limit: 80
1889
+ });
1890
+ events = res.events;
1891
+ } catch {
1892
+ return [];
1893
+ }
1894
+ const latest = /* @__PURE__ */ new Map();
1895
+ for (const ev of events) latest.set(ev.kind, ev);
1896
+ const out = [];
1897
+ const check = (kind, label) => {
1898
+ const ev = latest.get(kind);
1899
+ if (!ev || !ev.data || typeof ev.data !== "object") return;
1900
+ const d = ev.data;
1901
+ if (d.phase !== "show") return;
1902
+ out.push({ label, title: typeof d.title === "string" ? d.title : null });
1903
+ };
1904
+ check("alert", "Alert");
1905
+ check("actionsheet", "ActionSheet");
1906
+ return out;
1907
+ }
1908
+ async function inspectTimelineAdvanceCursor(bridge, cursorKey, watermark) {
1909
+ await bridge.send({
1910
+ type: "call",
1911
+ path: "SootSim.bridges.timeline.cursorAdvance",
1912
+ args: [cursorKey, watermark]
1913
+ });
1914
+ }
1915
+ async function inspectKeyboard(bridge) {
1916
+ const result = await bridge.send({
1917
+ type: "evaluate",
1918
+ code: `(() => {
1919
+ const kb = window.__sootsimKeyboard
1920
+ if (!kb || typeof kb.getLayout !== 'function') {
1921
+ return { error: 'keyboard bridge getLayout() not available' }
1922
+ }
1923
+ return kb.getLayout()
1924
+ })()`
1925
+ });
1926
+ return result ?? { error: "keyboard bridge returned no result" };
1927
+ }
1928
+ function isShellCommandUnavailable(error) {
1929
+ const message = error instanceof Error ? error.message : String(error);
1930
+ return message.includes("call target not found: SootSim.bridges.mainShell") || message.includes("test bridge unavailable before app-in-worker boot");
1931
+ }
1932
+ async function getShellState(bridge, readyTimeoutMs = 0) {
1933
+ const deadline = Date.now() + Math.max(0, readyTimeoutMs);
1934
+ while (true) {
1935
+ try {
1936
+ return await bridge.send({
1937
+ type: "call",
1938
+ path: "SootSim.bridges.mainShell.getState",
1939
+ args: []
1940
+ });
1941
+ } catch (error) {
1942
+ if (!isShellCommandUnavailable(error) || Date.now() >= deadline) throw error;
1943
+ await new Promise((r) => setTimeout(r, 50));
1944
+ }
1945
+ }
1946
+ }
1947
+ async function inspectScreens(bridge) {
1948
+ const payload = await bridge.send({
1949
+ type: "evaluate",
1950
+ code: `(async () => {
1951
+ const test = window.__sootsimTest
1952
+ const kb = window.__sootsimKeyboard
1953
+ const navSnap =
1954
+ test && typeof test.getNavigationSnapshot === 'function'
1955
+ ? await test.getNavigationSnapshot()
1956
+ : null
1957
+ const route =
1958
+ test && typeof test.getRouteInfo === 'function'
1959
+ ? await test.getRouteInfo()
1960
+ : null
1961
+ const keyboard =
1962
+ kb && typeof kb.getLayout === 'function'
1963
+ ? (() => {
1964
+ const layout = kb.getLayout()
1965
+ return layout ? {
1966
+ visible: layout.visible,
1967
+ mode: layout.mode,
1968
+ spec: layout.spec
2042
1969
  ? {
2043
- width: Math.round(c.info.layout.width || 0),
2044
- height: Math.round(c.info.layout.height || 0),
1970
+ keyboardType: layout.spec.keyboardType,
1971
+ returnKeyType: layout.spec.returnKeyType,
2045
1972
  }
2046
1973
  : null,
2047
- ancestorTestIDs: (c.ancestorTestIDs || []).slice(0, 5),
2048
- })),
1974
+ } : null
1975
+ })()
1976
+ : null
1977
+ return { nav: navSnap, route, keyboard }
1978
+ })()`
1979
+ });
1980
+ const shell = await getShellState(bridge, 500).catch(() => null);
1981
+ return {
1982
+ shell,
1983
+ nav: payload?.nav ?? null,
1984
+ route: payload?.route ?? null,
1985
+ keyboard: payload?.keyboard ?? null
1986
+ };
1987
+ }
1988
+ var DEBUG_CHANNELS = [
1989
+ "portals",
1990
+ "sheets",
1991
+ "layout",
1992
+ "onlayout",
1993
+ "animated",
1994
+ "render",
1995
+ "touch",
1996
+ "yoga"
1997
+ ];
1998
+ async function inspectDebugStatus(bridge) {
1999
+ return bridge.send({ type: "evaluate", code: "window.__sootsimDebug.status()" });
2000
+ }
2001
+ async function inspectDebugFlags(bridge) {
2002
+ return bridge.send({ type: "evaluate", code: "window.__sootsimDebug.flags()" });
2003
+ }
2004
+ async function inspectDebugFind(bridge, target) {
2005
+ return bridge.send({
2006
+ type: "evaluate",
2007
+ code: `window.__sootsimTest.debugFind(${JSON.stringify(target)})`
2008
+ });
2009
+ }
2010
+ async function inspectDebugRecent(bridge, channel, limit = 50) {
2011
+ const code = channel && channel !== "all" ? `window.__sootsimDebug.recent(${JSON.stringify(channel)}, ${limit})` : `window.__sootsimDebug.recent(undefined, ${limit})`;
2012
+ return bridge.send({ type: "evaluate", code });
2013
+ }
2014
+ async function setDebugChannels(bridge, action, channels) {
2015
+ const args = channels.length > 0 ? channels.map((c) => JSON.stringify(c)).join(", ") : action === "disable" ? "'all'" : "";
2016
+ return bridge.send({
2017
+ type: "evaluate",
2018
+ code: `window.__sootsimDebug.${action}(${args})`
2019
+ });
2020
+ }
2021
+ async function inspectMemory(bridge) {
2022
+ const result = await bridge.send({
2023
+ type: "evaluate",
2024
+ code: `(async () => {
2025
+ const host = window.__sootsimRenderHost
2026
+ const stats = host?.queryStats ? await host.queryStats() : null
2027
+ const shellRaw = window.__sootsimShellMemory
2028
+ ? await window.__sootsimShellMemory()
2029
+ : null
2030
+ const shell = shellRaw && !shellRaw.error ? shellRaw : null
2031
+ // the compositor paints home + app:one + app:two, so every app-surface
2032
+ // picture and raster image is counted here and nowhere else.
2033
+ let compositor = null
2034
+ try {
2035
+ const cs = await window.__sootsimCompositor?.getStats?.(false, true)
2036
+ compositor = cs?.memory ?? null
2037
+ } catch {}
2038
+ const normalize = (m) =>
2039
+ m
2040
+ ? {
2041
+ nodesRegistered: m.nodesRegistered ?? null,
2042
+ nodesDetached: m.nodesDetached ?? null,
2043
+ detachedTypes: m.detachedTypes ?? null,
2044
+ objects: m.objects ?? null,
2045
+ activeNativeAnimations: m.activeNativeAnimations ?? null,
2046
+ imageLoader: m.imageLoader ?? null,
2047
+ workerHeap: m.workerHeap ?? null,
2048
+ wasmHeapBytes: m.wasmHeapBytes ?? null,
2049
2049
  }
2050
+ : null
2051
+ const hostMem = performance.memory
2052
+ ? {
2053
+ usedJSHeapSize: performance.memory.usedJSHeapSize,
2054
+ totalJSHeapSize: performance.memory.totalJSHeapSize,
2055
+ jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
2050
2056
  }
2057
+ : null
2058
+ return {
2059
+ tenant: normalize(stats?.memory),
2060
+ shell: normalize(shell),
2061
+ compositor: normalize(compositor),
2062
+ hostHeap: hostMem,
2063
+ }
2064
+ })()`
2065
+ });
2066
+ return result ?? { tenant: null, shell: null, compositor: null, hostHeap: null };
2067
+ }
2068
+ function formatTimelineSummary(summary) {
2069
+ if (summary.total === 0) return "nothing recorded";
2070
+ const parts = [];
2071
+ const ORDER = [
2072
+ "error",
2073
+ "warning",
2074
+ "console",
2075
+ "fetch",
2076
+ "toast",
2077
+ "alert",
2078
+ "actionsheet",
2079
+ "picker",
2080
+ "notification",
2081
+ "screen",
2082
+ "route",
2083
+ "keyboard",
2084
+ "app-launch",
2085
+ "shell",
2086
+ "scroll",
2087
+ "gesture",
2088
+ "text-input",
2089
+ "react-commit",
2090
+ "animation",
2091
+ "reanimated"
2092
+ ];
2093
+ const seen = /* @__PURE__ */ new Set();
2094
+ for (const k of ORDER) {
2095
+ const n = summary.byKind[k];
2096
+ if (n) {
2097
+ parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
2098
+ seen.add(k);
2099
+ }
2100
+ }
2101
+ for (const [k, n] of Object.entries(summary.byKind)) {
2102
+ if (!seen.has(k) && n) parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
2103
+ }
2104
+ return parts.join(" \xB7 ");
2105
+ }
2106
+ var NOISY_TIMELINE_KINDS = /* @__PURE__ */ new Set([
2107
+ "react-commit",
2108
+ "layout",
2109
+ "scroll"
2110
+ ]);
2111
+ function formatTimelineRelativeTime(t, anchor) {
2112
+ if (anchor === null) return new Date(t).toLocaleTimeString();
2113
+ const dt = (t - anchor) / 1e3;
2114
+ const sign = dt >= 0 ? "+" : "";
2115
+ return `${sign}${dt.toFixed(2)}s`;
2116
+ }
2117
+ function formatTimelinePayload(kind, d) {
2118
+ switch (kind) {
2119
+ case "app-launch":
2120
+ return d.phase === "launch" ? `launch ${d.appName ?? d.toAppId ?? ""}` : `dismiss ${d.appName ?? d.fromAppId ?? ""} \u2192 ${d.toAppId ?? ""}`;
2121
+ case "toast":
2122
+ return `"${d.text ?? ""}"${d.durationMs ? ` (${d.durationMs}ms)` : ""}`;
2123
+ case "keyboard":
2124
+ return `${d.phase ?? "?"}${d.heightPx ? ` h=${d.heightPx}` : ""}${d.mode ? ` ${d.mode}` : ""}`;
2125
+ case "screen":
2126
+ return `${d.phase ?? "?"} ${d.name ?? d.activeName ?? ""}`;
2127
+ case "flow-step":
2128
+ return `${d.status ?? "?"} ${d.stepName ?? "step"}${d.durationMs ? ` ${d.durationMs}ms` : ""}`;
2129
+ case "route":
2130
+ return `${d.phase ?? "?"} ${d.path ?? d.pathname ?? ""}`;
2131
+ case "alert":
2132
+ case "actionsheet":
2133
+ case "picker":
2134
+ return `${d.phase ?? "?"} ${d.title ?? d.message ?? ""}`;
2135
+ case "notification":
2136
+ return `${d.title ?? ""}${d.body ? ` \u2014 ${d.body}` : ""}`;
2137
+ case "fetch":
2138
+ return `${d.method ?? "GET"} ${d.url ?? ""}${d.status ? ` -> ${d.status}` : ""}`;
2139
+ case "console":
2140
+ case "console-log":
2141
+ return `${d.level ?? "log"}: ${(d.message ?? "").toString().slice(0, 120)}`;
2142
+ case "shell":
2143
+ return `${d.event ?? d.type ?? d.phase ?? ""}`;
2144
+ case "scroll":
2145
+ return `${d.phase ?? "?"} ${d.target ?? ""}`;
2146
+ case "gesture":
2147
+ return `${d.phase ?? "?"} ${d.type ?? ""}`;
2148
+ case "text-input":
2149
+ return `${d.phase ?? "?"}${d.value !== void 0 ? ` "${String(d.value).slice(0, 40)}"` : ""}`;
2150
+ case "layout":
2151
+ return `${d.kind ?? "?"} ${d.testID ?? d.type ?? ""}${d.skipped ? ` skipped:${d.reason ?? "unknown"}` : ""}`;
2152
+ case "react-commit": {
2153
+ const slowest = d.slowest;
2154
+ return `${d.fiberCount ?? "?"} fibers ${d.durationMs ?? "?"}ms${slowest?.displayName ? ` \xB7 ${slowest.displayName} ${slowest.durationMs ?? "?"}ms` : ""}`;
2155
+ }
2156
+ case "frame":
2157
+ return `${d.totalMs ?? "?"}ms${d.renderMs ? ` \xB7 render ${d.renderMs}ms` : ""}${d.layoutMs ? ` \xB7 layout ${d.layoutMs}ms` : ""}${d.copyMs ? ` \xB7 copy ${d.copyMs}ms` : ""}`;
2158
+ case "reanimated":
2159
+ case "animation":
2160
+ return `${d.kind ?? ""} ${d.target ?? ""}${d.durationMs ? ` ${d.durationMs}ms` : ""}`;
2161
+ }
2162
+ return "";
2163
+ }
2164
+ function formatTimelineEvent(event, anchor) {
2165
+ const ts = formatTimelineRelativeTime(event.t, anchor).padStart(8);
2166
+ const ctx = event.context.padEnd(6);
2167
+ const kind = `[${event.kind}]`.padEnd(15);
2168
+ const d = event.data;
2169
+ const payload = d && typeof d === "object" ? formatTimelinePayload(event.kind, d) : "";
2170
+ return ` ${ts} ${ctx} ${kind} ${payload}`;
2171
+ }
2172
+ function formatTimelineEvents(events) {
2173
+ if (!events.length) return "";
2174
+ const anchor = events[0]?.t ?? null;
2175
+ return events.map((e) => formatTimelineEvent(e, anchor)).join("\n");
2176
+ }
2051
2177
 
2052
- const picked = candidates[idx]
2053
- const n = picked.info
2054
- if (!n.absolutePosition || !n.layout) return { matched: 0, total }
2055
-
2056
- const resolved =
2057
- typeof n.nodeId === 'number' &&
2058
- typeof t.resolveTapTarget === 'function'
2059
- ? await t.resolveTapTarget(n.nodeId)
2060
- : null
2061
- const target = (resolved && resolved.target) || n
2062
- const cx =
2063
- resolved && typeof resolved.cx === 'number'
2064
- ? resolved.cx
2065
- : n.absolutePosition.x + (n.layout.width || 0) / 2
2066
- const cy =
2067
- resolved && typeof resolved.cy === 'number'
2068
- ? resolved.cy
2069
- : n.absolutePosition.y + (n.layout.height || 0) / 2
2070
- const scr = ${SCREEN_EVAL}
2071
- const offscreen =
2072
- !!scr && (cx < 0 || cy < 0 || cx > scr.width || cy > scr.height)
2073
- return {
2074
- cx,
2075
- cy,
2076
- ...(offscreen ? { offscreen: true, screen: scr } : {}),
2077
- match: {
2078
- nodeId: n.nodeId ?? null,
2079
- id: n.id,
2080
- testID: n.testID,
2081
- type: n.type,
2082
- },
2083
- target: {
2084
- nodeId: target.nodeId ?? null,
2085
- id: target.id,
2086
- testID: target.testID,
2087
- text:
2088
- target.text ??
2089
- target.accessibilityLabel ??
2090
- n.text ??
2091
- n.accessibilityLabel ??
2092
- null,
2093
- type: target.type,
2094
- },
2095
- strategy: (resolved && resolved.strategy) || 'matched-node',
2096
- total,
2097
- idx,
2098
- }
2099
- })()`
2100
- })
2178
+ // cli/commands/inspect/resolve-target.ts
2179
+ async function resolveTargetCoords(bridge, target) {
2180
+ const arg = JSON.stringify(target.value);
2181
+ const findExpr = target.mode === "testid" ? `(await t.findByTestId(${arg})) || (await t.findById(${arg}))` : `await t.findByText(${arg})`;
2182
+ const result = await bridge.send({
2183
+ type: "evaluate",
2184
+ code: `(async () => {
2185
+ const t = window.__sootsimTest
2186
+ if (!t) return null
2187
+ const n = ${findExpr}
2188
+ if (!n || !n.absolutePosition || !n.layout) return null
2189
+ const resolved =
2190
+ typeof n.nodeId === 'number' && typeof t.resolveTapTarget === 'function'
2191
+ ? await t.resolveTapTarget(n.nodeId)
2192
+ : null
2193
+ const cx =
2194
+ typeof resolved?.cx === 'number'
2195
+ ? resolved.cx
2196
+ : n.absolutePosition.x + (n.layout.width || 0) / 2
2197
+ const cy =
2198
+ typeof resolved?.cy === 'number'
2199
+ ? resolved.cy
2200
+ : n.absolutePosition.y + (n.layout.height || 0) / 2
2201
+ return {
2202
+ x: cx,
2203
+ y: cy,
2204
+ id: n.id ?? null,
2205
+ testID: n.testID ?? null,
2206
+ text: ${JSON.stringify(target.mode === "text")} ? ${arg} : (n.text ?? n.accessibilityLabel ?? null),
2207
+ type: n.type ?? null,
2208
+ }
2209
+ })()`
2101
2210
  });
2211
+ return result ?? null;
2102
2212
  }
2103
- async function tapBest(bridge, query, opts = {}) {
2104
- const arg = JSON.stringify(query);
2105
- return tapResolvedTarget(bridge, {
2106
- agent: opts.agent,
2107
- timing: opts.timing,
2108
- textFallback: query,
2109
- resolve: async () => {
2110
- const payload = await bridge.send({
2111
- type: "evaluate",
2112
- code: `(async () => {
2113
- const t = window.__sootsimTest
2114
- if (!t) return { error: 'bridge-not-ready' }
2115
- const byTestId =
2116
- (await t.findByTestId(${arg})) || (await t.findById(${arg}))
2117
- if (byTestId && byTestId.absolutePosition && byTestId.layout) {
2118
- return {
2119
- strategy: 'testid',
2120
- node: {
2121
- nodeId: byTestId.nodeId ?? null,
2122
- id: byTestId.id,
2123
- testID: byTestId.testID,
2124
- type: byTestId.type,
2125
- text: byTestId.text,
2126
- absolutePosition: byTestId.absolutePosition,
2127
- layout: byTestId.layout,
2128
- },
2129
- screen: ${SCREEN_EVAL},
2130
- }
2131
- }
2132
- const byText = await t.findByText(${arg})
2133
- if (byText && byText.absolutePosition && byText.layout) {
2134
- return {
2135
- strategy: 'text',
2136
- node: {
2137
- nodeId: byText.nodeId ?? null,
2138
- id: byText.id,
2139
- testID: byText.testID,
2140
- type: byText.type,
2141
- text: byText.text,
2142
- absolutePosition: byText.absolutePosition,
2143
- layout: byText.layout,
2144
- },
2145
- screen: ${SCREEN_EVAL},
2146
- }
2147
- }
2148
- return { strategy: 'none' }
2149
- })()`
2213
+
2214
+ // src/sim-client.ts
2215
+ var SimClientError = class extends Error {
2216
+ constructor(message) {
2217
+ super(message);
2218
+ this.name = new.target.name;
2219
+ }
2220
+ };
2221
+ var SimConnectError = class extends SimClientError {
2222
+ };
2223
+ var SimReadyTimeoutError = class extends SimClientError {
2224
+ constructor(status) {
2225
+ super(`sim not ready: ${waitReadyReason(status)}`);
2226
+ __publicField(this, "status");
2227
+ this.status = status;
2228
+ }
2229
+ };
2230
+ var SimTapError = class extends SimClientError {
2231
+ constructor(message, outcome = null) {
2232
+ super(message);
2233
+ __publicField(this, "outcome");
2234
+ this.outcome = outcome;
2235
+ }
2236
+ };
2237
+ var SimKeyboardError = class extends SimClientError {
2238
+ };
2239
+ var SimPerformError = class extends SimClientError {
2240
+ constructor(result) {
2241
+ const failed = result.steps.find((step) => !step.ok);
2242
+ const at = failed ? `${failed.index} (${failed.type})` : String(result.completed);
2243
+ const cause = failed?.error ?? result.error;
2244
+ super(`perform failed at step ${at}${cause ? `: ${cause}` : ""}`);
2245
+ __publicField(this, "result");
2246
+ this.result = result;
2247
+ }
2248
+ };
2249
+ var SimResetError = class extends SimClientError {
2250
+ constructor(result) {
2251
+ super(`reset (${result.strategy}) failed${result.error ? `: ${result.error}` : ""}`);
2252
+ __publicField(this, "result");
2253
+ this.result = result;
2254
+ }
2255
+ };
2256
+ function sleep(ms) {
2257
+ return new Promise((resolve) => setTimeout(resolve, ms));
2258
+ }
2259
+ function decodePngDataUrl(dataUrl) {
2260
+ if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:image/png;base64,")) {
2261
+ throw new SimClientError(
2262
+ `screenshot bridge returned a non-png payload: ${JSON.stringify(
2263
+ typeof dataUrl === "string" ? dataUrl.slice(0, 80) : dataUrl
2264
+ )}`
2265
+ );
2266
+ }
2267
+ const base64 = dataUrl.slice("data:image/png;base64,".length);
2268
+ const binary = atob(base64);
2269
+ const bytes = new Uint8Array(binary.length);
2270
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
2271
+ return { dataUrl, bytes };
2272
+ }
2273
+ var TARGET_SEARCH_PARAMS = [
2274
+ "bundle",
2275
+ "demo",
2276
+ "app",
2277
+ "open",
2278
+ "port",
2279
+ "inspectOpen",
2280
+ "appFonts",
2281
+ "appSplash"
2282
+ ];
2283
+ function buildSimOpenUrl(currentUrl, target) {
2284
+ const url = new URL(currentUrl);
2285
+ for (const param of TARGET_SEARCH_PARAMS) url.searchParams.delete(param);
2286
+ const pathname = url.pathname.replace(/\/+$/, "") || "/";
2287
+ const prefix = pathname === "/__soot" || pathname.startsWith("/__soot/") ? "/__soot" : "";
2288
+ const normalized = String(target).trim();
2289
+ if (/^\d+$/.test(normalized)) {
2290
+ url.pathname = `${prefix}/rn/${normalized}`;
2291
+ } else {
2292
+ url.pathname = `${prefix}/rn`;
2293
+ url.searchParams.set("open", normalized);
2294
+ }
2295
+ return url.toString();
2296
+ }
2297
+ function isTextTapTarget(target) {
2298
+ return typeof target === "object" && "text" in target && typeof target.text === "string" && !("layout" in target) && !("absolutePosition" in target);
2299
+ }
2300
+ var SimClient = class {
2301
+ constructor(bridge, simId, _options = {}) {
2302
+ __publicField(this, "simId");
2303
+ __publicField(this, "bridge");
2304
+ __publicField(this, "closed", false);
2305
+ this.bridge = bridge;
2306
+ this.simId = simId;
2307
+ }
2308
+ // --- transport (WsBridge/InspectBridge) ---
2309
+ send(cmd, opts) {
2310
+ if (this.closed) {
2311
+ return Promise.reject(new SimConnectError("sim client is closed"));
2312
+ }
2313
+ const payload = cmd.simId === void 0 ? { ...cmd, simId: this.simId } : cmd;
2314
+ return this.bridge.send(payload, opts);
2315
+ }
2316
+ listSims() {
2317
+ return this.bridge.listSims();
2318
+ }
2319
+ focusSim(simId = this.simId) {
2320
+ return this.bridge.focusSim(simId);
2321
+ }
2322
+ closeSim(simId = this.simId) {
2323
+ return this.bridge.closeSim(simId);
2324
+ }
2325
+ claim(simId = this.simId, opts) {
2326
+ return this.bridge.claim(simId, opts);
2327
+ }
2328
+ /** release the socket. a script that connected must close to exit cleanly. */
2329
+ close() {
2330
+ if (this.closed) return;
2331
+ this.closed = true;
2332
+ this.bridge.close();
2333
+ }
2334
+ // --- navigation + readiness ---
2335
+ /**
2336
+ * point this sim at a dev target: a metro port (`8081`), or anything the
2337
+ * shell's connect flow accepts (`localhost:8081`, an app slug…). resolves
2338
+ * once the reloaded page answers on the new target route.
2339
+ */
2340
+ async open(target, opts = {}) {
2341
+ const sims = await this.listSims();
2342
+ const sim = sims.find((entry) => entry.id === this.simId);
2343
+ if (!sim) {
2344
+ throw new SimConnectError(`sim ${this.simId} is no longer connected`);
2345
+ }
2346
+ const currentUrl = sim.url ?? sim.origin;
2347
+ if (!currentUrl) {
2348
+ throw new SimConnectError(
2349
+ `sim ${this.simId} reported no URL to derive the shell from`
2350
+ );
2351
+ }
2352
+ const nextUrl = buildSimOpenUrl(currentUrl, target);
2353
+ const expected = new URL(nextUrl);
2354
+ await this.send({
2355
+ type: "evaluate",
2356
+ code: `window.location.href = ${JSON.stringify(nextUrl)}`
2357
+ });
2358
+ const deadline = Date.now() + (opts.timeoutMs ?? 3e4);
2359
+ while (Date.now() < deadline) {
2360
+ await sleep(250);
2361
+ try {
2362
+ const { url } = await inspectUrl(this);
2363
+ const current = new URL(url);
2364
+ if (current.pathname === expected.pathname && current.searchParams.get("open") === expected.searchParams.get("open")) {
2365
+ return;
2366
+ }
2367
+ } catch {
2368
+ }
2369
+ }
2370
+ throw new SimConnectError(
2371
+ `timed out waiting for sim ${this.simId} to load ${nextUrl}`
2372
+ );
2373
+ }
2374
+ /**
2375
+ * block until the guest app has mounted and painted real content.
2376
+ * throws SimReadyTimeoutError (with the last probe status) on timeout.
2377
+ */
2378
+ async waitReady(opts = {}) {
2379
+ const status = await inspectWaitReady(this, opts.timeoutMs ?? 2e4, {
2380
+ onProgress: opts.onProgress
2381
+ });
2382
+ if (!status.ready) throw new SimReadyTimeoutError(status);
2383
+ return status;
2384
+ }
2385
+ /** wait for layout/animation/transition settle after an interaction. */
2386
+ settle(opts = {}) {
2387
+ return waitForSootsimIdle({
2388
+ bridge: this,
2389
+ maxMs: opts.maxMs ?? 3e3,
2390
+ strict: opts.strict
2391
+ });
2392
+ }
2393
+ // --- read verbs ---
2394
+ /** first matching node, or null. query precedence matches the CLI's find. */
2395
+ async find(query) {
2396
+ const nodes = await this.findAll(query);
2397
+ return nodes[0] ?? null;
2398
+ }
2399
+ async findAll(query) {
2400
+ const mapped = {
2401
+ testId: query.testId ?? null,
2402
+ role: query.role ?? null,
2403
+ type: query.type ?? null,
2404
+ text: query.text ?? null,
2405
+ pressable: query.pressable
2406
+ };
2407
+ const found = await inspectFind(this, mapped);
2408
+ if (!found) {
2409
+ throw new SimClientError(
2410
+ "find() needs at least one of testId, text, role, type, or pressable"
2411
+ );
2412
+ }
2413
+ const { result } = found;
2414
+ if (Array.isArray(result)) return result;
2415
+ return result ? [result] : [];
2416
+ }
2417
+ async tree(depth = 5) {
2418
+ const { tree } = await inspectTree(this, depth);
2419
+ return tree;
2420
+ }
2421
+ /** compact visible-UI dump with shell + keyboard state, as the CLI prints. */
2422
+ describe(opts = {}) {
2423
+ return inspectDescribe(this, {
2424
+ describe: true,
2425
+ verbose: opts.verbose ?? false,
2426
+ filter: opts.filter ?? "",
2427
+ compact: !opts.verbose,
2428
+ hideXy: false,
2429
+ fullText: true
2430
+ });
2431
+ }
2432
+ accessibilityTree() {
2433
+ return inspectAccessibilityTree(this);
2434
+ }
2435
+ async logs(opts = {}) {
2436
+ return filterLogEntries(await inspectLogs(this), opts);
2437
+ }
2438
+ errors(limit = 20) {
2439
+ return inspectErrors(this, limit);
2440
+ }
2441
+ warnings(limit = 20) {
2442
+ return inspectWarnings(this, limit);
2443
+ }
2444
+ keyboard() {
2445
+ return inspectKeyboard(this);
2446
+ }
2447
+ shellState(readyTimeoutMs = 0) {
2448
+ return getShellState(this, readyTimeoutMs);
2449
+ }
2450
+ async url() {
2451
+ const { url } = await inspectUrl(this);
2452
+ return url;
2453
+ }
2454
+ async screenshot(opts = {}) {
2455
+ const result = await this.send({
2456
+ type: "screenshot",
2457
+ ...opts.layers ? { layers: opts.layers } : {},
2458
+ ...opts.crop ? { crop: opts.crop } : {}
2459
+ });
2460
+ return decodePngDataUrl(result);
2461
+ }
2462
+ // --- input verbs ---
2463
+ /**
2464
+ * tap by testID, visible text, node, or coordinates. resolution, retry, and
2465
+ * hit verification are the CLI's shared tap kernels. throws SimTapError when
2466
+ * the target is missing, covered, offscreen, or the press missed.
2467
+ */
2468
+ async tap(target) {
2469
+ if (typeof target === "string") {
2470
+ return this.throwUnlessTapped(await tapBest(this, target), target);
2471
+ }
2472
+ if ("testId" in target && typeof target.testId === "string") {
2473
+ return this.throwUnlessTapped(await tapById(this, target.testId), target.testId);
2474
+ }
2475
+ if (isTextTapTarget(target)) {
2476
+ const { text, ...textOpts } = target;
2477
+ return this.throwUnlessTapped(await tapByText(this, text, textOpts), text);
2478
+ }
2479
+ if ("testID" in target && typeof target.testID === "string") {
2480
+ return this.throwUnlessTapped(await tapById(this, target.testID), target.testID);
2481
+ }
2482
+ const coords = this.coordsForTapTarget(target);
2483
+ const result = await this.send({ type: "tap", x: coords.x, y: coords.y });
2484
+ if (!isTapSuccess(result)) {
2485
+ throw new SimTapError(
2486
+ `tap at ${coords.x},${coords.y} missed: ${JSON.stringify(result)}`
2487
+ );
2488
+ }
2489
+ }
2490
+ async longPress(target, opts = {}) {
2491
+ const coords = await this.resolveCoords(target);
2492
+ const result = await this.send(
2493
+ {
2494
+ type: "longPress",
2495
+ x: coords.x,
2496
+ y: coords.y,
2497
+ durationMs: opts.durationMs ?? 600
2498
+ },
2499
+ { timeoutMs: (opts.durationMs ?? 600) + 15e3 }
2500
+ );
2501
+ if (!result?.ok) {
2502
+ throw new SimTapError(`longPress at ${coords.x},${coords.y} missed`);
2503
+ }
2504
+ }
2505
+ /** type through the visual iOS keyboard. requires a focused input. */
2506
+ async type(text) {
2507
+ await this.requireVisibleKeyboard("type");
2508
+ await this.send({ type: "keyboard", action: "type", text });
2509
+ }
2510
+ /** press a keyboard key ('return', 'delete', 'space', …). */
2511
+ async press(key) {
2512
+ await this.requireVisibleKeyboard("press");
2513
+ await this.send({ type: "keyboard", action: "press", text: key });
2514
+ }
2515
+ async dismissKeyboard() {
2516
+ await this.send({ type: "keyboard", action: "dismiss" });
2517
+ }
2518
+ // --- batched input, compound state, reset ---
2519
+ /**
2520
+ * execute an ordered step batch inside the page in ONE round-trip, so waits
2521
+ * and gesture physics run on real wall-clock timing. `tapText` steps are
2522
+ * resolved to coordinates client-side before the batch is sent. throws
2523
+ * SimPerformError (carrying the per-step result) when the batch fails.
2524
+ */
2525
+ async perform(steps, options) {
2526
+ const resolved = [];
2527
+ for (const step of steps) {
2528
+ if (step.type !== "tapText") {
2529
+ resolved.push(step);
2530
+ continue;
2531
+ }
2532
+ const target = await resolveTargetCoords(this, {
2533
+ mode: "text",
2534
+ value: step.text
2150
2535
  });
2151
- if (!payload || !("node" in payload)) return payload;
2152
- const node = payload.node;
2153
- const cx = node.absolutePosition.x + node.layout.width / 2;
2154
- const cy = node.absolutePosition.y + node.layout.height / 2;
2155
- const screen = payload.screen ?? null;
2156
- const offscreen = !!screen && (cx < 0 || cy < 0 || cx > screen.width || cy > screen.height);
2157
- return {
2158
- ...payload,
2159
- cx,
2160
- cy,
2161
- ...offscreen ? { offscreen: true, screen } : {},
2536
+ if (!target) {
2537
+ throw new SimTapError(
2538
+ `perform tapText: no node with text ${JSON.stringify(step.text)}`
2539
+ );
2540
+ }
2541
+ resolved.push({
2542
+ type: "tap",
2543
+ x: target.x,
2544
+ y: target.y,
2162
2545
  target: {
2163
- id: node.id,
2164
- testID: node.testID,
2165
- text: payload.strategy === "text" ? query : node.text,
2166
- type: node.type
2546
+ testID: target.testID ?? void 0,
2547
+ text: target.text ?? void 0,
2548
+ type: target.type ?? void 0
2167
2549
  }
2550
+ });
2551
+ }
2552
+ const batchTimeoutMs = options?.timeoutMs ?? 3e4;
2553
+ const result = await this.send(
2554
+ {
2555
+ type: "perform",
2556
+ steps: resolved,
2557
+ ...options ? { performOptions: options } : {}
2558
+ },
2559
+ { timeoutMs: batchTimeoutMs + 5e3 }
2560
+ );
2561
+ if (!result?.ok) throw new SimPerformError(result);
2562
+ return result;
2563
+ }
2564
+ /** screenshot + tree + route + recent errors in one round-trip. */
2565
+ state(options) {
2566
+ return this.send({
2567
+ type: "state",
2568
+ ...options ? { stateOptions: options } : {}
2569
+ });
2570
+ }
2571
+ /**
2572
+ * two-tier app state wipe ('data' clears storage/caches, 'full' also clears
2573
+ * keychain/permissions/native-module state). throws SimResetError on failure.
2574
+ */
2575
+ async reset(options) {
2576
+ const result = await this.send(
2577
+ { type: "reset", ...options ? { resetOptions: options } : {} },
2578
+ { timeoutMs: 6e4 }
2579
+ );
2580
+ if (!result?.ok) throw new SimResetError(result);
2581
+ return result;
2582
+ }
2583
+ // --- internals ---
2584
+ throwUnlessTapped(outcome, label) {
2585
+ if (!outcome.failure && isTapSuccess(outcome.result)) return;
2586
+ const reason = outcome.failure === "not-found" ? "no matching node" : outcome.failure === "missed" ? outcome.result?.reason ?? "press missed" : outcome.payload?.ambiguous ? `ambiguous: ${outcome.payload.total} matches` : outcome.result?.reason ?? outcome.payload?.error ?? "tap failed";
2587
+ throw new SimTapError(`tap ${JSON.stringify(label)} failed: ${reason}`, outcome);
2588
+ }
2589
+ coordsForTapTarget(target) {
2590
+ if (typeof target === "object" && "x" in target && "y" in target && typeof target.x === "number" && typeof target.y === "number") {
2591
+ return { x: target.x, y: target.y };
2592
+ }
2593
+ if (typeof target === "object" && "absolutePosition" in target && target.absolutePosition && target.layout) {
2594
+ return {
2595
+ x: target.absolutePosition.x + (target.layout.width ?? 0) / 2,
2596
+ y: target.absolutePosition.y + (target.layout.height ?? 0) / 2
2168
2597
  };
2169
2598
  }
2170
- });
2171
- }
2599
+ throw new SimClientError(
2600
+ "tap target needs coordinates, a testId, text, or a node from find()"
2601
+ );
2602
+ }
2603
+ async resolveCoords(target) {
2604
+ if (typeof target === "string") {
2605
+ const resolved = await resolveTargetCoords(this, { mode: "testid", value: target }) ?? await resolveTargetCoords(this, { mode: "text", value: target });
2606
+ if (!resolved) throw new SimTapError(`no node matching ${JSON.stringify(target)}`);
2607
+ return resolved;
2608
+ }
2609
+ if ("testId" in target && typeof target.testId === "string") {
2610
+ const resolved = await resolveTargetCoords(this, {
2611
+ mode: "testid",
2612
+ value: target.testId
2613
+ });
2614
+ if (!resolved) {
2615
+ throw new SimTapError(`no node with testID ${JSON.stringify(target.testId)}`);
2616
+ }
2617
+ return resolved;
2618
+ }
2619
+ if ("text" in target && typeof target.text === "string" && !("layout" in target)) {
2620
+ const resolved = await resolveTargetCoords(this, {
2621
+ mode: "text",
2622
+ value: target.text
2623
+ });
2624
+ if (!resolved) {
2625
+ throw new SimTapError(`no node with text ${JSON.stringify(target.text)}`);
2626
+ }
2627
+ return resolved;
2628
+ }
2629
+ return this.coordsForTapTarget(target);
2630
+ }
2631
+ async requireVisibleKeyboard(action) {
2632
+ const deadline = Date.now() + 1500;
2633
+ for (; ; ) {
2634
+ const state = await inspectKeyboard(this);
2635
+ if (!("error" in state) && state.visible) return;
2636
+ if (Date.now() >= deadline) {
2637
+ throw new SimKeyboardError(
2638
+ `${action} requires the iOS keyboard to be visible \u2014 focus an input first (tap a TextInput)`
2639
+ );
2640
+ }
2641
+ await sleep(150);
2642
+ }
2643
+ }
2644
+ };
2172
2645
  export {
2173
2646
  DEBUG_CHANNELS,
2174
2647
  LAYOUT_GET_NATIVE_EVAL,
@@ -2177,8 +2650,17 @@ export {
2177
2650
  READY_CONTENT_NODE_FLOOR,
2178
2651
  READY_NODE_STABLE_MS,
2179
2652
  READY_PROBE_POLL_MS,
2653
+ SimClient,
2654
+ SimClientError,
2655
+ SimConnectError,
2656
+ SimKeyboardError,
2657
+ SimPerformError,
2658
+ SimReadyTimeoutError,
2659
+ SimResetError,
2660
+ SimTapError,
2180
2661
  WAIT_READY_PROBE,
2181
2662
  WAIT_READY_PROGRESS_INTERVAL_MS,
2663
+ buildSimOpenUrl,
2182
2664
  clearConsole,
2183
2665
  clearLogs,
2184
2666
  clearRequests,