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,3789 @@
1
+ /*! rnx v0.1.527 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
+ let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
+ "use strict";
4
+ var __create = Object.create;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getProtoOf = Object.getPrototypeOf;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __commonJS = (cb, mod) => function __require() {
11
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
12
+ };
13
+ var __export = (target, all) => {
14
+ for (var name in all)
15
+ __defProp(target, name, { get: all[name], enumerable: true });
16
+ };
17
+ var __copyProps = (to, from, except, desc) => {
18
+ if (from && typeof from === "object" || typeof from === "function") {
19
+ for (let key of __getOwnPropNames(from))
20
+ if (!__hasOwnProp.call(to, key) && key !== except)
21
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
22
+ }
23
+ return to;
24
+ };
25
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
26
+ // If the importer is in node compatibility mode or this is not an ESM
27
+ // file that has been converted to a CommonJS file using a Babel-
28
+ // compatible transform (i.e. "__esModule" has not been set), then set
29
+ // "default" to the CommonJS "module.exports" for node compatibility.
30
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
31
+ mod
32
+ ));
33
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
34
+
35
+ // detox/motion-change.cjs
36
+ var require_motion_change = __commonJS({
37
+ "detox/motion-change.cjs"(exports2, module2) {
38
+ "use strict";
39
+ var MOTION_CHANGE_DEFINITION2 = Object.freeze({
40
+ channelThreshold: 24,
41
+ minimumChangePercent: 0.1
42
+ });
43
+ function measureMotionChange2(input) {
44
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
45
+ throw new Error("motion change measurement requires an input object");
46
+ }
47
+ if (input.installGlobal === true) {
48
+ globalThis.__sootsimMeasureMotionChange = measureMotionChange2;
49
+ return null;
50
+ }
51
+ const definition = input.definition;
52
+ if (!definition || typeof definition !== "object" || !Number.isInteger(definition.channelThreshold) || definition.channelThreshold < 0 || definition.channelThreshold > 255 || !Number.isFinite(definition.minimumChangePercent) || definition.minimumChangePercent <= 0 || definition.minimumChangePercent >= 100) {
53
+ throw new Error(
54
+ "motion change definition needs a channel threshold from 0 to 255 and a minimum greater than 0% and less than 100%"
55
+ );
56
+ }
57
+ const label = typeof input.label === "string" && input.label.length > 0 ? input.label : "motion change";
58
+ let changePixels = input.changePixels;
59
+ let totalPixels = input.totalPixels;
60
+ if (input.baseline !== void 0 || input.candidate !== void 0) {
61
+ const baseline = input.baseline;
62
+ const candidate = input.candidate;
63
+ const width = input.width;
64
+ const height = input.height;
65
+ if (!baseline || !candidate || typeof baseline.length !== "number" || typeof candidate.length !== "number" || !Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
66
+ throw new Error(`${label} needs two RGBA pixel arrays and positive dimensions`);
67
+ }
68
+ totalPixels = width * height;
69
+ const expectedBytes = totalPixels * 4;
70
+ if (baseline.length !== expectedBytes || candidate.length !== expectedBytes) {
71
+ throw new Error(
72
+ `${label} pixel data has ${baseline.length}/${candidate.length} bytes; expected ${expectedBytes}`
73
+ );
74
+ }
75
+ changePixels = 0;
76
+ for (let offset = 0; offset < expectedBytes; offset += 4) {
77
+ if (Math.abs(baseline[offset] - candidate[offset]) > definition.channelThreshold || Math.abs(baseline[offset + 1] - candidate[offset + 1]) > definition.channelThreshold || Math.abs(baseline[offset + 2] - candidate[offset + 2]) > definition.channelThreshold || Math.abs(baseline[offset + 3] - candidate[offset + 3]) > definition.channelThreshold) {
78
+ changePixels++;
79
+ }
80
+ }
81
+ }
82
+ if (!Number.isInteger(totalPixels) || totalPixels <= 0) {
83
+ throw new Error(`${label} has no positive measured region area`);
84
+ }
85
+ if (!Number.isInteger(changePixels) || changePixels < 0 || changePixels > totalPixels) {
86
+ throw new Error(
87
+ `${label} changed pixel count ${changePixels} is outside its ${totalPixels}px measured region`
88
+ );
89
+ }
90
+ const changePercent = changePixels / totalPixels * 100;
91
+ return {
92
+ changePercent,
93
+ changePixels,
94
+ passedMinimum: changePercent >= definition.minimumChangePercent,
95
+ totalPixels
96
+ };
97
+ }
98
+ function measureProofMotionChange(input) {
99
+ return measureMotionChange2({
100
+ ...input,
101
+ definition: MOTION_CHANGE_DEFINITION2
102
+ });
103
+ }
104
+ module2.exports = {
105
+ MOTION_CHANGE_DEFINITION: MOTION_CHANGE_DEFINITION2,
106
+ measureMotionChange: measureMotionChange2,
107
+ measureProofMotionChange
108
+ };
109
+ }
110
+ });
111
+
112
+ // detox/attempt-events.cjs
113
+ var require_attempt_events = __commonJS({
114
+ "detox/attempt-events.cjs"(exports2, module2) {
115
+ "use strict";
116
+ var fs2 = require("node:fs");
117
+ var path2 = require("node:path");
118
+ function attemptEventsPath() {
119
+ const enabled = process.env.CONFORMANCE_PROOF === "1" || Boolean(process.env.CONFORMANCE_PROOF_DIR);
120
+ if (!enabled) return null;
121
+ const root = path2.resolve(
122
+ process.cwd(),
123
+ process.env.CONFORMANCE_PROOF_DIR || "artifacts/conformance-proof/latest"
124
+ );
125
+ return path2.join(root, "events.jsonl");
126
+ }
127
+ function writeAttemptEvent2(event) {
128
+ const output = attemptEventsPath();
129
+ if (output === null) return false;
130
+ fs2.mkdirSync(path2.dirname(output), { recursive: true });
131
+ fs2.appendFileSync(output, `${JSON.stringify(event)}
132
+ `);
133
+ return true;
134
+ }
135
+ module2.exports = { attemptEventsPath, writeAttemptEvent: writeAttemptEvent2 };
136
+ }
137
+ });
138
+
139
+ // detox/proof-frame.cjs
140
+ var require_proof_frame = __commonJS({
141
+ "detox/proof-frame.cjs"(exports2, module2) {
142
+ "use strict";
143
+ var PROOF_CONTENT_DEFINITION = Object.freeze({
144
+ channelThreshold: 24,
145
+ contentBandInsetFraction: 0.08,
146
+ minimumContentPercent: 5e-3
147
+ });
148
+ var REQUIRED_IDENTICAL_PROOF_FRAMES2 = 3;
149
+ var REQUIRED_IDENTICAL_FLOW_FRAMES = 1;
150
+ function framesIdentical2(a, b) {
151
+ if (!a || !b || a.length !== b.length) return false;
152
+ return a.equals(b);
153
+ }
154
+ function measureProofFrameContent(input) {
155
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
156
+ throw new Error("proof content measurement requires an input object");
157
+ }
158
+ const { data, width, height } = input;
159
+ if (!data || typeof data.length !== "number" || !Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0 || data.length !== width * height * 4) {
160
+ throw new Error("proof content measurement needs RGBA pixels and dimensions");
161
+ }
162
+ const yStart = Math.floor(height * PROOF_CONTENT_DEFINITION.contentBandInsetFraction);
163
+ const yEnd = Math.ceil(height * (1 - PROOF_CONTENT_DEFINITION.contentBandInsetFraction));
164
+ const bucketCounts = new Uint32Array(32 * 32 * 32);
165
+ let totalPixels = 0;
166
+ for (let y = yStart; y < yEnd; y++) {
167
+ for (let x = 0; x < width; x++) {
168
+ const offset = (y * width + x) * 4;
169
+ const bucket = data[offset] >> 3 << 10 | data[offset + 1] >> 3 << 5 | data[offset + 2] >> 3;
170
+ bucketCounts[bucket]++;
171
+ totalPixels++;
172
+ }
173
+ }
174
+ let dominantBucket = 0;
175
+ for (let index = 1; index < bucketCounts.length; index++) {
176
+ if (bucketCounts[index] > bucketCounts[dominantBucket]) dominantBucket = index;
177
+ }
178
+ const dominant = {
179
+ red: ((dominantBucket >> 10 & 31) << 3) + 4,
180
+ green: ((dominantBucket >> 5 & 31) << 3) + 4,
181
+ blue: ((dominantBucket & 31) << 3) + 4
182
+ };
183
+ let contentPixels = 0;
184
+ for (let y = yStart; y < yEnd; y++) {
185
+ for (let x = 0; x < width; x++) {
186
+ const offset = (y * width + x) * 4;
187
+ if (Math.max(
188
+ Math.abs(data[offset] - dominant.red),
189
+ Math.abs(data[offset + 1] - dominant.green),
190
+ Math.abs(data[offset + 2] - dominant.blue)
191
+ ) >= PROOF_CONTENT_DEFINITION.channelThreshold) {
192
+ contentPixels++;
193
+ }
194
+ }
195
+ }
196
+ const contentPercent = contentPixels / totalPixels * 100;
197
+ return {
198
+ contentPercent,
199
+ contentPixels,
200
+ dominant,
201
+ passedMinimum: contentPercent >= PROOF_CONTENT_DEFINITION.minimumContentPercent,
202
+ totalPixels
203
+ };
204
+ }
205
+ function settleDecision(previousPixels, currentFrame) {
206
+ if (!currentFrame) return { decision: "continue", content: null };
207
+ const content = measureProofFrameContent(currentFrame);
208
+ if (!content.passedMinimum) {
209
+ return { decision: "insufficient-content", content };
210
+ }
211
+ if (previousPixels && framesIdentical2(previousPixels, currentFrame.data)) {
212
+ return { decision: "identical", content };
213
+ }
214
+ return { decision: "changed", content };
215
+ }
216
+ module2.exports = {
217
+ PROOF_CONTENT_DEFINITION,
218
+ REQUIRED_IDENTICAL_PROOF_FRAMES: REQUIRED_IDENTICAL_PROOF_FRAMES2,
219
+ REQUIRED_IDENTICAL_FLOW_FRAMES,
220
+ framesIdentical: framesIdentical2,
221
+ measureProofFrameContent,
222
+ settleDecision
223
+ };
224
+ }
225
+ });
226
+
227
+ // detox/index.ts
228
+ var detox_exports = {};
229
+ __export(detox_exports, {
230
+ by: () => by,
231
+ cleanup: () => cleanup,
232
+ device: () => device,
233
+ element: () => element,
234
+ expect: () => sootsimExpect,
235
+ waitFor: () => sootsimWaitFor,
236
+ waitForSootsimIdle: () => waitForSootsimIdle
237
+ });
238
+ module.exports = __toCommonJS(detox_exports);
239
+ var fs = __toESM(require("fs"), 1);
240
+ var path = __toESM(require("path"), 1);
241
+ var import_playwright = require("playwright");
242
+
243
+ // ../../scripts/lib/reap-browser.ts
244
+ var import_node_child_process = require("node:child_process");
245
+ var discoveredChromePids = /* @__PURE__ */ new WeakMap();
246
+ var discoveredChromeProcesses = /* @__PURE__ */ new WeakMap();
247
+ var reapPids = /* @__PURE__ */ new Map();
248
+ var installed = false;
249
+ function currentRuntime() {
250
+ return { platform: process.platform, env: process.env };
251
+ }
252
+ function processCommand(pid) {
253
+ return (0, import_node_child_process.execFileSync)("/bin/ps", ["-p", String(pid), "-o", "command="], {
254
+ encoding: "utf8"
255
+ }).trim();
256
+ }
257
+ function processIdentity(pid) {
258
+ try {
259
+ const identity = (0, import_node_child_process.execFileSync)(
260
+ "/bin/ps",
261
+ ["-p", String(pid), "-o", "lstart=", "-o", "command="],
262
+ { encoding: "utf8" }
263
+ ).trim();
264
+ return identity || null;
265
+ } catch {
266
+ return null;
267
+ }
268
+ }
269
+ function softwareChromeAllowed(runtime = currentRuntime()) {
270
+ if (runtime.platform !== "linux") return false;
271
+ return runtime.env.GITHUB_ACTIONS === "true" && runtime.env.RUNNER_OS === "Linux" || runtime.env.CONTRAST_CF_FACTORY_CONTAINER === "1";
272
+ }
273
+ function isSoftwareRendererArgument(value) {
274
+ const arg = String(value);
275
+ return /^--disable-gpu(?:=.*)?$/i.test(arg) || /^--enable-unsafe-swiftshader(?:=.*)?$/i.test(arg) || /^--use-angle=(?:swiftshader|swiftshader-webgl)$/i.test(arg) || /^--use-gl=swiftshader$/i.test(arg);
276
+ }
277
+ function softwareRendererArgumentInText(command) {
278
+ const unsafe = command.match(
279
+ /(?:^|\s)(--disable-gpu(?:=[^\s]+)?|--enable-unsafe-swiftshader(?:=[^\s]+)?|--use-angle=(?:swiftshader|swiftshader-webgl)|--use-gl=swiftshader)(?=\s|$)/i
280
+ );
281
+ return unsafe?.[1] ?? null;
282
+ }
283
+ function expectedAngle(platform) {
284
+ if (platform === "darwin") return "metal";
285
+ if (platform === "linux") return "vulkan";
286
+ return "default";
287
+ }
288
+ function assertGpuBrowserCommand(command, runtime = currentRuntime()) {
289
+ if (softwareChromeAllowed(runtime)) return;
290
+ const unsafe = softwareRendererArgumentInText(command);
291
+ if (unsafe) {
292
+ throw new Error(
293
+ `[reap-browser] refused software-rendering Chrome process argument ${unsafe}`
294
+ );
295
+ }
296
+ const angle = expectedAngle(runtime.platform);
297
+ const args = command.split(/\s+/);
298
+ if (!args.includes(`--use-angle=${angle}`) || !args.includes("--enable-gpu")) {
299
+ throw new Error(
300
+ `[reap-browser] Chrome process did not retain the required ${angle} GPU arguments`
301
+ );
302
+ }
303
+ for (const name of ["--enable-features=", "--disable-features="]) {
304
+ const lists = args.filter((arg) => arg.startsWith(name)).map((arg) => arg.slice(name.length).split(",").filter(Boolean));
305
+ const kept = new Set(lists.at(-1) ?? []);
306
+ const dropped = lists.slice(0, -1).flat().filter((feature) => !kept.has(feature));
307
+ if (dropped.length > 0) {
308
+ throw new Error(
309
+ `[reap-browser] Chrome keeps only its last ${name.slice(0, -1)} switch, which dropped ${dropped.join(",")}; add them to the merged list in scripts/lib/reap-browser.ts`
310
+ );
311
+ }
312
+ }
313
+ if (runtime.platform === "darwin" && command.includes("/Applications/Google Chrome.app/")) {
314
+ throw new Error(
315
+ "[reap-browser] refused the user's installed Google Chrome; use Playwright channel chromium"
316
+ );
317
+ }
318
+ }
319
+ function collectGpuRendererStrings(info) {
320
+ if (!info || typeof info !== "object") return [];
321
+ const record = info;
322
+ const gpu = record.gpu;
323
+ if (!gpu || typeof gpu !== "object") return [];
324
+ const gpuRecord = gpu;
325
+ const values = [];
326
+ const aux = gpuRecord.auxAttributes;
327
+ if (aux && typeof aux === "object") {
328
+ for (const value of Object.values(aux)) {
329
+ if (typeof value === "string") values.push(value);
330
+ }
331
+ }
332
+ const devices = gpuRecord.devices;
333
+ if (Array.isArray(devices)) {
334
+ for (const device2 of devices) {
335
+ if (!device2 || typeof device2 !== "object") continue;
336
+ for (const value of Object.values(device2)) {
337
+ if (typeof value === "string") values.push(value);
338
+ }
339
+ }
340
+ }
341
+ return values;
342
+ }
343
+ function browserProcessPid(info) {
344
+ if (!info || typeof info !== "object") return null;
345
+ const record = info;
346
+ if (!Array.isArray(record.processInfo)) return null;
347
+ for (const entry of record.processInfo) {
348
+ if (!entry || typeof entry !== "object") continue;
349
+ const processInfo = entry;
350
+ if (processInfo.type === "browser" && typeof processInfo.id === "number" && Number.isInteger(processInfo.id) && processInfo.id > 0) {
351
+ return processInfo.id;
352
+ }
353
+ }
354
+ return null;
355
+ }
356
+ function assertGpuRendererInfo(info, runtime = currentRuntime()) {
357
+ if (softwareChromeAllowed(runtime)) return;
358
+ const renderers = collectGpuRendererStrings(info);
359
+ const summary = renderers.join(" | ");
360
+ if (!summary || /swiftshader|llvmpipe|software rasterizer/i.test(summary)) {
361
+ throw new Error(
362
+ `[reap-browser] Chrome reported a software renderer${summary ? `: ${summary}` : ""}`
363
+ );
364
+ }
365
+ if (runtime.platform === "darwin" && !/metal/i.test(summary)) {
366
+ throw new Error(
367
+ `[reap-browser] Chrome did not report an ANGLE Metal renderer: ${summary}`
368
+ );
369
+ }
370
+ }
371
+ function rootChromePid(b) {
372
+ if (b && typeof b === "object") {
373
+ const discovered = discoveredChromePids.get(b);
374
+ if (discovered !== void 0) return discovered;
375
+ }
376
+ const w = b;
377
+ if (w && typeof w.process === "function") {
378
+ try {
379
+ const p = w.process();
380
+ if (p && typeof p.pid === "number" && p.pid > 0) return p.pid;
381
+ } catch {
382
+ }
383
+ }
384
+ if (w && typeof w.browser === "function") {
385
+ try {
386
+ const child = w.browser();
387
+ if (child) return rootChromePid(child);
388
+ } catch {
389
+ }
390
+ }
391
+ return null;
392
+ }
393
+ function killTree(pid) {
394
+ let kids = [];
395
+ try {
396
+ kids = (0, import_node_child_process.execSync)(`pgrep -P ${pid} || true`).toString().split("\n").filter(Boolean);
397
+ } catch {
398
+ }
399
+ for (const k of kids) killTree(Number(k));
400
+ try {
401
+ process.kill(pid, "SIGKILL");
402
+ } catch {
403
+ }
404
+ }
405
+ function killOwnedTree(tracked) {
406
+ if (tracked.readIdentity(tracked.pid) !== tracked.identity) return;
407
+ killTree(tracked.pid);
408
+ }
409
+ function trackReapPid(pid, readIdentity = processIdentity) {
410
+ const identity = readIdentity(pid);
411
+ if (identity === null) return null;
412
+ const current = reapPids.get(pid);
413
+ if (current?.identity === identity && current.readIdentity === readIdentity) {
414
+ return current;
415
+ }
416
+ const tracked = { pid, identity, readIdentity };
417
+ reapPids.set(pid, tracked);
418
+ installReaper();
419
+ return tracked;
420
+ }
421
+ function untrackReapPid(tracked) {
422
+ if (reapPids.get(tracked.pid) === tracked) reapPids.delete(tracked.pid);
423
+ }
424
+ function browserControl(b) {
425
+ const candidate = b;
426
+ if (!candidate || typeof candidate !== "object") return null;
427
+ if (typeof candidate.newBrowserCDPSession === "function" || typeof candidate.process === "function") {
428
+ return candidate;
429
+ }
430
+ if (typeof candidate.browser !== "function") return null;
431
+ try {
432
+ const browser = candidate.browser();
433
+ return browser ? browserControl(browser) : null;
434
+ } catch {
435
+ return null;
436
+ }
437
+ }
438
+ function registerChromePid(browser, control, pid, readIdentity = processIdentity) {
439
+ if (browser && typeof browser === "object") discoveredChromePids.set(browser, pid);
440
+ discoveredChromePids.set(control, pid);
441
+ const tracked = trackReapPid(pid, readIdentity);
442
+ if (tracked === null) return;
443
+ if (browser && typeof browser === "object") {
444
+ discoveredChromeProcesses.set(browser, tracked);
445
+ }
446
+ discoveredChromeProcesses.set(control, tracked);
447
+ }
448
+ async function discoverChromePid(browser) {
449
+ const known = rootChromePid(browser);
450
+ if (known !== null) return known;
451
+ const control = browserControl(browser);
452
+ if (!control || typeof control.newBrowserCDPSession !== "function") return null;
453
+ const session = await control.newBrowserCDPSession();
454
+ try {
455
+ const pid = browserProcessPid(await session.send("SystemInfo.getProcessInfo"));
456
+ if (pid === null) return null;
457
+ registerChromePid(browser, control, pid);
458
+ return pid;
459
+ } finally {
460
+ await session.detach?.();
461
+ }
462
+ }
463
+ async function rejectLaunchedBrowser(browser) {
464
+ const tracked = browser && typeof browser === "object" ? discoveredChromeProcesses.get(browser) : void 0;
465
+ if (tracked) {
466
+ killOwnedTree(tracked);
467
+ untrackReapPid(tracked);
468
+ return;
469
+ }
470
+ const candidate = browser;
471
+ if (candidate && typeof candidate.close === "function") {
472
+ try {
473
+ await candidate.close();
474
+ } catch {
475
+ }
476
+ }
477
+ }
478
+ async function validateLaunchedGpuBrowser(browser, runtime, rendererTarget, commandForPid = processCommand, readIdentity = processIdentity) {
479
+ if (softwareChromeAllowed(runtime)) return;
480
+ const root = rootChromePid(browser);
481
+ const launchedControl = browserControl(browser);
482
+ if (root !== null && launchedControl) {
483
+ registerChromePid(browser, launchedControl, root, readIdentity);
484
+ }
485
+ if (root !== null && (runtime.platform === "darwin" || runtime.platform === "linux")) {
486
+ assertGpuBrowserCommand(commandForPid(root), runtime);
487
+ }
488
+ const rendererBrowser = rendererTarget ? await rendererTarget(browser) : browser;
489
+ const control = browserControl(rendererBrowser);
490
+ if (control && typeof control.newBrowserCDPSession === "function") {
491
+ const session = await control.newBrowserCDPSession();
492
+ try {
493
+ const pid = browserProcessPid(await session.send("SystemInfo.getProcessInfo"));
494
+ if (pid === null) {
495
+ throw new Error("[reap-browser] Chrome did not report its browser process id");
496
+ }
497
+ registerChromePid(browser, control, pid, readIdentity);
498
+ if (rendererBrowser && typeof rendererBrowser === "object") {
499
+ discoveredChromePids.set(rendererBrowser, pid);
500
+ }
501
+ if (runtime.platform === "darwin" || runtime.platform === "linux") {
502
+ assertGpuBrowserCommand(commandForPid(pid), runtime);
503
+ }
504
+ assertGpuRendererInfo(await session.send("SystemInfo.getInfo"), runtime);
505
+ return;
506
+ } finally {
507
+ await session.detach?.();
508
+ }
509
+ }
510
+ throw new Error("[reap-browser] could not validate the launched Chrome GPU renderer");
511
+ }
512
+ var signalShutdownClaimed = false;
513
+ function installReaper() {
514
+ if (installed) return;
515
+ installed = true;
516
+ const reap = () => {
517
+ for (const tracked of reapPids.values()) killOwnedTree(tracked);
518
+ };
519
+ if (!signalShutdownClaimed) {
520
+ process.on("SIGTERM", () => {
521
+ reap();
522
+ process.exit(0);
523
+ });
524
+ process.on("SIGINT", () => {
525
+ reap();
526
+ process.exit(0);
527
+ });
528
+ }
529
+ process.on("exit", reap);
530
+ }
531
+ async function launchReapedScoped(launch, readIdentity = processIdentity) {
532
+ const browser = await launch();
533
+ let root = rootChromePid(browser);
534
+ if (root === null) {
535
+ try {
536
+ root = await discoverChromePid(browser);
537
+ } catch {
538
+ root = null;
539
+ }
540
+ }
541
+ const mine = /* @__PURE__ */ new Set();
542
+ if (root != null) {
543
+ const tracked = trackReapPid(root, readIdentity);
544
+ if (tracked !== null) mine.add(tracked);
545
+ }
546
+ const evented = browserControl(browser) ?? browser;
547
+ if (typeof evented.on === "function") {
548
+ const disconnectEvent = typeof evented.newBrowserCDPSession === "function" ? "disconnected" : "close";
549
+ evented.on(disconnectEvent, () => {
550
+ for (const tracked of mine) {
551
+ killOwnedTree(tracked);
552
+ untrackReapPid(tracked);
553
+ }
554
+ mine.clear();
555
+ });
556
+ }
557
+ const reapNow = () => {
558
+ for (const tracked of mine) {
559
+ killOwnedTree(tracked);
560
+ untrackReapPid(tracked);
561
+ }
562
+ mine.clear();
563
+ };
564
+ return { browser, reapNow };
565
+ }
566
+ function gpuChromeArgs(platform = process.platform) {
567
+ const angle = expectedAngle(platform);
568
+ return [
569
+ // sootsim's compositor renders through skia graphite on webgpu, so a launch
570
+ // without navigator.gpu cannot present a frame at all. chrome enables webgpu
571
+ // by default on gpu-backed macOS but not on every headless configuration,
572
+ // and every caller that renders a sim was passing this by hand. own it here
573
+ // so a launcher cannot silently come up without the only backend there is.
574
+ "--enable-unsafe-webgpu",
575
+ `--use-angle=${angle}`,
576
+ "--enable-gpu",
577
+ "--enable-gpu-rasterization",
578
+ "--enable-zero-copy",
579
+ "--ignore-gpu-blocklist"
580
+ ];
581
+ }
582
+ var PLAYWRIGHT_DISABLED_FEATURES = [
583
+ "AvoidUnnecessaryBeforeUnloadCheckSync",
584
+ "BlockOriginHeaderModificationOnRedirect",
585
+ "BoundaryEventDispatchTracksNodeRemoval",
586
+ "DestroyProfileOnBrowserClose",
587
+ "DialMediaRouteProvider",
588
+ "GlobalMediaControls",
589
+ "HttpsUpgrades",
590
+ "LensOverlay",
591
+ "MediaRouter",
592
+ "PaintHolding",
593
+ "ThirdPartyStoragePartitioning",
594
+ "Translate",
595
+ "AutoDeElevate",
596
+ "RenderDocument",
597
+ "OptimizationHints",
598
+ "msForceBrowserSignIn",
599
+ "msEdgeUpdateLaunchServicesPreferredVersion"
600
+ ];
601
+ var PLAYWRIGHT_ENABLED_FEATURES = ["CDPScreenshotNewSurface"];
602
+ var DISABLED_FEATURES = ["NetworkTimeServiceQuerying"];
603
+ function mergeFeatureSwitches(args, enabled = []) {
604
+ const enable = /* @__PURE__ */ new Set([...PLAYWRIGHT_ENABLED_FEATURES, ...enabled]);
605
+ const disable = /* @__PURE__ */ new Set([...PLAYWRIGHT_DISABLED_FEATURES, ...DISABLED_FEATURES]);
606
+ const rest = [];
607
+ for (const arg of args) {
608
+ const match = /^--(enable|disable)-features=(.*)$/.exec(arg);
609
+ if (!match) {
610
+ rest.push(arg);
611
+ continue;
612
+ }
613
+ for (const feature of match[2].split(",")) {
614
+ if (feature) (match[1] === "enable" ? enable : disable).add(feature);
615
+ }
616
+ }
617
+ return [
618
+ ...rest,
619
+ `--enable-features=${[...enable].join(",")}`,
620
+ `--disable-features=${[...disable].join(",")}`
621
+ ];
622
+ }
623
+ function gpuChromeLaunchOptions(opts = {}, runtime = currentRuntime()) {
624
+ const callerArgs = Array.isArray(opts.args) ? opts.args : [];
625
+ const unsafe = callerArgs.find(isSoftwareRendererArgument);
626
+ if (unsafe !== void 0) {
627
+ throw new Error(
628
+ `[reap-browser] refused software-rendering Chrome launch argument ${String(unsafe)}`
629
+ );
630
+ }
631
+ if (opts.executablePath !== void 0) {
632
+ throw new Error(
633
+ "[reap-browser] executablePath bypasses Playwright Chrome for Testing and is not allowed"
634
+ );
635
+ }
636
+ if (opts.ignoreDefaultArgs === true) {
637
+ throw new Error(
638
+ "[reap-browser] ignoreDefaultArgs=true bypasses required Chrome launch defaults"
639
+ );
640
+ }
641
+ const managedGpuArgs = /* @__PURE__ */ new Set([
642
+ "--enable-unsafe-webgpu",
643
+ "--enable-gpu",
644
+ "--enable-gpu-rasterization",
645
+ "--enable-zero-copy",
646
+ "--ignore-gpu-blocklist"
647
+ ]);
648
+ const unmanagedCallerArgs = callerArgs.filter((arg) => {
649
+ const value = String(arg);
650
+ return !managedGpuArgs.has(value) && !value.startsWith("--use-angle=");
651
+ });
652
+ const callerIgnored = Array.isArray(opts.ignoreDefaultArgs) ? opts.ignoreDefaultArgs : [];
653
+ const ignoredDefaults = [
654
+ .../* @__PURE__ */ new Set([...callerIgnored, "--disable-gpu", "--enable-unsafe-swiftshader"])
655
+ ];
656
+ return {
657
+ ...opts,
658
+ // playwright's `chromium` channel is the full Google Chrome for Testing
659
+ // app, not chromium_headless_shell. on macOS it has bundle id
660
+ // com.google.chrome.for.testing, so automation cannot become the target of
661
+ // Dock open/quit events intended for the user's com.google.Chrome browser.
662
+ channel: "chromium",
663
+ args: mergeFeatureSwitches([
664
+ ...unmanagedCallerArgs,
665
+ ...gpuChromeArgs(runtime.platform)
666
+ ]),
667
+ ignoreDefaultArgs: ignoredDefaults
668
+ };
669
+ }
670
+ function platformChromeLaunch(launch, opts, rendererTarget, policy = {}) {
671
+ return async () => {
672
+ const runtime = policy.runtime ?? currentRuntime();
673
+ if (softwareChromeAllowed(runtime)) {
674
+ return launch(chromeLaunchOptions(opts, runtime));
675
+ }
676
+ const gpuOptions = chromeLaunchOptions(opts, runtime);
677
+ let browser;
678
+ try {
679
+ browser = await launch(gpuOptions);
680
+ } catch (err) {
681
+ const msg = err instanceof Error ? err.message : String(err);
682
+ const missing = /not found at|not installed|Chromium distribution|Executable doesn't exist|No such file/i.test(
683
+ msg
684
+ );
685
+ if (!missing) throw err;
686
+ throw new Error(
687
+ "[reap-browser] Playwright Chrome for Testing is required for local GPU browser work. Run `bun scripts/ops/install-playwright-browsers.ts chromium`.",
688
+ { cause: err }
689
+ );
690
+ }
691
+ try {
692
+ await validateLaunchedGpuBrowser(
693
+ browser,
694
+ runtime,
695
+ rendererTarget,
696
+ policy.processCommand,
697
+ policy.processIdentity
698
+ );
699
+ } catch (err) {
700
+ await rejectLaunchedBrowser(browser);
701
+ throw err;
702
+ }
703
+ return browser;
704
+ };
705
+ }
706
+ function softwareChromeLaunchOptions(opts) {
707
+ if (opts.ignoreDefaultArgs === true) {
708
+ throw new Error(
709
+ "[reap-browser] ignoreDefaultArgs=true bypasses required Chrome launch defaults"
710
+ );
711
+ }
712
+ const callerArgs = Array.isArray(opts.args) ? opts.args.map(String) : [];
713
+ const unmanagedCallerArgs = callerArgs.filter((arg) => {
714
+ return !arg.startsWith("--use-angle=") && arg !== "--enable-gpu" && arg !== "--enable-gpu-rasterization" && arg !== "--enable-zero-copy" && arg !== "--ignore-gpu-blocklist" && arg !== "--disable-gpu-watchdog" && arg !== "--enable-skia-graphite" && !/^--enable-unsafe-webgpu(?:=.*)?$/i.test(arg) && !/^--enable-unsafe-swiftshader(?:=.*)?$/i.test(arg);
715
+ });
716
+ const software = {
717
+ ...opts,
718
+ // playwright's bundled software chromium exposes no navigator.gpu without
719
+ // --enable-unsafe-webgpu plus the Vulkan feature, and the compositor's only
720
+ // backend is webgpu. emitted here rather than per caller so a hosted-linux
721
+ // launch and a local gpu launch agree on what a sim needs.
722
+ args: mergeFeatureSwitches(
723
+ [
724
+ ...unmanagedCallerArgs,
725
+ "--use-angle=swiftshader",
726
+ "--enable-unsafe-swiftshader",
727
+ "--enable-unsafe-webgpu",
728
+ // without Skia Graphite as chromium's own compositor, SwiftShader has no
729
+ // shared-image backing for a WebGPU canvas swap chain at any size: the
730
+ // first configured canvas fails "Could not find SharedImageBackingFactory"
731
+ // and chromium destroys the device mid-capture. compute-only WebGPU
732
+ // still works, so an adapter probe cannot see this.
733
+ "--enable-skia-graphite",
734
+ // the GPU watchdog kills the GPU process when a task outlives its
735
+ // timeout, and SwiftShader precompiling Graphite pipelines on two CPU
736
+ // cores can legitimately outlive it. the cost: a genuinely wedged GPU
737
+ // process now holds its job until the job's own timeout instead of
738
+ // dying fast.
739
+ "--disable-gpu-watchdog"
740
+ ],
741
+ ["Vulkan"]
742
+ )
743
+ };
744
+ delete software.channel;
745
+ delete software.executablePath;
746
+ return software;
747
+ }
748
+ function chromeLaunchOptions(opts = {}, runtime = currentRuntime()) {
749
+ if (signalShutdownClaimed) {
750
+ opts = { ...opts, handleSIGINT: false, handleSIGTERM: false, handleSIGHUP: false };
751
+ }
752
+ return softwareChromeAllowed(runtime) ? softwareChromeLaunchOptions(opts) : gpuChromeLaunchOptions(opts, runtime);
753
+ }
754
+ async function launchReapedChromeScoped(launch, opts = {}, rendererTarget, policy = {}) {
755
+ return launchReapedScoped(
756
+ platformChromeLaunch(launch, opts, rendererTarget, policy),
757
+ policy.processIdentity
758
+ );
759
+ }
760
+
761
+ // ../sootsim-engine/src/capture/browser-composite.ts
762
+ function resolveBrowserCompositeCdpClip(request, metrics) {
763
+ if (!metrics || typeof metrics !== "object") {
764
+ throw new Error("browser composite capture requires CDP layout metrics");
765
+ }
766
+ const physical = Reflect.get(metrics, "layoutViewport");
767
+ const css = Reflect.get(metrics, "cssLayoutViewport");
768
+ if (!physical || typeof physical !== "object" || !css || typeof css !== "object") {
769
+ throw new Error("browser composite capture requires physical and CSS viewports");
770
+ }
771
+ const physicalWidth = Reflect.get(physical, "clientWidth");
772
+ const cssWidth = Reflect.get(css, "clientWidth");
773
+ if (typeof physicalWidth !== "number" || !Number.isFinite(physicalWidth) || physicalWidth <= 0 || typeof cssWidth !== "number" || !Number.isFinite(cssWidth) || cssWidth <= 0) {
774
+ throw new Error("browser composite capture requires positive viewport widths");
775
+ }
776
+ const devicePixelRatio = physicalWidth / cssWidth;
777
+ const padding = 1;
778
+ return {
779
+ x: request.rect.x - padding,
780
+ y: request.rect.y - padding,
781
+ width: request.rect.width + padding * 2,
782
+ height: request.rect.height + padding * 2,
783
+ scale: request.outputWidth / request.rect.width / devicePixelRatio
784
+ };
785
+ }
786
+ function readBrowserCompositeCaptureRequest(value) {
787
+ if (!value || typeof value !== "object") {
788
+ throw new Error("browser composite capture request must be an object");
789
+ }
790
+ const rect = Reflect.get(value, "rect");
791
+ if (!rect || typeof rect !== "object") {
792
+ throw new Error("browser composite capture request requires a rect");
793
+ }
794
+ const request = {
795
+ rect: {
796
+ x: Reflect.get(rect, "x"),
797
+ y: Reflect.get(rect, "y"),
798
+ width: Reflect.get(rect, "width"),
799
+ height: Reflect.get(rect, "height")
800
+ },
801
+ outputWidth: Reflect.get(value, "outputWidth"),
802
+ outputHeight: Reflect.get(value, "outputHeight")
803
+ };
804
+ const positions = [request.rect.x, request.rect.y];
805
+ const dimensions = [
806
+ request.rect.width,
807
+ request.rect.height,
808
+ request.outputWidth,
809
+ request.outputHeight
810
+ ];
811
+ if (positions.some((number) => typeof number !== "number" || !Number.isFinite(number))) {
812
+ throw new Error("browser composite capture request positions must be finite");
813
+ }
814
+ if (dimensions.some(
815
+ (number) => typeof number !== "number" || !Number.isFinite(number) || number <= 0
816
+ ) || !Number.isInteger(request.outputWidth) || !Number.isInteger(request.outputHeight)) {
817
+ throw new Error("browser composite capture request dimensions must be positive");
818
+ }
819
+ const scaleX = request.outputWidth / request.rect.width;
820
+ const scaleY = request.outputHeight / request.rect.height;
821
+ if (Math.abs(scaleX - scaleY) > 1e-3) {
822
+ throw new Error("browser composite capture output scale must be uniform");
823
+ }
824
+ return request;
825
+ }
826
+
827
+ // src/poll-until.ts
828
+ async function pollUntil(read, ready, options) {
829
+ if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) {
830
+ throw new Error("poll timeout must be a positive finite number");
831
+ }
832
+ const intervalMs = options.intervalMs ?? 100;
833
+ if (!Number.isFinite(intervalMs) || intervalMs < 0) {
834
+ throw new Error("poll interval must be a non-negative finite number");
835
+ }
836
+ const deadline = Date.now() + options.timeoutMs;
837
+ let lastError;
838
+ let lastRead = null;
839
+ const describeLastRead = () => {
840
+ if (lastRead === null) {
841
+ if (lastError === void 0) return "no read settled";
842
+ const message = lastError instanceof Error ? lastError.message : String(lastError);
843
+ return `last read failed: ${message}`;
844
+ }
845
+ let serialized;
846
+ try {
847
+ const seen = /* @__PURE__ */ new WeakSet();
848
+ serialized = JSON.stringify(lastRead.value, (_key, entry) => {
849
+ if (typeof entry !== "object" || entry === null) return entry;
850
+ if (seen.has(entry)) return "[circular]";
851
+ seen.add(entry);
852
+ return entry;
853
+ }) ?? String(lastRead.value);
854
+ } catch {
855
+ return "last value (unserializable)";
856
+ }
857
+ if (serialized.length <= 300) return `last value ${serialized}`;
858
+ return `last value ${serialized.slice(0, 300)}... (${serialized.length} chars)`;
859
+ };
860
+ const timeoutError = () => new Error(
861
+ `${options.label} timed out after ${options.timeoutMs}ms; ${describeLastRead()}`,
862
+ { cause: lastError }
863
+ );
864
+ options.signal?.throwIfAborted();
865
+ let rejectBoundary = () => {
866
+ };
867
+ const boundary = new Promise((_, reject) => {
868
+ rejectBoundary = reject;
869
+ });
870
+ const onAbort = () => rejectBoundary(
871
+ options.signal?.reason ?? new DOMException("The operation was aborted", "AbortError")
872
+ );
873
+ options.signal?.addEventListener("abort", onAbort, { once: true });
874
+ const deadlineTimer = setTimeout(
875
+ () => rejectBoundary(timeoutError()),
876
+ options.timeoutMs
877
+ );
878
+ try {
879
+ while (true) {
880
+ if (Date.now() >= deadline) throw timeoutError();
881
+ const outcome = await Promise.race([
882
+ Promise.resolve().then(read).then(
883
+ (value) => ({ kind: "value", value }),
884
+ (error) => ({ kind: "error", error })
885
+ ),
886
+ boundary
887
+ ]);
888
+ options.signal?.throwIfAborted();
889
+ if (outcome.kind === "error") {
890
+ lastError = outcome.error;
891
+ } else {
892
+ lastRead = { value: outcome.value };
893
+ if (Date.now() >= deadline) throw timeoutError();
894
+ try {
895
+ if (ready(outcome.value)) return outcome.value;
896
+ } catch (error) {
897
+ lastError = error;
898
+ }
899
+ }
900
+ const remaining = deadline - Date.now();
901
+ if (remaining <= 0) throw timeoutError();
902
+ if (intervalMs > 0) {
903
+ let intervalTimer;
904
+ try {
905
+ await Promise.race([
906
+ new Promise((resolve) => {
907
+ intervalTimer = setTimeout(resolve, Math.min(intervalMs, remaining));
908
+ }),
909
+ boundary
910
+ ]);
911
+ } finally {
912
+ clearTimeout(intervalTimer);
913
+ }
914
+ }
915
+ }
916
+ } finally {
917
+ clearTimeout(deadlineTimer);
918
+ options.signal?.removeEventListener("abort", onAbort);
919
+ }
920
+ }
921
+
922
+ // detox/gestures.ts
923
+ function resolveScrollStartPosition(value, label) {
924
+ const normalized = value === void 0 || Number.isNaN(value) ? 0.5 : value;
925
+ if (!Number.isFinite(normalized) || normalized < 0 || normalized > 1) {
926
+ throw new Error(`${label} must be between 0 and 1`);
927
+ }
928
+ return normalized;
929
+ }
930
+ async function waitForSootsimGestureHandler(page, viewTag, handlerName) {
931
+ await pollUntil(
932
+ () => page.evaluate(
933
+ async ({ viewTag: viewTag2, handlerName: handlerName2 }) => {
934
+ const host = window.SootSim?.bridges?.shellHost;
935
+ if (typeof host?.callTestBridge !== "function") return false;
936
+ let debug = null;
937
+ try {
938
+ debug = await host.callTestBridge("getShellGestureSeamDebug");
939
+ } catch {
940
+ return false;
941
+ }
942
+ return debug?.attached?.some(
943
+ (entry) => entry.viewTag === viewTag2 && entry.nodeFound !== false && entry.handlers?.some((handler) => handler.name === handlerName2)
944
+ ) === true;
945
+ },
946
+ { viewTag, handlerName }
947
+ ),
948
+ Boolean,
949
+ {
950
+ timeoutMs: 5e3,
951
+ intervalMs: 100,
952
+ label: `${handlerName} gesture handler on view ${viewTag}`
953
+ }
954
+ );
955
+ }
956
+ async function readWorkletCallbackStats(page) {
957
+ return page.evaluate(async () => {
958
+ const g = window;
959
+ const tenant = typeof g.__sootsimTest?.getWorkletSlotStats === "function" ? await g.__sootsimTest.getWorkletSlotStats() : null;
960
+ const shellHost = g.SootSim?.bridges?.shellHost;
961
+ let shell = null;
962
+ if (typeof shellHost?.callTestBridge === "function") {
963
+ try {
964
+ shell = await shellHost.callTestBridge("getWorkletSlotStats");
965
+ } catch {
966
+ }
967
+ }
968
+ const tenantReceived = typeof tenant?.runJSCallbackReceived === "number" ? tenant.runJSCallbackReceived : 0;
969
+ const shellSent = typeof shell?.runJSCallbackSent === "number" ? shell.runJSCallbackSent : 0;
970
+ return { tenantReceived, shellSent };
971
+ });
972
+ }
973
+ async function waitForWorkletCallbackDrain(page, before, timeoutMs) {
974
+ if (!before) {
975
+ await page.waitForTimeout(250);
976
+ return;
977
+ }
978
+ let lastShellSent = before.shellSent;
979
+ let stableSince = Date.now();
980
+ let sawCallback = false;
981
+ await pollUntil(
982
+ () => readWorkletCallbackStats(page),
983
+ (current) => {
984
+ if (!current) return false;
985
+ if (current.shellSent !== lastShellSent) {
986
+ lastShellSent = current.shellSent;
987
+ stableSince = Date.now();
988
+ }
989
+ const sentDelta = current.shellSent - before.shellSent;
990
+ if (sentDelta > 0) sawCallback = true;
991
+ const receivedDelta = current.tenantReceived - before.tenantReceived;
992
+ return sawCallback && receivedDelta >= sentDelta && Date.now() - stableSince >= 100;
993
+ },
994
+ {
995
+ timeoutMs,
996
+ intervalMs: 20,
997
+ label: "worklet callback drain"
998
+ }
999
+ ).catch(() => {
1000
+ });
1001
+ if (!sawCallback) {
1002
+ await page.waitForTimeout(250);
1003
+ }
1004
+ }
1005
+ var sootsimCanvasFinderInstalled = /* @__PURE__ */ new WeakSet();
1006
+ function installInteractiveCanvasFinder() {
1007
+ window.__sootsimFindInteractiveCanvas = () => {
1008
+ const interactive = [];
1009
+ const appVisible = [];
1010
+ for (const c of Array.from(document.querySelectorAll("canvas"))) {
1011
+ const s = window.getComputedStyle(c);
1012
+ if (s.display === "none" || s.visibility === "hidden") continue;
1013
+ const r = c.getBoundingClientRect();
1014
+ if (r.width === 0 || r.height === 0) continue;
1015
+ const entry = {
1016
+ c,
1017
+ z: parseInt(s.zIndex, 10) || 0,
1018
+ app: c.dataset.surfaceId?.startsWith("app:") === true
1019
+ };
1020
+ if (entry.app) appVisible.push(entry);
1021
+ if (s.pointerEvents !== "none") interactive.push(entry);
1022
+ }
1023
+ const list = appVisible.length > 0 ? appVisible : interactive;
1024
+ if (list.length === 0) return null;
1025
+ list.sort((a, b) => Number(b.app) - Number(a.app) || b.z - a.z);
1026
+ return list[0].c;
1027
+ };
1028
+ }
1029
+ async function ensureCanvasFinder(page) {
1030
+ if (!sootsimCanvasFinderInstalled.has(page)) {
1031
+ sootsimCanvasFinderInstalled.add(page);
1032
+ await page.addInitScript(installInteractiveCanvasFinder);
1033
+ }
1034
+ await page.evaluate(installInteractiveCanvasFinder);
1035
+ }
1036
+ async function readSootsimPageMetrics(page, timeoutMs) {
1037
+ await ensureCanvasFinder(page);
1038
+ try {
1039
+ const metrics = await pollUntil(
1040
+ () => page.evaluate(async () => {
1041
+ const canvas = window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector("canvas");
1042
+ if (!canvas) return null;
1043
+ const rect = canvas.getBoundingClientRect();
1044
+ if (rect.width <= 0 || rect.height <= 0) return null;
1045
+ const surface = await window.__sootsimTest?.getSurfaceMetricsSnapshot?.();
1046
+ const screenWidth = surface?.window?.width;
1047
+ const screenHeight = surface?.window?.height;
1048
+ if (typeof screenWidth !== "number" || typeof screenHeight !== "number" || screenWidth <= 0 || screenHeight <= 0) {
1049
+ return null;
1050
+ }
1051
+ const homeIndicatorHeight = Number(surface?.device?.homeIndicatorHeight) || 0;
1052
+ const safeAreaBottom = Number(surface?.safeAreaInsets?.bottom) || 0;
1053
+ const bottomInset = Math.max(0, homeIndicatorHeight, safeAreaBottom);
1054
+ const visibleRectBottom = Number(surface?.visibleRect?.y) + Number(surface?.visibleRect?.height);
1055
+ const interactiveHeight = Number.isFinite(visibleRectBottom) && visibleRectBottom > 0 ? Math.min(screenHeight, visibleRectBottom) : Math.max(0, screenHeight - bottomInset);
1056
+ return {
1057
+ canvas: {
1058
+ left: rect.left,
1059
+ top: rect.top,
1060
+ width: rect.width,
1061
+ height: rect.height
1062
+ },
1063
+ window: {
1064
+ width: screenWidth,
1065
+ height: screenHeight
1066
+ },
1067
+ interactiveWindow: {
1068
+ width: screenWidth,
1069
+ height: interactiveHeight
1070
+ }
1071
+ };
1072
+ }),
1073
+ (metrics2) => metrics2 !== null,
1074
+ {
1075
+ timeoutMs,
1076
+ intervalMs: 50,
1077
+ label: "sootsim surface metrics"
1078
+ }
1079
+ );
1080
+ if (metrics === null) throw new Error("surface metrics poll settled without metrics");
1081
+ return metrics;
1082
+ } catch (error) {
1083
+ throw new Error("sootsim surface metrics are not available", { cause: error });
1084
+ }
1085
+ }
1086
+ async function readSootsimViewport(page, timeoutMs = 5e3) {
1087
+ return (await readSootsimPageMetrics(page, timeoutMs)).window;
1088
+ }
1089
+ async function readSootsimInteractiveViewport(page, timeoutMs = 5e3) {
1090
+ return (await readSootsimPageMetrics(page, timeoutMs)).interactiveWindow;
1091
+ }
1092
+ async function sootsimToPage(page, sootsimX, sootsimY, timeoutMs = 5e3) {
1093
+ const metrics = await readSootsimPageMetrics(page, timeoutMs);
1094
+ const scaleX = metrics.canvas.width / metrics.window.width;
1095
+ const scaleY = metrics.canvas.height / metrics.window.height;
1096
+ return {
1097
+ x: metrics.canvas.left + sootsimX * scaleX,
1098
+ y: metrics.canvas.top + sootsimY * scaleY
1099
+ };
1100
+ }
1101
+ async function dragScrollNode(page, node, pixels, direction, startPositionX, startPositionY) {
1102
+ const normalizedStartX = resolveScrollStartPosition(
1103
+ startPositionX,
1104
+ "scroll startPositionX"
1105
+ );
1106
+ const normalizedStartY = resolveScrollStartPosition(
1107
+ startPositionY,
1108
+ "scroll startPositionY"
1109
+ );
1110
+ const startSootSimX = node.absolutePosition.x + node.layout.width * normalizedStartX;
1111
+ const startSootSimY = node.absolutePosition.y + node.layout.height * normalizedStartY;
1112
+ let endSootSimX = startSootSimX;
1113
+ let endSootSimY = startSootSimY;
1114
+ switch (direction) {
1115
+ case "down":
1116
+ endSootSimY -= pixels;
1117
+ break;
1118
+ case "up":
1119
+ endSootSimY += pixels;
1120
+ break;
1121
+ case "left":
1122
+ endSootSimX += pixels;
1123
+ break;
1124
+ case "right":
1125
+ endSootSimX -= pixels;
1126
+ break;
1127
+ }
1128
+ const startPage = await sootsimToPage(page, startSootSimX, startSootSimY);
1129
+ const endPage = await sootsimToPage(page, endSootSimX, endSootSimY);
1130
+ const steps = Math.max(16, Math.ceil(pixels / 8));
1131
+ await page.mouse.move(startPage.x, startPage.y);
1132
+ await page.mouse.down();
1133
+ await page.waitForTimeout(20);
1134
+ const dx = (endPage.x - startPage.x) / steps;
1135
+ const dy = (endPage.y - startPage.y) / steps;
1136
+ for (let i = 1; i <= steps; i++) {
1137
+ await page.mouse.move(startPage.x + dx * i, startPage.y + dy * i);
1138
+ await page.waitForTimeout(24);
1139
+ }
1140
+ await page.mouse.up();
1141
+ await page.waitForTimeout(180);
1142
+ }
1143
+ async function dispatchTwoFingerGesture(page, startA, startB, endA, endB, options = {}) {
1144
+ const steps = options.steps ?? 10;
1145
+ const stepDelay = options.stepDelayMs ?? 18;
1146
+ const workletStatsTimeoutMs = options.workletStatsTimeoutMs ?? 1500;
1147
+ await ensureCanvasFinder(page);
1148
+ const beforeWorkletStats = await pollUntil(
1149
+ () => readWorkletCallbackStats(page),
1150
+ () => true,
1151
+ {
1152
+ timeoutMs: workletStatsTimeoutMs,
1153
+ intervalMs: 0,
1154
+ label: "pre-dispatch worklet callback stats"
1155
+ }
1156
+ ).catch(() => null);
1157
+ await page.evaluate(
1158
+ ({ startA: startA2, startB: startB2 }) => {
1159
+ const canvas = window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector("canvas");
1160
+ if (!canvas) throw new Error("sootsim interactive canvas not found");
1161
+ const fire = (point, id, primary) => {
1162
+ canvas.dispatchEvent(
1163
+ new PointerEvent("pointerdown", {
1164
+ bubbles: true,
1165
+ cancelable: true,
1166
+ clientX: point.x,
1167
+ clientY: point.y,
1168
+ isPrimary: primary,
1169
+ pointerId: id,
1170
+ pointerType: "touch"
1171
+ })
1172
+ );
1173
+ };
1174
+ fire(startA2, 1001, true);
1175
+ fire(startB2, 1002, false);
1176
+ },
1177
+ { startA, startB }
1178
+ );
1179
+ for (let i = 1; i <= steps; i++) {
1180
+ const t = i / steps;
1181
+ const ax = startA.x + (endA.x - startA.x) * t;
1182
+ const ay = startA.y + (endA.y - startA.y) * t;
1183
+ const bx = startB.x + (endB.x - startB.x) * t;
1184
+ const by2 = startB.y + (endB.y - startB.y) * t;
1185
+ await page.evaluate(
1186
+ ({ ax: ax2, ay: ay2, bx: bx2, by: by3 }) => {
1187
+ const canvas = window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector("canvas");
1188
+ if (!canvas) return;
1189
+ canvas.dispatchEvent(
1190
+ new PointerEvent("pointermove", {
1191
+ bubbles: true,
1192
+ cancelable: true,
1193
+ clientX: ax2,
1194
+ clientY: ay2,
1195
+ isPrimary: true,
1196
+ pointerId: 1001,
1197
+ pointerType: "touch"
1198
+ })
1199
+ );
1200
+ canvas.dispatchEvent(
1201
+ new PointerEvent("pointermove", {
1202
+ bubbles: true,
1203
+ cancelable: true,
1204
+ clientX: bx2,
1205
+ clientY: by3,
1206
+ isPrimary: false,
1207
+ pointerId: 1002,
1208
+ pointerType: "touch"
1209
+ })
1210
+ );
1211
+ },
1212
+ { ax, ay, bx, by: by2 }
1213
+ );
1214
+ await page.waitForTimeout(stepDelay);
1215
+ }
1216
+ await page.evaluate(
1217
+ ({ endA: endA2, endB: endB2 }) => {
1218
+ const canvas = window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector("canvas");
1219
+ if (!canvas) return;
1220
+ canvas.dispatchEvent(
1221
+ new PointerEvent("pointerup", {
1222
+ bubbles: true,
1223
+ cancelable: true,
1224
+ clientX: endA2.x,
1225
+ clientY: endA2.y,
1226
+ isPrimary: true,
1227
+ pointerId: 1001,
1228
+ pointerType: "touch"
1229
+ })
1230
+ );
1231
+ canvas.dispatchEvent(
1232
+ new PointerEvent("pointerup", {
1233
+ bubbles: true,
1234
+ cancelable: true,
1235
+ clientX: endB2.x,
1236
+ clientY: endB2.y,
1237
+ isPrimary: false,
1238
+ pointerId: 1002,
1239
+ pointerType: "touch"
1240
+ })
1241
+ );
1242
+ },
1243
+ { endA, endB }
1244
+ );
1245
+ await waitForWorkletCallbackDrain(page, beforeWorkletStats, workletStatsTimeoutMs);
1246
+ await page.waitForTimeout(250);
1247
+ }
1248
+
1249
+ // detox/expectations.ts
1250
+ var DEFAULT_VISIBILITY_THRESHOLD = 0.75;
1251
+ async function scrollMatcherElement(page, findNode, matcher, pixels, direction, startPositionX, startPositionY) {
1252
+ const node = await findNode(matcher);
1253
+ if (!node) {
1254
+ throw new Error(`scroll container not found: ${JSON.stringify(matcher)}`);
1255
+ }
1256
+ const before = scrollOffsetOf(node);
1257
+ await dragScrollNode(page, node, pixels, direction, startPositionX, startPositionY);
1258
+ const after = scrollOffsetOf(await findNode(matcher));
1259
+ return Math.abs(after.x - before.x) + Math.abs(after.y - before.y);
1260
+ }
1261
+ function scrollOffsetOf(node) {
1262
+ return { x: node?.scrollOffsetX ?? 0, y: node?.scrollOffsetY ?? 0 };
1263
+ }
1264
+ function visibleFraction(node) {
1265
+ const totalArea = (node?.layout?.width ?? 0) * (node?.layout?.height ?? 0);
1266
+ const frame = node?.visibleFrame;
1267
+ if (totalArea <= 0 || typeof frame?.area !== "number") return 0;
1268
+ return frame.area / totalArea;
1269
+ }
1270
+ function isNodeVisible(node) {
1271
+ return visibleFraction(node) >= DEFAULT_VISIBILITY_THRESHOLD;
1272
+ }
1273
+ async function isNodeFocused(node, page) {
1274
+ const nodeId = typeof node?.nodeId === "number" ? node.nodeId : typeof node?.id === "number" ? node.id : null;
1275
+ if (nodeId === null) return false;
1276
+ return page.evaluate((id) => {
1277
+ const active = document.activeElement;
1278
+ return active?.getAttribute("data-sootsim-id") === String(id);
1279
+ }, nodeId);
1280
+ }
1281
+ function describeNodeVisibility(node) {
1282
+ if (!node) return "not found";
1283
+ const absX = node.absolutePosition?.x ?? node.layout?.x ?? 0;
1284
+ const absY = node.absolutePosition?.y ?? node.layout?.y ?? 0;
1285
+ return JSON.stringify({
1286
+ layout: node.layout,
1287
+ absolutePosition: node.absolutePosition ?? null,
1288
+ frame: {
1289
+ x: absX,
1290
+ y: absY,
1291
+ right: absX + (node.layout?.width ?? 0),
1292
+ bottom: absY + (node.layout?.height ?? 0)
1293
+ },
1294
+ visibleFrame: node.visibleFrame ?? null,
1295
+ visibleFraction: visibleFraction(node),
1296
+ threshold: DEFAULT_VISIBILITY_THRESHOLD,
1297
+ style: node.style ?? null
1298
+ });
1299
+ }
1300
+ function createExpect(findNode, getPage2) {
1301
+ return function sootsimExpect2(el) {
1302
+ return new SootSimExpectation(el._matcher, findNode, getPage2, false);
1303
+ };
1304
+ }
1305
+ var SootSimExpectation = class _SootSimExpectation {
1306
+ matcher;
1307
+ findNode;
1308
+ getPage;
1309
+ negated;
1310
+ constructor(matcher, findNode, getPage2, negated) {
1311
+ this.matcher = matcher;
1312
+ this.findNode = findNode;
1313
+ this.getPage = getPage2;
1314
+ this.negated = negated;
1315
+ }
1316
+ get not() {
1317
+ return new _SootSimExpectation(
1318
+ this.matcher,
1319
+ this.findNode,
1320
+ this.getPage,
1321
+ !this.negated
1322
+ );
1323
+ }
1324
+ async toExist() {
1325
+ const node = await this.findNode(this.matcher);
1326
+ const exists = node !== null && node !== void 0;
1327
+ if (this.negated) {
1328
+ if (exists) {
1329
+ throw new Error(
1330
+ `expected element NOT to exist but it does: ${JSON.stringify(this.matcher)}`
1331
+ );
1332
+ }
1333
+ } else {
1334
+ if (!exists) {
1335
+ throw new Error(
1336
+ `expected element to exist but it was not found: ${JSON.stringify(this.matcher)}`
1337
+ );
1338
+ }
1339
+ }
1340
+ }
1341
+ async toBeVisible() {
1342
+ const node = await this.findNode(this.matcher);
1343
+ const visible = isNodeVisible(node);
1344
+ if (this.negated) {
1345
+ if (visible) {
1346
+ throw new Error(
1347
+ `expected element NOT to be visible: ${JSON.stringify(this.matcher)}`
1348
+ );
1349
+ }
1350
+ } else {
1351
+ if (!visible) {
1352
+ throw new Error(
1353
+ `expected element to be visible but it is ${describeNodeVisibility(node)}: ${JSON.stringify(this.matcher)}`
1354
+ );
1355
+ }
1356
+ }
1357
+ }
1358
+ async toBeNotVisible() {
1359
+ const node = await this.findNode(this.matcher);
1360
+ const visible = isNodeVisible(node);
1361
+ if (visible) {
1362
+ throw new Error(
1363
+ `expected element NOT to be visible: ${JSON.stringify(this.matcher)}`
1364
+ );
1365
+ }
1366
+ }
1367
+ async toHaveText(expectedText) {
1368
+ const node = await this.findNode(this.matcher);
1369
+ if (!node) {
1370
+ throw new Error(`element not found for toHaveText: ${JSON.stringify(this.matcher)}`);
1371
+ }
1372
+ const actualText = node.text || "";
1373
+ const matches = actualText === expectedText;
1374
+ if (this.negated) {
1375
+ if (matches) {
1376
+ throw new Error(
1377
+ `expected text NOT to be "${expectedText}" but got "${actualText}": ${JSON.stringify(this.matcher)}`
1378
+ );
1379
+ }
1380
+ } else {
1381
+ if (!matches) {
1382
+ throw new Error(
1383
+ `expected text "${expectedText}" but got "${actualText}": ${JSON.stringify(this.matcher)}`
1384
+ );
1385
+ }
1386
+ }
1387
+ }
1388
+ async toHaveId(expectedId) {
1389
+ const node = await this.findNode(this.matcher);
1390
+ if (!node) {
1391
+ throw new Error(`element not found for toHaveId: ${JSON.stringify(this.matcher)}`);
1392
+ }
1393
+ const hasId = node.testID === expectedId || node.id === expectedId;
1394
+ if (this.negated) {
1395
+ if (hasId) throw new Error(`expected element NOT to have id "${expectedId}"`);
1396
+ } else {
1397
+ if (!hasId)
1398
+ throw new Error(
1399
+ `expected element to have id "${expectedId}" but got "${node.testID || node.id}"`
1400
+ );
1401
+ }
1402
+ }
1403
+ async toHaveLabel(expectedLabel) {
1404
+ const node = await this.findNode(this.matcher);
1405
+ if (!node) {
1406
+ throw new Error(
1407
+ `element not found for toHaveLabel: ${JSON.stringify(this.matcher)}`
1408
+ );
1409
+ }
1410
+ const actual = node.accessibilityLabel || node.text || "";
1411
+ const matches = actual === expectedLabel;
1412
+ if (this.negated) {
1413
+ if (matches)
1414
+ throw new Error(`expected label NOT to be "${expectedLabel}" but it was`);
1415
+ } else {
1416
+ if (!matches)
1417
+ throw new Error(
1418
+ `expected label "${expectedLabel}" but got "${actual}": ${JSON.stringify(this.matcher)}`
1419
+ );
1420
+ }
1421
+ }
1422
+ async toHaveAccessibilityRole(expectedRole) {
1423
+ const node = await this.findNode(this.matcher);
1424
+ if (!node) {
1425
+ throw new Error(
1426
+ `element not found for toHaveAccessibilityRole: ${JSON.stringify(this.matcher)}`
1427
+ );
1428
+ }
1429
+ const actual = node.accessibilityRole || "";
1430
+ const matches = actual === expectedRole;
1431
+ if (this.negated) {
1432
+ if (matches) throw new Error(`expected role NOT to be "${expectedRole}" but it was`);
1433
+ } else {
1434
+ if (!matches)
1435
+ throw new Error(
1436
+ `expected role "${expectedRole}" but got "${actual}": ${JSON.stringify(this.matcher)}`
1437
+ );
1438
+ }
1439
+ }
1440
+ async toBeEnabled() {
1441
+ const node = await this.findNode(this.matcher);
1442
+ if (!node) {
1443
+ throw new Error(
1444
+ `element not found for toBeEnabled: ${JSON.stringify(this.matcher)}`
1445
+ );
1446
+ }
1447
+ const disabled = node.accessibilityState?.disabled ?? false;
1448
+ if (this.negated) {
1449
+ if (!disabled) throw new Error(`expected element to be disabled but it is enabled`);
1450
+ } else {
1451
+ if (disabled) throw new Error(`expected element to be enabled but it is disabled`);
1452
+ }
1453
+ }
1454
+ async toBeFocused() {
1455
+ const node = await this.findNode(this.matcher);
1456
+ if (!node) {
1457
+ throw new Error(
1458
+ `element not found for toBeFocused: ${JSON.stringify(this.matcher)}`
1459
+ );
1460
+ }
1461
+ const focused = await isNodeFocused(node, this.getPage());
1462
+ if (this.negated) {
1463
+ if (focused) {
1464
+ throw new Error(
1465
+ `expected element NOT to be focused: ${JSON.stringify(this.matcher)}`
1466
+ );
1467
+ }
1468
+ } else if (!focused) {
1469
+ throw new Error(`expected element to be focused: ${JSON.stringify(this.matcher)}`);
1470
+ }
1471
+ }
1472
+ async toHaveValue(value) {
1473
+ const node = await this.findNode(this.matcher);
1474
+ if (!node) {
1475
+ throw new Error(
1476
+ `element not found for toHaveValue: ${JSON.stringify(this.matcher)}`
1477
+ );
1478
+ }
1479
+ const actual = node.text || "";
1480
+ if (this.negated) {
1481
+ if (actual.includes(value)) {
1482
+ throw new Error(
1483
+ `expected element NOT to have value "${value}" but got "${actual}"`
1484
+ );
1485
+ }
1486
+ } else {
1487
+ if (!actual.includes(value)) {
1488
+ throw new Error(`expected element to have value "${value}" but got "${actual}"`);
1489
+ }
1490
+ }
1491
+ }
1492
+ async toHaveSliderPosition(normalizedPosition, tolerance) {
1493
+ await this.toExist();
1494
+ }
1495
+ async toHaveToggleValue(value) {
1496
+ await this.toExist();
1497
+ }
1498
+ };
1499
+ function createWaitFor(findNode, getPage2) {
1500
+ return function sootsimWaitFor2(el) {
1501
+ return new SootSimWaitForChain(el._matcher, findNode, getPage2);
1502
+ };
1503
+ }
1504
+ var SootSimWaitForChain = class {
1505
+ matcher;
1506
+ findNode;
1507
+ getPage;
1508
+ assertionFn = null;
1509
+ _negated = false;
1510
+ constructor(matcher, findNode, getPage2) {
1511
+ this.matcher = matcher;
1512
+ this.findNode = findNode;
1513
+ this.getPage = getPage2;
1514
+ }
1515
+ get not() {
1516
+ this._negated = true;
1517
+ return this;
1518
+ }
1519
+ toExist() {
1520
+ this.assertionFn = async () => {
1521
+ const node = await this.findNode(this.matcher);
1522
+ const exists = node !== null && node !== void 0;
1523
+ if (this._negated ? exists : !exists) {
1524
+ throw new Error(`waitFor toExist failed: ${JSON.stringify(this.matcher)}`);
1525
+ }
1526
+ };
1527
+ return new SootSimWaitForAction(this.assertionFn, this.findNode, this.getPage);
1528
+ }
1529
+ toBeVisible() {
1530
+ this.assertionFn = async () => {
1531
+ const node = await this.findNode(this.matcher);
1532
+ const visible = isNodeVisible(node);
1533
+ if (this._negated ? visible : !visible) {
1534
+ throw new Error(
1535
+ `waitFor toBeVisible failed (${describeNodeVisibility(node)}): ${JSON.stringify(this.matcher)}`
1536
+ );
1537
+ }
1538
+ };
1539
+ return new SootSimWaitForAction(this.assertionFn, this.findNode, this.getPage);
1540
+ }
1541
+ toBeNotVisible() {
1542
+ this.assertionFn = async () => {
1543
+ const node = await this.findNode(this.matcher);
1544
+ const visible = isNodeVisible(node);
1545
+ if (visible) {
1546
+ throw new Error(`waitFor toBeNotVisible failed: ${JSON.stringify(this.matcher)}`);
1547
+ }
1548
+ };
1549
+ return new SootSimWaitForAction(this.assertionFn, this.findNode, this.getPage);
1550
+ }
1551
+ toHaveText(expectedText) {
1552
+ this.assertionFn = async () => {
1553
+ const node = await this.findNode(this.matcher);
1554
+ if (!node)
1555
+ throw new Error(
1556
+ `waitFor toHaveText: element not found: ${JSON.stringify(this.matcher)}`
1557
+ );
1558
+ const actual = node.text || "";
1559
+ const matches = actual === expectedText;
1560
+ if (this._negated ? matches : !matches) {
1561
+ throw new Error(
1562
+ `waitFor toHaveText: expected "${expectedText}" but got "${actual}" ${JSON.stringify(this.matcher)}`
1563
+ );
1564
+ }
1565
+ };
1566
+ return new SootSimWaitForAction(this.assertionFn, this.findNode, this.getPage);
1567
+ }
1568
+ toHaveValue(value) {
1569
+ this.assertionFn = async () => {
1570
+ const node = await this.findNode(this.matcher);
1571
+ if (!node) throw new Error(`waitFor toHaveValue: element not found`);
1572
+ const actual = node.text || "";
1573
+ if (!actual.includes(value)) {
1574
+ throw new Error(`waitFor toHaveValue: expected "${value}" in "${actual}"`);
1575
+ }
1576
+ };
1577
+ return new SootSimWaitForAction(this.assertionFn, this.findNode, this.getPage);
1578
+ }
1579
+ toBeFocused() {
1580
+ this.assertionFn = async () => {
1581
+ const node = await this.findNode(this.matcher);
1582
+ if (!node)
1583
+ throw new Error(
1584
+ `waitFor toBeFocused: element not found: ${JSON.stringify(this.matcher)}`
1585
+ );
1586
+ const focused = await isNodeFocused(node, this.getPage());
1587
+ if (this._negated ? focused : !focused) {
1588
+ throw new Error(`waitFor toBeFocused failed: ${JSON.stringify(this.matcher)}`);
1589
+ }
1590
+ };
1591
+ return new SootSimWaitForAction(this.assertionFn, this.findNode, this.getPage);
1592
+ }
1593
+ };
1594
+ var SootSimWaitForAction = class {
1595
+ assertionFn;
1596
+ findNode;
1597
+ getPage;
1598
+ constructor(assertionFn, findNode, getPage2) {
1599
+ this.assertionFn = assertionFn;
1600
+ this.findNode = findNode;
1601
+ this.getPage = getPage2;
1602
+ }
1603
+ async withTimeout(ms) {
1604
+ const start = Date.now();
1605
+ const pollInterval = 100;
1606
+ let lastError = null;
1607
+ while (Date.now() - start < ms) {
1608
+ try {
1609
+ await this.assertionFn();
1610
+ return;
1611
+ } catch (e) {
1612
+ lastError = e;
1613
+ await new Promise((r) => setTimeout(r, pollInterval));
1614
+ }
1615
+ }
1616
+ throw lastError || new Error(`waitFor timed out after ${ms}ms`);
1617
+ }
1618
+ // if no withTimeout is called, just run the assertion once
1619
+ async then(resolve, reject) {
1620
+ try {
1621
+ await this.withTimeout(5e3);
1622
+ resolve(void 0);
1623
+ } catch (e) {
1624
+ reject(e);
1625
+ }
1626
+ }
1627
+ whileElement(matcher) {
1628
+ return new SootSimWaitForScrollAction(
1629
+ this.assertionFn,
1630
+ this.findNode,
1631
+ this.getPage,
1632
+ matcher
1633
+ );
1634
+ }
1635
+ };
1636
+ var SootSimWaitForScrollAction = class {
1637
+ assertionFn;
1638
+ findNode;
1639
+ getPage;
1640
+ matcher;
1641
+ constructor(assertionFn, findNode, getPage2, matcher) {
1642
+ this.assertionFn = assertionFn;
1643
+ this.findNode = findNode;
1644
+ this.getPage = getPage2;
1645
+ this.matcher = matcher;
1646
+ }
1647
+ // detox scrolls the container until the expectation passes or the container
1648
+ // can no longer move. bounding this by wall clock instead made the result
1649
+ // depend on how fast the host dispatches pointer events: the same fixture and
1650
+ // the same assertion pass on an idle machine and fail on a loaded one,
1651
+ // because each 200px drag is a real pointer stream and fewer of them fit in
1652
+ // the budget. end on scroll exhaustion, which is a property of the content.
1653
+ async scroll(pixels, direction, startPositionX, startPositionY) {
1654
+ const page = this.getPage();
1655
+ let lastError = null;
1656
+ let stalledSteps = 0;
1657
+ while (stalledSteps < 2) {
1658
+ try {
1659
+ await this.assertionFn();
1660
+ return;
1661
+ } catch (error) {
1662
+ lastError = error;
1663
+ }
1664
+ const travelled = await scrollMatcherElement(
1665
+ page,
1666
+ this.findNode,
1667
+ this.matcher,
1668
+ pixels,
1669
+ direction,
1670
+ startPositionX,
1671
+ startPositionY
1672
+ );
1673
+ stalledSteps = travelled < 0.5 ? stalledSteps + 1 : 0;
1674
+ }
1675
+ throw lastError || new Error(
1676
+ `waitFor whileElement(...).scroll() reached the end of ${JSON.stringify(this.matcher)}`
1677
+ );
1678
+ }
1679
+ };
1680
+
1681
+ // detox/matchers.ts
1682
+ var by = {
1683
+ id(testID) {
1684
+ return { type: "id", value: testID };
1685
+ },
1686
+ text(text) {
1687
+ return { type: "text", value: text };
1688
+ },
1689
+ label(accessibilityLabel) {
1690
+ return { type: "label", value: accessibilityLabel };
1691
+ },
1692
+ type(nativeType) {
1693
+ return { type: "type", value: nativeType };
1694
+ },
1695
+ role(accessibilityRole) {
1696
+ return { type: "role", value: accessibilityRole };
1697
+ }
1698
+ };
1699
+
1700
+ // detox/index.ts
1701
+ var import_motion_change = __toESM(require_motion_change(), 1);
1702
+
1703
+ // detox/navigation-lifecycle.ts
1704
+ var import_attempt_events = __toESM(require_attempt_events(), 1);
1705
+ var DIAGNOSTIC_LIMIT = 40;
1706
+ var pageDiagnostics = /* @__PURE__ */ new WeakMap();
1707
+ var activeNavigations = /* @__PURE__ */ new WeakMap();
1708
+ var lastNavigations = /* @__PURE__ */ new WeakMap();
1709
+ var nextNavigationIdentity = 0;
1710
+ function recordPageDiagnostic(page, diagnostic, persist = false) {
1711
+ const entries = pageDiagnostics.get(page);
1712
+ if (!entries) return;
1713
+ entries.push(diagnostic);
1714
+ if (entries.length > DIAGNOSTIC_LIMIT) entries.shift();
1715
+ if (persist) (0, import_attempt_events.writeAttemptEvent)(diagnostic);
1716
+ }
1717
+ function isTopLevelDocumentRequest(page, request) {
1718
+ return request.isNavigationRequest() && request.resourceType() === "document" && request.frame() === page.mainFrame();
1719
+ }
1720
+ function recordNavigationMilestoneFor(page, active, milestone, details = {}) {
1721
+ recordPageDiagnostic(
1722
+ page,
1723
+ {
1724
+ ...active,
1725
+ ...details,
1726
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
1727
+ milestone,
1728
+ target: process.env.CONFORMANCE_PROOF_TARGET || "sootsim",
1729
+ type: "rnx-navigation",
1730
+ url: details.url ?? active.navigationUrl
1731
+ },
1732
+ true
1733
+ );
1734
+ }
1735
+ function recordNavigationMilestone(page, milestone, details = {}) {
1736
+ const active = activeNavigations.get(page);
1737
+ if (active) recordNavigationMilestoneFor(page, active, milestone, details);
1738
+ }
1739
+ function attachPageDiagnostics(page) {
1740
+ if (pageDiagnostics.has(page)) return;
1741
+ pageDiagnostics.set(page, []);
1742
+ page.on("console", (message) => {
1743
+ recordPageDiagnostic(page, {
1744
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
1745
+ level: message.type(),
1746
+ message: message.text(),
1747
+ type: "page-console"
1748
+ });
1749
+ });
1750
+ page.on("pageerror", (error) => {
1751
+ recordPageDiagnostic(page, {
1752
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
1753
+ message: error.message,
1754
+ type: "page-error"
1755
+ });
1756
+ });
1757
+ page.on("request", (request) => {
1758
+ if (!isTopLevelDocumentRequest(page, request)) return;
1759
+ recordNavigationMilestone(page, "document-request", {
1760
+ method: request.method(),
1761
+ url: request.url()
1762
+ });
1763
+ });
1764
+ page.on("response", (response) => {
1765
+ const request = response.request();
1766
+ if (!isTopLevelDocumentRequest(page, request)) return;
1767
+ recordNavigationMilestone(page, "document-response", {
1768
+ headersReceived: true,
1769
+ status: response.status(),
1770
+ url: request.url()
1771
+ });
1772
+ });
1773
+ page.on("requestfinished", (request) => {
1774
+ if (!isTopLevelDocumentRequest(page, request)) return;
1775
+ recordNavigationMilestone(page, "document-request-finished", { url: request.url() });
1776
+ });
1777
+ page.on("requestfailed", (request) => {
1778
+ const failure = request.failure();
1779
+ const errorText = failure?.errorText ?? "";
1780
+ if (isTopLevelDocumentRequest(page, request)) {
1781
+ recordNavigationMilestone(page, "document-request-failed", {
1782
+ errorText,
1783
+ url: request.url()
1784
+ });
1785
+ return;
1786
+ }
1787
+ recordPageDiagnostic(page, {
1788
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
1789
+ errorText,
1790
+ method: request.method(),
1791
+ type: "subresource-request-failed",
1792
+ url: request.url()
1793
+ });
1794
+ });
1795
+ page.on("domcontentloaded", () => {
1796
+ recordNavigationMilestone(page, "domcontentloaded");
1797
+ });
1798
+ page.on("load", () => {
1799
+ recordNavigationMilestone(page, "load");
1800
+ });
1801
+ }
1802
+ async function readSootsimPageState(page) {
1803
+ try {
1804
+ return await page.evaluate(() => ({
1805
+ bodyText: document.body?.innerText?.slice(0, 500) ?? "",
1806
+ bridges: {
1807
+ sootsim: Boolean(window.SootSim),
1808
+ test: Boolean(window.__sootsimTest)
1809
+ },
1810
+ globals: Object.keys(window).filter((key) => key.startsWith("__sootsim") || key === "SootSim").sort(),
1811
+ readyState: document.readyState,
1812
+ title: document.title,
1813
+ url: window.location.href
1814
+ }));
1815
+ } catch (error) {
1816
+ return {
1817
+ evaluateError: error instanceof Error ? error.message : String(error)
1818
+ };
1819
+ }
1820
+ }
1821
+ async function describeSootsimBridgeFailure(page, cause, summary = "timed out waiting for sootsim test bridge", pageState) {
1822
+ const recordBridgeFailure = pageState === void 0;
1823
+ const state = pageState ?? await readSootsimPageState(page);
1824
+ const navigation = lastNavigations.get(page);
1825
+ if (recordBridgeFailure && navigation) {
1826
+ recordNavigationMilestoneFor(page, navigation, "error", {
1827
+ errorText: cause instanceof Error ? cause.message : String(cause),
1828
+ failurePhase: "bridge",
1829
+ pageState: state
1830
+ });
1831
+ }
1832
+ return new Error(
1833
+ [
1834
+ summary,
1835
+ `cause: ${cause instanceof Error ? cause.message : String(cause)}`,
1836
+ `page: ${JSON.stringify(state)}`,
1837
+ `recent page diagnostics:
1838
+ ${JSON.stringify(getSootsimNavigationDiagnostics(page))}`
1839
+ ].join("\n")
1840
+ );
1841
+ }
1842
+ async function navigateSootsimPage(page, url, identity) {
1843
+ attachPageDiagnostics(page);
1844
+ const navigation = {
1845
+ ...identity,
1846
+ navigationUrl: url,
1847
+ navigationId: ++nextNavigationIdentity
1848
+ };
1849
+ activeNavigations.set(page, navigation);
1850
+ lastNavigations.set(page, navigation);
1851
+ recordNavigationMilestone(page, "start");
1852
+ try {
1853
+ return await page.goto(url, { waitUntil: "load", timeout: 3e4 });
1854
+ } catch (error) {
1855
+ const pageState = await readSootsimPageState(page);
1856
+ recordNavigationMilestone(page, "error", {
1857
+ errorText: error instanceof Error ? error.message : String(error),
1858
+ failurePhase: "navigation",
1859
+ pageState
1860
+ });
1861
+ throw await describeSootsimBridgeFailure(
1862
+ page,
1863
+ error,
1864
+ "sootsim navigation failed",
1865
+ pageState
1866
+ );
1867
+ } finally {
1868
+ activeNavigations.delete(page);
1869
+ }
1870
+ }
1871
+ function getSootsimNavigationDiagnostics(page) {
1872
+ return [...pageDiagnostics.get(page) ?? []];
1873
+ }
1874
+
1875
+ // detox/index.ts
1876
+ var import_proof_frame = __toESM(require_proof_frame(), 1);
1877
+ var BASE_URL = process.env.RNX_URL || "http://localhost:5173";
1878
+ var SCREENSHOT_DIR = process.env.RNX_SCREENSHOT_DIR || path.join(process.cwd(), "test", "detox-driver", "screenshots");
1879
+ var DETOX_PLATFORM = process.env.RNX_PLATFORM === "android" ? "android" : "ios";
1880
+ var _browser = null;
1881
+ var _reapBrowserNow = null;
1882
+ var _context = null;
1883
+ var _page = null;
1884
+ var _synchronizationEnabled = false;
1885
+ var _suspendedAppId = null;
1886
+ var _browserCompromised = null;
1887
+ var browserIdentity = 0;
1888
+ var contextIdentity = 0;
1889
+ var nextBrowserIdentity = 0;
1890
+ var nextContextIdentity = 0;
1891
+ var BROWSER_LIFECYCLE_TIMEOUT_MS = 3e4;
1892
+ async function withLifecycleTimeout(actionName, targetContextId, fn, timeoutMs = BROWSER_LIFECYCLE_TIMEOUT_MS) {
1893
+ const start = Date.now();
1894
+ let timer;
1895
+ try {
1896
+ return await Promise.race([
1897
+ fn(),
1898
+ new Promise((_, reject) => {
1899
+ timer = setTimeout(() => {
1900
+ const elapsed = Date.now() - start;
1901
+ reject(
1902
+ new Error(
1903
+ `sootsim browser lifecycle error: ${actionName} did not settle after ${elapsed}ms (context=${targetContextId})`
1904
+ )
1905
+ );
1906
+ }, timeoutMs);
1907
+ })
1908
+ ]);
1909
+ } finally {
1910
+ if (timer) clearTimeout(timer);
1911
+ }
1912
+ }
1913
+ var STATUS_BAR_OVERRIDE_EVENT = "sootsim:statusBarOverride";
1914
+ function getPage() {
1915
+ if (_browserCompromised) {
1916
+ throw new Error(`sootsim browser is compromised: ${_browserCompromised}`);
1917
+ }
1918
+ if (!_page)
1919
+ throw new Error("sootsim driver not initialized -- call device.launchApp() first");
1920
+ return _page;
1921
+ }
1922
+ async function waitForSootsimTree(page, timeout = 3e4) {
1923
+ try {
1924
+ await page.waitForFunction(() => !!window.__sootsimTest?.waitForTree, {
1925
+ timeout
1926
+ });
1927
+ await page.evaluate(async (timeoutMs) => {
1928
+ await Promise.race([
1929
+ window.__sootsimTest.waitForTree(),
1930
+ new Promise((_, reject) => {
1931
+ setTimeout(
1932
+ () => reject(new Error(`sootsim waitForTree timed out after ${timeoutMs}ms`)),
1933
+ timeoutMs
1934
+ );
1935
+ })
1936
+ ]);
1937
+ }, timeout);
1938
+ } catch (error) {
1939
+ throw await describeSootsimBridgeFailure(page, error);
1940
+ }
1941
+ }
1942
+ async function waitForSootsimSurfaceMetrics(page, timeout = 3e4) {
1943
+ const deadline = Date.now() + timeout;
1944
+ let lastSnapshot = null;
1945
+ while (Date.now() < deadline) {
1946
+ lastSnapshot = await page.evaluate(async () => {
1947
+ const metricsWindow = (value) => {
1948
+ if (!value || typeof value !== "object") return null;
1949
+ const windowValue = value.window;
1950
+ if (!windowValue || typeof windowValue !== "object") return null;
1951
+ return windowValue;
1952
+ };
1953
+ const snapshot = await window.__sootsimTest?.getSurfaceMetricsSnapshot?.();
1954
+ const windowMetrics = metricsWindow(snapshot);
1955
+ const width = windowMetrics?.width;
1956
+ const height = windowMetrics?.height;
1957
+ if (typeof width === "number" && typeof height === "number" && width > 0 && height > 0) {
1958
+ return { ready: true };
1959
+ }
1960
+ return { ready: false, snapshot };
1961
+ });
1962
+ if (lastSnapshot && typeof lastSnapshot === "object" && lastSnapshot.ready) {
1963
+ return;
1964
+ }
1965
+ await page.waitForTimeout(50);
1966
+ }
1967
+ throw new Error(
1968
+ `timed out waiting for sootsim surface metrics: ${JSON.stringify(lastSnapshot)}`
1969
+ );
1970
+ }
1971
+ async function waitForSootsimShellReady(page, timeout = 3e4) {
1972
+ try {
1973
+ await page.evaluate(async (timeoutMs) => {
1974
+ const start = Date.now();
1975
+ const deadline = start + timeoutMs;
1976
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1977
+ const remaining = () => Math.max(1, deadline - Date.now());
1978
+ const withTimeout = async (label, promise) => {
1979
+ let timer = null;
1980
+ try {
1981
+ await Promise.race([
1982
+ promise,
1983
+ new Promise((_, reject) => {
1984
+ timer = setTimeout(
1985
+ () => reject(new Error(`${label} timed out after ${timeoutMs}ms`)),
1986
+ remaining()
1987
+ );
1988
+ })
1989
+ ]);
1990
+ } finally {
1991
+ if (timer) clearTimeout(timer);
1992
+ }
1993
+ };
1994
+ const isTenantOnly = new URL(window.location.href).searchParams.get("renderMode") === "tenant-only";
1995
+ const currentUrl = new URL(window.location.href);
1996
+ const expectsInitialAppPaint = currentUrl.searchParams.has("test") || currentUrl.pathname.includes("/app/");
1997
+ let shellHost = null;
1998
+ while (Date.now() < deadline) {
1999
+ const candidate = window.SootSim?.bridges?.shellHost;
2000
+ if (candidate || isTenantOnly) {
2001
+ shellHost = candidate ?? null;
2002
+ break;
2003
+ }
2004
+ await sleep(25);
2005
+ }
2006
+ if (!shellHost) {
2007
+ if (isTenantOnly) return;
2008
+ throw new Error(`sootsim shell host unavailable after ${timeoutMs}ms`);
2009
+ }
2010
+ if (shellHost.ready && typeof shellHost.ready.then === "function") {
2011
+ await withTimeout("sootsim shell ready", shellHost.ready);
2012
+ }
2013
+ if (!expectsInitialAppPaint && shellHost.firstContentPainted && typeof shellHost.firstContentPainted.then === "function") {
2014
+ await withTimeout(
2015
+ "sootsim shell first content paint",
2016
+ shellHost.firstContentPainted
2017
+ );
2018
+ }
2019
+ const waitForScreenTransitions = window.__sootsimTest?.waitForScreenTransitions;
2020
+ if (typeof waitForScreenTransitions === "function") {
2021
+ const transitionResult = await waitForScreenTransitions({
2022
+ timeoutMs: Math.min(remaining(), 3e3),
2023
+ settleMs: 64,
2024
+ startWindowMs: 700
2025
+ });
2026
+ if (transitionResult?.timedOut === true) {
2027
+ throw new Error(
2028
+ `sootsim app launch transitions timed out: ${JSON.stringify(transitionResult)}`
2029
+ );
2030
+ }
2031
+ }
2032
+ }, timeout);
2033
+ } catch (error) {
2034
+ throw await describeSootsimBridgeFailure(page, error);
2035
+ }
2036
+ }
2037
+ async function waitForSootsimWorkletsIdle(page, timeout = 3e3) {
2038
+ const result = await page.evaluate(
2039
+ async ({ timeout: timeout2 }) => {
2040
+ const getPeerStats = window.__sootsimTest?.getPeerStats;
2041
+ if (typeof getPeerStats !== "function") {
2042
+ return { settled: true, skipped: true };
2043
+ }
2044
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2045
+ const readStats = () => Promise.race([
2046
+ getPeerStats(false),
2047
+ new Promise((resolve) => setTimeout(() => resolve(null), 250))
2048
+ ]);
2049
+ const start = Date.now();
2050
+ const deadline = start + timeout2;
2051
+ const requiredStableMs = 120;
2052
+ let stableSince = null;
2053
+ let lastPending = 0;
2054
+ await sleep(0);
2055
+ while (Date.now() < deadline) {
2056
+ const stats = await readStats();
2057
+ const pending = stats && typeof stats === "object" && typeof stats.pendingRunWorklets === "number" ? stats.pendingRunWorklets : 0;
2058
+ lastPending = pending;
2059
+ if (pending === 0) {
2060
+ stableSince ??= Date.now();
2061
+ if (Date.now() - stableSince >= requiredStableMs) {
2062
+ return { settled: true, elapsed: Date.now() - start, pending };
2063
+ }
2064
+ } else {
2065
+ stableSince = null;
2066
+ }
2067
+ await sleep(25);
2068
+ }
2069
+ return {
2070
+ settled: false,
2071
+ elapsed: Date.now() - start,
2072
+ pending: lastPending
2073
+ };
2074
+ },
2075
+ { timeout }
2076
+ );
2077
+ if (!result?.settled) {
2078
+ throw new Error(
2079
+ `sootsim worklet runtime did not settle after ${result?.elapsed ?? timeout}ms (pending=${result?.pending ?? "unknown"})`
2080
+ );
2081
+ }
2082
+ }
2083
+ function reapOwnedBrowser() {
2084
+ const reap = _reapBrowserNow;
2085
+ _reapBrowserNow = null;
2086
+ _browser = null;
2087
+ try {
2088
+ reap?.();
2089
+ } catch {
2090
+ }
2091
+ }
2092
+ async function closeContext() {
2093
+ if (_browserCompromised) {
2094
+ throw new Error(`sootsim browser is compromised: ${_browserCompromised}`);
2095
+ }
2096
+ if (!_context) return;
2097
+ const context = _context;
2098
+ const closingContextId = contextIdentity;
2099
+ try {
2100
+ await withLifecycleTimeout("context.close()", closingContextId, () => context.close());
2101
+ _context = null;
2102
+ _page = null;
2103
+ contextIdentity = 0;
2104
+ _synchronizationEnabled = false;
2105
+ } catch (error) {
2106
+ _browserCompromised = error instanceof Error ? error.message : String(error);
2107
+ reapOwnedBrowser();
2108
+ throw error;
2109
+ }
2110
+ }
2111
+ async function closeBrowser() {
2112
+ if (!_browser) return;
2113
+ const browser = _browser;
2114
+ const reapBrowserNow = _reapBrowserNow;
2115
+ _browser = null;
2116
+ _reapBrowserNow = null;
2117
+ browserIdentity = 0;
2118
+ let closeError = null;
2119
+ try {
2120
+ await closeContext();
2121
+ } catch (error) {
2122
+ closeError = error instanceof Error ? error : new Error(String(error));
2123
+ }
2124
+ try {
2125
+ reapBrowserNow?.();
2126
+ } catch {
2127
+ }
2128
+ try {
2129
+ await browser.close();
2130
+ } catch {
2131
+ }
2132
+ if (closeError) {
2133
+ throw closeError;
2134
+ }
2135
+ }
2136
+ async function waitForSootsimIdle(page, maxMs = 3e3, strict = false) {
2137
+ const result = await page.evaluate(
2138
+ async ({ maxMs: maxMs2, strict: strict2 }) => {
2139
+ const start = Date.now();
2140
+ let transitionError = null;
2141
+ try {
2142
+ const waitForScreenTransitions = window.__sootsimTest?.waitForScreenTransitions;
2143
+ if (typeof waitForScreenTransitions === "function") {
2144
+ const transitionResult = await waitForScreenTransitions({
2145
+ timeoutMs: Math.min(maxMs2, 1800),
2146
+ settleMs: 48,
2147
+ startWindowMs: 600
2148
+ });
2149
+ if (transitionResult?.timedOut) {
2150
+ return {
2151
+ settled: false,
2152
+ elapsed: transitionResult.waitedMs,
2153
+ reason: "screen transition"
2154
+ };
2155
+ }
2156
+ }
2157
+ } catch (error) {
2158
+ transitionError = error instanceof Error ? error.message : String(error);
2159
+ }
2160
+ const deadline = start + maxMs2;
2161
+ const pollMs = 50;
2162
+ const requiredStablePolls = 3;
2163
+ const layoutTolerance = 1;
2164
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2165
+ const isLayoutStable = (a, b) => {
2166
+ if (!a || a.length !== b.length) return false;
2167
+ for (let i = 0; i < b.length; i++) {
2168
+ if (Math.abs(a[i] - b[i]) > layoutTolerance) return false;
2169
+ }
2170
+ return true;
2171
+ };
2172
+ const readSnapshot = async () => {
2173
+ const root = window.__sootsimRoot;
2174
+ const layout = [];
2175
+ let animating = false;
2176
+ let auxSurfaceDebug = null;
2177
+ let layoutDirty = false;
2178
+ let renderStatsAvailable = false;
2179
+ if (strict2) {
2180
+ try {
2181
+ const renderHost = Reflect.get(window, "__sootsimRenderHost");
2182
+ const queryStats = typeof renderHost === "object" && renderHost !== null ? Reflect.get(renderHost, "queryStats") : null;
2183
+ const stats = typeof queryStats === "function" ? await queryStats.call(renderHost) : null;
2184
+ renderStatsAvailable = !!stats;
2185
+ auxSurfaceDebug = stats?.auxSurfaceDebug ?? null;
2186
+ animating = stats?.hasActiveAnims === true || stats?.hasActiveNativeAnimations === true || stats?.hasPendingAnimationFrames === true;
2187
+ layoutDirty = stats?.layoutDirty === true && stats?.renderRequested === true;
2188
+ } catch {
2189
+ }
2190
+ }
2191
+ if (root) {
2192
+ const walk = (node) => {
2193
+ if (node.layout && node.layout.width > 0) {
2194
+ layout.push(
2195
+ Math.round(node.layout.x),
2196
+ Math.round(node.layout.y),
2197
+ Math.round(node.layout.width),
2198
+ Math.round(node.layout.height)
2199
+ );
2200
+ }
2201
+ for (const child of node.children || []) walk(child);
2202
+ };
2203
+ walk(root);
2204
+ }
2205
+ return {
2206
+ animating,
2207
+ auxSurfaceDebug,
2208
+ layout,
2209
+ layoutDirty,
2210
+ renderStatsAvailable
2211
+ };
2212
+ };
2213
+ let stableLayout = null;
2214
+ let stable = 0;
2215
+ let lastSnapshot = null;
2216
+ while (Date.now() < deadline) {
2217
+ const snapshot = await readSnapshot();
2218
+ lastSnapshot = snapshot;
2219
+ if ((!strict2 || snapshot.renderStatsAvailable) && !snapshot.animating && !snapshot.layoutDirty && isLayoutStable(stableLayout, snapshot.layout)) {
2220
+ stable++;
2221
+ if (stable >= requiredStablePolls) {
2222
+ return { settled: true, elapsed: Date.now() - start };
2223
+ }
2224
+ } else {
2225
+ stableLayout = snapshot.layout;
2226
+ stable = 0;
2227
+ }
2228
+ await sleep(pollMs);
2229
+ }
2230
+ return {
2231
+ settled: false,
2232
+ elapsed: Date.now() - start,
2233
+ reason: transitionError ? `layout fallback after screen transition wait failed: ${transitionError}` : strict2 ? `strict idle state unavailable: renderStats=${lastSnapshot?.renderStatsAvailable === true} animating=${lastSnapshot?.animating === true} layoutDirty=${lastSnapshot?.layoutDirty === true} layoutNodes=${(lastSnapshot?.layout.length ?? 0) / 4} aux=${JSON.stringify(lastSnapshot?.auxSurfaceDebug)}` : "layout fallback"
2234
+ };
2235
+ },
2236
+ { maxMs, strict }
2237
+ );
2238
+ if (!result?.settled) {
2239
+ throw new Error(
2240
+ `sootsim synchronization timed out after ${result?.elapsed ?? maxMs}ms${result?.reason ? ` (${result.reason})` : ""}`
2241
+ );
2242
+ }
2243
+ }
2244
+ async function findNodeByMatcher(matcher) {
2245
+ const page = getPage();
2246
+ if (matcher.index !== void 0) {
2247
+ return page.evaluate(async (indexedMatcher) => {
2248
+ const filter = indexedMatcher.type === "id" ? { hasId: indexedMatcher.value } : indexedMatcher.type === "text" ? { hasText: indexedMatcher.value } : indexedMatcher.type === "label" ? { hasLabel: indexedMatcher.value } : indexedMatcher.type === "role" ? { hasRole: indexedMatcher.value } : { type: indexedMatcher.value };
2249
+ const results = await window.__sootsimTest.queryAll(filter);
2250
+ const indexed = indexedMatcher.type === "text" ? results.filter(
2251
+ (node) => node.type === "text" && node.text === indexedMatcher.value
2252
+ ) : results;
2253
+ return indexed[indexedMatcher.index ?? 0] ?? null;
2254
+ }, matcher);
2255
+ }
2256
+ if (matcher.type === "id") {
2257
+ return page.evaluate(
2258
+ (id) => window.__sootsimTest.findByTestId(id),
2259
+ matcher.value
2260
+ );
2261
+ } else if (matcher.type === "text") {
2262
+ return page.evaluate(
2263
+ (text) => window.__sootsimTest.findByText(text),
2264
+ matcher.value
2265
+ );
2266
+ } else if (matcher.type === "label") {
2267
+ return page.evaluate(
2268
+ (label) => window.__sootsimTest.findByLabel(label),
2269
+ matcher.value
2270
+ );
2271
+ } else if (matcher.type === "role") {
2272
+ return page.evaluate(
2273
+ (role) => window.__sootsimTest.findByRole(role),
2274
+ matcher.value
2275
+ );
2276
+ } else if (matcher.type === "type") {
2277
+ return page.evaluate(async (type) => {
2278
+ const results = await window.__sootsimTest.queryAll({ type });
2279
+ return results[0] || null;
2280
+ }, matcher.value);
2281
+ }
2282
+ return null;
2283
+ }
2284
+ function getNodeCenter(nodeInfo) {
2285
+ return {
2286
+ x: nodeInfo.absolutePosition.x + nodeInfo.layout.width / 2,
2287
+ y: nodeInfo.absolutePosition.y + nodeInfo.layout.height / 2
2288
+ };
2289
+ }
2290
+ async function nodeOffsetToPage(page, nodeInfo, dx, dy) {
2291
+ const center = getNodeCenter(nodeInfo);
2292
+ return sootsimToPage(page, center.x + dx, center.y + dy);
2293
+ }
2294
+ async function getDefaultTapPoint(page, nodeInfo) {
2295
+ const resolvedTarget = await page.evaluate(
2296
+ (nodeId) => window.__sootsimTest?.resolveTapTarget?.(nodeId) ?? null,
2297
+ nodeInfo.nodeId
2298
+ );
2299
+ const center = resolvedTarget && typeof resolvedTarget.cx === "number" && typeof resolvedTarget.cy === "number" ? { x: resolvedTarget.cx, y: resolvedTarget.cy } : getNodeCenter(nodeInfo);
2300
+ const frame = nodeInfo.visibleFrame ?? {
2301
+ x: nodeInfo.absolutePosition?.x ?? nodeInfo.layout?.x ?? 0,
2302
+ y: nodeInfo.absolutePosition?.y ?? nodeInfo.layout?.y ?? 0,
2303
+ width: nodeInfo.layout?.width ?? 0,
2304
+ height: nodeInfo.layout?.height ?? 0
2305
+ };
2306
+ const viewport = await readSootsimInteractiveViewport(page);
2307
+ const fullFrameTolerance = 0.5;
2308
+ const canTapCenter = typeof nodeInfo.absolutePosition?.x === "number" && typeof nodeInfo.absolutePosition?.y === "number" && typeof nodeInfo.layout?.width === "number" && typeof nodeInfo.layout?.height === "number" && Math.abs(frame.x - nodeInfo.absolutePosition.x) <= fullFrameTolerance && Math.abs(frame.y - nodeInfo.absolutePosition.y) <= fullFrameTolerance && frame.width >= nodeInfo.layout.width - fullFrameTolerance && frame.height >= nodeInfo.layout.height - fullFrameTolerance && center.x >= 0 && center.y >= 0 && center.x < viewport.width && center.y < viewport.height;
2309
+ if (canTapCenter) return center;
2310
+ const left = Math.max(0, frame.x);
2311
+ const top = Math.max(0, frame.y);
2312
+ const right = Math.min(viewport.width, frame.x + frame.width);
2313
+ const bottom = Math.min(viewport.height, frame.y + frame.height);
2314
+ if (right <= left || bottom <= top) {
2315
+ return center;
2316
+ }
2317
+ return {
2318
+ x: left + (right - left) / 2,
2319
+ y: top + (bottom - top) / 2
2320
+ };
2321
+ }
2322
+ async function dispatchTapAtSootsimPoint(page, x, y) {
2323
+ await page.evaluate(
2324
+ async ({ x: x2, y: y2 }) => {
2325
+ const interactTap = window.SootSim?.bridges?.interact?.tap;
2326
+ if (typeof interactTap === "function") {
2327
+ const result = await interactTap(x2, y2);
2328
+ if (result && (typeof result !== "object" || result.hit !== false)) return;
2329
+ }
2330
+ const tap = window.__sootsimTest?.tap;
2331
+ if (typeof tap !== "function") {
2332
+ throw new Error("sootsim tap bridge is not installed");
2333
+ }
2334
+ await tap(x2, y2);
2335
+ },
2336
+ { x, y }
2337
+ );
2338
+ }
2339
+ async function isFocusedTextInputNode(page, nodeInfo) {
2340
+ const focused = await page.evaluate(
2341
+ () => window.__sootsimTest.getFocusedNode?.() ?? null
2342
+ );
2343
+ return !!focused && typeof focused === "object" && focused.nodeId === nodeInfo?.nodeId;
2344
+ }
2345
+ async function focusElementForTextEntry(el) {
2346
+ const page = getPage();
2347
+ const node = await findNodeByMatcher(el._matcher);
2348
+ if (!node) {
2349
+ throw new Error(`element not found for text entry: ${JSON.stringify(el._matcher)}`);
2350
+ }
2351
+ if (await isFocusedTextInputNode(page, node)) return;
2352
+ await el.tap();
2353
+ await page.waitForTimeout(100);
2354
+ }
2355
+ function ensureScreenshotDir() {
2356
+ if (!fs.existsSync(SCREENSHOT_DIR)) {
2357
+ fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
2358
+ }
2359
+ }
2360
+ function decodePngDataUrl(dataUrl) {
2361
+ const match = /^data:image\/png;base64,(.+)$/.exec(dataUrl);
2362
+ if (!match) {
2363
+ throw new Error(
2364
+ `rnx screenshot bridge returned a non-png payload: ${JSON.stringify(dataUrl.slice(0, 120))}`
2365
+ );
2366
+ }
2367
+ return Buffer.from(match[1], "base64");
2368
+ }
2369
+ function writeFastScreenshotFrames(namePrefix, frames) {
2370
+ ensureScreenshotDir();
2371
+ return frames.map((frame) => {
2372
+ const safeName = `${namePrefix}-${frame.label}`.replace(/[^a-zA-Z0-9._-]+/g, "-");
2373
+ const filePath = path.join(SCREENSHOT_DIR, `${safeName}.png`);
2374
+ fs.writeFileSync(filePath, decodePngDataUrl(frame.dataUrl));
2375
+ const { dataUrl: _dataUrl, ...metadata } = frame;
2376
+ return {
2377
+ ...metadata,
2378
+ path: filePath
2379
+ };
2380
+ });
2381
+ }
2382
+ async function captureSootsimPngFast(opts) {
2383
+ const page = getPage();
2384
+ const dataUrl = await page.evaluate(
2385
+ async (captureOpts) => {
2386
+ const screenshot = window.SootSim?.bridges?.screenshot;
2387
+ if (typeof screenshot !== "function") {
2388
+ throw new Error("rnx screenshot bridge is not installed");
2389
+ }
2390
+ return screenshot(captureOpts);
2391
+ },
2392
+ { crop: opts?.crop, format: "png" }
2393
+ );
2394
+ if (!dataUrl) {
2395
+ throw new Error("rnx screenshot bridge returned an empty image");
2396
+ }
2397
+ return decodePngDataUrl(dataUrl);
2398
+ }
2399
+ async function setSootsimCursorCaptureActive(page, active) {
2400
+ await page.evaluate(async (captureActive) => {
2401
+ const bridge = window.__sootsimTest;
2402
+ if (typeof bridge?.setCursorCaptureActive !== "function") {
2403
+ throw new Error("sootsim cursor capture control is not installed");
2404
+ }
2405
+ await bridge.setCursorCaptureActive(captureActive);
2406
+ }, active);
2407
+ }
2408
+ async function captureSootsimPng(opts) {
2409
+ const page = getPage();
2410
+ await setSootsimCursorCaptureActive(page, true);
2411
+ try {
2412
+ const deadline = Date.now() + 8e3;
2413
+ let previous = null;
2414
+ let identicalFrames = 1;
2415
+ let attempts = 0;
2416
+ while (Date.now() < deadline) {
2417
+ await waitForSootsimIdle(page, Math.max(1, deadline - Date.now()), true);
2418
+ const current = await captureSootsimPngFast(opts);
2419
+ attempts++;
2420
+ if ((0, import_proof_frame.framesIdentical)(previous, current)) {
2421
+ identicalFrames++;
2422
+ if (identicalFrames >= import_proof_frame.REQUIRED_IDENTICAL_PROOF_FRAMES) return current;
2423
+ } else {
2424
+ identicalFrames = 1;
2425
+ }
2426
+ previous = current;
2427
+ }
2428
+ throw new Error(
2429
+ `rnx screenshot did not reach ${import_proof_frame.REQUIRED_IDENTICAL_PROOF_FRAMES} identical idle frames in ${attempts} captures`
2430
+ );
2431
+ } finally {
2432
+ await setSootsimCursorCaptureActive(page, false);
2433
+ }
2434
+ }
2435
+ async function writeSootsimScreenshot(name, opts) {
2436
+ ensureScreenshotDir();
2437
+ const screenshotPath = path.join(SCREENSHOT_DIR, `${name}.png`);
2438
+ fs.writeFileSync(screenshotPath, await captureSootsimPng(opts));
2439
+ return screenshotPath;
2440
+ }
2441
+ async function dispatchStatusBarOverride(config) {
2442
+ const page = getPage();
2443
+ const time = typeof config.time === "string" && config.time.length > 0 ? config.time : null;
2444
+ await page.evaluate(
2445
+ ({ eventType, time: time2 }) => {
2446
+ window.dispatchEvent(
2447
+ new CustomEvent(eventType, {
2448
+ detail: { time: time2 }
2449
+ })
2450
+ );
2451
+ },
2452
+ { eventType: STATUS_BAR_OVERRIDE_EVENT, time }
2453
+ );
2454
+ await page.waitForTimeout(50);
2455
+ }
2456
+ var MAX_SETTLED_SPEED_PT_PER_SEC = 2;
2457
+ async function waitForFrameClock(page, minMs = 25) {
2458
+ const start = performance.now();
2459
+ await page.evaluate(
2460
+ () => new Promise((resolve) => {
2461
+ const timeoutId = setTimeout(() => resolve(), 50);
2462
+ requestAnimationFrame(() => {
2463
+ clearTimeout(timeoutId);
2464
+ resolve();
2465
+ });
2466
+ })
2467
+ );
2468
+ const elapsed = performance.now() - start;
2469
+ if (elapsed < minMs) {
2470
+ await page.waitForTimeout(minMs - elapsed);
2471
+ }
2472
+ }
2473
+ async function findStableNodeByMatcher(matcher, action = "tap", maxWaitMs = 1500) {
2474
+ const page = getPage();
2475
+ const start = performance.now();
2476
+ let lastPos = null;
2477
+ let lastTime = null;
2478
+ let lastRate = null;
2479
+ let sampleCount = 0;
2480
+ let stablePolls = 0;
2481
+ const requiredStable = 2;
2482
+ let hadNumericLayout = false;
2483
+ let disappearedMidWait = false;
2484
+ while (performance.now() - start < maxWaitMs) {
2485
+ const node = await findNodeByMatcher(matcher);
2486
+ if (!node) {
2487
+ if (hadNumericLayout) {
2488
+ disappearedMidWait = true;
2489
+ } else {
2490
+ return null;
2491
+ }
2492
+ } else {
2493
+ disappearedMidWait = false;
2494
+ const pos = node.absolutePosition ?? node.layout;
2495
+ if (pos && typeof pos.x === "number" && typeof pos.y === "number") {
2496
+ hadNumericLayout = true;
2497
+ sampleCount++;
2498
+ const now = performance.now();
2499
+ if (lastPos && lastTime !== null) {
2500
+ const dtSec = (now - lastTime) / 1e3;
2501
+ if (dtSec > 5e-3) {
2502
+ const vx = Math.abs(pos.x - lastPos.x) / dtSec;
2503
+ const vy = Math.abs(pos.y - lastPos.y) / dtSec;
2504
+ const speed = Math.sqrt(vx * vx + vy * vy);
2505
+ lastRate = { vx, vy, speed };
2506
+ if (speed < MAX_SETTLED_SPEED_PT_PER_SEC) {
2507
+ stablePolls++;
2508
+ if (stablePolls >= requiredStable) {
2509
+ return node;
2510
+ }
2511
+ } else {
2512
+ stablePolls = 0;
2513
+ }
2514
+ }
2515
+ }
2516
+ lastPos = { x: pos.x, y: pos.y };
2517
+ lastTime = now;
2518
+ }
2519
+ }
2520
+ await waitForFrameClock(page, 25);
2521
+ }
2522
+ const elapsed = Math.round(performance.now() - start);
2523
+ if (disappearedMidWait) {
2524
+ throw new Error(
2525
+ `element disappeared while waiting to come to rest for ${action}: ${JSON.stringify(matcher)} after ${elapsed}ms`
2526
+ );
2527
+ }
2528
+ if (!hadNumericLayout) {
2529
+ throw new Error(
2530
+ `element layout is not available for ${action}: ${JSON.stringify(matcher)} after ${elapsed}ms`
2531
+ );
2532
+ }
2533
+ if (sampleCount < 2 || lastRate === null) {
2534
+ throw new Error(
2535
+ `element did not come to rest for ${action}: ${JSON.stringify(matcher)}; only one sample obtained after ${elapsed}ms`
2536
+ );
2537
+ }
2538
+ throw new Error(
2539
+ `element did not come to rest for ${action}: ${JSON.stringify(matcher)}; still moving ${lastRate.vx.toFixed(1)} points/sec x / ${lastRate.vy.toFixed(1)} points/sec y after ${elapsed}ms`
2540
+ );
2541
+ }
2542
+ function createSootSimElement(matcher) {
2543
+ const el = {
2544
+ _matcher: matcher,
2545
+ async tap(point) {
2546
+ const page = getPage();
2547
+ await waitForSootsimWorkletsIdle(page, 3e3);
2548
+ const node = await findStableNodeByMatcher(matcher, "tap");
2549
+ if (!node) throw new Error(`element not found for tap: ${JSON.stringify(matcher)}`);
2550
+ if (!isNodeVisible(node)) {
2551
+ throw new Error(
2552
+ `Cannot perform action due to constraint failure: element not visible (${describeNodeVisibility(node)}): ${JSON.stringify(matcher)}`
2553
+ );
2554
+ }
2555
+ const defaultPoint = point ? null : await getDefaultTapPoint(page, node);
2556
+ const targetSootSimX = typeof point?.x === "number" ? node.absolutePosition.x + point.x : defaultPoint?.x ?? node.absolutePosition.x + node.layout.width / 2;
2557
+ const targetSootSimY = typeof point?.y === "number" ? node.absolutePosition.y + point.y : defaultPoint?.y ?? node.absolutePosition.y + node.layout.height / 2;
2558
+ await dispatchTapAtSootsimPoint(page, targetSootSimX, targetSootSimY);
2559
+ if (_synchronizationEnabled) {
2560
+ await waitForSootsimIdle(page);
2561
+ }
2562
+ },
2563
+ async multiTap(times) {
2564
+ const page = getPage();
2565
+ const node = await findStableNodeByMatcher(matcher, "multiTap");
2566
+ if (!node)
2567
+ throw new Error(`element not found for multiTap: ${JSON.stringify(matcher)}`);
2568
+ if (!isNodeVisible(node)) {
2569
+ throw new Error(
2570
+ `Cannot perform action due to constraint failure: element not visible (${describeNodeVisibility(node)}): ${JSON.stringify(matcher)}`
2571
+ );
2572
+ }
2573
+ const center = getNodeCenter(node);
2574
+ const pageCoords = await sootsimToPage(page, center.x, center.y);
2575
+ for (let i = 0; i < times; i++) {
2576
+ await page.mouse.move(pageCoords.x, pageCoords.y);
2577
+ await page.mouse.down();
2578
+ await page.waitForTimeout(50);
2579
+ await page.mouse.up();
2580
+ if (i < times - 1) {
2581
+ await page.waitForTimeout(80);
2582
+ }
2583
+ }
2584
+ await page.waitForTimeout(80);
2585
+ },
2586
+ // mirrors detox element.longPress(duration) and longPress(point, duration),
2587
+ // where the point is relative to the element's origin
2588
+ async longPress(pointOrDuration, durationArg = 1e3) {
2589
+ const page = getPage();
2590
+ const node = await findStableNodeByMatcher(matcher, "longPress");
2591
+ if (!node)
2592
+ throw new Error(`element not found for longPress: ${JSON.stringify(matcher)}`);
2593
+ if (!isNodeVisible(node)) {
2594
+ throw new Error(
2595
+ `Cannot perform action due to constraint failure: element not visible (${describeNodeVisibility(node)}): ${JSON.stringify(matcher)}`
2596
+ );
2597
+ }
2598
+ const duration = typeof pointOrDuration === "number" ? pointOrDuration : durationArg;
2599
+ const target = typeof pointOrDuration === "object" ? {
2600
+ x: node.absolutePosition.x + pointOrDuration.x,
2601
+ y: node.absolutePosition.y + pointOrDuration.y
2602
+ } : getNodeCenter(node);
2603
+ await page.evaluate(
2604
+ async ({ duration: duration2, x, y }) => {
2605
+ const longPress = window.SootSim?.bridges?.interact?.longPress;
2606
+ if (typeof longPress !== "function") {
2607
+ throw new Error("sootsim longPress bridge is not installed");
2608
+ }
2609
+ const hit = await longPress(x, y, duration2);
2610
+ if (!hit) {
2611
+ throw new Error(`sootsim longPress missed at ${x},${y}`);
2612
+ }
2613
+ },
2614
+ { duration, x: target.x, y: target.y }
2615
+ );
2616
+ await page.waitForTimeout(50);
2617
+ },
2618
+ async longPressAndDrag(duration, normalizedStartX, normalizedStartY, targetElement, normalizedEndX, normalizedEndY, speed = "fast", holdDuration = 0) {
2619
+ const page = getPage();
2620
+ const srcNode = await findStableNodeByMatcher(matcher, "longPressAndDrag");
2621
+ if (!srcNode)
2622
+ throw new Error(`source element not found: ${JSON.stringify(matcher)}`);
2623
+ const tgtNode = await findStableNodeByMatcher(
2624
+ targetElement._matcher,
2625
+ "longPressAndDrag"
2626
+ );
2627
+ if (!tgtNode)
2628
+ throw new Error(
2629
+ `target element not found: ${JSON.stringify(targetElement._matcher)}`
2630
+ );
2631
+ const startSootSimX = srcNode.absolutePosition.x + srcNode.layout.width * normalizedStartX;
2632
+ const startSootSimY = srcNode.absolutePosition.y + srcNode.layout.height * normalizedStartY;
2633
+ const endSootSimX = tgtNode.absolutePosition.x + tgtNode.layout.width * normalizedEndX;
2634
+ const endSootSimY = tgtNode.absolutePosition.y + tgtNode.layout.height * normalizedEndY;
2635
+ const startPage = await sootsimToPage(page, startSootSimX, startSootSimY);
2636
+ const endPage = await sootsimToPage(page, endSootSimX, endSootSimY);
2637
+ const steps = speed === "slow" ? 20 : 10;
2638
+ await page.mouse.move(startPage.x, startPage.y);
2639
+ await page.mouse.down();
2640
+ await page.waitForTimeout(Math.max(duration, holdDuration));
2641
+ const dx = (endPage.x - startPage.x) / steps;
2642
+ const dy = (endPage.y - startPage.y) / steps;
2643
+ for (let i = 1; i <= steps; i++) {
2644
+ await page.mouse.move(startPage.x + dx * i, startPage.y + dy * i);
2645
+ await page.waitForTimeout(speed === "slow" ? 30 : 10);
2646
+ }
2647
+ await page.mouse.up();
2648
+ await page.waitForTimeout(50);
2649
+ },
2650
+ async typeText(text) {
2651
+ const page = getPage();
2652
+ await focusElementForTextEntry(el);
2653
+ await page.keyboard.type(text);
2654
+ },
2655
+ async replaceText(text) {
2656
+ const page = getPage();
2657
+ const node = await findNodeByMatcher(matcher);
2658
+ if (matcher.type === "id" && typeof node?.nodeId === "number" && node.nodeId >= 1073741824) {
2659
+ const replaced = await page.evaluate(
2660
+ async ({ id, value: value2 }) => {
2661
+ const mainShell = window.SootSim?.bridges?.mainShell;
2662
+ if (typeof mainShell?.callTestBridge !== "function") return false;
2663
+ return mainShell.callTestBridge("replaceTextByTestId", id, value2);
2664
+ },
2665
+ { id: matcher.value, value: text }
2666
+ );
2667
+ if (replaced === true) return;
2668
+ }
2669
+ await focusElementForTextEntry(el);
2670
+ const value = typeof node?.text === "string" ? node.text : "";
2671
+ for (let i = 0; i < value.length; i++) {
2672
+ await page.keyboard.press("Backspace");
2673
+ }
2674
+ await page.keyboard.type(text);
2675
+ },
2676
+ async clearText() {
2677
+ const page = getPage();
2678
+ await focusElementForTextEntry(el);
2679
+ const node = await findNodeByMatcher(matcher);
2680
+ const value = typeof node?.text === "string" ? node.text : "";
2681
+ for (let i = 0; i < value.length; i++) {
2682
+ await page.keyboard.press("Backspace");
2683
+ }
2684
+ },
2685
+ async scroll(pixels, direction, startPositionX, startPositionY) {
2686
+ const node = await findNodeByMatcher(matcher);
2687
+ if (!node)
2688
+ throw new Error(`element not found for scroll: ${JSON.stringify(matcher)}`);
2689
+ const page = getPage();
2690
+ await dragScrollNode(page, node, pixels, direction, startPositionX, startPositionY);
2691
+ },
2692
+ async scrollTo(edge, startPositionX, startPositionY) {
2693
+ const node = await findNodeByMatcher(matcher);
2694
+ if (!node)
2695
+ throw new Error(`element not found for scrollTo: ${JSON.stringify(matcher)}`);
2696
+ const targetId = typeof node.testID === "string" && node.testID ? node.testID : typeof node.id === "string" && node.id ? node.id : null;
2697
+ if (!targetId) {
2698
+ throw new Error(
2699
+ `scrollTo requires an id-backed scroll view: ${JSON.stringify(matcher)}`
2700
+ );
2701
+ }
2702
+ const normalizedStartX = resolveScrollStartPosition(
2703
+ startPositionX,
2704
+ "scrollTo startPositionX"
2705
+ );
2706
+ const normalizedStartY = resolveScrollStartPosition(
2707
+ startPositionY,
2708
+ "scrollTo startPositionY"
2709
+ );
2710
+ const hitX = node.absolutePosition.x + node.layout.width * normalizedStartX;
2711
+ const hitY = node.absolutePosition.y + node.layout.height * normalizedStartY;
2712
+ const page = getPage();
2713
+ const result = await page.evaluate(
2714
+ async ({ edge: edge2, hitX: hitX2, hitY: hitY2, targetId: targetId2 }) => {
2715
+ const bridge = window.__sootsimTest;
2716
+ if (!bridge?.getScrollStateAt || !bridge.scrollTo) {
2717
+ return { ok: false, reason: "scroll bridge unavailable" };
2718
+ }
2719
+ const state = await bridge.getScrollStateAt(hitX2, hitY2);
2720
+ if (!state) return { ok: false, reason: "scroll state not found" };
2721
+ const maxOffset = state.maxOffset && typeof state.maxOffset === "object" ? state.maxOffset : null;
2722
+ const offset = state.offset && typeof state.offset === "object" ? state.offset : null;
2723
+ const maxX = typeof state.maxOffsetX === "number" ? state.maxOffsetX : maxOffset?.x ?? 0;
2724
+ const maxY = typeof state.maxOffsetY === "number" ? state.maxOffsetY : maxOffset?.y ?? 0;
2725
+ const currentX = typeof state.offsetX === "number" ? state.offsetX : offset?.x ?? 0;
2726
+ const currentY = typeof state.offsetY === "number" ? state.offsetY : offset?.y ?? 0;
2727
+ const x = edge2 === "left" ? 0 : edge2 === "right" ? maxX : currentX;
2728
+ const y = edge2 === "top" ? 0 : edge2 === "bottom" ? maxY : currentY;
2729
+ return bridge.scrollTo(targetId2, x, y, false);
2730
+ },
2731
+ { edge, hitX, hitY, targetId }
2732
+ );
2733
+ if (!result?.ok) {
2734
+ throw new Error(
2735
+ `scrollTo(${edge}) failed for ${targetId}: ${result?.reason ?? "unknown error"}`
2736
+ );
2737
+ }
2738
+ await page.evaluate(
2739
+ () => new Promise((resolve) => {
2740
+ requestAnimationFrame(() => resolve());
2741
+ })
2742
+ );
2743
+ if (_synchronizationEnabled) {
2744
+ await waitForSootsimIdle(page);
2745
+ }
2746
+ },
2747
+ async swipe(direction, speed = "fast", percentage = 0.75, normalizedStartingPointX = 0.5, normalizedStartingPointY = 0.5) {
2748
+ const node = await findStableNodeByMatcher(matcher, "swipe");
2749
+ if (!node)
2750
+ throw new Error(`element not found for swipe: ${JSON.stringify(matcher)}`);
2751
+ const page = getPage();
2752
+ const { absolutePosition, layout } = node;
2753
+ const startSootSimX = absolutePosition.x + layout.width * normalizedStartingPointX;
2754
+ const startSootSimY = absolutePosition.y + layout.height * normalizedStartingPointY;
2755
+ let endSootSimX = startSootSimX;
2756
+ let endSootSimY = startSootSimY;
2757
+ const viewport = await readSootsimViewport(page);
2758
+ const dist = direction === "up" || direction === "down" ? viewport.height * percentage : viewport.width * percentage;
2759
+ switch (direction) {
2760
+ case "up":
2761
+ endSootSimY -= dist;
2762
+ break;
2763
+ case "down":
2764
+ endSootSimY += dist;
2765
+ break;
2766
+ case "left":
2767
+ endSootSimX -= dist;
2768
+ break;
2769
+ case "right":
2770
+ endSootSimX += dist;
2771
+ break;
2772
+ }
2773
+ const steps = speed === "slow" ? 40 : 20;
2774
+ const stepDelay = 1e3 / 60;
2775
+ await page.evaluate(
2776
+ async ({ fromX, fromY, toX, toY, steps: steps2, stepMs }) => {
2777
+ const drag = window.__sootsimInteract?.drag;
2778
+ if (typeof drag !== "function") {
2779
+ throw new Error("sootsim interact bridge is not installed");
2780
+ }
2781
+ await drag(fromX, fromY, toX, toY, steps2, stepMs);
2782
+ },
2783
+ {
2784
+ fromX: startSootSimX,
2785
+ fromY: startSootSimY,
2786
+ toX: endSootSimX,
2787
+ toY: endSootSimY,
2788
+ steps,
2789
+ stepMs: stepDelay
2790
+ }
2791
+ );
2792
+ await page.waitForTimeout(100);
2793
+ },
2794
+ async setColumnToValue(column, value) {
2795
+ if (!Number.isInteger(column) || column < 0) {
2796
+ throw new Error(`picker column must be a non-negative integer, got ${column}`);
2797
+ }
2798
+ if (typeof value !== "string") {
2799
+ throw new Error(`picker value must be a string, got ${typeof value}`);
2800
+ }
2801
+ const picker = await findStableNodeByMatcher(matcher, "setColumnToValue");
2802
+ if (!picker) {
2803
+ throw new Error(
2804
+ `element not found for setColumnToValue: ${JSON.stringify(matcher)}`
2805
+ );
2806
+ }
2807
+ const page = getPage();
2808
+ const candidates = await page.evaluate(
2809
+ async ({ picker: picker2, column: column2, value: value2 }) => {
2810
+ const result = await window.__sootsimTest?.queryTextCandidates?.({
2811
+ query: value2,
2812
+ exact: true
2813
+ });
2814
+ if (!result || !Array.isArray(result.candidates)) return [];
2815
+ const left = picker2.absolutePosition.x;
2816
+ const top = picker2.absolutePosition.y;
2817
+ const right = left + picker2.layout.width;
2818
+ const bottom = top + picker2.layout.height;
2819
+ return result.candidates.filter((candidate) => candidate.ancestorPickerColumnIndices.includes(column2)).map((candidate) => candidate.info).filter((candidate) => {
2820
+ const x = candidate.absolutePosition?.x;
2821
+ const y = candidate.absolutePosition?.y;
2822
+ const width = candidate.layout?.width;
2823
+ const height = candidate.layout?.height;
2824
+ if (typeof x !== "number" || typeof y !== "number" || typeof width !== "number" || typeof height !== "number") {
2825
+ return false;
2826
+ }
2827
+ const centerX = x + width / 2;
2828
+ const centerY = y + height / 2;
2829
+ return centerX >= left && centerX <= right && centerY >= top && centerY <= bottom;
2830
+ });
2831
+ },
2832
+ { picker, column, value }
2833
+ );
2834
+ if (candidates.length !== 1) {
2835
+ throw new Error(
2836
+ `setColumnToValue(${column}, ${JSON.stringify(value)}) found ${candidates.length} matching rows inside ${JSON.stringify(matcher)}`
2837
+ );
2838
+ }
2839
+ const target = candidates[0];
2840
+ const tapTarget = await page.evaluate(
2841
+ async (nodeId) => window.__sootsimTest?.resolveTapTarget(nodeId),
2842
+ target.nodeId
2843
+ );
2844
+ if (!tapTarget) {
2845
+ throw new Error(
2846
+ `setColumnToValue(${column}, ${JSON.stringify(value)}) could not resolve its picker row`
2847
+ );
2848
+ }
2849
+ await dispatchTapAtSootsimPoint(
2850
+ page,
2851
+ tapTarget.target.absolutePosition.x + tapTarget.target.layout.width / 2,
2852
+ tapTarget.target.absolutePosition.y + tapTarget.target.layout.height / 2
2853
+ );
2854
+ },
2855
+ async pinch(scale, speed = "fast", angle = 0) {
2856
+ const page = getPage();
2857
+ const node = await findStableNodeByMatcher(matcher, "pinch");
2858
+ if (!node)
2859
+ throw new Error(`element not found for pinch: ${JSON.stringify(matcher)}`);
2860
+ if (!node.nativePinchEnabled) {
2861
+ await waitForSootsimGestureHandler(page, node.nodeId, "PinchGestureHandler");
2862
+ }
2863
+ const startSpread = Math.min(
2864
+ 60,
2865
+ Math.max(12, Math.min(node.layout.width, node.layout.height) * 0.25)
2866
+ );
2867
+ const endSpread = startSpread * Math.max(scale, 0.05);
2868
+ const cos = Math.cos(angle);
2869
+ const sin = Math.sin(angle);
2870
+ const offset = (d) => ({ dx: d * cos, dy: d * sin });
2871
+ const a0 = offset(-startSpread);
2872
+ const b0 = offset(startSpread);
2873
+ const a1 = offset(-endSpread);
2874
+ const b1 = offset(endSpread);
2875
+ await dispatchTwoFingerGesture(
2876
+ page,
2877
+ await nodeOffsetToPage(page, node, a0.dx, a0.dy),
2878
+ await nodeOffsetToPage(page, node, b0.dx, b0.dy),
2879
+ await nodeOffsetToPage(page, node, a1.dx, a1.dy),
2880
+ await nodeOffsetToPage(page, node, b1.dx, b1.dy),
2881
+ { steps: speed === "fast" ? 4 : 12, stepDelayMs: speed === "fast" ? 18 : 24 }
2882
+ );
2883
+ },
2884
+ async rotate(radians, speed = "fast") {
2885
+ const page = getPage();
2886
+ const node = await findStableNodeByMatcher(matcher, "rotate");
2887
+ if (!node)
2888
+ throw new Error(`element not found for rotate: ${JSON.stringify(matcher)}`);
2889
+ await waitForSootsimGestureHandler(page, node.nodeId, "RotationGestureHandler");
2890
+ const radius = Math.min(
2891
+ 60,
2892
+ Math.max(12, Math.min(node.layout.width, node.layout.height) * 0.25)
2893
+ );
2894
+ const startA = await nodeOffsetToPage(page, node, -radius, 0);
2895
+ const startB = await nodeOffsetToPage(page, node, radius, 0);
2896
+ const cos = Math.cos(radians);
2897
+ const sin = Math.sin(radians);
2898
+ const endA = await nodeOffsetToPage(page, node, -radius * cos, -radius * sin);
2899
+ const endB = await nodeOffsetToPage(page, node, radius * cos, radius * sin);
2900
+ await dispatchTwoFingerGesture(page, startA, startB, endA, endB, {
2901
+ steps: speed === "fast" ? 10 : 20,
2902
+ stepDelayMs: speed === "fast" ? 14 : 24
2903
+ });
2904
+ },
2905
+ async takeScreenshot(name) {
2906
+ const node = await findNodeByMatcher(matcher);
2907
+ if (!node)
2908
+ throw new Error(
2909
+ `element not found for takeScreenshot: ${JSON.stringify(matcher)}`
2910
+ );
2911
+ const box = node.frame ?? {
2912
+ x: node.absolutePosition.x,
2913
+ y: node.absolutePosition.y,
2914
+ width: node.layout.width,
2915
+ height: node.layout.height
2916
+ };
2917
+ return writeSootsimScreenshot(name, {
2918
+ crop: {
2919
+ x: box.x,
2920
+ y: box.y,
2921
+ w: box.width,
2922
+ h: box.height
2923
+ }
2924
+ });
2925
+ },
2926
+ async getAttributes() {
2927
+ const node = await findNodeByMatcher(matcher);
2928
+ if (!node)
2929
+ throw new Error(`element not found for getAttributes: ${JSON.stringify(matcher)}`);
2930
+ return {
2931
+ text: node.text || "",
2932
+ label: node.accessibilityLabel || node.text || "",
2933
+ identifier: node.testID || node.id || "",
2934
+ visible: node.layout.width > 0 && node.layout.height > 0,
2935
+ enabled: !node.accessibilityState?.disabled,
2936
+ ...node
2937
+ };
2938
+ },
2939
+ atIndex(index) {
2940
+ return createSootSimElement({ ...matcher, index });
2941
+ }
2942
+ };
2943
+ return el;
2944
+ }
2945
+ function element(matcher) {
2946
+ return createSootSimElement(matcher);
2947
+ }
2948
+ var device = {
2949
+ _currentUrl: "",
2950
+ _platform: DETOX_PLATFORM,
2951
+ async launchApp(opts) {
2952
+ if (_browserCompromised) {
2953
+ throw new Error(`sootsim browser is compromised: ${_browserCompromised}`);
2954
+ }
2955
+ if (_page && opts?.newInstance === false && !opts.delete && !opts.recordVideoDir && _suspendedAppId) {
2956
+ const page = _page;
2957
+ const appId = _suspendedAppId;
2958
+ await page.evaluate(
2959
+ (id) => window.SootSim?.bridges?.mainShell?.launchApp?.(id),
2960
+ appId
2961
+ );
2962
+ await pollUntil(
2963
+ () => page.evaluate(async (id) => {
2964
+ const state = await window.SootSim?.bridges?.mainShell?.getState?.();
2965
+ return state?.state === "app" && state.activeApp === id && state.switcherPhase === "idle";
2966
+ }, appId),
2967
+ Boolean,
2968
+ {
2969
+ timeoutMs: 1e4,
2970
+ label: `SootSim app ${appId} to resume`
2971
+ }
2972
+ );
2973
+ _suspendedAppId = null;
2974
+ await waitForSootsimTree(page, 1e4);
2975
+ return;
2976
+ }
2977
+ if (!_browser) {
2978
+ const launched = await launchReapedChromeScoped(
2979
+ (options) => import_playwright.chromium.launch(options),
2980
+ { headless: true, ignoreDefaultArgs: ["--disable-popup-blocking"] }
2981
+ );
2982
+ _browser = launched.browser;
2983
+ _reapBrowserNow = launched.reapNow;
2984
+ browserIdentity = ++nextBrowserIdentity;
2985
+ }
2986
+ if (!_page || opts?.newInstance || opts?.delete || opts?.recordVideoDir) {
2987
+ await closeContext();
2988
+ const contextOpts = {
2989
+ hasTouch: true,
2990
+ viewport: { width: 500, height: 900 }
2991
+ };
2992
+ if (opts?.recordVideoDir) {
2993
+ fs.mkdirSync(opts.recordVideoDir, { recursive: true });
2994
+ contextOpts.recordVideo = {
2995
+ dir: opts.recordVideoDir,
2996
+ size: { width: 500, height: 900 }
2997
+ };
2998
+ }
2999
+ const newContextIdentity = ++nextContextIdentity;
3000
+ try {
3001
+ _context = await withLifecycleTimeout(
3002
+ "_browser.newContext()",
3003
+ newContextIdentity,
3004
+ () => _browser.newContext(contextOpts)
3005
+ );
3006
+ } catch (error) {
3007
+ _browserCompromised = error instanceof Error ? error.message : String(error);
3008
+ reapOwnedBrowser();
3009
+ throw error;
3010
+ }
3011
+ await _context.exposeBinding("__sootsimHostCapture", async (source, value) => {
3012
+ const request = readBrowserCompositeCaptureRequest(value);
3013
+ const session = await source.page.context().newCDPSession(source.page);
3014
+ try {
3015
+ const result = await session.send("Page.captureScreenshot", {
3016
+ format: "png",
3017
+ fromSurface: true,
3018
+ captureBeyondViewport: true,
3019
+ clip: resolveBrowserCompositeCdpClip(
3020
+ request,
3021
+ await session.send("Page.getLayoutMetrics")
3022
+ )
3023
+ });
3024
+ return "data:image/png;base64," + result.data;
3025
+ } finally {
3026
+ await session.detach();
3027
+ }
3028
+ });
3029
+ try {
3030
+ _page = await withLifecycleTimeout(
3031
+ "_context.newPage()",
3032
+ newContextIdentity,
3033
+ () => _context.newPage()
3034
+ );
3035
+ } catch (error) {
3036
+ _browserCompromised = error instanceof Error ? error.message : String(error);
3037
+ reapOwnedBrowser();
3038
+ throw error;
3039
+ }
3040
+ contextIdentity = newContextIdentity;
3041
+ }
3042
+ const url = opts?.url || BASE_URL;
3043
+ device._currentUrl = url;
3044
+ await navigateSootsimPage(_page, url, {
3045
+ browserId: browserIdentity,
3046
+ contextId: contextIdentity,
3047
+ operation: "launchApp"
3048
+ });
3049
+ await waitForSootsimTree(_page, 3e4);
3050
+ await waitForSootsimSurfaceMetrics(_page, 3e4);
3051
+ await waitForSootsimShellReady(_page, 3e4);
3052
+ await waitForSootsimWorkletsIdle(_page, 3e3);
3053
+ device._lastReadyAtMs = Date.now();
3054
+ },
3055
+ _lastReadyAtMs: 0,
3056
+ async reloadReactNative() {
3057
+ const page = getPage();
3058
+ await page.reload({ waitUntil: "load", timeout: 3e4 });
3059
+ await waitForSootsimTree(page, 3e4);
3060
+ await waitForSootsimSurfaceMetrics(page, 3e4);
3061
+ await waitForSootsimShellReady(page, 3e4);
3062
+ await waitForSootsimWorkletsIdle(page, 3e3);
3063
+ },
3064
+ async terminateApp() {
3065
+ await closeBrowser();
3066
+ },
3067
+ async installApp() {
3068
+ },
3069
+ async uninstallApp() {
3070
+ },
3071
+ async openURL(url) {
3072
+ const page = getPage();
3073
+ await navigateSootsimPage(page, url.url, {
3074
+ browserId: browserIdentity,
3075
+ contextId: contextIdentity,
3076
+ operation: "openURL"
3077
+ });
3078
+ await waitForSootsimTree(page, 1e4);
3079
+ await waitForSootsimSurfaceMetrics(page, 1e4);
3080
+ await waitForSootsimShellReady(page, 1e4);
3081
+ await waitForSootsimWorkletsIdle(page, 3e3);
3082
+ },
3083
+ async takeScreenshot(name) {
3084
+ return writeSootsimScreenshot(name);
3085
+ },
3086
+ // single-shot variant for multi-stage animation captures. skips the
3087
+ // frame-stability poll so callers can sample mid-transition.
3088
+ async takeScreenshotFast(name) {
3089
+ ensureScreenshotDir();
3090
+ const screenshotPath = path.join(SCREENSHOT_DIR, `${name}.png`);
3091
+ fs.writeFileSync(screenshotPath, await captureSootsimPngFast());
3092
+ return screenshotPath;
3093
+ },
3094
+ // record the SootSim canvas via the engine's headless recorder
3095
+ // (window.__sootsimRecorder — @rnx/plugin-recording). this is the
3096
+ // SAME code path `rnx record video` drives, NOT the rail-button
3097
+ // path (SootSim.bridges.startRecording wires the dialog state machine
3098
+ // and never stashes the resulting blob into __sootsimRecorder.lastBlob,
3099
+ // which is what getBlobBase64 streams from).
3100
+ //
3101
+ // unlike playwright's page-level recordVideo, the headless recorder
3102
+ // captures ONLY the device canvas (composited shell + tenant surfaces)
3103
+ // — no browser chrome, no menu bar, no device frame. uses the EXISTING
3104
+ // page/context so per-test state is preserved across capture.
3105
+ async startBridgeRecording(opts) {
3106
+ const page = getPage();
3107
+ const startOpts = {
3108
+ format: "webm",
3109
+ fps: opts?.fps ?? 60,
3110
+ layers: opts?.layers ?? "tenant",
3111
+ durationMs: opts?.durationMs ?? 5e3
3112
+ };
3113
+ const result = await page.evaluate(async (startOpts2) => {
3114
+ const rec = window.__sootsimRecorder;
3115
+ const start = rec?.start;
3116
+ if (typeof start !== "function") {
3117
+ return { ok: false, error: "__sootsimRecorder.start not installed" };
3118
+ }
3119
+ return start(startOpts2);
3120
+ }, startOpts);
3121
+ if (!result.ok) {
3122
+ throw new Error(`startBridgeRecording failed: ${result.error ?? "unknown"}`);
3123
+ }
3124
+ },
3125
+ // stop the headless recorder, stream the resulting blob in chunks (same
3126
+ // protocol as `rnx record`), and write the webm to outPath.
3127
+ async stopAndSaveBridgeRecording(outPath) {
3128
+ const page = getPage();
3129
+ const stopResult = await page.evaluate(async () => {
3130
+ const rec = window.__sootsimRecorder;
3131
+ const stop = rec?.stop;
3132
+ if (typeof stop !== "function") {
3133
+ return { ok: false, error: "__sootsimRecorder.stop not installed" };
3134
+ }
3135
+ return stop();
3136
+ });
3137
+ if (!stopResult.ok) {
3138
+ throw new Error(`stopBridgeRecording failed: ${stopResult.error ?? "unknown"}`);
3139
+ }
3140
+ if (!stopResult.size) {
3141
+ throw new Error("stopBridgeRecording: recorder returned empty blob");
3142
+ }
3143
+ const chunks = [];
3144
+ let offset = 0;
3145
+ const CHUNK = 2 * 1024 * 1024;
3146
+ while (true) {
3147
+ const result = await page.evaluate(
3148
+ ({ offset: offset2, chunk }) => {
3149
+ const rec = window.__sootsimRecorder;
3150
+ const get = rec?.getBlobBase64;
3151
+ if (typeof get !== "function") return null;
3152
+ return get({ offset: offset2, chunk });
3153
+ },
3154
+ { offset, chunk: CHUNK }
3155
+ );
3156
+ if (!result) throw new Error("rnx recorder produced no blob");
3157
+ chunks.push(Buffer.from(result.data, "base64"));
3158
+ offset = result.offset;
3159
+ if (result.done) break;
3160
+ }
3161
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
3162
+ fs.writeFileSync(outPath, Buffer.concat(chunks));
3163
+ this._lastRecordingPath = outPath;
3164
+ return outPath;
3165
+ },
3166
+ _lastRecordingPath: "",
3167
+ // park every render clock before the interaction, detect the first changed
3168
+ // stepped frame inside the named component, then advance exact 60fps frames
3169
+ // to each requested offset. native extraction also chooses the first frame
3170
+ // at or after an offset, so both sides use the same rounding convention.
3171
+ async takeScreenshotStagesFromVisibleChangeFast(namePrefix, stages, options) {
3172
+ const page = getPage();
3173
+ const token = `motion-${process.pid}-${Date.now()}-${Math.random()}`;
3174
+ const settleMs = options.settleMs ?? 0;
3175
+ if (!Number.isFinite(settleMs) || settleMs < 0) {
3176
+ throw new Error("rnx motion capture needs a non-negative settleMs");
3177
+ }
3178
+ await page.evaluate(import_motion_change.measureMotionChange, {
3179
+ definition: import_motion_change.MOTION_CHANGE_DEFINITION,
3180
+ installGlobal: true
3181
+ });
3182
+ const capturePromise = page.evaluate(
3183
+ async (input) => {
3184
+ const captureWindow = window;
3185
+ const live = captureWindow.SootSim?.bridges?.liveComposite;
3186
+ const lockstep = captureWindow.SootSim?.bridges?.lockstepCapture;
3187
+ if (!live || typeof live.captureFresh !== "function") {
3188
+ throw new Error("rnx live composite bridge is not installed");
3189
+ }
3190
+ if (!lockstep || typeof lockstep.begin !== "function") {
3191
+ throw new Error("SootSim lockstep capture bridge is not installed");
3192
+ }
3193
+ const registry = captureWindow.__sootsimVisibleChangeCaptures ?? (captureWindow.__sootsimVisibleChangeCaptures = {});
3194
+ const state = {
3195
+ cancelled: false,
3196
+ ready: false,
3197
+ triggerCompleted: false
3198
+ };
3199
+ registry[input.token] = state;
3200
+ const measureChange = captureWindow.__sootsimMeasureMotionChange;
3201
+ if (!measureChange) {
3202
+ throw new Error("rnx motion change definition is not installed");
3203
+ }
3204
+ const captureBitmap = async () => {
3205
+ const { canvas } = await live.captureFresh();
3206
+ if (!canvas) throw new Error("rnx live composite produced no canvas");
3207
+ return createImageBitmap(canvas);
3208
+ };
3209
+ const boundsFor = (bitmap) => {
3210
+ const scale = input.rootFrame && input.rootFrame.width > 0 ? bitmap.width / input.rootFrame.width : 1;
3211
+ return {
3212
+ height: input.frame ? Math.max(
3213
+ 1,
3214
+ Math.min(
3215
+ bitmap.height,
3216
+ Math.ceil((input.frame.y + input.frame.height) * scale)
3217
+ ) - Math.max(0, Math.floor(input.frame.y * scale))
3218
+ ) : bitmap.height,
3219
+ left: input.frame ? Math.max(0, Math.floor(input.frame.x * scale)) : 0,
3220
+ top: input.frame ? Math.max(0, Math.floor(input.frame.y * scale)) : 0,
3221
+ width: input.frame ? Math.max(
3222
+ 1,
3223
+ Math.min(
3224
+ bitmap.width,
3225
+ Math.ceil((input.frame.x + input.frame.width) * scale)
3226
+ ) - Math.max(0, Math.floor(input.frame.x * scale))
3227
+ ) : bitmap.width
3228
+ };
3229
+ };
3230
+ const regionPixels = (bitmap) => {
3231
+ const bounds = boundsFor(bitmap);
3232
+ const canvas = document.createElement("canvas");
3233
+ canvas.width = bounds.width;
3234
+ canvas.height = bounds.height;
3235
+ const context = canvas.getContext("2d", { willReadFrequently: true });
3236
+ if (!context) throw new Error("rnx motion detector needs a 2d canvas");
3237
+ context.drawImage(
3238
+ bitmap,
3239
+ bounds.left,
3240
+ bounds.top,
3241
+ bounds.width,
3242
+ bounds.height,
3243
+ 0,
3244
+ 0,
3245
+ bounds.width,
3246
+ bounds.height
3247
+ );
3248
+ return context.getImageData(0, 0, bounds.width, bounds.height).data;
3249
+ };
3250
+ const encode = (snapshot) => {
3251
+ const canvas = document.createElement("canvas");
3252
+ canvas.width = snapshot.bitmap.width;
3253
+ canvas.height = snapshot.bitmap.height;
3254
+ const context = canvas.getContext("2d");
3255
+ if (!context) throw new Error("rnx motion capture needs a 2d canvas");
3256
+ context.drawImage(snapshot.bitmap, 0, 0);
3257
+ snapshot.bitmap.close();
3258
+ return {
3259
+ actualMs: snapshot.actualMs,
3260
+ dataUrl: canvas.toDataURL("image/png"),
3261
+ label: snapshot.label,
3262
+ offsetMs: snapshot.offsetMs,
3263
+ frameIndex: snapshot.frameIndex,
3264
+ virtualTimestampMs: snapshot.virtualTimestampMs
3265
+ };
3266
+ };
3267
+ const interact = captureWindow.SootSim?.bridges?.interact;
3268
+ if (input.hold && (!interact || typeof interact.touchDown !== "function")) {
3269
+ throw new Error("rnx interact bridge is not installed");
3270
+ }
3271
+ const frameMs = 1e3 / 60;
3272
+ const controller = await lockstep.begin(60);
3273
+ const moves = input.hold?.moves ?? [];
3274
+ const holdState = {
3275
+ nextMove: 0,
3276
+ pressFrameIndex: null,
3277
+ released: false,
3278
+ x: input.hold?.x ?? 0,
3279
+ y: input.hold?.y ?? 0
3280
+ };
3281
+ const holdEventFrameIndex = (afterMs) => (holdState.pressFrameIndex ?? 0) + Math.ceil(afterMs / frameMs);
3282
+ const nextHoldEventFrameIndex = () => {
3283
+ if (!input.hold || holdState.released || holdState.pressFrameIndex === null) {
3284
+ return null;
3285
+ }
3286
+ const move = moves[holdState.nextMove];
3287
+ return holdEventFrameIndex(move ? move.afterMs : input.hold.durationMs);
3288
+ };
3289
+ const advanceHold = async (receipt) => {
3290
+ if (!input.hold || !interact || holdState.released) return;
3291
+ if (holdState.pressFrameIndex === null) {
3292
+ holdState.pressFrameIndex = receipt.frameIndex - 1;
3293
+ }
3294
+ for (let move = moves[holdState.nextMove]; move && receipt.frameIndex >= holdEventFrameIndex(move.afterMs); move = moves[holdState.nextMove]) {
3295
+ holdState.nextMove++;
3296
+ holdState.x = move.x;
3297
+ holdState.y = move.y;
3298
+ await interact.touchMove(move.x, move.y);
3299
+ }
3300
+ if (receipt.frameIndex < holdEventFrameIndex(input.hold.durationMs)) return;
3301
+ holdState.released = true;
3302
+ await interact.touchUp(holdState.x, holdState.y);
3303
+ };
3304
+ try {
3305
+ const baselineBitmap = await captureBitmap();
3306
+ const baseline = regionPixels(baselineBitmap);
3307
+ baselineBitmap.close();
3308
+ state.ready = true;
3309
+ if (input.hold && interact) {
3310
+ const hit = await interact.touchDown(input.hold.x, input.hold.y);
3311
+ if (!hit) {
3312
+ throw new Error(`rnx motion hold missed at ${input.hold.x},${input.hold.y}`);
3313
+ }
3314
+ } else {
3315
+ while (!state.triggerCompleted) {
3316
+ if (state.cancelled) throw new Error("rnx motion capture was cancelled");
3317
+ await new Promise((resolve) => setTimeout(resolve, 0));
3318
+ }
3319
+ }
3320
+ let visibleStart = null;
3321
+ let detectedMeasurement = null;
3322
+ const detectionFrameLimit = Math.ceil(input.detectionTimeoutMs * 60 / 1e3);
3323
+ for (let frame = 0; frame < detectionFrameLimit; frame++) {
3324
+ if (state.cancelled) throw new Error("rnx motion capture was cancelled");
3325
+ const receipt = await controller.stepFrame();
3326
+ await advanceHold(receipt);
3327
+ const candidateBitmap = await captureBitmap();
3328
+ const bounds = boundsFor(candidateBitmap);
3329
+ const measurement = measureChange({
3330
+ baseline,
3331
+ candidate: regionPixels(candidateBitmap),
3332
+ definition: input.motionChangeDefinition,
3333
+ height: bounds.height,
3334
+ label: "SootSim visible-change detector",
3335
+ width: bounds.width
3336
+ });
3337
+ candidateBitmap.close();
3338
+ if (measurement.passedMinimum) {
3339
+ visibleStart = receipt;
3340
+ detectedMeasurement = measurement;
3341
+ break;
3342
+ }
3343
+ }
3344
+ if (!visibleStart || !detectedMeasurement) {
3345
+ throw new Error(
3346
+ `rnx motion had no visible change before ${input.detectionTimeoutMs}ms`
3347
+ );
3348
+ }
3349
+ const snapshots = [];
3350
+ for (const stage of input.stages) {
3351
+ if (state.cancelled) throw new Error("rnx motion capture was cancelled");
3352
+ const stageFrameIndex = visibleStart.frameIndex + Math.ceil(stage.offsetMs / frameMs);
3353
+ for (let eventFrameIndex = nextHoldEventFrameIndex(); eventFrameIndex !== null && stageFrameIndex > eventFrameIndex; eventFrameIndex = nextHoldEventFrameIndex()) {
3354
+ await advanceHold(await controller.stepFrame());
3355
+ }
3356
+ const receipt = await controller.stepToOffset(visibleStart, stage.offsetMs);
3357
+ await advanceHold(receipt);
3358
+ snapshots.push({
3359
+ actualMs: receipt.virtualTimestampMs - visibleStart.virtualTimestampMs,
3360
+ bitmap: await captureBitmap(),
3361
+ frameIndex: receipt.frameIndex,
3362
+ label: stage.label,
3363
+ offsetMs: stage.offsetMs,
3364
+ virtualTimestampMs: receipt.virtualTimestampMs
3365
+ });
3366
+ }
3367
+ if (input.settleMs > 0) {
3368
+ const maximumOffsetMs = input.stages.reduce(
3369
+ (maximum, stage) => Math.max(maximum, stage.offsetMs),
3370
+ 0
3371
+ );
3372
+ await controller.stepToOffset(visibleStart, maximumOffsetMs + input.settleMs);
3373
+ }
3374
+ return {
3375
+ frames: snapshots.map(encode),
3376
+ visibleChangeDetection: {
3377
+ actualMs: visibleStart.frameIndex * 1e3 / 60,
3378
+ changedPixelPercent: detectedMeasurement.changePercent,
3379
+ changedPixels: detectedMeasurement.changePixels,
3380
+ channelThreshold: input.motionChangeDefinition.channelThreshold,
3381
+ minimumChangePercent: input.motionChangeDefinition.minimumChangePercent,
3382
+ totalPixels: detectedMeasurement.totalPixels,
3383
+ frameIndex: visibleStart.frameIndex,
3384
+ virtualTimestampMs: visibleStart.virtualTimestampMs
3385
+ }
3386
+ };
3387
+ } finally {
3388
+ controller.end();
3389
+ delete registry[input.token];
3390
+ }
3391
+ },
3392
+ {
3393
+ detectionTimeoutMs: options.detectionTimeoutMs ?? 3e3,
3394
+ frame: options.frame ?? null,
3395
+ hold: options.hold ?? null,
3396
+ motionChangeDefinition: import_motion_change.MOTION_CHANGE_DEFINITION,
3397
+ rootFrame: options.rootFrame ?? null,
3398
+ settleMs,
3399
+ stages,
3400
+ token
3401
+ }
3402
+ );
3403
+ await page.waitForFunction(
3404
+ (captureToken) => {
3405
+ const registry = window.__sootsimVisibleChangeCaptures;
3406
+ return registry?.[captureToken]?.ready === true;
3407
+ },
3408
+ token,
3409
+ { timeout: 5e3 }
3410
+ );
3411
+ const triggerStartMs = Date.now();
3412
+ let triggerCompletedMs = 0;
3413
+ try {
3414
+ if (!options.hold) {
3415
+ if (typeof options.trigger !== "function") {
3416
+ throw new Error("rnx motion capture needs a trigger or a hold");
3417
+ }
3418
+ await Promise.resolve(options.trigger());
3419
+ }
3420
+ triggerCompletedMs = Date.now();
3421
+ await page.evaluate((captureToken) => {
3422
+ const registry = window.__sootsimVisibleChangeCaptures;
3423
+ const capture2 = registry?.[captureToken];
3424
+ if (!capture2) throw new Error("rnx motion capture disarmed before trigger");
3425
+ capture2.triggerCompleted = true;
3426
+ }, token);
3427
+ const capture = await capturePromise;
3428
+ return {
3429
+ frames: writeFastScreenshotFrames(namePrefix, capture.frames),
3430
+ triggerCompletedMs,
3431
+ triggerStartMs,
3432
+ visibleChangeDetection: {
3433
+ ...capture.visibleChangeDetection,
3434
+ frame: options.frame ?? null,
3435
+ rootFrame: options.rootFrame ?? null
3436
+ }
3437
+ };
3438
+ } catch (error) {
3439
+ await page.evaluate((captureToken) => {
3440
+ const registry = window.__sootsimVisibleChangeCaptures;
3441
+ const capture = registry?.[captureToken];
3442
+ if (capture) capture.cancelled = true;
3443
+ }, token).catch(() => {
3444
+ });
3445
+ await capturePromise.catch(() => {
3446
+ });
3447
+ throw error;
3448
+ }
3449
+ },
3450
+ // FAST multi-stage capture for sub-second animation diffs. each stage
3451
+ // schedules a setTimeout at its requested offsetMs and at that moment
3452
+ // SYNCHRONOUSLY snapshots the engine's `liveComposite` host-side canvas
3453
+ // (drawImage from the worker-transferred home + overlay canvases). PNG
3454
+ // encoding is queued after all snapshots land — encoding stalls don't
3455
+ // contaminate the animation window.
3456
+ //
3457
+ // unlike `takeScreenshotStages`, this does NOT go through screenshot()
3458
+ // (which calls forceRenderAll + composite + toDataURL serially at
3459
+ // 300-1300ms per call and blows the spring-open timing). the worker's
3460
+ // vsync-pumped rAF keeps the home canvas fresh; we just sample it.
3461
+ //
3462
+ // returns paths in stage order; actualMs reports when the snapshot
3463
+ // actually fired (typically within a few ms of offsetMs).
3464
+ async takeScreenshotStagesFast(namePrefix, stages, options) {
3465
+ const page = getPage();
3466
+ const token = `timed-${process.pid}-${Date.now()}-${Math.random()}`;
3467
+ const capturePromise = page.evaluate(
3468
+ async (input) => {
3469
+ const captureWindow = window;
3470
+ const screenshot = captureWindow.SootSim?.bridges?.screenshot;
3471
+ const live = captureWindow.SootSim?.bridges?.liveComposite;
3472
+ if (typeof screenshot !== "function") {
3473
+ throw new Error("rnx screenshot bridge is not installed");
3474
+ }
3475
+ const registry = captureWindow.__sootsimTimedCaptures ?? (captureWindow.__sootsimTimedCaptures = {});
3476
+ const state = {
3477
+ anchorPerformanceMs: null,
3478
+ anchorWallMs: null,
3479
+ cancelled: false,
3480
+ ready: false
3481
+ };
3482
+ registry[input.token] = state;
3483
+ if (!input.skipWarmup) {
3484
+ await screenshot({ format: "png" });
3485
+ }
3486
+ state.ready = true;
3487
+ while (state.anchorPerformanceMs === null || state.anchorWallMs === null) {
3488
+ if (state.cancelled) throw new Error("rnx timed capture was cancelled");
3489
+ await new Promise((resolve) => setTimeout(resolve, 0));
3490
+ }
3491
+ const captureAnchorMs = state.anchorWallMs;
3492
+ const captureAnchorPerformanceMs = state.anchorPerformanceMs;
3493
+ const snaps = await Promise.all(
3494
+ input.stages.map(
3495
+ (stage) => new Promise((resolve) => {
3496
+ const fire = async () => {
3497
+ const actualMs = performance.now() - captureAnchorPerformanceMs;
3498
+ const captureRequestedAtMs = Date.now();
3499
+ try {
3500
+ let dataUrl = null;
3501
+ if (live && typeof live.captureFresh === "function") {
3502
+ const { canvas } = await live.captureFresh();
3503
+ if (canvas) {
3504
+ const bmp = await createImageBitmap(canvas);
3505
+ const enc = document.createElement("canvas");
3506
+ enc.width = bmp.width;
3507
+ enc.height = bmp.height;
3508
+ const ctx = enc.getContext("2d");
3509
+ if (ctx) {
3510
+ ctx.drawImage(bmp, 0, 0);
3511
+ dataUrl = enc.toDataURL("image/png");
3512
+ }
3513
+ bmp.close();
3514
+ }
3515
+ }
3516
+ if (!dataUrl) {
3517
+ dataUrl = await screenshot({ format: "png" });
3518
+ }
3519
+ resolve({
3520
+ label: stage.label,
3521
+ offsetMs: stage.offsetMs,
3522
+ actualMs,
3523
+ captureRequestedAtMs,
3524
+ dataUrl
3525
+ });
3526
+ } catch {
3527
+ resolve({
3528
+ label: stage.label,
3529
+ offsetMs: stage.offsetMs,
3530
+ actualMs,
3531
+ captureRequestedAtMs,
3532
+ dataUrl: null
3533
+ });
3534
+ }
3535
+ };
3536
+ setTimeout(
3537
+ () => {
3538
+ void fire();
3539
+ },
3540
+ Math.max(
3541
+ 0,
3542
+ captureAnchorPerformanceMs + stage.offsetMs - performance.now()
3543
+ )
3544
+ );
3545
+ })
3546
+ )
3547
+ );
3548
+ const out = [];
3549
+ for (const snap of snaps) {
3550
+ if (!snap.dataUrl) continue;
3551
+ out.push({
3552
+ label: snap.label,
3553
+ offsetMs: snap.offsetMs,
3554
+ actualMs: snap.actualMs,
3555
+ captureRequestedAtMs: snap.captureRequestedAtMs,
3556
+ dataUrl: snap.dataUrl
3557
+ });
3558
+ }
3559
+ delete registry[input.token];
3560
+ return { captureAnchorMs, frames: out };
3561
+ },
3562
+ { stages, skipWarmup: options.skipWarmup === true, token }
3563
+ );
3564
+ await page.waitForFunction(
3565
+ (captureToken) => {
3566
+ const registry = window.__sootsimTimedCaptures;
3567
+ return registry?.[captureToken]?.ready === true;
3568
+ },
3569
+ token,
3570
+ { timeout: 5e3 }
3571
+ );
3572
+ const armCapture = (anchorMs) => page.evaluate(
3573
+ ({ anchorMs: anchorMs2, captureToken }) => {
3574
+ const registry = window.__sootsimTimedCaptures;
3575
+ const capture = registry?.[captureToken];
3576
+ if (!capture?.ready) throw new Error("rnx timed capture was not armed");
3577
+ const nowMs = Date.now();
3578
+ capture.anchorWallMs = anchorMs2 ?? nowMs;
3579
+ capture.anchorPerformanceMs = performance.now() - (nowMs - capture.anchorWallMs);
3580
+ return capture.anchorWallMs;
3581
+ },
3582
+ { anchorMs, captureToken: token }
3583
+ );
3584
+ let triggerStartMs = 0;
3585
+ let triggerCompletedMs = 0;
3586
+ try {
3587
+ let captureAnchorMs;
3588
+ if (options.anchorAfterTrigger === false) {
3589
+ captureAnchorMs = await armCapture();
3590
+ triggerStartMs = captureAnchorMs;
3591
+ const triggerPromise = Promise.resolve().then(options.trigger).then(() => {
3592
+ triggerCompletedMs = Date.now();
3593
+ });
3594
+ const [capture2] = await Promise.all([capturePromise, triggerPromise]);
3595
+ return {
3596
+ captureAnchorMs,
3597
+ frames: writeFastScreenshotFrames(namePrefix, capture2.frames),
3598
+ triggerCompletedMs,
3599
+ triggerStartMs
3600
+ };
3601
+ }
3602
+ triggerStartMs = Date.now();
3603
+ await Promise.resolve(options.trigger());
3604
+ triggerCompletedMs = Date.now();
3605
+ captureAnchorMs = await armCapture(triggerCompletedMs);
3606
+ const capture = await capturePromise;
3607
+ return {
3608
+ captureAnchorMs,
3609
+ frames: writeFastScreenshotFrames(namePrefix, capture.frames),
3610
+ triggerCompletedMs,
3611
+ triggerStartMs
3612
+ };
3613
+ } catch (error) {
3614
+ await page.evaluate((captureToken) => {
3615
+ const registry = window.__sootsimTimedCaptures;
3616
+ const capture = registry?.[captureToken];
3617
+ if (capture) capture.cancelled = true;
3618
+ }, token).catch(() => {
3619
+ });
3620
+ await capturePromise.catch(() => {
3621
+ });
3622
+ throw error;
3623
+ }
3624
+ },
3625
+ // batched multi-stage capture for animation diffs. all N captures happen
3626
+ // inside one page.evaluate round trip so browser-side timing controls the
3627
+ // offsets — sidesteps the ~300ms per-call bridge overhead that blows
3628
+ // sub-second animation windows. each stage gets a screenshot at the
3629
+ // requested offsetMs (from when the evaluate starts, i.e. right after
3630
+ // trigger() returns). returns absolute paths in stage order.
3631
+ async takeScreenshotStages(namePrefix, stages) {
3632
+ const page = getPage();
3633
+ const frames = await page.evaluate(
3634
+ async (input) => {
3635
+ const screenshot = window.SootSim?.bridges?.screenshot;
3636
+ if (typeof screenshot !== "function") {
3637
+ throw new Error("rnx screenshot bridge is not installed");
3638
+ }
3639
+ const start = Date.now();
3640
+ const out = [];
3641
+ for (const stage of input.stages) {
3642
+ const elapsed = Date.now() - start;
3643
+ const wait = Math.max(0, stage.offsetMs - elapsed);
3644
+ if (wait > 0) await new Promise((r) => setTimeout(r, wait));
3645
+ const actualMs = Date.now() - start;
3646
+ const dataUrl = await screenshot({ format: "png" });
3647
+ out.push({ label: stage.label, offsetMs: stage.offsetMs, actualMs, dataUrl });
3648
+ }
3649
+ return out;
3650
+ },
3651
+ { stages }
3652
+ );
3653
+ return writeFastScreenshotFrames(namePrefix, frames);
3654
+ },
3655
+ async shake() {
3656
+ },
3657
+ async setLocation(lat, lon) {
3658
+ },
3659
+ async setStatusBar(config) {
3660
+ await dispatchStatusBarOverride(config);
3661
+ },
3662
+ async setURLBlacklist(urls) {
3663
+ },
3664
+ async enableSynchronization() {
3665
+ _synchronizationEnabled = true;
3666
+ },
3667
+ async disableSynchronization() {
3668
+ _synchronizationEnabled = false;
3669
+ },
3670
+ getPlatform() {
3671
+ return device._platform;
3672
+ },
3673
+ async pressBack() {
3674
+ if (device._platform !== "android") return;
3675
+ const page = getPage();
3676
+ const dispatched = await page.evaluate(async () => {
3677
+ const goBack = window.SootSim?.bridges?.mainShell?.goBack;
3678
+ if (typeof goBack !== "function") return false;
3679
+ return await goBack() !== false;
3680
+ });
3681
+ if (!dispatched) throw new Error("sootsim Android back bridge is not installed");
3682
+ if (_synchronizationEnabled) {
3683
+ await waitForSootsimIdle(page);
3684
+ }
3685
+ },
3686
+ // a real browser key press. host.ts turns window keydown into the
3687
+ // `sootsim:hostKey` shell event that overlays listen to, so this drives the
3688
+ // same path a desktop user does for keys no device button covers (Escape).
3689
+ async pressKey(key) {
3690
+ const page = getPage();
3691
+ await page.keyboard.press(key);
3692
+ if (_synchronizationEnabled) {
3693
+ await waitForSootsimIdle(page);
3694
+ }
3695
+ },
3696
+ // raw coordinate tap, bypassing the element-based visibility/hit-test path.
3697
+ // mirrors detox 20.x device.tap({ x, y }) on real iOS — used when the
3698
+ // target is occluded by a native UIWindow (e.g. UIMenu's dim layer) so
3699
+ // `element(...).tap({x,y})` would fail Detox's visibility threshold.
3700
+ async tap(point) {
3701
+ if (point == null || typeof point === "boolean") return;
3702
+ const page = getPage();
3703
+ const { x, y } = point;
3704
+ await page.evaluate(
3705
+ async ({ x: tx, y: ty }) => {
3706
+ const interactTap = window.SootSim?.bridges?.interact?.tap;
3707
+ if (typeof interactTap === "function") {
3708
+ const result = await interactTap(tx, ty);
3709
+ if (result && (typeof result !== "object" || result.hit !== false)) return;
3710
+ }
3711
+ const tap = window.__sootsimTest?.tap;
3712
+ if (typeof tap !== "function") {
3713
+ throw new Error("sootsim tap bridge is not installed");
3714
+ }
3715
+ await tap(tx, ty);
3716
+ },
3717
+ { x, y }
3718
+ );
3719
+ await page.waitForTimeout(50);
3720
+ if (_synchronizationEnabled) {
3721
+ await waitForSootsimIdle(page);
3722
+ }
3723
+ },
3724
+ // window-point counterpart to element(...).longPress, for a target detox can
3725
+ // only address by coordinate (a native tab cell, a shell chrome control).
3726
+ async longPress(point, durationMs = 1e3) {
3727
+ const page = getPage();
3728
+ const { x, y } = point;
3729
+ await page.evaluate(
3730
+ async ({ x: px, y: py, ms }) => {
3731
+ const longPress = window.SootSim?.bridges?.interact?.longPress;
3732
+ if (typeof longPress !== "function") {
3733
+ throw new Error("sootsim longPress bridge is not installed");
3734
+ }
3735
+ const hit = await longPress(px, py, ms);
3736
+ if (!hit) {
3737
+ throw new Error(`sootsim longPress missed at ${px},${py}`);
3738
+ }
3739
+ },
3740
+ { x, y, ms: durationMs }
3741
+ );
3742
+ await page.waitForTimeout(50);
3743
+ if (_synchronizationEnabled) {
3744
+ await waitForSootsimIdle(page);
3745
+ }
3746
+ },
3747
+ async sendToHome() {
3748
+ const page = getPage();
3749
+ _suspendedAppId = await page.evaluate(async () => {
3750
+ const state = await window.SootSim?.bridges?.mainShell?.getState?.();
3751
+ const appId = state?.activeApp;
3752
+ if (typeof appId !== "string") {
3753
+ throw new Error(`cannot send an inactive SootSim app home`);
3754
+ }
3755
+ window.SootSim?.bridges?.mainShell?.goHome?.();
3756
+ return appId;
3757
+ });
3758
+ await pollUntil(
3759
+ () => page.evaluate(async () => {
3760
+ const state = await window.SootSim?.bridges?.mainShell?.getState?.();
3761
+ return state?.state === "home" && state.switcherPhase === "idle";
3762
+ }),
3763
+ Boolean,
3764
+ {
3765
+ timeoutMs: 1e4,
3766
+ label: "SootSim home transition"
3767
+ }
3768
+ );
3769
+ },
3770
+ // get direct access to the playwright page for advanced use
3771
+ getPage() {
3772
+ return getPage();
3773
+ }
3774
+ };
3775
+ var sootsimExpect = createExpect(findNodeByMatcher, getPage);
3776
+ var sootsimWaitFor = createWaitFor(findNodeByMatcher, getPage);
3777
+ async function cleanup() {
3778
+ await closeBrowser();
3779
+ }
3780
+ // Annotate the CommonJS export names for ESM import in node:
3781
+ 0 && (module.exports = {
3782
+ by,
3783
+ cleanup,
3784
+ device,
3785
+ element,
3786
+ expect,
3787
+ waitFor,
3788
+ waitForSootsimIdle
3789
+ });