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,29 @@
1
+ // jest preset for running existing detox suites against sootsim.
2
+ //
3
+ // users drop `preset: 'rnxsim/detox'` into their jest config
4
+ // (or `rnx-detox.config.cjs`) and their `import { by, device, element,
5
+ // expect, waitFor } from 'detox'` calls resolve to the sootsim driver.
6
+
7
+ const path = require('path')
8
+
9
+ // resolve the published driver entry so the mapping works from any cwd.
10
+ // `require.resolve('sootsim/detox')` honours the subpath export in the
11
+ // sootsim package.json.
12
+ const DRIVER_ENTRY = require.resolve('rnxsim/detox')
13
+ const PACKAGE_ROOT = path.dirname(require.resolve('rnxsim/package.json'))
14
+
15
+ /** @type {import('@jest/types').Config.InitialOptions} */
16
+ module.exports = {
17
+ testTimeout: 180000,
18
+ maxWorkers: 1,
19
+ watchman: false,
20
+ verbose: true,
21
+ testEnvironment: 'node',
22
+ setupFilesAfterEnv: [path.join(PACKAGE_ROOT, 'detox', 'jest-setup-after-env.cjs')],
23
+ transform: {
24
+ '^.+\\.[jt]sx?$': ['@sucrase/jest-plugin', { jsxRuntime: 'automatic' }],
25
+ },
26
+ moduleNameMapper: {
27
+ '^detox$': DRIVER_ENTRY,
28
+ },
29
+ }
@@ -0,0 +1,22 @@
1
+ // closes the driver's browser when a test file finishes.
2
+ //
3
+ // the driver launches chrome lazily on first use and tracks it with the shared
4
+ // reaper, but that reaper's safety net is a `process.on('exit')` hook and jest
5
+ // hands every test file a sandboxed COPY of `process`. a handler registered
6
+ // from inside a test therefore never fires on real termination, so under jest
7
+ // the reaper is a no-op and every suite that launched a browser strands its
8
+ // whole chrome tree. one orphan per test file adds up fast on a full run.
9
+ //
10
+ // `cleanup()` is the teardown that does run, and it both closes the browser and
11
+ // force-reaps its tree. registering it here means no suite has to remember an
12
+ // afterAll of its own.
13
+
14
+ // records which test bodies actually ran, so a downstream reader can tell a
15
+ // setup failure from a behavior failure. no-op unless the run asks for it.
16
+ require('./record-block-execution.cjs')
17
+
18
+ const { cleanup } = require('rnxsim/detox')
19
+
20
+ afterAll(async () => {
21
+ await cleanup()
22
+ })
@@ -0,0 +1,30 @@
1
+ // detox-compatible matchers: by.id(), by.text(), by.label(), by.type()
2
+ // these produce matcher descriptors consumed by element() and expect()
3
+
4
+ export interface Matcher {
5
+ type: 'id' | 'text' | 'label' | 'type' | 'role'
6
+ value: string
7
+ index?: number
8
+ }
9
+
10
+ export const by = {
11
+ id(testID: string): Matcher {
12
+ return { type: 'id', value: testID }
13
+ },
14
+
15
+ text(text: string): Matcher {
16
+ return { type: 'text', value: text }
17
+ },
18
+
19
+ label(accessibilityLabel: string): Matcher {
20
+ return { type: 'label', value: accessibilityLabel }
21
+ },
22
+
23
+ type(nativeType: string): Matcher {
24
+ return { type: 'type', value: nativeType }
25
+ },
26
+
27
+ role(accessibilityRole: string): Matcher {
28
+ return { type: 'role', value: accessibilityRole }
29
+ },
30
+ }
@@ -0,0 +1,109 @@
1
+ // native recordings and SootSim live frames must use this exact definition.
2
+ // retained static Android crops measured as high as 0.0331% after the channel
3
+ // filter. the smallest corrected component transition with visible motion was
4
+ // 0.1748%, so a 0.1% floor separates every measured static and moving frame.
5
+ const MOTION_CHANGE_DEFINITION = Object.freeze({
6
+ channelThreshold: 24,
7
+ minimumChangePercent: 0.1,
8
+ })
9
+
10
+ function measureMotionChange(input) {
11
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
12
+ throw new Error('motion change measurement requires an input object')
13
+ }
14
+ if (input.installGlobal === true) {
15
+ globalThis.__sootsimMeasureMotionChange = measureMotionChange
16
+ return null
17
+ }
18
+
19
+ const definition = input.definition
20
+ if (
21
+ !definition ||
22
+ typeof definition !== 'object' ||
23
+ !Number.isInteger(definition.channelThreshold) ||
24
+ definition.channelThreshold < 0 ||
25
+ definition.channelThreshold > 255 ||
26
+ !Number.isFinite(definition.minimumChangePercent) ||
27
+ definition.minimumChangePercent <= 0 ||
28
+ definition.minimumChangePercent >= 100
29
+ ) {
30
+ throw new Error(
31
+ 'motion change definition needs a channel threshold from 0 to 255 and a minimum greater than 0% and less than 100%',
32
+ )
33
+ }
34
+
35
+ const label =
36
+ typeof input.label === 'string' && input.label.length > 0
37
+ ? input.label
38
+ : 'motion change'
39
+ let changePixels = input.changePixels
40
+ let totalPixels = input.totalPixels
41
+ if (input.baseline !== undefined || input.candidate !== undefined) {
42
+ const baseline = input.baseline
43
+ const candidate = input.candidate
44
+ const width = input.width
45
+ const height = input.height
46
+ if (
47
+ !baseline ||
48
+ !candidate ||
49
+ typeof baseline.length !== 'number' ||
50
+ typeof candidate.length !== 'number' ||
51
+ !Number.isInteger(width) ||
52
+ !Number.isInteger(height) ||
53
+ width <= 0 ||
54
+ height <= 0
55
+ ) {
56
+ throw new Error(`${label} needs two RGBA pixel arrays and positive dimensions`)
57
+ }
58
+ totalPixels = width * height
59
+ const expectedBytes = totalPixels * 4
60
+ if (baseline.length !== expectedBytes || candidate.length !== expectedBytes) {
61
+ throw new Error(
62
+ `${label} pixel data has ${baseline.length}/${candidate.length} bytes; expected ${expectedBytes}`,
63
+ )
64
+ }
65
+ changePixels = 0
66
+ for (let offset = 0; offset < expectedBytes; offset += 4) {
67
+ if (
68
+ Math.abs(baseline[offset] - candidate[offset]) > definition.channelThreshold ||
69
+ Math.abs(baseline[offset + 1] - candidate[offset + 1]) >
70
+ definition.channelThreshold ||
71
+ Math.abs(baseline[offset + 2] - candidate[offset + 2]) >
72
+ definition.channelThreshold ||
73
+ Math.abs(baseline[offset + 3] - candidate[offset + 3]) >
74
+ definition.channelThreshold
75
+ ) {
76
+ changePixels++
77
+ }
78
+ }
79
+ }
80
+
81
+ if (!Number.isInteger(totalPixels) || totalPixels <= 0) {
82
+ throw new Error(`${label} has no positive measured region area`)
83
+ }
84
+ if (!Number.isInteger(changePixels) || changePixels < 0 || changePixels > totalPixels) {
85
+ throw new Error(
86
+ `${label} changed pixel count ${changePixels} is outside its ${totalPixels}px measured region`,
87
+ )
88
+ }
89
+ const changePercent = (changePixels / totalPixels) * 100
90
+ return {
91
+ changePercent,
92
+ changePixels,
93
+ passedMinimum: changePercent >= definition.minimumChangePercent,
94
+ totalPixels,
95
+ }
96
+ }
97
+
98
+ function measureProofMotionChange(input) {
99
+ return measureMotionChange({
100
+ ...input,
101
+ definition: MOTION_CHANGE_DEFINITION,
102
+ })
103
+ }
104
+
105
+ module.exports = {
106
+ MOTION_CHANGE_DEFINITION,
107
+ measureMotionChange,
108
+ measureProofMotionChange,
109
+ }
@@ -0,0 +1,35 @@
1
+ export type MotionChangeDefinition = {
2
+ channelThreshold: number
3
+ minimumChangePercent: number
4
+ }
5
+
6
+ type MotionChangePixels = ArrayLike<number>
7
+
8
+ export type MotionChangeInput = {
9
+ baseline?: MotionChangePixels
10
+ candidate?: MotionChangePixels
11
+ changePixels?: number
12
+ definition: MotionChangeDefinition
13
+ height?: number
14
+ installGlobal?: boolean
15
+ label?: string
16
+ totalPixels?: number
17
+ width?: number
18
+ }
19
+
20
+ export type MotionChangeMeasurement = {
21
+ changePercent: number
22
+ changePixels: number
23
+ passedMinimum: boolean
24
+ totalPixels: number
25
+ }
26
+
27
+ export const MOTION_CHANGE_DEFINITION: Readonly<MotionChangeDefinition>
28
+
29
+ export function measureMotionChange(
30
+ input: MotionChangeInput,
31
+ ): MotionChangeMeasurement | null
32
+
33
+ export function measureProofMotionChange(
34
+ input: Omit<MotionChangeInput, 'definition' | 'installGlobal'>,
35
+ ): MotionChangeMeasurement
@@ -0,0 +1,290 @@
1
+ import { writeAttemptEvent } from './attempt-events.cjs'
2
+ import type { Page, Request, Response } from 'playwright'
3
+
4
+ export type SootsimNavigationIdentity = {
5
+ browserId: number
6
+ contextId: number
7
+ operation: 'launchApp' | 'openURL'
8
+ }
9
+
10
+ type SootsimPageState = {
11
+ bodyText: string
12
+ bridges: {
13
+ sootsim: boolean
14
+ test: boolean
15
+ }
16
+ globals: string[]
17
+ readyState: string
18
+ title: string
19
+ url: string
20
+ }
21
+
22
+ type ActiveNavigation = SootsimNavigationIdentity & {
23
+ navigationUrl: string
24
+ navigationId: number
25
+ }
26
+
27
+ type NavigationMilestone =
28
+ | 'document-request'
29
+ | 'document-request-failed'
30
+ | 'document-request-finished'
31
+ | 'document-response'
32
+ | 'domcontentloaded'
33
+ | 'error'
34
+ | 'load'
35
+ | 'start'
36
+
37
+ type NavigationDiagnostic = {
38
+ browserId: number
39
+ capturedAt: string
40
+ contextId: number
41
+ errorText?: string
42
+ failurePhase?: 'bridge' | 'navigation'
43
+ headersReceived?: boolean
44
+ method?: string
45
+ milestone: NavigationMilestone
46
+ navigationUrl: string
47
+ navigationId: number
48
+ operation: SootsimNavigationIdentity['operation']
49
+ pageState?: SootsimPageState | { evaluateError: string }
50
+ status?: number
51
+ target: string
52
+ type: 'rnx-navigation'
53
+ url: string
54
+ }
55
+
56
+ type PageDiagnostic =
57
+ | NavigationDiagnostic
58
+ | {
59
+ capturedAt: string
60
+ level: string
61
+ message: string
62
+ type: 'page-console'
63
+ }
64
+ | {
65
+ capturedAt: string
66
+ message: string
67
+ type: 'page-error'
68
+ }
69
+ | {
70
+ capturedAt: string
71
+ errorText: string
72
+ method: string
73
+ type: 'subresource-request-failed'
74
+ url: string
75
+ }
76
+
77
+ const DIAGNOSTIC_LIMIT = 40
78
+ const pageDiagnostics = new WeakMap<Page, PageDiagnostic[]>()
79
+ const activeNavigations = new WeakMap<Page, ActiveNavigation>()
80
+ const lastNavigations = new WeakMap<Page, ActiveNavigation>()
81
+ let nextNavigationIdentity = 0
82
+
83
+ function recordPageDiagnostic(page: Page, diagnostic: PageDiagnostic, persist = false) {
84
+ const entries = pageDiagnostics.get(page)
85
+ if (!entries) return
86
+ entries.push(diagnostic)
87
+ if (entries.length > DIAGNOSTIC_LIMIT) entries.shift()
88
+ if (persist) writeAttemptEvent(diagnostic)
89
+ }
90
+
91
+ function isTopLevelDocumentRequest(page: Page, request: Request) {
92
+ return (
93
+ request.isNavigationRequest() &&
94
+ request.resourceType() === 'document' &&
95
+ request.frame() === page.mainFrame()
96
+ )
97
+ }
98
+
99
+ function recordNavigationMilestoneFor(
100
+ page: Page,
101
+ active: ActiveNavigation,
102
+ milestone: NavigationMilestone,
103
+ details: Partial<
104
+ Pick<
105
+ NavigationDiagnostic,
106
+ | 'errorText'
107
+ | 'failurePhase'
108
+ | 'headersReceived'
109
+ | 'method'
110
+ | 'pageState'
111
+ | 'status'
112
+ | 'url'
113
+ >
114
+ > = {},
115
+ ) {
116
+ recordPageDiagnostic(
117
+ page,
118
+ {
119
+ ...active,
120
+ ...details,
121
+ capturedAt: new Date().toISOString(),
122
+ milestone,
123
+ target: process.env.CONFORMANCE_PROOF_TARGET || 'sootsim',
124
+ type: 'rnx-navigation',
125
+ url: details.url ?? active.navigationUrl,
126
+ },
127
+ true,
128
+ )
129
+ }
130
+
131
+ function recordNavigationMilestone(
132
+ page: Page,
133
+ milestone: NavigationMilestone,
134
+ details: Parameters<typeof recordNavigationMilestoneFor>[3] = {},
135
+ ) {
136
+ const active = activeNavigations.get(page)
137
+ if (active) recordNavigationMilestoneFor(page, active, milestone, details)
138
+ }
139
+
140
+ function attachPageDiagnostics(page: Page) {
141
+ if (pageDiagnostics.has(page)) return
142
+ pageDiagnostics.set(page, [])
143
+ page.on('console', (message) => {
144
+ recordPageDiagnostic(page, {
145
+ capturedAt: new Date().toISOString(),
146
+ level: message.type(),
147
+ message: message.text(),
148
+ type: 'page-console',
149
+ })
150
+ })
151
+ page.on('pageerror', (error) => {
152
+ recordPageDiagnostic(page, {
153
+ capturedAt: new Date().toISOString(),
154
+ message: error.message,
155
+ type: 'page-error',
156
+ })
157
+ })
158
+ page.on('request', (request) => {
159
+ if (!isTopLevelDocumentRequest(page, request)) return
160
+ recordNavigationMilestone(page, 'document-request', {
161
+ method: request.method(),
162
+ url: request.url(),
163
+ })
164
+ })
165
+ page.on('response', (response) => {
166
+ const request = response.request()
167
+ if (!isTopLevelDocumentRequest(page, request)) return
168
+ recordNavigationMilestone(page, 'document-response', {
169
+ headersReceived: true,
170
+ status: response.status(),
171
+ url: request.url(),
172
+ })
173
+ })
174
+ page.on('requestfinished', (request) => {
175
+ if (!isTopLevelDocumentRequest(page, request)) return
176
+ recordNavigationMilestone(page, 'document-request-finished', { url: request.url() })
177
+ })
178
+ page.on('requestfailed', (request) => {
179
+ const failure = request.failure()
180
+ const errorText = failure?.errorText ?? ''
181
+ if (isTopLevelDocumentRequest(page, request)) {
182
+ recordNavigationMilestone(page, 'document-request-failed', {
183
+ errorText,
184
+ url: request.url(),
185
+ })
186
+ return
187
+ }
188
+ recordPageDiagnostic(page, {
189
+ capturedAt: new Date().toISOString(),
190
+ errorText,
191
+ method: request.method(),
192
+ type: 'subresource-request-failed',
193
+ url: request.url(),
194
+ })
195
+ })
196
+ page.on('domcontentloaded', () => {
197
+ recordNavigationMilestone(page, 'domcontentloaded')
198
+ })
199
+ page.on('load', () => {
200
+ recordNavigationMilestone(page, 'load')
201
+ })
202
+ }
203
+
204
+ async function readSootsimPageState(
205
+ page: Page,
206
+ ): Promise<SootsimPageState | { evaluateError: string }> {
207
+ try {
208
+ return await page.evaluate(() => ({
209
+ bodyText: document.body?.innerText?.slice(0, 500) ?? '',
210
+ bridges: {
211
+ sootsim: Boolean(window.SootSim),
212
+ test: Boolean(window.__sootsimTest),
213
+ },
214
+ globals: Object.keys(window)
215
+ .filter((key) => key.startsWith('__sootsim') || key === 'SootSim')
216
+ .sort(),
217
+ readyState: document.readyState,
218
+ title: document.title,
219
+ url: window.location.href,
220
+ }))
221
+ } catch (error) {
222
+ return {
223
+ evaluateError: error instanceof Error ? error.message : String(error),
224
+ }
225
+ }
226
+ }
227
+
228
+ export async function describeSootsimBridgeFailure(
229
+ page: Page,
230
+ cause: unknown,
231
+ summary = 'timed out waiting for sootsim test bridge',
232
+ pageState?: SootsimPageState | { evaluateError: string },
233
+ ) {
234
+ const recordBridgeFailure = pageState === undefined
235
+ const state = pageState ?? (await readSootsimPageState(page))
236
+ const navigation = lastNavigations.get(page)
237
+ if (recordBridgeFailure && navigation) {
238
+ recordNavigationMilestoneFor(page, navigation, 'error', {
239
+ errorText: cause instanceof Error ? cause.message : String(cause),
240
+ failurePhase: 'bridge',
241
+ pageState: state,
242
+ })
243
+ }
244
+ return new Error(
245
+ [
246
+ summary,
247
+ `cause: ${cause instanceof Error ? cause.message : String(cause)}`,
248
+ `page: ${JSON.stringify(state)}`,
249
+ `recent page diagnostics:\n${JSON.stringify(getSootsimNavigationDiagnostics(page))}`,
250
+ ].join('\n'),
251
+ )
252
+ }
253
+
254
+ export async function navigateSootsimPage(
255
+ page: Page,
256
+ url: string,
257
+ identity: SootsimNavigationIdentity,
258
+ ): Promise<Response | null> {
259
+ attachPageDiagnostics(page)
260
+ const navigation = {
261
+ ...identity,
262
+ navigationUrl: url,
263
+ navigationId: ++nextNavigationIdentity,
264
+ }
265
+ activeNavigations.set(page, navigation)
266
+ lastNavigations.set(page, navigation)
267
+ recordNavigationMilestone(page, 'start')
268
+ try {
269
+ return await page.goto(url, { waitUntil: 'load', timeout: 30000 })
270
+ } catch (error) {
271
+ const pageState = await readSootsimPageState(page)
272
+ recordNavigationMilestone(page, 'error', {
273
+ errorText: error instanceof Error ? error.message : String(error),
274
+ failurePhase: 'navigation',
275
+ pageState,
276
+ })
277
+ throw await describeSootsimBridgeFailure(
278
+ page,
279
+ error,
280
+ 'sootsim navigation failed',
281
+ pageState,
282
+ )
283
+ } finally {
284
+ activeNavigations.delete(page)
285
+ }
286
+ }
287
+
288
+ export function getSootsimNavigationDiagnostics(page: Page): readonly PageDiagnostic[] {
289
+ return [...(pageDiagnostics.get(page) ?? [])]
290
+ }
@@ -0,0 +1,43 @@
1
+ // sootsim-compatible replacement for kitchen-sink's e2e/utils/navigation.ts
2
+ // instead of navigating through the UI, we load the test case via URL query param
3
+
4
+ import { by, device, element, waitFor } from './index'
5
+
6
+ const BASE_URL = process.env.RNX_URL || 'http://localhost:5173'
7
+
8
+ /**
9
+ * navigate to a kitchen-sink test case in sootsim
10
+ * loads via URL query param instead of tapping through the UI
11
+ */
12
+ export async function navigateToTestCase(
13
+ testCaseName: string,
14
+ waitForElementId?: string,
15
+ ) {
16
+ const page = device.getPage()
17
+
18
+ // navigate to the test case directly via URL, preserving any params
19
+ // already on BASE_URL (device=, colorScheme=, etc.) — the conformance
20
+ // runner sets these via RNX_URL and dropping them silently rendered
21
+ // pro max at iphone 16 dimensions.
22
+ const target = new URL(BASE_URL)
23
+ target.searchParams.set('test', testCaseName)
24
+ if (target.pathname === '' || target.pathname === '/') target.pathname = '/'
25
+ const url = target.toString()
26
+ await page.goto(url, { waitUntil: 'load', timeout: 30000 })
27
+
28
+ // wait for sootsim engine to initialize
29
+ await page.waitForFunction(
30
+ 'window.__sootsimTest && window.__sootsimRoot && window.__sootsimRoot.children.length > 0',
31
+ { timeout: 30000 },
32
+ )
33
+
34
+ // let rendering settle
35
+ await page.waitForTimeout(500)
36
+
37
+ // wait for the target element if specified
38
+ if (waitForElementId) {
39
+ await waitFor(element(by.id(waitForElementId)))
40
+ .toExist()
41
+ .withTimeout(10000)
42
+ }
43
+ }
@@ -0,0 +1,114 @@
1
+ // proof captures reserve the top and bottom of a device frame for platform
2
+ // chrome. those pixels can be fully opaque while the app surface is blank or
3
+ // still fading in, so evidence density is measured only in the content band.
4
+ const PROOF_CONTENT_DEFINITION = Object.freeze({
5
+ channelThreshold: 24,
6
+ contentBandInsetFraction: 0.08,
7
+ minimumContentPercent: 0.005,
8
+ })
9
+
10
+ // conformance compares these captures against native ios baselines, so a frame
11
+ // caught mid-animation reads as a real pixel diff. three consecutive identical
12
+ // frames is the evidence that the screen actually stopped moving.
13
+ const REQUIRED_IDENTICAL_PROOF_FRAMES = 3
14
+
15
+ // a flow's `takeScreenshot` is a developer artifact, not pixel evidence. one
16
+ // settled capture is what it wants: re-capturing flashes the screen on every
17
+ // attempt, and a screen with a blinking caret never produces two identical
18
+ // frames at all, so the step fails on a page that is perfectly fine.
19
+ const REQUIRED_IDENTICAL_FLOW_FRAMES = 1
20
+
21
+ function framesIdentical(a, b) {
22
+ if (!a || !b || a.length !== b.length) return false
23
+ return a.equals(b)
24
+ }
25
+
26
+ function measureProofFrameContent(input) {
27
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
28
+ throw new Error('proof content measurement requires an input object')
29
+ }
30
+ const { data, width, height } = input
31
+ if (
32
+ !data ||
33
+ typeof data.length !== 'number' ||
34
+ !Number.isInteger(width) ||
35
+ !Number.isInteger(height) ||
36
+ width <= 0 ||
37
+ height <= 0 ||
38
+ data.length !== width * height * 4
39
+ ) {
40
+ throw new Error('proof content measurement needs RGBA pixels and dimensions')
41
+ }
42
+
43
+ const yStart = Math.floor(height * PROOF_CONTENT_DEFINITION.contentBandInsetFraction)
44
+ const yEnd = Math.ceil(height * (1 - PROOF_CONTENT_DEFINITION.contentBandInsetFraction))
45
+ const bucketCounts = new Uint32Array(32 * 32 * 32)
46
+ let totalPixels = 0
47
+ for (let y = yStart; y < yEnd; y++) {
48
+ for (let x = 0; x < width; x++) {
49
+ const offset = (y * width + x) * 4
50
+ const bucket =
51
+ ((data[offset] >> 3) << 10) |
52
+ ((data[offset + 1] >> 3) << 5) |
53
+ (data[offset + 2] >> 3)
54
+ bucketCounts[bucket]++
55
+ totalPixels++
56
+ }
57
+ }
58
+
59
+ let dominantBucket = 0
60
+ for (let index = 1; index < bucketCounts.length; index++) {
61
+ if (bucketCounts[index] > bucketCounts[dominantBucket]) dominantBucket = index
62
+ }
63
+ const dominant = {
64
+ red: (((dominantBucket >> 10) & 31) << 3) + 4,
65
+ green: (((dominantBucket >> 5) & 31) << 3) + 4,
66
+ blue: ((dominantBucket & 31) << 3) + 4,
67
+ }
68
+
69
+ let contentPixels = 0
70
+ for (let y = yStart; y < yEnd; y++) {
71
+ for (let x = 0; x < width; x++) {
72
+ const offset = (y * width + x) * 4
73
+ if (
74
+ Math.max(
75
+ Math.abs(data[offset] - dominant.red),
76
+ Math.abs(data[offset + 1] - dominant.green),
77
+ Math.abs(data[offset + 2] - dominant.blue),
78
+ ) >= PROOF_CONTENT_DEFINITION.channelThreshold
79
+ ) {
80
+ contentPixels++
81
+ }
82
+ }
83
+ }
84
+
85
+ const contentPercent = (contentPixels / totalPixels) * 100
86
+ return {
87
+ contentPercent,
88
+ contentPixels,
89
+ dominant,
90
+ passedMinimum: contentPercent >= PROOF_CONTENT_DEFINITION.minimumContentPercent,
91
+ totalPixels,
92
+ }
93
+ }
94
+
95
+ function settleDecision(previousPixels, currentFrame) {
96
+ if (!currentFrame) return { decision: 'continue', content: null }
97
+ const content = measureProofFrameContent(currentFrame)
98
+ if (!content.passedMinimum) {
99
+ return { decision: 'insufficient-content', content }
100
+ }
101
+ if (previousPixels && framesIdentical(previousPixels, currentFrame.data)) {
102
+ return { decision: 'identical', content }
103
+ }
104
+ return { decision: 'changed', content }
105
+ }
106
+
107
+ module.exports = {
108
+ PROOF_CONTENT_DEFINITION,
109
+ REQUIRED_IDENTICAL_PROOF_FRAMES,
110
+ REQUIRED_IDENTICAL_FLOW_FRAMES,
111
+ framesIdentical,
112
+ measureProofFrameContent,
113
+ settleDecision,
114
+ }