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