sootsim 0.1.83 → 0.1.84

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 (207) hide show
  1. package/README.md +0 -1
  2. package/detox/colors.ts +54 -0
  3. package/detox/config-loader.ts +135 -0
  4. package/detox/expectations.ts +477 -0
  5. package/detox/gestures.ts +442 -0
  6. package/detox/index.ts +1436 -0
  7. package/detox/jest-environment.ts +86 -0
  8. package/detox/jest-preset.cjs +50 -0
  9. package/detox/matchers.ts +29 -0
  10. package/detox/navigation.ts +43 -0
  11. package/detox/run-test.ts +113 -0
  12. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  13. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  14. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  15. package/detox/screenshots/theme-blue-toggle.png +0 -0
  16. package/detox/screenshots/theme-blue.png +0 -0
  17. package/detox/screenshots/theme-red-toggle.png +0 -0
  18. package/detox/screenshots/theme-red.png +0 -0
  19. package/dist-cli/bin.js +3 -3
  20. package/dist-cli/chunks/{agent-MQ7GLVIB.js → agent-2CWD6W6P.js} +2 -2
  21. package/dist-cli/chunks/{agent-wrapper-7KAFDQCN.js → agent-wrapper-5W3LOX6S.js} +2 -2
  22. package/dist-cli/chunks/{assert-TV46GUNU.js → assert-ZOMAMKRT.js} +2 -2
  23. package/dist-cli/chunks/auto-bootstrap-NYYSMTIM.js +2 -0
  24. package/dist-cli/chunks/beta-4K2SQACK.js +2 -0
  25. package/dist-cli/chunks/chunk-3HXQ7MJK.js +79 -0
  26. package/dist-cli/chunks/{chunk-4LS5MZAI.js → chunk-4K7BH2D4.js} +3 -3
  27. package/dist-cli/chunks/{chunk-FJYT7XL2.js → chunk-4OPRODFA.js} +2 -2
  28. package/dist-cli/chunks/{chunk-DP7O5MHK.js → chunk-4OWVPRZV.js} +2 -2
  29. package/dist-cli/chunks/{chunk-PM5NVKLP.js → chunk-5XCXOLG2.js} +2 -2
  30. package/dist-cli/chunks/chunk-67ZZ2CM5.js +1 -0
  31. package/dist-cli/chunks/{chunk-WN7M3QON.js → chunk-73UZXB4B.js} +2 -2
  32. package/dist-cli/chunks/{chunk-5DJXZIFZ.js → chunk-7NWNTUJF.js} +1 -1
  33. package/dist-cli/chunks/{chunk-Y2VJBRSP.js → chunk-7YHDJLO2.js} +1 -1
  34. package/dist-cli/chunks/{chunk-6NN2D4EJ.js → chunk-AJVTY6KY.js} +1 -1
  35. package/dist-cli/chunks/chunk-AWSQUOAS.js +67 -0
  36. package/dist-cli/chunks/{chunk-CJY3AVI7.js → chunk-BCBNVJVG.js} +1 -1
  37. package/dist-cli/chunks/{chunk-OYMFNU3M.js → chunk-BKBL6K2G.js} +1 -1
  38. package/dist-cli/chunks/{chunk-IBNRRAES.js → chunk-C3DPQZ4J.js} +2 -2
  39. package/dist-cli/chunks/chunk-D3ZSBIIY.js +2 -0
  40. package/dist-cli/chunks/{chunk-2AWQ7OB2.js → chunk-D4HUVLZR.js} +1 -1
  41. package/dist-cli/chunks/{chunk-F3HP444U.js → chunk-DUUSJDES.js} +1 -1
  42. package/dist-cli/chunks/{chunk-277XAALA.js → chunk-ELJLF4SG.js} +3 -3
  43. package/dist-cli/chunks/{chunk-RH4F2TF7.js → chunk-EQ7TFQ2F.js} +1 -1
  44. package/dist-cli/chunks/{chunk-HNWEELAE.js → chunk-EQCKGC4B.js} +1 -1
  45. package/dist-cli/chunks/chunk-FUCGLWNN.js +1 -0
  46. package/dist-cli/chunks/{chunk-FRM355UL.js → chunk-HYPJW65U.js} +2 -2
  47. package/dist-cli/chunks/chunk-IILJQCZA.js +2 -0
  48. package/dist-cli/chunks/{chunk-Y4BUVURT.js → chunk-KU6MSPAH.js} +2 -2
  49. package/dist-cli/chunks/{chunk-DM6WT7QM.js → chunk-OOOR7NT2.js} +1 -1
  50. package/dist-cli/chunks/{chunk-HAWOAQAG.js → chunk-P7WDNKOS.js} +3 -3
  51. package/dist-cli/chunks/{chunk-6TNANCQC.js → chunk-PPKKA5VW.js} +2 -2
  52. package/dist-cli/chunks/{chunk-JQ7ZXOXJ.js → chunk-PS2G44GT.js} +2 -2
  53. package/dist-cli/chunks/{chunk-ECJBV65H.js → chunk-QMSJR5R2.js} +2 -2
  54. package/dist-cli/chunks/{chunk-J2GYISVJ.js → chunk-RF4R2U46.js} +2 -2
  55. package/dist-cli/chunks/{chunk-VMXWC2JO.js → chunk-RIXUH3NK.js} +2 -2
  56. package/dist-cli/chunks/{chunk-2PY3UZVO.js → chunk-SFGUPL2X.js} +2 -2
  57. package/dist-cli/chunks/{chunk-572VSFNP.js → chunk-SQX5CAYG.js} +1 -1
  58. package/dist-cli/chunks/{chunk-NXATOWWF.js → chunk-SQZAC7C4.js} +1 -1
  59. package/dist-cli/chunks/{chunk-WTKTOL3C.js → chunk-SV7FOGJ3.js} +2 -2
  60. package/dist-cli/chunks/{chunk-JHJNODXN.js → chunk-TK3OJSEO.js} +2 -2
  61. package/dist-cli/chunks/{chunk-KASUZ5XV.js → chunk-TL7SIZ7S.js} +1 -1
  62. package/dist-cli/chunks/{chunk-6XZOEBTZ.js → chunk-V2GQ4WXJ.js} +2 -2
  63. package/dist-cli/chunks/{chunk-IP3QJLRH.js → chunk-VH7F45CN.js} +1 -1
  64. package/dist-cli/chunks/chunk-WNVNU2OW.js +4 -0
  65. package/dist-cli/chunks/{chunk-YUELRHGB.js → chunk-XQ2OBHBE.js} +2 -2
  66. package/dist-cli/chunks/{chunk-CYV6Y6YV.js → chunk-YCIA4BHJ.js} +2 -2
  67. package/dist-cli/chunks/chunk-ZSMMJMPA.js +1 -0
  68. package/dist-cli/chunks/cli-version-QB4VH24H.js +2 -0
  69. package/dist-cli/chunks/{compat-QLLWBTS3.js → compat-FWSEEGEH.js} +3 -3
  70. package/dist-cli/chunks/{config-2DSLDCXV.js → config-CYI2WAGP.js} +2 -2
  71. package/dist-cli/chunks/control-UXY7YQVX.js +2 -0
  72. package/dist-cli/chunks/{cpu-profile-GEIKHCPC.js → cpu-profile-IKAE3KTY.js} +2 -2
  73. package/dist-cli/chunks/{daemon-4EBUFN4D.js → daemon-ZUMF53YB.js} +2 -2
  74. package/dist-cli/chunks/{debug-WGD6XWOF.js → debug-P6KULKKS.js} +3 -3
  75. package/dist-cli/chunks/{detox-LNKGRZU6.js → detox-SPWAZCYG.js} +2 -2
  76. package/dist-cli/chunks/{device-AYKXKVIQ.js → device-JWEPK6I2.js} +2 -2
  77. package/dist-cli/chunks/{diagnose-TMXSDOOC.js → diagnose-IZODTXV2.js} +2 -2
  78. package/dist-cli/chunks/drivers-MK6WJKBC.js +2 -0
  79. package/dist-cli/chunks/{electron-QFPF7TBY.js → electron-R5GP6RVB.js} +3 -3
  80. package/dist-cli/chunks/flow-6O4GEOPJ.js +2 -0
  81. package/dist-cli/chunks/{hints-MXKRR4TG.js → hints-DYDNYX7N.js} +2 -2
  82. package/dist-cli/chunks/{home-paths-REMWQDAO.js → home-paths-GLMX5OKL.js} +2 -2
  83. package/dist-cli/chunks/{inspect-XGSQNFV7.js → inspect-FJOPCTY2.js} +3 -3
  84. package/dist-cli/chunks/install-A3TUGGHN.js +2 -0
  85. package/dist-cli/chunks/{install-desktop-NQG3RZSA.js → install-desktop-YPJZMZM5.js} +3 -3
  86. package/dist-cli/chunks/{keys-5QZWXL3F.js → keys-GSYPHWNY.js} +2 -2
  87. package/dist-cli/chunks/{launch-SBXOZWKO.js → launch-4G2PKW5X.js} +3 -3
  88. package/dist-cli/chunks/{login-EACQXE24.js → login-KJQGHA64.js} +4 -4
  89. package/dist-cli/chunks/{logout-IBQLMUML.js → logout-XM2SYH5C.js} +2 -2
  90. package/dist-cli/chunks/{maestro-LFYXUX7O.js → maestro-EOWGI7DG.js} +2 -2
  91. package/dist-cli/chunks/{preview-U4SBOEGQ.js → preview-F73TKK37.js} +2 -2
  92. package/dist-cli/chunks/{profile-GWS5ECMY.js → profile-22FDKBUO.js} +2 -2
  93. package/dist-cli/chunks/{react-QDHLMVYL.js → react-5L6VPFUP.js} +2 -2
  94. package/dist-cli/chunks/{record-BUEUWPDI.js → record-JZXCQ4IN.js} +2 -2
  95. package/dist-cli/chunks/runtime-EEBX7CFV.js +2 -0
  96. package/dist-cli/chunks/{runtime-delivery-G7L6RVZ7.js → runtime-delivery-LXUM3R4A.js} +2 -2
  97. package/dist-cli/chunks/{screenshot-T2HBA3VI.js → screenshot-HDRRG33Q.js} +2 -2
  98. package/dist-cli/chunks/{screenshot-mode-EG5HMIH3.js → screenshot-mode-WY63LZIX.js} +2 -2
  99. package/dist-cli/chunks/{screenshots-S52AFHTV.js → screenshots-MPV2ENL5.js} +2 -2
  100. package/dist-cli/chunks/{server-MFFVYUGG.js → server-5LBMCJ3G.js} +2 -2
  101. package/dist-cli/chunks/setup-repo-SZSYNKNI.js +2 -0
  102. package/dist-cli/chunks/{skills-HQGWBS2O.js → skills-BQ73YOBF.js} +2 -2
  103. package/dist-cli/chunks/{start-E3DRYY7W.js → start-2WU4W6ZU.js} +4 -4
  104. package/dist-cli/chunks/store-RE45SUBF.js +2 -0
  105. package/dist-cli/chunks/telemetry-DG6GJLCP.js +2 -0
  106. package/dist-cli/chunks/{test-ZY3EF62K.js → test-OVO4CQTG.js} +3 -3
  107. package/dist-cli/chunks/{three-mode-WSPKQCJ5.js → three-mode-BKM3KFM7.js} +2 -2
  108. package/dist-cli/chunks/{timeline-3XAB5EWZ.js → timeline-MDXGEDQL.js} +2 -2
  109. package/dist-cli/chunks/{upgrade-WNENPFM5.js → upgrade-JGQABWVF.js} +2 -2
  110. package/dist-cli/chunks/upload-UJNUA4ZV.js +2 -0
  111. package/dist-cli/chunks/{web-D2AOZY44.js → web-WYFAYQ72.js} +2 -2
  112. package/dist-cli/chunks/{what-happened-F43KNSG6.js → what-happened-PZW2KW6A.js} +2 -2
  113. package/dist-cli/chunks/{whoami-T22VBR7C.js → whoami-7ATWJQS6.js} +2 -2
  114. package/dist-lib/agent-daemon-client.cjs +1 -1
  115. package/dist-lib/agent-events.cjs +1 -1
  116. package/dist-lib/agent-sessions.cjs +1 -1
  117. package/dist-lib/attached-projects.cjs +1 -1
  118. package/dist-lib/auth/shared-session.cjs +1 -1
  119. package/dist-lib/backend-origin.cjs +1 -1
  120. package/dist-lib/beta.cjs +44 -0
  121. package/dist-lib/bridge-constants.cjs +1 -1
  122. package/dist-lib/cli-constants.cjs +1 -1
  123. package/dist-lib/config.cjs +1 -1
  124. package/dist-lib/detox/index.cjs +1770 -0
  125. package/dist-lib/detox/jest-preset.cjs +50 -0
  126. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  127. package/dist-lib/home-paths.cjs +1 -1
  128. package/dist-lib/host/bridge-host.cjs +1 -1
  129. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  130. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  131. package/dist-lib/index.cjs +1 -1
  132. package/dist-lib/metro.cjs +1 -1
  133. package/dist-lib/profiles.cjs +1 -1
  134. package/dist-lib/render-mode.cjs +1 -1
  135. package/dist-lib/scripts/demo-app-registry.cjs +809 -0
  136. package/dist-lib/scripts/dev-server-scanner.cjs +1269 -0
  137. package/dist-lib/skills.cjs +8322 -0
  138. package/dist-lib/vite-base.cjs +3 -3
  139. package/dist-lib/vite.cjs +1 -1
  140. package/package.json +39 -10
  141. package/scripts/demo-app-registry.ts +989 -0
  142. package/scripts/dev-server-scanner.ts +674 -0
  143. package/src/agent-daemon-client.ts +390 -0
  144. package/src/agent-events.ts +71 -0
  145. package/src/agent-prompt.ts +71 -0
  146. package/src/agent-sessions.ts +572 -0
  147. package/src/attached-projects.ts +536 -0
  148. package/src/auth/shared-session.ts +199 -0
  149. package/src/backend-origin.ts +49 -0
  150. package/src/beta.ts +21 -0
  151. package/src/bridge-constants.ts +10 -0
  152. package/src/cli-constants.ts +1 -0
  153. package/src/cli-version.ts +30 -0
  154. package/src/codex-client.ts +215 -0
  155. package/src/config.ts +110 -0
  156. package/src/dev-bundle-resolution.ts +180 -0
  157. package/src/home-paths.ts +382 -0
  158. package/src/host/agent-host.ts +576 -0
  159. package/src/host/bridge-host.ts +2293 -0
  160. package/src/host/fetch-proxy-handler.ts +288 -0
  161. package/src/host/fetch-proxy-overrides.ts +39 -0
  162. package/src/host/open-url.ts +234 -0
  163. package/src/index.ts +9 -0
  164. package/src/metro-plugin.ts +207 -0
  165. package/src/native-dev-bundle-url.ts +62 -0
  166. package/src/native-seam-manifest.ts +313 -0
  167. package/src/profiles.ts +179 -0
  168. package/src/render-mode.ts +27 -0
  169. package/src/runtime-delivery.ts +334 -0
  170. package/src/screenshots/compose.ts +422 -0
  171. package/src/screenshots/frame-compose.ts +438 -0
  172. package/src/screenshots/orchestrate.ts +244 -0
  173. package/src/screenshots/registry.ts +58 -0
  174. package/src/screenshots/schema.ts +364 -0
  175. package/src/skills/builtin/a11y-review.ts +126 -0
  176. package/src/skills/builtin/compat-check.ts +104 -0
  177. package/src/skills/builtin/perf-profile.ts +84 -0
  178. package/src/skills/builtin/screenshot-all.ts +46 -0
  179. package/src/skills/builtin/test-flow.ts +118 -0
  180. package/src/skills/builtin/visual-diff.ts +94 -0
  181. package/src/skills/registry.ts +107 -0
  182. package/src/skills/types.ts +41 -0
  183. package/src/vite-plugin-one.ts +187 -0
  184. package/src/vite-plugin.ts +1381 -0
  185. package/src/worklets-babel.ts +132 -0
  186. package/dist-cli/chunks/auto-bootstrap-FQS4ZD2K.js +0 -2
  187. package/dist-cli/chunks/beta-VG7CDY2U.js +0 -2
  188. package/dist-cli/chunks/chunk-2OIBDYHW.js +0 -1
  189. package/dist-cli/chunks/chunk-6BNLVMXA.js +0 -1
  190. package/dist-cli/chunks/chunk-6XD6CBJM.js +0 -2
  191. package/dist-cli/chunks/chunk-CHQTO426.js +0 -1
  192. package/dist-cli/chunks/chunk-FAPYGVIU.js +0 -4
  193. package/dist-cli/chunks/chunk-PEHFE3LG.js +0 -64
  194. package/dist-cli/chunks/chunk-RXH2SLKF.js +0 -2
  195. package/dist-cli/chunks/chunk-UXQWC5ZR.js +0 -79
  196. package/dist-cli/chunks/chunk-XFQL74PF.js +0 -5
  197. package/dist-cli/chunks/cli-version-PWF6I6LY.js +0 -2
  198. package/dist-cli/chunks/control-UIOXGYXU.js +0 -2
  199. package/dist-cli/chunks/demo-app-registry-G3BDOFWC.js +0 -2
  200. package/dist-cli/chunks/drivers-IDQF34HP.js +0 -2
  201. package/dist-cli/chunks/flow-3JN3Y7RF.js +0 -2
  202. package/dist-cli/chunks/install-2N3YOOSN.js +0 -2
  203. package/dist-cli/chunks/runtime-PVB4VGUH.js +0 -2
  204. package/dist-cli/chunks/setup-repo-YOF7NV5D.js +0 -2
  205. package/dist-cli/chunks/store-MAI6D3UO.js +0 -2
  206. package/dist-cli/chunks/telemetry-RCQKCJTH.js +0 -2
  207. package/dist-cli/chunks/upload-YLJ4RA73.js +0 -2
