peachsim 0.1.527

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 (324) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +640 -0
  3. package/cli/app-config.ts +82 -0
  4. package/cli/app-fonts.ts +388 -0
  5. package/cli/app-project.ts +59 -0
  6. package/cli/app-splash.ts +171 -0
  7. package/cli/app-state-reset.ts +26 -0
  8. package/cli/auth.ts +184 -0
  9. package/cli/bin.ts +59 -0
  10. package/cli/bridge-diagnostics.ts +226 -0
  11. package/cli/bridge-flow-runner.ts +2806 -0
  12. package/cli/browser-backend.ts +44 -0
  13. package/cli/browser-evals.ts +96 -0
  14. package/cli/cli-update.ts +261 -0
  15. package/cli/cloud-client.ts +1530 -0
  16. package/cli/cloud-dispatch.ts +206 -0
  17. package/cli/cloud-session.ts +227 -0
  18. package/cli/command-contract.ts +221 -0
  19. package/cli/command-registry.ts +304 -0
  20. package/cli/commands/agent-wrapper.ts +1048 -0
  21. package/cli/commands/agent.ts +439 -0
  22. package/cli/commands/app-fonts.ts +99 -0
  23. package/cli/commands/assert.ts +542 -0
  24. package/cli/commands/auth.ts +71 -0
  25. package/cli/commands/box/bundle-plane.ts +71 -0
  26. package/cli/commands/box/checkout-plane.ts +256 -0
  27. package/cli/commands/box/plane-share.ts +208 -0
  28. package/cli/commands/box/rnx-command.ts +60 -0
  29. package/cli/commands/box/search-command.ts +157 -0
  30. package/cli/commands/box.ts +925 -0
  31. package/cli/commands/camera.ts +266 -0
  32. package/cli/commands/cleanup.ts +169 -0
  33. package/cli/commands/compat.ts +176 -0
  34. package/cli/commands/config.ts +34 -0
  35. package/cli/commands/control.ts +2154 -0
  36. package/cli/commands/cpu-profile.ts +941 -0
  37. package/cli/commands/daemon-mac-app.ts +165 -0
  38. package/cli/commands/daemon.ts +800 -0
  39. package/cli/commands/debug.ts +751 -0
  40. package/cli/commands/desktop.ts +39 -0
  41. package/cli/commands/detect.ts +201 -0
  42. package/cli/commands/detox.ts +481 -0
  43. package/cli/commands/device.ts +134 -0
  44. package/cli/commands/diagnose.ts +589 -0
  45. package/cli/commands/do-chain.ts +408 -0
  46. package/cli/commands/electron.ts +94 -0
  47. package/cli/commands/film.ts +379 -0
  48. package/cli/commands/flow.ts +1240 -0
  49. package/cli/commands/inspect/actions.ts +516 -0
  50. package/cli/commands/inspect/core.ts +2086 -0
  51. package/cli/commands/inspect/count.ts +17 -0
  52. package/cli/commands/inspect/describe.ts +119 -0
  53. package/cli/commands/inspect/env.ts +25 -0
  54. package/cli/commands/inspect/find.ts +169 -0
  55. package/cli/commands/inspect/get-layout.ts +53 -0
  56. package/cli/commands/inspect/keyboard.ts +54 -0
  57. package/cli/commands/inspect/list.ts +175 -0
  58. package/cli/commands/inspect/memory.ts +215 -0
  59. package/cli/commands/inspect/redaction.ts +39 -0
  60. package/cli/commands/inspect/resolve-target.ts +77 -0
  61. package/cli/commands/inspect/screens.ts +78 -0
  62. package/cli/commands/inspect/settle.ts +32 -0
  63. package/cli/commands/inspect/settling.ts +197 -0
  64. package/cli/commands/inspect/shared.ts +259 -0
  65. package/cli/commands/inspect/sleep.ts +15 -0
  66. package/cli/commands/inspect/tree.ts +23 -0
  67. package/cli/commands/inspect/url.ts +17 -0
  68. package/cli/commands/inspect/wait-event.ts +211 -0
  69. package/cli/commands/inspect/wait-idle.ts +25 -0
  70. package/cli/commands/inspect/wait-ready.ts +87 -0
  71. package/cli/commands/inspect/wait-selector.ts +59 -0
  72. package/cli/commands/inspect/wait.ts +32 -0
  73. package/cli/commands/inspect.ts +4738 -0
  74. package/cli/commands/install-cli.ts +275 -0
  75. package/cli/commands/install-desktop.ts +385 -0
  76. package/cli/commands/login.ts +357 -0
  77. package/cli/commands/logout.ts +24 -0
  78. package/cli/commands/maestro-generate.ts +375 -0
  79. package/cli/commands/maestro.ts +536 -0
  80. package/cli/commands/mode.ts +58 -0
  81. package/cli/commands/no-bridge-hint.ts +80 -0
  82. package/cli/commands/perf.ts +67 -0
  83. package/cli/commands/permissions.ts +203 -0
  84. package/cli/commands/platform.ts +410 -0
  85. package/cli/commands/preview.ts +103 -0
  86. package/cli/commands/profile.ts +106 -0
  87. package/cli/commands/react.ts +353 -0
  88. package/cli/commands/record.ts +1435 -0
  89. package/cli/commands/report-issue.ts +305 -0
  90. package/cli/commands/reset.ts +89 -0
  91. package/cli/commands/runtime.ts +348 -0
  92. package/cli/commands/screenshot-command.ts +106 -0
  93. package/cli/commands/screenshot-layers.ts +144 -0
  94. package/cli/commands/screenshot-mode.ts +38 -0
  95. package/cli/commands/screenshot.ts +492 -0
  96. package/cli/commands/screenshots-capture.ts +608 -0
  97. package/cli/commands/screenshots.ts +127 -0
  98. package/cli/commands/serve.ts +139 -0
  99. package/cli/commands/setup.ts +416 -0
  100. package/cli/commands/shell-boolean-mode.ts +82 -0
  101. package/cli/commands/skills.ts +468 -0
  102. package/cli/commands/slides.ts +357 -0
  103. package/cli/commands/state.ts +94 -0
  104. package/cli/commands/storage.ts +58 -0
  105. package/cli/commands/telemetry.ts +55 -0
  106. package/cli/commands/test.ts +535 -0
  107. package/cli/commands/three-mode.ts +771 -0
  108. package/cli/commands/timeline.ts +121 -0
  109. package/cli/commands/upgrade.ts +167 -0
  110. package/cli/commands/upload.ts +1203 -0
  111. package/cli/commands/version.ts +63 -0
  112. package/cli/commands/welcome.ts +17 -0
  113. package/cli/commands/what-happened.ts +328 -0
  114. package/cli/current-sim.ts +217 -0
  115. package/cli/desktop-companion.ts +315 -0
  116. package/cli/drivers/electron.ts +70 -0
  117. package/cli/drivers/index.ts +20 -0
  118. package/cli/drivers/playwright-provisioning.ts +184 -0
  119. package/cli/drivers/playwright-sim-host.ts +537 -0
  120. package/cli/drivers/playwright.ts +209 -0
  121. package/cli/drivers/registry.ts +65 -0
  122. package/cli/drivers/types.ts +102 -0
  123. package/cli/flow-export.ts +55 -0
  124. package/cli/flow-file.ts +142 -0
  125. package/cli/flow-live-status.ts +126 -0
  126. package/cli/flow-session.ts +140 -0
  127. package/cli/help-core.ts +143 -0
  128. package/cli/help.ts +44 -0
  129. package/cli/hidden-runtime-alias.ts +19 -0
  130. package/cli/hints.ts +214 -0
  131. package/cli/inspect-notice-state.ts +114 -0
  132. package/cli/internal-child.ts +176 -0
  133. package/cli/maestro-js.ts +323 -0
  134. package/cli/main.ts +704 -0
  135. package/cli/open-url.ts +8 -0
  136. package/cli/outbound-endpoints.ts +830 -0
  137. package/cli/parent-pid.ts +204 -0
  138. package/cli/parse-args.ts +189 -0
  139. package/cli/privacy.ts +53 -0
  140. package/cli/prompt.ts +56 -0
  141. package/cli/recording-access.ts +108 -0
  142. package/cli/registry.ts +2 -0
  143. package/cli/resolve-assets.ts +63 -0
  144. package/cli/rnx-in-process.ts +98 -0
  145. package/cli/run-registry.ts +226 -0
  146. package/cli/run-rnx-core.ts +148 -0
  147. package/cli/run-rnx.ts +137 -0
  148. package/cli/runtime-notes.ts +66 -0
  149. package/cli/runtime-summary.ts +25 -0
  150. package/cli/secure-file.ts +31 -0
  151. package/cli/self-invocation.ts +58 -0
  152. package/cli/send-to-box.ts +109 -0
  153. package/cli/setup-repository.ts +191 -0
  154. package/cli/shell-init.ts +98 -0
  155. package/cli/standalone.ts +4 -0
  156. package/cli/telemetry.ts +84 -0
  157. package/cli/ui.ts +68 -0
  158. package/cli/ws-bridge.ts +1020 -0
  159. package/detox/attempt-events.cjs +23 -0
  160. package/detox/colors.ts +54 -0
  161. package/detox/config-loader.ts +135 -0
  162. package/detox/element-types.ts +48 -0
  163. package/detox/expectations.ts +534 -0
  164. package/detox/gestures.ts +501 -0
  165. package/detox/index.ts +2694 -0
  166. package/detox/jest-preset.cjs +29 -0
  167. package/detox/jest-setup-after-env.cjs +22 -0
  168. package/detox/matchers.ts +30 -0
  169. package/detox/motion-change.cjs +109 -0
  170. package/detox/motion-change.d.cts +35 -0
  171. package/detox/navigation-lifecycle.ts +290 -0
  172. package/detox/navigation.ts +43 -0
  173. package/detox/proof-frame.cjs +114 -0
  174. package/detox/record-block-execution.cjs +78 -0
  175. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  176. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  177. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  178. package/detox/screenshots/theme-blue-toggle.png +0 -0
  179. package/detox/screenshots/theme-blue.png +0 -0
  180. package/detox/screenshots/theme-red-toggle.png +0 -0
  181. package/detox/screenshots/theme-red.png +0 -0
  182. package/dist-lib/agent-daemon-client.cjs +502 -0
  183. package/dist-lib/agent-events.cjs +48 -0
  184. package/dist-lib/agent-identity.cjs +53 -0
  185. package/dist-lib/agent-sessions.cjs +759 -0
  186. package/dist-lib/attached-projects.cjs +526 -0
  187. package/dist-lib/auth/shared-session.cjs +208 -0
  188. package/dist-lib/backend-origin.cjs +88 -0
  189. package/dist-lib/beta.cjs +41 -0
  190. package/dist-lib/beta.mjs +13 -0
  191. package/dist-lib/bridge-constants.cjs +161 -0
  192. package/dist-lib/bridge-contract-input.cjs +451 -0
  193. package/dist-lib/bridge-contract-input.mjs +414 -0
  194. package/dist-lib/bridge-contract.cjs +130 -0
  195. package/dist-lib/bridge-contract.mjs +94 -0
  196. package/dist-lib/capture-contract.cjs +220 -0
  197. package/dist-lib/capture-contract.mjs +190 -0
  198. package/dist-lib/cli-constants.cjs +32 -0
  199. package/dist-lib/cloud-contract.cjs +194 -0
  200. package/dist-lib/cloud-contract.mjs +145 -0
  201. package/dist-lib/cloud.cjs +670 -0
  202. package/dist-lib/cloud.mjs +639 -0
  203. package/dist-lib/config.cjs +96 -0
  204. package/dist-lib/detox/index.cjs +3789 -0
  205. package/dist-lib/detox/jest-preset.cjs +29 -0
  206. package/dist-lib/dev-bundle-resolution.cjs +290 -0
  207. package/dist-lib/home-paths.cjs +638 -0
  208. package/dist-lib/host/bridge-host.cjs +10903 -0
  209. package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
  210. package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
  211. package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
  212. package/dist-lib/host/replacement-module-handler.cjs +469 -0
  213. package/dist-lib/host/websocket-proxy.cjs +298 -0
  214. package/dist-lib/index.cjs +4268 -0
  215. package/dist-lib/jump-to-source-babel.cjs +93 -0
  216. package/dist-lib/jump-to-source-native.cjs +112 -0
  217. package/dist-lib/menu.cjs +646 -0
  218. package/dist-lib/menu.mjs +604 -0
  219. package/dist-lib/metro-fingerprint-registry.cjs +186 -0
  220. package/dist-lib/metro-fingerprint-registry.mjs +156 -0
  221. package/dist-lib/metro-production-bundle.cjs +329 -0
  222. package/dist-lib/metro-production-bundle.mjs +295 -0
  223. package/dist-lib/metro.cjs +904 -0
  224. package/dist-lib/profiles.cjs +246 -0
  225. package/dist-lib/public-brand.cjs +39 -0
  226. package/dist-lib/react-native-host-modules.cjs +51 -0
  227. package/dist-lib/react-native-host-modules.mjs +25 -0
  228. package/dist-lib/render-mode.cjs +54 -0
  229. package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
  230. package/dist-lib/sdk.cjs +2433 -0
  231. package/dist-lib/sdk.mjs +2336 -0
  232. package/dist-lib/skills.cjs +39132 -0
  233. package/dist-lib/swift-submit.cjs +92 -0
  234. package/dist-lib/swift-submit.mjs +65 -0
  235. package/dist-lib/swift.cjs +1646 -0
  236. package/dist-lib/vite.cjs +1591 -0
  237. package/package.json +290 -0
  238. package/scripts/dev-server-scanner.ts +1291 -0
  239. package/scripts/optional-demo-registry.ts +113 -0
  240. package/skills/contrast/SKILL.md +165 -0
  241. package/skills/rnx-debug/SKILL.md +433 -0
  242. package/skills/rnx-perf/SKILL.md +399 -0
  243. package/skills/rnx-setup/SKILL.md +172 -0
  244. package/skills/rnx-test/SKILL.md +321 -0
  245. package/skills/rnx-visual/SKILL.md +126 -0
  246. package/src/agent-daemon-client.ts +410 -0
  247. package/src/agent-events.ts +71 -0
  248. package/src/agent-identity.ts +23 -0
  249. package/src/agent-prompt.ts +71 -0
  250. package/src/agent-sessions.ts +580 -0
  251. package/src/app-url.ts +39 -0
  252. package/src/attached-projects.ts +539 -0
  253. package/src/auth/shared-session.ts +235 -0
  254. package/src/backend-origin.ts +70 -0
  255. package/src/beta.ts +19 -0
  256. package/src/bridge-constants.ts +175 -0
  257. package/src/bridge-contract-input.ts +502 -0
  258. package/src/bridge-contract.ts +558 -0
  259. package/src/browser-cache.ts +16 -0
  260. package/src/capture-contract.ts +587 -0
  261. package/src/cli-constants.ts +1 -0
  262. package/src/cli-version.ts +40 -0
  263. package/src/cloud-contract.ts +274 -0
  264. package/src/cloud.ts +762 -0
  265. package/src/codex-client.ts +234 -0
  266. package/src/config.ts +132 -0
  267. package/src/connect.ts +101 -0
  268. package/src/dev-bundle-resolution.ts +346 -0
  269. package/src/dev-server-open.ts +198 -0
  270. package/src/disk-cleanup.ts +540 -0
  271. package/src/flow-timeout-scale.ts +15 -0
  272. package/src/home-paths.ts +806 -0
  273. package/src/host/agent-host.ts +666 -0
  274. package/src/host/bridge-host.ts +2788 -0
  275. package/src/host/fetch-proxy-handler.ts +449 -0
  276. package/src/host/fetch-proxy-overrides.ts +51 -0
  277. package/src/host/open-url.ts +89 -0
  278. package/src/host/plane-host.ts +203 -0
  279. package/src/host/replacement-module-handler.ts +158 -0
  280. package/src/host/websocket-proxy.ts +315 -0
  281. package/src/index.ts +71 -0
  282. package/src/jump-to-source-babel.ts +135 -0
  283. package/src/jump-to-source-native.ts +102 -0
  284. package/src/menu.ts +777 -0
  285. package/src/metro-fingerprint-registry.ts +302 -0
  286. package/src/metro-plugin.ts +341 -0
  287. package/src/metro-production-bundle.ts +471 -0
  288. package/src/native-dev-bundle-url.ts +118 -0
  289. package/src/native-seam-manifest.ts +341 -0
  290. package/src/playwright-host-log.ts +16 -0
  291. package/src/plist.ts +27 -0
  292. package/src/poll-until.ts +123 -0
  293. package/src/profiles.ts +179 -0
  294. package/src/public-brand.ts +9 -0
  295. package/src/react-native-host-modules.ts +26 -0
  296. package/src/render-mode.ts +27 -0
  297. package/src/rolldown-plugin-swift.ts +240 -0
  298. package/src/runtime-assets.ts +233 -0
  299. package/src/runtime-delivery.ts +54 -0
  300. package/src/screenshots/compose.ts +422 -0
  301. package/src/screenshots/frame-compose.ts +514 -0
  302. package/src/screenshots/orchestrate.ts +256 -0
  303. package/src/screenshots/org-deck.ts +91 -0
  304. package/src/screenshots/registry.ts +58 -0
  305. package/src/screenshots/schema.ts +364 -0
  306. package/src/sdk.ts +15 -0
  307. package/src/settings-bundle.ts +418 -0
  308. package/src/shell-bridge-identity.ts +15 -0
  309. package/src/sim-client.ts +697 -0
  310. package/src/skills/builtin/a11y-review.ts +132 -0
  311. package/src/skills/builtin/compat-check.ts +71 -0
  312. package/src/skills/builtin/maestro-test.ts +121 -0
  313. package/src/skills/builtin/perf-profile.ts +90 -0
  314. package/src/skills/builtin/screenshot-all.ts +52 -0
  315. package/src/skills/builtin/visual-diff.ts +100 -0
  316. package/src/skills/registry.ts +107 -0
  317. package/src/skills/types.ts +41 -0
  318. package/src/swift-submit.ts +185 -0
  319. package/src/swift.ts +7 -0
  320. package/src/team-machine-prompt.ts +73 -0
  321. package/src/vite-plugin-one.ts +264 -0
  322. package/src/vite-plugin-swift.ts +620 -0
  323. package/src/vite-plugin.ts +1946 -0
  324. package/src/worklets-babel.ts +250 -0
