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.
- package/README.md +14 -0
- package/cli/bin.ts +18 -0
- package/cli/bridge-flow-runner.ts +16 -153
- 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 +15 -76
- package/cli/commands/detox.ts +18 -1
- package/cli/commands/do-chain.ts +412 -0
- package/cli/commands/flow.ts +2 -11
- 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 +37 -18
- package/dist-cli/bin.js +5 -5
- package/dist-cli/chunks/{agent-BVMNQFKF.js → agent-EKKIAKP5.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-B6YFXGC3.js → agent-wrapper-MHLQUGA6.js} +2 -2
- package/dist-cli/chunks/{app-fonts-MEI6R6XR.js → app-fonts-HN6ADRTQ.js} +2 -2
- package/dist-cli/chunks/{assert-JGHI53HV.js → assert-IMR6QCN6.js} +3 -3
- package/dist-cli/chunks/{auth-STEOXUS7.js → auth-27LPMQNH.js} +2 -2
- package/dist-cli/chunks/{beta-HT3FLS5T.js → beta-6RXBB5AI.js} +2 -2
- package/dist-cli/chunks/box-DORLF3GP.js +1709 -0
- package/dist-cli/chunks/{camera-ZCLFSLON.js → camera-2G7JZ5DL.js} +2 -2
- package/dist-cli/chunks/{chunk-RI3NS77J.js → chunk-3QZJLNYY.js} +1 -1
- package/dist-cli/chunks/{chunk-FHWZPUGC.js → chunk-3VBVEDDA.js} +1 -1
- package/dist-cli/chunks/{chunk-EAFKG5R3.js → chunk-4BWGLTKU.js} +2 -2
- package/dist-cli/chunks/chunk-4XSNO374.js +423 -0
- package/dist-cli/chunks/{chunk-GZEUDDAW.js → chunk-5DGMFUZG.js} +2 -2
- package/dist-cli/chunks/{chunk-TKCGP3DD.js → chunk-6DBS5EGX.js} +2 -2
- package/dist-cli/chunks/{chunk-HIDS3PYC.js → chunk-6SO6I3D5.js} +2 -2
- package/dist-cli/chunks/chunk-75EFOPXE.js +32 -0
- package/dist-cli/chunks/{chunk-FIXRGPUT.js → chunk-A5AJFVOF.js} +1 -1
- package/dist-cli/chunks/{chunk-2SFMW5K6.js → chunk-AM2ODYAN.js} +1 -1
- package/dist-cli/chunks/{chunk-BGRGP3QR.js → chunk-AVOROGA3.js} +1 -1
- package/dist-cli/chunks/chunk-AXMMLO7D.js +130 -0
- package/dist-cli/chunks/{chunk-XXNPISER.js → chunk-BSEXEYNX.js} +2 -2
- package/dist-cli/chunks/{chunk-67ATAITF.js → chunk-CM6ZKZW7.js} +2 -2
- package/dist-cli/chunks/{chunk-MR3YPNUI.js → chunk-DNVABCEC.js} +2 -2
- package/dist-cli/chunks/{chunk-5AS7S6HY.js → chunk-E33A4AIR.js} +2 -2
- package/dist-cli/chunks/{chunk-DBJZUCFP.js → chunk-EFMOXYDD.js} +2 -2
- package/dist-cli/chunks/{chunk-ZTWUNK45.js → chunk-EV4P5UBP.js} +2 -2
- package/dist-cli/chunks/{chunk-EVBQDQVY.js → chunk-G5IUIGGC.js} +1 -1
- package/dist-cli/chunks/chunk-GMLOCV3E.js +312 -0
- package/dist-cli/chunks/{chunk-DQFOHIRF.js → chunk-GRN2FHVW.js} +1 -1
- package/dist-cli/chunks/{chunk-CYIE65QN.js → chunk-HFFXPUEP.js} +1 -1
- package/dist-cli/chunks/chunk-HG6T4OOR.js +4 -0
- package/dist-cli/chunks/{chunk-GK7PGYFH.js → chunk-HNT2OXPZ.js} +1 -1
- package/dist-cli/chunks/chunk-IPEURNLI.js +24 -0
- package/dist-cli/chunks/chunk-IR44SFBA.js +5 -0
- package/dist-cli/chunks/chunk-JMSLXZFW.js +4 -0
- package/dist-cli/chunks/{chunk-WGNDJARS.js → chunk-JPXQKLNH.js} +1 -1
- package/dist-cli/chunks/chunk-K3EC6JDE.js +4 -0
- package/dist-cli/chunks/chunk-KHX65HAU.js +4 -0
- package/dist-cli/chunks/{chunk-MACI34ST.js → chunk-LWOVKHRC.js} +2 -2
- package/dist-cli/chunks/{chunk-QIQL7Y5V.js → chunk-MQN5DWTI.js} +2 -2
- package/dist-cli/chunks/{chunk-5VHWQGGF.js → chunk-N3BXQEUF.js} +1 -1
- package/dist-cli/chunks/{chunk-O2ORLK6W.js → chunk-N5OSV26B.js} +2 -2
- package/dist-cli/chunks/chunk-PHNURIH3.js +4 -0
- package/dist-cli/chunks/{chunk-JRPU5TFI.js → chunk-PP2KNVXP.js} +3 -3
- package/dist-cli/chunks/{chunk-UR345KM3.js → chunk-PROKNJVY.js} +2 -2
- package/dist-cli/chunks/chunk-QXLLY4IM.js +7 -0
- package/dist-cli/chunks/{chunk-2CXHDTQ5.js → chunk-RNQTOGM7.js} +1 -1
- package/dist-cli/chunks/{chunk-M5KJCLUW.js → chunk-RPNKKVBN.js} +27 -27
- package/dist-cli/chunks/{chunk-EFXNSJ55.js → chunk-RSNLI2NH.js} +2 -2
- package/dist-cli/chunks/{chunk-25BTVHK2.js → chunk-RZGHV2WI.js} +2 -2
- package/dist-cli/chunks/chunk-SGGL3AFV.js +497 -0
- package/dist-cli/chunks/chunk-SH5ETJKG.js +4 -0
- package/dist-cli/chunks/{chunk-MRNW4GVL.js → chunk-T6J4YP56.js} +2 -2
- package/dist-cli/chunks/{chunk-R4FQZXJC.js → chunk-TWUE7WF5.js} +2 -2
- package/dist-cli/chunks/{chunk-X5ZOZVHC.js → chunk-U44VEQRN.js} +1 -1
- package/dist-cli/chunks/{chunk-XRO5WQXP.js → chunk-URLVQHC3.js} +1 -1
- package/dist-cli/chunks/{chunk-JAFF4RAU.js → chunk-VANZFTUE.js} +1 -1
- package/dist-cli/chunks/{chunk-6BAF6GFS.js → chunk-VB2YBH46.js} +1 -1
- package/dist-cli/chunks/{chunk-O6P4N2JD.js → chunk-VFU2SJ7M.js} +2 -2
- package/dist-cli/chunks/{chunk-XPOTXF5Q.js → chunk-VUBJ6FXU.js} +15 -15
- package/dist-cli/chunks/{chunk-XR4KID25.js → chunk-W5PATILQ.js} +2 -2
- package/dist-cli/chunks/chunk-WI5V4TYG.js +38 -0
- package/dist-cli/chunks/{chunk-NEH4T67X.js → chunk-XTRHQAOV.js} +2 -2
- package/dist-cli/chunks/chunk-XWUTJLBM.js +27 -0
- package/dist-cli/chunks/{chunk-TULWPOAY.js → chunk-XYRUQQUR.js} +2 -2
- package/dist-cli/chunks/chunk-YBFB3RM7.js +4 -0
- package/dist-cli/chunks/chunk-YGQVQ2VG.js +4 -0
- package/dist-cli/chunks/{chunk-J66US2OZ.js → chunk-YINACV32.js} +1 -1
- package/dist-cli/chunks/chunk-YV7ERO2V.js +5 -0
- package/dist-cli/chunks/{cleanup-G4TEYVKY.js → cleanup-GVFHQ3KH.js} +2 -2
- package/dist-cli/chunks/cli-version-DPX5MA2M.js +4 -0
- package/dist-cli/chunks/{compat-EZ5NP32J.js → compat-AYRTXYAM.js} +2 -2
- package/dist-cli/chunks/{config-27M2DGCS.js → config-K7LOOV62.js} +2 -2
- package/dist-cli/chunks/connect-H3CNY4IO.js +4 -0
- package/dist-cli/chunks/control-SMHZ2CF5.js +4 -0
- package/dist-cli/chunks/daemon-TEZUNI73.js +4 -0
- package/dist-cli/chunks/{debug-EXU3D7PC.js → debug-ELGHG57T.js} +3 -3
- package/dist-cli/chunks/{desktop-SHHPZAWQ.js → desktop-4G7FK3MP.js} +3 -3
- package/dist-cli/chunks/detox-JKXGEJIZ.js +52 -0
- package/dist-cli/chunks/{device-4NI26HG6.js → device-IV3AR6KG.js} +2 -2
- package/dist-cli/chunks/{diagnose-P4SFXH6J.js → diagnose-3ERGF7EW.js} +2 -2
- package/dist-cli/chunks/{disk-cleanup-BUAKWYMY.js → disk-cleanup-ALFEVZ4B.js} +2 -2
- package/dist-cli/chunks/do-chain-K2B4OHM7.js +5 -0
- package/dist-cli/chunks/drivers-KJYXA3M3.js +4 -0
- package/dist-cli/chunks/env-2KF6IYZE.js +4 -0
- package/dist-cli/chunks/{film-Y2BAQINB.js → film-UQXJFAD5.js} +3 -3
- package/dist-cli/chunks/flow-ASTUH56E.js +4 -0
- package/dist-cli/chunks/help-SNNYFELA.js +4 -0
- package/dist-cli/chunks/{hidden-runtime-alias-U2MAY2XV.js → hidden-runtime-alias-VJCWCFVO.js} +2 -2
- package/dist-cli/chunks/{home-paths-PAJMZCOS.js → home-paths-EGUZZ6J6.js} +2 -2
- package/dist-cli/chunks/inspect-XANAADQT.js +4 -0
- package/dist-cli/chunks/install-desktop-JKQA7UY6.js +4 -0
- package/dist-cli/chunks/{login-D53XKBEO.js → login-6KQGD7HK.js} +2 -2
- package/dist-cli/chunks/{logout-HZQKLNUJ.js → logout-2FCOVLBC.js} +2 -2
- package/dist-cli/chunks/{maestro-XSS2KX6L.js → maestro-HPSSEVJD.js} +3 -3
- package/dist-cli/chunks/{maestro-generate-DNDQX3JQ.js → maestro-generate-KQQ6JHMR.js} +5 -5
- package/dist-cli/chunks/{mode-LEVGOKI3.js → mode-SH7KRZET.js} +2 -2
- package/dist-cli/chunks/{optional-demo-registry-2XZIJNDG.js → optional-demo-registry-NAMCAVFX.js} +2 -2
- package/dist-cli/chunks/{perf-OKMAXDLB.js → perf-LWKAHXUL.js} +2 -2
- package/dist-cli/chunks/{permissions-VG5TBUTN.js → permissions-EZTKWTXV.js} +2 -2
- package/dist-cli/chunks/platform-KSOJIVN5.js +6 -0
- package/dist-cli/chunks/privacy-EJULWP7L.js +4 -0
- package/dist-cli/chunks/{record-BQYGZOMV.js → record-2IC7BAQN.js} +3 -3
- package/dist-cli/chunks/{report-issue-HCPZESWE.js → report-issue-MMX6HOQO.js} +2 -2
- package/dist-cli/chunks/reset-PKFZA4NC.js +4 -0
- package/dist-cli/chunks/resolve-target-AFKU3BRE.js +4 -0
- package/dist-cli/chunks/runtime-XHWWPOIN.js +4 -0
- package/dist-cli/chunks/{screenshot-command-DWEYPJE7.js → screenshot-command-JYUXDVSA.js} +3 -3
- package/dist-cli/chunks/{screenshot-layers-5GMBXLO7.js → screenshot-layers-OGRY6AA3.js} +2 -2
- package/dist-cli/chunks/{screenshots-capture-5KCI7IZD.js → screenshots-capture-QHDN65XS.js} +2 -2
- package/dist-cli/chunks/{serve-QNKTA2HR.js → serve-W4BKAG4J.js} +3 -3
- package/dist-cli/chunks/setup-WJPMPSAC.js +4 -0
- package/dist-cli/chunks/sim-client-UOQGFBPO.js +4 -0
- package/dist-cli/chunks/{skills-DOEALD3X.js → skills-OPPXOD63.js} +2 -2
- package/dist-cli/chunks/state-TDAN7BOM.js +14 -0
- package/dist-cli/chunks/{storage-E7WUWN3E.js → storage-CFB2YGLY.js} +2 -2
- package/dist-cli/chunks/store-4TEED4ZU.js +4 -0
- package/dist-cli/chunks/telemetry-S6N7CV6G.js +4 -0
- package/dist-cli/chunks/{timeline-IKNUGQ2A.js → timeline-ASOOVP4C.js} +2 -2
- package/dist-cli/chunks/{upgrade-VS3OKXE2.js → upgrade-6MVJISHG.js} +2 -2
- package/dist-cli/chunks/upload-CBNIRFEW.js +4 -0
- package/dist-cli/chunks/{version-F4XHNTBZ.js → version-5GS7CWRL.js} +2 -2
- package/dist-cli/chunks/{web-F72724QL.js → web-QRRMYMV4.js} +2 -2
- package/dist-cli/chunks/welcome-33FXMAMB.js +5 -0
- package/dist-cli/chunks/{what-happened-APK7YKOV.js → what-happened-GAZJGKXX.js} +2 -2
- package/dist-cli/chunks/ws-bridge-DIXUAPLQ.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 +35 -19
- 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 +244 -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 +303 -657
- package/dist-lib/sdk.mjs +301 -652
- package/dist-lib/skills.cjs +256 -339
- package/dist-lib/vite.cjs +1 -1
- package/package.json +10 -1
- 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/app-url.ts +40 -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 +24 -9
- package/dist-cli/chunks/chunk-2JFZ5Z7Q.js +0 -4
- package/dist-cli/chunks/chunk-4WFN4DFR.js +0 -5
- package/dist-cli/chunks/chunk-52ES4API.js +0 -38
- package/dist-cli/chunks/chunk-622E4JET.js +0 -767
- package/dist-cli/chunks/chunk-EFL2ASZO.js +0 -124
- package/dist-cli/chunks/chunk-IWPS3EKJ.js +0 -15
- package/dist-cli/chunks/chunk-MCHKBFUL.js +0 -4
- package/dist-cli/chunks/chunk-OSZ5HZSI.js +0 -24
- package/dist-cli/chunks/chunk-W3UIDI3W.js +0 -5
- package/dist-cli/chunks/chunk-XKRUNS7A.js +0 -32
- package/dist-cli/chunks/chunk-YE3M3YD4.js +0 -944
- package/dist-cli/chunks/chunk-ZCP4CRP6.js +0 -4
- package/dist-cli/chunks/chunk-ZGD7GZRI.js +0 -4
- package/dist-cli/chunks/chunk-ZGH5U4AJ.js +0 -27
- package/dist-cli/chunks/cli-version-ZM3PVKQ5.js +0 -4
- package/dist-cli/chunks/control-4QR43FXV.js +0 -4
- package/dist-cli/chunks/daemon-2U4CJ6AT.js +0 -4
- package/dist-cli/chunks/detox-RQ6FFSBS.js +0 -52
- package/dist-cli/chunks/drivers-OVCUPQTM.js +0 -4
- package/dist-cli/chunks/flow-ELTLKKZ3.js +0 -4
- package/dist-cli/chunks/help-CDEZAG2L.js +0 -4
- package/dist-cli/chunks/inspect-VVQRPJLS.js +0 -4
- package/dist-cli/chunks/install-desktop-YXSRKR35.js +0 -4
- package/dist-cli/chunks/privacy-MNRHREME.js +0 -4
- package/dist-cli/chunks/reset-R4E4W4YI.js +0 -4
- package/dist-cli/chunks/runtime-XRQZ7W34.js +0 -4
- package/dist-cli/chunks/setup-RNMM5GO5.js +0 -4
- package/dist-cli/chunks/state-3HH2IHIF.js +0 -14
- package/dist-cli/chunks/store-HUPZUCFG.js +0 -4
- package/dist-cli/chunks/telemetry-IJ53XR2Y.js +0 -4
- package/dist-cli/chunks/upload-A57NAAHF.js +0 -4
- package/dist-cli/chunks/welcome-B72RRMBW.js +0 -5
package/dist-lib/vite.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
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.
|
|
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:*",
|
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,
|
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
|
+
}
|
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
|
// ---------------------------------------------------------------------------
|