rnxsim 0.1.365 → 0.1.367

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 (240) hide show
  1. package/README.md +14 -0
  2. package/cli/bin.ts +18 -0
  3. package/cli/bridge-flow-runner.ts +16 -153
  4. package/cli/commands/assert.ts +1 -1
  5. package/cli/commands/box/checkout-plane.ts +126 -0
  6. package/cli/commands/box/rnx-command.ts +68 -0
  7. package/cli/commands/box.ts +200 -0
  8. package/cli/commands/control.ts +15 -76
  9. package/cli/commands/detox.ts +18 -1
  10. package/cli/commands/do-chain.ts +412 -0
  11. package/cli/commands/flow.ts +2 -11
  12. package/cli/commands/inspect/core.ts +302 -725
  13. package/cli/commands/inspect/describe.ts +3 -76
  14. package/cli/commands/inspect/find.ts +3 -6
  15. package/cli/commands/inspect/get-layout.ts +14 -1
  16. package/cli/commands/inspect/resolve-target.ts +41 -47
  17. package/cli/commands/inspect/shared.ts +28 -123
  18. package/cli/commands/inspect/tree.ts +6 -15
  19. package/cli/commands/inspect/wait-ready.ts +2 -2
  20. package/cli/commands/inspect/wait-selector.ts +4 -8
  21. package/cli/commands/inspect.ts +23 -17
  22. package/cli/commands/install-desktop.ts +44 -7
  23. package/cli/commands/maestro-generate.ts +0 -2
  24. package/cli/commands/platform.ts +68 -0
  25. package/cli/commands/state.ts +5 -2
  26. package/cli/outbound-endpoints.ts +6 -1
  27. package/cli/parse-args.ts +13 -2
  28. package/cli/ws-bridge.ts +30 -2
  29. package/detox/index.ts +37 -18
  30. package/dist-cli/bin.js +5 -5
  31. package/dist-cli/chunks/{agent-BVMNQFKF.js → agent-EKKIAKP5.js} +2 -2
  32. package/dist-cli/chunks/{agent-wrapper-B6YFXGC3.js → agent-wrapper-MHLQUGA6.js} +2 -2
  33. package/dist-cli/chunks/{app-fonts-MEI6R6XR.js → app-fonts-HN6ADRTQ.js} +2 -2
  34. package/dist-cli/chunks/{assert-JGHI53HV.js → assert-IMR6QCN6.js} +3 -3
  35. package/dist-cli/chunks/{auth-STEOXUS7.js → auth-27LPMQNH.js} +2 -2
  36. package/dist-cli/chunks/{beta-HT3FLS5T.js → beta-6RXBB5AI.js} +2 -2
  37. package/dist-cli/chunks/box-DORLF3GP.js +1709 -0
  38. package/dist-cli/chunks/{camera-ZCLFSLON.js → camera-2G7JZ5DL.js} +2 -2
  39. package/dist-cli/chunks/{chunk-RI3NS77J.js → chunk-3QZJLNYY.js} +1 -1
  40. package/dist-cli/chunks/{chunk-FHWZPUGC.js → chunk-3VBVEDDA.js} +1 -1
  41. package/dist-cli/chunks/{chunk-EAFKG5R3.js → chunk-4BWGLTKU.js} +2 -2
  42. package/dist-cli/chunks/chunk-4XSNO374.js +423 -0
  43. package/dist-cli/chunks/{chunk-GZEUDDAW.js → chunk-5DGMFUZG.js} +2 -2
  44. package/dist-cli/chunks/{chunk-TKCGP3DD.js → chunk-6DBS5EGX.js} +2 -2
  45. package/dist-cli/chunks/{chunk-HIDS3PYC.js → chunk-6SO6I3D5.js} +2 -2
  46. package/dist-cli/chunks/chunk-75EFOPXE.js +32 -0
  47. package/dist-cli/chunks/{chunk-FIXRGPUT.js → chunk-A5AJFVOF.js} +1 -1
  48. package/dist-cli/chunks/{chunk-2SFMW5K6.js → chunk-AM2ODYAN.js} +1 -1
  49. package/dist-cli/chunks/{chunk-BGRGP3QR.js → chunk-AVOROGA3.js} +1 -1
  50. package/dist-cli/chunks/chunk-AXMMLO7D.js +130 -0
  51. package/dist-cli/chunks/{chunk-XXNPISER.js → chunk-BSEXEYNX.js} +2 -2
  52. package/dist-cli/chunks/{chunk-67ATAITF.js → chunk-CM6ZKZW7.js} +2 -2
  53. package/dist-cli/chunks/{chunk-MR3YPNUI.js → chunk-DNVABCEC.js} +2 -2
  54. package/dist-cli/chunks/{chunk-5AS7S6HY.js → chunk-E33A4AIR.js} +2 -2
  55. package/dist-cli/chunks/{chunk-DBJZUCFP.js → chunk-EFMOXYDD.js} +2 -2
  56. package/dist-cli/chunks/{chunk-ZTWUNK45.js → chunk-EV4P5UBP.js} +2 -2
  57. package/dist-cli/chunks/{chunk-EVBQDQVY.js → chunk-G5IUIGGC.js} +1 -1
  58. package/dist-cli/chunks/chunk-GMLOCV3E.js +312 -0
  59. package/dist-cli/chunks/{chunk-DQFOHIRF.js → chunk-GRN2FHVW.js} +1 -1
  60. package/dist-cli/chunks/{chunk-CYIE65QN.js → chunk-HFFXPUEP.js} +1 -1
  61. package/dist-cli/chunks/chunk-HG6T4OOR.js +4 -0
  62. package/dist-cli/chunks/{chunk-GK7PGYFH.js → chunk-HNT2OXPZ.js} +1 -1
  63. package/dist-cli/chunks/chunk-IPEURNLI.js +24 -0
  64. package/dist-cli/chunks/chunk-IR44SFBA.js +5 -0
  65. package/dist-cli/chunks/chunk-JMSLXZFW.js +4 -0
  66. package/dist-cli/chunks/{chunk-WGNDJARS.js → chunk-JPXQKLNH.js} +1 -1
  67. package/dist-cli/chunks/chunk-K3EC6JDE.js +4 -0
  68. package/dist-cli/chunks/chunk-KHX65HAU.js +4 -0
  69. package/dist-cli/chunks/{chunk-MACI34ST.js → chunk-LWOVKHRC.js} +2 -2
  70. package/dist-cli/chunks/{chunk-QIQL7Y5V.js → chunk-MQN5DWTI.js} +2 -2
  71. package/dist-cli/chunks/{chunk-5VHWQGGF.js → chunk-N3BXQEUF.js} +1 -1
  72. package/dist-cli/chunks/{chunk-O2ORLK6W.js → chunk-N5OSV26B.js} +2 -2
  73. package/dist-cli/chunks/chunk-PHNURIH3.js +4 -0
  74. package/dist-cli/chunks/{chunk-JRPU5TFI.js → chunk-PP2KNVXP.js} +3 -3
  75. package/dist-cli/chunks/{chunk-UR345KM3.js → chunk-PROKNJVY.js} +2 -2
  76. package/dist-cli/chunks/chunk-QXLLY4IM.js +7 -0
  77. package/dist-cli/chunks/{chunk-2CXHDTQ5.js → chunk-RNQTOGM7.js} +1 -1
  78. package/dist-cli/chunks/{chunk-M5KJCLUW.js → chunk-RPNKKVBN.js} +27 -27
  79. package/dist-cli/chunks/{chunk-EFXNSJ55.js → chunk-RSNLI2NH.js} +2 -2
  80. package/dist-cli/chunks/{chunk-25BTVHK2.js → chunk-RZGHV2WI.js} +2 -2
  81. package/dist-cli/chunks/chunk-SGGL3AFV.js +497 -0
  82. package/dist-cli/chunks/chunk-SH5ETJKG.js +4 -0
  83. package/dist-cli/chunks/{chunk-MRNW4GVL.js → chunk-T6J4YP56.js} +2 -2
  84. package/dist-cli/chunks/{chunk-R4FQZXJC.js → chunk-TWUE7WF5.js} +2 -2
  85. package/dist-cli/chunks/{chunk-X5ZOZVHC.js → chunk-U44VEQRN.js} +1 -1
  86. package/dist-cli/chunks/{chunk-XRO5WQXP.js → chunk-URLVQHC3.js} +1 -1
  87. package/dist-cli/chunks/{chunk-JAFF4RAU.js → chunk-VANZFTUE.js} +1 -1
  88. package/dist-cli/chunks/{chunk-6BAF6GFS.js → chunk-VB2YBH46.js} +1 -1
  89. package/dist-cli/chunks/{chunk-O6P4N2JD.js → chunk-VFU2SJ7M.js} +2 -2
  90. package/dist-cli/chunks/{chunk-XPOTXF5Q.js → chunk-VUBJ6FXU.js} +15 -15
  91. package/dist-cli/chunks/{chunk-XR4KID25.js → chunk-W5PATILQ.js} +2 -2
  92. package/dist-cli/chunks/chunk-WI5V4TYG.js +38 -0
  93. package/dist-cli/chunks/{chunk-NEH4T67X.js → chunk-XTRHQAOV.js} +2 -2
  94. package/dist-cli/chunks/chunk-XWUTJLBM.js +27 -0
  95. package/dist-cli/chunks/{chunk-TULWPOAY.js → chunk-XYRUQQUR.js} +2 -2
  96. package/dist-cli/chunks/chunk-YBFB3RM7.js +4 -0
  97. package/dist-cli/chunks/chunk-YGQVQ2VG.js +4 -0
  98. package/dist-cli/chunks/{chunk-J66US2OZ.js → chunk-YINACV32.js} +1 -1
  99. package/dist-cli/chunks/chunk-YV7ERO2V.js +5 -0
  100. package/dist-cli/chunks/{cleanup-G4TEYVKY.js → cleanup-GVFHQ3KH.js} +2 -2
  101. package/dist-cli/chunks/cli-version-DPX5MA2M.js +4 -0
  102. package/dist-cli/chunks/{compat-EZ5NP32J.js → compat-AYRTXYAM.js} +2 -2
  103. package/dist-cli/chunks/{config-27M2DGCS.js → config-K7LOOV62.js} +2 -2
  104. package/dist-cli/chunks/connect-H3CNY4IO.js +4 -0
  105. package/dist-cli/chunks/control-SMHZ2CF5.js +4 -0
  106. package/dist-cli/chunks/daemon-TEZUNI73.js +4 -0
  107. package/dist-cli/chunks/{debug-EXU3D7PC.js → debug-ELGHG57T.js} +3 -3
  108. package/dist-cli/chunks/{desktop-SHHPZAWQ.js → desktop-4G7FK3MP.js} +3 -3
  109. package/dist-cli/chunks/detox-JKXGEJIZ.js +52 -0
  110. package/dist-cli/chunks/{device-4NI26HG6.js → device-IV3AR6KG.js} +2 -2
  111. package/dist-cli/chunks/{diagnose-P4SFXH6J.js → diagnose-3ERGF7EW.js} +2 -2
  112. package/dist-cli/chunks/{disk-cleanup-BUAKWYMY.js → disk-cleanup-ALFEVZ4B.js} +2 -2
  113. package/dist-cli/chunks/do-chain-K2B4OHM7.js +5 -0
  114. package/dist-cli/chunks/drivers-KJYXA3M3.js +4 -0
  115. package/dist-cli/chunks/env-2KF6IYZE.js +4 -0
  116. package/dist-cli/chunks/{film-Y2BAQINB.js → film-UQXJFAD5.js} +3 -3
  117. package/dist-cli/chunks/flow-ASTUH56E.js +4 -0
  118. package/dist-cli/chunks/help-SNNYFELA.js +4 -0
  119. package/dist-cli/chunks/{hidden-runtime-alias-U2MAY2XV.js → hidden-runtime-alias-VJCWCFVO.js} +2 -2
  120. package/dist-cli/chunks/{home-paths-PAJMZCOS.js → home-paths-EGUZZ6J6.js} +2 -2
  121. package/dist-cli/chunks/inspect-XANAADQT.js +4 -0
  122. package/dist-cli/chunks/install-desktop-JKQA7UY6.js +4 -0
  123. package/dist-cli/chunks/{login-D53XKBEO.js → login-6KQGD7HK.js} +2 -2
  124. package/dist-cli/chunks/{logout-HZQKLNUJ.js → logout-2FCOVLBC.js} +2 -2
  125. package/dist-cli/chunks/{maestro-XSS2KX6L.js → maestro-HPSSEVJD.js} +3 -3
  126. package/dist-cli/chunks/{maestro-generate-DNDQX3JQ.js → maestro-generate-KQQ6JHMR.js} +5 -5
  127. package/dist-cli/chunks/{mode-LEVGOKI3.js → mode-SH7KRZET.js} +2 -2
  128. package/dist-cli/chunks/{optional-demo-registry-2XZIJNDG.js → optional-demo-registry-NAMCAVFX.js} +2 -2
  129. package/dist-cli/chunks/{perf-OKMAXDLB.js → perf-LWKAHXUL.js} +2 -2
  130. package/dist-cli/chunks/{permissions-VG5TBUTN.js → permissions-EZTKWTXV.js} +2 -2
  131. package/dist-cli/chunks/platform-KSOJIVN5.js +6 -0
  132. package/dist-cli/chunks/privacy-EJULWP7L.js +4 -0
  133. package/dist-cli/chunks/{record-BQYGZOMV.js → record-2IC7BAQN.js} +3 -3
  134. package/dist-cli/chunks/{report-issue-HCPZESWE.js → report-issue-MMX6HOQO.js} +2 -2
  135. package/dist-cli/chunks/reset-PKFZA4NC.js +4 -0
  136. package/dist-cli/chunks/resolve-target-AFKU3BRE.js +4 -0
  137. package/dist-cli/chunks/runtime-XHWWPOIN.js +4 -0
  138. package/dist-cli/chunks/{screenshot-command-DWEYPJE7.js → screenshot-command-JYUXDVSA.js} +3 -3
  139. package/dist-cli/chunks/{screenshot-layers-5GMBXLO7.js → screenshot-layers-OGRY6AA3.js} +2 -2
  140. package/dist-cli/chunks/{screenshots-capture-5KCI7IZD.js → screenshots-capture-QHDN65XS.js} +2 -2
  141. package/dist-cli/chunks/{serve-QNKTA2HR.js → serve-W4BKAG4J.js} +3 -3
  142. package/dist-cli/chunks/setup-WJPMPSAC.js +4 -0
  143. package/dist-cli/chunks/sim-client-UOQGFBPO.js +4 -0
  144. package/dist-cli/chunks/{skills-DOEALD3X.js → skills-OPPXOD63.js} +2 -2
  145. package/dist-cli/chunks/state-TDAN7BOM.js +14 -0
  146. package/dist-cli/chunks/{storage-E7WUWN3E.js → storage-CFB2YGLY.js} +2 -2
  147. package/dist-cli/chunks/store-4TEED4ZU.js +4 -0
  148. package/dist-cli/chunks/telemetry-S6N7CV6G.js +4 -0
  149. package/dist-cli/chunks/{timeline-IKNUGQ2A.js → timeline-ASOOVP4C.js} +2 -2
  150. package/dist-cli/chunks/{upgrade-VS3OKXE2.js → upgrade-6MVJISHG.js} +2 -2
  151. package/dist-cli/chunks/upload-CBNIRFEW.js +4 -0
  152. package/dist-cli/chunks/{version-F4XHNTBZ.js → version-5GS7CWRL.js} +2 -2
  153. package/dist-cli/chunks/{web-F72724QL.js → web-QRRMYMV4.js} +2 -2
  154. package/dist-cli/chunks/welcome-33FXMAMB.js +5 -0
  155. package/dist-cli/chunks/{what-happened-APK7YKOV.js → what-happened-GAZJGKXX.js} +2 -2
  156. package/dist-cli/chunks/ws-bridge-DIXUAPLQ.js +4 -0
  157. package/dist-lib/agent-daemon-client.cjs +1 -1
  158. package/dist-lib/agent-events.cjs +1 -1
  159. package/dist-lib/agent-identity.cjs +1 -1
  160. package/dist-lib/agent-sessions.cjs +1 -1
  161. package/dist-lib/attached-projects.cjs +1 -1
  162. package/dist-lib/auth/shared-session.cjs +1 -1
  163. package/dist-lib/backend-origin.cjs +1 -1
  164. package/dist-lib/beta.cjs +1 -1
  165. package/dist-lib/beta.mjs +1 -1
  166. package/dist-lib/bridge-constants.cjs +1 -1
  167. package/dist-lib/bridge-contract-input.cjs +336 -0
  168. package/dist-lib/bridge-contract-input.mjs +303 -0
  169. package/dist-lib/bridge-contract.cjs +29 -1
  170. package/dist-lib/bridge-contract.mjs +19 -0
  171. package/dist-lib/cli-constants.cjs +1 -1
  172. package/dist-lib/config.cjs +1 -1
  173. package/dist-lib/detox/index.cjs +35 -19
  174. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  175. package/dist-lib/home-paths.cjs +1 -1
  176. package/dist-lib/host/bridge-host.cjs +5 -1
  177. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  178. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  179. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  180. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  181. package/dist-lib/host/websocket-proxy.cjs +1 -1
  182. package/dist-lib/index.cjs +244 -594
  183. package/dist-lib/jump-to-source-babel.cjs +1 -1
  184. package/dist-lib/menu.cjs +1 -1
  185. package/dist-lib/menu.mjs +1 -1
  186. package/dist-lib/metro-production-bundle.cjs +1 -1
  187. package/dist-lib/metro-production-bundle.mjs +1 -1
  188. package/dist-lib/metro.cjs +1 -1
  189. package/dist-lib/profiles.cjs +1 -1
  190. package/dist-lib/public-brand.cjs +1 -1
  191. package/dist-lib/react-native-host-modules.cjs +1 -1
  192. package/dist-lib/react-native-host-modules.mjs +1 -1
  193. package/dist-lib/render-mode.cjs +1 -1
  194. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  195. package/dist-lib/sdk.cjs +303 -657
  196. package/dist-lib/sdk.mjs +301 -652
  197. package/dist-lib/skills.cjs +256 -339
  198. package/dist-lib/vite.cjs +1 -1
  199. package/package.json +10 -1
  200. package/skills/contrast/SKILL.md +3 -0
  201. package/skills/rnx-debug/SKILL.md +8 -0
  202. package/skills/rnx-test/SKILL.md +21 -0
  203. package/src/app-url.ts +40 -0
  204. package/src/bridge-contract-input.ts +359 -0
  205. package/src/bridge-contract.ts +121 -1
  206. package/src/host/bridge-host.ts +7 -12
  207. package/src/index.ts +7 -0
  208. package/src/sim-client.ts +24 -9
  209. package/dist-cli/chunks/chunk-2JFZ5Z7Q.js +0 -4
  210. package/dist-cli/chunks/chunk-4WFN4DFR.js +0 -5
  211. package/dist-cli/chunks/chunk-52ES4API.js +0 -38
  212. package/dist-cli/chunks/chunk-622E4JET.js +0 -767
  213. package/dist-cli/chunks/chunk-EFL2ASZO.js +0 -124
  214. package/dist-cli/chunks/chunk-IWPS3EKJ.js +0 -15
  215. package/dist-cli/chunks/chunk-MCHKBFUL.js +0 -4
  216. package/dist-cli/chunks/chunk-OSZ5HZSI.js +0 -24
  217. package/dist-cli/chunks/chunk-W3UIDI3W.js +0 -5
  218. package/dist-cli/chunks/chunk-XKRUNS7A.js +0 -32
  219. package/dist-cli/chunks/chunk-YE3M3YD4.js +0 -944
  220. package/dist-cli/chunks/chunk-ZCP4CRP6.js +0 -4
  221. package/dist-cli/chunks/chunk-ZGD7GZRI.js +0 -4
  222. package/dist-cli/chunks/chunk-ZGH5U4AJ.js +0 -27
  223. package/dist-cli/chunks/cli-version-ZM3PVKQ5.js +0 -4
  224. package/dist-cli/chunks/control-4QR43FXV.js +0 -4
  225. package/dist-cli/chunks/daemon-2U4CJ6AT.js +0 -4
  226. package/dist-cli/chunks/detox-RQ6FFSBS.js +0 -52
  227. package/dist-cli/chunks/drivers-OVCUPQTM.js +0 -4
  228. package/dist-cli/chunks/flow-ELTLKKZ3.js +0 -4
  229. package/dist-cli/chunks/help-CDEZAG2L.js +0 -4
  230. package/dist-cli/chunks/inspect-VVQRPJLS.js +0 -4
  231. package/dist-cli/chunks/install-desktop-YXSRKR35.js +0 -4
  232. package/dist-cli/chunks/privacy-MNRHREME.js +0 -4
  233. package/dist-cli/chunks/reset-R4E4W4YI.js +0 -4
  234. package/dist-cli/chunks/runtime-XRQZ7W34.js +0 -4
  235. package/dist-cli/chunks/setup-RNMM5GO5.js +0 -4
  236. package/dist-cli/chunks/state-3HH2IHIF.js +0 -14
  237. package/dist-cli/chunks/store-HUPZUCFG.js +0 -4
  238. package/dist-cli/chunks/telemetry-IJ53XR2Y.js +0 -4
  239. package/dist-cli/chunks/upload-A57NAAHF.js +0 -4
  240. package/dist-cli/chunks/welcome-B72RRMBW.js +0 -5