@@ -0,0 +1,516 @@
1
+ import { inspectTree, type InspectBridge } from './core'
2
+ import { resolveTargetCoords, type ResolvedTargetCoords } from './resolve-target'
3
+ import { waitForSootsimIdle } from './settling'
4
+ import type { SimSemanticNode } from '../../../src/bridge-contract'
5
+ import type { SootSimReplayTarget } from '@rnx/globals'
6
+
7
+ export type TapFailure = 'not-found' | 'missed' | 'special'
8
+
9
+ export type TapTargetSummary = SootSimReplayTarget
10
+
11
+ export interface TapOutcome {
12
+ payload: any
13
+ result: any
14
+ attempts: number
15
+ failure?: TapFailure
16
+ }
17
+
18
+ export interface TapTextOptions {
19
+ exact?: boolean
20
+ role?: string | null
21
+ within?: string | null
22
+ minX?: number | null
23
+ maxX?: number | null
24
+ minY?: number | null
25
+ maxY?: number | null
26
+ near?: { x: number; y: number } | null
27
+ nth?: number | null
28
+ first?: boolean
29
+ }
30
+
31
+ export interface TapTimingOptions {
32
+ initialWaitMs?: number
33
+ deadlineMs?: number
34
+ retryWaitMs?: number
35
+ }
36
+
37
+ type ScreenSize = { width: number; height: number }
38
+
39
+ function isScreenSize(value: unknown): value is ScreenSize {
40
+ if (value === null || typeof value !== 'object') return false
41
+ const width = Reflect.get(value, 'width')
42
+ const height = Reflect.get(value, 'height')
43
+ return (
44
+ typeof width === 'number' &&
45
+ Number.isFinite(width) &&
46
+ width > 0 &&
47
+ typeof height === 'number' &&
48
+ Number.isFinite(height) &&
49
+ height > 0
50
+ )
51
+ }
52
+
53
+ function viewportFromCapture(value: unknown): unknown {
54
+ if (value === null || typeof value !== 'object') return undefined
55
+ const tree = Reflect.get(value, 'tree')
56
+ return tree !== null && typeof tree === 'object'
57
+ ? Reflect.get(tree, 'viewport')
58
+ : undefined
59
+ }
60
+
61
+ // the semantic tree has no covering root, so its first node is often a 64x64
62
+ // icon. capture.tree.viewport is the device layout size on local and cloud.
63
+ //
64
+ // a whole screen capture is the only way to ask for it today, and it is not
65
+ // cheap: the headless runtime forces a full redraw of the tree before it walks
66
+ // it, which measured 3.9-5.5s on a real app in a cloud box against 120-290ms
67
+ // for every other command. that is why this runs on the failure path only.
68
+ async function readScreen(bridge: InspectBridge): Promise<ScreenSize | null> {
69
+ try {
70
+ const viewport = viewportFromCapture(
71
+ await bridge.send({ type: 'capture', includeVisual: false }),
72
+ )
73
+ return isScreenSize(viewport)
74
+ ? { width: viewport.width, height: viewport.height }
75
+ : null
76
+ } catch {
77
+ return null
78
+ }
79
+ }
80
+
81
+ function isOffscreen(cx: number, cy: number, screen: ScreenSize): boolean {
82
+ return !(cx >= 0 && cy >= 0 && cx <= screen.width && cy <= screen.height)
83
+ }
84
+
85
+ const DEFAULT_AGENT_TIMING: Required<TapTimingOptions> = {
86
+ initialWaitMs: 3000,
87
+ deadlineMs: 5000,
88
+ retryWaitMs: 700,
89
+ }
90
+
91
+ const DEFAULT_INTERACTIVE_TIMING: Required<TapTimingOptions> = {
92
+ initialWaitMs: 1200,
93
+ deadlineMs: 2500,
94
+ retryWaitMs: 700,
95
+ }
96
+
97
+ function tapTiming(agent: boolean, opts: TapTimingOptions = {}) {
98
+ return {
99
+ ...(agent ? DEFAULT_AGENT_TIMING : DEFAULT_INTERACTIVE_TIMING),
100
+ ...opts,
101
+ }
102
+ }
103
+
104
+ export function isTapSuccess(result: any): boolean {
105
+ if (!result || result.hit === false || result.ok === false) return false
106
+ if (result.handled === false) return false
107
+ if (result.requestedTargetMatched === false) return false
108
+ if (
109
+ result.pointerTapHandled === false &&
110
+ !result.keyboardOpened &&
111
+ !result.isTextInput
112
+ ) {
113
+ return false
114
+ }
115
+ return true
116
+ }
117
+
118
+ export function tapTargetFromPayload(
119
+ payload: any,
120
+ textFallback?: string,
121
+ ): TapTargetSummary {
122
+ const id = payload.target?.id ?? payload.match?.id ?? payload.node?.id
123
+ return {
124
+ nodeId:
125
+ payload.target?.nodeId ?? payload.match?.nodeId ?? payload.node?.nodeId ?? null,
126
+ id: typeof id === 'string' ? id : null,
127
+ testID:
128
+ payload.target?.testID ?? payload.match?.testID ?? payload.node?.testID ?? null,
129
+ text:
130
+ payload.target?.text ??
131
+ payload.target?.accessibilityLabel ??
132
+ payload.match?.text ??
133
+ payload.match?.accessibilityLabel ??
134
+ payload.node?.text ??
135
+ textFallback ??
136
+ null,
137
+ type: payload.target?.type ?? payload.match?.type ?? payload.node?.type ?? null,
138
+ }
139
+ }
140
+
141
+ export async function tapResolvedTarget(
142
+ bridge: InspectBridge,
143
+ args: {
144
+ textFallback?: string
145
+ resolve: () => Promise<any>
146
+ agent?: boolean
147
+ timing?: TapTimingOptions
148
+ },
149
+ ): Promise<TapOutcome> {
150
+ const timing = tapTiming(!!args.agent, args.timing)
151
+ let attempts = 0
152
+ let lastPayload: any = null
153
+ let lastResult: any = null
154
+
155
+ // a CPU simulator only advances its animations while a command holds its
156
+ // clock, so a sheet that was still sliding in when the last command returned
157
+ // is still mid-slide now. settling first is what lets the resolved center and
158
+ // the hit test below describe the same tree.
159
+ const settleCloud = async () => {
160
+ try {
161
+ await bridge.send({ type: 'settle' })
162
+ } catch {
163
+ // best-effort readiness gate; the retry loop decides success.
164
+ }
165
+ }
166
+ if (bridge.plane === 'cloud') {
167
+ await settleCloud()
168
+ } else if (timing.initialWaitMs > 0) {
169
+ try {
170
+ await waitForSootsimIdle({
171
+ bridge,
172
+ maxMs: timing.initialWaitMs,
173
+ pollMs: 32,
174
+ stablePolls: 2,
175
+ })
176
+ } catch {
177
+ // best-effort readiness gate; the retry loop decides success.
178
+ }
179
+ }
180
+
181
+ const deadline = Date.now() + timing.deadlineMs
182
+ let lastOffscreenAt: string | null = null
183
+ // a cloud sim refuses a point outside its screen itself, and says so, so
184
+ // asking it for a capture first would buy nothing. every other plane still
185
+ // presses whatever the hit test finds at tree coordinates no finger can
186
+ // reach, so there the screen has to be read before dispatching.
187
+ const screen = bridge.plane === 'cloud' ? null : await readScreen(bridge)
188
+ while (Date.now() <= deadline || attempts === 0) {
189
+ attempts++
190
+ const resolved = await args.resolve()
191
+ const payload =
192
+ resolved &&
193
+ typeof resolved.cx === 'number' &&
194
+ typeof resolved.cy === 'number' &&
195
+ screen &&
196
+ isOffscreen(resolved.cx, resolved.cy, screen)
197
+ ? { ...resolved, offscreen: true, screen }
198
+ : resolved
199
+ lastPayload = payload
200
+
201
+ if (
202
+ payload?.error === 'bridge-not-ready' ||
203
+ payload?.ambiguous ||
204
+ payload?.nthOutOfRange
205
+ ) {
206
+ return { payload, result: null, attempts, failure: 'special' }
207
+ }
208
+
209
+ if (payload && typeof payload.cx === 'number' && typeof payload.cy === 'number') {
210
+ // don't dispatch a point outside the device screen: the press would do
211
+ // nothing a user could reproduce. report the offscreen miss instead so
212
+ // drivers scroll first rather than getting stuck trusting a phantom hit.
213
+ if (payload.offscreen) {
214
+ lastResult = {
215
+ hit: false,
216
+ reason: 'offscreen',
217
+ x: payload.cx,
218
+ y: payload.cy,
219
+ screen: payload.screen,
220
+ }
221
+ // stable offscreen position twice in a row is deterministic — the
222
+ // node is scrolled out of view, not settling in. bail early instead
223
+ // of retrying to the deadline.
224
+ const at = `${Math.round(payload.cx)},${Math.round(payload.cy)}`
225
+ if (lastOffscreenAt === at) {
226
+ return { payload, result: lastResult, attempts, failure: 'missed' }
227
+ }
228
+ lastOffscreenAt = at
229
+ } else {
230
+ lastOffscreenAt = null
231
+ const requestedTarget = tapTargetFromPayload(payload, args.textFallback)
232
+ const result = await tapCoordinates(
233
+ bridge,
234
+ payload.cx,
235
+ payload.cy,
236
+ requestedTarget,
237
+ )
238
+ // a cloud sim reports the offscreen refusal itself, carrying the point
239
+ // and the screen in its diagnostics. give it the same shape the local
240
+ // check produces so there is one offscreen failure to report.
241
+ lastResult =
242
+ result?.reason === 'offscreen' && result.diagnostics
243
+ ? {
244
+ ...result,
245
+ hit: false,
246
+ x: result.diagnostics.point?.x ?? payload.cx,
247
+ y: result.diagnostics.point?.y ?? payload.cy,
248
+ screen: result.diagnostics.screen,
249
+ }
250
+ : result
251
+ if (result?.requestedTargetMatched === false) {
252
+ return { payload, result, attempts, failure: 'missed' }
253
+ }
254
+ if (isTapSuccess(result)) return { payload, result, attempts }
255
+ }
256
+ }
257
+
258
+ const remaining = deadline - Date.now()
259
+ if (remaining <= 0) break
260
+ if (bridge.plane === 'cloud') {
261
+ if (lastResult?.reason !== 'target-moved' || attempts >= 3) break
262
+ await settleCloud()
263
+ continue
264
+ }
265
+ if (timing.retryWaitMs <= 0) break
266
+ try {
267
+ await waitForSootsimIdle({
268
+ bridge,
269
+ maxMs: Math.min(remaining, timing.retryWaitMs),
270
+ pollMs: 32,
271
+ stablePolls: 2,
272
+ })
273
+ } catch {
274
+ await new Promise((resolve) => setTimeout(resolve, Math.min(120, remaining)))
275
+ }
276
+ }
277
+
278
+ return {
279
+ payload: lastPayload,
280
+ result: lastResult,
281
+ attempts,
282
+ failure: lastPayload && typeof lastPayload.cx === 'number' ? 'missed' : 'not-found',
283
+ }
284
+ }
285
+
286
+ export async function tapCoordinates(
287
+ bridge: InspectBridge,
288
+ x: number,
289
+ y: number,
290
+ target?: TapTargetSummary,
291
+ ): Promise<any> {
292
+ return bridge.send({
293
+ type: 'tap',
294
+ x,
295
+ y,
296
+ ...(target ? { target } : {}),
297
+ })
298
+ }
299
+
300
+ function payloadFromResolved(
301
+ resolved: ResolvedTargetCoords,
302
+ match: SootSimReplayTarget,
303
+ extra: Record<string, unknown> = {},
304
+ ) {
305
+ return {
306
+ cx: resolved.x,
307
+ cy: resolved.y,
308
+ match: {
309
+ nodeId: match.nodeId ?? null,
310
+ id: match.testID ?? null,
311
+ testID: match.testID ?? null,
312
+ text: match.text ?? null,
313
+ type: match.type ?? null,
314
+ },
315
+ target: {
316
+ nodeId: resolved.nodeId ?? match.nodeId ?? null,
317
+ id: resolved.testID ?? match.testID ?? null,
318
+ testID: resolved.testID ?? match.testID ?? null,
319
+ text: resolved.text ?? match.text ?? null,
320
+ type: resolved.type ?? match.type ?? null,
321
+ },
322
+ strategy: 'matched-node',
323
+ ...extra,
324
+ }
325
+ }
326
+
327
+ interface TextCandidate {
328
+ node: SimSemanticNode
329
+ ancestorTestIDs: string[]
330
+ }
331
+
332
+ // a non-pressable node repeating the text of its nearest labelled ancestor,
333
+ // when that ancestor is pressable, is the ancestor's own caption (a button and
334
+ // its text child), so only the button is a candidate. otherwise every labelled
335
+ // button is an ambiguous pair. a pressable descendant is never folded into its
336
+ // ancestor: same text does not prove it shares the ancestor's tap target.
337
+ function collectTextCandidates(
338
+ nodes: readonly SimSemanticNode[],
339
+ ancestors: string[] = [],
340
+ pressableOwnerText = '',
341
+ ): TextCandidate[] {
342
+ const out: TextCandidate[] = []
343
+ for (const node of nodes) {
344
+ const text = nodeText(node)
345
+ const caption = !!text && text === pressableOwnerText && !node.pressable
346
+ if (!caption) out.push({ node, ancestorTestIDs: ancestors })
347
+ if (node.children?.length) {
348
+ const next = node.testID ? [...ancestors, node.testID] : ancestors
349
+ const owner = text ? (node.pressable ? text : '') : pressableOwnerText
350
+ out.push(...collectTextCandidates(node.children, next, owner))
351
+ }
352
+ }
353
+ return out
354
+ }
355
+
356
+ function nodeText(node: SimSemanticNode): string {
357
+ return node.text ?? node.label ?? ''
358
+ }
359
+
360
+ export async function tapById(
361
+ bridge: InspectBridge,
362
+ query: string,
363
+ opts: { agent?: boolean; timing?: TapTimingOptions } = {},
364
+ ): Promise<TapOutcome> {
365
+ return tapResolvedTarget(bridge, {
366
+ agent: opts.agent,
367
+ timing: opts.timing,
368
+ resolve: async () => {
369
+ const resolved = await resolveTargetCoords(bridge, { mode: 'testid', value: query })
370
+ if (!resolved) return { cx: null }
371
+ return payloadFromResolved(resolved, resolved)
372
+ },
373
+ })
374
+ }
375
+
376
+ export async function tapByText(
377
+ bridge: InspectBridge,
378
+ query: string,
379
+ options: TapTextOptions = {},
380
+ opts: { agent?: boolean; timing?: TapTimingOptions } = {},
381
+ ): Promise<TapOutcome> {
382
+ return tapResolvedTarget(bridge, {
383
+ agent: opts.agent,
384
+ timing: opts.timing,
385
+ textFallback: query,
386
+ resolve: async () => {
387
+ const { tree } = await inspectTree(bridge, 50)
388
+ if (!Array.isArray(tree)) return { matched: 0, total: 0 }
389
+ const needle = options.exact ? query : query.toLowerCase()
390
+ let candidates = collectTextCandidates(tree).filter(({ node, ancestorTestIDs }) => {
391
+ const text = nodeText(node)
392
+ if (!text) return false
393
+ if (options.exact ? text !== needle : !text.toLowerCase().includes(needle)) {
394
+ return false
395
+ }
396
+ if (options.role && node.role !== options.role) return false
397
+ if (options.within && !ancestorTestIDs.includes(options.within)) return false
398
+ const ax = node.geometry.x
399
+ const ay = node.geometry.y
400
+ if (options.minX != null && !(ax >= options.minX)) return false
401
+ if (options.maxX != null && !(ax <= options.maxX)) return false
402
+ if (options.minY != null && !(ay >= options.minY)) return false
403
+ if (options.maxY != null && !(ay <= options.maxY)) return false
404
+ return true
405
+ })
406
+
407
+ if (options.near) {
408
+ const { x, y } = options.near
409
+ candidates.sort(
410
+ (a, b) =>
411
+ Math.hypot(a.node.geometry.x - x, a.node.geometry.y - y) -
412
+ Math.hypot(b.node.geometry.x - x, b.node.geometry.y - y),
413
+ )
414
+ } else {
415
+ candidates.sort((a, b) => {
416
+ if (Math.abs(a.node.geometry.y - b.node.geometry.y) > 2) {
417
+ return a.node.geometry.y - b.node.geometry.y
418
+ }
419
+ return a.node.geometry.x - b.node.geometry.x
420
+ })
421
+ }
422
+
423
+ const total = candidates.length
424
+ if (total === 0) return { matched: 0, total: 0 }
425
+
426
+ let idx = 0
427
+ if (options.nth != null) {
428
+ idx = options.nth < 0 ? total + options.nth : options.nth
429
+ if (idx < 0 || idx >= total) {
430
+ return { matched: 0, total, nthOutOfRange: true, nth: options.nth }
431
+ }
432
+ } else if (total > 1 && !options.first && !options.near) {
433
+ return {
434
+ ambiguous: true,
435
+ total,
436
+ candidates: candidates.slice(0, 10).map((candidate, index) => ({
437
+ idx: index,
438
+ nodeId: candidate.node.nodeId,
439
+ type: candidate.node.type,
440
+ testID: candidate.node.testID ?? null,
441
+ text: nodeText(candidate.node).slice(0, 60),
442
+ abs: { x: candidate.node.geometry.x, y: candidate.node.geometry.y },
443
+ layout: {
444
+ width: Math.round(candidate.node.geometry.width),
445
+ height: Math.round(candidate.node.geometry.height),
446
+ },
447
+ ancestorTestIDs: candidate.ancestorTestIDs.slice(0, 5),
448
+ })),
449
+ }
450
+ }
451
+
452
+ const picked = candidates[idx]
453
+ if (!picked) return { matched: 0, total }
454
+ const node = picked.node
455
+ let resolved = node.testID
456
+ ? await resolveTargetCoords(bridge, { mode: 'testid', value: node.testID })
457
+ : await resolveTargetCoords(bridge, {
458
+ mode: 'text',
459
+ value: nodeText(node) || query,
460
+ })
461
+ if (!resolved) {
462
+ resolved = {
463
+ x: node.geometry.x + node.geometry.width / 2,
464
+ y: node.geometry.y + node.geometry.height / 2,
465
+ testID: node.testID ?? null,
466
+ text: nodeText(node) || query,
467
+ type: node.type,
468
+ }
469
+ }
470
+ return payloadFromResolved(resolved, node, { total, idx })
471
+ },
472
+ })
473
+ }
474
+
475
+ export async function tapBest(
476
+ bridge: InspectBridge,
477
+ query: string,
478
+ opts: { agent?: boolean; timing?: TapTimingOptions } = {},
479
+ ): Promise<TapOutcome> {
480
+ return tapResolvedTarget(bridge, {
481
+ agent: opts.agent,
482
+ timing: opts.timing,
483
+ textFallback: query,
484
+ resolve: async () => {
485
+ const byId = await resolveTargetCoords(bridge, { mode: 'testid', value: query })
486
+ if (byId) {
487
+ return {
488
+ ...payloadFromResolved(byId, byId),
489
+ strategy: 'testid',
490
+ node: {
491
+ nodeId: byId.nodeId ?? null,
492
+ id: byId.testID ?? query,
493
+ testID: byId.testID ?? query,
494
+ type: byId.type ?? null,
495
+ text: byId.text ?? null,
496
+ },
497
+ }
498
+ }
499
+ const byText = await resolveTargetCoords(bridge, { mode: 'text', value: query })
500
+ if (byText) {
501
+ return {
502
+ ...payloadFromResolved(byText, byText),
503
+ strategy: 'text',
504
+ node: {
505
+ nodeId: byText.nodeId ?? null,
506
+ id: byText.testID ?? null,
507
+ testID: byText.testID ?? null,
508
+ type: byText.type ?? null,
509
+ text: byText.text ?? query,
510
+ },
511
+ }
512
+ }
513
+ return { strategy: 'none' }
514
+ },
515
+ })
516
+ }