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,534 @@
1
+ // detox-compatible expectations for sootsim
2
+ // expect(element).toExist(), .toBeVisible(), .toHaveText(), etc.
3
+ // waitFor(element).toExist().withTimeout(ms)
4
+
5
+ import { dragScrollNode } from './gestures'
6
+ import type { SootSimElement } from './element-types'
7
+ import type { Matcher } from './matchers'
8
+ import type { SootSimNodeInfo } from '@rnx/globals'
9
+ import type { Page } from 'playwright'
10
+
11
+ type FindFn = (matcher: Matcher) => Promise<any>
12
+ type GetPageFn = () => Page
13
+ const DEFAULT_VISIBILITY_THRESHOLD = 0.75
14
+
15
+ // one scroll step on the container, reporting how far it actually travelled.
16
+ // the caller ends the search when the container stops moving, which is how
17
+ // detox ends a `whileElement(...).scroll()` on a device: the native scroll
18
+ // throws once it reaches the end of the content.
19
+ async function scrollMatcherElement(
20
+ page: Page,
21
+ findNode: FindFn,
22
+ matcher: Matcher,
23
+ pixels: number,
24
+ direction: 'up' | 'down' | 'left' | 'right',
25
+ startPositionX?: number,
26
+ startPositionY?: number,
27
+ ): Promise<number> {
28
+ const node = await findNode(matcher)
29
+ if (!node) {
30
+ throw new Error(`scroll container not found: ${JSON.stringify(matcher)}`)
31
+ }
32
+ const before = scrollOffsetOf(node)
33
+ await dragScrollNode(page, node, pixels, direction, startPositionX, startPositionY)
34
+ const after = scrollOffsetOf(await findNode(matcher))
35
+ return Math.abs(after.x - before.x) + Math.abs(after.y - before.y)
36
+ }
37
+
38
+ function scrollOffsetOf(node: any): { x: number; y: number } {
39
+ return { x: node?.scrollOffsetX ?? 0, y: node?.scrollOffsetY ?? 0 }
40
+ }
41
+
42
+ // the visible frame is the largest uncovered rectangle used for tapping.
43
+ // visibility includes every exposed region around overlays, not just that rectangle.
44
+ function visibleFraction(node: SootSimNodeInfo | null | undefined): number {
45
+ const totalArea = (node?.layout?.width ?? 0) * (node?.layout?.height ?? 0)
46
+ const frame = node?.visibleFrame
47
+ if (totalArea <= 0 || typeof frame?.area !== 'number') return 0
48
+ return frame.area / totalArea
49
+ }
50
+
51
+ export function isNodeVisible(node: SootSimNodeInfo | null | undefined): boolean {
52
+ return visibleFraction(node) >= DEFAULT_VISIBILITY_THRESHOLD
53
+ }
54
+
55
+ // the engine tags each rendered node with its id, so DOM focus is the source of
56
+ // truth for both the `expect` and `waitFor` forms of toBeFocused.
57
+ async function isNodeFocused(node: any, page: Page): Promise<boolean> {
58
+ const nodeId =
59
+ typeof node?.nodeId === 'number'
60
+ ? node.nodeId
61
+ : typeof node?.id === 'number'
62
+ ? node.id
63
+ : null
64
+ if (nodeId === null) return false
65
+ return page.evaluate((id) => {
66
+ const active = document.activeElement
67
+ return active?.getAttribute('data-sootsim-id') === String(id)
68
+ }, nodeId)
69
+ }
70
+
71
+ export function describeNodeVisibility(node: SootSimNodeInfo | null | undefined): string {
72
+ if (!node) return 'not found'
73
+ const absX = node.absolutePosition?.x ?? node.layout?.x ?? 0
74
+ const absY = node.absolutePosition?.y ?? node.layout?.y ?? 0
75
+ return JSON.stringify({
76
+ layout: node.layout,
77
+ absolutePosition: node.absolutePosition ?? null,
78
+ frame: {
79
+ x: absX,
80
+ y: absY,
81
+ right: absX + (node.layout?.width ?? 0),
82
+ bottom: absY + (node.layout?.height ?? 0),
83
+ },
84
+ visibleFrame: node.visibleFrame ?? null,
85
+ visibleFraction: visibleFraction(node),
86
+ threshold: DEFAULT_VISIBILITY_THRESHOLD,
87
+ style: node.style ?? null,
88
+ })
89
+ }
90
+
91
+ // expect(element) returns an object with assertion methods
92
+ export function createExpect(findNode: FindFn, getPage: GetPageFn) {
93
+ return function sootsimExpect(el: SootSimElement): SootSimExpectation {
94
+ return new SootSimExpectation(el._matcher, findNode, getPage, false)
95
+ }
96
+ }
97
+
98
+ class SootSimExpectation {
99
+ private matcher: Matcher
100
+ private findNode: FindFn
101
+ private getPage: GetPageFn
102
+ private negated: boolean
103
+
104
+ constructor(matcher: Matcher, findNode: FindFn, getPage: GetPageFn, negated: boolean) {
105
+ this.matcher = matcher
106
+ this.findNode = findNode
107
+ this.getPage = getPage
108
+ this.negated = negated
109
+ }
110
+
111
+ get not(): SootSimExpectation {
112
+ return new SootSimExpectation(
113
+ this.matcher,
114
+ this.findNode,
115
+ this.getPage,
116
+ !this.negated,
117
+ )
118
+ }
119
+
120
+ async toExist(): Promise<void> {
121
+ const node = await this.findNode(this.matcher)
122
+ const exists = node !== null && node !== undefined
123
+ if (this.negated) {
124
+ if (exists) {
125
+ throw new Error(
126
+ `expected element NOT to exist but it does: ${JSON.stringify(this.matcher)}`,
127
+ )
128
+ }
129
+ } else {
130
+ if (!exists) {
131
+ throw new Error(
132
+ `expected element to exist but it was not found: ${JSON.stringify(this.matcher)}`,
133
+ )
134
+ }
135
+ }
136
+ }
137
+
138
+ async toBeVisible(): Promise<void> {
139
+ const node = await this.findNode(this.matcher)
140
+ const visible = isNodeVisible(node)
141
+ if (this.negated) {
142
+ if (visible) {
143
+ throw new Error(
144
+ `expected element NOT to be visible: ${JSON.stringify(this.matcher)}`,
145
+ )
146
+ }
147
+ } else {
148
+ if (!visible) {
149
+ throw new Error(
150
+ `expected element to be visible but it is ${describeNodeVisibility(node)}: ${JSON.stringify(this.matcher)}`,
151
+ )
152
+ }
153
+ }
154
+ }
155
+
156
+ async toBeNotVisible(): Promise<void> {
157
+ const node = await this.findNode(this.matcher)
158
+ const visible = isNodeVisible(node)
159
+ if (visible) {
160
+ throw new Error(
161
+ `expected element NOT to be visible: ${JSON.stringify(this.matcher)}`,
162
+ )
163
+ }
164
+ }
165
+
166
+ async toHaveText(expectedText: string): Promise<void> {
167
+ const node = await this.findNode(this.matcher)
168
+ if (!node) {
169
+ throw new Error(`element not found for toHaveText: ${JSON.stringify(this.matcher)}`)
170
+ }
171
+ const actualText = node.text || ''
172
+ const matches = actualText === expectedText
173
+
174
+ if (this.negated) {
175
+ if (matches) {
176
+ throw new Error(
177
+ `expected text NOT to be "${expectedText}" but got "${actualText}": ${JSON.stringify(this.matcher)}`,
178
+ )
179
+ }
180
+ } else {
181
+ if (!matches) {
182
+ throw new Error(
183
+ `expected text "${expectedText}" but got "${actualText}": ${JSON.stringify(this.matcher)}`,
184
+ )
185
+ }
186
+ }
187
+ }
188
+
189
+ async toHaveId(expectedId: string): Promise<void> {
190
+ const node = await this.findNode(this.matcher)
191
+ if (!node) {
192
+ throw new Error(`element not found for toHaveId: ${JSON.stringify(this.matcher)}`)
193
+ }
194
+ const hasId = node.testID === expectedId || node.id === expectedId
195
+ if (this.negated) {
196
+ if (hasId) throw new Error(`expected element NOT to have id "${expectedId}"`)
197
+ } else {
198
+ if (!hasId)
199
+ throw new Error(
200
+ `expected element to have id "${expectedId}" but got "${node.testID || node.id}"`,
201
+ )
202
+ }
203
+ }
204
+
205
+ async toHaveLabel(expectedLabel: string): Promise<void> {
206
+ const node = await this.findNode(this.matcher)
207
+ if (!node) {
208
+ throw new Error(
209
+ `element not found for toHaveLabel: ${JSON.stringify(this.matcher)}`,
210
+ )
211
+ }
212
+ // check explicit accessibilityLabel, then derived text content
213
+ const actual = node.accessibilityLabel || node.text || ''
214
+ const matches = actual === expectedLabel
215
+ if (this.negated) {
216
+ if (matches)
217
+ throw new Error(`expected label NOT to be "${expectedLabel}" but it was`)
218
+ } else {
219
+ if (!matches)
220
+ throw new Error(
221
+ `expected label "${expectedLabel}" but got "${actual}": ${JSON.stringify(this.matcher)}`,
222
+ )
223
+ }
224
+ }
225
+
226
+ async toHaveAccessibilityRole(expectedRole: string): Promise<void> {
227
+ const node = await this.findNode(this.matcher)
228
+ if (!node) {
229
+ throw new Error(
230
+ `element not found for toHaveAccessibilityRole: ${JSON.stringify(this.matcher)}`,
231
+ )
232
+ }
233
+ const actual = node.accessibilityRole || ''
234
+ const matches = actual === expectedRole
235
+ if (this.negated) {
236
+ if (matches) throw new Error(`expected role NOT to be "${expectedRole}" but it was`)
237
+ } else {
238
+ if (!matches)
239
+ throw new Error(
240
+ `expected role "${expectedRole}" but got "${actual}": ${JSON.stringify(this.matcher)}`,
241
+ )
242
+ }
243
+ }
244
+
245
+ async toBeEnabled(): Promise<void> {
246
+ const node = await this.findNode(this.matcher)
247
+ if (!node) {
248
+ throw new Error(
249
+ `element not found for toBeEnabled: ${JSON.stringify(this.matcher)}`,
250
+ )
251
+ }
252
+ const disabled = node.accessibilityState?.disabled ?? false
253
+ if (this.negated) {
254
+ if (!disabled) throw new Error(`expected element to be disabled but it is enabled`)
255
+ } else {
256
+ if (disabled) throw new Error(`expected element to be enabled but it is disabled`)
257
+ }
258
+ }
259
+
260
+ async toBeFocused(): Promise<void> {
261
+ const node = await this.findNode(this.matcher)
262
+ if (!node) {
263
+ throw new Error(
264
+ `element not found for toBeFocused: ${JSON.stringify(this.matcher)}`,
265
+ )
266
+ }
267
+ const focused = await isNodeFocused(node, this.getPage())
268
+
269
+ if (this.negated) {
270
+ if (focused) {
271
+ throw new Error(
272
+ `expected element NOT to be focused: ${JSON.stringify(this.matcher)}`,
273
+ )
274
+ }
275
+ } else if (!focused) {
276
+ throw new Error(`expected element to be focused: ${JSON.stringify(this.matcher)}`)
277
+ }
278
+ }
279
+
280
+ async toHaveValue(value: string): Promise<void> {
281
+ const node = await this.findNode(this.matcher)
282
+ if (!node) {
283
+ throw new Error(
284
+ `element not found for toHaveValue: ${JSON.stringify(this.matcher)}`,
285
+ )
286
+ }
287
+ // value could be in text or props
288
+ const actual = node.text || ''
289
+ if (this.negated) {
290
+ if (actual.includes(value)) {
291
+ throw new Error(
292
+ `expected element NOT to have value "${value}" but got "${actual}"`,
293
+ )
294
+ }
295
+ } else {
296
+ if (!actual.includes(value)) {
297
+ throw new Error(`expected element to have value "${value}" but got "${actual}"`)
298
+ }
299
+ }
300
+ }
301
+
302
+ async toHaveSliderPosition(
303
+ normalizedPosition: number,
304
+ tolerance?: number,
305
+ ): Promise<void> {
306
+ // stub for slider tests -- sootsim doesn't have native sliders yet
307
+ await this.toExist()
308
+ }
309
+
310
+ async toHaveToggleValue(value: boolean): Promise<void> {
311
+ await this.toExist()
312
+ }
313
+ }
314
+
315
+ // waitFor(element) returns a chainable object that polls until the assertion passes
316
+ export function createWaitFor(findNode: FindFn, getPage: GetPageFn) {
317
+ return function sootsimWaitFor(el: SootSimElement): SootSimWaitForChain {
318
+ return new SootSimWaitForChain(el._matcher, findNode, getPage)
319
+ }
320
+ }
321
+
322
+ class SootSimWaitForChain {
323
+ private matcher: Matcher
324
+ private findNode: FindFn
325
+ private getPage: GetPageFn
326
+ private assertionFn: (() => Promise<void>) | null = null
327
+ private _negated = false
328
+
329
+ constructor(matcher: Matcher, findNode: FindFn, getPage: GetPageFn) {
330
+ this.matcher = matcher
331
+ this.findNode = findNode
332
+ this.getPage = getPage
333
+ }
334
+
335
+ get not(): SootSimWaitForChain {
336
+ this._negated = true
337
+ return this
338
+ }
339
+
340
+ toExist(): SootSimWaitForAction {
341
+ this.assertionFn = async () => {
342
+ const node = await this.findNode(this.matcher)
343
+ const exists = node !== null && node !== undefined
344
+ if (this._negated ? exists : !exists) {
345
+ throw new Error(`waitFor toExist failed: ${JSON.stringify(this.matcher)}`)
346
+ }
347
+ }
348
+ return new SootSimWaitForAction(this.assertionFn, this.findNode, this.getPage)
349
+ }
350
+
351
+ toBeVisible(): SootSimWaitForAction {
352
+ this.assertionFn = async () => {
353
+ const node = await this.findNode(this.matcher)
354
+ const visible = isNodeVisible(node)
355
+ if (this._negated ? visible : !visible) {
356
+ throw new Error(
357
+ `waitFor toBeVisible failed (${describeNodeVisibility(node)}): ${JSON.stringify(this.matcher)}`,
358
+ )
359
+ }
360
+ }
361
+ return new SootSimWaitForAction(this.assertionFn, this.findNode, this.getPage)
362
+ }
363
+
364
+ toBeNotVisible(): SootSimWaitForAction {
365
+ this.assertionFn = async () => {
366
+ const node = await this.findNode(this.matcher)
367
+ const visible = isNodeVisible(node)
368
+ if (visible) {
369
+ throw new Error(`waitFor toBeNotVisible failed: ${JSON.stringify(this.matcher)}`)
370
+ }
371
+ }
372
+ return new SootSimWaitForAction(this.assertionFn, this.findNode, this.getPage)
373
+ }
374
+
375
+ toHaveText(expectedText: string): SootSimWaitForAction {
376
+ this.assertionFn = async () => {
377
+ const node = await this.findNode(this.matcher)
378
+ if (!node)
379
+ throw new Error(
380
+ `waitFor toHaveText: element not found: ${JSON.stringify(this.matcher)}`,
381
+ )
382
+ const actual = node.text || ''
383
+ const matches = actual === expectedText
384
+ if (this._negated ? matches : !matches) {
385
+ throw new Error(
386
+ `waitFor toHaveText: expected "${expectedText}" but got "${actual}" ${JSON.stringify(this.matcher)}`,
387
+ )
388
+ }
389
+ }
390
+ return new SootSimWaitForAction(this.assertionFn, this.findNode, this.getPage)
391
+ }
392
+
393
+ toHaveValue(value: string): SootSimWaitForAction {
394
+ this.assertionFn = async () => {
395
+ const node = await this.findNode(this.matcher)
396
+ if (!node) throw new Error(`waitFor toHaveValue: element not found`)
397
+ const actual = node.text || ''
398
+ if (!actual.includes(value)) {
399
+ throw new Error(`waitFor toHaveValue: expected "${value}" in "${actual}"`)
400
+ }
401
+ }
402
+ return new SootSimWaitForAction(this.assertionFn, this.findNode, this.getPage)
403
+ }
404
+
405
+ toBeFocused(): SootSimWaitForAction {
406
+ this.assertionFn = async () => {
407
+ const node = await this.findNode(this.matcher)
408
+ if (!node)
409
+ throw new Error(
410
+ `waitFor toBeFocused: element not found: ${JSON.stringify(this.matcher)}`,
411
+ )
412
+ const focused = await isNodeFocused(node, this.getPage())
413
+ if (this._negated ? focused : !focused) {
414
+ throw new Error(`waitFor toBeFocused failed: ${JSON.stringify(this.matcher)}`)
415
+ }
416
+ }
417
+ return new SootSimWaitForAction(this.assertionFn, this.findNode, this.getPage)
418
+ }
419
+ }
420
+
421
+ class SootSimWaitForAction {
422
+ private assertionFn: () => Promise<void>
423
+ private findNode: FindFn
424
+ private getPage: GetPageFn
425
+
426
+ constructor(assertionFn: () => Promise<void>, findNode: FindFn, getPage: GetPageFn) {
427
+ this.assertionFn = assertionFn
428
+ this.findNode = findNode
429
+ this.getPage = getPage
430
+ }
431
+
432
+ async withTimeout(ms: number): Promise<void> {
433
+ const start = Date.now()
434
+ const pollInterval = 100
435
+ let lastError: Error | null = null
436
+
437
+ while (Date.now() - start < ms) {
438
+ try {
439
+ await this.assertionFn()
440
+ return // assertion passed
441
+ } catch (e) {
442
+ lastError = e as Error
443
+ await new Promise((r) => setTimeout(r, pollInterval))
444
+ }
445
+ }
446
+
447
+ throw lastError || new Error(`waitFor timed out after ${ms}ms`)
448
+ }
449
+
450
+ // if no withTimeout is called, just run the assertion once
451
+ async then(resolve: (v: void) => void, reject: (e: any) => void): Promise<void> {
452
+ try {
453
+ // default 5 second timeout
454
+ await this.withTimeout(5000)
455
+ resolve(undefined)
456
+ } catch (e) {
457
+ reject(e)
458
+ }
459
+ }
460
+
461
+ whileElement(matcher: Matcher): SootSimWaitForScrollAction {
462
+ return new SootSimWaitForScrollAction(
463
+ this.assertionFn,
464
+ this.findNode,
465
+ this.getPage,
466
+ matcher,
467
+ )
468
+ }
469
+ }
470
+
471
+ class SootSimWaitForScrollAction {
472
+ private assertionFn: () => Promise<void>
473
+ private findNode: FindFn
474
+ private getPage: GetPageFn
475
+ private matcher: Matcher
476
+
477
+ constructor(
478
+ assertionFn: () => Promise<void>,
479
+ findNode: FindFn,
480
+ getPage: GetPageFn,
481
+ matcher: Matcher,
482
+ ) {
483
+ this.assertionFn = assertionFn
484
+ this.findNode = findNode
485
+ this.getPage = getPage
486
+ this.matcher = matcher
487
+ }
488
+
489
+ // detox scrolls the container until the expectation passes or the container
490
+ // can no longer move. bounding this by wall clock instead made the result
491
+ // depend on how fast the host dispatches pointer events: the same fixture and
492
+ // the same assertion pass on an idle machine and fail on a loaded one,
493
+ // because each 200px drag is a real pointer stream and fewer of them fit in
494
+ // the budget. end on scroll exhaustion, which is a property of the content.
495
+ async scroll(
496
+ pixels: number,
497
+ direction: 'up' | 'down' | 'left' | 'right',
498
+ startPositionX?: number,
499
+ startPositionY?: number,
500
+ ): Promise<void> {
501
+ const page = this.getPage()
502
+ let lastError: Error | null = null
503
+ let stalledSteps = 0
504
+
505
+ // two consecutive steps, so one settling frame that reports the same offset
506
+ // does not read as the end of the content.
507
+ while (stalledSteps < 2) {
508
+ try {
509
+ await this.assertionFn()
510
+ return
511
+ } catch (error) {
512
+ lastError = error as Error
513
+ }
514
+
515
+ const travelled = await scrollMatcherElement(
516
+ page,
517
+ this.findNode,
518
+ this.matcher,
519
+ pixels,
520
+ direction,
521
+ startPositionX,
522
+ startPositionY,
523
+ )
524
+ stalledSteps = travelled < 0.5 ? stalledSteps + 1 : 0
525
+ }
526
+
527
+ throw (
528
+ lastError ||
529
+ new Error(
530
+ `waitFor whileElement(...).scroll() reached the end of ${JSON.stringify(this.matcher)}`,
531
+ )
532
+ )
533
+ }
534
+ }