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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// transport-agnostic sootsim inspect-verb kernels.
|
|
2
2
|
//
|
|
3
|
-
// each function here
|
|
4
|
-
// returns *structured data* — no arg parsing
|
|
3
|
+
// each function here sends the smallest bridge command its verb needs and
|
|
4
|
+
// returns *structured data* — no arg parsing or stdout. the
|
|
5
5
|
// CLI subcommand handlers wrap these with their WS bridge + text formatting;
|
|
6
6
|
// the in-browser agent wraps them with an in-process bridge over its preview
|
|
7
7
|
// window. one implementation, two transports — the DRY core of the inspect
|
|
@@ -11,6 +11,16 @@
|
|
|
11
11
|
// dependency is the `InspectBridge.send` seam below, which both transports
|
|
12
12
|
// satisfy (the CLI's `WsBridge` already has a matching `send`).
|
|
13
13
|
|
|
14
|
+
import type { SemanticWaitResult } from '../../../src/bridge-contract'
|
|
15
|
+
export {
|
|
16
|
+
readyProbeHasContent,
|
|
17
|
+
readyProbeHasTargetContent,
|
|
18
|
+
} from '../../../src/bridge-contract'
|
|
19
|
+
import type {
|
|
20
|
+
SimSemanticNode,
|
|
21
|
+
SimSemanticQueryResult,
|
|
22
|
+
SimSemanticSelector,
|
|
23
|
+
} from '../../../src/bridge-contract'
|
|
14
24
|
import type {
|
|
15
25
|
SootSimRouteInfo,
|
|
16
26
|
SootSimRequestEntry,
|
|
@@ -38,8 +48,7 @@ export function resolveMaxMsFlag(args: string[], fallbackMs: number): number {
|
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
// the minimal bridge an inspect kernel needs: a JSON command channel into a
|
|
41
|
-
// sootsim instance.
|
|
42
|
-
// `{ type: 'call', path, args }` invokes a method by dotted path.
|
|
51
|
+
// sootsim instance.
|
|
43
52
|
export interface InspectBridge {
|
|
44
53
|
send(
|
|
45
54
|
cmd: { type: string; [k: string]: any },
|
|
@@ -84,34 +93,23 @@ export async function shouldSkipAutoSettleForInspectPick(
|
|
|
84
93
|
export async function inspectNodeCount(
|
|
85
94
|
bridge: InspectBridge,
|
|
86
95
|
): Promise<{ nodes: number }> {
|
|
87
|
-
const
|
|
88
|
-
type: '
|
|
89
|
-
|
|
96
|
+
const result: unknown = await bridge.send({
|
|
97
|
+
type: 'query',
|
|
98
|
+
query: { kind: 'count' },
|
|
90
99
|
})
|
|
91
|
-
return { nodes:
|
|
100
|
+
return { nodes: isSemanticQueryResult(result) ? result.total : 0 }
|
|
92
101
|
}
|
|
93
102
|
|
|
94
|
-
// `get tree` —
|
|
95
|
-
// (shell + tenant). `tree` is returned raw so each frontend formats it.
|
|
96
|
-
// `format: 'boxes'` asks the engine for the structured rect tree (a JSON
|
|
97
|
-
// string on the wire) and parses it here.
|
|
103
|
+
// `get tree` — pruned visible semantic tree. formatting stays client-side.
|
|
98
104
|
export async function inspectTree(
|
|
99
105
|
bridge: InspectBridge,
|
|
100
106
|
depth = 5,
|
|
101
|
-
opts?: { format?: 'boxes' },
|
|
102
107
|
): Promise<{ depth: number; tree: unknown }> {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
code: `(async () => await window.__sootsimTest.dumpTree(${depth}${optsArg}))()`,
|
|
108
|
+
const result: unknown = await bridge.send({
|
|
109
|
+
type: 'query',
|
|
110
|
+
query: { kind: 'tree', depth },
|
|
107
111
|
})
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
return { depth, tree: JSON.parse(tree) }
|
|
111
|
-
} catch {
|
|
112
|
-
return { depth, tree }
|
|
113
|
-
}
|
|
114
|
-
}
|
|
112
|
+
const tree = isSemanticQueryResult(result) ? result.nodes : []
|
|
115
113
|
return { depth, tree }
|
|
116
114
|
}
|
|
117
115
|
|
|
@@ -132,7 +130,6 @@ export interface DescribeDumpOptions {
|
|
|
132
130
|
subtreeRoot?: string
|
|
133
131
|
compact: boolean
|
|
134
132
|
hideXy: boolean
|
|
135
|
-
includeOccluded?: boolean
|
|
136
133
|
// machine consumers keep the whole string; the terminal dump clips it
|
|
137
134
|
fullText?: boolean
|
|
138
135
|
// depth for the recursive dump. omitted means the default 12. setting it
|
|
@@ -141,130 +138,142 @@ export interface DescribeDumpOptions {
|
|
|
141
138
|
maxDepth?: number
|
|
142
139
|
}
|
|
143
140
|
|
|
144
|
-
export interface DescribeKeyboardLayout {
|
|
145
|
-
visible?: boolean
|
|
146
|
-
spec?: {
|
|
147
|
-
autoCapitalize?: string
|
|
148
|
-
keyboardType?: string
|
|
149
|
-
returnKeyType?: string
|
|
150
|
-
}
|
|
151
|
-
mode?: string
|
|
152
|
-
shifted?: boolean
|
|
153
|
-
capsLock?: boolean
|
|
154
|
-
accessoryBarId?: string | null
|
|
155
|
-
}
|
|
156
|
-
|
|
157
141
|
export interface DescribeResult {
|
|
158
142
|
tree?: string
|
|
159
|
-
shell?: {
|
|
160
|
-
state?: string
|
|
161
|
-
activeApp?: string
|
|
162
|
-
showSwitcher?: boolean
|
|
163
|
-
switcherPhase?: string
|
|
164
|
-
} | null
|
|
165
143
|
nodeCount?: number
|
|
166
|
-
keyboard?: DescribeKeyboardLayout | null
|
|
167
144
|
}
|
|
168
145
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const kb = window.__sootsimKeyboard
|
|
184
|
-
if (!t) return { error: 'no test bridge' }
|
|
146
|
+
function isSemanticGeometry(value: unknown): boolean {
|
|
147
|
+
return (
|
|
148
|
+
value !== null &&
|
|
149
|
+
typeof value === 'object' &&
|
|
150
|
+
[
|
|
151
|
+
Reflect.get(value, 'x'),
|
|
152
|
+
Reflect.get(value, 'y'),
|
|
153
|
+
Reflect.get(value, 'width'),
|
|
154
|
+
Reflect.get(value, 'height'),
|
|
155
|
+
].every((coordinate) =>
|
|
156
|
+
typeof coordinate === 'number' ? Number.isFinite(coordinate) : false,
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
}
|
|
185
160
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
161
|
+
export function isSimSemanticTree(value: unknown): value is SimSemanticNode[] {
|
|
162
|
+
if (!Array.isArray(value)) return false
|
|
163
|
+
return value.every((node) => {
|
|
164
|
+
if (
|
|
165
|
+
node === null ||
|
|
166
|
+
typeof node !== 'object' ||
|
|
167
|
+
!Number.isInteger(Reflect.get(node, 'nodeId')) ||
|
|
168
|
+
typeof Reflect.get(node, 'type') !== 'string' ||
|
|
169
|
+
!isSemanticGeometry(Reflect.get(node, 'geometry'))
|
|
170
|
+
) {
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
for (const key of ['testID', 'text', 'role', 'label']) {
|
|
174
|
+
const value = Reflect.get(node, key)
|
|
175
|
+
if (value !== undefined && typeof value !== 'string') return false
|
|
176
|
+
}
|
|
177
|
+
const pressable = Reflect.get(node, 'pressable')
|
|
178
|
+
if (pressable !== undefined && pressable !== true) return false
|
|
179
|
+
const children = Reflect.get(node, 'children')
|
|
180
|
+
return children === undefined || isSimSemanticTree(children)
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isSemanticQueryResult(value: unknown): value is SimSemanticQueryResult {
|
|
185
|
+
const total =
|
|
186
|
+
value !== null && typeof value === 'object' ? Reflect.get(value, 'total') : undefined
|
|
187
|
+
return (
|
|
188
|
+
value !== null &&
|
|
189
|
+
typeof value === 'object' &&
|
|
190
|
+
typeof total === 'number' &&
|
|
191
|
+
Number.isInteger(total) &&
|
|
192
|
+
total >= 0 &&
|
|
193
|
+
isSimSemanticTree(Reflect.get(value, 'nodes'))
|
|
194
|
+
)
|
|
195
|
+
}
|
|
190
196
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
197
|
+
function globRegex(glob: string): RegExp {
|
|
198
|
+
const escaped = glob.replace(/[.+^$(){}|[\]\\]/g, '\\$&')
|
|
199
|
+
return new RegExp(`^${escaped.replace(/\*/g, '.*').replace(/\?/g, '.')}$`, 'i')
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function formatSemanticTree(
|
|
203
|
+
nodes: readonly SimSemanticNode[],
|
|
204
|
+
opts: Partial<DescribeDumpOptions> = {},
|
|
205
|
+
): string {
|
|
206
|
+
const filter = opts.filter?.toLowerCase() ?? ''
|
|
207
|
+
const testIDRegex = opts.testIdLike ? globRegex(opts.testIdLike) : null
|
|
208
|
+
const onlyRegex = opts.onlyGlob ? globRegex(opts.onlyGlob) : null
|
|
209
|
+
const lines: string[] = []
|
|
210
|
+
const visit = (node: SimSemanticNode, indent: number): void => {
|
|
211
|
+
const joined = [node.role, node.label, node.text, node.testID]
|
|
212
|
+
.filter((value): value is string => typeof value === 'string')
|
|
213
|
+
.join(' ')
|
|
214
|
+
const matches =
|
|
215
|
+
(!filter || joined.toLowerCase().includes(filter)) &&
|
|
216
|
+
(!testIDRegex || testIDRegex.test(node.testID ?? '')) &&
|
|
217
|
+
(!onlyRegex || onlyRegex.test(joined))
|
|
218
|
+
if (matches) {
|
|
219
|
+
const parts = [
|
|
220
|
+
node.role ? `[${node.role}]` : node.pressable ? '[tap]' : `<${node.type}>`,
|
|
221
|
+
]
|
|
222
|
+
if (node.text) {
|
|
223
|
+
const text =
|
|
224
|
+
opts.fullText || node.text.length <= 50
|
|
225
|
+
? node.text
|
|
226
|
+
: `${node.text.slice(0, 49).trimEnd()}…`
|
|
227
|
+
parts.push(JSON.stringify(text))
|
|
198
228
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
// ellipsis nothing downstream can undo.
|
|
203
|
-
const clipForDisplay = (value, max) => {
|
|
204
|
-
const text = typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''
|
|
205
|
-
if (opts.fullText || text.length <= max) return text
|
|
206
|
-
return text.slice(0, max - 1).trimEnd() + '…'
|
|
229
|
+
if (node.testID) parts.push(`#${node.testID}`)
|
|
230
|
+
if (node.label && node.label !== node.text) {
|
|
231
|
+
parts.push(`label=${JSON.stringify(node.label)}`)
|
|
207
232
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const testIdRegex = opts.testIdLike ? globToRegex(opts.testIdLike) : null
|
|
214
|
-
const onlyRegex = opts.onlyGlob ? globToRegex(opts.onlyGlob) : null
|
|
215
|
-
const lines = []
|
|
216
|
-
let matched = 0
|
|
217
|
-
for (const node of list) {
|
|
218
|
-
const testID = node?.testID || node?.testId || ''
|
|
219
|
-
const role = node?.role || ''
|
|
220
|
-
const label = node?.accessibilityLabel || node?.label || ''
|
|
221
|
-
const text = node?.text || ''
|
|
222
|
-
if (testIdRegex && !testIdRegex.test(testID)) continue
|
|
223
|
-
const joined = [role, label, text, testID].filter(Boolean).join(' ')
|
|
224
|
-
if (onlyRegex && !onlyRegex.test(joined)) continue
|
|
225
|
-
if (filter && !joined.toLowerCase().includes(filter)) continue
|
|
226
|
-
matched++
|
|
227
|
-
|
|
228
|
-
const type = String(node?.type || 'node').toLowerCase()
|
|
229
|
-
const handlers = Array.isArray(node?.handlers) ? node.handlers : []
|
|
230
|
-
const parts = []
|
|
231
|
-
if (role) parts.push('[' + role + ']')
|
|
232
|
-
else if (handlers.length > 0) parts.push('[tap]')
|
|
233
|
-
else parts.push('<' + type + '>')
|
|
234
|
-
const visibleText = clipForDisplay(text, 50)
|
|
235
|
-
if (visibleText) parts.push(JSON.stringify(visibleText))
|
|
236
|
-
if (testID) parts.push('#' + testID)
|
|
237
|
-
if (label && label !== text) {
|
|
238
|
-
parts.push('label=' + JSON.stringify(clipForDisplay(label, 50)))
|
|
239
|
-
}
|
|
240
|
-
if (!opts.hideXy) {
|
|
241
|
-
const abs = node?.absolute || {}
|
|
242
|
-
const layout = node?.layout || {}
|
|
243
|
-
const x = Math.round(Number(abs.x) || 0)
|
|
244
|
-
const y = Math.round(Number(abs.y) || 0)
|
|
245
|
-
const w = Math.round(Number(layout.width) || 0)
|
|
246
|
-
const h = Math.round(Number(layout.height) || 0)
|
|
247
|
-
parts.push('@(' + x + ',' + y + ') ' + w + 'x' + h)
|
|
248
|
-
}
|
|
249
|
-
if (handlers.length > 0) parts.push('(tap)')
|
|
250
|
-
lines.push(' ' + parts.join(' '))
|
|
251
|
-
}
|
|
252
|
-
return { tree: lines.join('\\n'), matched }
|
|
233
|
+
if (!opts.hideXy) {
|
|
234
|
+
const geometry = node.geometry
|
|
235
|
+
parts.push(
|
|
236
|
+
`@(${Math.round(geometry.x)},${Math.round(geometry.y)}) ${Math.round(geometry.width)}x${Math.round(geometry.height)}`,
|
|
237
|
+
)
|
|
253
238
|
}
|
|
239
|
+
if (node.pressable) parts.push('(tap)')
|
|
240
|
+
lines.push(`${' '.repeat(indent + 1)}${parts.join(' ')}`)
|
|
241
|
+
}
|
|
242
|
+
for (const child of node.children ?? []) visit(child, indent + 1)
|
|
243
|
+
}
|
|
244
|
+
let roots = nodes
|
|
245
|
+
if (opts.subtreeRoot) {
|
|
246
|
+
const pending = [...nodes]
|
|
247
|
+
let found: SimSemanticNode | undefined
|
|
248
|
+
while (pending.length > 0 && !found) {
|
|
249
|
+
const candidate = pending.shift()
|
|
250
|
+
if (!candidate) continue
|
|
251
|
+
if (candidate.testID === opts.subtreeRoot) found = candidate
|
|
252
|
+
else pending.push(...(candidate.children ?? []))
|
|
253
|
+
}
|
|
254
|
+
if (!found) return `__SUBTREE_NOT_FOUND__:${opts.subtreeRoot}`
|
|
255
|
+
roots = [found]
|
|
256
|
+
}
|
|
257
|
+
for (const root of roots) visit(root, 0)
|
|
258
|
+
return lines.join('\n')
|
|
259
|
+
}
|
|
254
260
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return
|
|
261
|
+
// `describe` — format the visible structured tree for humans. callers own
|
|
262
|
+
// arg-parsing, preflight, hints, and the watch loop.
|
|
263
|
+
export async function inspectDescribe(
|
|
264
|
+
bridge: InspectBridge,
|
|
265
|
+
dumpOpts: DescribeDumpOptions,
|
|
266
|
+
): Promise<DescribeResult> {
|
|
267
|
+
const dumpDepth = dumpOpts.maxDepth ?? 12
|
|
268
|
+
const result: unknown = await bridge.send({
|
|
269
|
+
type: 'query',
|
|
270
|
+
query: { kind: 'tree', depth: dumpDepth },
|
|
271
|
+
})
|
|
272
|
+
if (!isSemanticQueryResult(result)) return {}
|
|
273
|
+
return {
|
|
274
|
+
tree: formatSemanticTree(result.nodes, dumpOpts),
|
|
275
|
+
nodeCount: result.total,
|
|
276
|
+
}
|
|
268
277
|
}
|
|
269
278
|
|
|
270
279
|
// ─── get-layout ───
|
|
@@ -463,10 +472,11 @@ export interface LayoutElement {
|
|
|
463
472
|
text?: string
|
|
464
473
|
// on-screen bounding box in device/viewport px (x/y are absolute).
|
|
465
474
|
box: LayoutBox
|
|
466
|
-
// box-model fields are
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
475
|
+
// box-model fields are available only when the caller opts into styling.
|
|
476
|
+
// measured zero remains distinct from an unavailable field.
|
|
477
|
+
borderRadius?: number
|
|
478
|
+
padding?: LayoutPadding
|
|
479
|
+
fontSize?: number
|
|
470
480
|
// styling — so a text-only agent (no vision) can still reason about visual
|
|
471
481
|
// quality: theme correctness (is this screen actually emerald?), contrast
|
|
472
482
|
// (color vs bg), washed-out opacity, garbled/struck text, clipping
|
|
@@ -621,9 +631,6 @@ export function layoutGetNativeEval(opts: LayoutExtractionOptions = {}): string
|
|
|
621
631
|
type: typeof n.type === 'string' ? n.type : 'node',
|
|
622
632
|
text,
|
|
623
633
|
box: { x: round(abs.x), y: round(abs.y), w: round(layout.width), h: round(layout.height) },
|
|
624
|
-
borderRadius: 0,
|
|
625
|
-
padding: { t: 0, r: 0, b: 0, l: 0 },
|
|
626
|
-
fontSize: 0,
|
|
627
634
|
}
|
|
628
635
|
if (!includeStyle) return base
|
|
629
636
|
// computedStyle has the resolved values; style is the curated subset —
|
|
@@ -702,6 +709,34 @@ export async function inspectGetLayout(
|
|
|
702
709
|
bridge: InspectBridge,
|
|
703
710
|
opts: LayoutExtractionOptions = {},
|
|
704
711
|
): Promise<LayoutElement[]> {
|
|
712
|
+
if (!opts.styling) {
|
|
713
|
+
const result: unknown = await bridge.send({
|
|
714
|
+
type: 'query',
|
|
715
|
+
query: { kind: 'find', selector: { visible: true } },
|
|
716
|
+
})
|
|
717
|
+
if (!isSemanticQueryResult(result)) return []
|
|
718
|
+
const filter = opts.filter?.trim().toLowerCase() ?? ''
|
|
719
|
+
return result.nodes
|
|
720
|
+
.filter((node) => {
|
|
721
|
+
if (!filter) return true
|
|
722
|
+
return [node.text, node.testID, node.role, node.type].some(
|
|
723
|
+
(value) => typeof value === 'string' && value.toLowerCase().includes(filter),
|
|
724
|
+
)
|
|
725
|
+
})
|
|
726
|
+
.map((node) => ({
|
|
727
|
+
nodeId: node.nodeId,
|
|
728
|
+
...(node.testID ? { testID: node.testID } : {}),
|
|
729
|
+
...(node.role ? { role: node.role } : {}),
|
|
730
|
+
type: node.type,
|
|
731
|
+
...(node.text ? { text: node.text } : {}),
|
|
732
|
+
box: {
|
|
733
|
+
x: Math.round(node.geometry.x),
|
|
734
|
+
y: Math.round(node.geometry.y),
|
|
735
|
+
w: Math.round(node.geometry.width),
|
|
736
|
+
h: Math.round(node.geometry.height),
|
|
737
|
+
},
|
|
738
|
+
}))
|
|
739
|
+
}
|
|
705
740
|
const raw = await bridge.send({
|
|
706
741
|
type: 'evaluate',
|
|
707
742
|
code: layoutGetNativeEval(opts),
|
|
@@ -726,13 +761,17 @@ export function formatLayoutElements(
|
|
|
726
761
|
? `[${el.role}]`
|
|
727
762
|
: `<${el.type}>`
|
|
728
763
|
const parts = [ident, `@(${el.box.x},${el.box.y})`, `${el.box.w}x${el.box.h}`]
|
|
729
|
-
if (el.borderRadius > 0)
|
|
764
|
+
if (el.borderRadius !== undefined && el.borderRadius > 0) {
|
|
765
|
+
parts.push(`radius:${el.borderRadius}`)
|
|
766
|
+
}
|
|
730
767
|
const p = el.padding
|
|
731
|
-
if (p.t || p.r || p.b || p.l) {
|
|
768
|
+
if (p && (p.t || p.r || p.b || p.l)) {
|
|
732
769
|
const uniform = p.t === p.r && p.r === p.b && p.b === p.l
|
|
733
770
|
parts.push(uniform ? `pad:${p.t}` : `pad:${p.t},${p.r},${p.b},${p.l}`)
|
|
734
771
|
}
|
|
735
|
-
if (el.fontSize > 0)
|
|
772
|
+
if (el.fontSize !== undefined && el.fontSize > 0) {
|
|
773
|
+
parts.push(`font:${el.fontSize}`)
|
|
774
|
+
}
|
|
736
775
|
// styling is opt-in (the `styling` flag): the default rows stay scannable
|
|
737
776
|
// for UI inspection, and a no-vision agent asks for styling when judging
|
|
738
777
|
// visual quality (contrast, washed-out, garbled/struck, clipping, flat).
|
|
@@ -780,89 +819,38 @@ export interface AccessibilityTreeNode {
|
|
|
780
819
|
size: { w: number; h: number } | null
|
|
781
820
|
}
|
|
782
821
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
if (!t) return []
|
|
786
|
-
|
|
787
|
-
const readLayout = (n) => n?.layout || null
|
|
788
|
-
const readAbs = (n) => n?.absolutePosition || n?.absolute || null
|
|
789
|
-
const readRole = (n) => n?.accessibilityRole || n?.role || null
|
|
790
|
-
const readLabel = (n) => n?.accessibilityLabel || n?.label || n?.text || null
|
|
791
|
-
const readHint = (n) => n?.accessibilityHint || n?.hint || null
|
|
792
|
-
const readState = (n) => n?.accessibilityState || n?.state || null
|
|
793
|
-
const readTestId = (n) => n?.testID || n?.testId || null
|
|
794
|
-
const readType = (n) => typeof n?.type === 'string' ? n.type.toLowerCase() : n?.type
|
|
795
|
-
const readPressable = (n) => {
|
|
796
|
-
if (n?.pressable) return true
|
|
797
|
-
const handlers = Array.isArray(n?.handlers) ? n.handlers : []
|
|
798
|
-
const role = readRole(n)
|
|
799
|
-
return handlers.length > 0 || role === 'button' || role === 'link' || role === 'tab'
|
|
800
|
-
}
|
|
801
|
-
const isTextInput = (n) => n?.isTextInput === true
|
|
802
|
-
const isTextNode = (n) => readType(n) === 'text'
|
|
803
|
-
const isVisibleTarget = (n) => {
|
|
804
|
-
const layout = readLayout(n)
|
|
805
|
-
if (!layout || layout.width <= 0 || layout.height <= 0) return false
|
|
806
|
-
const abs = readAbs(n)
|
|
807
|
-
if (!abs) return true
|
|
808
|
-
const device = n?.device || {}
|
|
809
|
-
const screenW = Number(device.width) || window.innerWidth || 0
|
|
810
|
-
const screenH = Number(device.height) || window.innerHeight || 0
|
|
811
|
-
return (
|
|
812
|
-
abs.x + layout.width > 0 &&
|
|
813
|
-
abs.y + layout.height > 0 &&
|
|
814
|
-
abs.x < screenW &&
|
|
815
|
-
abs.y < screenH
|
|
816
|
-
)
|
|
817
|
-
}
|
|
818
|
-
const hasAccessibleSignal = (n) => {
|
|
819
|
-
const role = readRole(n)
|
|
820
|
-
const label = readLabel(n)
|
|
821
|
-
const hint = readHint(n)
|
|
822
|
-
if (role) return true
|
|
823
|
-
if (hint) return true
|
|
824
|
-
if (isTextInput(n)) return true
|
|
825
|
-
if (readPressable(n)) return true
|
|
826
|
-
if (isTextNode(n) && n?.text) return true
|
|
827
|
-
if (typeof label === 'string' && label.length <= 30 && label !== n?.text) return true
|
|
828
|
-
return false
|
|
829
|
-
}
|
|
830
|
-
const normalize = (n) => {
|
|
831
|
-
const layout = readLayout(n)
|
|
832
|
-
const abs = readAbs(n)
|
|
833
|
-
const role = readRole(n) || (readPressable(n) ? 'button' : isTextInput(n) ? 'textfield' : isTextNode(n) ? 'statictext' : 'none')
|
|
834
|
-
return {
|
|
835
|
-
role,
|
|
836
|
-
label: readLabel(n),
|
|
837
|
-
hint: readHint(n),
|
|
838
|
-
state: readState(n),
|
|
839
|
-
testID: readTestId(n),
|
|
840
|
-
position: abs ? { x: Math.round(abs.x), y: Math.round(abs.y) } : null,
|
|
841
|
-
size: layout ? { w: Math.round(layout.width), h: Math.round(layout.height) } : null,
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
if (typeof t.listInspectable === 'function') {
|
|
846
|
-
const list = await t.listInspectable({})
|
|
847
|
-
if (Array.isArray(list)) {
|
|
848
|
-
return list.filter(n => isVisibleTarget(n) && hasAccessibleSignal(n)).map(normalize)
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
const all = await t.queryAll({ pruneHidden: true })
|
|
853
|
-
return all
|
|
854
|
-
.filter(n => isVisibleTarget(n) && hasAccessibleSignal(n))
|
|
855
|
-
.map(normalize)
|
|
856
|
-
})()`
|
|
857
|
-
|
|
858
|
-
// `get a11y` — flat accessibility summary for the currently visible SootSim
|
|
859
|
-
// surface. listInspectable is preferred because it already resolves the active
|
|
860
|
-
// shell/tenant surface and excludes covered launcher/app-switcher content.
|
|
822
|
+
// `get a11y` — flat accessibility summary projected from the visible semantic
|
|
823
|
+
// tree, which already excludes hidden and covered content.
|
|
861
824
|
export async function inspectAccessibilityTree(
|
|
862
825
|
bridge: InspectBridge,
|
|
863
826
|
): Promise<AccessibilityTreeNode[]> {
|
|
864
|
-
const
|
|
865
|
-
|
|
827
|
+
const result: unknown = await bridge.send({
|
|
828
|
+
type: 'query',
|
|
829
|
+
query: { kind: 'find', selector: { visible: true } },
|
|
830
|
+
})
|
|
831
|
+
if (!isSemanticQueryResult(result)) return []
|
|
832
|
+
return result.nodes.flatMap((node) => {
|
|
833
|
+
if (!node.role && !node.label && !node.text && !node.pressable) return []
|
|
834
|
+
return [
|
|
835
|
+
{
|
|
836
|
+
role:
|
|
837
|
+
node.role ??
|
|
838
|
+
(node.pressable ? 'button' : node.type === 'text' ? 'statictext' : 'none'),
|
|
839
|
+
label: node.label ?? node.text ?? null,
|
|
840
|
+
hint: null,
|
|
841
|
+
state: null,
|
|
842
|
+
testID: node.testID ?? null,
|
|
843
|
+
position: {
|
|
844
|
+
x: Math.round(node.geometry.x),
|
|
845
|
+
y: Math.round(node.geometry.y),
|
|
846
|
+
},
|
|
847
|
+
size: {
|
|
848
|
+
w: Math.round(node.geometry.width),
|
|
849
|
+
h: Math.round(node.geometry.height),
|
|
850
|
+
},
|
|
851
|
+
},
|
|
852
|
+
]
|
|
853
|
+
})
|
|
866
854
|
}
|
|
867
855
|
|
|
868
856
|
// ─── find ───
|
|
@@ -886,164 +874,32 @@ export interface FindQuery {
|
|
|
886
874
|
interactive?: boolean
|
|
887
875
|
}
|
|
888
876
|
|
|
889
|
-
|
|
890
|
-
const fromInspectable = async () => {
|
|
891
|
-
if (typeof t.listInspectable !== 'function') return null
|
|
892
|
-
const list = await t.listInspectable({})
|
|
893
|
-
if (!Array.isArray(list)) return null
|
|
894
|
-
return list.map((n) => {
|
|
895
|
-
const role = n.accessibilityRole ?? n.role
|
|
896
|
-
const label = n.accessibilityLabel ?? n.label
|
|
897
|
-
const layout = n.layout
|
|
898
|
-
const absolutePosition = n.absolutePosition ?? n.absolute
|
|
899
|
-
const screenW = Number(n.device?.width) || window.innerWidth || 0
|
|
900
|
-
const screenH = Number(n.device?.height) || window.innerHeight || 0
|
|
901
|
-
const visibleFrame = layout && absolutePosition
|
|
902
|
-
? {
|
|
903
|
-
x: Math.max(0, absolutePosition.x),
|
|
904
|
-
y: Math.max(0, absolutePosition.y),
|
|
905
|
-
width: Math.max(
|
|
906
|
-
0,
|
|
907
|
-
Math.min(screenW, absolutePosition.x + layout.width) -
|
|
908
|
-
Math.max(0, absolutePosition.x),
|
|
909
|
-
),
|
|
910
|
-
height: Math.max(
|
|
911
|
-
0,
|
|
912
|
-
Math.min(screenH, absolutePosition.y + layout.height) -
|
|
913
|
-
Math.max(0, absolutePosition.y),
|
|
914
|
-
),
|
|
915
|
-
}
|
|
916
|
-
: null
|
|
917
|
-
const handlers = Array.isArray(n.handlers) ? n.handlers : []
|
|
918
|
-
const pressable =
|
|
919
|
-
handlers.length > 0 ||
|
|
920
|
-
role === 'button' ||
|
|
921
|
-
role === 'link' ||
|
|
922
|
-
role === 'tab'
|
|
923
|
-
return {
|
|
924
|
-
type: typeof n.type === 'string' ? n.type.toLowerCase() : n.type,
|
|
925
|
-
id: n.id ?? n.nodeId,
|
|
926
|
-
nodeId: n.nodeId,
|
|
927
|
-
testID: n.testID ?? n.testId,
|
|
928
|
-
text: n.text,
|
|
929
|
-
layout,
|
|
930
|
-
absolutePosition,
|
|
931
|
-
visibleFrame,
|
|
932
|
-
style: n.computedStyle ?? n.style ?? {},
|
|
933
|
-
childCount: n.childCount ?? 0,
|
|
934
|
-
pressed: n.pressed ?? false,
|
|
935
|
-
pressable,
|
|
936
|
-
isTextInput: n.isTextInput ?? false,
|
|
937
|
-
accessible: n.accessible ?? true,
|
|
938
|
-
accessibilityLabel: label,
|
|
939
|
-
accessibilityRole: role,
|
|
940
|
-
accessibilityHint: n.accessibilityHint,
|
|
941
|
-
accessibilityState: n.accessibilityState,
|
|
942
|
-
accessibilityValue: n.accessibilityValue,
|
|
943
|
-
}
|
|
944
|
-
})
|
|
945
|
-
}
|
|
946
|
-
const isVisibleTarget = (n) => {
|
|
947
|
-
if (!n?.layout) return false
|
|
948
|
-
const frame = n.visibleFrame
|
|
949
|
-
if (frame) return frame.width > 0 && frame.height > 0
|
|
950
|
-
const abs = n.absolutePosition
|
|
951
|
-
if (!abs) return n.layout.width > 0 && n.layout.height > 0
|
|
952
|
-
const screenW = window.innerWidth || 0
|
|
953
|
-
const screenH = window.innerHeight || 0
|
|
954
|
-
return (
|
|
955
|
-
abs.x + n.layout.width > 0 &&
|
|
956
|
-
abs.y + n.layout.height > 0 &&
|
|
957
|
-
abs.x < screenW &&
|
|
958
|
-
abs.y < screenH
|
|
959
|
-
)
|
|
960
|
-
}
|
|
961
|
-
`
|
|
962
|
-
|
|
963
|
-
// resolve a find query to its mode + the JS to evaluate. precedence matches
|
|
877
|
+
// resolve a find query to its mode + structured selector. precedence matches
|
|
964
878
|
// the CLI: testid → role → type → pressable → interactive → visible → text.
|
|
965
879
|
// returns null when nothing was asked for (caller prints usage).
|
|
966
|
-
export function resolveFindMode(
|
|
880
|
+
export function resolveFindMode(
|
|
881
|
+
q: FindQuery,
|
|
882
|
+
): { mode: FindMode; selector: SimSemanticSelector } | null {
|
|
967
883
|
if (q.testId) {
|
|
968
|
-
return {
|
|
969
|
-
mode: 'testid',
|
|
970
|
-
code: `(async () => {
|
|
971
|
-
const t = window.__sootsimTest
|
|
972
|
-
if (!t) return null
|
|
973
|
-
return (await t.findByTestId(${JSON.stringify(q.testId)})) || (await t.findById(${JSON.stringify(q.testId)}))
|
|
974
|
-
})()`,
|
|
975
|
-
}
|
|
884
|
+
return { mode: 'testid', selector: { testID: q.testId } }
|
|
976
885
|
}
|
|
977
886
|
if (q.role) {
|
|
978
|
-
return {
|
|
979
|
-
mode: 'role',
|
|
980
|
-
code: `(async () => {
|
|
981
|
-
const t = window.__sootsimTest
|
|
982
|
-
if (!t) return []
|
|
983
|
-
return await t.queryAll({ hasRole: ${JSON.stringify(q.role)}, pruneHidden: true })
|
|
984
|
-
})()`,
|
|
985
|
-
}
|
|
887
|
+
return { mode: 'role', selector: { role: q.role } }
|
|
986
888
|
}
|
|
987
889
|
if (q.type) {
|
|
988
|
-
return {
|
|
989
|
-
mode: 'type',
|
|
990
|
-
code: `(async () => {
|
|
991
|
-
const t = window.__sootsimTest
|
|
992
|
-
if (!t) return []
|
|
993
|
-
return await t.queryAll({ type: ${JSON.stringify(q.type)}, pruneHidden: true })
|
|
994
|
-
})()`,
|
|
995
|
-
}
|
|
890
|
+
return { mode: 'type', selector: { type: q.type } }
|
|
996
891
|
}
|
|
997
892
|
if (q.pressable) {
|
|
998
|
-
return {
|
|
999
|
-
mode: 'pressable',
|
|
1000
|
-
code: `(async () => {
|
|
1001
|
-
const t = window.__sootsimTest
|
|
1002
|
-
if (!t) return []
|
|
1003
|
-
${FIND_INSPECTABLE_TARGETS}
|
|
1004
|
-
const inspectable = await fromInspectable()
|
|
1005
|
-
if (inspectable) return inspectable.filter(n => n.pressable && isVisibleTarget(n))
|
|
1006
|
-
const all = await t.queryAll({ pruneHidden: true })
|
|
1007
|
-
return all.filter(n => n.pressable && isVisibleTarget(n))
|
|
1008
|
-
})()`,
|
|
1009
|
-
}
|
|
893
|
+
return { mode: 'pressable', selector: { pressable: true } }
|
|
1010
894
|
}
|
|
1011
895
|
if (q.interactive) {
|
|
1012
|
-
return {
|
|
1013
|
-
mode: 'interactive-targets',
|
|
1014
|
-
code: `(async () => {
|
|
1015
|
-
const t = window.__sootsimTest
|
|
1016
|
-
if (!t) return []
|
|
1017
|
-
${FIND_INSPECTABLE_TARGETS}
|
|
1018
|
-
const inspectable = await fromInspectable()
|
|
1019
|
-
if (inspectable) {
|
|
1020
|
-
return inspectable.filter(n => n.pressable && isVisibleTarget(n))
|
|
1021
|
-
}
|
|
1022
|
-
const all = await t.queryAll({ pruneHidden: true })
|
|
1023
|
-
return all.filter(n => n.pressable && isVisibleTarget(n))
|
|
1024
|
-
})()`,
|
|
1025
|
-
}
|
|
896
|
+
return { mode: 'interactive-targets', selector: { pressable: true } }
|
|
1026
897
|
}
|
|
1027
898
|
if (q.visible) {
|
|
1028
|
-
return {
|
|
1029
|
-
mode: 'visible',
|
|
1030
|
-
code: `(async () => {
|
|
1031
|
-
const t = window.__sootsimTest
|
|
1032
|
-
if (!t) return []
|
|
1033
|
-
const all = await t.queryAll({ pruneHidden: true })
|
|
1034
|
-
return all.filter(n => n.layout && n.layout.width > 0 && n.layout.height > 0)
|
|
1035
|
-
})()`,
|
|
1036
|
-
}
|
|
899
|
+
return { mode: 'visible', selector: { visible: true } }
|
|
1037
900
|
}
|
|
1038
901
|
if (q.text) {
|
|
1039
|
-
return {
|
|
1040
|
-
mode: 'text',
|
|
1041
|
-
code: `(async () => {
|
|
1042
|
-
const t = window.__sootsimTest
|
|
1043
|
-
if (!t) return null
|
|
1044
|
-
return await t.findByText(${JSON.stringify(q.text)})
|
|
1045
|
-
})()`,
|
|
1046
|
-
}
|
|
902
|
+
return { mode: 'text', selector: { text: q.text } }
|
|
1047
903
|
}
|
|
1048
904
|
return null
|
|
1049
905
|
}
|
|
@@ -1057,8 +913,27 @@ export async function inspectFind(
|
|
|
1057
913
|
): Promise<{ mode: FindMode; result: unknown } | null> {
|
|
1058
914
|
const resolved = resolveFindMode(q)
|
|
1059
915
|
if (!resolved) return null
|
|
1060
|
-
const result = await bridge.send({
|
|
1061
|
-
|
|
916
|
+
const result: unknown = await bridge.send({
|
|
917
|
+
type: 'query',
|
|
918
|
+
query: { kind: 'find', selector: resolved.selector },
|
|
919
|
+
})
|
|
920
|
+
if (!isSemanticQueryResult(result)) return { mode: resolved.mode, result: [] }
|
|
921
|
+
const nodes = result.nodes.map(
|
|
922
|
+
(node): InteractiveNode => ({
|
|
923
|
+
type: node.type,
|
|
924
|
+
nodeId: node.nodeId,
|
|
925
|
+
...(node.testID ? { testID: node.testID } : {}),
|
|
926
|
+
...(node.text ? { text: node.text } : {}),
|
|
927
|
+
...(node.pressable ? { pressable: true } : {}),
|
|
928
|
+
...(node.role ? { accessibilityRole: node.role } : {}),
|
|
929
|
+
...(node.label ? { accessibilityLabel: node.label } : {}),
|
|
930
|
+
absolutePosition: { x: node.geometry.x, y: node.geometry.y },
|
|
931
|
+
layout: { width: node.geometry.width, height: node.geometry.height },
|
|
932
|
+
}),
|
|
933
|
+
)
|
|
934
|
+
const resultValue =
|
|
935
|
+
resolved.mode === 'testid' || resolved.mode === 'text' ? (nodes[0] ?? null) : nodes
|
|
936
|
+
return { mode: resolved.mode, result: resultValue }
|
|
1062
937
|
}
|
|
1063
938
|
|
|
1064
939
|
export interface InteractiveNode {
|
|
@@ -1121,189 +996,6 @@ function shellEscape(value: string): string {
|
|
|
1121
996
|
|
|
1122
997
|
// ─── wait ───
|
|
1123
998
|
|
|
1124
|
-
export interface ReadyProbe {
|
|
1125
|
-
flag: unknown
|
|
1126
|
-
at: number
|
|
1127
|
-
nodes: number
|
|
1128
|
-
targets: number
|
|
1129
|
-
liveFrameActive: boolean
|
|
1130
|
-
liveFrameChannels: number
|
|
1131
|
-
liveFramePublishes: number
|
|
1132
|
-
errors: number
|
|
1133
|
-
loadingText: string
|
|
1134
|
-
externalReady: boolean | null
|
|
1135
|
-
externalStatus: string
|
|
1136
|
-
externalError: string
|
|
1137
|
-
// entry-require throw that bundle-loader suppressed-and-continued past.
|
|
1138
|
-
// diagnostic only — never gates readiness.
|
|
1139
|
-
suppressedEntryError: string
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
export const WAIT_READY_PROBE = `(async () => {
|
|
1143
|
-
const t = window.__sootsimTest
|
|
1144
|
-
let nodes = 0
|
|
1145
|
-
try { nodes = (await t?.getNodeCount?.()) || 0 } catch {}
|
|
1146
|
-
let targets = 0
|
|
1147
|
-
let liveFrameActive = false
|
|
1148
|
-
let liveFrameChannels = 0
|
|
1149
|
-
let liveFramePublishes = 0
|
|
1150
|
-
let loadingText = ''
|
|
1151
|
-
const cleanText = (value) =>
|
|
1152
|
-
typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''
|
|
1153
|
-
const loadingSignal = (value) => {
|
|
1154
|
-
const text = cleanText(value)
|
|
1155
|
-
if (!text) return ''
|
|
1156
|
-
return /^(opening app\\b|looking for your app\\b|building bundle\\b|preparing rnx runtime\\b|loading native bundle\\b|loading rnx stubs\\b|loading bundle loader\\b|loading native stubs\\b|loading compat bridge\\b|loading react-native shim\\b|loading react bridge\\b|loading reconciler\\b|loading replacement modules\\b|injecting rnx modules\\b|warming sqlite engine\\b|capturing root component\\b|reloading app\\b|loading(?:[.!…]*)?$|connecting(?:[.!…]*)?$)/i.test(text)
|
|
1157
|
-
? text
|
|
1158
|
-
: ''
|
|
1159
|
-
}
|
|
1160
|
-
try {
|
|
1161
|
-
const loading = window.__sootsimConnectRNLoading
|
|
1162
|
-
if (loading && loading.active === true) {
|
|
1163
|
-
loadingText = [loading.title, loading.detail].map(cleanText).filter(Boolean).join(' ')
|
|
1164
|
-
}
|
|
1165
|
-
} catch {}
|
|
1166
|
-
try {
|
|
1167
|
-
if (typeof t?.listInspectable === 'function') {
|
|
1168
|
-
const list = await t.listInspectable({})
|
|
1169
|
-
if (Array.isArray(list)) {
|
|
1170
|
-
const screenW = window.innerWidth || 0
|
|
1171
|
-
const screenH = window.innerHeight || 0
|
|
1172
|
-
const isVisible = (n) => {
|
|
1173
|
-
const layout = n?.layout
|
|
1174
|
-
if (!layout || layout.width <= 0 || layout.height <= 0) return false
|
|
1175
|
-
const abs = n?.absolutePosition || n?.absolute
|
|
1176
|
-
if (!abs) return true
|
|
1177
|
-
const device = n?.device || {}
|
|
1178
|
-
const w = Number(device.width) || screenW
|
|
1179
|
-
const h = Number(device.height) || screenH
|
|
1180
|
-
return (
|
|
1181
|
-
abs.x + layout.width > 0 &&
|
|
1182
|
-
abs.y + layout.height > 0 &&
|
|
1183
|
-
abs.x < w &&
|
|
1184
|
-
abs.y < h
|
|
1185
|
-
)
|
|
1186
|
-
}
|
|
1187
|
-
const hasContentSignal = (n) => {
|
|
1188
|
-
const role = n?.accessibilityRole || n?.role
|
|
1189
|
-
const label = cleanText(n?.accessibilityLabel || n?.label)
|
|
1190
|
-
const handlers = Array.isArray(n?.handlers) ? n.handlers : []
|
|
1191
|
-
const text = cleanText(n?.text)
|
|
1192
|
-
const testID = cleanText(n?.testID || n?.testId)
|
|
1193
|
-
const loading = loadingSignal(text) || loadingSignal(label) || loadingSignal(testID)
|
|
1194
|
-
if (!loadingText && loading) {
|
|
1195
|
-
loadingText = loading
|
|
1196
|
-
}
|
|
1197
|
-
if (handlers.length > 0) return true
|
|
1198
|
-
if (role) return true
|
|
1199
|
-
if (n?.isTextInput) return true
|
|
1200
|
-
if (text) return true
|
|
1201
|
-
if (label) return true
|
|
1202
|
-
if (testID && !/^splash|loading/i.test(testID)) return true
|
|
1203
|
-
return false
|
|
1204
|
-
}
|
|
1205
|
-
targets = list.filter(n => isVisible(n) && hasContentSignal(n)).length
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
} catch {}
|
|
1209
|
-
try {
|
|
1210
|
-
const stats = await t?.getImageLoaderStats?.()
|
|
1211
|
-
const presentation = stats?.liveFramePresentation
|
|
1212
|
-
liveFrameChannels = Number(presentation?.activeChannels) || 0
|
|
1213
|
-
liveFramePublishes = Number(presentation?.publishes) || 0
|
|
1214
|
-
const previous = window.__sootsimWaitReadyLiveFramePublishes
|
|
1215
|
-
liveFrameActive =
|
|
1216
|
-
liveFrameChannels > 0 &&
|
|
1217
|
-
typeof previous === 'number' &&
|
|
1218
|
-
liveFramePublishes > previous
|
|
1219
|
-
window.__sootsimWaitReadyLiveFramePublishes = liveFramePublishes
|
|
1220
|
-
} catch {}
|
|
1221
|
-
let externalReady = null
|
|
1222
|
-
let externalStatus = ''
|
|
1223
|
-
let externalError = ''
|
|
1224
|
-
let suppressedEntryError = ''
|
|
1225
|
-
try {
|
|
1226
|
-
const getExternalAppState = t?.getExternalAppState
|
|
1227
|
-
const external = typeof getExternalAppState === 'function'
|
|
1228
|
-
? await Promise.race([
|
|
1229
|
-
getExternalAppState(),
|
|
1230
|
-
new Promise((resolve) => setTimeout(
|
|
1231
|
-
() => resolve({
|
|
1232
|
-
state: {
|
|
1233
|
-
ready: false,
|
|
1234
|
-
loading: true,
|
|
1235
|
-
status: 'waiting for tenant worker...',
|
|
1236
|
-
},
|
|
1237
|
-
}),
|
|
1238
|
-
1000,
|
|
1239
|
-
)),
|
|
1240
|
-
])
|
|
1241
|
-
: null
|
|
1242
|
-
const state = external?.state
|
|
1243
|
-
if (state && typeof state === 'object') {
|
|
1244
|
-
externalReady = state.ready === true
|
|
1245
|
-
if (typeof state.status === 'string') externalStatus = state.status
|
|
1246
|
-
if (typeof state.error === 'string') externalError = state.error
|
|
1247
|
-
}
|
|
1248
|
-
if (!externalError && Array.isArray(external?.entryErrors)) {
|
|
1249
|
-
// suppressed-and-continued entry errors (bundle-loader's patched __r
|
|
1250
|
-
// swallows entry throws and moves on) must NOT gate readiness — the
|
|
1251
|
-
// app frequently runs fine after one (e.g. metro's "Unknown named
|
|
1252
|
-
// module" probing noise). they surface separately as a diagnostic
|
|
1253
|
-
// for the timeout path.
|
|
1254
|
-
const isReal = (item) => item && typeof item.message === 'string' && item.message.trim()
|
|
1255
|
-
const fatal = external.entryErrors.find((item) => isReal(item) && item.suppressed !== true)
|
|
1256
|
-
if (fatal) externalError = fatal.message
|
|
1257
|
-
const suppressed = external.entryErrors.find((item) => isReal(item) && item.suppressed === true)
|
|
1258
|
-
if (suppressed) suppressedEntryError = suppressed.message
|
|
1259
|
-
}
|
|
1260
|
-
// once the app has errored its last status ("injecting sootsim
|
|
1261
|
-
// modules...") is stale — reporting it makes a dead app read as one that
|
|
1262
|
-
// is still booting.
|
|
1263
|
-
if (externalError) loadingText = ''
|
|
1264
|
-
else if (externalReady === false && externalStatus) loadingText = externalStatus
|
|
1265
|
-
} catch {}
|
|
1266
|
-
let errors = 0
|
|
1267
|
-
try { errors = window.__sootsimConsole?.count?.()?.errors ?? 0 } catch {}
|
|
1268
|
-
return {
|
|
1269
|
-
flag: (window).__sootsimExternalAppReady,
|
|
1270
|
-
at: (window).__sootsimExternalAppReadyAt || 0,
|
|
1271
|
-
nodes,
|
|
1272
|
-
targets,
|
|
1273
|
-
liveFrameActive,
|
|
1274
|
-
liveFrameChannels,
|
|
1275
|
-
liveFramePublishes,
|
|
1276
|
-
errors,
|
|
1277
|
-
loadingText,
|
|
1278
|
-
externalReady,
|
|
1279
|
-
externalStatus,
|
|
1280
|
-
externalError,
|
|
1281
|
-
suppressedEntryError,
|
|
1282
|
-
}
|
|
1283
|
-
})()`
|
|
1284
|
-
|
|
1285
|
-
export const READY_CONTENT_NODE_FLOOR = 100
|
|
1286
|
-
export const READY_NODE_STABLE_MS = 750
|
|
1287
|
-
export const WAIT_READY_PROGRESS_INTERVAL_MS = 2000
|
|
1288
|
-
// how often the readiness probe re-runs. shared so `sootsim wait ready` and the
|
|
1289
|
-
// flow runner's launchApp wait resolve a ready app with the same latency.
|
|
1290
|
-
export const READY_PROBE_POLL_MS = 150
|
|
1291
|
-
|
|
1292
|
-
export function readyProbeHasContent(
|
|
1293
|
-
probe: Pick<ReadyProbe, 'nodes' | 'targets'> &
|
|
1294
|
-
Partial<Pick<ReadyProbe, 'liveFrameActive'>>,
|
|
1295
|
-
) {
|
|
1296
|
-
return (
|
|
1297
|
-
probe.liveFrameActive || probe.targets > 0 || probe.nodes >= READY_CONTENT_NODE_FLOOR
|
|
1298
|
-
)
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
export function readyProbeHasTargetContent(
|
|
1302
|
-
probe: Pick<ReadyProbe, 'targets'> & Partial<Pick<ReadyProbe, 'liveFrameActive'>>,
|
|
1303
|
-
) {
|
|
1304
|
-
return probe.liveFrameActive || probe.targets > 0
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
999
|
export interface WaitReadyStatus {
|
|
1308
1000
|
ready: boolean
|
|
1309
1001
|
elapsedMs: number
|
|
@@ -1369,126 +1061,57 @@ export function waitReadyReason(
|
|
|
1369
1061
|
// the tenant tree, and the node count can then jump through placeholder
|
|
1370
1062
|
// counts before the app has finished publishing its first real surface. require
|
|
1371
1063
|
// visible inspectable content (or a larger non-placeholder tree) and a brief
|
|
1372
|
-
// stable count.
|
|
1373
|
-
// bridge
|
|
1064
|
+
// stable count. the engine owns the full polling loop, so every caller pays
|
|
1065
|
+
// exactly one bridge request even for a cold guest bundle.
|
|
1374
1066
|
export async function inspectWaitReady(
|
|
1375
1067
|
bridge: InspectBridge,
|
|
1376
1068
|
timeoutMs = 20_000,
|
|
1377
1069
|
options: WaitReadyOptions = {},
|
|
1378
1070
|
): Promise<WaitReadyStatus> {
|
|
1379
1071
|
const start = Date.now()
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
flag: undefined,
|
|
1392
|
-
at: 0,
|
|
1393
|
-
nodes: 0,
|
|
1394
|
-
targets: 0,
|
|
1395
|
-
liveFrameActive: false,
|
|
1396
|
-
liveFrameChannels: 0,
|
|
1397
|
-
liveFramePublishes: 0,
|
|
1398
|
-
errors: 0,
|
|
1399
|
-
loadingText: '',
|
|
1400
|
-
externalReady: null,
|
|
1401
|
-
externalStatus: '',
|
|
1402
|
-
externalError: '',
|
|
1403
|
-
suppressedEntryError: '',
|
|
1404
|
-
}
|
|
1405
|
-
while (Date.now() < deadline) {
|
|
1406
|
-
try {
|
|
1407
|
-
last = (await bridge.send({ type: 'evaluate', code: WAIT_READY_PROBE })) ?? last
|
|
1408
|
-
bridgeError = ''
|
|
1409
|
-
} catch (error) {
|
|
1410
|
-
bridgeError = error instanceof Error ? error.message : String(error)
|
|
1411
|
-
// launching can make an unreachable sim transient, but target
|
|
1412
|
-
// ambiguity cannot resolve until the caller chooses a sim.
|
|
1413
|
-
if (
|
|
1414
|
-
bridgeError.startsWith('multiple sims are connected:') ||
|
|
1415
|
-
bridgeError.startsWith('saved sim ')
|
|
1416
|
-
) {
|
|
1417
|
-
throw error
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
const now = Date.now()
|
|
1421
|
-
const status: WaitReadyStatus = {
|
|
1422
|
-
ready: false,
|
|
1423
|
-
elapsedMs: now - start,
|
|
1424
|
-
nodes: last.nodes,
|
|
1425
|
-
targets: last.targets,
|
|
1426
|
-
liveFrameActive: last.liveFrameActive,
|
|
1427
|
-
liveFrameChannels: last.liveFrameChannels,
|
|
1428
|
-
liveFramePublishes: last.liveFramePublishes,
|
|
1429
|
-
flag: last.flag,
|
|
1430
|
-
loadingText: last.loadingText,
|
|
1431
|
-
externalReady: last.externalReady,
|
|
1432
|
-
externalStatus: last.externalStatus,
|
|
1433
|
-
externalError: last.externalError,
|
|
1434
|
-
suppressedEntryError: last.suppressedEntryError,
|
|
1435
|
-
errors: last.errors,
|
|
1436
|
-
bridgeError,
|
|
1437
|
-
}
|
|
1438
|
-
if (last.nodes !== lastNodes) {
|
|
1439
|
-
lastNodes = last.nodes
|
|
1440
|
-
nodeStableSince = now
|
|
1441
|
-
}
|
|
1442
|
-
const hasReadySignal = last.flag === true && readyProbeHasContent(last)
|
|
1443
|
-
const hasRenderedContentWithoutReadySignal =
|
|
1444
|
-
last.flag !== true && readyProbeHasTargetContent(last)
|
|
1072
|
+
let result: SemanticWaitResult
|
|
1073
|
+
try {
|
|
1074
|
+
result = await bridge.send(
|
|
1075
|
+
{
|
|
1076
|
+
type: 'waitFor',
|
|
1077
|
+
waitForOptions: { condition: { type: 'ready' }, timeoutMs },
|
|
1078
|
+
},
|
|
1079
|
+
{ timeoutMs: timeoutMs + 1_000 },
|
|
1080
|
+
)
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
const bridgeError = error instanceof Error ? error.message : String(error)
|
|
1445
1083
|
if (
|
|
1446
|
-
(
|
|
1447
|
-
|
|
1448
|
-
!last.externalError &&
|
|
1449
|
-
!last.loadingText
|
|
1084
|
+
bridgeError.startsWith('multiple sims are connected:') ||
|
|
1085
|
+
bridgeError.startsWith('saved sim ')
|
|
1450
1086
|
) {
|
|
1451
|
-
|
|
1452
|
-
return {
|
|
1453
|
-
...status,
|
|
1454
|
-
ready: true,
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1087
|
+
throw error
|
|
1457
1088
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
if (last.externalError) return status
|
|
1464
|
-
if (options.onProgress && progressIntervalMs > 0 && now >= nextProgressAt) {
|
|
1465
|
-
options.onProgress(status)
|
|
1466
|
-
do {
|
|
1467
|
-
nextProgressAt += progressIntervalMs
|
|
1468
|
-
} while (now >= nextProgressAt)
|
|
1089
|
+
result = {
|
|
1090
|
+
matched: false,
|
|
1091
|
+
elapsedMs: Date.now() - start,
|
|
1092
|
+
polls: 0,
|
|
1093
|
+
error: bridgeError,
|
|
1469
1094
|
}
|
|
1470
|
-
await new Promise((r) => setTimeout(r, READY_PROBE_POLL_MS))
|
|
1471
1095
|
}
|
|
1096
|
+
const probe = result.probe
|
|
1472
1097
|
const status: WaitReadyStatus = {
|
|
1473
|
-
ready:
|
|
1474
|
-
elapsedMs:
|
|
1475
|
-
nodes:
|
|
1476
|
-
targets:
|
|
1477
|
-
liveFrameActive:
|
|
1478
|
-
liveFrameChannels:
|
|
1479
|
-
liveFramePublishes:
|
|
1480
|
-
flag:
|
|
1481
|
-
loadingText:
|
|
1482
|
-
externalReady:
|
|
1483
|
-
externalStatus:
|
|
1484
|
-
externalError:
|
|
1485
|
-
suppressedEntryError:
|
|
1486
|
-
errors:
|
|
1487
|
-
bridgeError,
|
|
1488
|
-
}
|
|
1489
|
-
if (options.onProgress && progressIntervalMs > 0 && Date.now() >= nextProgressAt) {
|
|
1490
|
-
options.onProgress(status)
|
|
1098
|
+
ready: result.matched,
|
|
1099
|
+
elapsedMs: result.elapsedMs,
|
|
1100
|
+
nodes: probe?.nodes ?? 0,
|
|
1101
|
+
targets: probe?.targets ?? 0,
|
|
1102
|
+
liveFrameActive: probe?.liveFrameActive ?? false,
|
|
1103
|
+
liveFrameChannels: probe?.liveFrameChannels ?? 0,
|
|
1104
|
+
liveFramePublishes: probe?.liveFramePublishes ?? 0,
|
|
1105
|
+
flag: probe?.flag,
|
|
1106
|
+
loadingText: probe?.loadingText ?? '',
|
|
1107
|
+
externalReady: probe?.externalReady ?? null,
|
|
1108
|
+
externalStatus: probe?.externalStatus ?? '',
|
|
1109
|
+
externalError: probe?.externalError ?? '',
|
|
1110
|
+
suppressedEntryError: probe?.suppressedEntryError ?? '',
|
|
1111
|
+
errors: probe?.errors ?? 0,
|
|
1112
|
+
bridgeError: result.error ?? '',
|
|
1491
1113
|
}
|
|
1114
|
+
if (!status.ready && options.onProgress) options.onProgress(status)
|
|
1492
1115
|
return status
|
|
1493
1116
|
}
|
|
1494
1117
|
|
|
@@ -1499,72 +1122,26 @@ export async function inspectWaitSelector(
|
|
|
1499
1122
|
testId: string,
|
|
1500
1123
|
timeoutMs = 5000,
|
|
1501
1124
|
opts: { gone?: boolean } = {},
|
|
1502
|
-
): Promise<{ found: boolean; node?:
|
|
1503
|
-
// gone-mode inverts the predicate: resolve (found:true) when the node is
|
|
1504
|
-
// confirmed absent from the active visible inspect tree. this is the
|
|
1505
|
-
// positive "the gate/screen left" signal — e.g. waiting for an access-gate
|
|
1506
|
-
// button to disappear after a successful sign-in instead of re-tapping it.
|
|
1507
|
-
// an indeterminate bridge (test hooks not ready yet) never counts as gone,
|
|
1508
|
-
// and visible absence is debounced across two polls so a transient tree swap
|
|
1509
|
-
// doesn't resolve early. `gone` only steers the predicate; the return shape
|
|
1510
|
-
// is the same as presence-mode (callers/the handler don't need a gone flag).
|
|
1125
|
+
): Promise<{ found: boolean; node?: SimSemanticNode; elapsed: number }> {
|
|
1511
1126
|
const gone = opts.gone === true
|
|
1512
|
-
const result = await bridge.send(
|
|
1513
|
-
{
|
|
1514
|
-
type: 'evaluate',
|
|
1515
|
-
code: `(async () => {
|
|
1516
|
-
const start = Date.now()
|
|
1517
|
-
const deadline = start + ${timeoutMs}
|
|
1518
|
-
const gone = ${gone}
|
|
1519
|
-
const ID = ${JSON.stringify(testId)}
|
|
1520
|
-
const present = (node) => !!(node && node.layout && node.layout.width > 0 && node.layout.height > 0)
|
|
1521
|
-
const find = async () => {
|
|
1522
|
-
const t = window.__sootsimTest
|
|
1523
|
-
if (!t) return undefined // bridge not ready — indeterminate, not "gone"
|
|
1524
|
-
try {
|
|
1525
|
-
// gone-mode confirms a gate/screen LEFT — it only needs structural
|
|
1526
|
-
// existence + layout, NOT the full-tree occlusion pass (pruneHidden).
|
|
1527
|
-
// that pass (computeOccludedInspectNodeIds) walks the whole tree and
|
|
1528
|
-
// does O(n²)-ish rect-occlusion testing EVERY poll; on a large
|
|
1529
|
-
// post-login app tree it costs seconds per poll, so --gone can never
|
|
1530
|
-
// get two clean "absent" polls inside budget and falsely reports the
|
|
1531
|
-
// gate as still blocked — the root cause of the 3pc PR-preview outage.
|
|
1532
|
-
// presence-mode keeps pruneHidden so "appeared" still means
|
|
1533
|
-
// visibly-reachable. a departed gate is unmounted or zero-sized, so the
|
|
1534
|
-
// cheap structural+layout check is correct here and ~O(n) cheap.
|
|
1535
|
-
const matches = await t.queryAll?.(gone ? { hasId: ID } : { hasId: ID, pruneHidden: true })
|
|
1536
|
-
if (!Array.isArray(matches)) return undefined
|
|
1537
|
-
return matches.find(present) || null
|
|
1538
|
-
} catch {
|
|
1539
|
-
return undefined
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
let goneStreak = 0
|
|
1543
|
-
while (Date.now() < deadline) {
|
|
1544
|
-
const node = await find()
|
|
1545
|
-
if (gone) {
|
|
1546
|
-
if (node === null) {
|
|
1547
|
-
goneStreak += 1
|
|
1548
|
-
if (goneStreak >= 2) return { found: true, elapsed: Date.now() - start }
|
|
1549
|
-
} else {
|
|
1550
|
-
goneStreak = 0
|
|
1551
|
-
}
|
|
1552
|
-
} else if (present(node)) {
|
|
1553
|
-
return { found: true, node, elapsed: Date.now() - start }
|
|
1554
|
-
}
|
|
1555
|
-
await new Promise((r) => setTimeout(r, 80))
|
|
1556
|
-
}
|
|
1557
|
-
return { found: false, elapsed: Date.now() - start }
|
|
1558
|
-
})()`,
|
|
1559
|
-
},
|
|
1127
|
+
const result: SemanticWaitResult = await bridge.send(
|
|
1560
1128
|
{
|
|
1561
|
-
|
|
1129
|
+
type: 'waitFor',
|
|
1130
|
+
waitForOptions: {
|
|
1131
|
+
condition: { type: gone ? 'absent' : 'present', testID: testId },
|
|
1132
|
+
timeoutMs,
|
|
1133
|
+
},
|
|
1562
1134
|
},
|
|
1135
|
+
{ timeoutMs: timeoutMs + 1_000 },
|
|
1563
1136
|
)
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
node
|
|
1567
|
-
|
|
1137
|
+
if (result?.error) throw new Error(result.error)
|
|
1138
|
+
if (result?.node && !isSimSemanticTree([result.node])) {
|
|
1139
|
+
throw new Error('waitFor returned an invalid semantic node')
|
|
1140
|
+
}
|
|
1141
|
+
return {
|
|
1142
|
+
found: result?.matched === true,
|
|
1143
|
+
...(result?.node ? { node: result.node } : {}),
|
|
1144
|
+
elapsed: result?.elapsedMs ?? timeoutMs,
|
|
1568
1145
|
}
|
|
1569
1146
|
}
|
|
1570
1147
|
|