peachsim 0.1.527

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (324) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +640 -0
  3. package/cli/app-config.ts +82 -0
  4. package/cli/app-fonts.ts +388 -0
  5. package/cli/app-project.ts +59 -0
  6. package/cli/app-splash.ts +171 -0
  7. package/cli/app-state-reset.ts +26 -0
  8. package/cli/auth.ts +184 -0
  9. package/cli/bin.ts +59 -0
  10. package/cli/bridge-diagnostics.ts +226 -0
  11. package/cli/bridge-flow-runner.ts +2806 -0
  12. package/cli/browser-backend.ts +44 -0
  13. package/cli/browser-evals.ts +96 -0
  14. package/cli/cli-update.ts +261 -0
  15. package/cli/cloud-client.ts +1530 -0
  16. package/cli/cloud-dispatch.ts +206 -0
  17. package/cli/cloud-session.ts +227 -0
  18. package/cli/command-contract.ts +221 -0
  19. package/cli/command-registry.ts +304 -0
  20. package/cli/commands/agent-wrapper.ts +1048 -0
  21. package/cli/commands/agent.ts +439 -0
  22. package/cli/commands/app-fonts.ts +99 -0
  23. package/cli/commands/assert.ts +542 -0
  24. package/cli/commands/auth.ts +71 -0
  25. package/cli/commands/box/bundle-plane.ts +71 -0
  26. package/cli/commands/box/checkout-plane.ts +256 -0
  27. package/cli/commands/box/plane-share.ts +208 -0
  28. package/cli/commands/box/rnx-command.ts +60 -0
  29. package/cli/commands/box/search-command.ts +157 -0
  30. package/cli/commands/box.ts +925 -0
  31. package/cli/commands/camera.ts +266 -0
  32. package/cli/commands/cleanup.ts +169 -0
  33. package/cli/commands/compat.ts +176 -0
  34. package/cli/commands/config.ts +34 -0
  35. package/cli/commands/control.ts +2154 -0
  36. package/cli/commands/cpu-profile.ts +941 -0
  37. package/cli/commands/daemon-mac-app.ts +165 -0
  38. package/cli/commands/daemon.ts +800 -0
  39. package/cli/commands/debug.ts +751 -0
  40. package/cli/commands/desktop.ts +39 -0
  41. package/cli/commands/detect.ts +201 -0
  42. package/cli/commands/detox.ts +481 -0
  43. package/cli/commands/device.ts +134 -0
  44. package/cli/commands/diagnose.ts +589 -0
  45. package/cli/commands/do-chain.ts +408 -0
  46. package/cli/commands/electron.ts +94 -0
  47. package/cli/commands/film.ts +379 -0
  48. package/cli/commands/flow.ts +1240 -0
  49. package/cli/commands/inspect/actions.ts +516 -0
  50. package/cli/commands/inspect/core.ts +2086 -0
  51. package/cli/commands/inspect/count.ts +17 -0
  52. package/cli/commands/inspect/describe.ts +119 -0
  53. package/cli/commands/inspect/env.ts +25 -0
  54. package/cli/commands/inspect/find.ts +169 -0
  55. package/cli/commands/inspect/get-layout.ts +53 -0
  56. package/cli/commands/inspect/keyboard.ts +54 -0
  57. package/cli/commands/inspect/list.ts +175 -0
  58. package/cli/commands/inspect/memory.ts +215 -0
  59. package/cli/commands/inspect/redaction.ts +39 -0
  60. package/cli/commands/inspect/resolve-target.ts +77 -0
  61. package/cli/commands/inspect/screens.ts +78 -0
  62. package/cli/commands/inspect/settle.ts +32 -0
  63. package/cli/commands/inspect/settling.ts +197 -0
  64. package/cli/commands/inspect/shared.ts +259 -0
  65. package/cli/commands/inspect/sleep.ts +15 -0
  66. package/cli/commands/inspect/tree.ts +23 -0
  67. package/cli/commands/inspect/url.ts +17 -0
  68. package/cli/commands/inspect/wait-event.ts +211 -0
  69. package/cli/commands/inspect/wait-idle.ts +25 -0
  70. package/cli/commands/inspect/wait-ready.ts +87 -0
  71. package/cli/commands/inspect/wait-selector.ts +59 -0
  72. package/cli/commands/inspect/wait.ts +32 -0
  73. package/cli/commands/inspect.ts +4738 -0
  74. package/cli/commands/install-cli.ts +275 -0
  75. package/cli/commands/install-desktop.ts +385 -0
  76. package/cli/commands/login.ts +357 -0
  77. package/cli/commands/logout.ts +24 -0
  78. package/cli/commands/maestro-generate.ts +375 -0
  79. package/cli/commands/maestro.ts +536 -0
  80. package/cli/commands/mode.ts +58 -0
  81. package/cli/commands/no-bridge-hint.ts +80 -0
  82. package/cli/commands/perf.ts +67 -0
  83. package/cli/commands/permissions.ts +203 -0
  84. package/cli/commands/platform.ts +410 -0
  85. package/cli/commands/preview.ts +103 -0
  86. package/cli/commands/profile.ts +106 -0
  87. package/cli/commands/react.ts +353 -0
  88. package/cli/commands/record.ts +1435 -0
  89. package/cli/commands/report-issue.ts +305 -0
  90. package/cli/commands/reset.ts +89 -0
  91. package/cli/commands/runtime.ts +348 -0
  92. package/cli/commands/screenshot-command.ts +106 -0
  93. package/cli/commands/screenshot-layers.ts +144 -0
  94. package/cli/commands/screenshot-mode.ts +38 -0
  95. package/cli/commands/screenshot.ts +492 -0
  96. package/cli/commands/screenshots-capture.ts +608 -0
  97. package/cli/commands/screenshots.ts +127 -0
  98. package/cli/commands/serve.ts +139 -0
  99. package/cli/commands/setup.ts +416 -0
  100. package/cli/commands/shell-boolean-mode.ts +82 -0
  101. package/cli/commands/skills.ts +468 -0
  102. package/cli/commands/slides.ts +357 -0
  103. package/cli/commands/state.ts +94 -0
  104. package/cli/commands/storage.ts +58 -0
  105. package/cli/commands/telemetry.ts +55 -0
  106. package/cli/commands/test.ts +535 -0
  107. package/cli/commands/three-mode.ts +771 -0
  108. package/cli/commands/timeline.ts +121 -0
  109. package/cli/commands/upgrade.ts +167 -0
  110. package/cli/commands/upload.ts +1203 -0
  111. package/cli/commands/version.ts +63 -0
  112. package/cli/commands/welcome.ts +17 -0
  113. package/cli/commands/what-happened.ts +328 -0
  114. package/cli/current-sim.ts +217 -0
  115. package/cli/desktop-companion.ts +315 -0
  116. package/cli/drivers/electron.ts +70 -0
  117. package/cli/drivers/index.ts +20 -0
  118. package/cli/drivers/playwright-provisioning.ts +184 -0
  119. package/cli/drivers/playwright-sim-host.ts +537 -0
  120. package/cli/drivers/playwright.ts +209 -0
  121. package/cli/drivers/registry.ts +65 -0
  122. package/cli/drivers/types.ts +102 -0
  123. package/cli/flow-export.ts +55 -0
  124. package/cli/flow-file.ts +142 -0
  125. package/cli/flow-live-status.ts +126 -0
  126. package/cli/flow-session.ts +140 -0
  127. package/cli/help-core.ts +143 -0
  128. package/cli/help.ts +44 -0
  129. package/cli/hidden-runtime-alias.ts +19 -0
  130. package/cli/hints.ts +214 -0
  131. package/cli/inspect-notice-state.ts +114 -0
  132. package/cli/internal-child.ts +176 -0
  133. package/cli/maestro-js.ts +323 -0
  134. package/cli/main.ts +704 -0
  135. package/cli/open-url.ts +8 -0
  136. package/cli/outbound-endpoints.ts +830 -0
  137. package/cli/parent-pid.ts +204 -0
  138. package/cli/parse-args.ts +189 -0
  139. package/cli/privacy.ts +53 -0
  140. package/cli/prompt.ts +56 -0
  141. package/cli/recording-access.ts +108 -0
  142. package/cli/registry.ts +2 -0
  143. package/cli/resolve-assets.ts +63 -0
  144. package/cli/rnx-in-process.ts +98 -0
  145. package/cli/run-registry.ts +226 -0
  146. package/cli/run-rnx-core.ts +148 -0
  147. package/cli/run-rnx.ts +137 -0
  148. package/cli/runtime-notes.ts +66 -0
  149. package/cli/runtime-summary.ts +25 -0
  150. package/cli/secure-file.ts +31 -0
  151. package/cli/self-invocation.ts +58 -0
  152. package/cli/send-to-box.ts +109 -0
  153. package/cli/setup-repository.ts +191 -0
  154. package/cli/shell-init.ts +98 -0
  155. package/cli/standalone.ts +4 -0
  156. package/cli/telemetry.ts +84 -0
  157. package/cli/ui.ts +68 -0
  158. package/cli/ws-bridge.ts +1020 -0
  159. package/detox/attempt-events.cjs +23 -0
  160. package/detox/colors.ts +54 -0
  161. package/detox/config-loader.ts +135 -0
  162. package/detox/element-types.ts +48 -0
  163. package/detox/expectations.ts +534 -0
  164. package/detox/gestures.ts +501 -0
  165. package/detox/index.ts +2694 -0
  166. package/detox/jest-preset.cjs +29 -0
  167. package/detox/jest-setup-after-env.cjs +22 -0
  168. package/detox/matchers.ts +30 -0
  169. package/detox/motion-change.cjs +109 -0
  170. package/detox/motion-change.d.cts +35 -0
  171. package/detox/navigation-lifecycle.ts +290 -0
  172. package/detox/navigation.ts +43 -0
  173. package/detox/proof-frame.cjs +114 -0
  174. package/detox/record-block-execution.cjs +78 -0
  175. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  176. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  177. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  178. package/detox/screenshots/theme-blue-toggle.png +0 -0
  179. package/detox/screenshots/theme-blue.png +0 -0
  180. package/detox/screenshots/theme-red-toggle.png +0 -0
  181. package/detox/screenshots/theme-red.png +0 -0
  182. package/dist-lib/agent-daemon-client.cjs +502 -0
  183. package/dist-lib/agent-events.cjs +48 -0
  184. package/dist-lib/agent-identity.cjs +53 -0
  185. package/dist-lib/agent-sessions.cjs +759 -0
  186. package/dist-lib/attached-projects.cjs +526 -0
  187. package/dist-lib/auth/shared-session.cjs +208 -0
  188. package/dist-lib/backend-origin.cjs +88 -0
  189. package/dist-lib/beta.cjs +41 -0
  190. package/dist-lib/beta.mjs +13 -0
  191. package/dist-lib/bridge-constants.cjs +161 -0
  192. package/dist-lib/bridge-contract-input.cjs +451 -0
  193. package/dist-lib/bridge-contract-input.mjs +414 -0
  194. package/dist-lib/bridge-contract.cjs +130 -0
  195. package/dist-lib/bridge-contract.mjs +94 -0
  196. package/dist-lib/capture-contract.cjs +220 -0
  197. package/dist-lib/capture-contract.mjs +190 -0
  198. package/dist-lib/cli-constants.cjs +32 -0
  199. package/dist-lib/cloud-contract.cjs +194 -0
  200. package/dist-lib/cloud-contract.mjs +145 -0
  201. package/dist-lib/cloud.cjs +670 -0
  202. package/dist-lib/cloud.mjs +639 -0
  203. package/dist-lib/config.cjs +96 -0
  204. package/dist-lib/detox/index.cjs +3789 -0
  205. package/dist-lib/detox/jest-preset.cjs +29 -0
  206. package/dist-lib/dev-bundle-resolution.cjs +290 -0
  207. package/dist-lib/home-paths.cjs +638 -0
  208. package/dist-lib/host/bridge-host.cjs +10903 -0
  209. package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
  210. package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
  211. package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
  212. package/dist-lib/host/replacement-module-handler.cjs +469 -0
  213. package/dist-lib/host/websocket-proxy.cjs +298 -0
  214. package/dist-lib/index.cjs +4268 -0
  215. package/dist-lib/jump-to-source-babel.cjs +93 -0
  216. package/dist-lib/jump-to-source-native.cjs +112 -0
  217. package/dist-lib/menu.cjs +646 -0
  218. package/dist-lib/menu.mjs +604 -0
  219. package/dist-lib/metro-fingerprint-registry.cjs +186 -0
  220. package/dist-lib/metro-fingerprint-registry.mjs +156 -0
  221. package/dist-lib/metro-production-bundle.cjs +329 -0
  222. package/dist-lib/metro-production-bundle.mjs +295 -0
  223. package/dist-lib/metro.cjs +904 -0
  224. package/dist-lib/profiles.cjs +246 -0
  225. package/dist-lib/public-brand.cjs +39 -0
  226. package/dist-lib/react-native-host-modules.cjs +51 -0
  227. package/dist-lib/react-native-host-modules.mjs +25 -0
  228. package/dist-lib/render-mode.cjs +54 -0
  229. package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
  230. package/dist-lib/sdk.cjs +2433 -0
  231. package/dist-lib/sdk.mjs +2336 -0
  232. package/dist-lib/skills.cjs +39132 -0
  233. package/dist-lib/swift-submit.cjs +92 -0
  234. package/dist-lib/swift-submit.mjs +65 -0
  235. package/dist-lib/swift.cjs +1646 -0
  236. package/dist-lib/vite.cjs +1591 -0
  237. package/package.json +290 -0
  238. package/scripts/dev-server-scanner.ts +1291 -0
  239. package/scripts/optional-demo-registry.ts +113 -0
  240. package/skills/contrast/SKILL.md +165 -0
  241. package/skills/rnx-debug/SKILL.md +433 -0
  242. package/skills/rnx-perf/SKILL.md +399 -0
  243. package/skills/rnx-setup/SKILL.md +172 -0
  244. package/skills/rnx-test/SKILL.md +321 -0
  245. package/skills/rnx-visual/SKILL.md +126 -0
  246. package/src/agent-daemon-client.ts +410 -0
  247. package/src/agent-events.ts +71 -0
  248. package/src/agent-identity.ts +23 -0
  249. package/src/agent-prompt.ts +71 -0
  250. package/src/agent-sessions.ts +580 -0
  251. package/src/app-url.ts +39 -0
  252. package/src/attached-projects.ts +539 -0
  253. package/src/auth/shared-session.ts +235 -0
  254. package/src/backend-origin.ts +70 -0
  255. package/src/beta.ts +19 -0
  256. package/src/bridge-constants.ts +175 -0
  257. package/src/bridge-contract-input.ts +502 -0
  258. package/src/bridge-contract.ts +558 -0
  259. package/src/browser-cache.ts +16 -0
  260. package/src/capture-contract.ts +587 -0
  261. package/src/cli-constants.ts +1 -0
  262. package/src/cli-version.ts +40 -0
  263. package/src/cloud-contract.ts +274 -0
  264. package/src/cloud.ts +762 -0
  265. package/src/codex-client.ts +234 -0
  266. package/src/config.ts +132 -0
  267. package/src/connect.ts +101 -0
  268. package/src/dev-bundle-resolution.ts +346 -0
  269. package/src/dev-server-open.ts +198 -0
  270. package/src/disk-cleanup.ts +540 -0
  271. package/src/flow-timeout-scale.ts +15 -0
  272. package/src/home-paths.ts +806 -0
  273. package/src/host/agent-host.ts +666 -0
  274. package/src/host/bridge-host.ts +2788 -0
  275. package/src/host/fetch-proxy-handler.ts +449 -0
  276. package/src/host/fetch-proxy-overrides.ts +51 -0
  277. package/src/host/open-url.ts +89 -0
  278. package/src/host/plane-host.ts +203 -0
  279. package/src/host/replacement-module-handler.ts +158 -0
  280. package/src/host/websocket-proxy.ts +315 -0
  281. package/src/index.ts +71 -0
  282. package/src/jump-to-source-babel.ts +135 -0
  283. package/src/jump-to-source-native.ts +102 -0
  284. package/src/menu.ts +777 -0
  285. package/src/metro-fingerprint-registry.ts +302 -0
  286. package/src/metro-plugin.ts +341 -0
  287. package/src/metro-production-bundle.ts +471 -0
  288. package/src/native-dev-bundle-url.ts +118 -0
  289. package/src/native-seam-manifest.ts +341 -0
  290. package/src/playwright-host-log.ts +16 -0
  291. package/src/plist.ts +27 -0
  292. package/src/poll-until.ts +123 -0
  293. package/src/profiles.ts +179 -0
  294. package/src/public-brand.ts +9 -0
  295. package/src/react-native-host-modules.ts +26 -0
  296. package/src/render-mode.ts +27 -0
  297. package/src/rolldown-plugin-swift.ts +240 -0
  298. package/src/runtime-assets.ts +233 -0
  299. package/src/runtime-delivery.ts +54 -0
  300. package/src/screenshots/compose.ts +422 -0
  301. package/src/screenshots/frame-compose.ts +514 -0
  302. package/src/screenshots/orchestrate.ts +256 -0
  303. package/src/screenshots/org-deck.ts +91 -0
  304. package/src/screenshots/registry.ts +58 -0
  305. package/src/screenshots/schema.ts +364 -0
  306. package/src/sdk.ts +15 -0
  307. package/src/settings-bundle.ts +418 -0
  308. package/src/shell-bridge-identity.ts +15 -0
  309. package/src/sim-client.ts +697 -0
  310. package/src/skills/builtin/a11y-review.ts +132 -0
  311. package/src/skills/builtin/compat-check.ts +71 -0
  312. package/src/skills/builtin/maestro-test.ts +121 -0
  313. package/src/skills/builtin/perf-profile.ts +90 -0
  314. package/src/skills/builtin/screenshot-all.ts +52 -0
  315. package/src/skills/builtin/visual-diff.ts +100 -0
  316. package/src/skills/registry.ts +107 -0
  317. package/src/skills/types.ts +41 -0
  318. package/src/swift-submit.ts +185 -0
  319. package/src/swift.ts +7 -0
  320. package/src/team-machine-prompt.ts +73 -0
  321. package/src/vite-plugin-one.ts +264 -0
  322. package/src/vite-plugin-swift.ts +620 -0
  323. package/src/vite-plugin.ts +1946 -0
  324. package/src/worklets-babel.ts +250 -0
