rnxsim 0.0.0 → 0.1.312

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +512 -0
  3. package/detox/colors.ts +54 -0
  4. package/detox/config-loader.ts +135 -0
  5. package/detox/element-types.ts +39 -0
  6. package/detox/expectations.ts +537 -0
  7. package/detox/gestures.ts +480 -0
  8. package/detox/index.ts +2118 -0
  9. package/detox/jest-preset.cjs +51 -0
  10. package/detox/jest-setup-after-env.cjs +18 -0
  11. package/detox/matchers.ts +30 -0
  12. package/detox/motion-change.cjs +109 -0
  13. package/detox/motion-change.d.cts +35 -0
  14. package/detox/navigation.ts +43 -0
  15. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  16. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  17. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  18. package/detox/screenshots/theme-blue-toggle.png +0 -0
  19. package/detox/screenshots/theme-blue.png +0 -0
  20. package/detox/screenshots/theme-red-toggle.png +0 -0
  21. package/detox/screenshots/theme-red.png +0 -0
  22. package/dist-cli/bin.js +15 -0
  23. package/dist-cli/chunks/agent-NXVDDCFG.js +48 -0
  24. package/dist-cli/chunks/agent-wrapper-DXJCFKL3.js +17 -0
  25. package/dist-cli/chunks/app-fonts-W5AJZQJF.js +4 -0
  26. package/dist-cli/chunks/assert-3LOJEUDT.js +49 -0
  27. package/dist-cli/chunks/auth-MFTYYPNX.js +12 -0
  28. package/dist-cli/chunks/beta-GT3NN67B.js +4 -0
  29. package/dist-cli/chunks/camera-6EAK7CUL.js +22 -0
  30. package/dist-cli/chunks/chunk-274UD5FD.js +4 -0
  31. package/dist-cli/chunks/chunk-2SA2VTGP.js +5 -0
  32. package/dist-cli/chunks/chunk-33FKQH54.js +5 -0
  33. package/dist-cli/chunks/chunk-37I5AIS7.js +3 -0
  34. package/dist-cli/chunks/chunk-3D57IBPC.js +4 -0
  35. package/dist-cli/chunks/chunk-4BXCZHAU.js +4 -0
  36. package/dist-cli/chunks/chunk-5OX7ANRA.js +5 -0
  37. package/dist-cli/chunks/chunk-66NF2KNG.js +9 -0
  38. package/dist-cli/chunks/chunk-6UKJB2VC.js +125 -0
  39. package/dist-cli/chunks/chunk-7INBNA3U.js +26 -0
  40. package/dist-cli/chunks/chunk-7VWOSTD5.js +5 -0
  41. package/dist-cli/chunks/chunk-AY47WKS5.js +23 -0
  42. package/dist-cli/chunks/chunk-B7KMNHX7.js +4 -0
  43. package/dist-cli/chunks/chunk-BKNOLP3J.js +4 -0
  44. package/dist-cli/chunks/chunk-BRIZOR4K.js +28 -0
  45. package/dist-cli/chunks/chunk-DM6KUAOZ.js +4 -0
  46. package/dist-cli/chunks/chunk-EUAVBTKU.js +4 -0
  47. package/dist-cli/chunks/chunk-FTC4M26Q.js +4 -0
  48. package/dist-cli/chunks/chunk-FWYYCUAZ.js +4 -0
  49. package/dist-cli/chunks/chunk-G74YIEVS.js +7 -0
  50. package/dist-cli/chunks/chunk-GZDHEONW.js +507 -0
  51. package/dist-cli/chunks/chunk-HG7ORJMT.js +5 -0
  52. package/dist-cli/chunks/chunk-HMDOLCKG.js +15 -0
  53. package/dist-cli/chunks/chunk-JG6S4RSH.js +4 -0
  54. package/dist-cli/chunks/chunk-KR5BGMBZ.js +4 -0
  55. package/dist-cli/chunks/chunk-KZW5WBGU.js +983 -0
  56. package/dist-cli/chunks/chunk-L24Z3CRS.js +30 -0
  57. package/dist-cli/chunks/chunk-LDQ7HVCW.js +4 -0
  58. package/dist-cli/chunks/chunk-MENV7VLD.js +4 -0
  59. package/dist-cli/chunks/chunk-MNGQ42QE.js +568 -0
  60. package/dist-cli/chunks/chunk-N6A3SXMR.js +4 -0
  61. package/dist-cli/chunks/chunk-NLEMQ4PY.js +6 -0
  62. package/dist-cli/chunks/chunk-NPVGMH2N.js +101 -0
  63. package/dist-cli/chunks/chunk-OR3XTXEB.js +107 -0
  64. package/dist-cli/chunks/chunk-PEPUGJVI.js +4 -0
  65. package/dist-cli/chunks/chunk-Q3RGLERJ.js +142 -0
  66. package/dist-cli/chunks/chunk-SI3BUTQR.js +4 -0
  67. package/dist-cli/chunks/chunk-T4FT6CM7.js +4 -0
  68. package/dist-cli/chunks/chunk-TBMVBI26.js +7 -0
  69. package/dist-cli/chunks/chunk-TCWQJILS.js +5 -0
  70. package/dist-cli/chunks/chunk-TGQNQ6XT.js +766 -0
  71. package/dist-cli/chunks/chunk-TYE3WBSR.js +5 -0
  72. package/dist-cli/chunks/chunk-U4PN6JOT.js +6 -0
  73. package/dist-cli/chunks/chunk-VUFRB347.js +4 -0
  74. package/dist-cli/chunks/chunk-VZBLFMYC.js +10 -0
  75. package/dist-cli/chunks/chunk-WBT32R6N.js +25 -0
  76. package/dist-cli/chunks/chunk-XRGEEPJ5.js +38 -0
  77. package/dist-cli/chunks/chunk-Y2FSZSO7.js +71 -0
  78. package/dist-cli/chunks/chunk-YRV7HR3P.js +74 -0
  79. package/dist-cli/chunks/chunk-YZRFF4YA.js +14 -0
  80. package/dist-cli/chunks/chunk-ZMZTHU7V.js +4 -0
  81. package/dist-cli/chunks/cleanup-H3GWQB63.js +25 -0
  82. package/dist-cli/chunks/cli-version-3QPAJ3LC.js +4 -0
  83. package/dist-cli/chunks/compat-5WG2XJV7.js +20 -0
  84. package/dist-cli/chunks/config-ANMBVVLL.js +13 -0
  85. package/dist-cli/chunks/control-NSGNQFJG.js +4 -0
  86. package/dist-cli/chunks/daemon-JHF4BJ3U.js +4 -0
  87. package/dist-cli/chunks/debug-CHV2O2KF.js +200 -0
  88. package/dist-cli/chunks/desktop-FIPIK4CH.js +31 -0
  89. package/dist-cli/chunks/detox-CVUJP7K6.js +52 -0
  90. package/dist-cli/chunks/device-6WCUM4W6.js +18 -0
  91. package/dist-cli/chunks/diagnose-I6EMWEII.js +43 -0
  92. package/dist-cli/chunks/disk-cleanup-P27CYFTN.js +4 -0
  93. package/dist-cli/chunks/drivers-HQ6AKZT6.js +4 -0
  94. package/dist-cli/chunks/film-UOQFFYNF.js +74 -0
  95. package/dist-cli/chunks/flow-N27MRRNL.js +4 -0
  96. package/dist-cli/chunks/help-SF6FEGEE.js +4 -0
  97. package/dist-cli/chunks/hidden-runtime-alias-NJYFSJKZ.js +4 -0
  98. package/dist-cli/chunks/home-paths-XOV44JEF.js +4 -0
  99. package/dist-cli/chunks/inspect-CWFRFQTY.js +4 -0
  100. package/dist-cli/chunks/install-desktop-KLQDNXZQ.js +4 -0
  101. package/dist-cli/chunks/login-BPFQENCX.js +28 -0
  102. package/dist-cli/chunks/logout-5A45ZGY7.js +4 -0
  103. package/dist-cli/chunks/maestro-E4CSDMLM.js +104 -0
  104. package/dist-cli/chunks/maestro-generate-CKMOJQX2.js +36 -0
  105. package/dist-cli/chunks/mode-SZYNMIQF.js +26 -0
  106. package/dist-cli/chunks/optional-demo-registry-FRH4SFVG.js +4 -0
  107. package/dist-cli/chunks/perf-VXU2XJAX.js +48 -0
  108. package/dist-cli/chunks/permissions-IRIAOVND.js +13 -0
  109. package/dist-cli/chunks/record-RY7FPLLH.js +72 -0
  110. package/dist-cli/chunks/report-issue-KNWXOE53.js +29 -0
  111. package/dist-cli/chunks/runtime-AR3GJTGK.js +4 -0
  112. package/dist-cli/chunks/screenshot-command-QLALR6GY.js +159 -0
  113. package/dist-cli/chunks/screenshot-layers-OQFMFVRL.js +20 -0
  114. package/dist-cli/chunks/screenshots-capture-6TSRJFGB.js +42 -0
  115. package/dist-cli/chunks/serve-F54J2NQO.js +44 -0
  116. package/dist-cli/chunks/setup-7V6UKX4U.js +32 -0
  117. package/dist-cli/chunks/skills-QQBOSETH.js +50 -0
  118. package/dist-cli/chunks/storage-TAPMKK5O.js +37 -0
  119. package/dist-cli/chunks/store-7NX3S43H.js +4 -0
  120. package/dist-cli/chunks/telemetry-E7BUK6RA.js +4 -0
  121. package/dist-cli/chunks/timeline-IAM564NA.js +24 -0
  122. package/dist-cli/chunks/upgrade-CD55QXIX.js +7 -0
  123. package/dist-cli/chunks/upload-GZ6LIMYM.js +4 -0
  124. package/dist-cli/chunks/version-3DYJ5AS3.js +6 -0
  125. package/dist-cli/chunks/web-6NW6YLPI.js +4 -0
  126. package/dist-cli/chunks/what-happened-CZZDOOVG.js +25 -0
  127. package/dist-lib/agent-daemon-client.cjs +468 -0
  128. package/dist-lib/agent-events.cjs +48 -0
  129. package/dist-lib/agent-identity.cjs +53 -0
  130. package/dist-lib/agent-sessions.cjs +725 -0
  131. package/dist-lib/attached-projects.cjs +526 -0
  132. package/dist-lib/auth/shared-session.cjs +195 -0
  133. package/dist-lib/backend-origin.cjs +70 -0
  134. package/dist-lib/beta.cjs +41 -0
  135. package/dist-lib/beta.mjs +13 -0
  136. package/dist-lib/bridge-constants.cjs +161 -0
  137. package/dist-lib/cli-constants.cjs +32 -0
  138. package/dist-lib/config.cjs +96 -0
  139. package/dist-lib/detox/index.cjs +2863 -0
  140. package/dist-lib/detox/jest-preset.cjs +51 -0
  141. package/dist-lib/dev-bundle-resolution.cjs +223 -0
  142. package/dist-lib/home-paths.cjs +421 -0
  143. package/dist-lib/host/bridge-host.cjs +5600 -0
  144. package/dist-lib/host/fetch-proxy-handler.cjs +373 -0
  145. package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
  146. package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
  147. package/dist-lib/host/websocket-proxy.cjs +214 -0
  148. package/dist-lib/index.cjs +513 -0
  149. package/dist-lib/jump-to-source-babel.cjs +93 -0
  150. package/dist-lib/menu.cjs +620 -0
  151. package/dist-lib/menu.mjs +581 -0
  152. package/dist-lib/metro.cjs +297 -0
  153. package/dist-lib/profiles.cjs +246 -0
  154. package/dist-lib/public-brand.cjs +39 -0
  155. package/dist-lib/render-mode.cjs +54 -0
  156. package/dist-lib/scripts/dev-server-scanner.cjs +1034 -0
  157. package/dist-lib/sdk.cjs +2333 -0
  158. package/dist-lib/sdk.mjs +2240 -0
  159. package/dist-lib/skills.cjs +23911 -0
  160. package/dist-lib/vite.cjs +358 -0
  161. package/package.json +189 -18
  162. package/scripts/dev-server-scanner.ts +941 -0
  163. package/scripts/optional-demo-registry.ts +113 -0
  164. package/scripts/postinstall.cjs +3 -0
  165. package/scripts/preuninstall.cjs +118 -0
  166. package/skills/contrast/SKILL.md +171 -0
  167. package/skills/rnx-debug/SKILL.md +425 -0
  168. package/skills/rnx-perf/SKILL.md +295 -0
  169. package/skills/rnx-setup/SKILL.md +160 -0
  170. package/skills/rnx-test/SKILL.md +292 -0
  171. package/skills/rnx-visual/SKILL.md +122 -0
  172. package/src/agent-daemon-client.ts +410 -0
  173. package/src/agent-events.ts +71 -0
  174. package/src/agent-identity.ts +23 -0
  175. package/src/agent-prompt.ts +71 -0
  176. package/src/agent-sessions.ts +573 -0
  177. package/src/attached-projects.ts +539 -0
  178. package/src/auth/shared-session.ts +204 -0
  179. package/src/backend-origin.ts +49 -0
  180. package/src/beta.ts +19 -0
  181. package/src/bridge-constants.ts +171 -0
  182. package/src/browser-cache.ts +16 -0
  183. package/src/cli-constants.ts +1 -0
  184. package/src/cli-version.ts +32 -0
  185. package/src/codex-client.ts +215 -0
  186. package/src/config.ts +134 -0
  187. package/src/dev-bundle-resolution.ts +236 -0
  188. package/src/disk-cleanup.ts +466 -0
  189. package/src/home-paths.ts +552 -0
  190. package/src/host/agent-host.ts +666 -0
  191. package/src/host/bridge-host.ts +2626 -0
  192. package/src/host/fetch-proxy-handler.ts +397 -0
  193. package/src/host/fetch-proxy-overrides.ts +51 -0
  194. package/src/host/open-url.ts +89 -0
  195. package/src/host/websocket-proxy.ts +210 -0
  196. package/src/index.ts +12 -705
  197. package/src/jump-to-source-babel.ts +135 -0
  198. package/src/menu.ts +734 -0
  199. package/src/metro-plugin.ts +139 -0
  200. package/src/native-dev-bundle-url.ts +96 -0
  201. package/src/native-seam-manifest.ts +341 -0
  202. package/src/plist.ts +27 -0
  203. package/src/poll-until.ts +92 -0
  204. package/src/profiles.ts +179 -0
  205. package/src/public-brand.ts +9 -0
  206. package/src/render-mode.ts +27 -0
  207. package/src/runtime-assets.ts +228 -0
  208. package/src/runtime-delivery.ts +54 -0
  209. package/src/screenshots/compose.ts +422 -0
  210. package/src/screenshots/frame-compose.ts +454 -0
  211. package/src/screenshots/orchestrate.ts +256 -0
  212. package/src/screenshots/org-deck.ts +91 -0
  213. package/src/screenshots/registry.ts +58 -0
  214. package/src/screenshots/schema.ts +364 -0
  215. package/src/sdk.ts +7 -0
  216. package/src/settings-bundle.ts +418 -0
  217. package/src/skills/builtin/a11y-review.ts +132 -0
  218. package/src/skills/builtin/compat-check.ts +71 -0
  219. package/src/skills/builtin/maestro-test.ts +121 -0
  220. package/src/skills/builtin/perf-profile.ts +90 -0
  221. package/src/skills/builtin/screenshot-all.ts +52 -0
  222. package/src/skills/builtin/visual-diff.ts +100 -0
  223. package/src/skills/registry.ts +107 -0
  224. package/src/skills/types.ts +41 -0
  225. package/src/team-machine-prompt.ts +73 -0
  226. package/src/vite-plugin-one.ts +190 -0
  227. package/src/vite-plugin.ts +1750 -0
  228. package/src/worklets-babel.ts +134 -0
