sootsim 0.1.83 → 0.1.85

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 (209) hide show
  1. package/README.md +0 -1
  2. package/detox/colors.ts +54 -0
  3. package/detox/config-loader.ts +135 -0
  4. package/detox/element-types.ts +36 -0
  5. package/detox/expectations.ts +477 -0
  6. package/detox/gestures.ts +442 -0
  7. package/detox/index.ts +1403 -0
  8. package/detox/jest-environment.ts +86 -0
  9. package/detox/jest-preset.cjs +50 -0
  10. package/detox/matchers.ts +29 -0
  11. package/detox/navigation.ts +43 -0
  12. package/detox/run-test.ts +113 -0
  13. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  14. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  15. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  16. package/detox/screenshots/theme-blue-toggle.png +0 -0
  17. package/detox/screenshots/theme-blue.png +0 -0
  18. package/detox/screenshots/theme-red-toggle.png +0 -0
  19. package/detox/screenshots/theme-red.png +0 -0
  20. package/dist-cli/bin.js +3 -3
  21. package/dist-cli/chunks/{agent-MQ7GLVIB.js → agent-T3DUH5YJ.js} +2 -2
  22. package/dist-cli/chunks/{agent-wrapper-7KAFDQCN.js → agent-wrapper-NSBF4THI.js} +2 -2
  23. package/dist-cli/chunks/{assert-TV46GUNU.js → assert-X3F7TRCZ.js} +2 -2
  24. package/dist-cli/chunks/auto-bootstrap-47RN2V5G.js +2 -0
  25. package/dist-cli/chunks/beta-BRCGAF2N.js +2 -0
  26. package/dist-cli/chunks/chunk-36RPD6JI.js +2 -0
  27. package/dist-cli/chunks/{chunk-PM5NVKLP.js → chunk-3WGHC7JN.js} +2 -2
  28. package/dist-cli/chunks/chunk-4DBPNLGI.js +1 -0
  29. package/dist-cli/chunks/{chunk-J2GYISVJ.js → chunk-4EVSIUNB.js} +2 -2
  30. package/dist-cli/chunks/{chunk-JHJNODXN.js → chunk-4QZHZ6BC.js} +2 -2
  31. package/dist-cli/chunks/{chunk-F3HP444U.js → chunk-5DIGWOY7.js} +1 -1
  32. package/dist-cli/chunks/{chunk-DP7O5MHK.js → chunk-5N3V7OCG.js} +2 -2
  33. package/dist-cli/chunks/{chunk-Y4BUVURT.js → chunk-5S6D7K4L.js} +2 -2
  34. package/dist-cli/chunks/{chunk-ECJBV65H.js → chunk-7LKUN46F.js} +2 -2
  35. package/dist-cli/chunks/{chunk-WTKTOL3C.js → chunk-AC6QGW22.js} +2 -2
  36. package/dist-cli/chunks/{chunk-IBNRRAES.js → chunk-AFNDVS4E.js} +2 -2
  37. package/dist-cli/chunks/{chunk-6TNANCQC.js → chunk-BESAZ2HA.js} +2 -2
  38. package/dist-cli/chunks/{chunk-WN7M3QON.js → chunk-BHZJ6RIH.js} +2 -2
  39. package/dist-cli/chunks/{chunk-277XAALA.js → chunk-BZL6D4TV.js} +3 -3
  40. package/dist-cli/chunks/{chunk-CYV6Y6YV.js → chunk-CF2LPRXD.js} +2 -2
  41. package/dist-cli/chunks/chunk-DWTLRPEN.js +79 -0
  42. package/dist-cli/chunks/{chunk-CJY3AVI7.js → chunk-E2QE5FFP.js} +1 -1
  43. package/dist-cli/chunks/chunk-EBEL6TTJ.js +4 -0
  44. package/dist-cli/chunks/{chunk-DM6WT7QM.js → chunk-EFM53PZ5.js} +1 -1
  45. package/dist-cli/chunks/{chunk-YUELRHGB.js → chunk-EKXK3SWK.js} +2 -2
  46. package/dist-cli/chunks/{chunk-4LS5MZAI.js → chunk-G7CIZ5S3.js} +3 -3
  47. package/dist-cli/chunks/{chunk-6NN2D4EJ.js → chunk-GTAD6IUV.js} +1 -1
  48. package/dist-cli/chunks/{chunk-OYMFNU3M.js → chunk-H44IQHKZ.js} +1 -1
  49. package/dist-cli/chunks/{chunk-IP3QJLRH.js → chunk-HQDJ5BOF.js} +1 -1
  50. package/dist-cli/chunks/{chunk-5DJXZIFZ.js → chunk-KUSQ4NNJ.js} +1 -1
  51. package/dist-cli/chunks/{chunk-HAWOAQAG.js → chunk-MAO7F5PH.js} +3 -3
  52. package/dist-cli/chunks/{chunk-572VSFNP.js → chunk-NVTL3JQG.js} +1 -1
  53. package/dist-cli/chunks/{chunk-6XZOEBTZ.js → chunk-O6N2CEET.js} +2 -2
  54. package/dist-cli/chunks/{chunk-HNWEELAE.js → chunk-OISHLFON.js} +1 -1
  55. package/dist-cli/chunks/{chunk-2PY3UZVO.js → chunk-OUNLJM56.js} +2 -2
  56. package/dist-cli/chunks/chunk-OXOARRKR.js +67 -0
  57. package/dist-cli/chunks/{chunk-NXATOWWF.js → chunk-PHPXGLME.js} +1 -1
  58. package/dist-cli/chunks/{chunk-JQ7ZXOXJ.js → chunk-PQFFUJR6.js} +2 -2
  59. package/dist-cli/chunks/{chunk-KASUZ5XV.js → chunk-QLJNSOS7.js} +1 -1
  60. package/dist-cli/chunks/chunk-QQAECG5B.js +2 -0
  61. package/dist-cli/chunks/{chunk-FJYT7XL2.js → chunk-RZHREO3M.js} +2 -2
  62. package/dist-cli/chunks/{chunk-FRM355UL.js → chunk-SBGOUA6F.js} +2 -2
  63. package/dist-cli/chunks/chunk-SSCA2AEA.js +1 -0
  64. package/dist-cli/chunks/{chunk-Y2VJBRSP.js → chunk-UYRGCJ4N.js} +1 -1
  65. package/dist-cli/chunks/{chunk-2AWQ7OB2.js → chunk-WGDL5V6C.js} +1 -1
  66. package/dist-cli/chunks/{chunk-VMXWC2JO.js → chunk-Y5PLPEEU.js} +2 -2
  67. package/dist-cli/chunks/chunk-ZFAM4N5B.js +1 -0
  68. package/dist-cli/chunks/{chunk-RH4F2TF7.js → chunk-ZO3VHP6W.js} +1 -1
  69. package/dist-cli/chunks/cli-version-WPFDM2A6.js +2 -0
  70. package/dist-cli/chunks/{compat-QLLWBTS3.js → compat-PCXGGZBZ.js} +3 -3
  71. package/dist-cli/chunks/{config-2DSLDCXV.js → config-LULEVEYL.js} +2 -2
  72. package/dist-cli/chunks/control-6P6HY7UF.js +2 -0
  73. package/dist-cli/chunks/{cpu-profile-GEIKHCPC.js → cpu-profile-NOK73ZYW.js} +2 -2
  74. package/dist-cli/chunks/{daemon-4EBUFN4D.js → daemon-4A3DMUYL.js} +2 -2
  75. package/dist-cli/chunks/{debug-WGD6XWOF.js → debug-74BWB2ZG.js} +3 -3
  76. package/dist-cli/chunks/{detox-LNKGRZU6.js → detox-HEOMINSC.js} +2 -2
  77. package/dist-cli/chunks/{device-AYKXKVIQ.js → device-TTXXBJFZ.js} +2 -2
  78. package/dist-cli/chunks/{diagnose-TMXSDOOC.js → diagnose-QZ3GOHSE.js} +2 -2
  79. package/dist-cli/chunks/drivers-QRPWNOIT.js +2 -0
  80. package/dist-cli/chunks/{electron-QFPF7TBY.js → electron-QVOWV44R.js} +3 -3
  81. package/dist-cli/chunks/flow-QMA7GVN6.js +2 -0
  82. package/dist-cli/chunks/{hints-MXKRR4TG.js → hints-YKWRNMJC.js} +2 -2
  83. package/dist-cli/chunks/{home-paths-REMWQDAO.js → home-paths-SFADSTJM.js} +2 -2
  84. package/dist-cli/chunks/{inspect-XGSQNFV7.js → inspect-LEWGQCIU.js} +3 -3
  85. package/dist-cli/chunks/install-7N2N7Q32.js +2 -0
  86. package/dist-cli/chunks/{install-desktop-NQG3RZSA.js → install-desktop-22HYQZ2G.js} +3 -3
  87. package/dist-cli/chunks/{keys-5QZWXL3F.js → keys-3ZT3MICU.js} +2 -2
  88. package/dist-cli/chunks/{launch-SBXOZWKO.js → launch-ZXW2NFLG.js} +3 -3
  89. package/dist-cli/chunks/{login-EACQXE24.js → login-NJKJ7GZO.js} +4 -4
  90. package/dist-cli/chunks/{logout-IBQLMUML.js → logout-VMMQL7CB.js} +2 -2
  91. package/dist-cli/chunks/{maestro-LFYXUX7O.js → maestro-OJY4MTI7.js} +2 -2
  92. package/dist-cli/chunks/{preview-U4SBOEGQ.js → preview-QU2GXTEV.js} +2 -2
  93. package/dist-cli/chunks/{profile-GWS5ECMY.js → profile-7APWK47T.js} +2 -2
  94. package/dist-cli/chunks/{react-QDHLMVYL.js → react-RSVO5JZZ.js} +2 -2
  95. package/dist-cli/chunks/{record-BUEUWPDI.js → record-UWH4MDEO.js} +2 -2
  96. package/dist-cli/chunks/runtime-3FUENRHM.js +2 -0
  97. package/dist-cli/chunks/{runtime-delivery-G7L6RVZ7.js → runtime-delivery-QMKGRV7N.js} +2 -2
  98. package/dist-cli/chunks/{screenshot-T2HBA3VI.js → screenshot-43M27ALE.js} +2 -2
  99. package/dist-cli/chunks/{screenshot-mode-EG5HMIH3.js → screenshot-mode-EBYYN6TY.js} +2 -2
  100. package/dist-cli/chunks/{screenshots-S52AFHTV.js → screenshots-7TQZL6Z6.js} +2 -2
  101. package/dist-cli/chunks/{server-MFFVYUGG.js → server-VCFM25Z6.js} +2 -2
  102. package/dist-cli/chunks/setup-repo-HFH4VKJQ.js +2 -0
  103. package/dist-cli/chunks/{skills-HQGWBS2O.js → skills-RQA6EJQL.js} +2 -2
  104. package/dist-cli/chunks/{start-E3DRYY7W.js → start-ZT6MBYND.js} +4 -4
  105. package/dist-cli/chunks/store-BJBTDSZE.js +2 -0
  106. package/dist-cli/chunks/telemetry-ZZZKTILZ.js +2 -0
  107. package/dist-cli/chunks/{test-ZY3EF62K.js → test-RNRX5SWV.js} +3 -3
  108. package/dist-cli/chunks/{three-mode-WSPKQCJ5.js → three-mode-TQZH25ZO.js} +2 -2
  109. package/dist-cli/chunks/{timeline-3XAB5EWZ.js → timeline-GGN3AY6P.js} +2 -2
  110. package/dist-cli/chunks/{upgrade-WNENPFM5.js → upgrade-XT22D67C.js} +2 -2
  111. package/dist-cli/chunks/upload-NC2AYLC5.js +2 -0
  112. package/dist-cli/chunks/{web-D2AOZY44.js → web-KEHVF5MB.js} +2 -2
  113. package/dist-cli/chunks/{what-happened-F43KNSG6.js → what-happened-PATQRJ5T.js} +2 -2
  114. package/dist-cli/chunks/{whoami-T22VBR7C.js → whoami-CXVY26VV.js} +2 -2
  115. package/dist-lib/agent-daemon-client.cjs +1 -1
  116. package/dist-lib/agent-events.cjs +1 -1
  117. package/dist-lib/agent-sessions.cjs +1 -1
  118. package/dist-lib/attached-projects.cjs +1 -1
  119. package/dist-lib/auth/shared-session.cjs +1 -1
  120. package/dist-lib/backend-origin.cjs +1 -1
  121. package/dist-lib/beta.cjs +44 -0
  122. package/dist-lib/bridge-constants.cjs +1 -1
  123. package/dist-lib/cli-constants.cjs +1 -1
  124. package/dist-lib/config.cjs +1 -1
  125. package/dist-lib/detox/index.cjs +1770 -0
  126. package/dist-lib/detox/jest-preset.cjs +50 -0
  127. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  128. package/dist-lib/home-paths.cjs +1 -1
  129. package/dist-lib/host/bridge-host.cjs +1 -1
  130. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  131. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  132. package/dist-lib/index.cjs +136 -138
  133. package/dist-lib/metro.cjs +31 -26
  134. package/dist-lib/profiles.cjs +1 -1
  135. package/dist-lib/render-mode.cjs +1 -1
  136. package/dist-lib/scripts/demo-app-registry.cjs +809 -0
  137. package/dist-lib/scripts/dev-server-scanner.cjs +1269 -0
  138. package/dist-lib/skills.cjs +17766 -0
  139. package/dist-lib/vite.cjs +129 -39
  140. package/package.json +39 -14
  141. package/scripts/demo-app-registry.ts +989 -0
  142. package/scripts/dev-server-scanner.ts +674 -0
  143. package/src/agent-daemon-client.ts +390 -0
  144. package/src/agent-events.ts +71 -0
  145. package/src/agent-prompt.ts +71 -0
  146. package/src/agent-sessions.ts +572 -0
  147. package/src/attached-projects.ts +536 -0
  148. package/src/auth/shared-session.ts +199 -0
  149. package/src/backend-origin.ts +49 -0
  150. package/src/beta.ts +21 -0
  151. package/src/bridge-constants.ts +10 -0
  152. package/src/cli-constants.ts +1 -0
  153. package/src/cli-version.ts +30 -0
  154. package/src/codex-client.ts +215 -0
  155. package/src/config.ts +110 -0
  156. package/src/dev-bundle-resolution.ts +180 -0
  157. package/src/home-paths.ts +382 -0
  158. package/src/host/agent-host.ts +576 -0
  159. package/src/host/bridge-host.ts +2293 -0
  160. package/src/host/fetch-proxy-handler.ts +288 -0
  161. package/src/host/fetch-proxy-overrides.ts +39 -0
  162. package/src/host/open-url.ts +234 -0
  163. package/src/index.ts +9 -0
  164. package/src/metro-plugin.ts +139 -0
  165. package/src/native-dev-bundle-url.ts +62 -0
  166. package/src/native-seam-manifest.ts +313 -0
  167. package/src/profiles.ts +179 -0
  168. package/src/render-mode.ts +27 -0
  169. package/src/runtime-assets.ts +84 -0
  170. package/src/runtime-delivery.ts +334 -0
  171. package/src/screenshots/compose.ts +422 -0
  172. package/src/screenshots/frame-compose.ts +438 -0
  173. package/src/screenshots/orchestrate.ts +244 -0
  174. package/src/screenshots/registry.ts +58 -0
  175. package/src/screenshots/schema.ts +364 -0
  176. package/src/skills/builtin/a11y-review.ts +126 -0
  177. package/src/skills/builtin/compat-check.ts +104 -0
  178. package/src/skills/builtin/perf-profile.ts +84 -0
  179. package/src/skills/builtin/screenshot-all.ts +46 -0
  180. package/src/skills/builtin/test-flow.ts +118 -0
  181. package/src/skills/builtin/visual-diff.ts +94 -0
  182. package/src/skills/registry.ts +107 -0
  183. package/src/skills/types.ts +41 -0
  184. package/src/vite-plugin-one.ts +189 -0
  185. package/src/vite-plugin.ts +1381 -0
  186. package/src/worklets-babel.ts +132 -0
  187. package/dist-cli/chunks/auto-bootstrap-FQS4ZD2K.js +0 -2
  188. package/dist-cli/chunks/beta-VG7CDY2U.js +0 -2
  189. package/dist-cli/chunks/chunk-2OIBDYHW.js +0 -1
  190. package/dist-cli/chunks/chunk-6BNLVMXA.js +0 -1
  191. package/dist-cli/chunks/chunk-6XD6CBJM.js +0 -2
  192. package/dist-cli/chunks/chunk-CHQTO426.js +0 -1
  193. package/dist-cli/chunks/chunk-FAPYGVIU.js +0 -4
  194. package/dist-cli/chunks/chunk-PEHFE3LG.js +0 -64
  195. package/dist-cli/chunks/chunk-RXH2SLKF.js +0 -2
  196. package/dist-cli/chunks/chunk-UXQWC5ZR.js +0 -79
  197. package/dist-cli/chunks/chunk-XFQL74PF.js +0 -5
  198. package/dist-cli/chunks/cli-version-PWF6I6LY.js +0 -2
  199. package/dist-cli/chunks/control-UIOXGYXU.js +0 -2
  200. package/dist-cli/chunks/demo-app-registry-G3BDOFWC.js +0 -2
  201. package/dist-cli/chunks/drivers-IDQF34HP.js +0 -2
  202. package/dist-cli/chunks/flow-3JN3Y7RF.js +0 -2
  203. package/dist-cli/chunks/install-2N3YOOSN.js +0 -2
  204. package/dist-cli/chunks/runtime-PVB4VGUH.js +0 -2
  205. package/dist-cli/chunks/setup-repo-YOF7NV5D.js +0 -2
  206. package/dist-cli/chunks/store-MAI6D3UO.js +0 -2
  207. package/dist-cli/chunks/telemetry-RCQKCJTH.js +0 -2
  208. package/dist-cli/chunks/upload-YLJ4RA73.js +0 -2
  209. package/dist-lib/vite-base.cjs +0 -6937
