rnxsim 0.1.366 → 0.1.368
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -2
- package/cli/bin.ts +18 -0
- package/cli/bridge-flow-runner.ts +14 -151
- package/cli/commands/assert.ts +1 -1
- package/cli/commands/box/checkout-plane.ts +126 -0
- package/cli/commands/box/rnx-command.ts +68 -0
- package/cli/commands/box.ts +200 -0
- package/cli/commands/control.ts +12 -32
- package/cli/commands/do-chain.ts +412 -0
- package/cli/commands/flow.ts +0 -10
- package/cli/commands/inspect/core.ts +302 -725
- package/cli/commands/inspect/describe.ts +3 -76
- package/cli/commands/inspect/find.ts +3 -6
- package/cli/commands/inspect/get-layout.ts +14 -1
- package/cli/commands/inspect/resolve-target.ts +41 -47
- package/cli/commands/inspect/shared.ts +28 -123
- package/cli/commands/inspect/tree.ts +6 -15
- package/cli/commands/inspect/wait-ready.ts +2 -2
- package/cli/commands/inspect/wait-selector.ts +4 -8
- package/cli/commands/inspect.ts +23 -17
- package/cli/commands/install-desktop.ts +44 -7
- package/cli/commands/maestro-generate.ts +0 -2
- package/cli/commands/platform.ts +68 -0
- package/cli/commands/state.ts +5 -2
- package/cli/outbound-endpoints.ts +6 -1
- package/cli/parse-args.ts +13 -2
- package/cli/ws-bridge.ts +30 -2
- package/detox/index.ts +34 -17
- package/dist-cli/bin.js +5 -5
- package/dist-cli/chunks/{agent-SO6IOPRK.js → agent-GXCQMYV4.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-PMYCB2KO.js → agent-wrapper-Y3PQAL6I.js} +2 -2
- package/dist-cli/chunks/{app-fonts-LULQGFY7.js → app-fonts-MDD3W4VN.js} +2 -2
- package/dist-cli/chunks/{assert-WGNCD4ZU.js → assert-BSRQBRH7.js} +3 -3
- package/dist-cli/chunks/{auth-G7CCHH3M.js → auth-SLPHDPCF.js} +2 -2
- package/dist-cli/chunks/{beta-N4LXL5TO.js → beta-R4IZ5SWO.js} +2 -2
- package/dist-cli/chunks/box-2NQYYY3B.js +1709 -0
- package/dist-cli/chunks/{camera-4JHCYBQS.js → camera-ER66WWAX.js} +2 -2
- package/dist-cli/chunks/chunk-2YK5J2D2.js +24 -0
- package/dist-cli/chunks/{chunk-I6KIM6PN.js → chunk-3MPFC3DS.js} +1 -1
- package/dist-cli/chunks/{chunk-77ZHQJZR.js → chunk-3S2J6IVC.js} +2 -2
- package/dist-cli/chunks/chunk-3XMEIZEV.js +4 -0
- package/dist-cli/chunks/chunk-42TLK75A.js +497 -0
- package/dist-cli/chunks/{chunk-OW5XHAA4.js → chunk-4FLQYRGN.js} +1 -1
- package/dist-cli/chunks/{chunk-EFR5DVNP.js → chunk-4VA2ISWX.js} +1 -1
- package/dist-cli/chunks/chunk-57LITN73.js +7 -0
- package/dist-cli/chunks/{chunk-EQK5MJCU.js → chunk-7B22IPSC.js} +2 -2
- package/dist-cli/chunks/{chunk-EGAGZHO6.js → chunk-7V3S2DY2.js} +1 -1
- package/dist-cli/chunks/{chunk-IZKHOM37.js → chunk-AUPJX4KI.js} +1 -1
- package/dist-cli/chunks/{chunk-2S7BKUP2.js → chunk-BWJOLJOK.js} +2 -2
- package/dist-cli/chunks/{chunk-CN6XYYEA.js → chunk-CKNNO6ZG.js} +27 -27
- package/dist-cli/chunks/{chunk-MGDSJZ37.js → chunk-DJSPNOF6.js} +2 -2
- package/dist-cli/chunks/{chunk-YXXKSL6R.js → chunk-E6XYIIJK.js} +1 -1
- package/dist-cli/chunks/{chunk-ONORMKWX.js → chunk-ECXDCED5.js} +2 -2
- package/dist-cli/chunks/chunk-EHA65UVJ.js +5 -0
- package/dist-cli/chunks/chunk-ESVUVW3R.js +32 -0
- package/dist-cli/chunks/{chunk-XJU2QIRY.js → chunk-FDAFSRQE.js} +2 -2
- package/dist-cli/chunks/{chunk-NM2L2QV4.js → chunk-FSPZY4AI.js} +2 -2
- package/dist-cli/chunks/chunk-FVODUDPS.js +4 -0
- package/dist-cli/chunks/{chunk-H23KWIKA.js → chunk-GFWUIGGY.js} +2 -2
- package/dist-cli/chunks/{chunk-CTG7HBCA.js → chunk-GS5VUDCF.js} +1 -1
- package/dist-cli/chunks/{chunk-ORWP2E7R.js → chunk-GXHCJRKE.js} +2 -2
- package/dist-cli/chunks/{chunk-TTASWLKM.js → chunk-HKQXGCQT.js} +1 -1
- package/dist-cli/chunks/{chunk-5OK3LCEJ.js → chunk-IXRVFQMT.js} +2 -2
- package/dist-cli/chunks/{chunk-TIAEYO6I.js → chunk-JFGW75UL.js} +2 -2
- package/dist-cli/chunks/{chunk-RYIMSKDO.js → chunk-JHYXRORU.js} +2 -2
- package/dist-cli/chunks/chunk-LAKWOJ5Y.js +5 -0
- package/dist-cli/chunks/{chunk-GAJQSITE.js → chunk-LPAOOJIW.js} +1 -1
- package/dist-cli/chunks/chunk-M7COA4BE.js +312 -0
- package/dist-cli/chunks/{chunk-6PL2CCGP.js → chunk-MCO7V4VN.js} +2 -2
- package/dist-cli/chunks/{chunk-YGMEDJKJ.js → chunk-MGOAQHMO.js} +2 -2
- package/dist-cli/chunks/chunk-NERY6HGZ.js +4 -0
- package/dist-cli/chunks/chunk-NGWUGNPV.js +4 -0
- package/dist-cli/chunks/{chunk-ZMJ75EDG.js → chunk-NPSPWMBO.js} +2 -2
- package/dist-cli/chunks/{chunk-2UXU53MG.js → chunk-OXCDE3DD.js} +1 -1
- package/dist-cli/chunks/{chunk-ZLVRYXZ7.js → chunk-OZJMWTGA.js} +1 -1
- package/dist-cli/chunks/{chunk-HT6GFZOZ.js → chunk-PNBQBBG5.js} +1 -1
- package/dist-cli/chunks/{chunk-JHXETDVC.js → chunk-Q65KH25G.js} +1 -1
- package/dist-cli/chunks/{chunk-NQFJOCSF.js → chunk-QKOY7UW5.js} +2 -2
- package/dist-cli/chunks/{chunk-5DAIA77C.js → chunk-QMVBWZH2.js} +2 -2
- package/dist-cli/chunks/chunk-RRALQ4WM.js +38 -0
- package/dist-cli/chunks/{chunk-GXAB52K7.js → chunk-SHMLYTNH.js} +2 -2
- package/dist-cli/chunks/chunk-SSUPPM6K.js +130 -0
- package/dist-cli/chunks/chunk-T2MGLXY5.js +27 -0
- package/dist-cli/chunks/{chunk-ZJIS3CTW.js → chunk-T4ZZ26A2.js} +2 -2
- package/dist-cli/chunks/{chunk-YCCCPFWT.js → chunk-TC6DNQL6.js} +2 -2
- package/dist-cli/chunks/chunk-TE645QCO.js +423 -0
- package/dist-cli/chunks/{chunk-UCHW7ZHR.js → chunk-TOE5O6JE.js} +2 -2
- package/dist-cli/chunks/{chunk-N3UC7HIX.js → chunk-TPIPKXJL.js} +1 -1
- package/dist-cli/chunks/chunk-UBH2BWCY.js +4 -0
- package/dist-cli/chunks/{chunk-AFV32WZT.js → chunk-UXROQPIN.js} +3 -3
- package/dist-cli/chunks/{chunk-SA6XVI55.js → chunk-WF2CRNYF.js} +1 -1
- package/dist-cli/chunks/chunk-WFAFAGMG.js +4 -0
- package/dist-cli/chunks/{chunk-A5FXX5PJ.js → chunk-WJQB6AXZ.js} +1 -1
- package/dist-cli/chunks/{chunk-5NMCF5VF.js → chunk-WVVAYLWZ.js} +1 -1
- package/dist-cli/chunks/chunk-WXRDFUR7.js +4 -0
- package/dist-cli/chunks/{chunk-M3BNCF7E.js → chunk-X5MXQB3L.js} +2 -2
- package/dist-cli/chunks/{chunk-LN3RNW6X.js → chunk-YSHJ3LMQ.js} +2 -2
- package/dist-cli/chunks/{chunk-34IQM22N.js → chunk-YYPTSF6W.js} +15 -15
- package/dist-cli/chunks/{cleanup-KRIATKFH.js → cleanup-WHGLBAJ2.js} +2 -2
- package/dist-cli/chunks/cli-version-DMXIXBIM.js +4 -0
- package/dist-cli/chunks/{compat-6PTQ3QEA.js → compat-MGBKGYGQ.js} +2 -2
- package/dist-cli/chunks/{config-ETGDUANO.js → config-TRDOMP4T.js} +2 -2
- package/dist-cli/chunks/connect-N4FV3DDV.js +4 -0
- package/dist-cli/chunks/control-5GRBZVYK.js +4 -0
- package/dist-cli/chunks/daemon-MBT5E3ZC.js +4 -0
- package/dist-cli/chunks/{debug-WIIB6ZKC.js → debug-AHEK43AJ.js} +3 -3
- package/dist-cli/chunks/{desktop-TD7Q5LNB.js → desktop-LDRIXOVV.js} +3 -3
- package/dist-cli/chunks/{detox-7GNR33VM.js → detox-6HAKYISO.js} +2 -2
- package/dist-cli/chunks/{device-QOR3PGUF.js → device-QM3S7OVU.js} +2 -2
- package/dist-cli/chunks/{diagnose-RRCBLP6E.js → diagnose-WDP3CVIX.js} +2 -2
- package/dist-cli/chunks/{disk-cleanup-MJUGJPXT.js → disk-cleanup-JRT5MKFW.js} +2 -2
- package/dist-cli/chunks/do-chain-7FDMO56C.js +5 -0
- package/dist-cli/chunks/drivers-AJL2EVXH.js +4 -0
- package/dist-cli/chunks/env-YIBOMB24.js +4 -0
- package/dist-cli/chunks/{film-D7Y64LID.js → film-R3EQADMG.js} +3 -3
- package/dist-cli/chunks/flow-BDAA6BWL.js +4 -0
- package/dist-cli/chunks/help-5EJD6LJF.js +4 -0
- package/dist-cli/chunks/{hidden-runtime-alias-JGURVR2Z.js → hidden-runtime-alias-6M4UERRQ.js} +2 -2
- package/dist-cli/chunks/{home-paths-4ICFIGXO.js → home-paths-QJNOHZJO.js} +2 -2
- package/dist-cli/chunks/inspect-S3LR3R2A.js +4 -0
- package/dist-cli/chunks/install-desktop-GXE6K7BJ.js +4 -0
- package/dist-cli/chunks/{login-RU33QR2G.js → login-ISWDW4WU.js} +2 -2
- package/dist-cli/chunks/{logout-2IUG3Y5J.js → logout-VNJX2IEW.js} +2 -2
- package/dist-cli/chunks/{maestro-2MXTGG7I.js → maestro-SQS2OS2D.js} +3 -3
- package/dist-cli/chunks/{maestro-generate-2PYIFGLR.js → maestro-generate-GWVPISG5.js} +5 -5
- package/dist-cli/chunks/{mode-3DAPG3HF.js → mode-3URDGBZB.js} +2 -2
- package/dist-cli/chunks/{optional-demo-registry-BWSDJF2Z.js → optional-demo-registry-L3LRBQHG.js} +2 -2
- package/dist-cli/chunks/{perf-2TKPWB4N.js → perf-XLPC4UKR.js} +2 -2
- package/dist-cli/chunks/{permissions-5CSMHYNA.js → permissions-QRHWVMDP.js} +2 -2
- package/dist-cli/chunks/platform-F47URZNB.js +6 -0
- package/dist-cli/chunks/privacy-BF23T67T.js +4 -0
- package/dist-cli/chunks/{record-I2HJCFP3.js → record-2DN3OUE5.js} +3 -3
- package/dist-cli/chunks/{report-issue-A4FZWBOC.js → report-issue-D3BQPSLU.js} +2 -2
- package/dist-cli/chunks/reset-T5CNZ7D3.js +4 -0
- package/dist-cli/chunks/resolve-target-KAAS274S.js +4 -0
- package/dist-cli/chunks/runtime-AQJDBR3X.js +4 -0
- package/dist-cli/chunks/{screenshot-command-UFW5BFHP.js → screenshot-command-RK7IJHVK.js} +3 -3
- package/dist-cli/chunks/{screenshot-layers-5BIAL4YN.js → screenshot-layers-DP2QKSMN.js} +2 -2
- package/dist-cli/chunks/{screenshots-capture-EHYS3BV2.js → screenshots-capture-MA7P3FUT.js} +2 -2
- package/dist-cli/chunks/{serve-O4FDM3AC.js → serve-UBXNGLNP.js} +3 -3
- package/dist-cli/chunks/setup-GH7OW4GC.js +4 -0
- package/dist-cli/chunks/sim-client-VTKFEDJX.js +4 -0
- package/dist-cli/chunks/{skills-ZK55SY32.js → skills-HPK25R4B.js} +2 -2
- package/dist-cli/chunks/state-273XJPUE.js +14 -0
- package/dist-cli/chunks/{storage-WP33ISEM.js → storage-RTDFWIY7.js} +2 -2
- package/dist-cli/chunks/store-DLKNNM4L.js +4 -0
- package/dist-cli/chunks/telemetry-7CWDXEOO.js +4 -0
- package/dist-cli/chunks/{timeline-DKGW5S4I.js → timeline-IF2PGK7C.js} +2 -2
- package/dist-cli/chunks/{upgrade-LL7G4B7G.js → upgrade-R74NVHKG.js} +2 -2
- package/dist-cli/chunks/upload-3JFFVCZ4.js +4 -0
- package/dist-cli/chunks/{version-YLWR5G4F.js → version-RTFCYQZ7.js} +2 -2
- package/dist-cli/chunks/{web-QELZ4CP6.js → web-35SSKBRU.js} +2 -2
- package/dist-cli/chunks/welcome-N5V352SE.js +5 -0
- package/dist-cli/chunks/{what-happened-MIOMZWM5.js → what-happened-WXTP6EPR.js} +2 -2
- package/dist-cli/chunks/ws-bridge-JXQUAWFA.js +4 -0
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +336 -0
- package/dist-lib/bridge-contract-input.mjs +303 -0
- package/dist-lib/bridge-contract.cjs +29 -1
- package/dist-lib/bridge-contract.mjs +19 -0
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +32 -18
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +5 -1
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +214 -594
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +273 -657
- package/dist-lib/sdk.mjs +271 -652
- package/dist-lib/skills.cjs +190 -287
- package/dist-lib/vite.cjs +1 -1
- package/package.json +9 -6
- package/skills/contrast/SKILL.md +3 -0
- package/skills/rnx-debug/SKILL.md +8 -0
- package/skills/rnx-test/SKILL.md +21 -0
- package/src/bridge-contract-input.ts +359 -0
- package/src/bridge-contract.ts +121 -1
- package/src/host/bridge-host.ts +7 -12
- package/src/index.ts +7 -0
- package/src/sim-client.ts +12 -3
- package/dist-cli/chunks/chunk-3X6IBUBX.js +0 -38
- package/dist-cli/chunks/chunk-7B37IZBN.js +0 -15
- package/dist-cli/chunks/chunk-DMPF2D24.js +0 -4
- package/dist-cli/chunks/chunk-DW37EVX5.js +0 -767
- package/dist-cli/chunks/chunk-DWFIH7CB.js +0 -32
- package/dist-cli/chunks/chunk-GUI3PDH2.js +0 -27
- package/dist-cli/chunks/chunk-KLF3K6ZZ.js +0 -4
- package/dist-cli/chunks/chunk-M5QCGLXR.js +0 -944
- package/dist-cli/chunks/chunk-M6SKQGAN.js +0 -5
- package/dist-cli/chunks/chunk-NAYLPOC5.js +0 -24
- package/dist-cli/chunks/chunk-VIND4SP3.js +0 -124
- package/dist-cli/chunks/chunk-XXNLYR64.js +0 -5
- package/dist-cli/chunks/chunk-ZFM24DOX.js +0 -4
- package/dist-cli/chunks/cli-version-SYGNHGFR.js +0 -4
- package/dist-cli/chunks/control-RBTNOLQM.js +0 -4
- package/dist-cli/chunks/daemon-N3SC5DK6.js +0 -4
- package/dist-cli/chunks/drivers-W6E3BZH4.js +0 -4
- package/dist-cli/chunks/flow-7QAE2V5Y.js +0 -4
- package/dist-cli/chunks/help-NLD6KWVG.js +0 -4
- package/dist-cli/chunks/inspect-C347BG52.js +0 -4
- package/dist-cli/chunks/install-desktop-YR6TWZYK.js +0 -4
- package/dist-cli/chunks/privacy-PWWC2BXO.js +0 -4
- package/dist-cli/chunks/reset-Y3PBPFYA.js +0 -4
- package/dist-cli/chunks/runtime-IC6NVVGA.js +0 -4
- package/dist-cli/chunks/setup-Y4QGSJBM.js +0 -4
- package/dist-cli/chunks/state-KFLAZNEY.js +0 -14
- package/dist-cli/chunks/store-VEE22CAY.js +0 -4
- package/dist-cli/chunks/telemetry-PZV4BISJ.js +0 -4
- package/dist-cli/chunks/upload-Y4C3YZDK.js +0 -4
- package/dist-cli/chunks/welcome-ZPKA5PZJ.js +0 -5
- package/dist-lib/mcp.cjs +0 -673
- package/dist-lib/mcp.mjs +0 -641
- package/src/mcp.ts +0 -791
package/skills/contrast/SKILL.md
CHANGED
|
@@ -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
|
|
package/skills/rnx-test/SKILL.md
CHANGED
|
@@ -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,
|
|
@@ -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
|
+
}
|
package/src/bridge-contract.ts
CHANGED
|
@@ -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?:
|
|
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
|
// ---------------------------------------------------------------------------
|
package/src/host/bridge-host.ts
CHANGED
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
isReplacementModuleRequestUrl,
|
|
55
55
|
} from './replacement-module-handler.ts'
|
|
56
56
|
import { handleWebSocketProxyUpgrade } from './websocket-proxy.ts'
|
|
57
|
+
import type { WsCommand } from '../bridge-contract.ts'
|
|
57
58
|
|
|
58
59
|
const SOOTSIM_CROSS_ORIGIN_EMBEDDER_POLICY = 'require-corp'
|
|
59
60
|
|
|
@@ -92,17 +93,7 @@ export interface BridgeLockInfo {
|
|
|
92
93
|
expiresInMs: number
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
export
|
|
96
|
-
id: number
|
|
97
|
-
type: 'evaluate' | 'screenshot' | 'tap' | 'keyboard' | 'tree' | 'focus' | 'close'
|
|
98
|
-
code?: string
|
|
99
|
-
x?: number
|
|
100
|
-
y?: number
|
|
101
|
-
action?: string
|
|
102
|
-
text?: string
|
|
103
|
-
depth?: number
|
|
104
|
-
simId?: string
|
|
105
|
-
}
|
|
96
|
+
export type BridgeSimCommand = Omit<WsCommand, 'id'> & { id: number }
|
|
106
97
|
|
|
107
98
|
interface BridgeSimRegistrationMessage {
|
|
108
99
|
type: 'bridge:register'
|
|
@@ -173,7 +164,7 @@ interface BridgeCliLease {
|
|
|
173
164
|
}
|
|
174
165
|
|
|
175
166
|
// interactive commands change app state and must respect the lease.
|
|
176
|
-
// observational/lifecycle commands (evaluate/tree/screenshot/focus/call/close)
|
|
167
|
+
// observational/lifecycle commands (evaluate/tree/query/resolve/screenshot/focus/call/close)
|
|
177
168
|
// pass through without taking or checking a lease so cleanup never gets stuck
|
|
178
169
|
// behind a stale cli owner.
|
|
179
170
|
// `call` used to acquire a lease too, but most call paths are queries
|
|
@@ -2555,6 +2546,10 @@ export class SootSimBridgeHost {
|
|
|
2555
2546
|
return msg?.action === 'type' ? 'typed text' : 'used keyboard'
|
|
2556
2547
|
case 'tree':
|
|
2557
2548
|
return 'dumped tree'
|
|
2549
|
+
case 'query':
|
|
2550
|
+
return 'queried semantic tree'
|
|
2551
|
+
case 'resolve':
|
|
2552
|
+
return 'resolved semantic selector'
|
|
2558
2553
|
case 'focus':
|
|
2559
2554
|
return 'focused sim'
|
|
2560
2555
|
case 'close':
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,13 @@ export type {
|
|
|
27
27
|
ResetResult,
|
|
28
28
|
SimStateOptions,
|
|
29
29
|
SimStateResult,
|
|
30
|
+
SimSemanticGeometry,
|
|
31
|
+
SimSemanticNode,
|
|
32
|
+
SimSemanticQuery,
|
|
33
|
+
SimSemanticQueryResult,
|
|
34
|
+
SimSemanticResolveResult,
|
|
35
|
+
SimSemanticResolveSelector,
|
|
36
|
+
SimSemanticSelector,
|
|
30
37
|
} from './bridge-contract'
|
|
31
38
|
|
|
32
39
|
export { rnxPlugin } from './vite-plugin-one'
|
package/src/sim-client.ts
CHANGED
|
@@ -55,6 +55,9 @@ import type {
|
|
|
55
55
|
PerformStep,
|
|
56
56
|
ResetOptions,
|
|
57
57
|
ResetResult,
|
|
58
|
+
SimSemanticNode,
|
|
59
|
+
SimSemanticResolveResult,
|
|
60
|
+
SimSemanticResolveSelector,
|
|
58
61
|
SimStateOptions,
|
|
59
62
|
SimStateResult,
|
|
60
63
|
} from './bridge-contract'
|
|
@@ -386,12 +389,18 @@ export class SimClient implements WsBridge, InspectBridge {
|
|
|
386
389
|
return result ? [result as InteractiveNode] : []
|
|
387
390
|
}
|
|
388
391
|
|
|
389
|
-
async tree(depth = 5): Promise<
|
|
392
|
+
async tree(depth = 5): Promise<SimSemanticNode[]> {
|
|
390
393
|
const { tree } = await inspectTree(this, depth)
|
|
391
|
-
return tree
|
|
394
|
+
return Array.isArray(tree) ? tree : []
|
|
392
395
|
}
|
|
393
396
|
|
|
394
|
-
|
|
397
|
+
resolve(
|
|
398
|
+
selector: SimSemanticResolveSelector,
|
|
399
|
+
): Promise<SimSemanticResolveResult | null> {
|
|
400
|
+
return this.send({ type: 'resolve', selector })
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** compact visible semantic tree, formatted as the CLI prints it. */
|
|
395
404
|
describe(opts: { filter?: string; verbose?: boolean } = {}): Promise<DescribeResult> {
|
|
396
405
|
return inspectDescribe(this, {
|
|
397
406
|
describe: true,
|