@@ -0,0 +1,481 @@
1
+ // rnx detox — run existing detox suites against rnx
2
+ //
3
+ // use the customer's jest config unchanged. the rnx preset maps the detox
4
+ // driver and owns browser cleanup; jest owns setup, selection and deadlines.
5
+
6
+ import { spawn } from 'child_process'
7
+ import {
8
+ existsSync,
9
+ mkdirSync,
10
+ readFileSync,
11
+ statSync,
12
+ writeFileSync,
13
+ unlinkSync,
14
+ } from 'fs'
15
+ import { createRequire } from 'module'
16
+ import { fileURLToPath } from 'node:url'
17
+ import { tmpdir } from 'os'
18
+ import { dirname, resolve, join, relative } from 'path'
19
+ import { rnxPublicBrand } from '../../src/public-brand'
20
+ import { resolveCliAuth } from '../auth'
21
+ import { registerRun, shouldRegisterRun } from '../run-registry'
22
+ import { rnxExit } from '../run-rnx'
23
+ import { buildShellUrl } from './control'
24
+ import { discoverSootsimUrl } from './flow'
25
+ import { resolveDefaultUploadOrigin } from './upload'
26
+
27
+ interface RunDetoxOpts {
28
+ port?: number
29
+ verbose?: boolean
30
+ }
31
+
32
+ const HELP = `
33
+ rnx detox — run detox-style tests against an rnx shell
34
+
35
+ usage:
36
+ rnx detox [testFiles...] run all tests under e2e/, test/e2e/, or detox/
37
+ rnx detox --config <path> use a specific jest config
38
+ rnx detox init scaffold rnx-detox.config.cjs + sample test
39
+ rnx detox --watch jest watch mode
40
+ rnx detox --headed keep the rnx shell window visible
41
+ rnx detox -t <pattern> pass a jest --testNamePattern
42
+ rnx detox --no-launch skip auto-launching an rnx shell
43
+
44
+ rnx detox auto-launches an rnx shell if none is running on the
45
+ expected port. disable with --no-launch if you're managing the shell yourself.
46
+ `
47
+
48
+ export async function runDetox(args: string[], opts: RunDetoxOpts = {}): Promise<void> {
49
+ if (args.includes('--help') || args.includes('-h')) {
50
+ console.log(HELP)
51
+ rnxExit(0)
52
+ }
53
+
54
+ if (args[0] === 'init') {
55
+ return initDetoxProject()
56
+ }
57
+
58
+ const getFlag = (name: string) => {
59
+ const inline = args.find((arg) => arg.startsWith(`${name}=`))
60
+ if (inline) return inline.slice(name.length + 1)
61
+ const i = args.indexOf(name)
62
+ return i >= 0 ? args[i + 1] : undefined
63
+ }
64
+ const hasFlag = (name: string) => args.includes(name)
65
+
66
+ const watch = hasFlag('--watch') || hasFlag('--watchAll')
67
+ const headed = hasFlag('--headed')
68
+ const noLaunch = hasFlag('--no-launch')
69
+ const configFlag = getFlag('--config')
70
+ const port =
71
+ Number(getFlag('--port')) || opts.port || Number(process.env.SOOTSIM_PORT) || 5173
72
+ if (getFlag('--sim')) {
73
+ throw new Error(
74
+ 'Detox owns its browser context; pin its shell with RNX_URL and --device instead of --sim',
75
+ )
76
+ }
77
+
78
+ // consume only rnx options. jest must receive its complete argv, including
79
+ // selection, timeouts, reporters, coverage, and options added by future jest versions.
80
+ const jestArgs: string[] = ['jest']
81
+ for (let index = 0; index < args.length; index++) {
82
+ const arg = args[index]
83
+ if (arg === '--headed' || arg === '--no-launch') continue
84
+ const name = arg.split('=', 1)[0]
85
+ if (['--config', '--app', '--device', '--port'].includes(name)) {
86
+ if (!arg.includes('=')) {
87
+ if (!args[index + 1] || args[index + 1].startsWith('-')) {
88
+ throw new Error(`${name} requires a value`)
89
+ }
90
+ index++
91
+ }
92
+ continue
93
+ }
94
+ jestArgs.push(arg === '--grep' ? '--testNamePattern' : arg)
95
+ }
96
+
97
+ let localConfig =
98
+ configFlag ||
99
+ firstExisting([
100
+ 'rnx-detox.config.cjs',
101
+ 'jest.config.ts',
102
+ 'jest.config.mts',
103
+ 'jest.config.mjs',
104
+ 'jest.config.cjs',
105
+ 'jest.config.js',
106
+ 'jest.config.cts',
107
+ 'jest.config.json',
108
+ ])
109
+ if (!localConfig && existsSync('package.json')) {
110
+ const pkg = JSON.parse(readFileSync('package.json', 'utf8'))
111
+ if (pkg.jest) localConfig = 'package.json'
112
+ }
113
+
114
+ // test dir discovery. external test suites set RNX_TEST_DIR; otherwise
115
+ // we auto-detect the conventional locations.
116
+ const detoxDirs = ['e2e', 'test/e2e', 'detox']
117
+ let testDir: string | null = process.env.RNX_TEST_DIR
118
+ ? resolve(process.cwd(), process.env.RNX_TEST_DIR)
119
+ : null
120
+ if (!testDir && !localConfig && args[0] && !args[0].startsWith('-')) {
121
+ const target = resolve(args[0])
122
+ if (existsSync(target))
123
+ testDir = statSync(target).isDirectory() ? target : dirname(target)
124
+ }
125
+ if (!testDir && !localConfig) {
126
+ for (const dir of detoxDirs) {
127
+ const full = resolve(process.cwd(), dir)
128
+ if (existsSync(full)) {
129
+ testDir = full
130
+ break
131
+ }
132
+ }
133
+ }
134
+ if (!testDir && !localConfig) {
135
+ console.error(
136
+ ` error: no detox tests found. expected one of: ${detoxDirs.join(', ')}\n` +
137
+ ` run 'rnx detox init' to scaffold a starter suite.`,
138
+ )
139
+ rnxExit(1)
140
+ }
141
+ testDir ??= process.cwd()
142
+
143
+ // auto-launch: make sure an rnx shell is reachable before running jest.
144
+ // Reuse the Maestro discovery path: scan local dev servers.
145
+ // and `rnx open` the first match.
146
+ if (!noLaunch) {
147
+ await ensureShellRunning(port)
148
+ }
149
+
150
+ // build the jest command. prefer the user's config if they passed --config
151
+ // or if a rnx-detox.config.cjs exists in cwd. otherwise write a temporary
152
+ // config that loads our preset.
153
+ let tempConfig: string | null = null
154
+ if (localConfig) {
155
+ jestArgs.push(
156
+ '--config',
157
+ localConfig.startsWith('{') ? localConfig : resolve(process.cwd(), localConfig),
158
+ )
159
+ } else {
160
+ // no local config — write a temp config that pulls in our preset.
161
+ // jest's --preset flag has quirky module resolution that breaks with
162
+ // subpath exports, so we generate a real config file instead.
163
+ // scope roots + testMatch to the discovered test dir so jest doesn't
164
+ // scan the entire repo (which causes haste collisions in monorepos).
165
+ const presetPath = fileURLToPath(
166
+ import.meta.resolve(`${rnxPublicBrand.packageName}/detox/jest-preset`),
167
+ )
168
+ // an external suite may optionally declare its own ignore patterns via e2e/jest-ignore.cjs
169
+ const suiteIgnoreFile = testDir ? join(testDir, 'e2e', 'jest-ignore.cjs') : null
170
+ let suiteIgnorePatterns: string[] = []
171
+ if (suiteIgnoreFile && existsSync(suiteIgnoreFile)) {
172
+ const req = createRequire(import.meta.url)
173
+ const exported = req(suiteIgnoreFile)
174
+ const patterns = Array.isArray(exported)
175
+ ? exported
176
+ : exported.testPathIgnorePatterns
177
+ if (
178
+ !Array.isArray(patterns) ||
179
+ !patterns.every((pattern) => typeof pattern === 'string')
180
+ ) {
181
+ throw new Error(`${suiteIgnoreFile} must export string ignore patterns`)
182
+ }
183
+ suiteIgnorePatterns = patterns
184
+ }
185
+ tempConfig = join(tmpdir(), `rnx-detox-${process.pid}.config.cjs`)
186
+ const cfg = {
187
+ rootDir: testDir,
188
+ roots: ['<rootDir>'],
189
+ testMatch: ['<rootDir>/**/*.test.ts', '<rootDir>/**/*.test.js'],
190
+ ...(suiteIgnorePatterns.length > 0
191
+ ? { testPathIgnorePatterns: suiteIgnorePatterns }
192
+ : {}),
193
+ }
194
+ writeFileSync(
195
+ tempConfig,
196
+ `module.exports = { ...require(${JSON.stringify(presetPath)}), ...${JSON.stringify(cfg)} }\n`,
197
+ )
198
+ jestArgs.push('--config', tempConfig)
199
+ }
200
+
201
+ // hosted run registration (org dashboard pass/fail row). when it applies,
202
+ // capture jest's structured results via --json --outputFile so the row can
203
+ // carry test counts + the first failure; console output stays untouched.
204
+ const auth = resolveCliAuth()
205
+ const wantsRegistration =
206
+ !watch &&
207
+ (process.env.RNX_REQUIRE_REGISTRATION === 'true' ||
208
+ shouldRegisterRun({ uploadedShare: false, auth }))
209
+ let resultsFile: string | null =
210
+ getFlag('--outputFile') || process.env.SOOTSIM_JEST_OUTPUT_FILE || null
211
+ if ((process.env.SOOTSIM_JEST_JSON === '1' || wantsRegistration) && !hasFlag('--json'))
212
+ jestArgs.push('--json')
213
+ if (!getFlag('--outputFile') && process.env.SOOTSIM_JEST_OUTPUT_FILE) {
214
+ jestArgs.push('--outputFile', process.env.SOOTSIM_JEST_OUTPUT_FILE)
215
+ } else if (!resultsFile && wantsRegistration) {
216
+ resultsFile = join(tmpdir(), `rnx-detox-results-${process.pid}.json`)
217
+ jestArgs.push('--outputFile', resultsFile)
218
+ }
219
+
220
+ console.log(` rnx detox`)
221
+ console.log(` test dir: ${testDir}`)
222
+ console.log(` port: ${port}${headed ? ' (headed)' : ''}`)
223
+ if (localConfig) console.log(` config: ${localConfig}`)
224
+
225
+ const shellBaseUrl = process.env.RNX_URL || `http://localhost:${port}`
226
+ const app = getFlag('--app')
227
+ const shellUrl = new URL(app ? await buildShellUrl(app, shellBaseUrl) : shellBaseUrl)
228
+ const device = getFlag('--device')
229
+ if (device) shellUrl.searchParams.set('device', device)
230
+
231
+ // proof capture runs under the repo's pinned node runtime. its v8 collector
232
+ // crashes in ClearStaleLeftTrimmedPointerVisitor when Sparkplug baseline
233
+ // compilation (Builtins_BaselineOutOfLinePrologue) pushes untagged register
234
+ // values onto the stack across CEntry, which MarkCompact root iteration
235
+ // then visits as tagged heap pointers. disable both Maglev and Sparkplug so
236
+ // execution stays on the stable Ignition + TurboFan pipeline under Node 24.
237
+ // normal user detox runs retain their node defaults and resolve jest through npx.
238
+ const proofCapture =
239
+ process.env.CONFORMANCE_PROOF === '1' ||
240
+ !!process.env.CONFORMANCE_PROOF_DIR ||
241
+ process.env.CONFORMANCE_TARGET === 'sootsim'
242
+ const command = proofCapture ? 'node' : 'npx'
243
+ const commandArgs = proofCapture
244
+ ? [
245
+ '--no-maglev',
246
+ '--no-sparkplug',
247
+ createRequire(join(process.cwd(), 'package.json')).resolve('jest/bin/jest'),
248
+ ...jestArgs.slice(1),
249
+ ]
250
+ : jestArgs
251
+ const child = spawn(command, commandArgs, {
252
+ cwd: process.cwd(),
253
+ stdio: 'inherit',
254
+ env: {
255
+ ...process.env,
256
+ RNX_TEST_DIR: testDir,
257
+ SOOTSIM_PORT: String(port),
258
+ SOOTSIM_HEADED: headed ? '1' : '',
259
+ RNX_URL: shellUrl.toString(),
260
+ },
261
+ })
262
+
263
+ // jest owns the browsers, and it only reaps them from its own signal and exit
264
+ // handlers. this process is the one a caller has a pid for, so a SIGTERM aimed
265
+ // at `rnx detox` lands here and jest never hears about it: it is a
266
+ // grandchild (this -> npx -> jest), so it keeps running with no parent and
267
+ // every chrome it launched is orphaned for good. forward the signal instead of
268
+ // exiting out from under it, and let its exit code end this process.
269
+ const forward = (signal: NodeJS.Signals) => () => {
270
+ child.kill(signal)
271
+ }
272
+ const onTerm = forward('SIGTERM')
273
+ const onInt = forward('SIGINT')
274
+ process.on('SIGTERM', onTerm)
275
+ process.on('SIGINT', onInt)
276
+
277
+ const startedAt = Date.now()
278
+ let code: number
279
+ try {
280
+ code = await new Promise<number>((resolveCode, reject) => {
281
+ child.once('error', reject)
282
+ child.once('exit', (exitCode, signal) => {
283
+ if (signal) {
284
+ console.error(` error: jest terminated by ${signal}`)
285
+ resolveCode(1)
286
+ return
287
+ }
288
+ resolveCode(exitCode ?? 1)
289
+ })
290
+ })
291
+ } finally {
292
+ process.off('SIGTERM', onTerm)
293
+ process.off('SIGINT', onInt)
294
+ if (tempConfig) unlinkSync(tempConfig)
295
+ }
296
+
297
+ if (wantsRegistration) {
298
+ const results =
299
+ resultsFile && existsSync(resultsFile) && statSync(resultsFile).mtimeMs >= startedAt
300
+ ? readJestResults(resultsFile)
301
+ : null
302
+ // a selection that skipped every test is not a passing suite.
303
+ if (code === 0 && results && results.numPassedTests === 0) {
304
+ console.error(' error: no tests were selected or executed')
305
+ code = 1
306
+ }
307
+ const origin = await resolveDefaultUploadOrigin()
308
+ const run = await registerRun({
309
+ origin,
310
+ kind: 'detox',
311
+ name: relative(process.cwd(), testDir) || 'e2e',
312
+ status: code === 0 ? 'passed' : 'failed',
313
+ summary: results
314
+ ? `${results.numPassedTests}/${results.numTotalTests} tests passed`
315
+ : null,
316
+ failureMessage: code === 0 ? null : (results?.firstFailure ?? null),
317
+ // tests map onto the row's step summary: total count + first failing
318
+ // index, same shape flow lanes fill from their step trace.
319
+ stepCount: results?.numTotalTests ?? null,
320
+ failedStepIndex: results?.firstFailedIndex ?? null,
321
+ durationMs: Date.now() - startedAt,
322
+ auth,
323
+ })
324
+ if (!run) rnxExit(code || 1)
325
+ console.log(` run: ${run.id}`)
326
+ writeFileSync(
327
+ join(process.cwd(), 'test-result.json'),
328
+ JSON.stringify({
329
+ runId: run.id,
330
+ previewUrl: run.previewUrl,
331
+ traceUrl: run.traceUrl,
332
+ }),
333
+ )
334
+ }
335
+ if (
336
+ resultsFile &&
337
+ existsSync(resultsFile) &&
338
+ !getFlag('--outputFile') &&
339
+ !process.env.SOOTSIM_JEST_OUTPUT_FILE
340
+ ) {
341
+ unlinkSync(resultsFile)
342
+ }
343
+ rnxExit(code)
344
+ }
345
+
346
+ export type JestRunSummary = {
347
+ numTotalTests: number
348
+ numPassedTests: number
349
+ firstFailure: string | null
350
+ firstFailedIndex: number | null
351
+ }
352
+
353
+ // parse jest's --json output file down to the counts + first failure the
354
+ // hosted run row carries. tolerant: any parse problem returns null and the
355
+ // run registers with exit-code status only.
356
+ export function readJestResults(file: string): JestRunSummary | null {
357
+ try {
358
+ const parsed: unknown = JSON.parse(readFileSync(file, 'utf8'))
359
+ if (
360
+ !parsed ||
361
+ typeof parsed !== 'object' ||
362
+ !('numTotalTests' in parsed) ||
363
+ typeof parsed.numTotalTests !== 'number' ||
364
+ !('numPassedTests' in parsed) ||
365
+ typeof parsed.numPassedTests !== 'number' ||
366
+ !('testResults' in parsed) ||
367
+ !Array.isArray(parsed.testResults)
368
+ )
369
+ return null
370
+ let firstFailure: string | null = null
371
+ let firstFailedIndex: number | null = null
372
+ let index = 0
373
+ for (const suite of parsed.testResults) {
374
+ if (!suite || typeof suite !== 'object' || !Array.isArray(suite.assertionResults))
375
+ return null
376
+ for (const test of suite.assertionResults) {
377
+ if (!test || typeof test !== 'object') return null
378
+ if (firstFailedIndex == null && test.status === 'failed') {
379
+ firstFailedIndex = index
380
+ const messages: unknown[] = Array.isArray(test.failureMessages)
381
+ ? test.failureMessages
382
+ : []
383
+ const message = messages
384
+ .filter((value): value is string => typeof value === 'string')
385
+ .join('\n')
386
+ firstFailure =
387
+ [typeof test.fullName === 'string' ? test.fullName : '', message]
388
+ .filter(Boolean)
389
+ .join(': ') || null
390
+ }
391
+ index += 1
392
+ }
393
+ }
394
+ return {
395
+ numTotalTests: parsed.numTotalTests,
396
+ numPassedTests: parsed.numPassedTests,
397
+ firstFailure,
398
+ firstFailedIndex,
399
+ }
400
+ } catch {
401
+ return null
402
+ }
403
+ }
404
+
405
+ function firstExisting(names: string[]): string | null {
406
+ for (const n of names) {
407
+ if (existsSync(resolve(process.cwd(), n))) return n
408
+ }
409
+ return null
410
+ }
411
+
412
+ async function ensureShellRunning(port: number): Promise<void> {
413
+ const shellUrl = await discoverSootsimUrl()
414
+ if (shellUrl) return
415
+
416
+ console.warn(
417
+ ` warn: no rnx shell reachable on :${port}. start your app, then run \`rnx open <app-port>\`\n` +
418
+ ` or pass --no-launch if you're managing the shell yourself.`,
419
+ )
420
+ }
421
+
422
+ function initDetoxProject(): void {
423
+ const cwd = process.cwd()
424
+ const configPath = resolve(cwd, 'rnx-detox.config.cjs')
425
+ if (existsSync(configPath)) {
426
+ console.log(` skip: ${configPath} already exists`)
427
+ } else {
428
+ writeFileSync(
429
+ configPath,
430
+ `// rnx detox jest config.
431
+ // extends the rnx preset, which rewrites \`import ... from 'detox'\`
432
+ // to the rnx driver and keeps your jest transforms intact.
433
+
434
+ /** @type {import('@jest/types').Config.InitialOptions} */
435
+ module.exports = {
436
+ preset: 'rnxsim/detox',
437
+ rootDir: __dirname,
438
+ testMatch: ['<rootDir>/e2e/**/*.test.ts', '<rootDir>/e2e/**/*.test.js'],
439
+ }
440
+ `,
441
+ )
442
+ console.log(` created ${configPath}`)
443
+ }
444
+
445
+ const e2eDir = resolve(cwd, 'e2e')
446
+ const samplePath = resolve(e2eDir, 'example.test.ts')
447
+ if (existsSync(samplePath)) {
448
+ console.log(` skip: ${samplePath} already exists`)
449
+ } else {
450
+ mkdirSync(e2eDir, { recursive: true })
451
+ writeFileSync(
452
+ samplePath,
453
+ `// sample rnx detox test. resolve \`detox\` via the jest preset in
454
+ // rnx-detox.config.cjs — no extra imports required.
455
+
456
+ import { by, device, element, expect, waitFor } from 'detox'
457
+
458
+ describe('example', () => {
459
+ beforeAll(async () => {
460
+ await device.launchApp()
461
+ })
462
+
463
+ it('shows the welcome text', async () => {
464
+ await waitFor(element(by.text('Welcome'))).toBeVisible().withTimeout(5000)
465
+ })
466
+
467
+ it('taps a button by id', async () => {
468
+ await element(by.id('start-button')).tap()
469
+ await expect(element(by.id('home-screen'))).toBeVisible()
470
+ })
471
+ })
472
+ `,
473
+ )
474
+ console.log(` created ${samplePath}`)
475
+ }
476
+
477
+ console.log(`\n next:`)
478
+ console.log(` rnx detox # run the sample`)
479
+ console.log(` rnx detox --watch`)
480
+ rnxExit(0)
481
+ }
@@ -0,0 +1,134 @@
1
+ import {
2
+ devices,
3
+ getDevice,
4
+ listSelectableDeviceModels,
5
+ type DeviceModel,
6
+ } from 'sootsim-engine/settings'
7
+ import { rnxExit } from '../run-rnx'
8
+ import {
9
+ callInBridge,
10
+ createBridgeFromParsed,
11
+ parseBridgeCliArgs,
12
+ type ParsedBridgeCliArgs,
13
+ } from '../ws-bridge'
14
+
15
+ interface DeviceCommandOptions {
16
+ port?: number
17
+ }
18
+
19
+ type CurrentDeviceSnapshot = {
20
+ model: string
21
+ width: number
22
+ height: number
23
+ scale: number
24
+ name: string
25
+ } | null
26
+
27
+ function usage() {
28
+ console.log(`
29
+ rnx device — inspect or change the live device preset for a sim
30
+
31
+ usage:
32
+ rnx device
33
+ rnx device get
34
+ rnx device set <model> [--sim <id>]
35
+ rnx device list
36
+
37
+ examples:
38
+ rnx device
39
+ rnx device set iphone-16
40
+ rnx device set iphone-16-pro-max --sim a7
41
+ `)
42
+ }
43
+
44
+ function printDevice(model: DeviceModel) {
45
+ const spec = getDevice(model)
46
+ console.log(
47
+ ` ${model.padEnd(14)} ${String(spec.width).padStart(4)}x${String(spec.height).padEnd(4)} pt ${String(spec.width * spec.scale).padStart(4)}x${String(spec.height * spec.scale).padEnd(4)} px ${spec.name}`,
48
+ )
49
+ }
50
+
51
+ async function readCurrentDevice(
52
+ parsed: ParsedBridgeCliArgs,
53
+ ): Promise<CurrentDeviceSnapshot> {
54
+ const bridge = createBridgeFromParsed(parsed)
55
+ try {
56
+ const settings = (await callInBridge<Record<string, unknown> | null>(
57
+ bridge,
58
+ 'SootSim.bridges.settings.get',
59
+ )) ?? { deviceModel: null }
60
+ const model = typeof settings.deviceModel === 'string' ? settings.deviceModel : null
61
+ if (!model || !(model in devices)) return null
62
+ const spec = getDevice(model as DeviceModel)
63
+ return {
64
+ model,
65
+ width: spec.width,
66
+ height: spec.height,
67
+ scale: spec.scale,
68
+ name: spec.name,
69
+ }
70
+ } finally {
71
+ bridge.close()
72
+ }
73
+ }
74
+
75
+ export async function runDeviceCommand(args: string[], opts: DeviceCommandOptions) {
76
+ if (args.includes('--help') || args.includes('-h')) {
77
+ usage()
78
+ rnxExit(0)
79
+ }
80
+
81
+ const subcommand = args[0]
82
+ if (!subcommand || subcommand === 'get') {
83
+ const parsed = parseBridgeCliArgs(args, { port: opts.port })
84
+ const current = await readCurrentDevice(parsed)
85
+ if (!current) {
86
+ console.error(' error: could not read current device from the target sim')
87
+ rnxExit(1)
88
+ }
89
+ console.log(
90
+ ` ${current.model} ${current.width}x${current.height} pt ${current.width * current.scale}x${current.height * current.scale} px ${current.name}`,
91
+ )
92
+ return
93
+ }
94
+
95
+ if (subcommand === 'list') {
96
+ console.log(' available devices:\n')
97
+ for (const model of listSelectableDeviceModels()) printDevice(model)
98
+ return
99
+ }
100
+
101
+ if (subcommand === 'set') {
102
+ const model = args[1]
103
+ if (!model) {
104
+ console.error(' error: device set expects a model')
105
+ rnxExit(1)
106
+ }
107
+ if (!(model in devices)) {
108
+ console.error(` error: unknown device "${model}"`)
109
+ console.error(' run `rnx device list` to see valid models')
110
+ rnxExit(1)
111
+ }
112
+ const parsed = parseBridgeCliArgs(args, { port: opts.port })
113
+ const bridge = createBridgeFromParsed(parsed)
114
+ try {
115
+ const changed = await callInBridge<boolean>(
116
+ bridge,
117
+ 'SootSim.bridges.settings.set',
118
+ 'deviceModel',
119
+ model,
120
+ )
121
+ const spec = getDevice(model as DeviceModel)
122
+ console.log(
123
+ ` ${changed ? 'set' : 'kept'} device: ${model} (${spec.width}x${spec.height} pt, ${spec.width * spec.scale}x${spec.height * spec.scale} px)${changed ? ' — reloading sim' : ''}`,
124
+ )
125
+ } finally {
126
+ bridge.close()
127
+ }
128
+ return
129
+ }
130
+
131
+ usage()
132
+ console.error(` unknown subcommand: ${subcommand}`)
133
+ rnxExit(1)
134
+ }