@@ -0,0 +1,442 @@
1
+ // pull in @soot/sootsim-globals so window.__sootsimTest and
2
+ // window.__sootsimFindInteractiveCanvas are typed inside page.evaluate
3
+ // bodies. all sootsim-namespaced globals are declared canonically there —
4
+ // add new entries to packages/sootsim-globals/src/index.ts, not here.
5
+ import type {} from '@soot/sootsim-globals'
6
+ import type { Page } from 'playwright'
7
+
8
+ // shape of the surface-metrics bridge response we care about. the bridge
9
+ // types it as `Record<string, unknown> | null` (the contract is open) so we
10
+ // narrow at the call sites by casting to this local view.
11
+ type SurfaceMetricsView = {
12
+ window?: { width?: number; height?: number; scale?: number }
13
+ device?: { homeIndicatorHeight?: number }
14
+ safeAreaInsets?: { bottom?: number }
15
+ visibleRect?: { y?: number; height?: number }
16
+ }
17
+ type SurfaceMetricsResult = SurfaceMetricsView | null | undefined
18
+
19
+ type SootsimPageMetrics = {
20
+ canvas: { left: number; top: number; width: number; height: number }
21
+ window: { width: number; height: number }
22
+ interactiveWindow: { width: number; height: number }
23
+ }
24
+ type WorkletCallbackStats = {
25
+ tenantReceived: number
26
+ shellSent: number
27
+ }
28
+
29
+ type ShellGestureSeamDebug = {
30
+ attached?: Array<{
31
+ viewTag?: unknown
32
+ nodeFound?: unknown
33
+ handlers?: Array<{ name?: unknown }>
34
+ }>
35
+ }
36
+
37
+ export async function waitForSootsimGestureHandler(
38
+ page: Page,
39
+ viewTag: number,
40
+ handlerName: string,
41
+ ): Promise<void> {
42
+ await page.waitForFunction(
43
+ async ({ viewTag, handlerName }) => {
44
+ const host = (
45
+ window as typeof window & {
46
+ SootSim?: {
47
+ bridges?: {
48
+ shellHost?: {
49
+ callTestBridge?: (
50
+ method: string,
51
+ ...args: unknown[]
52
+ ) => Promise<ShellGestureSeamDebug>
53
+ } | null
54
+ }
55
+ }
56
+ }
57
+ ).SootSim?.bridges?.shellHost
58
+ if (typeof host?.callTestBridge !== 'function') return false
59
+ let debug: ShellGestureSeamDebug | null = null
60
+ try {
61
+ debug = await host.callTestBridge('getShellGestureSeamDebug')
62
+ } catch {
63
+ return false
64
+ }
65
+ return (
66
+ debug?.attached?.some(
67
+ (entry) =>
68
+ entry.viewTag === viewTag &&
69
+ entry.nodeFound !== false &&
70
+ entry.handlers?.some((handler) => handler.name === handlerName),
71
+ ) === true
72
+ )
73
+ },
74
+ { viewTag, handlerName },
75
+ { timeout: 5000 },
76
+ )
77
+ }
78
+
79
+ async function readWorkletCallbackStats(
80
+ page: Page,
81
+ ): Promise<WorkletCallbackStats | null> {
82
+ return page.evaluate(async () => {
83
+ type Stats = {
84
+ runJSCallbackSent?: unknown
85
+ runJSCallbackReceived?: unknown
86
+ } | null
87
+ const g = window as typeof window & {
88
+ __sootsimTest?: { getWorkletSlotStats?: () => Promise<Stats> | Stats }
89
+ SootSim?: {
90
+ bridges?: {
91
+ shellHost?: {
92
+ callTestBridge?: (method: string, ...args: unknown[]) => Promise<Stats>
93
+ } | null
94
+ }
95
+ }
96
+ }
97
+ const tenant =
98
+ typeof g.__sootsimTest?.getWorkletSlotStats === 'function'
99
+ ? await g.__sootsimTest.getWorkletSlotStats()
100
+ : null
101
+ const shellHost = g.SootSim?.bridges?.shellHost
102
+ let shell: Stats = null
103
+ if (typeof shellHost?.callTestBridge === 'function') {
104
+ try {
105
+ shell = await shellHost.callTestBridge('getWorkletSlotStats')
106
+ } catch {}
107
+ }
108
+ const tenantReceived =
109
+ typeof tenant?.runJSCallbackReceived === 'number' ? tenant.runJSCallbackReceived : 0
110
+ const shellSent =
111
+ typeof shell?.runJSCallbackSent === 'number' ? shell.runJSCallbackSent : 0
112
+ return { tenantReceived, shellSent }
113
+ })
114
+ }
115
+
116
+ async function waitForWorkletCallbackDrain(
117
+ page: Page,
118
+ before: WorkletCallbackStats | null,
119
+ ): Promise<void> {
120
+ if (!before) {
121
+ await page.waitForTimeout(250)
122
+ return
123
+ }
124
+ const deadline = Date.now() + 1500
125
+ let lastShellSent = before.shellSent
126
+ let stableSince = Date.now()
127
+ let sawCallback = false
128
+
129
+ while (Date.now() < deadline) {
130
+ const current = await readWorkletCallbackStats(page)
131
+ if (!current) {
132
+ await page.waitForTimeout(20)
133
+ continue
134
+ }
135
+ if (current.shellSent !== lastShellSent) {
136
+ lastShellSent = current.shellSent
137
+ stableSince = Date.now()
138
+ }
139
+ const sentDelta = current.shellSent - before.shellSent
140
+ if (sentDelta > 0) sawCallback = true
141
+ const receivedDelta = current.tenantReceived - before.tenantReceived
142
+ if (sawCallback && receivedDelta >= sentDelta && Date.now() - stableSince >= 100) {
143
+ return
144
+ }
145
+ await page.waitForTimeout(20)
146
+ }
147
+
148
+ if (!sawCallback) {
149
+ // Some gestures are purely native and do not schedule RN callbacks. A
150
+ // bounded fallback still preserves Detox's "gesture has settled" contract.
151
+ await page.waitForTimeout(250)
152
+ }
153
+ }
154
+
155
+ // helper script injected via page.addInitScript so every page.evaluate
156
+ // can reach it as `__sootsimFindInteractiveCanvas()`. picks the canvas
157
+ // that actually receives pointer input. sootsim renders multiple
158
+ // <canvas> layers per surface (background/render targets, hidden
159
+ // offscreen, overlay) and `document.querySelector('canvas')` returns
160
+ // the first one in DOM order — usually a non-interactive layer with
161
+ // `pointer-events: none`. dispatching synthetic PointerEvents onto
162
+ // that layer silently no-ops because the gesture system only listens
163
+ // on the interactive layer (pointer-events: auto, non-zero rect).
164
+ const sootsimCanvasFinderInstalled = new WeakSet<Page>()
165
+ function installInteractiveCanvasFinder() {
166
+ window.__sootsimFindInteractiveCanvas = (): HTMLCanvasElement | null => {
167
+ const interactive: Array<{ c: HTMLCanvasElement; z: number; app: boolean }> = []
168
+ const appVisible: Array<{ c: HTMLCanvasElement; z: number; app: boolean }> = []
169
+ for (const c of Array.from(document.querySelectorAll('canvas'))) {
170
+ const s = window.getComputedStyle(c)
171
+ if (s.display === 'none' || s.visibility === 'hidden') continue
172
+ const r = c.getBoundingClientRect()
173
+ if (r.width === 0 || r.height === 0) continue
174
+ const entry = {
175
+ c,
176
+ z: parseInt(s.zIndex, 10) || 0,
177
+ app: c.dataset.surfaceId?.startsWith('app:') === true,
178
+ }
179
+ if (entry.app) appVisible.push(entry)
180
+ if (s.pointerEvents !== 'none') interactive.push(entry)
181
+ }
182
+ const list = appVisible.length > 0 ? appVisible : interactive
183
+ if (list.length === 0) return null
184
+ list.sort((a, b) => Number(b.app) - Number(a.app) || b.z - a.z)
185
+ return list[0].c
186
+ }
187
+ }
188
+
189
+ async function ensureCanvasFinder(page: Page) {
190
+ if (!sootsimCanvasFinderInstalled.has(page)) {
191
+ sootsimCanvasFinderInstalled.add(page)
192
+ await page.addInitScript(installInteractiveCanvasFinder)
193
+ }
194
+ await page.evaluate(installInteractiveCanvasFinder)
195
+ }
196
+
197
+ async function readSootsimPageMetrics(page: Page): Promise<SootsimPageMetrics> {
198
+ await ensureCanvasFinder(page)
199
+ const deadline = Date.now() + 5000
200
+ while (Date.now() < deadline) {
201
+ const metrics = await page.evaluate(async () => {
202
+ const canvas =
203
+ window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector('canvas')
204
+ if (!canvas) return null
205
+ const rect = canvas.getBoundingClientRect()
206
+ if (rect.width <= 0 || rect.height <= 0) return null
207
+ const surface =
208
+ (await window.__sootsimTest?.getSurfaceMetricsSnapshot?.()) as SurfaceMetricsResult
209
+ const screenWidth = surface?.window?.width
210
+ const screenHeight = surface?.window?.height
211
+ if (
212
+ typeof screenWidth !== 'number' ||
213
+ typeof screenHeight !== 'number' ||
214
+ screenWidth <= 0 ||
215
+ screenHeight <= 0
216
+ ) {
217
+ return null
218
+ }
219
+ const homeIndicatorHeight = Number(surface?.device?.homeIndicatorHeight) || 0
220
+ const safeAreaBottom = Number(surface?.safeAreaInsets?.bottom) || 0
221
+ const bottomInset = Math.max(0, homeIndicatorHeight, safeAreaBottom)
222
+ const visibleRectBottom =
223
+ Number(surface?.visibleRect?.y) + Number(surface?.visibleRect?.height)
224
+ const interactiveHeight =
225
+ Number.isFinite(visibleRectBottom) && visibleRectBottom > 0
226
+ ? Math.min(screenHeight, visibleRectBottom)
227
+ : Math.max(0, screenHeight - bottomInset)
228
+ return {
229
+ canvas: {
230
+ left: rect.left,
231
+ top: rect.top,
232
+ width: rect.width,
233
+ height: rect.height,
234
+ },
235
+ window: {
236
+ width: screenWidth,
237
+ height: screenHeight,
238
+ },
239
+ interactiveWindow: {
240
+ width: screenWidth,
241
+ height: interactiveHeight,
242
+ },
243
+ }
244
+ })
245
+ if (metrics) return metrics
246
+ await page.waitForTimeout(50)
247
+ }
248
+ throw new Error('sootsim surface metrics are not available')
249
+ }
250
+
251
+ export async function readSootsimViewport(
252
+ page: Page,
253
+ ): Promise<{ width: number; height: number }> {
254
+ return (await readSootsimPageMetrics(page)).window
255
+ }
256
+
257
+ export async function readSootsimInteractiveViewport(
258
+ page: Page,
259
+ ): Promise<{ width: number; height: number }> {
260
+ return (await readSootsimPageMetrics(page)).interactiveWindow
261
+ }
262
+
263
+ export async function sootsimToPage(
264
+ page: Page,
265
+ sootX: number,
266
+ sootY: number,
267
+ ): Promise<{ x: number; y: number }> {
268
+ const metrics = await readSootsimPageMetrics(page)
269
+ const scaleX = metrics.canvas.width / metrics.window.width
270
+ const scaleY = metrics.canvas.height / metrics.window.height
271
+ return {
272
+ x: metrics.canvas.left + sootX * scaleX,
273
+ y: metrics.canvas.top + sootY * scaleY,
274
+ }
275
+ }
276
+
277
+ export async function dragScrollNode(
278
+ page: Page,
279
+ node: {
280
+ absolutePosition: { x: number; y: number }
281
+ layout: { width: number; height: number }
282
+ },
283
+ pixels: number,
284
+ direction: 'up' | 'down' | 'left' | 'right',
285
+ ): Promise<void> {
286
+ const startSootX = node.absolutePosition.x + node.layout.width / 2
287
+ const startSootY = node.absolutePosition.y + node.layout.height / 2
288
+ let endSootX = startSootX
289
+ let endSootY = startSootY
290
+
291
+ switch (direction) {
292
+ case 'down':
293
+ endSootY -= pixels
294
+ break
295
+ case 'up':
296
+ endSootY += pixels
297
+ break
298
+ case 'left':
299
+ endSootX += pixels
300
+ break
301
+ case 'right':
302
+ endSootX -= pixels
303
+ break
304
+ }
305
+
306
+ const startPage = await sootsimToPage(page, startSootX, startSootY)
307
+ const endPage = await sootsimToPage(page, endSootX, endSootY)
308
+ const steps = 12
309
+
310
+ await page.mouse.move(startPage.x, startPage.y)
311
+ await page.mouse.down()
312
+ await page.waitForTimeout(20)
313
+
314
+ const dx = (endPage.x - startPage.x) / steps
315
+ const dy = (endPage.y - startPage.y) / steps
316
+ for (let i = 1; i <= steps; i++) {
317
+ await page.mouse.move(startPage.x + dx * i, startPage.y + dy * i)
318
+ await page.waitForTimeout(16)
319
+ }
320
+
321
+ await page.mouse.up()
322
+ await page.waitForTimeout(180)
323
+ }
324
+
325
+ // Fire synthetic two-finger pointer events directly on the sootsim canvas. We
326
+ // use PointerEvents (not page.mouse) because page.mouse is single-cursor;
327
+ // sootsim's touch tracker keys on pointerId, so we just need to dispatch
328
+ // events with two distinct ids and pointerType 'touch'. Used for pinch /
329
+ // rotation drivers — RNGH's PinchGesture/RotationGesture both require ≥2
330
+ // concurrent active touches on iOS UIPinchGestureRecognizer parity.
331
+ export async function dispatchTwoFingerGesture(
332
+ page: Page,
333
+ startA: { x: number; y: number },
334
+ startB: { x: number; y: number },
335
+ endA: { x: number; y: number },
336
+ endB: { x: number; y: number },
337
+ options: { steps?: number; stepDelayMs?: number } = {},
338
+ ): Promise<void> {
339
+ const steps = options.steps ?? 10
340
+ const stepDelay = options.stepDelayMs ?? 18
341
+ await ensureCanvasFinder(page)
342
+ const beforeWorkletStats = await readWorkletCallbackStats(page)
343
+
344
+ await page.evaluate(
345
+ ({ startA, startB }) => {
346
+ const canvas: HTMLCanvasElement | null =
347
+ window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector('canvas')
348
+ if (!canvas) throw new Error('sootsim interactive canvas not found')
349
+ const fire = (point: { x: number; y: number }, id: number, primary: boolean) => {
350
+ canvas.dispatchEvent(
351
+ new PointerEvent('pointerdown', {
352
+ bubbles: true,
353
+ cancelable: true,
354
+ clientX: point.x,
355
+ clientY: point.y,
356
+ isPrimary: primary,
357
+ pointerId: id,
358
+ pointerType: 'touch',
359
+ }),
360
+ )
361
+ }
362
+ fire(startA, 1001, true)
363
+ fire(startB, 1002, false)
364
+ },
365
+ { startA, startB },
366
+ )
367
+
368
+ for (let i = 1; i <= steps; i++) {
369
+ const t = i / steps
370
+ const ax = startA.x + (endA.x - startA.x) * t
371
+ const ay = startA.y + (endA.y - startA.y) * t
372
+ const bx = startB.x + (endB.x - startB.x) * t
373
+ const by = startB.y + (endB.y - startB.y) * t
374
+ await page.evaluate(
375
+ ({ ax, ay, bx, by }) => {
376
+ const canvas: HTMLCanvasElement | null =
377
+ window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector('canvas')
378
+ if (!canvas) return
379
+ canvas.dispatchEvent(
380
+ new PointerEvent('pointermove', {
381
+ bubbles: true,
382
+ cancelable: true,
383
+ clientX: ax,
384
+ clientY: ay,
385
+ isPrimary: true,
386
+ pointerId: 1001,
387
+ pointerType: 'touch',
388
+ }),
389
+ )
390
+ canvas.dispatchEvent(
391
+ new PointerEvent('pointermove', {
392
+ bubbles: true,
393
+ cancelable: true,
394
+ clientX: bx,
395
+ clientY: by,
396
+ isPrimary: false,
397
+ pointerId: 1002,
398
+ pointerType: 'touch',
399
+ }),
400
+ )
401
+ },
402
+ { ax, ay, bx, by },
403
+ )
404
+ await page.waitForTimeout(stepDelay)
405
+ }
406
+
407
+ await page.evaluate(
408
+ ({ endA, endB }) => {
409
+ const canvas: HTMLCanvasElement | null =
410
+ window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector('canvas')
411
+ if (!canvas) return
412
+ canvas.dispatchEvent(
413
+ new PointerEvent('pointerup', {
414
+ bubbles: true,
415
+ cancelable: true,
416
+ clientX: endA.x,
417
+ clientY: endA.y,
418
+ isPrimary: true,
419
+ pointerId: 1001,
420
+ pointerType: 'touch',
421
+ }),
422
+ )
423
+ canvas.dispatchEvent(
424
+ new PointerEvent('pointerup', {
425
+ bubbles: true,
426
+ cancelable: true,
427
+ clientX: endB.x,
428
+ clientY: endB.y,
429
+ isPrimary: false,
430
+ pointerId: 1002,
431
+ pointerType: 'touch',
432
+ }),
433
+ )
434
+ },
435
+ { endA, endB },
436
+ )
437
+ await waitForWorkletCallbackDrain(page, beforeWorkletStats)
438
+ // runOnJS delivery increments before the callback's React setState commits.
439
+ // Detox gestures settle after native UI has observed the gesture result, so
440
+ // leave one bounded commit window after the worklet bridge drains.
441
+ await page.waitForTimeout(250)
442
+ }