package/dist-lib/vite.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.365 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.367 | (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 __create = Object.create;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rnxsim",
3
- "version": "0.1.365",
3
+ "version": "0.1.367",
4
4
  "description": "rnx CLI, Vite and Metro plugins, and agent skills for driving React Native apps in the browser.",
5
5
  "author": "Tamagui LLC",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -71,6 +71,12 @@
71
71
  "import": "./dist-lib/sdk.mjs",
72
72
  "default": "./dist-lib/sdk.cjs"
73
73
  },
74
+ "./bridge-contract-input": {
75
+ "types": "./src/bridge-contract-input.ts",
76
+ "source": "./src/bridge-contract-input.ts",
77
+ "import": "./dist-lib/bridge-contract-input.mjs",
78
+ "default": "./dist-lib/bridge-contract-input.cjs"
79
+ },
74
80
  "./beta": {
75
81
  "source": "./src/beta.ts",
76
82
  "import": "./dist-lib/beta.mjs",
@@ -105,6 +111,8 @@
105
111
  "./bridge-contract": {
106
112
  "types": "./src/bridge-contract.ts",
107
113
  "source": "./src/bridge-contract.ts",
114
+ "bun": "./src/bridge-contract.ts",
115
+ "import": "./dist-lib/bridge-contract.mjs",
108
116
  "default": "./dist-lib/bridge-contract.cjs"
109
117
  },
110
118
  "./bridge-constants": {
@@ -213,6 +221,7 @@
213
221
  }
214
222
  },
