sootsim 0.1.83 → 0.1.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/README.md +0 -1
  2. package/detox/colors.ts +54 -0
  3. package/detox/config-loader.ts +135 -0
  4. package/detox/element-types.ts +36 -0
  5. package/detox/expectations.ts +477 -0
  6. package/detox/gestures.ts +442 -0
  7. package/detox/index.ts +1403 -0
  8. package/detox/jest-environment.ts +86 -0
  9. package/detox/jest-preset.cjs +50 -0
  10. package/detox/matchers.ts +29 -0
  11. package/detox/navigation.ts +43 -0
  12. package/detox/run-test.ts +113 -0
  13. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  14. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  15. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  16. package/detox/screenshots/theme-blue-toggle.png +0 -0
  17. package/detox/screenshots/theme-blue.png +0 -0
  18. package/detox/screenshots/theme-red-toggle.png +0 -0
  19. package/detox/screenshots/theme-red.png +0 -0
  20. package/dist-cli/bin.js +3 -3
  21. package/dist-cli/chunks/{agent-MQ7GLVIB.js → agent-T3DUH5YJ.js} +2 -2
  22. package/dist-cli/chunks/{agent-wrapper-7KAFDQCN.js → agent-wrapper-NSBF4THI.js} +2 -2
  23. package/dist-cli/chunks/{assert-TV46GUNU.js → assert-X3F7TRCZ.js} +2 -2
  24. package/dist-cli/chunks/auto-bootstrap-47RN2V5G.js +2 -0
  25. package/dist-cli/chunks/beta-BRCGAF2N.js +2 -0
  26. package/dist-cli/chunks/chunk-36RPD6JI.js +2 -0
  27. package/dist-cli/chunks/{chunk-PM5NVKLP.js → chunk-3WGHC7JN.js} +2 -2
  28. package/dist-cli/chunks/chunk-4DBPNLGI.js +1 -0
  29. package/dist-cli/chunks/{chunk-J2GYISVJ.js → chunk-4EVSIUNB.js} +2 -2
  30. package/dist-cli/chunks/{chunk-JHJNODXN.js → chunk-4QZHZ6BC.js} +2 -2
  31. package/dist-cli/chunks/{chunk-F3HP444U.js → chunk-5DIGWOY7.js} +1 -1
  32. package/dist-cli/chunks/{chunk-DP7O5MHK.js → chunk-5N3V7OCG.js} +2 -2
  33. package/dist-cli/chunks/{chunk-Y4BUVURT.js → chunk-5S6D7K4L.js} +2 -2
  34. package/dist-cli/chunks/{chunk-ECJBV65H.js → chunk-7LKUN46F.js} +2 -2
  35. package/dist-cli/chunks/{chunk-WTKTOL3C.js → chunk-AC6QGW22.js} +2 -2
  36. package/dist-cli/chunks/{chunk-IBNRRAES.js → chunk-AFNDVS4E.js} +2 -2
  37. package/dist-cli/chunks/{chunk-6TNANCQC.js → chunk-BESAZ2HA.js} +2 -2
  38. package/dist-cli/chunks/{chunk-WN7M3QON.js → chunk-BHZJ6RIH.js} +2 -2
  39. package/dist-cli/chunks/{chunk-277XAALA.js → chunk-BZL6D4TV.js} +3 -3
  40. package/dist-cli/chunks/{chunk-CYV6Y6YV.js → chunk-CF2LPRXD.js} +2 -2
  41. package/dist-cli/chunks/chunk-DWTLRPEN.js +79 -0
  42. package/dist-cli/chunks/{chunk-CJY3AVI7.js → chunk-E2QE5FFP.js} +1 -1
  43. package/dist-cli/chunks/chunk-EBEL6TTJ.js +4 -0
  44. package/dist-cli/chunks/{chunk-DM6WT7QM.js → chunk-EFM53PZ5.js} +1 -1
  45. package/dist-cli/chunks/{chunk-YUELRHGB.js → chunk-EKXK3SWK.js} +2 -2
  46. package/dist-cli/chunks/{chunk-4LS5MZAI.js → chunk-G7CIZ5S3.js} +3 -3
  47. package/dist-cli/chunks/{chunk-6NN2D4EJ.js → chunk-GTAD6IUV.js} +1 -1
  48. package/dist-cli/chunks/{chunk-OYMFNU3M.js → chunk-H44IQHKZ.js} +1 -1
  49. package/dist-cli/chunks/{chunk-IP3QJLRH.js → chunk-HQDJ5BOF.js} +1 -1
  50. package/dist-cli/chunks/{chunk-5DJXZIFZ.js → chunk-KUSQ4NNJ.js} +1 -1
  51. package/dist-cli/chunks/{chunk-HAWOAQAG.js → chunk-MAO7F5PH.js} +3 -3
  52. package/dist-cli/chunks/{chunk-572VSFNP.js → chunk-NVTL3JQG.js} +1 -1
  53. package/dist-cli/chunks/{chunk-6XZOEBTZ.js → chunk-O6N2CEET.js} +2 -2
  54. package/dist-cli/chunks/{chunk-HNWEELAE.js → chunk-OISHLFON.js} +1 -1
  55. package/dist-cli/chunks/{chunk-2PY3UZVO.js → chunk-OUNLJM56.js} +2 -2
  56. package/dist-cli/chunks/chunk-OXOARRKR.js +67 -0
  57. package/dist-cli/chunks/{chunk-NXATOWWF.js → chunk-PHPXGLME.js} +1 -1
  58. package/dist-cli/chunks/{chunk-JQ7ZXOXJ.js → chunk-PQFFUJR6.js} +2 -2
  59. package/dist-cli/chunks/{chunk-KASUZ5XV.js → chunk-QLJNSOS7.js} +1 -1
  60. package/dist-cli/chunks/chunk-QQAECG5B.js +2 -0
  61. package/dist-cli/chunks/{chunk-FJYT7XL2.js → chunk-RZHREO3M.js} +2 -2
  62. package/dist-cli/chunks/{chunk-FRM355UL.js → chunk-SBGOUA6F.js} +2 -2
  63. package/dist-cli/chunks/chunk-SSCA2AEA.js +1 -0
  64. package/dist-cli/chunks/{chunk-Y2VJBRSP.js → chunk-UYRGCJ4N.js} +1 -1
  65. package/dist-cli/chunks/{chunk-2AWQ7OB2.js → chunk-WGDL5V6C.js} +1 -1
  66. package/dist-cli/chunks/{chunk-VMXWC2JO.js → chunk-Y5PLPEEU.js} +2 -2
  67. package/dist-cli/chunks/chunk-ZFAM4N5B.js +1 -0
  68. package/dist-cli/chunks/{chunk-RH4F2TF7.js → chunk-ZO3VHP6W.js} +1 -1
  69. package/dist-cli/chunks/cli-version-WPFDM2A6.js +2 -0
  70. package/dist-cli/chunks/{compat-QLLWBTS3.js → compat-PCXGGZBZ.js} +3 -3
  71. package/dist-cli/chunks/{config-2DSLDCXV.js → config-LULEVEYL.js} +2 -2
  72. package/dist-cli/chunks/control-6P6HY7UF.js +2 -0
  73. package/dist-cli/chunks/{cpu-profile-GEIKHCPC.js → cpu-profile-NOK73ZYW.js} +2 -2
  74. package/dist-cli/chunks/{daemon-4EBUFN4D.js → daemon-4A3DMUYL.js} +2 -2
  75. package/dist-cli/chunks/{debug-WGD6XWOF.js → debug-74BWB2ZG.js} +3 -3
  76. package/dist-cli/chunks/{detox-LNKGRZU6.js → detox-HEOMINSC.js} +2 -2
  77. package/dist-cli/chunks/{device-AYKXKVIQ.js → device-TTXXBJFZ.js} +2 -2
  78. package/dist-cli/chunks/{diagnose-TMXSDOOC.js → diagnose-QZ3GOHSE.js} +2 -2
  79. package/dist-cli/chunks/drivers-QRPWNOIT.js +2 -0
  80. package/dist-cli/chunks/{electron-QFPF7TBY.js → electron-QVOWV44R.js} +3 -3
  81. package/dist-cli/chunks/flow-QMA7GVN6.js +2 -0
  82. package/dist-cli/chunks/{hints-MXKRR4TG.js → hints-YKWRNMJC.js} +2 -2
  83. package/dist-cli/chunks/{home-paths-REMWQDAO.js → home-paths-SFADSTJM.js} +2 -2
  84. package/dist-cli/chunks/{inspect-XGSQNFV7.js → inspect-LEWGQCIU.js} +3 -3
  85. package/dist-cli/chunks/install-7N2N7Q32.js +2 -0
  86. package/dist-cli/chunks/{install-desktop-NQG3RZSA.js → install-desktop-22HYQZ2G.js} +3 -3
  87. package/dist-cli/chunks/{keys-5QZWXL3F.js → keys-3ZT3MICU.js} +2 -2
  88. package/dist-cli/chunks/{launch-SBXOZWKO.js → launch-ZXW2NFLG.js} +3 -3
  89. package/dist-cli/chunks/{login-EACQXE24.js → login-NJKJ7GZO.js} +4 -4
  90. package/dist-cli/chunks/{logout-IBQLMUML.js → logout-VMMQL7CB.js} +2 -2
  91. package/dist-cli/chunks/{maestro-LFYXUX7O.js → maestro-OJY4MTI7.js} +2 -2
  92. package/dist-cli/chunks/{preview-U4SBOEGQ.js → preview-QU2GXTEV.js} +2 -2
  93. package/dist-cli/chunks/{profile-GWS5ECMY.js → profile-7APWK47T.js} +2 -2
  94. package/dist-cli/chunks/{react-QDHLMVYL.js → react-RSVO5JZZ.js} +2 -2
  95. package/dist-cli/chunks/{record-BUEUWPDI.js → record-UWH4MDEO.js} +2 -2
  96. package/dist-cli/chunks/runtime-3FUENRHM.js +2 -0
  97. package/dist-cli/chunks/{runtime-delivery-G7L6RVZ7.js → runtime-delivery-QMKGRV7N.js} +2 -2
  98. package/dist-cli/chunks/{screenshot-T2HBA3VI.js → screenshot-43M27ALE.js} +2 -2
  99. package/dist-cli/chunks/{screenshot-mode-EG5HMIH3.js → screenshot-mode-EBYYN6TY.js} +2 -2
  100. package/dist-cli/chunks/{screenshots-S52AFHTV.js → screenshots-7TQZL6Z6.js} +2 -2
  101. package/dist-cli/chunks/{server-MFFVYUGG.js → server-VCFM25Z6.js} +2 -2
  102. package/dist-cli/chunks/setup-repo-HFH4VKJQ.js +2 -0
  103. package/dist-cli/chunks/{skills-HQGWBS2O.js → skills-RQA6EJQL.js} +2 -2
  104. package/dist-cli/chunks/{start-E3DRYY7W.js → start-ZT6MBYND.js} +4 -4
  105. package/dist-cli/chunks/store-BJBTDSZE.js +2 -0
  106. package/dist-cli/chunks/telemetry-ZZZKTILZ.js +2 -0
  107. package/dist-cli/chunks/{test-ZY3EF62K.js → test-RNRX5SWV.js} +3 -3
  108. package/dist-cli/chunks/{three-mode-WSPKQCJ5.js → three-mode-TQZH25ZO.js} +2 -2
  109. package/dist-cli/chunks/{timeline-3XAB5EWZ.js → timeline-GGN3AY6P.js} +2 -2
  110. package/dist-cli/chunks/{upgrade-WNENPFM5.js → upgrade-XT22D67C.js} +2 -2
  111. package/dist-cli/chunks/upload-NC2AYLC5.js +2 -0
  112. package/dist-cli/chunks/{web-D2AOZY44.js → web-KEHVF5MB.js} +2 -2
  113. package/dist-cli/chunks/{what-happened-F43KNSG6.js → what-happened-PATQRJ5T.js} +2 -2
  114. package/dist-cli/chunks/{whoami-T22VBR7C.js → whoami-CXVY26VV.js} +2 -2
  115. package/dist-lib/agent-daemon-client.cjs +1 -1
  116. package/dist-lib/agent-events.cjs +1 -1
  117. package/dist-lib/agent-sessions.cjs +1 -1
  118. package/dist-lib/attached-projects.cjs +1 -1
  119. package/dist-lib/auth/shared-session.cjs +1 -1
  120. package/dist-lib/backend-origin.cjs +1 -1
  121. package/dist-lib/beta.cjs +44 -0
  122. package/dist-lib/bridge-constants.cjs +1 -1
  123. package/dist-lib/cli-constants.cjs +1 -1
  124. package/dist-lib/config.cjs +1 -1
  125. package/dist-lib/detox/index.cjs +1770 -0
  126. package/dist-lib/detox/jest-preset.cjs +50 -0
  127. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  128. package/dist-lib/home-paths.cjs +1 -1
  129. package/dist-lib/host/bridge-host.cjs +1 -1
  130. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  131. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  132. package/dist-lib/index.cjs +136 -138
  133. package/dist-lib/metro.cjs +31 -26
  134. package/dist-lib/profiles.cjs +1 -1
  135. package/dist-lib/render-mode.cjs +1 -1
  136. package/dist-lib/scripts/demo-app-registry.cjs +809 -0
  137. package/dist-lib/scripts/dev-server-scanner.cjs +1269 -0
  138. package/dist-lib/skills.cjs +17766 -0
  139. package/dist-lib/vite.cjs +129 -39
  140. package/package.json +39 -14
  141. package/scripts/demo-app-registry.ts +989 -0
  142. package/scripts/dev-server-scanner.ts +674 -0
  143. package/src/agent-daemon-client.ts +390 -0
  144. package/src/agent-events.ts +71 -0
  145. package/src/agent-prompt.ts +71 -0
  146. package/src/agent-sessions.ts +572 -0
  147. package/src/attached-projects.ts +536 -0
  148. package/src/auth/shared-session.ts +199 -0
  149. package/src/backend-origin.ts +49 -0
  150. package/src/beta.ts +21 -0
  151. package/src/bridge-constants.ts +10 -0
  152. package/src/cli-constants.ts +1 -0
  153. package/src/cli-version.ts +30 -0
  154. package/src/codex-client.ts +215 -0
  155. package/src/config.ts +110 -0
  156. package/src/dev-bundle-resolution.ts +180 -0
  157. package/src/home-paths.ts +382 -0
  158. package/src/host/agent-host.ts +576 -0
  159. package/src/host/bridge-host.ts +2293 -0
  160. package/src/host/fetch-proxy-handler.ts +288 -0
  161. package/src/host/fetch-proxy-overrides.ts +39 -0
  162. package/src/host/open-url.ts +234 -0
  163. package/src/index.ts +9 -0
  164. package/src/metro-plugin.ts +139 -0
  165. package/src/native-dev-bundle-url.ts +62 -0
  166. package/src/native-seam-manifest.ts +313 -0
  167. package/src/profiles.ts +179 -0
  168. package/src/render-mode.ts +27 -0
  169. package/src/runtime-assets.ts +84 -0
  170. package/src/runtime-delivery.ts +334 -0
  171. package/src/screenshots/compose.ts +422 -0
  172. package/src/screenshots/frame-compose.ts +438 -0
  173. package/src/screenshots/orchestrate.ts +244 -0
  174. package/src/screenshots/registry.ts +58 -0
  175. package/src/screenshots/schema.ts +364 -0
  176. package/src/skills/builtin/a11y-review.ts +126 -0
  177. package/src/skills/builtin/compat-check.ts +104 -0
  178. package/src/skills/builtin/perf-profile.ts +84 -0
  179. package/src/skills/builtin/screenshot-all.ts +46 -0
  180. package/src/skills/builtin/test-flow.ts +118 -0
  181. package/src/skills/builtin/visual-diff.ts +94 -0
  182. package/src/skills/registry.ts +107 -0
  183. package/src/skills/types.ts +41 -0
  184. package/src/vite-plugin-one.ts +189 -0
  185. package/src/vite-plugin.ts +1381 -0
  186. package/src/worklets-babel.ts +132 -0
  187. package/dist-cli/chunks/auto-bootstrap-FQS4ZD2K.js +0 -2
  188. package/dist-cli/chunks/beta-VG7CDY2U.js +0 -2
  189. package/dist-cli/chunks/chunk-2OIBDYHW.js +0 -1
  190. package/dist-cli/chunks/chunk-6BNLVMXA.js +0 -1
  191. package/dist-cli/chunks/chunk-6XD6CBJM.js +0 -2
  192. package/dist-cli/chunks/chunk-CHQTO426.js +0 -1
  193. package/dist-cli/chunks/chunk-FAPYGVIU.js +0 -4
  194. package/dist-cli/chunks/chunk-PEHFE3LG.js +0 -64
  195. package/dist-cli/chunks/chunk-RXH2SLKF.js +0 -2
  196. package/dist-cli/chunks/chunk-UXQWC5ZR.js +0 -79
  197. package/dist-cli/chunks/chunk-XFQL74PF.js +0 -5
  198. package/dist-cli/chunks/cli-version-PWF6I6LY.js +0 -2
  199. package/dist-cli/chunks/control-UIOXGYXU.js +0 -2
  200. package/dist-cli/chunks/demo-app-registry-G3BDOFWC.js +0 -2
  201. package/dist-cli/chunks/drivers-IDQF34HP.js +0 -2
  202. package/dist-cli/chunks/flow-3JN3Y7RF.js +0 -2
  203. package/dist-cli/chunks/install-2N3YOOSN.js +0 -2
  204. package/dist-cli/chunks/runtime-PVB4VGUH.js +0 -2
  205. package/dist-cli/chunks/setup-repo-YOF7NV5D.js +0 -2
  206. package/dist-cli/chunks/store-MAI6D3UO.js +0 -2
  207. package/dist-cli/chunks/telemetry-RCQKCJTH.js +0 -2
  208. package/dist-cli/chunks/upload-YLJ4RA73.js +0 -2
  209. package/dist-lib/vite-base.cjs +0 -6937
