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,501 @@
1
+ import { pollUntil } from '../src/poll-until'
2
+ // pull in @rnx/globals so window.__sootsimTest and
3
+ // window.__sootsimFindInteractiveCanvas are typed inside page.evaluate
4
+ // bodies. all sootsim-namespaced globals are declared canonically there —
5
+ // add new entries to packages/rnx-globals/src/index.ts, not here.
6
+ import type {} from '@rnx/globals'
7
+ import type { Page } from 'playwright'
8
+
9
+ // shape of the surface-metrics bridge response we care about. the bridge
10
+ // types it as `Record<string, unknown> | null` (the contract is open) so we
11
+ // narrow at the call sites by casting to this local view.
12
+ type SurfaceMetricsView = {
13
+ window?: { width?: number; height?: number; scale?: number }
14
+ device?: { homeIndicatorHeight?: number }
15
+ safeAreaInsets?: { bottom?: number }
16
+ visibleRect?: { y?: number; height?: number }
17
+ }
18
+ type SurfaceMetricsResult = SurfaceMetricsView | null | undefined
19
+
20
+ type SootsimPageMetrics = {
21
+ canvas: { left: number; top: number; width: number; height: number }
22
+ window: { width: number; height: number }
23
+ interactiveWindow: { width: number; height: number }
24
+ }
25
+ type WorkletCallbackStats = {
26
+ tenantReceived: number
27
+ shellSent: number
28
+ }
29
+
30
+ export function resolveScrollStartPosition(
31
+ value: number | undefined,
32
+ label: string,
33
+ ): number {
34
+ const normalized = value === undefined || Number.isNaN(value) ? 0.5 : value
35
+ if (!Number.isFinite(normalized) || normalized < 0 || normalized > 1) {
36
+ throw new Error(`${label} must be between 0 and 1`)
37
+ }
38
+ return normalized
39
+ }
40
+
41
+ type ShellGestureSeamDebug = {
42
+ attached?: Array<{
43
+ viewTag?: unknown
44
+ nodeFound?: unknown
45
+ handlers?: Array<{ name?: unknown }>
46
+ }>
47
+ }
48
+
49
+ export async function waitForSootsimGestureHandler(
50
+ page: Page,
51
+ viewTag: number,
52
+ handlerName: string,
53
+ ): Promise<void> {
54
+ await pollUntil(
55
+ () =>
56
+ page.evaluate(
57
+ async ({ viewTag, handlerName }) => {
58
+ const host = (
59
+ window as typeof window & {
60
+ SootSim?: {
61
+ bridges?: {
62
+ shellHost?: {
63
+ callTestBridge?: (
64
+ method: string,
65
+ ...args: unknown[]
66
+ ) => Promise<ShellGestureSeamDebug>
67
+ } | null
68
+ }
69
+ }
70
+ }
71
+ ).SootSim?.bridges?.shellHost
72
+ if (typeof host?.callTestBridge !== 'function') return false
73
+ let debug: ShellGestureSeamDebug | null = null
74
+ try {
75
+ debug = await host.callTestBridge('getShellGestureSeamDebug')
76
+ } catch {
77
+ return false
78
+ }
79
+ return (
80
+ debug?.attached?.some(
81
+ (entry) =>
82
+ entry.viewTag === viewTag &&
83
+ entry.nodeFound !== false &&
84
+ entry.handlers?.some((handler) => handler.name === handlerName),
85
+ ) === true
86
+ )
87
+ },
88
+ { viewTag, handlerName },
89
+ ),
90
+ Boolean,
91
+ {
92
+ timeoutMs: 5000,
93
+ intervalMs: 100,
94
+ label: `${handlerName} gesture handler on view ${viewTag}`,
95
+ },
96
+ )
97
+ }
98
+
99
+ async function readWorkletCallbackStats(
100
+ page: Page,
101
+ ): Promise<WorkletCallbackStats | null> {
102
+ return page.evaluate(async () => {
103
+ type Stats = {
104
+ runJSCallbackSent?: unknown
105
+ runJSCallbackReceived?: unknown
106
+ } | null
107
+ const g = window as typeof window & {
108
+ __sootsimTest?: { getWorkletSlotStats?: () => Promise<Stats> | Stats }
109
+ SootSim?: {
110
+ bridges?: {
111
+ shellHost?: {
112
+ callTestBridge?: (method: string, ...args: unknown[]) => Promise<Stats>
113
+ } | null
114
+ }
115
+ }
116
+ }
117
+ const tenant =
118
+ typeof g.__sootsimTest?.getWorkletSlotStats === 'function'
119
+ ? await g.__sootsimTest.getWorkletSlotStats()
120
+ : null
121
+ const shellHost = g.SootSim?.bridges?.shellHost
122
+ let shell: Stats = null
123
+ if (typeof shellHost?.callTestBridge === 'function') {
124
+ try {
125
+ shell = await shellHost.callTestBridge('getWorkletSlotStats')
126
+ } catch {}
127
+ }
128
+ const tenantReceived =
129
+ typeof tenant?.runJSCallbackReceived === 'number' ? tenant.runJSCallbackReceived : 0
130
+ const shellSent =
131
+ typeof shell?.runJSCallbackSent === 'number' ? shell.runJSCallbackSent : 0
132
+ return { tenantReceived, shellSent }
133
+ })
134
+ }
135
+
136
+ async function waitForWorkletCallbackDrain(
137
+ page: Page,
138
+ before: WorkletCallbackStats | null,
139
+ timeoutMs: number,
140
+ ): Promise<void> {
141
+ if (!before) {
142
+ await page.waitForTimeout(250)
143
+ return
144
+ }
145
+ let lastShellSent = before.shellSent
146
+ let stableSince = Date.now()
147
+ let sawCallback = false
148
+
149
+ await pollUntil(
150
+ () => readWorkletCallbackStats(page),
151
+ (current) => {
152
+ if (!current) return false
153
+ if (current.shellSent !== lastShellSent) {
154
+ lastShellSent = current.shellSent
155
+ stableSince = Date.now()
156
+ }
157
+ const sentDelta = current.shellSent - before.shellSent
158
+ if (sentDelta > 0) sawCallback = true
159
+ const receivedDelta = current.tenantReceived - before.tenantReceived
160
+ return sawCallback && receivedDelta >= sentDelta && Date.now() - stableSince >= 100
161
+ },
162
+ {
163
+ timeoutMs,
164
+ intervalMs: 20,
165
+ label: 'worklet callback drain',
166
+ },
167
+ ).catch(() => {})
168
+
169
+ if (!sawCallback) {
170
+ // Some gestures are purely native and do not schedule RN callbacks. A
171
+ // bounded fallback still preserves Detox's "gesture has settled" contract.
172
+ await page.waitForTimeout(250)
173
+ }
174
+ }
175
+
176
+ // helper script injected via page.addInitScript so every page.evaluate
177
+ // can reach it as `__sootsimFindInteractiveCanvas()`. picks the canvas
178
+ // that actually receives pointer input. sootsim renders multiple
179
+ // <canvas> layers per surface (background/render targets, hidden
180
+ // offscreen, overlay) and `document.querySelector('canvas')` returns
181
+ // the first one in DOM order — usually a non-interactive layer with
182
+ // `pointer-events: none`. dispatching synthetic PointerEvents onto
183
+ // that layer silently no-ops because the gesture system only listens
184
+ // on the interactive layer (pointer-events: auto, non-zero rect).
185
+ const sootsimCanvasFinderInstalled = new WeakSet<Page>()
186
+ function installInteractiveCanvasFinder() {
187
+ window.__sootsimFindInteractiveCanvas = (): HTMLCanvasElement | null => {
188
+ const interactive: Array<{ c: HTMLCanvasElement; z: number; app: boolean }> = []
189
+ const appVisible: Array<{ c: HTMLCanvasElement; z: number; app: boolean }> = []
190
+ for (const c of Array.from(document.querySelectorAll('canvas'))) {
191
+ const s = window.getComputedStyle(c)
192
+ if (s.display === 'none' || s.visibility === 'hidden') continue
193
+ const r = c.getBoundingClientRect()
194
+ if (r.width === 0 || r.height === 0) continue
195
+ const entry = {
196
+ c,
197
+ z: parseInt(s.zIndex, 10) || 0,
198
+ app: c.dataset.surfaceId?.startsWith('app:') === true,
199
+ }
200
+ if (entry.app) appVisible.push(entry)
201
+ if (s.pointerEvents !== 'none') interactive.push(entry)
202
+ }
203
+ const list = appVisible.length > 0 ? appVisible : interactive
204
+ if (list.length === 0) return null
205
+ list.sort((a, b) => Number(b.app) - Number(a.app) || b.z - a.z)
206
+ return list[0].c
207
+ }
208
+ }
209
+
210
+ async function ensureCanvasFinder(page: Page) {
211
+ if (!sootsimCanvasFinderInstalled.has(page)) {
212
+ sootsimCanvasFinderInstalled.add(page)
213
+ await page.addInitScript(installInteractiveCanvasFinder)
214
+ }
215
+ await page.evaluate(installInteractiveCanvasFinder)
216
+ }
217
+
218
+ async function readSootsimPageMetrics(
219
+ page: Page,
220
+ timeoutMs: number,
221
+ ): Promise<SootsimPageMetrics> {
222
+ await ensureCanvasFinder(page)
223
+ try {
224
+ const metrics = await pollUntil(
225
+ () =>
226
+ page.evaluate(async () => {
227
+ const canvas =
228
+ window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector('canvas')
229
+ if (!canvas) return null
230
+ const rect = canvas.getBoundingClientRect()
231
+ if (rect.width <= 0 || rect.height <= 0) return null
232
+ const surface =
233
+ (await window.__sootsimTest?.getSurfaceMetricsSnapshot?.()) as SurfaceMetricsResult
234
+ const screenWidth = surface?.window?.width
235
+ const screenHeight = surface?.window?.height
236
+ if (
237
+ typeof screenWidth !== 'number' ||
238
+ typeof screenHeight !== 'number' ||
239
+ screenWidth <= 0 ||
240
+ screenHeight <= 0
241
+ ) {
242
+ return null
243
+ }
244
+ const homeIndicatorHeight = Number(surface?.device?.homeIndicatorHeight) || 0
245
+ const safeAreaBottom = Number(surface?.safeAreaInsets?.bottom) || 0
246
+ const bottomInset = Math.max(0, homeIndicatorHeight, safeAreaBottom)
247
+ const visibleRectBottom =
248
+ Number(surface?.visibleRect?.y) + Number(surface?.visibleRect?.height)
249
+ const interactiveHeight =
250
+ Number.isFinite(visibleRectBottom) && visibleRectBottom > 0
251
+ ? Math.min(screenHeight, visibleRectBottom)
252
+ : Math.max(0, screenHeight - bottomInset)
253
+ return {
254
+ canvas: {
255
+ left: rect.left,
256
+ top: rect.top,
257
+ width: rect.width,
258
+ height: rect.height,
259
+ },
260
+ window: {
261
+ width: screenWidth,
262
+ height: screenHeight,
263
+ },
264
+ interactiveWindow: {
265
+ width: screenWidth,
266
+ height: interactiveHeight,
267
+ },
268
+ }
269
+ }),
270
+ (metrics) => metrics !== null,
271
+ {
272
+ timeoutMs,
273
+ intervalMs: 50,
274
+ label: 'sootsim surface metrics',
275
+ },
276
+ )
277
+ if (metrics === null) throw new Error('surface metrics poll settled without metrics')
278
+ return metrics
279
+ } catch (error) {
280
+ throw new Error('sootsim surface metrics are not available', { cause: error })
281
+ }
282
+ }
283
+
284
+ export async function readSootsimViewport(
285
+ page: Page,
286
+ timeoutMs = 5000,
287
+ ): Promise<{ width: number; height: number }> {
288
+ return (await readSootsimPageMetrics(page, timeoutMs)).window
289
+ }
290
+
291
+ export async function readSootsimInteractiveViewport(
292
+ page: Page,
293
+ timeoutMs = 5000,
294
+ ): Promise<{ width: number; height: number }> {
295
+ return (await readSootsimPageMetrics(page, timeoutMs)).interactiveWindow
296
+ }
297
+
298
+ export async function sootsimToPage(
299
+ page: Page,
300
+ sootsimX: number,
301
+ sootsimY: number,
302
+ timeoutMs = 5000,
303
+ ): Promise<{ x: number; y: number }> {
304
+ const metrics = await readSootsimPageMetrics(page, timeoutMs)
305
+ const scaleX = metrics.canvas.width / metrics.window.width
306
+ const scaleY = metrics.canvas.height / metrics.window.height
307
+ return {
308
+ x: metrics.canvas.left + sootsimX * scaleX,
309
+ y: metrics.canvas.top + sootsimY * scaleY,
310
+ }
311
+ }
312
+
313
+ export async function dragScrollNode(
314
+ page: Page,
315
+ node: {
316
+ absolutePosition: { x: number; y: number }
317
+ layout: { width: number; height: number }
318
+ },
319
+ pixels: number,
320
+ direction: 'up' | 'down' | 'left' | 'right',
321
+ startPositionX?: number,
322
+ startPositionY?: number,
323
+ ): Promise<void> {
324
+ const normalizedStartX = resolveScrollStartPosition(
325
+ startPositionX,
326
+ 'scroll startPositionX',
327
+ )
328
+ const normalizedStartY = resolveScrollStartPosition(
329
+ startPositionY,
330
+ 'scroll startPositionY',
331
+ )
332
+ const startSootSimX = node.absolutePosition.x + node.layout.width * normalizedStartX
333
+ const startSootSimY = node.absolutePosition.y + node.layout.height * normalizedStartY
334
+ let endSootSimX = startSootSimX
335
+ let endSootSimY = startSootSimY
336
+
337
+ switch (direction) {
338
+ case 'down':
339
+ endSootSimY -= pixels
340
+ break
341
+ case 'up':
342
+ endSootSimY += pixels
343
+ break
344
+ case 'left':
345
+ endSootSimX += pixels
346
+ break
347
+ case 'right':
348
+ endSootSimX -= pixels
349
+ break
350
+ }
351
+
352
+ const startPage = await sootsimToPage(page, startSootSimX, startSootSimY)
353
+ const endPage = await sootsimToPage(page, endSootSimX, endSootSimY)
354
+ const steps = Math.max(16, Math.ceil(pixels / 8))
355
+
356
+ await page.mouse.move(startPage.x, startPage.y)
357
+ await page.mouse.down()
358
+ await page.waitForTimeout(20)
359
+
360
+ const dx = (endPage.x - startPage.x) / steps
361
+ const dy = (endPage.y - startPage.y) / steps
362
+ for (let i = 1; i <= steps; i++) {
363
+ await page.mouse.move(startPage.x + dx * i, startPage.y + dy * i)
364
+ await page.waitForTimeout(24)
365
+ }
366
+
367
+ await page.mouse.up()
368
+ await page.waitForTimeout(180)
369
+ }
370
+
371
+ // Fire synthetic two-finger pointer events directly on the sootsim canvas. We
372
+ // use PointerEvents (not page.mouse) because page.mouse is single-cursor;
373
+ // sootsim's touch tracker keys on pointerId, so we just need to dispatch
374
+ // events with two distinct ids and pointerType 'touch'. Used for pinch /
375
+ // rotation drivers — RNGH's PinchGesture/RotationGesture both require ≥2
376
+ // concurrent active touches on iOS UIPinchGestureRecognizer parity.
377
+ export async function dispatchTwoFingerGesture(
378
+ page: Page,
379
+ startA: { x: number; y: number },
380
+ startB: { x: number; y: number },
381
+ endA: { x: number; y: number },
382
+ endB: { x: number; y: number },
383
+ options: {
384
+ steps?: number
385
+ stepDelayMs?: number
386
+ workletStatsTimeoutMs?: number
387
+ } = {},
388
+ ): Promise<void> {
389
+ const steps = options.steps ?? 10
390
+ const stepDelay = options.stepDelayMs ?? 18
391
+ const workletStatsTimeoutMs = options.workletStatsTimeoutMs ?? 1500
392
+ await ensureCanvasFinder(page)
393
+ const beforeWorkletStats = await pollUntil(
394
+ () => readWorkletCallbackStats(page),
395
+ () => true,
396
+ {
397
+ timeoutMs: workletStatsTimeoutMs,
398
+ intervalMs: 0,
399
+ label: 'pre-dispatch worklet callback stats',
400
+ },
401
+ ).catch(() => null)
402
+
403
+ await page.evaluate(
404
+ ({ startA, startB }) => {
405
+ const canvas: HTMLCanvasElement | null =
406
+ window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector('canvas')
407
+ if (!canvas) throw new Error('sootsim interactive canvas not found')
408
+ const fire = (point: { x: number; y: number }, id: number, primary: boolean) => {
409
+ canvas.dispatchEvent(
410
+ new PointerEvent('pointerdown', {
411
+ bubbles: true,
412
+ cancelable: true,
413
+ clientX: point.x,
414
+ clientY: point.y,
415
+ isPrimary: primary,
416
+ pointerId: id,
417
+ pointerType: 'touch',
418
+ }),
419
+ )
420
+ }
421
+ fire(startA, 1001, true)
422
+ fire(startB, 1002, false)
423
+ },
424
+ { startA, startB },
425
+ )
426
+
427
+ for (let i = 1; i <= steps; i++) {
428
+ const t = i / steps
429
+ const ax = startA.x + (endA.x - startA.x) * t
430
+ const ay = startA.y + (endA.y - startA.y) * t
431
+ const bx = startB.x + (endB.x - startB.x) * t
432
+ const by = startB.y + (endB.y - startB.y) * t
433
+ await page.evaluate(
434
+ ({ ax, ay, bx, by }) => {
435
+ const canvas: HTMLCanvasElement | null =
436
+ window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector('canvas')
437
+ if (!canvas) return
438
+ canvas.dispatchEvent(
439
+ new PointerEvent('pointermove', {
440
+ bubbles: true,
441
+ cancelable: true,
442
+ clientX: ax,
443
+ clientY: ay,
444
+ isPrimary: true,
445
+ pointerId: 1001,
446
+ pointerType: 'touch',
447
+ }),
448
+ )
449
+ canvas.dispatchEvent(
450
+ new PointerEvent('pointermove', {
451
+ bubbles: true,
452
+ cancelable: true,
453
+ clientX: bx,
454
+ clientY: by,
455
+ isPrimary: false,
456
+ pointerId: 1002,
457
+ pointerType: 'touch',
458
+ }),
459
+ )
460
+ },
461
+ { ax, ay, bx, by },
462
+ )
463
+ await page.waitForTimeout(stepDelay)
464
+ }
465
+
466
+ await page.evaluate(
467
+ ({ endA, endB }) => {
468
+ const canvas: HTMLCanvasElement | null =
469
+ window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector('canvas')
470
+ if (!canvas) return
471
+ canvas.dispatchEvent(
472
+ new PointerEvent('pointerup', {
473
+ bubbles: true,
474
+ cancelable: true,
475
+ clientX: endA.x,
476
+ clientY: endA.y,
477
+ isPrimary: true,
478
+ pointerId: 1001,
479
+ pointerType: 'touch',
480
+ }),
481
+ )
482
+ canvas.dispatchEvent(
483
+ new PointerEvent('pointerup', {
484
+ bubbles: true,
485
+ cancelable: true,
486
+ clientX: endB.x,
487
+ clientY: endB.y,
488
+ isPrimary: false,
489
+ pointerId: 1002,
490
+ pointerType: 'touch',
491
+ }),
492
+ )
493
+ },
494
+ { endA, endB },
495
+ )
496
+ await waitForWorkletCallbackDrain(page, beforeWorkletStats, workletStatsTimeoutMs)
497
+ // runOnJS delivery increments before the callback's React setState commits.
498
+ // Detox gestures settle after native UI has observed the gesture result, so
499
+ // leave one bounded commit window after the worklet bridge drains.
500
+ await page.waitForTimeout(250)
501
+ }