215
223
  "devDependencies": {
224
+ "@contrast/harness": "workspace:*",
216
225
  "@contrast/runtime-delivery": "workspace:*",
217
226
  "@sootsim/compat": "workspace:*",
218
227
  "@sootsim/globals": "workspace:*",
@@ -42,6 +42,9 @@ automation, inspecting the render tree, and managing sims.
42
42
  | `rnx debug` | drive __sootsimDebug: channels, snapshots, and inspectors |
43
43
  | `rnx what-happened` | show recent semantic events: toasts, keyboards, screens, alerts, fetches, errors |
44
44
  | `rnx timeline` | control the semantic event timeline (enable opt-in kinds, clear, dump) |
45
+ | `rnx box` | start a local box and enter its shell |
46
+ | `rnx ios` | start an iOS simulator |
47
+ | `rnx android` | start an Android simulator |
45
48
  | `rnx open` | load a target into the current sim or open a new one |
46
49
  | `rnx list` | list connected sims or available launch drivers |
47
50
  | `rnx use` | select a connected sim |
@@ -147,6 +147,14 @@ rnx debug diff before after # structural diff: what changed?
147
147
  rnx what-happened # timeline events during the repro
148
148
  ```
149
149
 
150
+ when the repro needs several closely timed inputs, replace the individual
151
+ write calls with one inline batch so WebSocket scheduling cannot change the
152
+ gesture:
153
+
154
+ ```sh
155
+ rnx do touch down 200 600 --then sleep 0.03 --then touch move 200 250 --then touch up 200 250
156
+ ```
157
+
150
158
  for nested scroll ownership or momentum transfer, compare the shell's resolved
151
159
  gesture chain with the published scroll registry:
152
160
 
@@ -21,6 +21,7 @@ the official Vite or Metro plugin, and test-only work must not open that window.
21
21
  - have an existing `.maestro/` directory? → [Maestro](#branch--maestro)
22
22
  - have existing Detox specs? → [detox drop-in](#branch--detox-drop-in)
23
23
  - writing tests fresh, want plain-language generation, or want live authoring? → [Maestro](#branch--maestro)
24
+ - need one short, timing-sensitive sequence from the shell? → [inline chains](#one-off-inline-chains)
24
25
 
25
26
  Do not invent a third runner for how a Maestro flow was authored.
26
27
 
@@ -43,6 +44,8 @@ Do not invent a third runner for how a Maestro flow was authored.
43
44
  - **don't run a 40-step "smoke everything" flow.** when it fails, you
44
45
  can't tell what regressed. split into `login.yaml`, `compose.yaml`,
45
46
  `settings.yaml`, chain with `runFlow`.
47
+ - **inside an `rnx do` chain, use `sleep` only for gesture timing.** after the
48
+ chain, use `rnx wait` for app state so automation advances on a real condition.
46
49
 
47
50
  ## you're done when
48
51
 
@@ -52,6 +55,24 @@ Do not invent a third runner for how a Maestro flow was authored.
52
55
  - the green run took noticeably less wall-clock than the previous
53
56
  baseline (a suite that grows slower without growing wider is a smell)
54
57
 
58
+ ## one-off inline chains
59
+
60
+ repeat `--then` before each additional `do` action to run one ordered engine
61
+ batch. use a chain for a short repro, form fill, or timing-sensitive gesture
62
+ that does not belong in a saved test:
63
+
64
+ ```sh
65
+ rnx do tap email --then type user@example.com --then tap submit
66
+ rnx wait selector dashboard
67
+ ```
68
+
69
+ the batch stops on the first failure. add `--json` for per-action results.
70
+ `tap <id>` and `tap-id <id>` are exact; use `tap-text <text>` for visible text.
71
+ punctuation in action data remains data. the exact `--then` token is reserved
72
+ as the action boundary. use Maestro when the sequence should be reviewed,
73
+ reused, or run in CI; an inline chain is not a third test runner and does not
74
+ read flow files.
75
+
55
76
  ## branch — Maestro
56
77
 
57
78
  Write declarative Maestro YAML. The same command runs existing suites,
package/src/app-url.ts ADDED
@@ -0,0 +1,40 @@
1
+ import type { InspectBridge } from '../cli/commands/inspect/core'
2
+
3
+ const APP_URL_SCHEME_RE = /^[A-Za-z][A-Za-z0-9+.-]*:/
4
+
5
+ export function normalizeAppUrlTarget(target: string): string {
6
+ const trimmed = target.trim()
7
+ if (!trimmed) {
8
+ throw new Error('expected <url-or-route>')
9
+ }
10
+ if (APP_URL_SCHEME_RE.test(trimmed) || trimmed.startsWith('/')) {
11
+ return trimmed
12
+ }
13
+ return `/${trimmed}`
14
+ }
15
+
16
+ export function isAppDeepLinkTarget(target: string): boolean {
17
+ const trimmed = target.trim()
18
+ if (!trimmed) return false
19
+ if (trimmed.startsWith('/')) return true
20
+ if (!APP_URL_SCHEME_RE.test(trimmed)) return false
21
+ return !/^https?:\/\//i.test(trimmed)
22
+ }
23
+
24
+ export async function openAppUrl(bridge: InspectBridge, target: string): Promise<string> {
25
+ const normalized = normalizeAppUrlTarget(target)
26
+ const result: unknown = await bridge.send({
27
+ type: 'call',
28
+ path: 'SootSim.bridges.test.openDeepLink',
29
+ args: [normalized],
30
+ })
31
+ if (
32
+ result !== null &&
33
+ typeof result === 'object' &&
34
+ Reflect.get(result, 'ok') === false
35
+ ) {
36
+ const error = Reflect.get(result, 'error')
37
+ throw new Error(typeof error === 'string' ? error : 'deep-link dispatch failed')
38
+ }
39
+ return normalized
40
+ }
@@ -0,0 +1,359 @@
1
+ // untrusted JSON -> bridge-contract argument. one owner for that conversion, so
2
+ // every transport in front of a sim (the RNX Cloud sim service's HTTP and
3
+ // WebSocket routes) validates its request bodies here rather than growing a
4
+ // second reader for the same shapes.
5
+
6
+ import type {
7
+ ResetOptions,
8
+ SemanticWaitOptions,
9
+ SimSemanticQuery,
10
+ SimSemanticResolveSelector,
11
+ SimSemanticSelector,
12
+ SimStateOptions,
13
+ } from './bridge-contract'
14
+ import type { SimPerformStep, SimScreenshotOptions } from './sim-client'
15
+
16
+ /** the caller sent a value this bridge argument cannot be read from. */
17
+ export class BridgeInputError extends Error {
18
+ constructor(message: string) {
19
+ super(message)
20
+ this.name = 'BridgeInputError'
21
+ }
22
+ }
23
+
24
+ export const SEMANTIC_WAIT_MAX_TIMEOUT_MS = 30_000
25
+
26
+ function validationFailure(path: string, expectation: string): never {
27
+ throw new BridgeInputError(`${path} ${expectation}`)
28
+ }
29
+
30
+ function isRecordValue(value: unknown): value is Record<string, unknown> {
31
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
32
+ }
33
+
34
+ function recordValue(value: unknown, path: string): Record<string, unknown> {
35
+ if (!isRecordValue(value)) {
36
+ validationFailure(path, 'must be an object')
37
+ }
38
+ return value
39
+ }
40
+
41
+ function stringValue(value: unknown, path: string): string {
42
+ if (typeof value !== 'string') validationFailure(path, 'must be a string')
43
+ return value
44
+ }
45
+
46
+ function numberValue(value: unknown, path: string): number {
47
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
48
+ validationFailure(path, 'must be a finite number')
49
+ }
50
+ return value
51
+ }
52
+
53
+ function booleanValue(value: unknown, path: string): boolean {
54
+ if (typeof value !== 'boolean') validationFailure(path, 'must be a boolean')
55
+ return value
56
+ }
57
+
58
+ function optionalNumber(
59
+ record: Record<string, unknown>,
60
+ key: string,
61
+ path: string,
62
+ ): number | undefined {
63
+ return record[key] === undefined
64
+ ? undefined
65
+ : numberValue(record[key], `${path}.${key}`)
66
+ }
67
+
68
+ function optionalBoolean(
69
+ record: Record<string, unknown>,
70
+ key: string,
71
+ path: string,
72
+ ): boolean | undefined {
73
+ return record[key] === undefined
74
+ ? undefined
75
+ : booleanValue(record[key], `${path}.${key}`)
76
+ }
77
+
78
+ function semanticWaitTimeout(
79
+ record: Record<string, unknown>,
80
+ path: string,
81
+ ): number | undefined {
82
+ const timeoutMs = optionalNumber(record, 'timeoutMs', path)
83
+ if (timeoutMs !== undefined && timeoutMs > SEMANTIC_WAIT_MAX_TIMEOUT_MS) {
84
+ validationFailure(
85
+ `${path}.timeoutMs`,
86
+ `must be at most ${SEMANTIC_WAIT_MAX_TIMEOUT_MS}`,
87
+ )
88
+ }
89
+ return timeoutMs
90
+ }
91
+
92
+ export function parsePerformStep(value: unknown, index: number): SimPerformStep {
93
+ const path = `perform.steps[${index}]`
94
+ const step = recordValue(value, path)
95
+ const type = stringValue(step.type, `${path}.type`)
96
+ switch (type) {
97
+ case 'touchDown':
98
+ case 'touchMove':
99
+ case 'touchUp': {
100
+ const pointerId = optionalNumber(step, 'pointerId', path)
101
+ return {
102
+ type,
103
+ x: numberValue(step.x, `${path}.x`),
104
+ y: numberValue(step.y, `${path}.y`),
105
+ ...(pointerId === undefined ? {} : { pointerId }),
106
+ }
107
+ }
108
+ case 'touchCancel': {
109
+ const pointerId = optionalNumber(step, 'pointerId', path)
110
+ return { type, ...(pointerId === undefined ? {} : { pointerId }) }
111
+ }
112
+ case 'tap':
113
+ return {
114
+ type,
115
+ x: numberValue(step.x, `${path}.x`),
116
+ y: numberValue(step.y, `${path}.y`),
117
+ }
118
+ case 'tapId':
119
+ return { type, id: stringValue(step.id, `${path}.id`) }
120
+ case 'tapText':
121
+ return { type, text: stringValue(step.text, `${path}.text`) }
122
+ case 'doubleTap': {
123
+ const gapMs = optionalNumber(step, 'gapMs', path)
124
+ return {
125
+ type,
126
+ x: numberValue(step.x, `${path}.x`),
127
+ y: numberValue(step.y, `${path}.y`),
128
+ ...(gapMs === undefined ? {} : { gapMs }),
129
+ }
130
+ }
131
+ case 'longPress': {
132
+ const durationMs = optionalNumber(step, 'durationMs', path)
133
+ return {
134
+ type,
135
+ x: numberValue(step.x, `${path}.x`),
136
+ y: numberValue(step.y, `${path}.y`),
137
+ ...(durationMs === undefined ? {} : { durationMs }),
138
+ }
139
+ }
140
+ case 'drag': {
141
+ const steps = optionalNumber(step, 'steps', path)
142
+ const stepMs = optionalNumber(step, 'stepMs', path)
143
+ return {
144
+ type,
145
+ fromX: numberValue(step.fromX, `${path}.fromX`),
146
+ fromY: numberValue(step.fromY, `${path}.fromY`),
147
+ toX: numberValue(step.toX, `${path}.toX`),
148
+ toY: numberValue(step.toY, `${path}.toY`),
149
+ ...(steps === undefined ? {} : { steps }),
150
+ ...(stepMs === undefined ? {} : { stepMs }),
151
+ }
152
+ }
153
+ case 'pinch': {
154
+ const steps = optionalNumber(step, 'steps', path)
155
+ const stepMs = optionalNumber(step, 'stepMs', path)
156
+ return {
157
+ type,
158
+ fromX1: numberValue(step.fromX1, `${path}.fromX1`),
159
+ fromY1: numberValue(step.fromY1, `${path}.fromY1`),
160
+ fromX2: numberValue(step.fromX2, `${path}.fromX2`),
161
+ fromY2: numberValue(step.fromY2, `${path}.fromY2`),
162
+ toX1: numberValue(step.toX1, `${path}.toX1`),
163
+ toY1: numberValue(step.toY1, `${path}.toY1`),
164
+ toX2: numberValue(step.toX2, `${path}.toX2`),
165
+ toY2: numberValue(step.toY2, `${path}.toY2`),
166
+ ...(steps === undefined ? {} : { steps }),
167
+ ...(stepMs === undefined ? {} : { stepMs }),
168
+ }
169
+ }
170
+ case 'scroll': {
171
+ const x = optionalNumber(step, 'x', path)
172
+ const y = optionalNumber(step, 'y', path)
173
+ const animated = optionalBoolean(step, 'animated', path)
174
+ return {
175
+ type,
176
+ id: stringValue(step.id, `${path}.id`),
177
+ ...(x === undefined ? {} : { x }),
178
+ ...(y === undefined ? {} : { y }),
179
+ ...(animated === undefined ? {} : { animated }),
180
+ }
181
+ }
182
+ case 'type':
183
+ return { type, text: stringValue(step.text, `${path}.text`) }
184
+ case 'key':
185
+ return { type, key: stringValue(step.key, `${path}.key`) }
186
+ case 'dismissKeyboard':
187
+ return { type }
188
+ case 'wait':
189
+ return { type, ms: numberValue(step.ms, `${path}.ms`) }
190
+ case 'waitFor': {
191
+ const timeoutMs = semanticWaitTimeout(step, path)
192
+ const selector =
193
+ step.testID === undefined
194
+ ? { id: stringValue(step.id, `${path}.id`) }
195
+ : { testID: stringValue(step.testID, `${path}.testID`) }
196
+ return {
197
+ type,
198
+ ...selector,
199
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
200
+ }
201
+ }
202
+ default:
203
+ return validationFailure(`${path}.type`, 'is not a supported perform step')
204
+ }
205
+ }
206
+
207
+ export function parseStateOptions(value: unknown): SimStateOptions {
208
+ const input = recordValue(value, 'state')
209
+ const route = optionalBoolean(input, 'route', 'state')
210
+ const errors = optionalBoolean(input, 'errors', 'state')
211
+ const depth = optionalNumber(input, 'depth', 'state')
212
+ const errorLimit = optionalNumber(input, 'errorLimit', 'state')
213
+ const layers =
214
+ input.layers === undefined ? undefined : stringValue(input.layers, 'state.layers')
215
+ if (
216
+ layers !== undefined &&
217
+ layers !== 'full' &&
218
+ layers !== 'tenant' &&
219
+ layers !== 'shell'
220
+ ) {
221
+ return validationFailure('state.layers', 'must be full, tenant, or shell')
222
+ }
223
+ return {
224
+ screenshot: optionalBoolean(input, 'screenshot', 'state') ?? true,
225
+ tree: optionalBoolean(input, 'tree', 'state') ?? true,
226
+ ...(route === undefined ? {} : { route }),
227
+ ...(errors === undefined ? {} : { errors }),
228
+ ...(depth === undefined ? {} : { depth }),
229
+ ...(layers === undefined ? {} : { layers }),
230
+ ...(errorLimit === undefined ? {} : { errorLimit }),
231
+ }
232
+ }
233
+
234
+ function parseSemanticSelector(value: unknown, path: string): SimSemanticSelector {
235
+ const selector = recordValue(value, path)
236
+ if (selector.testID !== undefined) {
237
+ return { testID: stringValue(selector.testID, `${path}.testID`) }
238
+ }
239
+ if (selector.text !== undefined) {
240
+ return { text: stringValue(selector.text, `${path}.text`) }
241
+ }
242
+ if (selector.role !== undefined) {
243
+ return { role: stringValue(selector.role, `${path}.role`) }
244
+ }
245
+ if (selector.type !== undefined) {
246
+ return { type: stringValue(selector.type, `${path}.type`) }
247
+ }
248
+ if (selector.pressable !== undefined) {
249
+ if (booleanValue(selector.pressable, `${path}.pressable`) !== true) {
250
+ return validationFailure(`${path}.pressable`, 'must be true')
251
+ }
252
+ return { pressable: true }
253
+ }
254
+ if (selector.visible !== undefined) {
255
+ if (booleanValue(selector.visible, `${path}.visible`) !== true) {
256
+ return validationFailure(`${path}.visible`, 'must be true')
257
+ }
258
+ return { visible: true }
259
+ }
260
+ return validationFailure(
261
+ path,
262
+ 'must select testID, text, role, type, pressable, or visible',
263
+ )
264
+ }
265
+
266
+ export function parseSemanticQuery(value: unknown): SimSemanticQuery {
267
+ const query = recordValue(value, 'query')
268
+ const kind = stringValue(query.kind, 'query.kind')
269
+ if (kind === 'count') return { kind }
270
+ if (kind === 'tree') {
271
+ const depth = optionalNumber(query, 'depth', 'query')
272
+ return { kind, ...(depth === undefined ? {} : { depth }) }
273
+ }
274
+ if (kind === 'find') {
275
+ return { kind, selector: parseSemanticSelector(query.selector, 'query.selector') }
276
+ }
277
+ return validationFailure('query.kind', 'must be tree, find, or count')
278
+ }
279
+
280
+ export function parseSemanticResolveSelector(value: unknown): SimSemanticResolveSelector {
281
+ const selector = recordValue(value, 'selector')
282
+ if (selector.testID !== undefined) {
283
+ return { testID: stringValue(selector.testID, 'selector.testID') }
284
+ }
285
+ if (selector.text !== undefined) {
286
+ return { text: stringValue(selector.text, 'selector.text') }
287
+ }
288
+ return validationFailure('selector', 'must select testID or text')
289
+ }
290
+
291
+ export function parseSemanticWaitOptions(value: unknown): SemanticWaitOptions {
292
+ const input = recordValue(value, 'waitFor')
293
+ const condition = recordValue(input.condition, 'waitFor.condition')
294
+ const type = stringValue(condition.type, 'waitFor.condition.type')
295
+ const timeoutMs = semanticWaitTimeout(input, 'waitFor')
296
+ if (type === 'ready') {
297
+ return {
298
+ condition: { type },
299
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
300
+ }
301
+ }
302
+ if (type !== 'present' && type !== 'absent') {
303
+ return validationFailure(
304
+ 'waitFor.condition.type',
305
+ 'must be ready, present, or absent',
306
+ )
307
+ }
308
+ return {
309
+ condition: {
310
+ type,
311
+ testID: stringValue(condition.testID, 'waitFor.condition.testID'),
312
+ },
313
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
314
+ }
315
+ }
316
+
317
+ export function parseResetOptions(value: unknown): ResetOptions {
318
+ const input = recordValue(value, 'reset')
319
+ const strategy =
320
+ input.strategy === undefined ? 'data' : stringValue(input.strategy, 'reset.strategy')
321
+ if (strategy !== 'data' && strategy !== 'full') {
322
+ return validationFailure('reset.strategy', 'must be data or full')
323
+ }
324
+ return {
325
+ strategy,
326
+ relaunch: optionalBoolean(input, 'relaunch', 'reset') ?? true,
327
+ }
328
+ }
329
+
330
+ export function parseScreenshotOptions(value: unknown): SimScreenshotOptions {
331
+ const input = recordValue(value, 'screenshot')
332
+ const layers =
333
+ input.layers === undefined
334
+ ? undefined
335
+ : stringValue(input.layers, 'screenshot.layers')
336
+ if (
337
+ layers !== undefined &&
338
+ layers !== 'full' &&
339
+ layers !== 'tenant' &&
340
+ layers !== 'shell'
341
+ ) {
342
+ return validationFailure('screenshot.layers', 'must be full, tenant, or shell')
343
+ }
344
+ const crop =
345
+ input.crop === undefined ? undefined : recordValue(input.crop, 'screenshot.crop')
346
+ return {
347
+ ...(layers === undefined ? {} : { layers }),
348
+ ...(crop === undefined
349
+ ? {}
350
+ : {
351
+ crop: {
352
+ x: numberValue(crop.x, 'screenshot.crop.x'),
353
+ y: numberValue(crop.y, 'screenshot.crop.y'),
354
+ w: numberValue(crop.w, 'screenshot.crop.w'),
355
+ h: numberValue(crop.h, 'screenshot.crop.h'),
356
+ },
357
+ }),
358
+ }
359
+ }
@@ -25,13 +25,18 @@ export type BridgeCommandType =
25
25
  | 'close'
26
26
  | 'flowStatus'
27
27
  | 'perform'
28
+ | 'waitFor'
28
29
  | 'state'
30
+ | 'query'
31
+ | 'resolve'
29
32
  | 'reset'
30
33
  | 'camera'
31
34
 
32
35
  export interface WsCommand {
33
36
  id: string | number
34
37
  type: BridgeCommandType
38
+ // local daemon routing; cloud transport strips it before execution.
39
+ simId?: string
35
40
  // flowStatus — live flow-runner progress payload (see flow-run-store)
36
41
  status?: Record<string, unknown>
37
42
  code?: string
@@ -57,8 +62,13 @@ export interface WsCommand {
57
62
  // perform — ordered batch executed inside the page, no round trips
58
63
  steps?: PerformStep[]
59
64
  performOptions?: PerformOptions
65
+ // waitFor — one engine-side polling loop and one transport round trip
66
+ waitForOptions?: SemanticWaitOptions
60
67
  // state — compound screenshot + tree + route + errors
61
68
  stateOptions?: SimStateOptions
69
+ // query / resolve — structured visible-tree reads and selector geometry
70
+ query?: SimSemanticQuery
71
+ selector?: SimSemanticResolveSelector
62
72
  // reset — two-tier app state wipe
63
73
  resetOptions?: ResetOptions
64
74
  // camera — fixture injection into the host camera pipeline
@@ -158,6 +168,67 @@ export interface PerformResult {
158
168
  error?: string
159
169
  }
160
170
 
171
+ // ---------------------------------------------------------------------------
172
+ // waitFor — engine-side semantic condition polling
173
+ // ---------------------------------------------------------------------------
174
+
175
+ export type SemanticWaitCondition =
176
+ | { type: 'ready' }
177
+ | { type: 'present'; testID: string }
178
+ | { type: 'absent'; testID: string }
179
+
180
+ export interface SemanticWaitOptions {
181
+ condition: SemanticWaitCondition
182
+ timeoutMs?: number
183
+ }
184
+
185
+ export interface SemanticReadyProbe {
186
+ flag: unknown
187
+ at: number
188
+ nodes: number
189
+ targets: number
190
+ liveFrameActive: boolean
191
+ liveFrameChannels: number
192
+ liveFramePublishes: number
193
+ errors: number
194
+ loadingText: string
195
+ externalReady: boolean | null
196
+ externalStatus: string
197
+ externalError: string
198
+ suppressedEntryError: string
199
+ }
200
+
201
+ export const SEMANTIC_READY_CONTENT_NODE_FLOOR = 100
202
+ export const SEMANTIC_READY_NODE_STABLE_MS = 750
203
+ export const SEMANTIC_READY_POLL_MS = 150
204
+
205
+ export function readyProbeHasContent(
206
+ probe: Pick<SemanticReadyProbe, 'nodes' | 'targets'> &
207
+ Partial<Pick<SemanticReadyProbe, 'liveFrameActive'>>,
208
+ ): boolean {
209
+ return (
210
+ probe.liveFrameActive ||
211
+ probe.targets > 0 ||
212
+ probe.nodes >= SEMANTIC_READY_CONTENT_NODE_FLOOR
213
+ )
214
+ }
215
+
216
+ export function readyProbeHasTargetContent(
217
+ probe: Pick<SemanticReadyProbe, 'targets'> &
218
+ Partial<Pick<SemanticReadyProbe, 'liveFrameActive'>>,
219
+ ): boolean {
220
+ return probe.liveFrameActive || probe.targets > 0
221
+ }
222
+
223
+ export interface SemanticWaitResult {
224
+ matched: boolean
225
+ elapsedMs: number
226
+ polls: number
227
+ probe?: SemanticReadyProbe
228
+ node?: SimSemanticNode
229
+ error?: string
230
+ }
231
+
161
232
  // ---------------------------------------------------------------------------
162
233
  // state — one round-trip situational awareness
163
234
  // ---------------------------------------------------------------------------
@@ -178,13 +249,62 @@ export interface SimStateOptions {
178
249
  export interface SimStateResult {
179
250
  // png data uri, same encoding the `screenshot` command returns
180
251
  screenshot?: unknown
181
- tree?: unknown
252
+ tree?: SimSemanticNode[]
182
253
  route?: { url?: string; screen?: string | null; stack?: string[] } | null
183
254
  recentErrors?: Array<{ level: string; text: string; at?: number }>
184
255
  keyboard?: { visible: boolean; mode?: string | null }
185
256
  capturedAt: number
186
257
  }
187
258
 
259
+ // ---------------------------------------------------------------------------
260
+ // semantic tree — pruned visible structure shared by local and cloud runtimes
261
+ // ---------------------------------------------------------------------------
262
+
263
+ export interface SimSemanticGeometry {
264
+ x: number
265
+ y: number
266
+ width: number
267
+ height: number
268
+ }
269
+
270
+ export interface SimSemanticNode {
271
+ nodeId: number
272
+ type: string
273
+ testID?: string
274
+ text?: string
275
+ role?: string
276
+ label?: string
277
+ pressable?: true
278
+ geometry: SimSemanticGeometry
279
+ children?: SimSemanticNode[]
280
+ }
281
+
282
+ export type SimSemanticSelector =
283
+ | { testID: string }
284
+ | { text: string }
285
+ | { role: string }
286
+ | { type: string }
287
+ | { pressable: true }
288
+ | { visible: true }
289
+
290
+ export type SimSemanticQuery =
291
+ | { kind: 'tree'; depth?: number }
292
+ | { kind: 'find'; selector: SimSemanticSelector }
293
+ | { kind: 'count' }
294
+
295
+ export interface SimSemanticQueryResult {
296
+ nodes: SimSemanticNode[]
297
+ total: number
298
+ }
299
+
300
+ export type SimSemanticResolveSelector = { testID: string } | { text: string }
301
+
302
+ export interface SimSemanticResolveResult {
303
+ match: SimSemanticNode
304
+ target: SimSemanticNode
305
+ point: { x: number; y: number }
306
+ }
307
+
188
308
  // ---------------------------------------------------------------------------
189
309
  // reset — two-tier app state wipe
190
310
  // ---------------------------------------------------------------------------