package/README.md CHANGED
@@ -184,7 +184,6 @@ the frame for quick art-direction passes.
184
184
  the published CLI registry and the generated website docs both come from
185
185
  `packages/sootsim-skills/`.
186
186
 
187
- - `packages/sootsim/cli/registry.ts` re-exports the shared CLI metadata
188
187
  - `packages/sootsim/skills/soot.md` is generated from that registry
189
188
  - `src/features/site/docs/sootsim/cli/*` is generated output, not hand-edited source
190
189
 
@@ -0,0 +1,54 @@
1
+ // re-export color utilities from kitchen-sink for convenience
2
+ // these are the same implementations used in detox tests
3
+
4
+ import * as fs from 'fs'
5
+ import { PNG } from 'pngjs'
6
+
7
+ export type RGB = { r: number; g: number; b: number }
8
+
9
+ export function getDominantColor(screenshotPath: string): RGB {
10
+ const data = fs.readFileSync(screenshotPath)
11
+ const png = PNG.sync.read(data)
12
+
13
+ const startX = Math.floor(png.width * 0.25)
14
+ const endX = Math.floor(png.width * 0.75)
15
+ const startY = Math.floor(png.height * 0.25)
16
+ const endY = Math.floor(png.height * 0.75)
17
+
18
+ let totalR = 0,
19
+ totalG = 0,
20
+ totalB = 0,
21
+ count = 0
22
+
23
+ for (let y = startY; y < endY; y++) {
24
+ for (let x = startX; x < endX; x++) {
25
+ const idx = (png.width * y + x) * 4
26
+ totalR += png.data[idx]
27
+ totalG += png.data[idx + 1]
28
+ totalB += png.data[idx + 2]
29
+ count++
30
+ }
31
+ }
32
+
33
+ return {
34
+ r: Math.round(totalR / count),
35
+ g: Math.round(totalG / count),
36
+ b: Math.round(totalB / count),
37
+ }
38
+ }
39
+
40
+ export function isBlueish(color: RGB): boolean {
41
+ return color.b > 100 && color.b > color.r && color.b > color.g
42
+ }
43
+
44
+ export function isReddish(color: RGB): boolean {
45
+ return color.r > 100 && color.r > color.b && color.r > color.g
46
+ }
47
+
48
+ export function isGreenish(color: RGB): boolean {
49
+ return color.g > 100 && color.g > color.r && color.g > color.b
50
+ }
51
+
52
+ export function formatRGB(color: RGB): string {
53
+ return `RGB(${color.r}, ${color.g}, ${color.b})`
54
+ }
@@ -0,0 +1,135 @@
1
+ // parse detox configuration from various locations
2
+ // maps detox device config to sootsim settings
3
+
4
+ import * as fs from 'fs'
5
+ import * as path from 'path'
6
+
7
+ export interface DetoxConfig {
8
+ testRunner?: {
9
+ args?: Record<string, any>
10
+ jest?: { setupTimeout?: number; teardownTimeout?: number }
11
+ }
12
+ apps?: Record<string, { type: string; binaryPath?: string; build?: string }>
13
+ devices?: Record<string, { type: string; device?: { type?: string } }>
14
+ configurations?: Record<string, { device?: string; app?: string }>
15
+ testMatch?: string[]
16
+ testDir?: string
17
+ }
18
+
19
+ export interface SootSimDetoxConfig {
20
+ testFiles: string[]
21
+ testTimeout: number
22
+ sootsimUrl: string
23
+ deviceModel: string
24
+ maxWorkers: number
25
+ }
26
+
27
+ // search order: .detoxrc.js, .detoxrc.json, detox.config.js, detox.config.json, package.json#detox
28
+ export function loadDetoxConfig(projectDir: string): DetoxConfig | null {
29
+ const candidates = [
30
+ '.detoxrc.js',
31
+ '.detoxrc.json',
32
+ 'detox.config.js',
33
+ 'detox.config.json',
34
+ ]
35
+
36
+ for (const file of candidates) {
37
+ const filePath = path.join(projectDir, file)
38
+ if (fs.existsSync(filePath)) {
39
+ try {
40
+ if (file.endsWith('.json')) {
41
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'))
42
+ }
43
+ // for JS files, we can't easily require them without running them
44
+ // return a basic config indicating file was found
45
+ return { testDir: 'e2e' }
46
+ } catch {
47
+ continue
48
+ }
49
+ }
50
+ }
51
+
52
+ // check package.json
53
+ const pkgPath = path.join(projectDir, 'package.json')
54
+ if (fs.existsSync(pkgPath)) {
55
+ try {
56
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
57
+ if (pkg.detox) return pkg.detox
58
+ } catch {}
59
+ }
60
+
61
+ return null
62
+ }
63
+
64
+ // find test files matching detox patterns
65
+ export function findDetoxTestFiles(
66
+ projectDir: string,
67
+ config: DetoxConfig | null,
68
+ ): string[] {
69
+ const testDirs = ['e2e', 'test/e2e', 'detox', '__tests__/e2e']
70
+ const patterns = [
71
+ '.test.ts',
72
+ '.test.js',
73
+ '.test.tsx',
74
+ '.test.jsx',
75
+ '.spec.ts',
76
+ '.spec.js',
77
+ ]
78
+
79
+ // if config specifies a test dir, prioritize it
80
+ if (config?.testDir) {
81
+ testDirs.unshift(config.testDir)
82
+ }
83
+
84
+ const files: string[] = []
85
+
86
+ for (const dir of testDirs) {
87
+ const fullDir = path.join(projectDir, dir)
88
+ if (!fs.existsSync(fullDir)) continue
89
+
90
+ const entries = fs.readdirSync(fullDir, { recursive: true })
91
+ for (const entry of entries) {
92
+ const entryStr = String(entry)
93
+ if (patterns.some((p) => entryStr.endsWith(p))) {
94
+ files.push(path.join(fullDir, entryStr))
95
+ }
96
+ }
97
+ }
98
+
99
+ return files
100
+ }
101
+
102
+ // map detox device type to sootsim device model
103
+ export function mapDetoxDevice(config: DetoxConfig | null): string {
104
+ if (!config?.devices) return 'iphone-15-pro'
105
+
106
+ const devices = Object.values(config.devices)
107
+ for (const dev of devices) {
108
+ const type = dev.device?.type?.toLowerCase() || ''
109
+ if (type.includes('se')) return 'iphone-se'
110
+ if (type.includes('16 pro max') || type.includes('16-pro-max'))
111
+ return 'iphone-16-pro-max'
112
+ if (type.includes('16 pro') || type.includes('16-pro')) return 'iphone-16-pro'
113
+ if (type.includes('16')) return 'iphone-16'
114
+ if (type.includes('15 pro max') || type.includes('15-pro-max'))
115
+ return 'iphone-15-pro-max'
116
+ if (type.includes('15 pro') || type.includes('15-pro')) return 'iphone-15-pro'
117
+ if (type.includes('15')) return 'iphone-15'
118
+ }
119
+
120
+ return 'iphone-15-pro'
121
+ }
122
+
123
+ // generate a sootsim-compatible config from detox config
124
+ export function toSootSimConfig(
125
+ projectDir: string,
126
+ config: DetoxConfig | null,
127
+ ): SootSimDetoxConfig {
128
+ return {
129
+ testFiles: findDetoxTestFiles(projectDir, config),
130
+ testTimeout: config?.testRunner?.jest?.setupTimeout || 120000,
131
+ sootsimUrl: process.env.SOOTSIM_URL || 'http://localhost:5173',
132
+ deviceModel: mapDetoxDevice(config),
133
+ maxWorkers: 1,
134
+ }
135
+ }
@@ -0,0 +1,36 @@
1
+ import type { Matcher } from './matchers'
2
+
3
+ // element interaction object returned by element(matcher)
4
+ export interface SootElement {
5
+ // reference to the matcher for lazy evaluation
6
+ _matcher: Matcher
7
+
8
+ tap(point?: { x?: number; y?: number }): Promise<void>
9
+ multiTap(times: number): Promise<void>
10
+ longPress(duration?: number): Promise<void>
11
+ longPressAndDrag(
12
+ duration: number,
13
+ normalizedStartX: number,
14
+ normalizedStartY: number,
15
+ targetElement: SootElement,
16
+ normalizedEndX: number,
17
+ normalizedEndY: number,
18
+ speed?: string,
19
+ holdDuration?: number,
20
+ ): Promise<void>
21
+ typeText(text: string): Promise<void>
22
+ replaceText(text: string): Promise<void>
23
+ clearText(): Promise<void>
24
+ scroll(pixels: number, direction: 'up' | 'down' | 'left' | 'right'): Promise<void>
25
+ scrollTo(edge: 'top' | 'bottom' | 'left' | 'right'): Promise<void>
26
+ swipe(
27
+ direction: 'up' | 'down' | 'left' | 'right',
28
+ speed?: string,
29
+ percentage?: number,
30
+ ): Promise<void>
31
+ pinch(scale: number, speed?: 'fast' | 'slow', angle?: number): Promise<void>
32
+ rotate(radians: number, speed?: 'fast' | 'slow'): Promise<void>
33
+ takeScreenshot(name: string): Promise<string>
34
+ getAttributes(): Promise<any>
35
+ atIndex(index: number): SootElement
36
+ }
@@ -0,0 +1,477 @@
1
+ // detox-compatible expectations for sootsim
2
+ // expect(element).toExist(), .toBeVisible(), .toHaveText(), etc.
3
+ // waitFor(element).toExist().withTimeout(ms)
4
+
5
+ import { dragScrollNode, readSootsimInteractiveViewport } from './gestures'
6
+ import type { SootElement } from './element-types'
7
+ import type { Matcher } from './matchers'
8
+ import type { Page } from 'playwright'
9
+
10
+ type FindFn = (matcher: Matcher) => Promise<any>
11
+ type GetPageFn = () => Page
12
+ const DEFAULT_VISIBILITY_THRESHOLD = 0.75
13
+
14
+ async function scrollMatcherElement(
15
+ page: Page,
16
+ findNode: FindFn,
17
+ matcher: Matcher,
18
+ pixels: number,
19
+ direction: 'up' | 'down' | 'left' | 'right',
20
+ ): Promise<void> {
21
+ const node = await findNode(matcher)
22
+ if (!node) {
23
+ throw new Error(`scroll container not found: ${JSON.stringify(matcher)}`)
24
+ }
25
+ await dragScrollNode(page, node, pixels, direction)
26
+ }
27
+
28
+ type Viewport = { width: number; height: number }
29
+
30
+ function isNodeVisibleInViewport(node: any, viewport: Viewport): boolean {
31
+ if (!node || node.layout.width <= 0 || node.layout.height <= 0) {
32
+ return false
33
+ }
34
+
35
+ const frame = node.visibleFrame ?? {
36
+ x: node.absolutePosition?.x ?? node.layout?.x ?? 0,
37
+ y: node.absolutePosition?.y ?? node.layout?.y ?? 0,
38
+ width: node.layout.width,
39
+ height: node.layout.height,
40
+ }
41
+ const right = frame.x + frame.width
42
+ const bottom = frame.y + frame.height
43
+
44
+ const visibleWidth = Math.max(0, Math.min(right, viewport.width) - Math.max(frame.x, 0))
45
+ const visibleHeight = Math.max(
46
+ 0,
47
+ Math.min(bottom, viewport.height) - Math.max(frame.y, 0),
48
+ )
49
+ const visibleArea = visibleWidth * visibleHeight
50
+ const totalArea = node.layout.width * node.layout.height
51
+
52
+ return totalArea > 0 && visibleArea / totalArea >= DEFAULT_VISIBILITY_THRESHOLD
53
+ }
54
+
55
+ async function isNodeVisible(page: Page, node: any): Promise<boolean> {
56
+ if (!node || node.layout.width <= 0 || node.layout.height <= 0) {
57
+ return false
58
+ }
59
+ return isNodeVisibleInViewport(node, await readSootsimInteractiveViewport(page))
60
+ }
61
+
62
+ function describeNodeVisibility(node: any, viewport?: Viewport): string {
63
+ if (!node) return 'not found'
64
+ const absX = node.absolutePosition?.x ?? node.layout?.x ?? 0
65
+ const absY = node.absolutePosition?.y ?? node.layout?.y ?? 0
66
+ return JSON.stringify({
67
+ layout: node.layout,
68
+ absolutePosition: node.absolutePosition ?? null,
69
+ frame: {
70
+ x: absX,
71
+ y: absY,
72
+ right: absX + (node.layout?.width ?? 0),
73
+ bottom: absY + (node.layout?.height ?? 0),
74
+ },
75
+ visibleFrame: node.visibleFrame ?? null,
76
+ viewport: viewport ?? null,
77
+ style: node.style ?? null,
78
+ })
79
+ }
80
+
81
+ // expect(element) returns an object with assertion methods
82
+ export function createExpect(findNode: FindFn, getPage: GetPageFn) {
83
+ return function sootExpect(el: SootElement): SootExpectation {
84
+ return new SootExpectation(el._matcher, findNode, getPage, false)
85
+ }
86
+ }
87
+
88
+ class SootExpectation {
89
+ private matcher: Matcher
90
+ private findNode: FindFn
91
+ private getPage: GetPageFn
92
+ private negated: boolean
93
+
94
+ constructor(matcher: Matcher, findNode: FindFn, getPage: GetPageFn, negated: boolean) {
95
+ this.matcher = matcher
96
+ this.findNode = findNode
97
+ this.getPage = getPage
98
+ this.negated = negated
99
+ }
100
+
101
+ get not(): SootExpectation {
102
+ return new SootExpectation(this.matcher, this.findNode, this.getPage, !this.negated)
103
+ }
104
+
105
+ async toExist(): Promise<void> {
106
+ const node = await this.findNode(this.matcher)
107
+ const exists = node !== null && node !== undefined
108
+ if (this.negated) {
109
+ if (exists) {
110
+ throw new Error(
111
+ `expected element NOT to exist but it does: ${JSON.stringify(this.matcher)}`,
112
+ )
113
+ }
114
+ } else {
115
+ if (!exists) {
116
+ throw new Error(
117
+ `expected element to exist but it was not found: ${JSON.stringify(this.matcher)}`,
118
+ )
119
+ }
120
+ }
121
+ }
122
+
123
+ async toBeVisible(): Promise<void> {
124
+ const node = await this.findNode(this.matcher)
125
+ const page = this.getPage()
126
+ const viewport = node ? await readSootsimInteractiveViewport(page) : undefined
127
+ const visible = viewport ? isNodeVisibleInViewport(node, viewport) : false
128
+ if (this.negated) {
129
+ if (visible) {
130
+ throw new Error(
131
+ `expected element NOT to be visible: ${JSON.stringify(this.matcher)}`,
132
+ )
133
+ }
134
+ } else {
135
+ if (!visible) {
136
+ throw new Error(
137
+ `expected element to be visible but it is ${describeNodeVisibility(node, viewport)}: ${JSON.stringify(this.matcher)}`,
138
+ )
139
+ }
140
+ }
141
+ }
142
+
143
+ async toBeNotVisible(): Promise<void> {
144
+ const node = await this.findNode(this.matcher)
145
+ const visible = await isNodeVisible(this.getPage(), node)
146
+ if (visible) {
147
+ throw new Error(
148
+ `expected element NOT to be visible: ${JSON.stringify(this.matcher)}`,
149
+ )
150
+ }
151
+ }
152
+
153
+ async toHaveText(expectedText: string): Promise<void> {
154
+ const node = await this.findNode(this.matcher)
155
+ if (!node) {
156
+ throw new Error(`element not found for toHaveText: ${JSON.stringify(this.matcher)}`)
157
+ }
158
+ const actualText = node.text || ''
159
+ const matches = actualText === expectedText || actualText.includes(expectedText)
160
+
161
+ if (this.negated) {
162
+ if (matches) {
163
+ throw new Error(
164
+ `expected text NOT to be "${expectedText}" but got "${actualText}": ${JSON.stringify(this.matcher)}`,
165
+ )
166
+ }
167
+ } else {
168
+ if (!matches) {
169
+ throw new Error(
170
+ `expected text "${expectedText}" but got "${actualText}": ${JSON.stringify(this.matcher)}`,
171
+ )
172
+ }
173
+ }
174
+ }
175
+
176
+ async toHaveId(expectedId: string): Promise<void> {
177
+ const node = await this.findNode(this.matcher)
178
+ if (!node) {
179
+ throw new Error(`element not found for toHaveId: ${JSON.stringify(this.matcher)}`)
180
+ }
181
+ const hasId = node.testID === expectedId || node.id === expectedId
182
+ if (this.negated) {
183
+ if (hasId) throw new Error(`expected element NOT to have id "${expectedId}"`)
184
+ } else {
185
+ if (!hasId)
186
+ throw new Error(
187
+ `expected element to have id "${expectedId}" but got "${node.testID || node.id}"`,
188
+ )
189
+ }
190
+ }
191
+
192
+ async toHaveLabel(expectedLabel: string): Promise<void> {
193
+ const node = await this.findNode(this.matcher)
194
+ if (!node) {
195
+ throw new Error(
196
+ `element not found for toHaveLabel: ${JSON.stringify(this.matcher)}`,
197
+ )
198
+ }
199
+ // check explicit accessibilityLabel, then derived text content
200
+ const actual = node.accessibilityLabel || node.text || ''
201
+ const matches = actual === expectedLabel
202
+ if (this.negated) {
203
+ if (matches)
204
+ throw new Error(`expected label NOT to be "${expectedLabel}" but it was`)
205
+ } else {
206
+ if (!matches)
207
+ throw new Error(
208
+ `expected label "${expectedLabel}" but got "${actual}": ${JSON.stringify(this.matcher)}`,
209
+ )
210
+ }
211
+ }
212
+
213
+ async toHaveAccessibilityRole(expectedRole: string): Promise<void> {
214
+ const node = await this.findNode(this.matcher)
215
+ if (!node) {
216
+ throw new Error(
217
+ `element not found for toHaveAccessibilityRole: ${JSON.stringify(this.matcher)}`,
218
+ )
219
+ }
220
+ const actual = node.accessibilityRole || ''
221
+ const matches = actual === expectedRole
222
+ if (this.negated) {
223
+ if (matches) throw new Error(`expected role NOT to be "${expectedRole}" but it was`)
224
+ } else {
225
+ if (!matches)
226
+ throw new Error(
227
+ `expected role "${expectedRole}" but got "${actual}": ${JSON.stringify(this.matcher)}`,
228
+ )
229
+ }
230
+ }
231
+
232
+ async toBeEnabled(): Promise<void> {
233
+ const node = await this.findNode(this.matcher)
234
+ if (!node) {
235
+ throw new Error(
236
+ `element not found for toBeEnabled: ${JSON.stringify(this.matcher)}`,
237
+ )
238
+ }
239
+ const disabled = node.accessibilityState?.disabled ?? false
240
+ if (this.negated) {
241
+ if (!disabled) throw new Error(`expected element to be disabled but it is enabled`)
242
+ } else {
243
+ if (disabled) throw new Error(`expected element to be enabled but it is disabled`)
244
+ }
245
+ }
246
+
247
+ async toBeFocused(): Promise<void> {
248
+ // in sootsim we don't track focus state fully, so just check existence
249
+ await this.toExist()
250
+ }
251
+
252
+ async toHaveValue(value: string): Promise<void> {
253
+ const node = await this.findNode(this.matcher)
254
+ if (!node) {
255
+ throw new Error(
256
+ `element not found for toHaveValue: ${JSON.stringify(this.matcher)}`,
257
+ )
258
+ }
259
+ // value could be in text or props
260
+ const actual = node.text || ''
261
+ if (this.negated) {
262
+ if (actual.includes(value)) {
263
+ throw new Error(
264
+ `expected element NOT to have value "${value}" but got "${actual}"`,
265
+ )
266
+ }
267
+ } else {
268
+ if (!actual.includes(value)) {
269
+ throw new Error(`expected element to have value "${value}" but got "${actual}"`)
270
+ }
271
+ }
272
+ }
273
+
274
+ async toHaveSliderPosition(
275
+ normalizedPosition: number,
276
+ tolerance?: number,
277
+ ): Promise<void> {
278
+ // stub for slider tests -- sootsim doesn't have native sliders yet
279
+ await this.toExist()
280
+ }
281
+
282
+ async toHaveToggleValue(value: boolean): Promise<void> {
283
+ await this.toExist()
284
+ }
285
+ }
286
+
287
+ // waitFor(element) returns a chainable object that polls until the assertion passes
288
+ export function createWaitFor(findNode: FindFn, getPage: GetPageFn) {
289
+ return function sootWaitFor(el: SootElement): SootWaitForChain {
290
+ return new SootWaitForChain(el._matcher, findNode, getPage)
291
+ }
292
+ }
293
+
294
+ class SootWaitForChain {
295
+ private matcher: Matcher
296
+ private findNode: FindFn
297
+ private getPage: GetPageFn
298
+ private assertionFn: (() => Promise<void>) | null = null
299
+ private _negated = false
300
+
301
+ constructor(matcher: Matcher, findNode: FindFn, getPage: GetPageFn) {
302
+ this.matcher = matcher
303
+ this.findNode = findNode
304
+ this.getPage = getPage
305
+ }
306
+
307
+ get not(): SootWaitForChain {
308
+ this._negated = true
309
+ return this
310
+ }
311
+
312
+ toExist(): SootWaitForAction {
313
+ this.assertionFn = async () => {
314
+ const node = await this.findNode(this.matcher)
315
+ const exists = node !== null && node !== undefined
316
+ if (this._negated ? exists : !exists) {
317
+ throw new Error(`waitFor toExist failed: ${JSON.stringify(this.matcher)}`)
318
+ }
319
+ }
320
+ return new SootWaitForAction(this.assertionFn, this.findNode, this.getPage)
321
+ }
322
+
323
+ toBeVisible(): SootWaitForAction {
324
+ this.assertionFn = async () => {
325
+ const node = await this.findNode(this.matcher)
326
+ const page = this.getPage()
327
+ const viewport = node ? await readSootsimInteractiveViewport(page) : undefined
328
+ const visible = viewport ? isNodeVisibleInViewport(node, viewport) : false
329
+ if (this._negated ? visible : !visible) {
330
+ throw new Error(
331
+ `waitFor toBeVisible failed (${describeNodeVisibility(node, viewport)}): ${JSON.stringify(this.matcher)}`,
332
+ )
333
+ }
334
+ }
335
+ return new SootWaitForAction(this.assertionFn, this.findNode, this.getPage)
336
+ }
337
+
338
+ toBeNotVisible(): SootWaitForAction {
339
+ this.assertionFn = async () => {
340
+ const node = await this.findNode(this.matcher)
341
+ const visible = await isNodeVisible(this.getPage(), node)
342
+ if (visible) {
343
+ throw new Error(`waitFor toBeNotVisible failed: ${JSON.stringify(this.matcher)}`)
344
+ }
345
+ }
346
+ return new SootWaitForAction(this.assertionFn, this.findNode, this.getPage)
347
+ }
348
+
349
+ toHaveText(expectedText: string): SootWaitForAction {
350
+ this.assertionFn = async () => {
351
+ const node = await this.findNode(this.matcher)
352
+ if (!node)
353
+ throw new Error(
354
+ `waitFor toHaveText: element not found: ${JSON.stringify(this.matcher)}`,
355
+ )
356
+ const actual = node.text || ''
357
+ const matches = actual === expectedText || actual.includes(expectedText)
358
+ if (this._negated ? matches : !matches) {
359
+ throw new Error(
360
+ `waitFor toHaveText: expected "${expectedText}" but got "${actual}" ${JSON.stringify(this.matcher)}`,
361
+ )
362
+ }
363
+ }
364
+ return new SootWaitForAction(this.assertionFn, this.findNode, this.getPage)
365
+ }
366
+
367
+ toHaveValue(value: string): SootWaitForAction {
368
+ this.assertionFn = async () => {
369
+ const node = await this.findNode(this.matcher)
370
+ if (!node) throw new Error(`waitFor toHaveValue: element not found`)
371
+ const actual = node.text || ''
372
+ if (!actual.includes(value)) {
373
+ throw new Error(`waitFor toHaveValue: expected "${value}" in "${actual}"`)
374
+ }
375
+ }
376
+ return new SootWaitForAction(this.assertionFn, this.findNode, this.getPage)
377
+ }
378
+ }
379
+
380
+ class SootWaitForAction {
381
+ private assertionFn: () => Promise<void>
382
+ private findNode: FindFn
383
+ private getPage: GetPageFn
384
+
385
+ constructor(assertionFn: () => Promise<void>, findNode: FindFn, getPage: GetPageFn) {
386
+ this.assertionFn = assertionFn
387
+ this.findNode = findNode
388
+ this.getPage = getPage
389
+ }
390
+
391
+ async withTimeout(ms: number): Promise<void> {
392
+ const start = Date.now()
393
+ const pollInterval = 100
394
+ let lastError: Error | null = null
395
+
396
+ while (Date.now() - start < ms) {
397
+ try {
398
+ await this.assertionFn()
399
+ return // assertion passed
400
+ } catch (e) {
401
+ lastError = e as Error
402
+ await new Promise((r) => setTimeout(r, pollInterval))
403
+ }
404
+ }
405
+
406
+ throw lastError || new Error(`waitFor timed out after ${ms}ms`)
407
+ }
408
+
409
+ // if no withTimeout is called, just run the assertion once
410
+ async then(resolve: (v: void) => void, reject: (e: any) => void): Promise<void> {
411
+ try {
412
+ // default 5 second timeout
413
+ await this.withTimeout(5000)
414
+ resolve(undefined)
415
+ } catch (e) {
416
+ reject(e)
417
+ }
418
+ }
419
+
420
+ whileElement(matcher: Matcher): SootWaitForScrollAction {
421
+ return new SootWaitForScrollAction(
422
+ this.assertionFn,
423
+ this.findNode,
424
+ this.getPage,
425
+ matcher,
426
+ )
427
+ }
428
+ }
429
+
430
+ class SootWaitForScrollAction {
431
+ private assertionFn: () => Promise<void>
432
+ private findNode: FindFn
433
+ private getPage: GetPageFn
434
+ private matcher: Matcher
435
+
436
+ constructor(
437
+ assertionFn: () => Promise<void>,
438
+ findNode: FindFn,
439
+ getPage: GetPageFn,
440
+ matcher: Matcher,
441
+ ) {
442
+ this.assertionFn = assertionFn
443
+ this.findNode = findNode
444
+ this.getPage = getPage
445
+ this.matcher = matcher
446
+ }
447
+
448
+ async scroll(
449
+ pixels: number,
450
+ direction: 'up' | 'down' | 'left' | 'right',
451
+ _startPositionX?: number,
452
+ _startPositionY?: number,
453
+ ): Promise<void> {
454
+ const page = this.getPage()
455
+ const start = Date.now()
456
+ const timeoutMs = 5000
457
+ let lastError: Error | null = null
458
+
459
+ while (Date.now() - start < timeoutMs) {
460
+ try {
461
+ await this.assertionFn()
462
+ return
463
+ } catch (error) {
464
+ lastError = error as Error
465
+ }
466
+
467
+ await scrollMatcherElement(page, this.findNode, this.matcher, pixels, direction)
468
+ }
469
+
470
+ throw (
471
+ lastError ||
472
+ new Error(
473
+ `waitFor whileElement(...).scroll() timed out: ${JSON.stringify(this.matcher)}`,
474
+ )
475
+ )
476
+ }
477
+ }