@@ -0,0 +1,2240 @@
1
+ /*! rnx v0.1.312 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
+
3
+ // cli/commands/inspect/core.ts
4
+ function resolveMaxMsFlag(args, fallbackMs) {
5
+ const aliases = ["--max-ms", "--maxMs", "--maxms", "--max_ms"];
6
+ for (const flag of aliases) {
7
+ const i = args.indexOf(flag);
8
+ if (i >= 0 && args[i + 1]) {
9
+ const n = Number(args[i + 1]);
10
+ if (Number.isFinite(n)) return Math.max(100, n);
11
+ }
12
+ }
13
+ return fallbackMs;
14
+ }
15
+ var INSPECT_PICK_COMMANDS = /* @__PURE__ */ new Set([
16
+ "tap",
17
+ "double-tap",
18
+ "tap-text",
19
+ "tap-id",
20
+ "long-press",
21
+ "touch"
22
+ ]);
23
+ function isInspectPickCommand(subcommand) {
24
+ return typeof subcommand === "string" && INSPECT_PICK_COMMANDS.has(subcommand);
25
+ }
26
+ async function isInspectModeActive(bridge) {
27
+ const active = await bridge.send({
28
+ type: "evaluate",
29
+ code: "window.__sootsimEngineState?.inspectActive === true"
30
+ });
31
+ return active === true;
32
+ }
33
+ async function shouldSkipAutoSettleForInspectPick(bridge, subcommand) {
34
+ if (!isInspectPickCommand(subcommand)) return false;
35
+ try {
36
+ return await isInspectModeActive(bridge);
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+ async function inspectNodeCount(bridge) {
42
+ const count = await bridge.send({
43
+ type: "evaluate",
44
+ code: "(async () => await window.__sootsimTest.getNodeCount())()"
45
+ });
46
+ return { nodes: typeof count === "number" ? count : 0 };
47
+ }
48
+ async function inspectTree(bridge, depth = 5, opts) {
49
+ const optsArg = opts?.format ? `, ${JSON.stringify(opts)}` : "";
50
+ const tree = await bridge.send({
51
+ type: "evaluate",
52
+ code: `(async () => await window.__sootsimTest.dumpTree(${depth}${optsArg}))()`
53
+ });
54
+ if (opts?.format === "boxes" && typeof tree === "string") {
55
+ try {
56
+ return { depth, tree: JSON.parse(tree) };
57
+ } catch {
58
+ return { depth, tree };
59
+ }
60
+ }
61
+ return { depth, tree };
62
+ }
63
+ async function inspectUrl(bridge) {
64
+ const url = await bridge.send({ type: "evaluate", code: "window.location.href" });
65
+ return { url: typeof url === "string" ? url : "" };
66
+ }
67
+ async function inspectDescribe(bridge, dumpOpts) {
68
+ const code = `(async () => {
69
+ const t = window.__sootsimTest
70
+ const mainShell = window.SootSim?.bridges?.mainShell
71
+ const kb = window.__sootsimKeyboard
72
+ if (!t) return { error: 'no test bridge' }
73
+
74
+ let shell = null
75
+ try {
76
+ shell = typeof mainShell?.getState === 'function' ? await mainShell.getState() : null
77
+ } catch {}
78
+
79
+ const opts = ${JSON.stringify(dumpOpts)}
80
+ const needsRecursiveDump = opts.verbose || opts.subtreeRoot || opts.includeOccluded
81
+ const globToRegex = (glob) => {
82
+ const escaped = String(glob).replace(/[.+^$(){}|[\\]\\\\]/g, '\\\\$&')
83
+ const body = escaped.replace(/\\*/g, '.*').replace(/\\?/g, '.')
84
+ return new RegExp('^' + body + '$', 'i')
85
+ }
86
+ // clipping a describe line is a DISPLAY decision. opts.fullText (set by
87
+ // \`describe --json\`) keeps the real string, so a bundle-eval stack or a
88
+ // red-box message reaches the caller whole instead of ending in an
89
+ // ellipsis nothing downstream can undo.
90
+ const clipForDisplay = (value, max) => {
91
+ const text = typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''
92
+ if (opts.fullText || text.length <= max) return text
93
+ return text.slice(0, max - 1).trimEnd() + '\u2026'
94
+ }
95
+ const formatInspectableTree = async () => {
96
+ if (typeof t.listInspectable !== 'function') return null
97
+ const list = await t.listInspectable({})
98
+ if (!Array.isArray(list)) return null
99
+ const filter = typeof opts.filter === 'string' ? opts.filter.toLowerCase() : ''
100
+ const testIdRegex = opts.testIdLike ? globToRegex(opts.testIdLike) : null
101
+ const onlyRegex = opts.onlyGlob ? globToRegex(opts.onlyGlob) : null
102
+ const lines = []
103
+ let matched = 0
104
+ for (const node of list) {
105
+ const testID = node?.testID || node?.testId || ''
106
+ const role = node?.role || ''
107
+ const label = node?.accessibilityLabel || node?.label || ''
108
+ const text = node?.text || ''
109
+ if (testIdRegex && !testIdRegex.test(testID)) continue
110
+ const joined = [role, label, text, testID].filter(Boolean).join(' ')
111
+ if (onlyRegex && !onlyRegex.test(joined)) continue
112
+ if (filter && !joined.toLowerCase().includes(filter)) continue
113
+ matched++
114
+
115
+ const type = String(node?.type || 'node').toLowerCase()
116
+ const handlers = Array.isArray(node?.handlers) ? node.handlers : []
117
+ const parts = []
118
+ if (role) parts.push('[' + role + ']')
119
+ else if (handlers.length > 0) parts.push('[tap]')
120
+ else parts.push('<' + type + '>')
121
+ const visibleText = clipForDisplay(text, 50)
122
+ if (visibleText) parts.push(JSON.stringify(visibleText))
123
+ if (testID) parts.push('#' + testID)
124
+ if (label && label !== text) {
125
+ parts.push('label=' + JSON.stringify(clipForDisplay(label, 50)))
126
+ }
127
+ if (!opts.hideXy) {
128
+ const abs = node?.absolute || {}
129
+ const layout = node?.layout || {}
130
+ const x = Math.round(Number(abs.x) || 0)
131
+ const y = Math.round(Number(abs.y) || 0)
132
+ const w = Math.round(Number(layout.width) || 0)
133
+ const h = Math.round(Number(layout.height) || 0)
134
+ parts.push('@(' + x + ',' + y + ') ' + w + 'x' + h)
135
+ }
136
+ if (handlers.length > 0) parts.push('(tap)')
137
+ lines.push(' ' + parts.join(' '))
138
+ }
139
+ return { tree: lines.join('\\n'), matched }
140
+ }
141
+
142
+ let tree = ''
143
+ const compactResult = needsRecursiveDump ? null : await formatInspectableTree()
144
+ if (compactResult) {
145
+ tree = compactResult.tree
146
+ } else {
147
+ tree = await t.dumpTree(12, opts)
148
+ }
149
+ const nodeCount = (await t.getNodeCount?.()) || 0
150
+ const keyboard = kb && typeof kb.getLayout === 'function' ? kb.getLayout() : null
151
+ return { tree, shell, nodeCount, keyboard }
152
+ })()`;
153
+ const result = await bridge.send({ type: "evaluate", code });
154
+ return result ?? {};
155
+ }
156
+ function scoreLayoutTextContrast(input) {
157
+ const required = input.fontSize >= 18 || input.fontSize >= 14 && (input.fontWeight ?? 400) >= 700 ? 3 : 4.5;
158
+ const { normal, background } = input;
159
+ if (normal.width <= 0 || normal.height <= 0 || normal.width !== background.width || normal.height !== background.height) {
160
+ return { ratio: null, required, reason: "capture dimensions do not match" };
161
+ }
162
+ const left = Math.max(0, Math.floor(input.box.x));
163
+ const top = Math.max(0, Math.floor(input.box.y));
164
+ const right = Math.min(normal.width, Math.ceil(input.box.x + input.box.w));
165
+ const bottom = Math.min(normal.height, Math.ceil(input.box.y + input.box.h));
166
+ if (right <= left || bottom <= top) {
167
+ return { ratio: null, required, reason: "text is outside the captured frame" };
168
+ }
169
+ const linear = (value) => {
170
+ const channel = value / 255;
171
+ return channel <= 0.04045 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
172
+ };
173
+ const ratioOf = (foreground, behind) => {
174
+ const foregroundLuminance = 0.2126 * linear(foreground[0]) + 0.7152 * linear(foreground[1]) + 0.0722 * linear(foreground[2]);
175
+ const backgroundLuminance = 0.2126 * linear(behind[0]) + 0.7152 * linear(behind[1]) + 0.0722 * linear(behind[2]);
176
+ return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05) / (Math.min(foregroundLuminance, backgroundLuminance) + 0.05);
177
+ };
178
+ const hex = (color) => `#${color.slice(0, 3).map((value) => Math.round(value).toString(16).padStart(2, "0")).join("")}`;
179
+ const area = (right - left) * (bottom - top);
180
+ const step = Math.max(1, Math.floor(Math.sqrt(area / 12e3)));
181
+ const offsetAt = (x, y) => (y * normal.width + x) * 4;
182
+ const deltaAt = (offset) => Math.max(
183
+ Math.abs(normal.data[offset] - background.data[offset]),
184
+ Math.abs(normal.data[offset + 1] - background.data[offset + 1]),
185
+ Math.abs(normal.data[offset + 2] - background.data[offset + 2])
186
+ );
187
+ let strongestDelta = 0;
188
+ let strongestForeground = null;
189
+ let opaqueBackground = null;
190
+ for (let y = top; y < bottom; y += step) {
191
+ for (let x = left; x < right; x += step) {
192
+ const offset = offsetAt(x, y);
193
+ if (background.data[offset + 3] < 250) continue;
194
+ const behind = [
195
+ background.data[offset],
196
+ background.data[offset + 1],
197
+ background.data[offset + 2]
198
+ ];
199
+ opaqueBackground ?? (opaqueBackground = behind);
200
+ const delta = deltaAt(offset);
201
+ if (delta <= strongestDelta) continue;
202
+ strongestDelta = delta;
203
+ strongestForeground = [
204
+ normal.data[offset],
205
+ normal.data[offset + 1],
206
+ normal.data[offset + 2]
207
+ ];
208
+ }
209
+ }
210
+ let bestRatio = Infinity;
211
+ let bestForeground = null;
212
+ let bestBackground = null;
213
+ const consider = (foreground, behind) => {
214
+ const ratio = ratioOf(foreground, behind);
215
+ if (ratio >= bestRatio) return;
216
+ bestRatio = ratio;
217
+ bestForeground = foreground;
218
+ bestBackground = behind;
219
+ };
220
+ if (strongestDelta >= 2 && strongestForeground) {
221
+ for (let y = top; y < bottom; y += step) {
222
+ for (let x = left; x < right; x += step) {
223
+ const offset = offsetAt(x, y);
224
+ if (background.data[offset + 3] < 250 || deltaAt(offset) < 2) {
225
+ continue;
226
+ }
227
+ consider(strongestForeground, [
228
+ background.data[offset],
229
+ background.data[offset + 1],
230
+ background.data[offset + 2]
231
+ ]);
232
+ }
233
+ }
234
+ } else if (opaqueBackground) {
235
+ consider(opaqueBackground, opaqueBackground);
236
+ }
237
+ if (!bestForeground || !bestBackground || !Number.isFinite(bestRatio)) {
238
+ return {
239
+ ratio: null,
240
+ required,
241
+ reason: "could not isolate opaque rendered text pixels"
242
+ };
243
+ }
244
+ return {
245
+ ratio: Math.round(bestRatio * 100) / 100,
246
+ required,
247
+ foreground: hex(bestForeground),
248
+ background: hex(bestBackground)
249
+ };
250
+ }
251
+ function layoutGetNativeEval(opts = {}) {
252
+ const includeStyle = opts.styling === true;
253
+ const filter = opts.filter?.trim().toLowerCase() ?? "";
254
+ return `(async () => {
255
+ const t = window.__sootsimTest
256
+ if (!t || typeof t.listInspectable !== 'function') return []
257
+ const list = await t.listInspectable({})
258
+ if (!Array.isArray(list)) return []
259
+ const includeStyle = ${JSON.stringify(includeStyle)}
260
+ const scoreContrast = ${scoreLayoutTextContrast.toString()}
261
+ // narrows by ELEMENT, not by style field: fewer rows, full detail on each.
262
+ const filter = ${JSON.stringify(filter)}
263
+ const matchesFilter = (n, text) => {
264
+ if (!filter) return true
265
+ const fields = [text, n.testID || n.testId, n.accessibilityRole || n.role, n.type]
266
+ return fields.some((f) => typeof f === 'string' && f.toLowerCase().includes(filter))
267
+ }
268
+
269
+ const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0)
270
+ const round = (v) => Math.round(num(v))
271
+
272
+ // borderRadius can be authored as a string ("12px") or split per-corner;
273
+ // prefer the uniform borderRadius, else the largest corner radius present.
274
+ const radiusOf = (style) => {
275
+ if (!style) return 0
276
+ const keys = ['borderRadius','borderTopLeftRadius','borderTopRightRadius','borderBottomLeftRadius','borderBottomRightRadius']
277
+ let max = 0
278
+ for (const k of keys) {
279
+ const raw = style[k]
280
+ const n = typeof raw === 'number' ? raw : (typeof raw === 'string' ? parseFloat(raw) : NaN)
281
+ if (Number.isFinite(n)) max = Math.max(max, n)
282
+ }
283
+ return Math.round(max)
284
+ }
285
+ const fontSizeOf = (style) => {
286
+ if (!style) return 0
287
+ const raw = style.fontSize
288
+ const n = typeof raw === 'number' ? raw : (typeof raw === 'string' ? parseFloat(raw) : NaN)
289
+ return Number.isFinite(n) ? Math.round(n) : 0
290
+ }
291
+ // styling extractors \u2014 omit boring defaults so the agent only sees notable
292
+ // styling (low opacity, struck text, clipping, a real border/color).
293
+ const colorStr = (v) => {
294
+ if (v == null) return undefined
295
+ const s = String(v).split(' ').join('')
296
+ return s && s !== 'transparent' && s !== 'rgba(0,0,0,0)' ? s : undefined
297
+ }
298
+ const opacityOf = (style) => {
299
+ const o = style && style.opacity
300
+ const n = typeof o === 'number' ? o : (typeof o === 'string' ? parseFloat(o) : NaN)
301
+ return Number.isFinite(n) && n < 1 ? Math.round(n * 100) / 100 : undefined
302
+ }
303
+ const weightOf = (style) => {
304
+ const w = style && style.fontWeight
305
+ const n = w === 'bold' ? 700 : (typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN))
306
+ return Number.isFinite(n) && n >= 600 ? n : undefined
307
+ }
308
+ const decoOf = (style) => {
309
+ const d = style && (style.textDecorationLine || style.textDecoration)
310
+ return d && d !== 'none' && d !== 'normal' ? String(d).split(' ')[0] : undefined
311
+ }
312
+ const overflowOf = (style) => (style && style.overflow === 'hidden' ? 'hidden' : undefined)
313
+ const borderWOf = (style) => {
314
+ if (!style) return undefined
315
+ const keys = ['borderWidth','borderTopWidth','borderBottomWidth','borderLeftWidth','borderRightWidth']
316
+ let max = 0
317
+ for (const k of keys) {
318
+ const r = style[k]
319
+ const n = typeof r === 'number' ? r : (typeof r === 'string' ? parseFloat(r) : NaN)
320
+ if (Number.isFinite(n)) max = Math.max(max, n)
321
+ }
322
+ return max > 0 ? Math.round(max) : undefined
323
+ }
324
+ // only meaningful alongside a real border width, so it is resolved by the
325
+ // caller below rather than on its own.
326
+ const borderColorOf = (style) => {
327
+ if (!style) return undefined
328
+ const keys = ['borderColor','borderTopColor','borderBottomColor','borderLeftColor','borderRightColor']
329
+ for (const k of keys) {
330
+ const c = colorStr(style[k])
331
+ if (c) return c
332
+ }
333
+ return undefined
334
+ }
335
+
336
+ const isVisible = (n) => {
337
+ const layout = n && n.layout
338
+ if (!layout || layout.width <= 0 || layout.height <= 0) return false
339
+ const abs = (n && (n.absolute || n.absolutePosition)) || null
340
+ if (!abs) return true
341
+ const device = (n && n.device) || {}
342
+ const screenW = Number(device.width) || window.innerWidth || 0
343
+ const screenH = Number(device.height) || window.innerHeight || 0
344
+ if (!screenW || !screenH) return true
345
+ return abs.x + layout.width > 0 && abs.y + layout.height > 0 && abs.x < screenW && abs.y < screenH
346
+ }
347
+
348
+ const elements = list.filter(isVisible).filter((n) => {
349
+ const t = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
350
+ return matchesFilter(n, t)
351
+ }).map((n) => {
352
+ const layout = n.layout || { x: 0, y: 0, width: 0, height: 0 }
353
+ const abs = n.absolute || n.absolutePosition || { x: layout.x, y: layout.y }
354
+ const text = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
355
+ const base = {
356
+ ...(typeof n.nodeId === 'number'
357
+ ? {
358
+ nodeId: n.nodeId,
359
+ ancestorIds: Array.isArray(n.ancestors)
360
+ ? n.ancestors.map((ancestor) => ancestor && ancestor.nodeId).filter((id) => typeof id === 'number')
361
+ : [],
362
+ }
363
+ : {}),
364
+ testID: n.testID || n.testId || undefined,
365
+ role: n.accessibilityRole || n.role || undefined,
366
+ type: typeof n.type === 'string' ? n.type : 'node',
367
+ text,
368
+ box: { x: round(abs.x), y: round(abs.y), w: round(layout.width), h: round(layout.height) },
369
+ borderRadius: 0,
370
+ padding: { t: 0, r: 0, b: 0, l: 0 },
371
+ fontSize: 0,
372
+ }
373
+ if (!includeStyle) return base
374
+ // computedStyle has the resolved values; style is the curated subset \u2014
375
+ // fall back across both so radius/fontSize survive either shape.
376
+ const style = Object.assign({}, n.style || {}, n.computedStyle || {})
377
+ const pad = (n.boxModel && n.boxModel.padding) || {}
378
+ const borderWidth = borderWOf(style)
379
+ return {
380
+ ...base,
381
+ borderRadius: radiusOf(style),
382
+ padding: { t: round(pad.top), r: round(pad.right), b: round(pad.bottom), l: round(pad.left) },
383
+ fontSize: text ? (fontSizeOf(style) || 14) : 0,
384
+ color: text ? colorStr(style.color) : undefined,
385
+ bg: colorStr(style.backgroundColor),
386
+ opacity: opacityOf(style),
387
+ fontWeight: weightOf(style),
388
+ textDecoration: decoOf(style),
389
+ overflow: overflowOf(style),
390
+ borderWidth,
391
+ borderColor: borderWidth ? borderColorOf(style) : undefined,
392
+ }
393
+ })
394
+ const textElements = elements.filter((element) => element.text && element.fontSize > 0)
395
+ if (!includeStyle || textElements.length === 0) return elements
396
+
397
+ const screenshot = window.SootSim && window.SootSim.bridges && window.SootSim.bridges.screenshot
398
+ if (typeof screenshot !== 'function') {
399
+ throw new Error('styled native layout requires the rnx capture bridge')
400
+ }
401
+ const device = list.find((node) => node && node.device)?.device || {}
402
+ const width = Math.round(Number(device.width) || window.innerWidth || 0)
403
+ const height = Math.round(Number(device.height) || window.innerHeight || 0)
404
+ if (!width || !height) throw new Error('native layout has no capture dimensions')
405
+ const capture = async (suppressText) => {
406
+ const dataUrl = await screenshot({
407
+ format: 'png',
408
+ outputWidth: width,
409
+ outputHeight: height,
410
+ ...(suppressText ? { suppressText: true } : {}),
411
+ })
412
+ const image = await new Promise((resolve, reject) => {
413
+ const next = new Image()
414
+ next.onload = () => resolve(next)
415
+ next.onerror = () => reject(new Error('could not decode native layout pixels'))
416
+ next.src = dataUrl
417
+ })
418
+ const canvas = document.createElement('canvas')
419
+ canvas.width = image.naturalWidth
420
+ canvas.height = image.naturalHeight
421
+ const context = canvas.getContext('2d', { willReadFrequently: true })
422
+ if (!context) throw new Error('could not read native layout pixels')
423
+ context.drawImage(image, 0, 0)
424
+ return context.getImageData(0, 0, canvas.width, canvas.height)
425
+ }
426
+ const normal = await capture(false)
427
+ const background = await capture(true)
428
+ for (const element of textElements) {
429
+ element.contrast = scoreContrast({
430
+ normal,
431
+ background,
432
+ box: element.box,
433
+ fontSize: element.fontSize,
434
+ fontWeight: element.fontWeight,
435
+ })
436
+ }
437
+ return elements
438
+ })()`;
439
+ }
440
+ var LAYOUT_GET_NATIVE_EVAL = layoutGetNativeEval();
441
+ async function inspectGetLayout(bridge, opts = {}) {
442
+ const raw = await bridge.send({
443
+ type: "evaluate",
444
+ code: layoutGetNativeEval(opts)
445
+ });
446
+ return Array.isArray(raw) ? raw : [];
447
+ }
448
+ function formatLayoutElements(elements, opts) {
449
+ if (elements.length === 0) return " no visible elements found";
450
+ const lines = elements.map((el) => {
451
+ const ident = el.testID ? `#${el.testID}` : el.selector ? el.selector : el.role ? `[${el.role}]` : `<${el.type}>`;
452
+ const parts = [ident, `@(${el.box.x},${el.box.y})`, `${el.box.w}x${el.box.h}`];
453
+ if (el.borderRadius > 0) parts.push(`radius:${el.borderRadius}`);
454
+ const p = el.padding;
455
+ if (p.t || p.r || p.b || p.l) {
456
+ const uniform = p.t === p.r && p.r === p.b && p.b === p.l;
457
+ parts.push(uniform ? `pad:${p.t}` : `pad:${p.t},${p.r},${p.b},${p.l}`);
458
+ }
459
+ if (el.fontSize > 0) parts.push(`font:${el.fontSize}`);
460
+ if (opts?.styling) {
461
+ if (el.fontWeight) parts.push(`weight:${el.fontWeight}`);
462
+ if (el.color) parts.push(`color:${el.color}`);
463
+ if (el.bg) parts.push(`bg:${el.bg}`);
464
+ if (el.contrast) {
465
+ const score = el.contrast.ratio == null ? `?/${el.contrast.required}` : `${el.contrast.ratio}/${el.contrast.required}${el.contrast.ratio < el.contrast.required ? " FAIL" : ""}`;
466
+ parts.push(`contrast:${score}`);
467
+ if (el.contrast.background) parts.push(`on:${el.contrast.background}`);
468
+ }
469
+ if (el.opacity != null) parts.push(`opacity:${el.opacity}`);
470
+ if (el.textDecoration) parts.push(`deco:${el.textDecoration}`);
471
+ if (el.overflow) parts.push(`overflow:${el.overflow}`);
472
+ if (el.borderWidth) {
473
+ parts.push(
474
+ el.borderColor ? `border:${el.borderWidth}/${el.borderColor}` : `border:${el.borderWidth}`
475
+ );
476
+ }
477
+ }
478
+ if (el.text) {
479
+ const text = el.text.length > 40 ? `${el.text.slice(0, 39)}\u2026` : el.text;
480
+ parts.push(`"${text}"`);
481
+ }
482
+ return ` ${parts.join(" ")}`;
483
+ });
484
+ return lines.join("\n");
485
+ }
486
+ var INSPECT_A11Y_CODE = `(async () => {
487
+ const t = window.__sootsimTest
488
+ if (!t) return []
489
+
490
+ const readLayout = (n) => n?.layout || null
491
+ const readAbs = (n) => n?.absolutePosition || n?.absolute || null
492
+ const readRole = (n) => n?.accessibilityRole || n?.role || null
493
+ const readLabel = (n) => n?.accessibilityLabel || n?.label || n?.text || null
494
+ const readHint = (n) => n?.accessibilityHint || n?.hint || null
495
+ const readState = (n) => n?.accessibilityState || n?.state || null
496
+ const readTestId = (n) => n?.testID || n?.testId || null
497
+ const readType = (n) => typeof n?.type === 'string' ? n.type.toLowerCase() : n?.type
498
+ const readPressable = (n) => {
499
+ if (n?.pressable) return true
500
+ const handlers = Array.isArray(n?.handlers) ? n.handlers : []
501
+ const role = readRole(n)
502
+ return handlers.length > 0 || role === 'button' || role === 'link' || role === 'tab'
503
+ }
504
+ const isTextInput = (n) => n?.isTextInput === true
505
+ const isTextNode = (n) => readType(n) === 'text'
506
+ const isVisibleTarget = (n) => {
507
+ const layout = readLayout(n)
508
+ if (!layout || layout.width <= 0 || layout.height <= 0) return false
509
+ const abs = readAbs(n)
510
+ if (!abs) return true
511
+ const device = n?.device || {}
512
+ const screenW = Number(device.width) || window.innerWidth || 0
513
+ const screenH = Number(device.height) || window.innerHeight || 0
514
+ return (
515
+ abs.x + layout.width > 0 &&
516
+ abs.y + layout.height > 0 &&
517
+ abs.x < screenW &&
518
+ abs.y < screenH
519
+ )
520
+ }
521
+ const hasAccessibleSignal = (n) => {
522
+ const role = readRole(n)
523
+ const label = readLabel(n)
524
+ const hint = readHint(n)
525
+ if (role) return true
526
+ if (hint) return true
527
+ if (isTextInput(n)) return true
528
+ if (readPressable(n)) return true
529
+ if (isTextNode(n) && n?.text) return true
530
+ if (typeof label === 'string' && label.length <= 30 && label !== n?.text) return true
531
+ return false
532
+ }
533
+ const normalize = (n) => {
534
+ const layout = readLayout(n)
535
+ const abs = readAbs(n)
536
+ const role = readRole(n) || (readPressable(n) ? 'button' : isTextInput(n) ? 'textfield' : isTextNode(n) ? 'statictext' : 'none')
537
+ return {
538
+ role,
539
+ label: readLabel(n),
540
+ hint: readHint(n),
541
+ state: readState(n),
542
+ testID: readTestId(n),
543
+ position: abs ? { x: Math.round(abs.x), y: Math.round(abs.y) } : null,
544
+ size: layout ? { w: Math.round(layout.width), h: Math.round(layout.height) } : null,
545
+ }
546
+ }
547
+
548
+ if (typeof t.listInspectable === 'function') {
549
+ const list = await t.listInspectable({})
550
+ if (Array.isArray(list)) {
551
+ return list.filter(n => isVisibleTarget(n) && hasAccessibleSignal(n)).map(normalize)
552
+ }
553
+ }
554
+
555
+ const all = await t.queryAll({ pruneHidden: true })
556
+ return all
557
+ .filter(n => isVisibleTarget(n) && hasAccessibleSignal(n))
558
+ .map(normalize)
559
+ })()`;
560
+ async function inspectAccessibilityTree(bridge) {
561
+ const nodes = await bridge.send({ type: "evaluate", code: INSPECT_A11Y_CODE });
562
+ return Array.isArray(nodes) ? nodes : [];
563
+ }
564
+ var FIND_INSPECTABLE_TARGETS = `
565
+ const fromInspectable = async () => {
566
+ if (typeof t.listInspectable !== 'function') return null
567
+ const list = await t.listInspectable({})
568
+ if (!Array.isArray(list)) return null
569
+ return list.map((n) => {
570
+ const role = n.accessibilityRole ?? n.role
571
+ const label = n.accessibilityLabel ?? n.label
572
+ const layout = n.layout
573
+ const absolutePosition = n.absolutePosition ?? n.absolute
574
+ const screenW = Number(n.device?.width) || window.innerWidth || 0
575
+ const screenH = Number(n.device?.height) || window.innerHeight || 0
576
+ const visibleFrame = layout && absolutePosition
577
+ ? {
578
+ x: Math.max(0, absolutePosition.x),
579
+ y: Math.max(0, absolutePosition.y),
580
+ width: Math.max(
581
+ 0,
582
+ Math.min(screenW, absolutePosition.x + layout.width) -
583
+ Math.max(0, absolutePosition.x),
584
+ ),
585
+ height: Math.max(
586
+ 0,
587
+ Math.min(screenH, absolutePosition.y + layout.height) -
588
+ Math.max(0, absolutePosition.y),
589
+ ),
590
+ }
591
+ : null
592
+ const handlers = Array.isArray(n.handlers) ? n.handlers : []
593
+ const pressable =
594
+ handlers.length > 0 ||
595
+ role === 'button' ||
596
+ role === 'link' ||
597
+ role === 'tab'
598
+ return {
599
+ type: typeof n.type === 'string' ? n.type.toLowerCase() : n.type,
600
+ id: n.id ?? n.nodeId,
601
+ nodeId: n.nodeId,
602
+ testID: n.testID ?? n.testId,
603
+ text: n.text,
604
+ layout,
605
+ absolutePosition,
606
+ visibleFrame,
607
+ style: n.computedStyle ?? n.style ?? {},
608
+ childCount: n.childCount ?? 0,
609
+ pressed: n.pressed ?? false,
610
+ pressable,
611
+ isTextInput: n.isTextInput ?? false,
612
+ accessible: n.accessible ?? true,
613
+ accessibilityLabel: label,
614
+ accessibilityRole: role,
615
+ accessibilityHint: n.accessibilityHint,
616
+ accessibilityState: n.accessibilityState,
617
+ accessibilityValue: n.accessibilityValue,
618
+ }
619
+ })
620
+ }
621
+ const isVisibleTarget = (n) => {
622
+ if (!n?.layout) return false
623
+ const frame = n.visibleFrame
624
+ if (frame) return frame.width > 0 && frame.height > 0
625
+ const abs = n.absolutePosition
626
+ if (!abs) return n.layout.width > 0 && n.layout.height > 0
627
+ const screenW = window.innerWidth || 0
628
+ const screenH = window.innerHeight || 0
629
+ return (
630
+ abs.x + n.layout.width > 0 &&
631
+ abs.y + n.layout.height > 0 &&
632
+ abs.x < screenW &&
633
+ abs.y < screenH
634
+ )
635
+ }
636
+ `;
637
+ function resolveFindMode(q) {
638
+ if (q.testId) {
639
+ return {
640
+ mode: "testid",
641
+ code: `(async () => {
642
+ const t = window.__sootsimTest
643
+ if (!t) return null
644
+ return (await t.findByTestId(${JSON.stringify(q.testId)})) || (await t.findById(${JSON.stringify(q.testId)}))
645
+ })()`
646
+ };
647
+ }
648
+ if (q.role) {
649
+ return {
650
+ mode: "role",
651
+ code: `(async () => {
652
+ const t = window.__sootsimTest
653
+ if (!t) return []
654
+ return await t.queryAll({ hasRole: ${JSON.stringify(q.role)}, pruneHidden: true })
655
+ })()`
656
+ };
657
+ }
658
+ if (q.type) {
659
+ return {
660
+ mode: "type",
661
+ code: `(async () => {
662
+ const t = window.__sootsimTest
663
+ if (!t) return []
664
+ return await t.queryAll({ type: ${JSON.stringify(q.type)}, pruneHidden: true })
665
+ })()`
666
+ };
667
+ }
668
+ if (q.pressable) {
669
+ return {
670
+ mode: "pressable",
671
+ code: `(async () => {
672
+ const t = window.__sootsimTest
673
+ if (!t) return []
674
+ ${FIND_INSPECTABLE_TARGETS}
675
+ const inspectable = await fromInspectable()
676
+ if (inspectable) return inspectable.filter(n => n.pressable && isVisibleTarget(n))
677
+ const all = await t.queryAll({ pruneHidden: true })
678
+ return all.filter(n => n.pressable && isVisibleTarget(n))
679
+ })()`
680
+ };
681
+ }
682
+ if (q.interactive) {
683
+ return {
684
+ mode: "interactive-targets",
685
+ code: `(async () => {
686
+ const t = window.__sootsimTest
687
+ if (!t) return []
688
+ ${FIND_INSPECTABLE_TARGETS}
689
+ const inspectable = await fromInspectable()
690
+ if (inspectable) {
691
+ return inspectable.filter(n => n.pressable && isVisibleTarget(n))
692
+ }
693
+ const all = await t.queryAll({ pruneHidden: true })
694
+ return all.filter(n => n.pressable && isVisibleTarget(n))
695
+ })()`
696
+ };
697
+ }
698
+ if (q.visible) {
699
+ return {
700
+ mode: "visible",
701
+ code: `(async () => {
702
+ const t = window.__sootsimTest
703
+ if (!t) return []
704
+ const all = await t.queryAll({ pruneHidden: true })
705
+ return all.filter(n => n.layout && n.layout.width > 0 && n.layout.height > 0)
706
+ })()`
707
+ };
708
+ }
709
+ if (q.text) {
710
+ return {
711
+ mode: "text",
712
+ code: `(async () => {
713
+ const t = window.__sootsimTest
714
+ if (!t) return null
715
+ return await t.findByText(${JSON.stringify(q.text)})
716
+ })()`
717
+ };
718
+ }
719
+ return null;
720
+ }
721
+ async function inspectFind(bridge, q) {
722
+ const resolved = resolveFindMode(q);
723
+ if (!resolved) return null;
724
+ const result = await bridge.send({ type: "evaluate", code: resolved.code });
725
+ return { mode: resolved.mode, result };
726
+ }
727
+ function rankInteractive(nodes) {
728
+ return [...nodes].sort((a, b) => scoreInteractive(b) - scoreInteractive(a));
729
+ }
730
+ function scoreInteractive(n) {
731
+ let score = 0;
732
+ if (n.testID) score += 100;
733
+ if (typeof n.text === "string" && n.text.trim().length > 0) score += 60;
734
+ if (typeof n.accessibilityLabel === "string" && n.accessibilityLabel.trim().length > 0) {
735
+ score += 30;
736
+ }
737
+ if (n.accessibilityRole) score += 15;
738
+ const w = n.layout?.width ?? 0;
739
+ const h = n.layout?.height ?? 0;
740
+ const area = w * h;
741
+ if (area >= 400 && area <= 6e4) score += 25;
742
+ else if (area > 6e4) score -= 20;
743
+ const y = n.absolutePosition?.y ?? 0;
744
+ if (y < 0) score -= 30;
745
+ return score;
746
+ }
747
+ function tapCommandForNode(n) {
748
+ if (n.testID) return `rnxsim do tap-id ${shellEscape(n.testID)}`;
749
+ const text = typeof n.text === "string" ? n.text.trim() : "";
750
+ if (text.length > 0 && text.length <= 80)
751
+ return `rnxsim do tap-text ${shellEscape(text)}`;
752
+ const x = Math.round(((n.absolutePosition?.x ?? 0) + (n.layout?.width ?? 0) / 2) * 10) / 10;
753
+ const y = Math.round(((n.absolutePosition?.y ?? 0) + (n.layout?.height ?? 0) / 2) * 10) / 10;
754
+ return `rnxsim do tap ${x} ${y}`;
755
+ }
756
+ function shellEscape(value) {
757
+ if (/^[A-Za-z0-9_./@:-]+$/.test(value)) return value;
758
+ return `'${value.replace(/'/g, `'\\''`)}'`;
759
+ }
760
+ var WAIT_READY_PROBE = `(async () => {
761
+ const t = window.__sootsimTest
762
+ let nodes = 0
763
+ try { nodes = (await t?.getNodeCount?.()) || 0 } catch {}
764
+ let targets = 0
765
+ let liveFrameActive = false
766
+ let liveFrameChannels = 0
767
+ let liveFramePublishes = 0
768
+ let loadingText = ''
769
+ const cleanText = (value) =>
770
+ typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''
771
+ const loadingSignal = (value) => {
772
+ const text = cleanText(value)
773
+ if (!text) return ''
774
+ return /^(opening app\\b|looking for your app\\b|building bundle\\b|preparing rnxsim runtime\\b|loading native bundle\\b|loading rnx stubs\\b|loading bundle loader\\b|loading native stubs\\b|loading compat bridge\\b|loading react-native shim\\b|loading react bridge\\b|loading reconciler\\b|loading replacement modules\\b|injecting rnx modules\\b|warming sqlite engine\\b|capturing root component\\b|reloading app\\b|loading(?:[.!\u2026]*)?$|connecting(?:[.!\u2026]*)?$)/i.test(text)
775
+ ? text
776
+ : ''
777
+ }
778
+ try {
779
+ const loading = window.__sootsimConnectRNLoading
780
+ if (loading && loading.active === true) {
781
+ loadingText = [loading.title, loading.detail].map(cleanText).filter(Boolean).join(' ')
782
+ }
783
+ } catch {}
784
+ try {
785
+ if (typeof t?.listInspectable === 'function') {
786
+ const list = await t.listInspectable({})
787
+ if (Array.isArray(list)) {
788
+ const screenW = window.innerWidth || 0
789
+ const screenH = window.innerHeight || 0
790
+ const isVisible = (n) => {
791
+ const layout = n?.layout
792
+ if (!layout || layout.width <= 0 || layout.height <= 0) return false
793
+ const abs = n?.absolutePosition || n?.absolute
794
+ if (!abs) return true
795
+ const device = n?.device || {}
796
+ const w = Number(device.width) || screenW
797
+ const h = Number(device.height) || screenH
798
+ return (
799
+ abs.x + layout.width > 0 &&
800
+ abs.y + layout.height > 0 &&
801
+ abs.x < w &&
802
+ abs.y < h
803
+ )
804
+ }
805
+ const hasContentSignal = (n) => {
806
+ const role = n?.accessibilityRole || n?.role
807
+ const label = cleanText(n?.accessibilityLabel || n?.label)
808
+ const handlers = Array.isArray(n?.handlers) ? n.handlers : []
809
+ const text = cleanText(n?.text)
810
+ const testID = cleanText(n?.testID || n?.testId)
811
+ const loading = loadingSignal(text) || loadingSignal(label) || loadingSignal(testID)
812
+ if (!loadingText && loading) {
813
+ loadingText = loading
814
+ }
815
+ if (handlers.length > 0) return true
816
+ if (role) return true
817
+ if (n?.isTextInput) return true
818
+ if (text) return true
819
+ if (label) return true
820
+ if (testID && !/^splash|loading/i.test(testID)) return true
821
+ return false
822
+ }
823
+ targets = list.filter(n => isVisible(n) && hasContentSignal(n)).length
824
+ }
825
+ }
826
+ } catch {}
827
+ try {
828
+ const stats = await t?.getImageLoaderStats?.()
829
+ const presentation = stats?.liveFramePresentation
830
+ liveFrameChannels = Number(presentation?.activeChannels) || 0
831
+ liveFramePublishes = Number(presentation?.publishes) || 0
832
+ const previous = window.__sootsimWaitReadyLiveFramePublishes
833
+ liveFrameActive =
834
+ liveFrameChannels > 0 &&
835
+ typeof previous === 'number' &&
836
+ liveFramePublishes > previous
837
+ window.__sootsimWaitReadyLiveFramePublishes = liveFramePublishes
838
+ } catch {}
839
+ let externalReady = null
840
+ let externalStatus = ''
841
+ let externalError = ''
842
+ let suppressedEntryError = ''
843
+ try {
844
+ const getExternalAppState = t?.getExternalAppState
845
+ const external = typeof getExternalAppState === 'function'
846
+ ? await Promise.race([
847
+ getExternalAppState(),
848
+ new Promise((resolve) => setTimeout(
849
+ () => resolve({
850
+ state: {
851
+ ready: false,
852
+ loading: true,
853
+ status: 'waiting for tenant worker...',
854
+ },
855
+ }),
856
+ 1000,
857
+ )),
858
+ ])
859
+ : null
860
+ const state = external?.state
861
+ if (state && typeof state === 'object') {
862
+ externalReady = state.ready === true
863
+ if (typeof state.status === 'string') externalStatus = state.status
864
+ if (typeof state.error === 'string') externalError = state.error
865
+ }
866
+ if (!externalError && Array.isArray(external?.entryErrors)) {
867
+ // suppressed-and-continued entry errors (bundle-loader's patched __r
868
+ // swallows entry throws and moves on) must NOT gate readiness \u2014 the
869
+ // app frequently runs fine after one (e.g. metro's "Unknown named
870
+ // module" probing noise). they surface separately as a diagnostic
871
+ // for the timeout path.
872
+ const isReal = (item) => item && typeof item.message === 'string' && item.message.trim()
873
+ const fatal = external.entryErrors.find((item) => isReal(item) && item.suppressed !== true)
874
+ if (fatal) externalError = fatal.message
875
+ const suppressed = external.entryErrors.find((item) => isReal(item) && item.suppressed === true)
876
+ if (suppressed) suppressedEntryError = suppressed.message
877
+ }
878
+ // once the app has errored its last status ("injecting sootsim
879
+ // modules...") is stale \u2014 reporting it makes a dead app read as one that
880
+ // is still booting.
881
+ if (externalError) loadingText = ''
882
+ else if (externalReady === false && externalStatus) loadingText = externalStatus
883
+ } catch {}
884
+ let errors = 0
885
+ try { errors = window.__sootsimConsole?.count?.()?.errors ?? 0 } catch {}
886
+ return {
887
+ flag: (window).__sootsimExternalAppReady,
888
+ at: (window).__sootsimExternalAppReadyAt || 0,
889
+ nodes,
890
+ targets,
891
+ liveFrameActive,
892
+ liveFrameChannels,
893
+ liveFramePublishes,
894
+ errors,
895
+ loadingText,
896
+ externalReady,
897
+ externalStatus,
898
+ externalError,
899
+ suppressedEntryError,
900
+ }
901
+ })()`;
902
+ var READY_CONTENT_NODE_FLOOR = 100;
903
+ var READY_NODE_STABLE_MS = 750;
904
+ var WAIT_READY_PROGRESS_INTERVAL_MS = 2e3;
905
+ var READY_PROBE_POLL_MS = 150;
906
+ function readyProbeHasContent(probe) {
907
+ return probe.liveFrameActive || probe.targets > 0 || probe.nodes >= READY_CONTENT_NODE_FLOOR;
908
+ }
909
+ function readyProbeHasTargetContent(probe) {
910
+ return probe.liveFrameActive || probe.targets > 0;
911
+ }
912
+ function waitReadyReason(status) {
913
+ if (status.bridgeError) {
914
+ return `sim unreachable: ${status.bridgeError} \u2014 the target tab is closed or no sim is connected (check \`rnxsim list\`, reopen with \`rnxsim open\`)`;
915
+ }
916
+ const base = status.externalError ? `guest app errored: ${status.externalError}` : status.loadingText ? `still showing "${status.loadingText}"` : status.externalReady === false ? "guest app is still loading" : status.flag !== true && status.targets > 0 ? "native content is rendered but the ready signal has not settled" : status.flag !== true ? "guest app has not emitted sootsim:externalAppReady" : status.targets <= 0 ? "ready flag emitted but no visible app content is inspectable yet" : "node tree is still changing";
917
+ return !status.externalError && status.suppressedEntryError ? `${base} (suppressed entry error: ${status.suppressedEntryError})` : base;
918
+ }
919
+ async function inspectWaitReady(bridge, timeoutMs = 2e4, options = {}) {
920
+ const start = Date.now();
921
+ const deadline = start + timeoutMs;
922
+ const progressIntervalMs = options.progressIntervalMs ?? WAIT_READY_PROGRESS_INTERVAL_MS;
923
+ let nextProgressAt = start + progressIntervalMs;
924
+ let lastNodes = -1;
925
+ let nodeStableSince = start;
926
+ let bridgeError = "";
927
+ let last = {
928
+ flag: void 0,
929
+ at: 0,
930
+ nodes: 0,
931
+ targets: 0,
932
+ liveFrameActive: false,
933
+ liveFrameChannels: 0,
934
+ liveFramePublishes: 0,
935
+ errors: 0,
936
+ loadingText: "",
937
+ externalReady: null,
938
+ externalStatus: "",
939
+ externalError: "",
940
+ suppressedEntryError: ""
941
+ };
942
+ while (Date.now() < deadline) {
943
+ try {
944
+ last = await bridge.send({ type: "evaluate", code: WAIT_READY_PROBE }) ?? last;
945
+ bridgeError = "";
946
+ } catch (error) {
947
+ bridgeError = error instanceof Error ? error.message : String(error);
948
+ if (bridgeError.startsWith("multiple sims are connected:") || bridgeError.startsWith("saved sim ")) {
949
+ throw error;
950
+ }
951
+ }
952
+ const now = Date.now();
953
+ const status2 = {
954
+ ready: false,
955
+ elapsedMs: now - start,
956
+ nodes: last.nodes,
957
+ targets: last.targets,
958
+ liveFrameActive: last.liveFrameActive,
959
+ liveFrameChannels: last.liveFrameChannels,
960
+ liveFramePublishes: last.liveFramePublishes,
961
+ flag: last.flag,
962
+ loadingText: last.loadingText,
963
+ externalReady: last.externalReady,
964
+ externalStatus: last.externalStatus,
965
+ externalError: last.externalError,
966
+ suppressedEntryError: last.suppressedEntryError,
967
+ errors: last.errors,
968
+ bridgeError
969
+ };
970
+ if (last.nodes !== lastNodes) {
971
+ lastNodes = last.nodes;
972
+ nodeStableSince = now;
973
+ }
974
+ const hasReadySignal = last.flag === true && readyProbeHasContent(last);
975
+ const hasRenderedContentWithoutReadySignal = last.flag !== true && readyProbeHasTargetContent(last);
976
+ if ((hasReadySignal || hasRenderedContentWithoutReadySignal) && last.externalReady !== false && !last.externalError && !last.loadingText) {
977
+ if (now - nodeStableSince >= READY_NODE_STABLE_MS) {
978
+ return {
979
+ ...status2,
980
+ ready: true
981
+ };
982
+ }
983
+ }
984
+ if (last.externalError) return status2;
985
+ if (options.onProgress && progressIntervalMs > 0 && now >= nextProgressAt) {
986
+ options.onProgress(status2);
987
+ do {
988
+ nextProgressAt += progressIntervalMs;
989
+ } while (now >= nextProgressAt);
990
+ }
991
+ await new Promise((r) => setTimeout(r, READY_PROBE_POLL_MS));
992
+ }
993
+ const status = {
994
+ ready: false,
995
+ elapsedMs: Date.now() - start,
996
+ nodes: last.nodes,
997
+ targets: last.targets,
998
+ liveFrameActive: last.liveFrameActive,
999
+ liveFrameChannels: last.liveFrameChannels,
1000
+ liveFramePublishes: last.liveFramePublishes,
1001
+ flag: last.flag,
1002
+ loadingText: last.loadingText,
1003
+ externalReady: last.externalReady,
1004
+ externalStatus: last.externalStatus,
1005
+ externalError: last.externalError,
1006
+ suppressedEntryError: last.suppressedEntryError,
1007
+ errors: last.errors,
1008
+ bridgeError
1009
+ };
1010
+ if (options.onProgress && progressIntervalMs > 0 && Date.now() >= nextProgressAt) {
1011
+ options.onProgress(status);
1012
+ }
1013
+ return status;
1014
+ }
1015
+ async function inspectWaitSelector(bridge, testId, timeoutMs = 5e3, opts = {}) {
1016
+ const gone = opts.gone === true;
1017
+ const result = await bridge.send(
1018
+ {
1019
+ type: "evaluate",
1020
+ code: `(async () => {
1021
+ const start = Date.now()
1022
+ const deadline = start + ${timeoutMs}
1023
+ const gone = ${gone}
1024
+ const ID = ${JSON.stringify(testId)}
1025
+ const present = (node) => !!(node && node.layout && node.layout.width > 0 && node.layout.height > 0)
1026
+ const find = async () => {
1027
+ const t = window.__sootsimTest
1028
+ if (!t) return undefined // bridge not ready \u2014 indeterminate, not "gone"
1029
+ try {
1030
+ // gone-mode confirms a gate/screen LEFT \u2014 it only needs structural
1031
+ // existence + layout, NOT the full-tree occlusion pass (pruneHidden).
1032
+ // that pass (computeOccludedInspectNodeIds) walks the whole tree and
1033
+ // does O(n\xB2)-ish rect-occlusion testing EVERY poll; on a large
1034
+ // post-login app tree it costs seconds per poll, so --gone can never
1035
+ // get two clean "absent" polls inside budget and falsely reports the
1036
+ // gate as still blocked \u2014 the root cause of the 3pc PR-preview outage.
1037
+ // presence-mode keeps pruneHidden so "appeared" still means
1038
+ // visibly-reachable. a departed gate is unmounted or zero-sized, so the
1039
+ // cheap structural+layout check is correct here and ~O(n) cheap.
1040
+ const matches = await t.queryAll?.(gone ? { hasId: ID } : { hasId: ID, pruneHidden: true })
1041
+ if (!Array.isArray(matches)) return undefined
1042
+ return matches.find(present) || null
1043
+ } catch {
1044
+ return undefined
1045
+ }
1046
+ }
1047
+ let goneStreak = 0
1048
+ while (Date.now() < deadline) {
1049
+ const node = await find()
1050
+ if (gone) {
1051
+ if (node === null) {
1052
+ goneStreak += 1
1053
+ if (goneStreak >= 2) return { found: true, elapsed: Date.now() - start }
1054
+ } else {
1055
+ goneStreak = 0
1056
+ }
1057
+ } else if (present(node)) {
1058
+ return { found: true, node, elapsed: Date.now() - start }
1059
+ }
1060
+ await new Promise((r) => setTimeout(r, 80))
1061
+ }
1062
+ return { found: false, elapsed: Date.now() - start }
1063
+ })()`
1064
+ },
1065
+ {
1066
+ timeoutMs: timeoutMs + 1e3
1067
+ }
1068
+ );
1069
+ return result ?? { found: false, elapsed: timeoutMs };
1070
+ }
1071
+ function consoleEntriesEval(level, limit) {
1072
+ const getter = level === "error" ? "getErrors" : "getWarnings";
1073
+ return `(() => {
1074
+ const out = []
1075
+ const seen = new Set()
1076
+ const norm = (s) => (typeof s === 'string' ? s : (() => { try { return JSON.stringify(s) } catch { return String(s) } })())
1077
+ const key = (ts, args) => Math.round((ts || 0) / 250) + '|' + (Array.isArray(args) ? args.map(norm).join(' ') : norm(args))
1078
+ const push = (e) => {
1079
+ if (!e) return
1080
+ const ts = typeof e.timestamp === 'number' ? e.timestamp : (typeof e.ts === 'number' ? e.ts : 0)
1081
+ const k = key(ts, e.args)
1082
+ if (seen.has(k)) return
1083
+ seen.add(k)
1084
+ out.push({ timestamp: ts, args: Array.isArray(e.args) ? e.args : [e.args], stack: e.stack || undefined, source: e.source || undefined })
1085
+ }
1086
+ // page-realm ws-bridge buffer first (richest stacks for page-origin errors)
1087
+ try { for (const e of (window.__sootsimConsole?.${getter}(${limit}) || [])) push(e) } catch {}
1088
+ // engine observability store \u2014 the always-on render-worker / forwarded path
1089
+ try {
1090
+ const obs = window.__sootsimObservability
1091
+ const snap = obs && obs.logs && typeof obs.logs.getSnapshot === 'function' ? obs.logs.getSnapshot() : []
1092
+ for (const e of snap) if (e && e.level === ${JSON.stringify(level)}) push(e)
1093
+ } catch {}
1094
+ return out.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)).slice(-${limit})
1095
+ })()`;
1096
+ }
1097
+ async function inspectErrors(bridge, limit = 20) {
1098
+ const result = await bridge.send({
1099
+ type: "evaluate",
1100
+ code: consoleEntriesEval("error", limit)
1101
+ });
1102
+ return Array.isArray(result) ? result : [];
1103
+ }
1104
+ async function inspectWarnings(bridge, limit = 20) {
1105
+ const result = await bridge.send({
1106
+ type: "evaluate",
1107
+ code: consoleEntriesEval("warn", limit)
1108
+ });
1109
+ return Array.isArray(result) ? result : [];
1110
+ }
1111
+ var MERGED_CONSOLE_COUNT_EVAL = `(() => {
1112
+ const norm = (s) => (typeof s === 'string' ? s : (() => { try { return JSON.stringify(s) } catch { return String(s) } })())
1113
+ const key = (lvl, ts, args) => lvl + '|' + Math.round((ts || 0) / 250) + '|' + (Array.isArray(args) ? args.map(norm).join(' ') : norm(args))
1114
+ const seen = new Set()
1115
+ let errors = 0
1116
+ let warnings = 0
1117
+ const add = (lvl, ts, args) => {
1118
+ if (lvl !== 'error' && lvl !== 'warn') return
1119
+ const k = key(lvl, ts, args)
1120
+ if (seen.has(k)) return
1121
+ seen.add(k)
1122
+ if (lvl === 'error') errors++
1123
+ else warnings++
1124
+ }
1125
+ try {
1126
+ const c = window.__sootsimConsole
1127
+ for (const e of (c?.getErrors?.(200) || [])) add('error', e?.timestamp ?? e?.ts, e?.args)
1128
+ for (const e of (c?.getWarnings?.(200) || [])) add('warn', e?.timestamp ?? e?.ts, e?.args)
1129
+ } catch {}
1130
+ try {
1131
+ const obs = window.__sootsimObservability
1132
+ const snap = obs && obs.logs && typeof obs.logs.getSnapshot === 'function' ? obs.logs.getSnapshot() : []
1133
+ for (const e of snap) if (e) add(e.level, e.ts, e.args)
1134
+ } catch {}
1135
+ return { errors, warnings, total: errors + warnings }
1136
+ })()`;
1137
+ async function clearConsole(bridge) {
1138
+ await bridge.send({
1139
+ type: "evaluate",
1140
+ code: 'window.__sootsimConsole?.clear(); window.__sootsimObservability?.logs?.clear?.(); "cleared"'
1141
+ });
1142
+ }
1143
+ async function inspectRequests(bridge, opts = {}) {
1144
+ const limit = opts.limit ?? 20;
1145
+ const method = opts.failed === false ? "getRequests" : "getFailedRequests";
1146
+ const result = await bridge.send({
1147
+ type: "call",
1148
+ path: `__sootsimTest.${method}`,
1149
+ args: [limit]
1150
+ });
1151
+ return Array.isArray(result) ? result : [];
1152
+ }
1153
+ async function clearRequests(bridge) {
1154
+ await bridge.send({ type: "call", path: "__sootsimTest.clearRequests", args: [] });
1155
+ }
1156
+ async function inspectScrollStateAt(bridge, x, y) {
1157
+ const result = await bridge.send({
1158
+ type: "call",
1159
+ path: "__sootsimTest.getScrollStateAt",
1160
+ args: [x, y]
1161
+ });
1162
+ if (!result || typeof result !== "object") return null;
1163
+ const state = result;
1164
+ return {
1165
+ ...state,
1166
+ scrollOffsetX: typeof state.scrollOffsetX === "number" ? state.scrollOffsetX : state.offsetX,
1167
+ scrollOffsetY: typeof state.scrollOffsetY === "number" ? state.scrollOffsetY : state.offsetY
1168
+ };
1169
+ }
1170
+ async function inspectLogs(bridge) {
1171
+ const res = await bridge.send({
1172
+ type: "evaluate",
1173
+ code: `(() => {
1174
+ const obs = window.__sootsimObservability;
1175
+ if (!obs) return { ok: false };
1176
+ return { ok: true, entries: obs.logs.getSnapshot() };
1177
+ })()`
1178
+ });
1179
+ if (!res || !res.ok) return [];
1180
+ return res.entries ?? [];
1181
+ }
1182
+ async function clearLogs(bridge) {
1183
+ await bridge.send({
1184
+ type: "evaluate",
1185
+ code: 'window.__sootsimObservability?.logs.clear(); "cleared"'
1186
+ });
1187
+ }
1188
+ function isForwardedWorkerLogTwin(a, b) {
1189
+ if (a.source === b.source) return false;
1190
+ if (a.level !== b.level) return false;
1191
+ if (Math.abs(a.ts - b.ts) > 1e3) return false;
1192
+ if (a.args.length !== b.args.length) return false;
1193
+ const sources = /* @__PURE__ */ new Set([a.source, b.source]);
1194
+ if (!sources.has("sootsim-worker")) return false;
1195
+ if (!sources.has("render-worker") && !sources.has("forwarded-render-worker")) {
1196
+ return false;
1197
+ }
1198
+ return a.args.every((arg, index) => arg === b.args[index]);
1199
+ }
1200
+ function filterLogEntries(entries, opts = {}) {
1201
+ let out = [];
1202
+ for (const entry of entries) {
1203
+ if (out.some((candidate) => isForwardedWorkerLogTwin(candidate, entry))) {
1204
+ continue;
1205
+ }
1206
+ out.push(entry);
1207
+ }
1208
+ if (!opts.showInternal) {
1209
+ out = out.filter((e) => {
1210
+ const first = e.args[0];
1211
+ return !(typeof first === "string" && first.startsWith("[rnx]"));
1212
+ });
1213
+ }
1214
+ if (opts.level) out = out.filter((e) => opts.level.has(e.level));
1215
+ if (opts.filter) {
1216
+ const lf = opts.filter.toLowerCase();
1217
+ out = out.filter((e) => e.args.join(" ").toLowerCase().includes(lf));
1218
+ }
1219
+ return out;
1220
+ }
1221
+ async function inspectTimelineSummary(bridge, query) {
1222
+ return await bridge.send({
1223
+ type: "call",
1224
+ path: "SootSim.bridges.timeline.summary",
1225
+ args: [query]
1226
+ });
1227
+ }
1228
+ async function inspectTimelineRecent(bridge, query) {
1229
+ return await bridge.send({
1230
+ type: "call",
1231
+ path: "SootSim.bridges.timeline.recent",
1232
+ args: [query]
1233
+ });
1234
+ }
1235
+ async function detectOpenNativeUI(bridge) {
1236
+ let events;
1237
+ try {
1238
+ const res = await inspectTimelineRecent(bridge, {
1239
+ kinds: ["alert", "actionsheet"],
1240
+ limit: 80
1241
+ });
1242
+ events = res.events;
1243
+ } catch {
1244
+ return [];
1245
+ }
1246
+ const latest = /* @__PURE__ */ new Map();
1247
+ for (const ev of events) latest.set(ev.kind, ev);
1248
+ const out = [];
1249
+ const check = (kind, label) => {
1250
+ const ev = latest.get(kind);
1251
+ if (!ev || !ev.data || typeof ev.data !== "object") return;
1252
+ const d = ev.data;
1253
+ if (d.phase !== "show") return;
1254
+ out.push({ label, title: typeof d.title === "string" ? d.title : null });
1255
+ };
1256
+ check("alert", "Alert");
1257
+ check("actionsheet", "ActionSheet");
1258
+ return out;
1259
+ }
1260
+ async function inspectTimelineAdvanceCursor(bridge, cursorKey, watermark) {
1261
+ await bridge.send({
1262
+ type: "call",
1263
+ path: "SootSim.bridges.timeline.cursorAdvance",
1264
+ args: [cursorKey, watermark]
1265
+ });
1266
+ }
1267
+ async function inspectKeyboard(bridge) {
1268
+ const result = await bridge.send({
1269
+ type: "evaluate",
1270
+ code: `(() => {
1271
+ const kb = window.__sootsimKeyboard
1272
+ if (!kb || typeof kb.getLayout !== 'function') {
1273
+ return { error: 'keyboard bridge getLayout() not available' }
1274
+ }
1275
+ return kb.getLayout()
1276
+ })()`
1277
+ });
1278
+ return result ?? { error: "keyboard bridge returned no result" };
1279
+ }
1280
+ function isShellCommandUnavailable(error) {
1281
+ const message = error instanceof Error ? error.message : String(error);
1282
+ return message.includes("call target not found: SootSim.bridges.mainShell") || message.includes("test bridge unavailable before app-in-worker boot");
1283
+ }
1284
+ async function getShellState(bridge, readyTimeoutMs = 0) {
1285
+ const deadline = Date.now() + Math.max(0, readyTimeoutMs);
1286
+ while (true) {
1287
+ try {
1288
+ return await bridge.send({
1289
+ type: "call",
1290
+ path: "SootSim.bridges.mainShell.getState",
1291
+ args: []
1292
+ });
1293
+ } catch (error) {
1294
+ if (!isShellCommandUnavailable(error) || Date.now() >= deadline) throw error;
1295
+ await new Promise((r) => setTimeout(r, 50));
1296
+ }
1297
+ }
1298
+ }
1299
+ async function inspectScreens(bridge) {
1300
+ const payload = await bridge.send({
1301
+ type: "evaluate",
1302
+ code: `(async () => {
1303
+ const test = window.__sootsimTest
1304
+ const kb = window.__sootsimKeyboard
1305
+ const navSnap =
1306
+ test && typeof test.getNavigationSnapshot === 'function'
1307
+ ? await test.getNavigationSnapshot()
1308
+ : null
1309
+ const route =
1310
+ test && typeof test.getRouteInfo === 'function'
1311
+ ? await test.getRouteInfo()
1312
+ : null
1313
+ const keyboard =
1314
+ kb && typeof kb.getLayout === 'function'
1315
+ ? (() => {
1316
+ const layout = kb.getLayout()
1317
+ return layout ? {
1318
+ visible: layout.visible,
1319
+ mode: layout.mode,
1320
+ spec: layout.spec
1321
+ ? {
1322
+ keyboardType: layout.spec.keyboardType,
1323
+ returnKeyType: layout.spec.returnKeyType,
1324
+ }
1325
+ : null,
1326
+ } : null
1327
+ })()
1328
+ : null
1329
+ return { nav: navSnap, route, keyboard }
1330
+ })()`
1331
+ });
1332
+ const shell = await getShellState(bridge, 500).catch(() => null);
1333
+ return {
1334
+ shell,
1335
+ nav: payload?.nav ?? null,
1336
+ route: payload?.route ?? null,
1337
+ keyboard: payload?.keyboard ?? null
1338
+ };
1339
+ }
1340
+ var DEBUG_CHANNELS = [
1341
+ "portals",
1342
+ "sheets",
1343
+ "layout",
1344
+ "onlayout",
1345
+ "animated",
1346
+ "render",
1347
+ "touch",
1348
+ "yoga"
1349
+ ];
1350
+ async function inspectDebugStatus(bridge) {
1351
+ return bridge.send({ type: "evaluate", code: "window.__sootsimDebug.status()" });
1352
+ }
1353
+ async function inspectDebugFlags(bridge) {
1354
+ return bridge.send({ type: "evaluate", code: "window.__sootsimDebug.flags()" });
1355
+ }
1356
+ async function inspectDebugFind(bridge, target) {
1357
+ return bridge.send({
1358
+ type: "evaluate",
1359
+ code: `window.__sootsimTest.debugFind(${JSON.stringify(target)})`
1360
+ });
1361
+ }
1362
+ async function inspectDebugRecent(bridge, channel, limit = 50) {
1363
+ const code = channel && channel !== "all" ? `window.__sootsimDebug.recent(${JSON.stringify(channel)}, ${limit})` : `window.__sootsimDebug.recent(undefined, ${limit})`;
1364
+ return bridge.send({ type: "evaluate", code });
1365
+ }
1366
+ async function setDebugChannels(bridge, action, channels) {
1367
+ const args = channels.length > 0 ? channels.map((c) => JSON.stringify(c)).join(", ") : action === "disable" ? "'all'" : "";
1368
+ return bridge.send({
1369
+ type: "evaluate",
1370
+ code: `window.__sootsimDebug.${action}(${args})`
1371
+ });
1372
+ }
1373
+ async function inspectMemory(bridge) {
1374
+ const result = await bridge.send({
1375
+ type: "evaluate",
1376
+ code: `(async () => {
1377
+ const host = window.__sootsimRenderHost
1378
+ const stats = host?.queryStats ? await host.queryStats() : null
1379
+ const shellRaw = window.__sootsimShellMemory
1380
+ ? await window.__sootsimShellMemory()
1381
+ : null
1382
+ const shell = shellRaw && !shellRaw.error ? shellRaw : null
1383
+ // the compositor paints home + app:one + app:two, so every app-surface
1384
+ // picture and raster image is counted here and nowhere else.
1385
+ let compositor = null
1386
+ try {
1387
+ const cs = await window.__sootsimCompositor?.getStats?.(false, true)
1388
+ compositor = cs?.memory ?? null
1389
+ } catch {}
1390
+ const normalize = (m) =>
1391
+ m
1392
+ ? {
1393
+ nodesRegistered: m.nodesRegistered ?? null,
1394
+ nodesDetached: m.nodesDetached ?? null,
1395
+ detachedTypes: m.detachedTypes ?? null,
1396
+ objects: m.objects ?? null,
1397
+ activeNativeAnimations: m.activeNativeAnimations ?? null,
1398
+ imageLoader: m.imageLoader ?? null,
1399
+ workerHeap: m.workerHeap ?? null,
1400
+ wasmHeapBytes: m.wasmHeapBytes ?? null,
1401
+ }
1402
+ : null
1403
+ const hostMem = performance.memory
1404
+ ? {
1405
+ usedJSHeapSize: performance.memory.usedJSHeapSize,
1406
+ totalJSHeapSize: performance.memory.totalJSHeapSize,
1407
+ jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
1408
+ }
1409
+ : null
1410
+ return {
1411
+ tenant: normalize(stats?.memory),
1412
+ shell: normalize(shell),
1413
+ compositor: normalize(compositor),
1414
+ hostHeap: hostMem,
1415
+ }
1416
+ })()`
1417
+ });
1418
+ return result ?? { tenant: null, shell: null, compositor: null, hostHeap: null };
1419
+ }
1420
+ function formatTimelineSummary(summary) {
1421
+ if (summary.total === 0) return "nothing recorded";
1422
+ const parts = [];
1423
+ const ORDER = [
1424
+ "error",
1425
+ "warning",
1426
+ "console",
1427
+ "fetch",
1428
+ "toast",
1429
+ "alert",
1430
+ "actionsheet",
1431
+ "picker",
1432
+ "notification",
1433
+ "screen",
1434
+ "route",
1435
+ "keyboard",
1436
+ "app-launch",
1437
+ "shell",
1438
+ "scroll",
1439
+ "gesture",
1440
+ "text-input",
1441
+ "react-commit",
1442
+ "animation",
1443
+ "reanimated"
1444
+ ];
1445
+ const seen = /* @__PURE__ */ new Set();
1446
+ for (const k of ORDER) {
1447
+ const n = summary.byKind[k];
1448
+ if (n) {
1449
+ parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
1450
+ seen.add(k);
1451
+ }
1452
+ }
1453
+ for (const [k, n] of Object.entries(summary.byKind)) {
1454
+ if (!seen.has(k) && n) parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
1455
+ }
1456
+ return parts.join(" \xB7 ");
1457
+ }
1458
+ var NOISY_TIMELINE_KINDS = /* @__PURE__ */ new Set([
1459
+ "react-commit",
1460
+ "layout",
1461
+ "scroll"
1462
+ ]);
1463
+ function formatTimelineRelativeTime(t, anchor) {
1464
+ if (anchor === null) return new Date(t).toLocaleTimeString();
1465
+ const dt = (t - anchor) / 1e3;
1466
+ const sign = dt >= 0 ? "+" : "";
1467
+ return `${sign}${dt.toFixed(2)}s`;
1468
+ }
1469
+ function formatTimelinePayload(kind, d) {
1470
+ switch (kind) {
1471
+ case "app-launch":
1472
+ return d.phase === "launch" ? `launch ${d.appName ?? d.toAppId ?? ""}` : `dismiss ${d.appName ?? d.fromAppId ?? ""} \u2192 ${d.toAppId ?? ""}`;
1473
+ case "toast":
1474
+ return `"${d.text ?? ""}"${d.durationMs ? ` (${d.durationMs}ms)` : ""}`;
1475
+ case "keyboard":
1476
+ return `${d.phase ?? "?"}${d.heightPx ? ` h=${d.heightPx}` : ""}${d.mode ? ` ${d.mode}` : ""}`;
1477
+ case "screen":
1478
+ return `${d.phase ?? "?"} ${d.name ?? d.activeName ?? ""}`;
1479
+ case "flow-step":
1480
+ return `${d.status ?? "?"} ${d.stepName ?? "step"}${d.durationMs ? ` ${d.durationMs}ms` : ""}`;
1481
+ case "route":
1482
+ return `${d.phase ?? "?"} ${d.path ?? d.pathname ?? ""}`;
1483
+ case "alert":
1484
+ case "actionsheet":
1485
+ case "picker":
1486
+ return `${d.phase ?? "?"} ${d.title ?? d.message ?? ""}`;
1487
+ case "notification":
1488
+ return `${d.title ?? ""}${d.body ? ` \u2014 ${d.body}` : ""}`;
1489
+ case "fetch":
1490
+ return `${d.method ?? "GET"} ${d.url ?? ""}${d.status ? ` -> ${d.status}` : ""}`;
1491
+ case "console":
1492
+ case "console-log":
1493
+ return `${d.level ?? "log"}: ${(d.message ?? "").toString().slice(0, 120)}`;
1494
+ case "shell":
1495
+ return `${d.event ?? d.type ?? d.phase ?? ""}`;
1496
+ case "scroll":
1497
+ return `${d.phase ?? "?"} ${d.target ?? ""}`;
1498
+ case "gesture":
1499
+ return `${d.phase ?? "?"} ${d.type ?? ""}`;
1500
+ case "text-input":
1501
+ return `${d.phase ?? "?"}${d.value !== void 0 ? ` "${String(d.value).slice(0, 40)}"` : ""}`;
1502
+ case "layout":
1503
+ return `${d.kind ?? "?"} ${d.testID ?? d.type ?? ""}${d.skipped ? ` skipped:${d.reason ?? "unknown"}` : ""}`;
1504
+ case "react-commit": {
1505
+ const slowest = d.slowest;
1506
+ return `${d.fiberCount ?? "?"} fibers ${d.durationMs ?? "?"}ms${slowest?.displayName ? ` \xB7 ${slowest.displayName} ${slowest.durationMs ?? "?"}ms` : ""}`;
1507
+ }
1508
+ case "frame":
1509
+ return `${d.totalMs ?? "?"}ms${d.renderMs ? ` \xB7 render ${d.renderMs}ms` : ""}${d.layoutMs ? ` \xB7 layout ${d.layoutMs}ms` : ""}${d.copyMs ? ` \xB7 copy ${d.copyMs}ms` : ""}`;
1510
+ case "reanimated":
1511
+ case "animation":
1512
+ return `${d.kind ?? ""} ${d.target ?? ""}${d.durationMs ? ` ${d.durationMs}ms` : ""}`;
1513
+ }
1514
+ return "";
1515
+ }
1516
+ function formatTimelineEvent(event, anchor) {
1517
+ const ts = formatTimelineRelativeTime(event.t, anchor).padStart(8);
1518
+ const ctx = event.context.padEnd(6);
1519
+ const kind = `[${event.kind}]`.padEnd(15);
1520
+ const d = event.data;
1521
+ const payload = d && typeof d === "object" ? formatTimelinePayload(event.kind, d) : "";
1522
+ return ` ${ts} ${ctx} ${kind} ${payload}`;
1523
+ }
1524
+ function formatTimelineEvents(events) {
1525
+ if (!events.length) return "";
1526
+ const anchor = events[0]?.t ?? null;
1527
+ return events.map((e) => formatTimelineEvent(e, anchor)).join("\n");
1528
+ }
1529
+
1530
+ // cli/commands/inspect/settling.ts
1531
+ async function waitForSootsimIdle({
1532
+ bridge,
1533
+ simId,
1534
+ maxMs,
1535
+ pollMs = 50,
1536
+ stablePolls = 3,
1537
+ strict = false
1538
+ }) {
1539
+ const result = await bridge.send(
1540
+ {
1541
+ type: "evaluate",
1542
+ simId,
1543
+ code: `(async () => {
1544
+ const start = Date.now()
1545
+ const deadline = start + ${Math.max(0, Math.round(maxMs))}
1546
+ const pollMs = ${Math.max(1, Math.round(pollMs))}
1547
+ const requiredStablePolls = ${Math.max(1, Math.round(stablePolls))}
1548
+ const strict = ${strict ? "true" : "false"}
1549
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
1550
+
1551
+ // route-aware settle: a tap that pushes/pops a screen is async \u2014 the
1552
+ // old screen still renders for a moment, then the new one mounts and
1553
+ // its content loads. a pure layout-hash check reports "idle" on the
1554
+ // outgoing screen or on the incoming skeleton, so a driver re-taps
1555
+ // and stacks duplicate navigations. drain any in-flight screen
1556
+ // transition FIRST, bounded by the overall budget. when no
1557
+ // transition is happening this returns in ~80ms (startWindowMs), so
1558
+ // a plain button tap barely pays for it.
1559
+ try {
1560
+ const wfst = window.__sootsimTest?.waitForScreenTransitions
1561
+ if (typeof wfst === 'function') {
1562
+ const remaining = deadline - Date.now()
1563
+ if (remaining > 120) {
1564
+ await wfst({
1565
+ timeoutMs: Math.min(remaining - 80, 4000),
1566
+ settleMs: 64,
1567
+ startWindowMs: 80,
1568
+ })
1569
+ }
1570
+ }
1571
+ } catch {}
1572
+
1573
+ const readSnapshot = async () => {
1574
+ let animating = false
1575
+ let pendingFetches = 0
1576
+ let requestTotal = 0
1577
+ let requestInFlight = 0
1578
+ try {
1579
+ const stats = await window.__sootsimRenderHost?.queryStats?.()
1580
+ if (stats) {
1581
+ animating =
1582
+ stats.hasActiveAnims === true ||
1583
+ stats.hasActiveNativeAnimations === true ||
1584
+ stats.hasPendingAnimationFrames === true
1585
+ // a freshly-pushed screen showing a skeleton is layout-stable
1586
+ // but not actually settled \u2014 its data/images are still in
1587
+ // flight. treat bounded image-loader fetches as not-idle so
1588
+ // settle waits for real content, capped by the budget.
1589
+ const pf = stats.memory && stats.memory.imageLoader
1590
+ ? stats.memory.imageLoader.pendingFetches
1591
+ : 0
1592
+ pendingFetches = typeof pf === 'number' ? pf : 0
1593
+ }
1594
+ } catch {}
1595
+ try {
1596
+ const counts = await window.__sootsimTest?.getRequestCounts?.()
1597
+ requestTotal = typeof counts?.total === 'number' ? counts.total : 0
1598
+ requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
1599
+ } catch {}
1600
+ const root = window.__sootsimRoot
1601
+ const nodes = []
1602
+ if (root) {
1603
+ const walk = (n) => {
1604
+ if (n.layout && n.layout.width > 0) {
1605
+ nodes.push(Math.round(n.layout.x) + ',' + Math.round(n.layout.y) + ',' + Math.round(n.layout.width))
1606
+ }
1607
+ for (const c of n.children || []) walk(c)
1608
+ }
1609
+ walk(root)
1610
+ }
1611
+ return { layout: nodes.join(';'), animating, pendingFetches, requestTotal, requestInFlight }
1612
+ }
1613
+
1614
+ // how long generic background network is allowed to keep us waiting AFTER
1615
+ // the screen is otherwise visually settled. apps with continuous traffic
1616
+ // (polling, realtime sockets, a tap that fires 20-50 fetches) never reach
1617
+ // requestInFlight===0 / a stable requestTotal, so requiring full network
1618
+ // quiet burns the ENTIRE budget on every command \u2014 the leading idle that
1619
+ // failed PR-preview recordings for network-heavy apps (3pc fight-pulse).
1620
+ // layout-stability + the image-loader already prove visible content
1621
+ // loaded (a data load changes layout; an image load shows in the loader),
1622
+ // so the generic request counters are a courtesy, not a gate: honor them
1623
+ // only within this grace once the screen is visually still.
1624
+ const networkQuietGraceMs = 1200
1625
+ let lastLayout = ''
1626
+ let lastRequestTotal = -1
1627
+ let stable = 0
1628
+ let visuallyStillSince = 0
1629
+ while (Date.now() < deadline) {
1630
+ const snapshot = await readSnapshot()
1631
+ const strictOk = !strict || !snapshot.animating
1632
+ // visually settled: no animations, no image content loading, layout
1633
+ // unchanged since the last poll.
1634
+ const visuallyStill =
1635
+ strictOk && snapshot.pendingFetches === 0 && snapshot.layout === lastLayout
1636
+ // generic network quiet: nothing in flight and no new requests issued.
1637
+ const networkQuiet =
1638
+ snapshot.requestInFlight === 0 && snapshot.requestTotal === lastRequestTotal
1639
+ if (visuallyStill) {
1640
+ stable++
1641
+ if (visuallyStillSince === 0) visuallyStillSince = Date.now()
1642
+ // settle once the screen has held still long enough AND either the
1643
+ // network is genuinely quiet OR it has refused to quiet within the
1644
+ // grace (continuous background traffic must not block forever).
1645
+ if (
1646
+ stable >= requiredStablePolls &&
1647
+ (networkQuiet || Date.now() - visuallyStillSince >= networkQuietGraceMs)
1648
+ ) {
1649
+ return { settled: true, elapsed: Date.now() - start }
1650
+ }
1651
+ } else {
1652
+ stable = 0
1653
+ visuallyStillSince = 0
1654
+ }
1655
+ lastLayout = snapshot.layout
1656
+ lastRequestTotal = snapshot.requestTotal
1657
+ await sleep(pollMs)
1658
+ }
1659
+ return { settled: false, elapsed: Date.now() - start }
1660
+ })()`
1661
+ },
1662
+ {
1663
+ timeoutMs: maxMs + 1e3
1664
+ }
1665
+ );
1666
+ const { elapsed, settled } = result ?? {};
1667
+ return {
1668
+ elapsed: typeof elapsed === "number" ? elapsed : maxMs,
1669
+ settled: settled === true
1670
+ };
1671
+ }
1672
+
1673
+ // cli/commands/inspect/actions.ts
1674
+ var SCREEN_EVAL = `(() => {
1675
+ const spec = window.SootSim?.state?.engineSnapshot?.windowState?.deviceSpec
1676
+ return spec && spec.width > 0 && spec.height > 0
1677
+ ? { width: spec.width, height: spec.height }
1678
+ : null
1679
+ })()`;
1680
+ var DEFAULT_AGENT_TIMING = {
1681
+ initialWaitMs: 3e3,
1682
+ deadlineMs: 5e3,
1683
+ retryWaitMs: 700
1684
+ };
1685
+ var DEFAULT_INTERACTIVE_TIMING = {
1686
+ initialWaitMs: 1200,
1687
+ deadlineMs: 2500,
1688
+ retryWaitMs: 700
1689
+ };
1690
+ function tapTiming(agent, opts = {}) {
1691
+ return {
1692
+ ...agent ? DEFAULT_AGENT_TIMING : DEFAULT_INTERACTIVE_TIMING,
1693
+ ...opts
1694
+ };
1695
+ }
1696
+ function isTapSuccess(result) {
1697
+ if (!result || result.hit === false || result.ok === false) return false;
1698
+ if (result.requestedTargetMatched === false) return false;
1699
+ if (result.pointerTapHandled === false && !result.keyboardOpened && !result.isTextInput) {
1700
+ return false;
1701
+ }
1702
+ return true;
1703
+ }
1704
+ function tapTargetFromPayload(payload, textFallback) {
1705
+ return {
1706
+ nodeId: payload.target?.nodeId ?? payload.match?.nodeId ?? payload.node?.nodeId ?? null,
1707
+ id: payload.target?.id ?? payload.match?.id ?? payload.node?.id ?? null,
1708
+ testID: payload.target?.testID ?? payload.match?.testID ?? payload.node?.testID ?? null,
1709
+ text: payload.target?.text ?? payload.target?.accessibilityLabel ?? payload.match?.text ?? payload.match?.accessibilityLabel ?? payload.node?.text ?? textFallback ?? null,
1710
+ type: payload.target?.type ?? payload.match?.type ?? payload.node?.type ?? null
1711
+ };
1712
+ }
1713
+ async function tapResolvedTarget(bridge, args) {
1714
+ const timing = tapTiming(!!args.agent, args.timing);
1715
+ let attempts = 0;
1716
+ let lastPayload = null;
1717
+ let lastResult = null;
1718
+ try {
1719
+ await waitForSootsimIdle({
1720
+ bridge,
1721
+ maxMs: timing.initialWaitMs,
1722
+ pollMs: 32,
1723
+ stablePolls: 2
1724
+ });
1725
+ } catch {
1726
+ }
1727
+ const deadline = Date.now() + timing.deadlineMs;
1728
+ let lastOffscreenAt = null;
1729
+ while (Date.now() <= deadline || attempts === 0) {
1730
+ attempts++;
1731
+ const payload = await args.resolve();
1732
+ lastPayload = payload;
1733
+ if (payload?.error === "bridge-not-ready" || payload?.ambiguous || payload?.nthOutOfRange) {
1734
+ return { payload, result: null, attempts, failure: "special" };
1735
+ }
1736
+ if (payload && typeof payload.cx === "number" && typeof payload.cy === "number") {
1737
+ if (payload.offscreen) {
1738
+ lastResult = {
1739
+ hit: false,
1740
+ reason: "offscreen",
1741
+ x: payload.cx,
1742
+ y: payload.cy,
1743
+ screen: payload.screen
1744
+ };
1745
+ const at = `${Math.round(payload.cx)},${Math.round(payload.cy)}`;
1746
+ if (lastOffscreenAt === at) {
1747
+ return { payload, result: lastResult, attempts, failure: "missed" };
1748
+ }
1749
+ lastOffscreenAt = at;
1750
+ } else {
1751
+ lastOffscreenAt = null;
1752
+ const requestedTarget = tapTargetFromPayload(payload, args.textFallback);
1753
+ const verification = await bridge.send({
1754
+ type: "evaluate",
1755
+ code: `(async () => {
1756
+ const t = window.__sootsimTest
1757
+ if (
1758
+ !t ||
1759
+ typeof t.inspectAt !== 'function' ||
1760
+ typeof t.resolveTapTarget !== 'function'
1761
+ ) {
1762
+ return { error: 'tap-target-inspection-unavailable' }
1763
+ }
1764
+ const requestedTarget = ${JSON.stringify(requestedTarget)}
1765
+ const inspected = await t.inspectAt(${payload.cx}, ${payload.cy})
1766
+ const resolved =
1767
+ inspected && typeof inspected.nodeId === 'number'
1768
+ ? await t.resolveTapTarget(inspected.nodeId)
1769
+ : null
1770
+ const node = (resolved && resolved.target) || inspected
1771
+ const coordinateTarget = node
1772
+ ? {
1773
+ nodeId: node.nodeId ?? null,
1774
+ id: node.id ?? null,
1775
+ testID: node.testID ?? null,
1776
+ text: node.text ?? node.accessibilityLabel ?? null,
1777
+ type: node.type ?? null,
1778
+ }
1779
+ : null
1780
+ const coordinateAncestors = Array.isArray(inspected?.ancestors)
1781
+ ? inspected.ancestors
1782
+ : []
1783
+ const ancestorMatchesRequested = coordinateAncestors.some(
1784
+ (ancestor) =>
1785
+ (typeof requestedTarget.nodeId === 'number' &&
1786
+ ancestor?.nodeId === requestedTarget.nodeId) ||
1787
+ (requestedTarget.testID &&
1788
+ ancestor?.testID === requestedTarget.testID) ||
1789
+ (requestedTarget.id && ancestor?.id === requestedTarget.id),
1790
+ )
1791
+ const requestedTargetMatched =
1792
+ coordinateTarget === null
1793
+ ? false
1794
+ : typeof requestedTarget.nodeId === 'number' &&
1795
+ typeof coordinateTarget.nodeId === 'number'
1796
+ ? requestedTarget.nodeId === coordinateTarget.nodeId ||
1797
+ ancestorMatchesRequested
1798
+ : requestedTarget.testID && coordinateTarget.testID
1799
+ ? requestedTarget.testID === coordinateTarget.testID
1800
+ || ancestorMatchesRequested
1801
+ : requestedTarget.id && coordinateTarget.id
1802
+ ? requestedTarget.id === coordinateTarget.id
1803
+ || ancestorMatchesRequested
1804
+ : ancestorMatchesRequested
1805
+ if (!requestedTargetMatched) {
1806
+ return {
1807
+ requestedTargetMatched,
1808
+ requestedTarget,
1809
+ coordinateTarget,
1810
+ }
1811
+ }
1812
+ if (typeof t.activatePressAt !== 'function') {
1813
+ return { error: 'tap-target-activation-unavailable' }
1814
+ }
1815
+ const activation = await t.activatePressAt(
1816
+ ${payload.cx},
1817
+ ${payload.cy},
1818
+ requestedTarget.nodeId,
1819
+ )
1820
+ if (activation?.ok) {
1821
+ window.dispatchEvent(
1822
+ new CustomEvent('sootsim:agentAction', {
1823
+ detail: {
1824
+ type: 'tap',
1825
+ x: ${payload.cx},
1826
+ y: ${payload.cy},
1827
+ target: requestedTarget,
1828
+ },
1829
+ }),
1830
+ )
1831
+ }
1832
+ return {
1833
+ activation,
1834
+ requestedTargetMatched,
1835
+ requestedTarget,
1836
+ coordinateTarget,
1837
+ }
1838
+ })()`
1839
+ });
1840
+ if (verification?.error) {
1841
+ return {
1842
+ payload,
1843
+ result: {
1844
+ hit: false,
1845
+ reason: verification.error,
1846
+ requestedTarget
1847
+ },
1848
+ attempts,
1849
+ failure: "special"
1850
+ };
1851
+ }
1852
+ if (verification?.requestedTargetMatched === false) {
1853
+ return {
1854
+ payload,
1855
+ result: {
1856
+ hit: false,
1857
+ reason: "target-covered",
1858
+ ...verification
1859
+ },
1860
+ attempts,
1861
+ failure: "missed"
1862
+ };
1863
+ }
1864
+ const dispatched = verification?.activation?.tap ?? verification?.activation;
1865
+ const result = dispatched && typeof dispatched === "object" ? { ...dispatched, ...verification?.activation, ...verification } : { hit: !!dispatched, ...verification };
1866
+ lastResult = result;
1867
+ if (isTapSuccess(result)) return { payload, result, attempts };
1868
+ }
1869
+ }
1870
+ const remaining = deadline - Date.now();
1871
+ if (remaining <= 0) break;
1872
+ try {
1873
+ await waitForSootsimIdle({
1874
+ bridge,
1875
+ maxMs: Math.min(remaining, timing.retryWaitMs),
1876
+ pollMs: 32,
1877
+ stablePolls: 2
1878
+ });
1879
+ } catch {
1880
+ await new Promise((resolve) => setTimeout(resolve, Math.min(120, remaining)));
1881
+ }
1882
+ }
1883
+ return {
1884
+ payload: lastPayload,
1885
+ result: lastResult,
1886
+ attempts,
1887
+ failure: lastPayload && typeof lastPayload.cx === "number" ? "missed" : "not-found"
1888
+ };
1889
+ }
1890
+ async function tapCoordinates(bridge, x, y, target) {
1891
+ return bridge.send({
1892
+ type: "tap",
1893
+ x,
1894
+ y,
1895
+ ...target ? { target } : {}
1896
+ });
1897
+ }
1898
+ async function tapById(bridge, query, opts = {}) {
1899
+ const arg = JSON.stringify(query);
1900
+ return tapResolvedTarget(bridge, {
1901
+ agent: opts.agent,
1902
+ timing: opts.timing,
1903
+ resolve: () => bridge.send({
1904
+ type: "evaluate",
1905
+ code: `(async () => {
1906
+ const t = window.__sootsimTest
1907
+ if (!t) return null
1908
+ const n = (await t.findByTestId(${arg})) || (await t.findById(${arg}))
1909
+ if (!n || !n.absolutePosition || !n.layout) return { cx: null }
1910
+ const resolved =
1911
+ typeof n.nodeId === 'number' && typeof t.resolveTapTarget === 'function'
1912
+ ? await t.resolveTapTarget(n.nodeId)
1913
+ : null
1914
+ const target = (resolved && resolved.target) || n
1915
+ const cx =
1916
+ resolved && typeof resolved.cx === 'number'
1917
+ ? resolved.cx
1918
+ : n.absolutePosition.x + (n.layout.width || 0) / 2
1919
+ const cy =
1920
+ resolved && typeof resolved.cy === 'number'
1921
+ ? resolved.cy
1922
+ : n.absolutePosition.y + (n.layout.height || 0) / 2
1923
+ const scr = ${SCREEN_EVAL}
1924
+ const offscreen =
1925
+ !!scr && (cx < 0 || cy < 0 || cx > scr.width || cy > scr.height)
1926
+ return {
1927
+ cx,
1928
+ cy,
1929
+ ...(offscreen ? { offscreen: true, screen: scr } : {}),
1930
+ match: {
1931
+ nodeId: n.nodeId ?? null,
1932
+ id: n.id,
1933
+ testID: n.testID,
1934
+ text: n.text ?? n.accessibilityLabel ?? null,
1935
+ type: n.type,
1936
+ },
1937
+ target: {
1938
+ nodeId: target.nodeId ?? null,
1939
+ id: target.id,
1940
+ testID: target.testID,
1941
+ text:
1942
+ target.text ??
1943
+ target.accessibilityLabel ??
1944
+ n.text ??
1945
+ n.accessibilityLabel ??
1946
+ null,
1947
+ type: target.type,
1948
+ },
1949
+ strategy: (resolved && resolved.strategy) || 'matched-node',
1950
+ }
1951
+ })()`
1952
+ })
1953
+ });
1954
+ }
1955
+ async function tapByText(bridge, query, options = {}, opts = {}) {
1956
+ const filterArg = JSON.stringify({
1957
+ query,
1958
+ exact: !!options.exact,
1959
+ role: options.role ?? null,
1960
+ within: options.within ?? null,
1961
+ minX: options.minX ?? null,
1962
+ maxX: options.maxX ?? null,
1963
+ minY: options.minY ?? null,
1964
+ maxY: options.maxY ?? null,
1965
+ near: options.near ?? null,
1966
+ nth: options.nth ?? null,
1967
+ first: !!options.first
1968
+ });
1969
+ return tapResolvedTarget(bridge, {
1970
+ agent: opts.agent,
1971
+ timing: opts.timing,
1972
+ textFallback: query,
1973
+ resolve: () => bridge.send({
1974
+ type: "evaluate",
1975
+ code: `(async () => {
1976
+ const t = window.__sootsimTest
1977
+ if (!t) return { error: 'bridge-not-ready' }
1978
+ const F = ${filterArg}
1979
+
1980
+ const res = await t.queryTextCandidates({
1981
+ query: F.query,
1982
+ exact: !!F.exact,
1983
+ ...(F.role ? { role: F.role } : {}),
1984
+ })
1985
+
1986
+ let candidates = res.candidates || []
1987
+
1988
+ candidates = candidates.filter((c) => {
1989
+ const ax = c.info.absolutePosition && c.info.absolutePosition.x
1990
+ const ay = c.info.absolutePosition && c.info.absolutePosition.y
1991
+ if (F.minX !== null && !(ax >= F.minX)) return false
1992
+ if (F.maxX !== null && !(ax <= F.maxX)) return false
1993
+ if (F.minY !== null && !(ay >= F.minY)) return false
1994
+ if (F.maxY !== null && !(ay <= F.maxY)) return false
1995
+ if (F.within && !c.ancestorTestIDs.includes(F.within)) return false
1996
+ return true
1997
+ })
1998
+
1999
+ if (F.near) {
2000
+ candidates.sort((a, b) => {
2001
+ const ax = (a.info.absolutePosition && a.info.absolutePosition.x) || 0
2002
+ const ay = (a.info.absolutePosition && a.info.absolutePosition.y) || 0
2003
+ const bx = (b.info.absolutePosition && b.info.absolutePosition.x) || 0
2004
+ const by = (b.info.absolutePosition && b.info.absolutePosition.y) || 0
2005
+ return (
2006
+ Math.hypot(ax - F.near.x, ay - F.near.y) -
2007
+ Math.hypot(bx - F.near.x, by - F.near.y)
2008
+ )
2009
+ })
2010
+ } else {
2011
+ candidates.sort((a, b) => {
2012
+ const ay = (a.info.absolutePosition && a.info.absolutePosition.y) || 0
2013
+ const by = (b.info.absolutePosition && b.info.absolutePosition.y) || 0
2014
+ if (Math.abs(ay - by) > 2) return ay - by
2015
+ const ax = (a.info.absolutePosition && a.info.absolutePosition.x) || 0
2016
+ const bx = (b.info.absolutePosition && b.info.absolutePosition.x) || 0
2017
+ return ax - bx
2018
+ })
2019
+ }
2020
+
2021
+ const total = candidates.length
2022
+ if (total === 0) return { matched: 0, total: 0 }
2023
+
2024
+ let idx = 0
2025
+ if (F.nth !== null) {
2026
+ idx = F.nth < 0 ? total + F.nth : F.nth
2027
+ if (idx < 0 || idx >= total) {
2028
+ return { matched: 0, total, nthOutOfRange: true, nth: F.nth }
2029
+ }
2030
+ } else if (total > 1 && !F.first && !F.near) {
2031
+ return {
2032
+ ambiguous: true,
2033
+ total,
2034
+ candidates: candidates.slice(0, 10).map((c, i) => ({
2035
+ idx: i,
2036
+ nodeId: c.info.nodeId,
2037
+ type: c.info.type,
2038
+ testID: c.info.testID,
2039
+ text: (c.info.text || '').slice(0, 60),
2040
+ abs: c.info.absolutePosition,
2041
+ layout: c.info.layout
2042
+ ? {
2043
+ width: Math.round(c.info.layout.width || 0),
2044
+ height: Math.round(c.info.layout.height || 0),
2045
+ }
2046
+ : null,
2047
+ ancestorTestIDs: (c.ancestorTestIDs || []).slice(0, 5),
2048
+ })),
2049
+ }
2050
+ }
2051
+
2052
+ const picked = candidates[idx]
2053
+ const n = picked.info
2054
+ if (!n.absolutePosition || !n.layout) return { matched: 0, total }
2055
+
2056
+ const resolved =
2057
+ typeof n.nodeId === 'number' &&
2058
+ typeof t.resolveTapTarget === 'function'
2059
+ ? await t.resolveTapTarget(n.nodeId)
2060
+ : null
2061
+ const target = (resolved && resolved.target) || n
2062
+ const cx =
2063
+ resolved && typeof resolved.cx === 'number'
2064
+ ? resolved.cx
2065
+ : n.absolutePosition.x + (n.layout.width || 0) / 2
2066
+ const cy =
2067
+ resolved && typeof resolved.cy === 'number'
2068
+ ? resolved.cy
2069
+ : n.absolutePosition.y + (n.layout.height || 0) / 2
2070
+ const scr = ${SCREEN_EVAL}
2071
+ const offscreen =
2072
+ !!scr && (cx < 0 || cy < 0 || cx > scr.width || cy > scr.height)
2073
+ return {
2074
+ cx,
2075
+ cy,
2076
+ ...(offscreen ? { offscreen: true, screen: scr } : {}),
2077
+ match: {
2078
+ nodeId: n.nodeId ?? null,
2079
+ id: n.id,
2080
+ testID: n.testID,
2081
+ type: n.type,
2082
+ },
2083
+ target: {
2084
+ nodeId: target.nodeId ?? null,
2085
+ id: target.id,
2086
+ testID: target.testID,
2087
+ text:
2088
+ target.text ??
2089
+ target.accessibilityLabel ??
2090
+ n.text ??
2091
+ n.accessibilityLabel ??
2092
+ null,
2093
+ type: target.type,
2094
+ },
2095
+ strategy: (resolved && resolved.strategy) || 'matched-node',
2096
+ total,
2097
+ idx,
2098
+ }
2099
+ })()`
2100
+ })
2101
+ });
2102
+ }
2103
+ async function tapBest(bridge, query, opts = {}) {
2104
+ const arg = JSON.stringify(query);
2105
+ return tapResolvedTarget(bridge, {
2106
+ agent: opts.agent,
2107
+ timing: opts.timing,
2108
+ textFallback: query,
2109
+ resolve: async () => {
2110
+ const payload = await bridge.send({
2111
+ type: "evaluate",
2112
+ code: `(async () => {
2113
+ const t = window.__sootsimTest
2114
+ if (!t) return { error: 'bridge-not-ready' }
2115
+ const byTestId =
2116
+ (await t.findByTestId(${arg})) || (await t.findById(${arg}))
2117
+ if (byTestId && byTestId.absolutePosition && byTestId.layout) {
2118
+ return {
2119
+ strategy: 'testid',
2120
+ node: {
2121
+ nodeId: byTestId.nodeId ?? null,
2122
+ id: byTestId.id,
2123
+ testID: byTestId.testID,
2124
+ type: byTestId.type,
2125
+ text: byTestId.text,
2126
+ absolutePosition: byTestId.absolutePosition,
2127
+ layout: byTestId.layout,
2128
+ },
2129
+ screen: ${SCREEN_EVAL},
2130
+ }
2131
+ }
2132
+ const byText = await t.findByText(${arg})
2133
+ if (byText && byText.absolutePosition && byText.layout) {
2134
+ return {
2135
+ strategy: 'text',
2136
+ node: {
2137
+ nodeId: byText.nodeId ?? null,
2138
+ id: byText.id,
2139
+ testID: byText.testID,
2140
+ type: byText.type,
2141
+ text: byText.text,
2142
+ absolutePosition: byText.absolutePosition,
2143
+ layout: byText.layout,
2144
+ },
2145
+ screen: ${SCREEN_EVAL},
2146
+ }
2147
+ }
2148
+ return { strategy: 'none' }
2149
+ })()`
2150
+ });
2151
+ if (!payload || !("node" in payload)) return payload;
2152
+ const node = payload.node;
2153
+ const cx = node.absolutePosition.x + node.layout.width / 2;
2154
+ const cy = node.absolutePosition.y + node.layout.height / 2;
2155
+ const screen = payload.screen ?? null;
2156
+ const offscreen = !!screen && (cx < 0 || cy < 0 || cx > screen.width || cy > screen.height);
2157
+ return {
2158
+ ...payload,
2159
+ cx,
2160
+ cy,
2161
+ ...offscreen ? { offscreen: true, screen } : {},
2162
+ target: {
2163
+ id: node.id,
2164
+ testID: node.testID,
2165
+ text: payload.strategy === "text" ? query : node.text,
2166
+ type: node.type
2167
+ }
2168
+ };
2169
+ }
2170
+ });
2171
+ }
2172
+ export {
2173
+ DEBUG_CHANNELS,
2174
+ LAYOUT_GET_NATIVE_EVAL,
2175
+ MERGED_CONSOLE_COUNT_EVAL,
2176
+ NOISY_TIMELINE_KINDS,
2177
+ READY_CONTENT_NODE_FLOOR,
2178
+ READY_NODE_STABLE_MS,
2179
+ READY_PROBE_POLL_MS,
2180
+ WAIT_READY_PROBE,
2181
+ WAIT_READY_PROGRESS_INTERVAL_MS,
2182
+ clearConsole,
2183
+ clearLogs,
2184
+ clearRequests,
2185
+ detectOpenNativeUI,
2186
+ filterLogEntries,
2187
+ formatLayoutElements,
2188
+ formatTimelineEvent,
2189
+ formatTimelineEvents,
2190
+ formatTimelinePayload,
2191
+ formatTimelineSummary,
2192
+ getShellState,
2193
+ inspectAccessibilityTree,
2194
+ inspectDebugFind,
2195
+ inspectDebugFlags,
2196
+ inspectDebugRecent,
2197
+ inspectDebugStatus,
2198
+ inspectDescribe,
2199
+ inspectErrors,
2200
+ inspectFind,
2201
+ inspectGetLayout,
2202
+ inspectKeyboard,
2203
+ inspectLogs,
2204
+ inspectMemory,
2205
+ inspectNodeCount,
2206
+ inspectRequests,
2207
+ inspectScreens,
2208
+ inspectScrollStateAt,
2209
+ inspectTimelineAdvanceCursor,
2210
+ inspectTimelineRecent,
2211
+ inspectTimelineSummary,
2212
+ inspectTree,
2213
+ inspectUrl,
2214
+ inspectWaitReady,
2215
+ inspectWaitSelector,
2216
+ inspectWarnings,
2217
+ isInspectModeActive,
2218
+ isInspectPickCommand,
2219
+ isShellCommandUnavailable,
2220
+ isTapSuccess,
2221
+ layoutGetNativeEval,
2222
+ rankInteractive,
2223
+ readyProbeHasContent,
2224
+ readyProbeHasTargetContent,
2225
+ resolveFindMode,
2226
+ resolveMaxMsFlag,
2227
+ scoreInteractive,
2228
+ scoreLayoutTextContrast,
2229
+ setDebugChannels,
2230
+ shouldSkipAutoSettleForInspectPick,
2231
+ tapBest,
2232
+ tapById,
2233
+ tapByText,
2234
+ tapCommandForNode,
2235
+ tapCoordinates,
2236
+ tapResolvedTarget,
2237
+ tapTargetFromPayload,
2238
+ waitForSootsimIdle,
2239
+ waitReadyReason
2240
+ };