@@ -0,0 +1,1770 @@
1
+ /*! sootsim v0.1.84 | (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 __export = (target, all) => {
11
+ for (var name in all)
12
+ __defProp(target, name, { get: all[name], enumerable: true });
13
+ };
14
+ var __copyProps = (to, from, except, desc) => {
15
+ if (from && typeof from === "object" || typeof from === "function") {
16
+ for (let key of __getOwnPropNames(from))
17
+ if (!__hasOwnProp.call(to, key) && key !== except)
18
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
23
+ // If the importer is in node compatibility mode or this is not an ESM
24
+ // file that has been converted to a CommonJS file using a Babel-
25
+ // compatible transform (i.e. "__esModule" has not been set), then set
26
+ // "default" to the CommonJS "module.exports" for node compatibility.
27
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
28
+ mod
29
+ ));
30
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
31
+
32
+ // detox/index.ts
33
+ var detox_exports = {};
34
+ __export(detox_exports, {
35
+ by: () => by,
36
+ cleanup: () => cleanup,
37
+ device: () => device,
38
+ element: () => element,
39
+ expect: () => sootExpect,
40
+ waitFor: () => sootWaitFor
41
+ });
42
+ module.exports = __toCommonJS(detox_exports);
43
+ var fs = __toESM(require("fs"), 1);
44
+ var path = __toESM(require("path"), 1);
45
+ var import_playwright = require("playwright");
46
+
47
+ // detox/gestures.ts
48
+ async function waitForSootsimGestureHandler(page, viewTag, handlerName) {
49
+ await page.waitForFunction(
50
+ async ({ viewTag: viewTag2, handlerName: handlerName2 }) => {
51
+ const host = window.SootSim?.bridges?.shellHost;
52
+ if (typeof host?.callTestBridge !== "function") return false;
53
+ let debug = null;
54
+ try {
55
+ debug = await host.callTestBridge("getShellGestureSeamDebug");
56
+ } catch {
57
+ return false;
58
+ }
59
+ return debug?.attached?.some(
60
+ (entry) => entry.viewTag === viewTag2 && entry.nodeFound !== false && entry.handlers?.some((handler) => handler.name === handlerName2)
61
+ ) === true;
62
+ },
63
+ { viewTag, handlerName },
64
+ { timeout: 5e3 }
65
+ );
66
+ }
67
+ async function readWorkletCallbackStats(page) {
68
+ return page.evaluate(async () => {
69
+ const g = window;
70
+ const tenant = typeof g.__sootsimTest?.getWorkletSlotStats === "function" ? await g.__sootsimTest.getWorkletSlotStats() : null;
71
+ const shellHost = g.SootSim?.bridges?.shellHost;
72
+ let shell = null;
73
+ if (typeof shellHost?.callTestBridge === "function") {
74
+ try {
75
+ shell = await shellHost.callTestBridge("getWorkletSlotStats");
76
+ } catch {
77
+ }
78
+ }
79
+ const tenantReceived = typeof tenant?.runJSCallbackReceived === "number" ? tenant.runJSCallbackReceived : 0;
80
+ const shellSent = typeof shell?.runJSCallbackSent === "number" ? shell.runJSCallbackSent : 0;
81
+ return { tenantReceived, shellSent };
82
+ });
83
+ }
84
+ async function waitForWorkletCallbackDrain(page, before) {
85
+ if (!before) {
86
+ await page.waitForTimeout(250);
87
+ return;
88
+ }
89
+ const deadline = Date.now() + 1500;
90
+ let lastShellSent = before.shellSent;
91
+ let stableSince = Date.now();
92
+ let sawCallback = false;
93
+ while (Date.now() < deadline) {
94
+ const current = await readWorkletCallbackStats(page);
95
+ if (!current) {
96
+ await page.waitForTimeout(20);
97
+ continue;
98
+ }
99
+ if (current.shellSent !== lastShellSent) {
100
+ lastShellSent = current.shellSent;
101
+ stableSince = Date.now();
102
+ }
103
+ const sentDelta = current.shellSent - before.shellSent;
104
+ if (sentDelta > 0) sawCallback = true;
105
+ const receivedDelta = current.tenantReceived - before.tenantReceived;
106
+ if (sawCallback && receivedDelta >= sentDelta && Date.now() - stableSince >= 100) {
107
+ return;
108
+ }
109
+ await page.waitForTimeout(20);
110
+ }
111
+ if (!sawCallback) {
112
+ await page.waitForTimeout(250);
113
+ }
114
+ }
115
+ var sootsimCanvasFinderInstalled = /* @__PURE__ */ new WeakSet();
116
+ function installInteractiveCanvasFinder() {
117
+ window.__sootsimFindInteractiveCanvas = () => {
118
+ const interactive = [];
119
+ const appVisible = [];
120
+ for (const c of Array.from(document.querySelectorAll("canvas"))) {
121
+ const s = window.getComputedStyle(c);
122
+ if (s.display === "none" || s.visibility === "hidden") continue;
123
+ const r = c.getBoundingClientRect();
124
+ if (r.width === 0 || r.height === 0) continue;
125
+ const entry = {
126
+ c,
127
+ z: parseInt(s.zIndex, 10) || 0,
128
+ app: c.dataset.surfaceId?.startsWith("app:") === true
129
+ };
130
+ if (entry.app) appVisible.push(entry);
131
+ if (s.pointerEvents !== "none") interactive.push(entry);
132
+ }
133
+ const list = appVisible.length > 0 ? appVisible : interactive;
134
+ if (list.length === 0) return null;
135
+ list.sort((a, b) => Number(b.app) - Number(a.app) || b.z - a.z);
136
+ return list[0].c;
137
+ };
138
+ }
139
+ async function ensureCanvasFinder(page) {
140
+ if (!sootsimCanvasFinderInstalled.has(page)) {
141
+ sootsimCanvasFinderInstalled.add(page);
142
+ await page.addInitScript(installInteractiveCanvasFinder);
143
+ }
144
+ await page.evaluate(installInteractiveCanvasFinder);
145
+ }
146
+ async function readSootsimPageMetrics(page) {
147
+ await ensureCanvasFinder(page);
148
+ const deadline = Date.now() + 5e3;
149
+ while (Date.now() < deadline) {
150
+ const metrics = await page.evaluate(async () => {
151
+ const canvas = window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector("canvas");
152
+ if (!canvas) return null;
153
+ const rect = canvas.getBoundingClientRect();
154
+ if (rect.width <= 0 || rect.height <= 0) return null;
155
+ const surface = await window.__sootsimTest?.getSurfaceMetricsSnapshot?.();
156
+ const screenWidth = surface?.window?.width;
157
+ const screenHeight = surface?.window?.height;
158
+ if (typeof screenWidth !== "number" || typeof screenHeight !== "number" || screenWidth <= 0 || screenHeight <= 0) {
159
+ return null;
160
+ }
161
+ const homeIndicatorHeight = Number(surface?.device?.homeIndicatorHeight) || 0;
162
+ const safeAreaBottom = Number(surface?.safeAreaInsets?.bottom) || 0;
163
+ const bottomInset = Math.max(0, homeIndicatorHeight, safeAreaBottom);
164
+ const visibleRectBottom = Number(surface?.visibleRect?.y) + Number(surface?.visibleRect?.height);
165
+ const interactiveHeight = Number.isFinite(visibleRectBottom) && visibleRectBottom > 0 ? Math.min(screenHeight, visibleRectBottom) : Math.max(0, screenHeight - bottomInset);
166
+ return {
167
+ canvas: {
168
+ left: rect.left,
169
+ top: rect.top,
170
+ width: rect.width,
171
+ height: rect.height
172
+ },
173
+ window: {
174
+ width: screenWidth,
175
+ height: screenHeight
176
+ },
177
+ interactiveWindow: {
178
+ width: screenWidth,
179
+ height: interactiveHeight
180
+ }
181
+ };
182
+ });
183
+ if (metrics) return metrics;
184
+ await page.waitForTimeout(50);
185
+ }
186
+ throw new Error("sootsim surface metrics are not available");
187
+ }
188
+ async function readSootsimInteractiveViewport(page) {
189
+ return (await readSootsimPageMetrics(page)).interactiveWindow;
190
+ }
191
+ async function sootsimToPage(page, sootX, sootY) {
192
+ const metrics = await readSootsimPageMetrics(page);
193
+ const scaleX = metrics.canvas.width / metrics.window.width;
194
+ const scaleY = metrics.canvas.height / metrics.window.height;
195
+ return {
196
+ x: metrics.canvas.left + sootX * scaleX,
197
+ y: metrics.canvas.top + sootY * scaleY
198
+ };
199
+ }
200
+ async function dragScrollNode(page, node, pixels, direction) {
201
+ const startSootX = node.absolutePosition.x + node.layout.width / 2;
202
+ const startSootY = node.absolutePosition.y + node.layout.height / 2;
203
+ let endSootX = startSootX;
204
+ let endSootY = startSootY;
205
+ switch (direction) {
206
+ case "down":
207
+ endSootY -= pixels;
208
+ break;
209
+ case "up":
210
+ endSootY += pixels;
211
+ break;
212
+ case "left":
213
+ endSootX += pixels;
214
+ break;
215
+ case "right":
216
+ endSootX -= pixels;
217
+ break;
218
+ }
219
+ const startPage = await sootsimToPage(page, startSootX, startSootY);
220
+ const endPage = await sootsimToPage(page, endSootX, endSootY);
221
+ const steps = 12;
222
+ await page.mouse.move(startPage.x, startPage.y);
223
+ await page.mouse.down();
224
+ await page.waitForTimeout(20);
225
+ const dx = (endPage.x - startPage.x) / steps;
226
+ const dy = (endPage.y - startPage.y) / steps;
227
+ for (let i = 1; i <= steps; i++) {
228
+ await page.mouse.move(startPage.x + dx * i, startPage.y + dy * i);
229
+ await page.waitForTimeout(16);
230
+ }
231
+ await page.mouse.up();
232
+ await page.waitForTimeout(180);
233
+ }
234
+ async function dispatchTwoFingerGesture(page, startA, startB, endA, endB, options = {}) {
235
+ const steps = options.steps ?? 10;
236
+ const stepDelay = options.stepDelayMs ?? 18;
237
+ await ensureCanvasFinder(page);
238
+ const beforeWorkletStats = await readWorkletCallbackStats(page);
239
+ await page.evaluate(
240
+ ({ startA: startA2, startB: startB2 }) => {
241
+ const canvas = window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector("canvas");
242
+ if (!canvas) throw new Error("sootsim interactive canvas not found");
243
+ const fire = (point, id, primary) => {
244
+ canvas.dispatchEvent(
245
+ new PointerEvent("pointerdown", {
246
+ bubbles: true,
247
+ cancelable: true,
248
+ clientX: point.x,
249
+ clientY: point.y,
250
+ isPrimary: primary,
251
+ pointerId: id,
252
+ pointerType: "touch"
253
+ })
254
+ );
255
+ };
256
+ fire(startA2, 1001, true);
257
+ fire(startB2, 1002, false);
258
+ },
259
+ { startA, startB }
260
+ );
261
+ for (let i = 1; i <= steps; i++) {
262
+ const t = i / steps;
263
+ const ax = startA.x + (endA.x - startA.x) * t;
264
+ const ay = startA.y + (endA.y - startA.y) * t;
265
+ const bx = startB.x + (endB.x - startB.x) * t;
266
+ const by2 = startB.y + (endB.y - startB.y) * t;
267
+ await page.evaluate(
268
+ ({ ax: ax2, ay: ay2, bx: bx2, by: by3 }) => {
269
+ const canvas = window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector("canvas");
270
+ if (!canvas) return;
271
+ canvas.dispatchEvent(
272
+ new PointerEvent("pointermove", {
273
+ bubbles: true,
274
+ cancelable: true,
275
+ clientX: ax2,
276
+ clientY: ay2,
277
+ isPrimary: true,
278
+ pointerId: 1001,
279
+ pointerType: "touch"
280
+ })
281
+ );
282
+ canvas.dispatchEvent(
283
+ new PointerEvent("pointermove", {
284
+ bubbles: true,
285
+ cancelable: true,
286
+ clientX: bx2,
287
+ clientY: by3,
288
+ isPrimary: false,
289
+ pointerId: 1002,
290
+ pointerType: "touch"
291
+ })
292
+ );
293
+ },
294
+ { ax, ay, bx, by: by2 }
295
+ );
296
+ await page.waitForTimeout(stepDelay);
297
+ }
298
+ await page.evaluate(
299
+ ({ endA: endA2, endB: endB2 }) => {
300
+ const canvas = window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector("canvas");
301
+ if (!canvas) return;
302
+ canvas.dispatchEvent(
303
+ new PointerEvent("pointerup", {
304
+ bubbles: true,
305
+ cancelable: true,
306
+ clientX: endA2.x,
307
+ clientY: endA2.y,
308
+ isPrimary: true,
309
+ pointerId: 1001,
310
+ pointerType: "touch"
311
+ })
312
+ );
313
+ canvas.dispatchEvent(
314
+ new PointerEvent("pointerup", {
315
+ bubbles: true,
316
+ cancelable: true,
317
+ clientX: endB2.x,
318
+ clientY: endB2.y,
319
+ isPrimary: false,
320
+ pointerId: 1002,
321
+ pointerType: "touch"
322
+ })
323
+ );
324
+ },
325
+ { endA, endB }
326
+ );
327
+ await waitForWorkletCallbackDrain(page, beforeWorkletStats);
328
+ await page.waitForTimeout(250);
329
+ }
330
+
331
+ // detox/expectations.ts
332
+ var DEFAULT_VISIBILITY_THRESHOLD = 0.75;
333
+ async function scrollMatcherElement(page, findNode, matcher, pixels, direction) {
334
+ const node = await findNode(matcher);
335
+ if (!node) {
336
+ throw new Error(`scroll container not found: ${JSON.stringify(matcher)}`);
337
+ }
338
+ await dragScrollNode(page, node, pixels, direction);
339
+ }
340
+ function isNodeVisibleInViewport(node, viewport) {
341
+ if (!node || node.layout.width <= 0 || node.layout.height <= 0) {
342
+ return false;
343
+ }
344
+ const frame = node.visibleFrame ?? {
345
+ x: node.absolutePosition?.x ?? node.layout?.x ?? 0,
346
+ y: node.absolutePosition?.y ?? node.layout?.y ?? 0,
347
+ width: node.layout.width,
348
+ height: node.layout.height
349
+ };
350
+ const right = frame.x + frame.width;
351
+ const bottom = frame.y + frame.height;
352
+ const visibleWidth = Math.max(0, Math.min(right, viewport.width) - Math.max(frame.x, 0));
353
+ const visibleHeight = Math.max(
354
+ 0,
355
+ Math.min(bottom, viewport.height) - Math.max(frame.y, 0)
356
+ );
357
+ const visibleArea = visibleWidth * visibleHeight;
358
+ const totalArea = node.layout.width * node.layout.height;
359
+ return totalArea > 0 && visibleArea / totalArea >= DEFAULT_VISIBILITY_THRESHOLD;
360
+ }
361
+ async function isNodeVisible(page, node) {
362
+ if (!node || node.layout.width <= 0 || node.layout.height <= 0) {
363
+ return false;
364
+ }
365
+ return isNodeVisibleInViewport(node, await readSootsimInteractiveViewport(page));
366
+ }
367
+ function describeNodeVisibility(node, viewport) {
368
+ if (!node) return "not found";
369
+ const absX = node.absolutePosition?.x ?? node.layout?.x ?? 0;
370
+ const absY = node.absolutePosition?.y ?? node.layout?.y ?? 0;
371
+ return JSON.stringify({
372
+ layout: node.layout,
373
+ absolutePosition: node.absolutePosition ?? null,
374
+ frame: {
375
+ x: absX,
376
+ y: absY,
377
+ right: absX + (node.layout?.width ?? 0),
378
+ bottom: absY + (node.layout?.height ?? 0)
379
+ },
380
+ visibleFrame: node.visibleFrame ?? null,
381
+ viewport: viewport ?? null,
382
+ style: node.style ?? null
383
+ });
384
+ }
385
+ function createExpect(findNode, getPage2) {
386
+ return function sootExpect2(el) {
387
+ return new SootExpectation(el._matcher, findNode, getPage2, false);
388
+ };
389
+ }
390
+ var SootExpectation = class _SootExpectation {
391
+ matcher;
392
+ findNode;
393
+ getPage;
394
+ negated;
395
+ constructor(matcher, findNode, getPage2, negated) {
396
+ this.matcher = matcher;
397
+ this.findNode = findNode;
398
+ this.getPage = getPage2;
399
+ this.negated = negated;
400
+ }
401
+ get not() {
402
+ return new _SootExpectation(this.matcher, this.findNode, this.getPage, !this.negated);
403
+ }
404
+ async toExist() {
405
+ const node = await this.findNode(this.matcher);
406
+ const exists = node !== null && node !== void 0;
407
+ if (this.negated) {
408
+ if (exists) {
409
+ throw new Error(
410
+ `expected element NOT to exist but it does: ${JSON.stringify(this.matcher)}`
411
+ );
412
+ }
413
+ } else {
414
+ if (!exists) {
415
+ throw new Error(
416
+ `expected element to exist but it was not found: ${JSON.stringify(this.matcher)}`
417
+ );
418
+ }
419
+ }
420
+ }
421
+ async toBeVisible() {
422
+ const node = await this.findNode(this.matcher);
423
+ const page = this.getPage();
424
+ const viewport = node ? await readSootsimInteractiveViewport(page) : void 0;
425
+ const visible = viewport ? isNodeVisibleInViewport(node, viewport) : false;
426
+ if (this.negated) {
427
+ if (visible) {
428
+ throw new Error(
429
+ `expected element NOT to be visible: ${JSON.stringify(this.matcher)}`
430
+ );
431
+ }
432
+ } else {
433
+ if (!visible) {
434
+ throw new Error(
435
+ `expected element to be visible but it is ${describeNodeVisibility(node, viewport)}: ${JSON.stringify(this.matcher)}`
436
+ );
437
+ }
438
+ }
439
+ }
440
+ async toBeNotVisible() {
441
+ const node = await this.findNode(this.matcher);
442
+ const visible = await isNodeVisible(this.getPage(), node);
443
+ if (visible) {
444
+ throw new Error(
445
+ `expected element NOT to be visible: ${JSON.stringify(this.matcher)}`
446
+ );
447
+ }
448
+ }
449
+ async toHaveText(expectedText) {
450
+ const node = await this.findNode(this.matcher);
451
+ if (!node) {
452
+ throw new Error(`element not found for toHaveText: ${JSON.stringify(this.matcher)}`);
453
+ }
454
+ const actualText = node.text || "";
455
+ const matches = actualText === expectedText || actualText.includes(expectedText);
456
+ if (this.negated) {
457
+ if (matches) {
458
+ throw new Error(
459
+ `expected text NOT to be "${expectedText}" but got "${actualText}": ${JSON.stringify(this.matcher)}`
460
+ );
461
+ }
462
+ } else {
463
+ if (!matches) {
464
+ throw new Error(
465
+ `expected text "${expectedText}" but got "${actualText}": ${JSON.stringify(this.matcher)}`
466
+ );
467
+ }
468
+ }
469
+ }
470
+ async toHaveId(expectedId) {
471
+ const node = await this.findNode(this.matcher);
472
+ if (!node) {
473
+ throw new Error(`element not found for toHaveId: ${JSON.stringify(this.matcher)}`);
474
+ }
475
+ const hasId = node.testID === expectedId || node.id === expectedId;
476
+ if (this.negated) {
477
+ if (hasId) throw new Error(`expected element NOT to have id "${expectedId}"`);
478
+ } else {
479
+ if (!hasId)
480
+ throw new Error(
481
+ `expected element to have id "${expectedId}" but got "${node.testID || node.id}"`
482
+ );
483
+ }
484
+ }
485
+ async toHaveLabel(expectedLabel) {
486
+ const node = await this.findNode(this.matcher);
487
+ if (!node) {
488
+ throw new Error(
489
+ `element not found for toHaveLabel: ${JSON.stringify(this.matcher)}`
490
+ );
491
+ }
492
+ const actual = node.accessibilityLabel || node.text || "";
493
+ const matches = actual === expectedLabel;
494
+ if (this.negated) {
495
+ if (matches)
496
+ throw new Error(`expected label NOT to be "${expectedLabel}" but it was`);
497
+ } else {
498
+ if (!matches)
499
+ throw new Error(
500
+ `expected label "${expectedLabel}" but got "${actual}": ${JSON.stringify(this.matcher)}`
501
+ );
502
+ }
503
+ }
504
+ async toHaveAccessibilityRole(expectedRole) {
505
+ const node = await this.findNode(this.matcher);
506
+ if (!node) {
507
+ throw new Error(
508
+ `element not found for toHaveAccessibilityRole: ${JSON.stringify(this.matcher)}`
509
+ );
510
+ }
511
+ const actual = node.accessibilityRole || "";
512
+ const matches = actual === expectedRole;
513
+ if (this.negated) {
514
+ if (matches) throw new Error(`expected role NOT to be "${expectedRole}" but it was`);
515
+ } else {
516
+ if (!matches)
517
+ throw new Error(
518
+ `expected role "${expectedRole}" but got "${actual}": ${JSON.stringify(this.matcher)}`
519
+ );
520
+ }
521
+ }
522
+ async toBeEnabled() {
523
+ const node = await this.findNode(this.matcher);
524
+ if (!node) {
525
+ throw new Error(
526
+ `element not found for toBeEnabled: ${JSON.stringify(this.matcher)}`
527
+ );
528
+ }
529
+ const disabled = node.accessibilityState?.disabled ?? false;
530
+ if (this.negated) {
531
+ if (!disabled) throw new Error(`expected element to be disabled but it is enabled`);
532
+ } else {
533
+ if (disabled) throw new Error(`expected element to be enabled but it is disabled`);
534
+ }
535
+ }
536
+ async toBeFocused() {
537
+ await this.toExist();
538
+ }
539
+ async toHaveValue(value) {
540
+ const node = await this.findNode(this.matcher);
541
+ if (!node) {
542
+ throw new Error(
543
+ `element not found for toHaveValue: ${JSON.stringify(this.matcher)}`
544
+ );
545
+ }
546
+ const actual = node.text || "";
547
+ if (this.negated) {
548
+ if (actual.includes(value)) {
549
+ throw new Error(
550
+ `expected element NOT to have value "${value}" but got "${actual}"`
551
+ );
552
+ }
553
+ } else {
554
+ if (!actual.includes(value)) {
555
+ throw new Error(`expected element to have value "${value}" but got "${actual}"`);
556
+ }
557
+ }
558
+ }
559
+ async toHaveSliderPosition(normalizedPosition, tolerance) {
560
+ await this.toExist();
561
+ }
562
+ async toHaveToggleValue(value) {
563
+ await this.toExist();
564
+ }
565
+ };
566
+ function createWaitFor(findNode, getPage2) {
567
+ return function sootWaitFor2(el) {
568
+ return new SootWaitForChain(el._matcher, findNode, getPage2);
569
+ };
570
+ }
571
+ var SootWaitForChain = class {
572
+ matcher;
573
+ findNode;
574
+ getPage;
575
+ assertionFn = null;
576
+ _negated = false;
577
+ constructor(matcher, findNode, getPage2) {
578
+ this.matcher = matcher;
579
+ this.findNode = findNode;
580
+ this.getPage = getPage2;
581
+ }
582
+ get not() {
583
+ this._negated = true;
584
+ return this;
585
+ }
586
+ toExist() {
587
+ this.assertionFn = async () => {
588
+ const node = await this.findNode(this.matcher);
589
+ const exists = node !== null && node !== void 0;
590
+ if (this._negated ? exists : !exists) {
591
+ throw new Error(`waitFor toExist failed: ${JSON.stringify(this.matcher)}`);
592
+ }
593
+ };
594
+ return new SootWaitForAction(this.assertionFn, this.findNode, this.getPage);
595
+ }
596
+ toBeVisible() {
597
+ this.assertionFn = async () => {
598
+ const node = await this.findNode(this.matcher);
599
+ const page = this.getPage();
600
+ const viewport = node ? await readSootsimInteractiveViewport(page) : void 0;
601
+ const visible = viewport ? isNodeVisibleInViewport(node, viewport) : false;
602
+ if (this._negated ? visible : !visible) {
603
+ throw new Error(
604
+ `waitFor toBeVisible failed (${describeNodeVisibility(node, viewport)}): ${JSON.stringify(this.matcher)}`
605
+ );
606
+ }
607
+ };
608
+ return new SootWaitForAction(this.assertionFn, this.findNode, this.getPage);
609
+ }
610
+ toBeNotVisible() {
611
+ this.assertionFn = async () => {
612
+ const node = await this.findNode(this.matcher);
613
+ const visible = await isNodeVisible(this.getPage(), node);
614
+ if (visible) {
615
+ throw new Error(`waitFor toBeNotVisible failed: ${JSON.stringify(this.matcher)}`);
616
+ }
617
+ };
618
+ return new SootWaitForAction(this.assertionFn, this.findNode, this.getPage);
619
+ }
620
+ toHaveText(expectedText) {
621
+ this.assertionFn = async () => {
622
+ const node = await this.findNode(this.matcher);
623
+ if (!node)
624
+ throw new Error(
625
+ `waitFor toHaveText: element not found: ${JSON.stringify(this.matcher)}`
626
+ );
627
+ const actual = node.text || "";
628
+ const matches = actual === expectedText || actual.includes(expectedText);
629
+ if (this._negated ? matches : !matches) {
630
+ throw new Error(
631
+ `waitFor toHaveText: expected "${expectedText}" but got "${actual}" ${JSON.stringify(this.matcher)}`
632
+ );
633
+ }
634
+ };
635
+ return new SootWaitForAction(this.assertionFn, this.findNode, this.getPage);
636
+ }
637
+ toHaveValue(value) {
638
+ this.assertionFn = async () => {
639
+ const node = await this.findNode(this.matcher);
640
+ if (!node) throw new Error(`waitFor toHaveValue: element not found`);
641
+ const actual = node.text || "";
642
+ if (!actual.includes(value)) {
643
+ throw new Error(`waitFor toHaveValue: expected "${value}" in "${actual}"`);
644
+ }
645
+ };
646
+ return new SootWaitForAction(this.assertionFn, this.findNode, this.getPage);
647
+ }
648
+ };
649
+ var SootWaitForAction = class {
650
+ assertionFn;
651
+ findNode;
652
+ getPage;
653
+ constructor(assertionFn, findNode, getPage2) {
654
+ this.assertionFn = assertionFn;
655
+ this.findNode = findNode;
656
+ this.getPage = getPage2;
657
+ }
658
+ async withTimeout(ms) {
659
+ const start = Date.now();
660
+ const pollInterval = 100;
661
+ let lastError = null;
662
+ while (Date.now() - start < ms) {
663
+ try {
664
+ await this.assertionFn();
665
+ return;
666
+ } catch (e) {
667
+ lastError = e;
668
+ await new Promise((r) => setTimeout(r, pollInterval));
669
+ }
670
+ }
671
+ throw lastError || new Error(`waitFor timed out after ${ms}ms`);
672
+ }
673
+ // if no withTimeout is called, just run the assertion once
674
+ async then(resolve, reject) {
675
+ try {
676
+ await this.withTimeout(5e3);
677
+ resolve(void 0);
678
+ } catch (e) {
679
+ reject(e);
680
+ }
681
+ }
682
+ whileElement(matcher) {
683
+ return new SootWaitForScrollAction(
684
+ this.assertionFn,
685
+ this.findNode,
686
+ this.getPage,
687
+ matcher
688
+ );
689
+ }
690
+ };
691
+ var SootWaitForScrollAction = class {
692
+ assertionFn;
693
+ findNode;
694
+ getPage;
695
+ matcher;
696
+ constructor(assertionFn, findNode, getPage2, matcher) {
697
+ this.assertionFn = assertionFn;
698
+ this.findNode = findNode;
699
+ this.getPage = getPage2;
700
+ this.matcher = matcher;
701
+ }
702
+ async scroll(pixels, direction, _startPositionX, _startPositionY) {
703
+ const page = this.getPage();
704
+ const start = Date.now();
705
+ const timeoutMs = 5e3;
706
+ let lastError = null;
707
+ while (Date.now() - start < timeoutMs) {
708
+ try {
709
+ await this.assertionFn();
710
+ return;
711
+ } catch (error) {
712
+ lastError = error;
713
+ }
714
+ await scrollMatcherElement(page, this.findNode, this.matcher, pixels, direction);
715
+ }
716
+ throw lastError || new Error(
717
+ `waitFor whileElement(...).scroll() timed out: ${JSON.stringify(this.matcher)}`
718
+ );
719
+ }
720
+ };
721
+
722
+ // detox/matchers.ts
723
+ var by = {
724
+ id(testID) {
725
+ return { type: "id", value: testID };
726
+ },
727
+ text(text) {
728
+ return { type: "text", value: text };
729
+ },
730
+ label(accessibilityLabel) {
731
+ return { type: "label", value: accessibilityLabel };
732
+ },
733
+ type(nativeType) {
734
+ return { type: "type", value: nativeType };
735
+ },
736
+ role(accessibilityRole) {
737
+ return { type: "role", value: accessibilityRole };
738
+ }
739
+ };
740
+
741
+ // detox/index.ts
742
+ var BASE_URL = process.env.SOOTSIM_URL || "http://localhost:5173";
743
+ var SCREENSHOT_DIR = process.env.SOOTSIM_SCREENSHOT_DIR || path.join(process.cwd(), "test", "detox-driver", "screenshots");
744
+ var DETOX_PLATFORM = process.env.SOOTSIM_PLATFORM === "android" ? "android" : "ios";
745
+ var _browser = null;
746
+ var _context = null;
747
+ var _page = null;
748
+ var _synchronizationEnabled = false;
749
+ var pageDiagnostics = /* @__PURE__ */ new WeakMap();
750
+ var STATUS_BAR_OVERRIDE_EVENT = "sootsim:statusBarOverride";
751
+ function getPage() {
752
+ if (!_page)
753
+ throw new Error("sootsim driver not initialized -- call device.launchApp() first");
754
+ return _page;
755
+ }
756
+ function recordPageDiagnostic(page, message) {
757
+ const entries = pageDiagnostics.get(page);
758
+ if (!entries) return;
759
+ entries.push(message);
760
+ if (entries.length > 40) entries.shift();
761
+ }
762
+ function attachPageDiagnostics(page) {
763
+ if (pageDiagnostics.has(page)) return;
764
+ pageDiagnostics.set(page, []);
765
+ page.on("console", (message) => {
766
+ recordPageDiagnostic(page, `[console:${message.type()}] ${message.text()}`);
767
+ });
768
+ page.on("pageerror", (error) => {
769
+ recordPageDiagnostic(page, `[pageerror] ${error.message}`);
770
+ });
771
+ page.on("requestfailed", (request) => {
772
+ const failure = request.failure();
773
+ recordPageDiagnostic(
774
+ page,
775
+ `[requestfailed] ${request.method()} ${request.url()} ${failure?.errorText ?? ""}`
776
+ );
777
+ });
778
+ }
779
+ async function describeSootsimBridgeFailure(page, cause) {
780
+ let pageState = null;
781
+ try {
782
+ pageState = await page.evaluate(() => ({
783
+ bodyText: document.body?.innerText?.slice(0, 500) ?? "",
784
+ globals: Object.keys(window).filter((key) => key.startsWith("__sootsim") || key === "SootSim").sort(),
785
+ readyState: document.readyState,
786
+ title: document.title,
787
+ url: window.location.href
788
+ }));
789
+ } catch (error) {
790
+ pageState = {
791
+ evaluateError: error instanceof Error ? error.message : String(error)
792
+ };
793
+ }
794
+ return new Error(
795
+ [
796
+ `timed out waiting for sootsim test bridge`,
797
+ `cause: ${cause instanceof Error ? cause.message : String(cause)}`,
798
+ `page: ${JSON.stringify(pageState)}`,
799
+ `recent page diagnostics:
800
+ ${(pageDiagnostics.get(page) ?? []).join("\n") || "(none)"}`
801
+ ].join("\n")
802
+ );
803
+ }
804
+ async function waitForSootsimTree(page, timeout = 3e4) {
805
+ try {
806
+ await page.waitForFunction(() => !!window.__sootsimTest?.waitForTree, {
807
+ timeout
808
+ });
809
+ await page.evaluate(async (timeoutMs) => {
810
+ await Promise.race([
811
+ window.__sootsimTest.waitForTree(),
812
+ new Promise((_, reject) => {
813
+ setTimeout(
814
+ () => reject(new Error(`sootsim waitForTree timed out after ${timeoutMs}ms`)),
815
+ timeoutMs
816
+ );
817
+ })
818
+ ]);
819
+ }, timeout);
820
+ } catch (error) {
821
+ throw await describeSootsimBridgeFailure(page, error);
822
+ }
823
+ }
824
+ async function closeContext() {
825
+ const context = _context;
826
+ _context = null;
827
+ _page = null;
828
+ _synchronizationEnabled = false;
829
+ if (context) await context.close();
830
+ }
831
+ async function closeBrowser() {
832
+ if (!_browser) return;
833
+ const browser = _browser;
834
+ _browser = null;
835
+ await closeContext();
836
+ await browser.close();
837
+ }
838
+ async function waitForSootsimIdle(page, maxMs = 3e3) {
839
+ const result = await page.evaluate(
840
+ async ({ maxMs: maxMs2 }) => {
841
+ let transitionError = null;
842
+ try {
843
+ const waitForScreenTransitions = window.__sootsimTest?.waitForScreenTransitions;
844
+ if (typeof waitForScreenTransitions === "function") {
845
+ const transitionResult = await waitForScreenTransitions({
846
+ timeoutMs: Math.min(maxMs2, 1800),
847
+ settleMs: 48,
848
+ startWindowMs: 600
849
+ });
850
+ if (transitionResult?.started === true && transitionResult.settled === true && transitionResult.timedOut !== true) {
851
+ return { settled: true, elapsed: transitionResult.waitedMs ?? 0 };
852
+ }
853
+ if (transitionResult?.timedOut) {
854
+ return {
855
+ settled: false,
856
+ elapsed: transitionResult.waitedMs,
857
+ reason: "screen transition"
858
+ };
859
+ }
860
+ }
861
+ } catch (error) {
862
+ transitionError = error instanceof Error ? error.message : String(error);
863
+ }
864
+ const start = Date.now();
865
+ const deadline = start + maxMs2;
866
+ const pollMs = 50;
867
+ const requiredStablePolls = 3;
868
+ const layoutTolerance = 1;
869
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
870
+ const isLayoutStable = (a, b) => {
871
+ if (!a || a.length !== b.length) return false;
872
+ for (let i = 0; i < b.length; i++) {
873
+ if (Math.abs(a[i] - b[i]) > layoutTolerance) return false;
874
+ }
875
+ return true;
876
+ };
877
+ const readSnapshot = async () => {
878
+ const root = window.__sootsimRoot;
879
+ const layout = [];
880
+ if (root) {
881
+ const walk = (node) => {
882
+ if (node.layout && node.layout.width > 0) {
883
+ layout.push(
884
+ Math.round(node.layout.x),
885
+ Math.round(node.layout.y),
886
+ Math.round(node.layout.width),
887
+ Math.round(node.layout.height)
888
+ );
889
+ }
890
+ for (const child of node.children || []) walk(child);
891
+ };
892
+ walk(root);
893
+ }
894
+ return layout;
895
+ };
896
+ let stableLayout = null;
897
+ let stable = 0;
898
+ while (Date.now() < deadline) {
899
+ const layout = await readSnapshot();
900
+ if (isLayoutStable(stableLayout, layout)) {
901
+ stable++;
902
+ if (stable >= requiredStablePolls) {
903
+ return { settled: true, elapsed: Date.now() - start };
904
+ }
905
+ } else {
906
+ stableLayout = layout;
907
+ stable = 0;
908
+ }
909
+ await sleep(pollMs);
910
+ }
911
+ return {
912
+ settled: false,
913
+ elapsed: Date.now() - start,
914
+ reason: transitionError ? `layout fallback after screen transition wait failed: ${transitionError}` : "layout fallback"
915
+ };
916
+ },
917
+ { maxMs }
918
+ );
919
+ if (!result?.settled) {
920
+ throw new Error(
921
+ `sootsim synchronization timed out after ${result?.elapsed ?? maxMs}ms${result?.reason ? ` (${result.reason})` : ""}`
922
+ );
923
+ }
924
+ }
925
+ async function findNodeByMatcher(matcher) {
926
+ const page = getPage();
927
+ if (matcher.type === "id") {
928
+ return page.evaluate(
929
+ (id) => window.__sootsimTest.findByTestId(id),
930
+ matcher.value
931
+ );
932
+ } else if (matcher.type === "text") {
933
+ return page.evaluate(
934
+ (text) => window.__sootsimTest.findByText(text),
935
+ matcher.value
936
+ );
937
+ } else if (matcher.type === "label") {
938
+ return page.evaluate(
939
+ (label) => window.__sootsimTest.findByLabel(label),
940
+ matcher.value
941
+ );
942
+ } else if (matcher.type === "role") {
943
+ return page.evaluate(
944
+ (role) => window.__sootsimTest.findByRole(role),
945
+ matcher.value
946
+ );
947
+ } else if (matcher.type === "type") {
948
+ return page.evaluate(async (type) => {
949
+ const results = await window.__sootsimTest.queryAll({ type });
950
+ return results[0] || null;
951
+ }, matcher.value);
952
+ }
953
+ return null;
954
+ }
955
+ function getNodeCenter(nodeInfo) {
956
+ return {
957
+ x: nodeInfo.absolutePosition.x + nodeInfo.layout.width / 2,
958
+ y: nodeInfo.absolutePosition.y + nodeInfo.layout.height / 2
959
+ };
960
+ }
961
+ async function getDefaultTapPoint(page, nodeInfo) {
962
+ const center = getNodeCenter(nodeInfo);
963
+ const viewport = await readSootsimInteractiveViewport(page);
964
+ const frame = nodeInfo.visibleFrame ?? {
965
+ x: nodeInfo.absolutePosition?.x ?? nodeInfo.layout?.x ?? 0,
966
+ y: nodeInfo.absolutePosition?.y ?? nodeInfo.layout?.y ?? 0,
967
+ width: nodeInfo.layout?.width ?? 0,
968
+ height: nodeInfo.layout?.height ?? 0
969
+ };
970
+ const left = Math.max(0, frame.x);
971
+ const top = Math.max(0, frame.y);
972
+ const right = Math.min(viewport.width, frame.x + frame.width);
973
+ const bottom = Math.min(viewport.height, frame.y + frame.height);
974
+ if (right <= left || bottom <= top) {
975
+ return center;
976
+ }
977
+ return {
978
+ x: left + (right - left) / 2,
979
+ y: top + (bottom - top) / 2
980
+ };
981
+ }
982
+ async function isFocusedTextInputNode(page, nodeInfo) {
983
+ const focused = await page.evaluate(
984
+ () => window.__sootsimTest.getFocusedNode?.() ?? null
985
+ );
986
+ return !!focused && typeof focused === "object" && focused.nodeId === nodeInfo?.nodeId;
987
+ }
988
+ async function focusElementForTextEntry(el) {
989
+ const page = getPage();
990
+ const node = await findNodeByMatcher(el._matcher);
991
+ if (!node) {
992
+ throw new Error(`element not found for text entry: ${JSON.stringify(el._matcher)}`);
993
+ }
994
+ if (await isFocusedTextInputNode(page, node)) return;
995
+ await el.tap();
996
+ await page.waitForTimeout(100);
997
+ }
998
+ function ensureScreenshotDir() {
999
+ if (!fs.existsSync(SCREENSHOT_DIR)) {
1000
+ fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
1001
+ }
1002
+ }
1003
+ function decodePngDataUrl(dataUrl) {
1004
+ const match = /^data:image\/png;base64,(.+)$/.exec(dataUrl);
1005
+ if (!match) {
1006
+ throw new Error("sootsim screenshot bridge returned a non-png payload");
1007
+ }
1008
+ return Buffer.from(match[1], "base64");
1009
+ }
1010
+ async function captureSootsimPngFast(opts) {
1011
+ const page = getPage();
1012
+ const dataUrl = await page.evaluate(
1013
+ async (captureOpts) => {
1014
+ const screenshot = window.SootSim?.bridges?.screenshot;
1015
+ if (typeof screenshot !== "function") {
1016
+ throw new Error("sootsim screenshot bridge is not installed");
1017
+ }
1018
+ return screenshot(captureOpts);
1019
+ },
1020
+ { crop: opts?.crop }
1021
+ );
1022
+ if (!dataUrl) {
1023
+ throw new Error("sootsim screenshot bridge returned an empty image");
1024
+ }
1025
+ return decodePngDataUrl(dataUrl);
1026
+ }
1027
+ async function captureSootsimPng(opts) {
1028
+ const page = getPage();
1029
+ const result = await page.evaluate(
1030
+ async (captureOpts) => {
1031
+ const screenshot = window.SootSim?.bridges?.screenshot;
1032
+ if (typeof screenshot !== "function") {
1033
+ throw new Error("sootsim screenshot bridge is not installed");
1034
+ }
1035
+ const gapMs = 100;
1036
+ const maxWaitMs = 8e3;
1037
+ const minAttempts = 3;
1038
+ const t0 = Date.now();
1039
+ let prev = await screenshot(captureOpts);
1040
+ let attempts = 1;
1041
+ while (attempts < minAttempts || Date.now() - t0 < maxWaitMs) {
1042
+ await new Promise((r) => setTimeout(r, gapMs));
1043
+ const next = await screenshot(captureOpts);
1044
+ attempts++;
1045
+ if (next === prev) {
1046
+ return {
1047
+ dataUrl: next,
1048
+ stable: true,
1049
+ attempts,
1050
+ elapsedMs: Date.now() - t0
1051
+ };
1052
+ }
1053
+ prev = next;
1054
+ }
1055
+ return { dataUrl: prev, stable: false, attempts, elapsedMs: Date.now() - t0 };
1056
+ },
1057
+ {
1058
+ crop: opts?.crop
1059
+ }
1060
+ );
1061
+ if (!result.dataUrl) {
1062
+ throw new Error("sootsim screenshot bridge returned an empty image");
1063
+ }
1064
+ if (!result.stable) {
1065
+ console.warn(
1066
+ `[sootsim-detox] screenshot did not stabilize after ${result.elapsedMs}ms (${result.attempts} captures); using last frame`
1067
+ );
1068
+ }
1069
+ return decodePngDataUrl(result.dataUrl);
1070
+ }
1071
+ async function writeSootsimScreenshot(name, opts) {
1072
+ ensureScreenshotDir();
1073
+ const screenshotPath = path.join(SCREENSHOT_DIR, `${name}.png`);
1074
+ fs.writeFileSync(screenshotPath, await captureSootsimPng(opts));
1075
+ return screenshotPath;
1076
+ }
1077
+ async function dispatchStatusBarOverride(config) {
1078
+ const page = getPage();
1079
+ const time = typeof config.time === "string" && config.time.length > 0 ? config.time : null;
1080
+ await page.evaluate(
1081
+ ({ eventType, time: time2 }) => {
1082
+ window.dispatchEvent(
1083
+ new CustomEvent(eventType, {
1084
+ detail: { time: time2 }
1085
+ })
1086
+ );
1087
+ },
1088
+ { eventType: STATUS_BAR_OVERRIDE_EVENT, time }
1089
+ );
1090
+ await page.waitForTimeout(50);
1091
+ }
1092
+ async function dispatchSootsimEventInPage(type, detail) {
1093
+ const page = getPage();
1094
+ await page.evaluate(
1095
+ ({ detail: detail2, type: type2 }) => {
1096
+ window.dispatchEvent(new CustomEvent(type2, { detail: detail2 }));
1097
+ },
1098
+ { detail, type }
1099
+ );
1100
+ }
1101
+ function createSootElement(matcher) {
1102
+ const el = {
1103
+ _matcher: matcher,
1104
+ async tap(point) {
1105
+ const page = getPage();
1106
+ const node = await findNodeByMatcher(matcher);
1107
+ if (!node) throw new Error(`element not found for tap: ${JSON.stringify(matcher)}`);
1108
+ const defaultPoint = point ? null : await getDefaultTapPoint(page, node);
1109
+ const targetSootX = typeof point?.x === "number" ? node.absolutePosition.x + point.x : defaultPoint?.x ?? node.absolutePosition.x + node.layout.width / 2;
1110
+ const targetSootY = typeof point?.y === "number" ? node.absolutePosition.y + point.y : defaultPoint?.y ?? node.absolutePosition.y + node.layout.height / 2;
1111
+ await page.evaluate(
1112
+ async ({ x, y }) => {
1113
+ const interactTap = window.SootSim?.bridges?.interact?.tap;
1114
+ if (typeof interactTap === "function") {
1115
+ const result = await interactTap(x, y);
1116
+ if (result && (typeof result !== "object" || result.hit !== false)) return;
1117
+ }
1118
+ const tap = window.__sootsimTest?.tap;
1119
+ if (typeof tap !== "function") {
1120
+ throw new Error("sootsim tap bridge is not installed");
1121
+ }
1122
+ await tap(x, y);
1123
+ },
1124
+ { x: targetSootX, y: targetSootY }
1125
+ );
1126
+ await page.waitForTimeout(50);
1127
+ if (_synchronizationEnabled) {
1128
+ await waitForSootsimIdle(page);
1129
+ }
1130
+ },
1131
+ async multiTap(times) {
1132
+ const page = getPage();
1133
+ const node = await findNodeByMatcher(matcher);
1134
+ if (!node)
1135
+ throw new Error(`element not found for multiTap: ${JSON.stringify(matcher)}`);
1136
+ const center = getNodeCenter(node);
1137
+ const pageCoords = await sootsimToPage(page, center.x, center.y);
1138
+ for (let i = 0; i < times; i++) {
1139
+ await page.mouse.move(pageCoords.x, pageCoords.y);
1140
+ await page.mouse.down();
1141
+ await page.waitForTimeout(50);
1142
+ await page.mouse.up();
1143
+ if (i < times - 1) {
1144
+ await page.waitForTimeout(80);
1145
+ }
1146
+ }
1147
+ await page.waitForTimeout(80);
1148
+ },
1149
+ async longPress(duration = 1e3) {
1150
+ const page = getPage();
1151
+ const node = await findNodeByMatcher(matcher);
1152
+ if (!node)
1153
+ throw new Error(`element not found for longPress: ${JSON.stringify(matcher)}`);
1154
+ const center = getNodeCenter(node);
1155
+ const pageCoords = await sootsimToPage(page, center.x, center.y);
1156
+ await page.mouse.move(pageCoords.x, pageCoords.y);
1157
+ await page.mouse.down();
1158
+ await page.waitForTimeout(duration);
1159
+ await page.mouse.up();
1160
+ await page.waitForTimeout(50);
1161
+ },
1162
+ async longPressAndDrag(duration, normalizedStartX, normalizedStartY, targetElement, normalizedEndX, normalizedEndY, speed = "fast", holdDuration = 0) {
1163
+ const page = getPage();
1164
+ const srcNode = await findNodeByMatcher(matcher);
1165
+ if (!srcNode)
1166
+ throw new Error(`source element not found: ${JSON.stringify(matcher)}`);
1167
+ const tgtNode = await findNodeByMatcher(targetElement._matcher);
1168
+ if (!tgtNode)
1169
+ throw new Error(
1170
+ `target element not found: ${JSON.stringify(targetElement._matcher)}`
1171
+ );
1172
+ const startSootX = srcNode.absolutePosition.x + srcNode.layout.width * normalizedStartX;
1173
+ const startSootY = srcNode.absolutePosition.y + srcNode.layout.height * normalizedStartY;
1174
+ const endSootX = tgtNode.absolutePosition.x + tgtNode.layout.width * normalizedEndX;
1175
+ const endSootY = tgtNode.absolutePosition.y + tgtNode.layout.height * normalizedEndY;
1176
+ const startPage = await sootsimToPage(page, startSootX, startSootY);
1177
+ const endPage = await sootsimToPage(page, endSootX, endSootY);
1178
+ const steps = speed === "slow" ? 20 : 10;
1179
+ await page.mouse.move(startPage.x, startPage.y);
1180
+ await page.mouse.down();
1181
+ await page.waitForTimeout(Math.max(duration, holdDuration));
1182
+ const dx = (endPage.x - startPage.x) / steps;
1183
+ const dy = (endPage.y - startPage.y) / steps;
1184
+ for (let i = 1; i <= steps; i++) {
1185
+ await page.mouse.move(startPage.x + dx * i, startPage.y + dy * i);
1186
+ await page.waitForTimeout(speed === "slow" ? 30 : 10);
1187
+ }
1188
+ await page.mouse.up();
1189
+ await page.waitForTimeout(50);
1190
+ },
1191
+ async typeText(text) {
1192
+ const page = getPage();
1193
+ await focusElementForTextEntry(el);
1194
+ await page.keyboard.type(text);
1195
+ },
1196
+ async replaceText(text) {
1197
+ const page = getPage();
1198
+ await focusElementForTextEntry(el);
1199
+ const node = await findNodeByMatcher(matcher);
1200
+ const value = typeof node?.text === "string" ? node.text : "";
1201
+ for (let i = 0; i < value.length; i++) {
1202
+ await page.keyboard.press("Backspace");
1203
+ }
1204
+ await page.keyboard.type(text);
1205
+ },
1206
+ async clearText() {
1207
+ const page = getPage();
1208
+ await focusElementForTextEntry(el);
1209
+ const node = await findNodeByMatcher(matcher);
1210
+ const value = typeof node?.text === "string" ? node.text : "";
1211
+ for (let i = 0; i < value.length; i++) {
1212
+ await page.keyboard.press("Backspace");
1213
+ }
1214
+ },
1215
+ async scroll(pixels, direction) {
1216
+ const node = await findNodeByMatcher(matcher);
1217
+ if (!node)
1218
+ throw new Error(`element not found for scroll: ${JSON.stringify(matcher)}`);
1219
+ const page = getPage();
1220
+ await dragScrollNode(page, node, pixels, direction);
1221
+ },
1222
+ async scrollTo(edge) {
1223
+ const node = await findNodeByMatcher(matcher);
1224
+ if (!node)
1225
+ throw new Error(`element not found for scrollTo: ${JSON.stringify(matcher)}`);
1226
+ const targetId = typeof node.testID === "string" && node.testID ? node.testID : typeof node.id === "string" && node.id ? node.id : null;
1227
+ if (!targetId) {
1228
+ throw new Error(
1229
+ `scrollTo requires an id-backed scroll view: ${JSON.stringify(matcher)}`
1230
+ );
1231
+ }
1232
+ const hitX = node.absolutePosition.x + node.layout.width / 2;
1233
+ const hitY = node.absolutePosition.y + node.layout.height / 2;
1234
+ const page = getPage();
1235
+ const result = await page.evaluate(
1236
+ async ({ edge: edge2, hitX: hitX2, hitY: hitY2, targetId: targetId2 }) => {
1237
+ const bridge = window.__sootsimTest;
1238
+ if (!bridge?.getScrollStateAt || !bridge.scrollTo) {
1239
+ return { ok: false, reason: "scroll bridge unavailable" };
1240
+ }
1241
+ const state = await bridge.getScrollStateAt(hitX2, hitY2);
1242
+ if (!state) return { ok: false, reason: "scroll state not found" };
1243
+ const maxOffset = state.maxOffset && typeof state.maxOffset === "object" ? state.maxOffset : null;
1244
+ const offset = state.offset && typeof state.offset === "object" ? state.offset : null;
1245
+ const maxX = typeof state.maxOffsetX === "number" ? state.maxOffsetX : maxOffset?.x ?? 0;
1246
+ const maxY = typeof state.maxOffsetY === "number" ? state.maxOffsetY : maxOffset?.y ?? 0;
1247
+ const currentX = typeof state.offsetX === "number" ? state.offsetX : offset?.x ?? 0;
1248
+ const currentY = typeof state.offsetY === "number" ? state.offsetY : offset?.y ?? 0;
1249
+ const x = edge2 === "left" ? 0 : edge2 === "right" ? maxX : currentX;
1250
+ const y = edge2 === "top" ? 0 : edge2 === "bottom" ? maxY : currentY;
1251
+ return bridge.scrollTo(targetId2, x, y, false);
1252
+ },
1253
+ { edge, hitX, hitY, targetId }
1254
+ );
1255
+ if (!result?.ok) {
1256
+ throw new Error(
1257
+ `scrollTo(${edge}) failed for ${targetId}: ${result?.reason ?? "unknown error"}`
1258
+ );
1259
+ }
1260
+ await page.evaluate(
1261
+ () => new Promise((resolve) => {
1262
+ requestAnimationFrame(() => resolve());
1263
+ })
1264
+ );
1265
+ if (_synchronizationEnabled) {
1266
+ await waitForSootsimIdle(page);
1267
+ }
1268
+ },
1269
+ async swipe(direction, speed = "fast", percentage = 0.75) {
1270
+ const node = await findNodeByMatcher(matcher);
1271
+ if (!node)
1272
+ throw new Error(`element not found for swipe: ${JSON.stringify(matcher)}`);
1273
+ const page = getPage();
1274
+ const { absolutePosition, layout } = node;
1275
+ const startSootX = absolutePosition.x + layout.width * 0.5;
1276
+ const startSootY = absolutePosition.y + layout.height * 0.5;
1277
+ let endSootX = startSootX;
1278
+ let endSootY = startSootY;
1279
+ const dist = direction === "up" || direction === "down" ? layout.height * percentage : layout.width * percentage;
1280
+ switch (direction) {
1281
+ case "up":
1282
+ endSootY -= dist;
1283
+ break;
1284
+ case "down":
1285
+ endSootY += dist;
1286
+ break;
1287
+ case "left":
1288
+ endSootX -= dist;
1289
+ break;
1290
+ case "right":
1291
+ endSootX += dist;
1292
+ break;
1293
+ }
1294
+ const steps = speed === "slow" ? 20 : speed === "fast" ? 8 : 12;
1295
+ const stepDelay = speed === "slow" ? 25 : speed === "fast" ? 5 : 15;
1296
+ await page.evaluate(
1297
+ async ({ fromX, fromY, toX, toY, steps: steps2, stepMs }) => {
1298
+ const drag = window.__sootsimTest?.drag;
1299
+ if (typeof drag !== "function") {
1300
+ throw new Error("sootsim drag bridge is not installed");
1301
+ }
1302
+ await drag(fromX, fromY, toX, toY, steps2, stepMs);
1303
+ },
1304
+ {
1305
+ fromX: startSootX,
1306
+ fromY: startSootY,
1307
+ toX: endSootX,
1308
+ toY: endSootY,
1309
+ steps,
1310
+ stepMs: stepDelay
1311
+ }
1312
+ );
1313
+ await page.waitForTimeout(100);
1314
+ },
1315
+ async pinch(scale, speed = "fast", angle = 0) {
1316
+ const page = getPage();
1317
+ const node = await findNodeByMatcher(matcher);
1318
+ if (!node)
1319
+ throw new Error(`element not found for pinch: ${JSON.stringify(matcher)}`);
1320
+ await waitForSootsimGestureHandler(page, node.nodeId, "PinchGestureHandler");
1321
+ const center = getNodeCenter(node);
1322
+ const centerPage = await sootsimToPage(page, center.x, center.y);
1323
+ const startSpread = 60;
1324
+ const endSpread = startSpread * Math.max(scale, 0.05);
1325
+ const cos = Math.cos(angle);
1326
+ const sin = Math.sin(angle);
1327
+ const offset = (d) => ({ dx: d * cos, dy: d * sin });
1328
+ const a0 = offset(-startSpread);
1329
+ const b0 = offset(startSpread);
1330
+ const a1 = offset(-endSpread);
1331
+ const b1 = offset(endSpread);
1332
+ await dispatchTwoFingerGesture(
1333
+ page,
1334
+ { x: centerPage.x + a0.dx, y: centerPage.y + a0.dy },
1335
+ { x: centerPage.x + b0.dx, y: centerPage.y + b0.dy },
1336
+ { x: centerPage.x + a1.dx, y: centerPage.y + a1.dy },
1337
+ { x: centerPage.x + b1.dx, y: centerPage.y + b1.dy },
1338
+ { steps: speed === "fast" ? 4 : 12, stepDelayMs: speed === "fast" ? 18 : 24 }
1339
+ );
1340
+ },
1341
+ async rotate(radians, speed = "fast") {
1342
+ const page = getPage();
1343
+ const node = await findNodeByMatcher(matcher);
1344
+ if (!node)
1345
+ throw new Error(`element not found for rotate: ${JSON.stringify(matcher)}`);
1346
+ await waitForSootsimGestureHandler(page, node.nodeId, "RotationGestureHandler");
1347
+ const center = getNodeCenter(node);
1348
+ const centerPage = await sootsimToPage(page, center.x, center.y);
1349
+ const radius = 60;
1350
+ const startA = { x: centerPage.x - radius, y: centerPage.y };
1351
+ const startB = { x: centerPage.x + radius, y: centerPage.y };
1352
+ const cos = Math.cos(radians);
1353
+ const sin = Math.sin(radians);
1354
+ const endA = {
1355
+ x: centerPage.x - radius * cos,
1356
+ y: centerPage.y - radius * sin
1357
+ };
1358
+ const endB = {
1359
+ x: centerPage.x + radius * cos,
1360
+ y: centerPage.y + radius * sin
1361
+ };
1362
+ await dispatchTwoFingerGesture(page, startA, startB, endA, endB, {
1363
+ steps: speed === "fast" ? 10 : 20,
1364
+ stepDelayMs: speed === "fast" ? 14 : 24
1365
+ });
1366
+ },
1367
+ async takeScreenshot(name) {
1368
+ const node = await findNodeByMatcher(matcher);
1369
+ if (!node)
1370
+ throw new Error(
1371
+ `element not found for takeScreenshot: ${JSON.stringify(matcher)}`
1372
+ );
1373
+ return writeSootsimScreenshot(name, {
1374
+ crop: {
1375
+ x: node.absolutePosition.x,
1376
+ y: node.absolutePosition.y,
1377
+ w: node.layout.width,
1378
+ h: node.layout.height
1379
+ }
1380
+ });
1381
+ },
1382
+ async getAttributes() {
1383
+ const node = await findNodeByMatcher(matcher);
1384
+ if (!node)
1385
+ throw new Error(`element not found for getAttributes: ${JSON.stringify(matcher)}`);
1386
+ return {
1387
+ text: node.text || "",
1388
+ label: node.accessibilityLabel || node.text || "",
1389
+ identifier: node.testID || node.id || "",
1390
+ visible: node.layout.width > 0 && node.layout.height > 0,
1391
+ enabled: !node.accessibilityState?.disabled,
1392
+ ...node
1393
+ };
1394
+ },
1395
+ atIndex(_index) {
1396
+ return el;
1397
+ }
1398
+ };
1399
+ return el;
1400
+ }
1401
+ function element(matcher) {
1402
+ return createSootElement(matcher);
1403
+ }
1404
+ var device = {
1405
+ _currentUrl: "",
1406
+ _platform: DETOX_PLATFORM,
1407
+ async launchApp(opts) {
1408
+ if (!_browser) {
1409
+ _browser = await import_playwright.chromium.launch({ headless: true });
1410
+ }
1411
+ if (!_page || opts?.newInstance || opts?.delete || opts?.recordVideoDir) {
1412
+ await closeContext();
1413
+ const contextOpts = {
1414
+ hasTouch: true,
1415
+ viewport: { width: 500, height: 900 }
1416
+ };
1417
+ if (opts?.recordVideoDir) {
1418
+ fs.mkdirSync(opts.recordVideoDir, { recursive: true });
1419
+ contextOpts.recordVideo = {
1420
+ dir: opts.recordVideoDir,
1421
+ size: { width: 500, height: 900 }
1422
+ };
1423
+ }
1424
+ _context = await _browser.newContext(contextOpts);
1425
+ _page = await _context.newPage();
1426
+ attachPageDiagnostics(_page);
1427
+ }
1428
+ const url = opts?.url || BASE_URL;
1429
+ device._currentUrl = url;
1430
+ await _page.goto(url, { waitUntil: "load", timeout: 3e4 });
1431
+ await waitForSootsimTree(_page, 3e4);
1432
+ device._lastReadyAtMs = Date.now();
1433
+ await _page.waitForTimeout(500);
1434
+ },
1435
+ _lastReadyAtMs: 0,
1436
+ async reloadReactNative() {
1437
+ const page = getPage();
1438
+ await page.reload({ waitUntil: "load", timeout: 3e4 });
1439
+ await waitForSootsimTree(page, 3e4);
1440
+ await page.waitForTimeout(500);
1441
+ },
1442
+ async terminateApp() {
1443
+ await closeBrowser();
1444
+ },
1445
+ async installApp() {
1446
+ },
1447
+ async uninstallApp() {
1448
+ },
1449
+ async openURL(url) {
1450
+ const page = getPage();
1451
+ await page.goto(url.url, { waitUntil: "load", timeout: 3e4 });
1452
+ await waitForSootsimTree(page, 1e4);
1453
+ },
1454
+ async takeScreenshot(name) {
1455
+ return writeSootsimScreenshot(name);
1456
+ },
1457
+ // single-shot variant for multi-stage animation captures. skips the
1458
+ // frame-stability poll so callers can sample mid-transition.
1459
+ async takeScreenshotFast(name) {
1460
+ ensureScreenshotDir();
1461
+ const screenshotPath = path.join(SCREENSHOT_DIR, `${name}.png`);
1462
+ fs.writeFileSync(screenshotPath, await captureSootsimPngFast());
1463
+ return screenshotPath;
1464
+ },
1465
+ // record the SootSim canvas via the engine's headless recorder
1466
+ // (window.__sootsimRecorder — @sootsim/plugin-recording). this is the
1467
+ // SAME code path `bun sootsim record video` drives, NOT the rail-button
1468
+ // path (SootSim.bridges.startRecording wires the dialog state machine
1469
+ // and never stashes the resulting blob into __sootsimRecorder.lastBlob,
1470
+ // which is what getBlobBase64 streams from).
1471
+ //
1472
+ // unlike playwright's page-level recordVideo, the headless recorder
1473
+ // captures ONLY the device canvas (composited shell + tenant surfaces)
1474
+ // — no browser chrome, no menu bar, no device frame. uses the EXISTING
1475
+ // page/context so per-test state is preserved across capture.
1476
+ async startBridgeRecording(opts) {
1477
+ const page = getPage();
1478
+ const startOpts = {
1479
+ format: "webm",
1480
+ fps: opts?.fps ?? 60,
1481
+ layers: opts?.layers ?? "tenant",
1482
+ durationMs: opts?.durationMs ?? 5e3
1483
+ };
1484
+ const result = await page.evaluate(async (startOpts2) => {
1485
+ const rec = window.__sootsimRecorder;
1486
+ const start = rec?.start;
1487
+ if (typeof start !== "function") {
1488
+ return { ok: false, error: "__sootsimRecorder.start not installed" };
1489
+ }
1490
+ return start(startOpts2);
1491
+ }, startOpts);
1492
+ if (!result.ok) {
1493
+ throw new Error(`startBridgeRecording failed: ${result.error ?? "unknown"}`);
1494
+ }
1495
+ },
1496
+ // stop the headless recorder, stream the resulting blob in chunks (same
1497
+ // protocol as `bun sootsim record`), and write the webm to outPath.
1498
+ async stopAndSaveBridgeRecording(outPath) {
1499
+ const page = getPage();
1500
+ const stopResult = await page.evaluate(async () => {
1501
+ const rec = window.__sootsimRecorder;
1502
+ const stop = rec?.stop;
1503
+ if (typeof stop !== "function") {
1504
+ return { ok: false, error: "__sootsimRecorder.stop not installed" };
1505
+ }
1506
+ return stop();
1507
+ });
1508
+ if (!stopResult.ok) {
1509
+ throw new Error(`stopBridgeRecording failed: ${stopResult.error ?? "unknown"}`);
1510
+ }
1511
+ if (!stopResult.size) {
1512
+ throw new Error("stopBridgeRecording: recorder returned empty blob");
1513
+ }
1514
+ const chunks = [];
1515
+ let offset = 0;
1516
+ const CHUNK = 2 * 1024 * 1024;
1517
+ while (true) {
1518
+ const result = await page.evaluate(
1519
+ ({ offset: offset2, chunk }) => {
1520
+ const rec = window.__sootsimRecorder;
1521
+ const get = rec?.getBlobBase64;
1522
+ if (typeof get !== "function") return null;
1523
+ return get({ offset: offset2, chunk });
1524
+ },
1525
+ { offset, chunk: CHUNK }
1526
+ );
1527
+ if (!result) throw new Error("SootSim recorder produced no blob");
1528
+ chunks.push(Buffer.from(result.data, "base64"));
1529
+ offset = result.offset;
1530
+ if (result.done) break;
1531
+ }
1532
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
1533
+ fs.writeFileSync(outPath, Buffer.concat(chunks));
1534
+ this._lastRecordingPath = outPath;
1535
+ return outPath;
1536
+ },
1537
+ _lastRecordingPath: "",
1538
+ // set the engine's ContextMenu animation-progress override. visual-proof
1539
+ // harnesses use this to drive each capture stage to a known animProgress
1540
+ // (matched against the iOS oracle's curve) instead of fighting capture
1541
+ // latency vs. sub-second spring timing. pass null to clear.
1542
+ //
1543
+ // dispatches a host CustomEvent that the engine forwards to the tenant
1544
+ // worker (see shell-event-registry.ts + input-bridge.ts handleShellEvent).
1545
+ // the worker writes globalThis.__sootsimMenuAnimOverride which the engine
1546
+ // ContextMenu reads each rAF tick.
1547
+ async setMenuAnimOverride(progress) {
1548
+ await dispatchSootsimEventInPage("sootsim:menuAnimOverride", { value: progress });
1549
+ },
1550
+ async setBottomTabAnimOverride(value) {
1551
+ await dispatchSootsimEventInPage("sootsim:bottomTabAnimOverride", { value });
1552
+ },
1553
+ // FAST multi-stage capture for sub-second animation diffs. each stage
1554
+ // schedules a setTimeout at its requested offsetMs and at that moment
1555
+ // SYNCHRONOUSLY snapshots the engine's `liveComposite` host-side canvas
1556
+ // (drawImage from the worker-transferred home + overlay canvases). PNG
1557
+ // encoding is queued after all snapshots land — encoding stalls don't
1558
+ // contaminate the animation window.
1559
+ //
1560
+ // unlike `takeScreenshotStages`, this does NOT go through screenshot()
1561
+ // (which calls forceRenderAll + composite + toDataURL serially at
1562
+ // 300-1300ms per call and blows the spring-open timing). the worker's
1563
+ // vsync-pumped rAF keeps the home canvas fresh; we just sample it.
1564
+ //
1565
+ // returns paths in stage order; actualMs reports when the snapshot
1566
+ // actually fired (typically within a few ms of offsetMs).
1567
+ async takeScreenshotStagesFast(namePrefix, stages) {
1568
+ const page = getPage();
1569
+ const frames = await page.evaluate(
1570
+ async (input) => {
1571
+ const sim = window;
1572
+ const screenshot = sim.SootSim?.bridges?.screenshot;
1573
+ const live = sim.SootSim?.bridges?.liveComposite;
1574
+ if (typeof screenshot !== "function") {
1575
+ throw new Error("SootSim screenshot bridge is not installed");
1576
+ }
1577
+ await screenshot();
1578
+ const start = performance.now();
1579
+ const snaps = await Promise.all(
1580
+ input.stages.map(
1581
+ (stage) => new Promise((resolve) => {
1582
+ const fire = async () => {
1583
+ const actualMs = performance.now() - start;
1584
+ try {
1585
+ let dataUrl = null;
1586
+ if (live && typeof live.captureFresh === "function") {
1587
+ const { canvas } = await live.captureFresh();
1588
+ if (canvas) {
1589
+ const bmp = await createImageBitmap(canvas);
1590
+ const enc = document.createElement("canvas");
1591
+ enc.width = bmp.width;
1592
+ enc.height = bmp.height;
1593
+ const ctx = enc.getContext("2d");
1594
+ if (ctx) {
1595
+ ctx.drawImage(bmp, 0, 0);
1596
+ dataUrl = enc.toDataURL("image/png");
1597
+ }
1598
+ bmp.close();
1599
+ }
1600
+ }
1601
+ if (!dataUrl) {
1602
+ dataUrl = await screenshot();
1603
+ }
1604
+ resolve({
1605
+ label: stage.label,
1606
+ offsetMs: stage.offsetMs,
1607
+ actualMs,
1608
+ dataUrl
1609
+ });
1610
+ } catch {
1611
+ resolve({
1612
+ label: stage.label,
1613
+ offsetMs: stage.offsetMs,
1614
+ actualMs,
1615
+ dataUrl: null
1616
+ });
1617
+ }
1618
+ };
1619
+ setTimeout(
1620
+ () => {
1621
+ void fire();
1622
+ },
1623
+ Math.max(0, stage.offsetMs)
1624
+ );
1625
+ })
1626
+ )
1627
+ );
1628
+ const out = [];
1629
+ for (const snap of snaps) {
1630
+ if (!snap.dataUrl) continue;
1631
+ out.push({
1632
+ label: snap.label,
1633
+ offsetMs: snap.offsetMs,
1634
+ actualMs: snap.actualMs,
1635
+ dataUrl: snap.dataUrl
1636
+ });
1637
+ }
1638
+ return out;
1639
+ },
1640
+ { stages }
1641
+ );
1642
+ ensureScreenshotDir();
1643
+ return frames.map((f) => {
1644
+ const safeName = `${namePrefix}-${f.label}`.replace(/[^a-zA-Z0-9._-]+/g, "-");
1645
+ const filePath = path.join(SCREENSHOT_DIR, `${safeName}.png`);
1646
+ fs.writeFileSync(filePath, decodePngDataUrl(f.dataUrl));
1647
+ return {
1648
+ label: f.label,
1649
+ offsetMs: f.offsetMs,
1650
+ actualMs: f.actualMs,
1651
+ path: filePath
1652
+ };
1653
+ });
1654
+ },
1655
+ // batched multi-stage capture for animation diffs. all N captures happen
1656
+ // inside one page.evaluate round trip so browser-side timing controls the
1657
+ // offsets — sidesteps the ~300ms per-call bridge overhead that blows
1658
+ // sub-second animation windows. each stage gets a screenshot at the
1659
+ // requested offsetMs (from when the evaluate starts, i.e. right after
1660
+ // trigger() returns). returns absolute paths in stage order.
1661
+ async takeScreenshotStages(namePrefix, stages) {
1662
+ const page = getPage();
1663
+ const frames = await page.evaluate(
1664
+ async (input) => {
1665
+ const screenshot = window.SootSim?.bridges?.screenshot;
1666
+ if (typeof screenshot !== "function") {
1667
+ throw new Error("sootsim screenshot bridge is not installed");
1668
+ }
1669
+ const start = Date.now();
1670
+ const out = [];
1671
+ for (const stage of input.stages) {
1672
+ const elapsed = Date.now() - start;
1673
+ const wait = Math.max(0, stage.offsetMs - elapsed);
1674
+ if (wait > 0) await new Promise((r) => setTimeout(r, wait));
1675
+ const actualMs = Date.now() - start;
1676
+ const dataUrl = await screenshot();
1677
+ out.push({ label: stage.label, offsetMs: stage.offsetMs, actualMs, dataUrl });
1678
+ }
1679
+ return out;
1680
+ },
1681
+ { stages }
1682
+ );
1683
+ ensureScreenshotDir();
1684
+ return frames.map((f) => {
1685
+ const safeName = `${namePrefix}-${f.label}`.replace(/[^a-zA-Z0-9._-]+/g, "-");
1686
+ const filePath = path.join(SCREENSHOT_DIR, `${safeName}.png`);
1687
+ fs.writeFileSync(filePath, decodePngDataUrl(f.dataUrl));
1688
+ return {
1689
+ label: f.label,
1690
+ offsetMs: f.offsetMs,
1691
+ actualMs: f.actualMs,
1692
+ path: filePath
1693
+ };
1694
+ });
1695
+ },
1696
+ async shake() {
1697
+ },
1698
+ async setLocation(lat, lon) {
1699
+ },
1700
+ async setStatusBar(config) {
1701
+ await dispatchStatusBarOverride(config);
1702
+ },
1703
+ async setURLBlacklist(urls) {
1704
+ },
1705
+ async enableSynchronization() {
1706
+ _synchronizationEnabled = true;
1707
+ },
1708
+ async disableSynchronization() {
1709
+ _synchronizationEnabled = false;
1710
+ },
1711
+ getPlatform() {
1712
+ return device._platform;
1713
+ },
1714
+ async pressBack() {
1715
+ if (device._platform !== "android") return;
1716
+ await element(by.id("android-nav-back")).tap();
1717
+ },
1718
+ // raw coordinate tap, bypassing the element-based visibility/hit-test path.
1719
+ // mirrors detox 20.x device.tap({ x, y }) on real iOS — used when the
1720
+ // target is occluded by a native UIWindow (e.g. UIMenu's dim layer) so
1721
+ // `element(...).tap({x,y})` would fail Detox's visibility threshold.
1722
+ async tap(point) {
1723
+ if (point == null || typeof point === "boolean") return;
1724
+ const page = getPage();
1725
+ const { x, y } = point;
1726
+ await page.evaluate(
1727
+ async ({ x: tx, y: ty }) => {
1728
+ const interactTap = window.SootSim?.bridges?.interact?.tap;
1729
+ if (typeof interactTap === "function") {
1730
+ const result = await interactTap(tx, ty);
1731
+ if (result && (typeof result !== "object" || result.hit !== false)) return;
1732
+ }
1733
+ const tap = window.__sootsimTest?.tap;
1734
+ if (typeof tap !== "function") {
1735
+ throw new Error("sootsim tap bridge is not installed");
1736
+ }
1737
+ await tap(tx, ty);
1738
+ },
1739
+ { x, y }
1740
+ );
1741
+ await page.waitForTimeout(50);
1742
+ if (_synchronizationEnabled) {
1743
+ await waitForSootsimIdle(page);
1744
+ }
1745
+ },
1746
+ async sendToHome() {
1747
+ },
1748
+ // get direct access to the playwright page for advanced use
1749
+ getPage() {
1750
+ return getPage();
1751
+ }
1752
+ };
1753
+ var sootExpect = createExpect(findNodeByMatcher, getPage);
1754
+ var sootWaitFor = createWaitFor(findNodeByMatcher, getPage);
1755
+ async function cleanup() {
1756
+ if (_browser) {
1757
+ await _browser.close();
1758
+ _browser = null;
1759
+ _page = null;
1760
+ }
1761
+ }
1762
+ // Annotate the CommonJS export names for ESM import in node:
1763
+ 0 && (module.exports = {
1764
+ by,
1765
+ cleanup,
1766
+ device,
1767
+ element,
1768
+ expect,
1769
+ waitFor
1770
+ });