peachsim 0.1.578 → 0.1.579

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 (178) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +11 -9
  3. package/cli/app-catalog.ts +77 -0
  4. package/cli/app-fonts.ts +4 -20
  5. package/cli/app-splash.ts +2 -18
  6. package/cli/bridge-flow-runner.ts +505 -139
  7. package/cli/cloud-client.ts +1 -4
  8. package/cli/cloud-session.ts +1 -4
  9. package/cli/cloud-test-result-files.ts +3 -0
  10. package/cli/cloud-test-tunnel.ts +2 -5
  11. package/cli/command-registry.ts +2 -0
  12. package/cli/commands/agent.ts +149 -23
  13. package/cli/commands/assert.ts +1 -4
  14. package/cli/commands/box/peach-command.ts +62 -0
  15. package/cli/commands/box/plane-share.ts +0 -70
  16. package/cli/commands/box/rnx-command.ts +1 -60
  17. package/cli/commands/box/search-command.ts +14 -0
  18. package/cli/commands/box.ts +2 -2
  19. package/cli/commands/control.ts +121 -27
  20. package/cli/commands/cpu-profile.ts +23 -10
  21. package/cli/commands/daemon.ts +1 -9
  22. package/cli/commands/detox.ts +26 -18
  23. package/cli/commands/device.ts +129 -1
  24. package/cli/commands/film.ts +25 -16
  25. package/cli/commands/flow.ts +9 -2
  26. package/cli/commands/inspect/actions.ts +75 -1
  27. package/cli/commands/inspect/core.ts +81 -2
  28. package/cli/commands/inspect/describe.ts +2 -6
  29. package/cli/commands/inspect/env.ts +1 -1
  30. package/cli/commands/inspect/settling.ts +20 -3
  31. package/cli/commands/inspect/shared.ts +1 -4
  32. package/cli/commands/inspect/sleep.ts +1 -1
  33. package/cli/commands/inspect.ts +16 -49
  34. package/cli/commands/install-desktop.ts +8 -6
  35. package/cli/commands/login.ts +3 -5
  36. package/cli/commands/maestro-generate.ts +1 -4
  37. package/cli/commands/maestro.ts +38 -21
  38. package/cli/commands/no-bridge-hint.ts +0 -14
  39. package/cli/commands/permissions.ts +2 -2
  40. package/cli/commands/platform.ts +13 -6
  41. package/cli/commands/screenshot.ts +6 -1
  42. package/cli/commands/skills.ts +1 -1
  43. package/cli/commands/test.ts +6 -2
  44. package/cli/commands/three-mode.ts +162 -19
  45. package/cli/commands/upload.ts +3 -18
  46. package/cli/current-sim.ts +0 -1
  47. package/cli/desktop-companion.ts +40 -8
  48. package/cli/drivers/playwright-provisioning.ts +1 -4
  49. package/cli/flow-export.ts +1 -8
  50. package/cli/flow-session.ts +0 -4
  51. package/cli/hints.ts +1 -16
  52. package/cli/inspect-notice-state.ts +0 -4
  53. package/cli/main.ts +2 -4
  54. package/cli/outbound-endpoints.ts +1 -24
  55. package/cli/parse-args.ts +1 -2
  56. package/cli/rnx-in-process.ts +3 -1
  57. package/cli/run-rnx-core.ts +4 -2
  58. package/cli/run-rnx.ts +18 -10
  59. package/cli/send-to-box.ts +1 -4
  60. package/cli/telemetry.ts +1 -1
  61. package/cli/ws-bridge.ts +1 -5
  62. package/detox/gestures.ts +20 -6
  63. package/detox/index.ts +379 -123
  64. package/detox/navigation-lifecycle.ts +22 -13
  65. package/dist-lib/agent-daemon-client.cjs +21 -15
  66. package/dist-lib/agent-events.cjs +1 -1
  67. package/dist-lib/agent-identity.cjs +3 -17
  68. package/dist-lib/agent-sessions.cjs +5 -4
  69. package/dist-lib/attached-projects.cjs +1 -77
  70. package/dist-lib/auth/shared-session.cjs +1 -1
  71. package/dist-lib/backend-origin.cjs +11 -1
  72. package/dist-lib/beta.cjs +1 -1
  73. package/dist-lib/beta.mjs +1 -1
  74. package/dist-lib/bridge-constants.cjs +1 -1
  75. package/dist-lib/bridge-contract-input.cjs +7 -5
  76. package/dist-lib/bridge-contract-input.mjs +7 -5
  77. package/dist-lib/bridge-contract.cjs +1 -1
  78. package/dist-lib/bridge-contract.mjs +1 -1
  79. package/dist-lib/capture-contract.cjs +30 -7
  80. package/dist-lib/capture-contract.mjs +30 -7
  81. package/dist-lib/cli-constants.cjs +1 -1
  82. package/dist-lib/cloud-contract.cjs +1 -1
  83. package/dist-lib/cloud-contract.mjs +1 -1
  84. package/dist-lib/cloud-test-args.cjs +1 -1
  85. package/dist-lib/cloud-test-args.mjs +1 -1
  86. package/dist-lib/cloud.cjs +17 -11
  87. package/dist-lib/cloud.mjs +17 -11
  88. package/dist-lib/config.cjs +1 -1
  89. package/dist-lib/detox/index.cjs +321 -122
  90. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  91. package/dist-lib/home-paths.cjs +1 -1
  92. package/dist-lib/host/bridge-host.cjs +427 -212
  93. package/dist-lib/host/fetch-proxy-handler.cjs +5 -28
  94. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  95. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  96. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  97. package/dist-lib/host/websocket-proxy.cjs +1 -1
  98. package/dist-lib/index.cjs +315 -58
  99. package/dist-lib/jump-to-source-babel.cjs +1 -1
  100. package/dist-lib/jump-to-source-native.cjs +1 -1
  101. package/dist-lib/menu.cjs +67 -31
  102. package/dist-lib/menu.mjs +64 -28
  103. package/dist-lib/metro-fingerprint-registry.cjs +8 -4
  104. package/dist-lib/metro-fingerprint-registry.mjs +6 -4
  105. package/dist-lib/metro-production-bundle.cjs +2 -2
  106. package/dist-lib/metro-production-bundle.mjs +2 -2
  107. package/dist-lib/metro.cjs +8 -4
  108. package/dist-lib/profiles.cjs +1 -1
  109. package/dist-lib/public-brand.cjs +7 -4
  110. package/dist-lib/react-native-host-modules.cjs +1 -1
  111. package/dist-lib/react-native-host-modules.mjs +1 -1
  112. package/dist-lib/render-mode.cjs +3 -13
  113. package/dist-lib/scripts/dev-server-scanner.cjs +5 -17
  114. package/dist-lib/sdk.cjs +129 -10
  115. package/dist-lib/sdk.mjs +127 -10
  116. package/dist-lib/skills.cjs +1635 -1121
  117. package/dist-lib/swift-compile.cjs +277 -70
  118. package/dist-lib/swift-compile.mjs +275 -70
  119. package/dist-lib/swift-package.cjs +17 -5
  120. package/dist-lib/swift-package.mjs +15 -4
  121. package/dist-lib/swift-submit.cjs +32 -8
  122. package/dist-lib/swift-submit.mjs +29 -7
  123. package/dist-lib/swift.cjs +30912 -577
  124. package/dist-lib/vite-kotlin.cjs +356 -0
  125. package/dist-lib/vite-swift.cjs +1743 -0
  126. package/dist-lib/vite.cjs +208 -1039
  127. package/kotlin/README.md +18 -0
  128. package/kotlin/Sources/androidx/annotation/Keep.kt +4 -0
  129. package/kotlin/Sources/dev/onejs/one/OneNativeSourceDispatch.kt +7 -0
  130. package/kotlin/Sources/dev/onejs/one/source/OneModule.kt +4 -0
  131. package/kotlin/Sources/org/json/JSON.kt +244 -0
  132. package/kotlin/toolchain.json +19 -0
  133. package/package.json +14 -2
  134. package/scripts/dev-server-scanner.ts +0 -18
  135. package/scripts/optional-demo-registry.ts +0 -17
  136. package/skills/contrast/SKILL.md +2 -2
  137. package/skills/peach-setup/SKILL.md +16 -0
  138. package/src/agent-daemon-client.ts +36 -22
  139. package/src/agent-identity.ts +0 -15
  140. package/src/agent-prompt.ts +12 -0
  141. package/src/attached-projects.ts +0 -72
  142. package/src/backend-origin.ts +10 -0
  143. package/src/bridge-contract-input.ts +2 -5
  144. package/src/bridge-contract.ts +2 -0
  145. package/src/capture-contract.ts +53 -11
  146. package/src/cloud.ts +4 -9
  147. package/src/dev-server-open.ts +0 -99
  148. package/src/home-paths.ts +2 -3
  149. package/src/host/agent-host.ts +82 -36
  150. package/src/host/bridge-host.ts +5 -11
  151. package/src/host/fetch-proxy-handler.ts +19 -43
  152. package/src/is-record.ts +4 -0
  153. package/src/menu.ts +82 -37
  154. package/src/metro-fingerprint-registry.ts +1 -4
  155. package/src/metro-production-bundle.ts +1 -1
  156. package/src/plist.ts +1 -4
  157. package/src/public-brand.ts +5 -3
  158. package/src/render-mode.ts +0 -12
  159. package/src/runtime-assets.ts +9 -2
  160. package/src/screenshots/compose.ts +7 -1
  161. package/src/screenshots/frame-compose.ts +21 -10
  162. package/src/screenshots/schema.ts +1 -4
  163. package/src/sim-client.ts +2 -4
  164. package/src/skills/builtin/maestro-test.ts +1 -4
  165. package/src/sleep.ts +4 -0
  166. package/src/swift-compile.ts +209 -47
  167. package/src/swift-icu.ts +140 -0
  168. package/src/swift-package.ts +26 -2
  169. package/src/swift-submit.ts +84 -4
  170. package/src/swift.ts +2 -0
  171. package/src/vite-plugin-kotlin.ts +376 -0
  172. package/src/vite-plugin-one.ts +224 -36
  173. package/src/vite-plugin-swift.ts +382 -96
  174. package/src/vite-plugin.ts +121 -134
  175. package/src/vite.d.ts +10 -0
  176. package/cli/resolve-assets.ts +0 -63
  177. package/src/rolldown-plugin-swift.ts +0 -244
  178. package/src/team-machine-prompt.ts +0 -73
@@ -11,7 +11,7 @@ import { mkdirSync, writeFileSync } from 'node:fs'
11
11
  import { dirname, resolve } from 'node:path'
12
12
  import yaml from 'yaml'
13
13
  import { validateFlowSteps } from './flow-file'
14
- import { finalizeFlowSession, readFlowSession } from './flow-session'
14
+ import { readFlowSession } from './flow-session'
15
15
 
16
16
  export function exportFlowSession(outputPath?: string) {
17
17
  const state = readFlowSession()
@@ -46,10 +46,3 @@ export function exportFlowSession(outputPath?: string) {
46
46
  outputPath: resolvedOutputPath,
47
47
  }
48
48
  }
49
-
50
- export function endFlowSession(outputPath?: string) {
51
- const result = exportFlowSession(outputPath)
52
- if (!result.active) return result
53
- finalizeFlowSession()
54
- return result
55
- }
@@ -62,10 +62,6 @@ export function readFlowSession(): FlowSessionState | null {
62
62
  }
63
63
  }
64
64
 
65
- export function hasActiveFlowSession() {
66
- return readFlowSession() !== null
67
- }
68
-
69
65
  export function startFlowSession() {
70
66
  const now = new Date().toISOString()
71
67
  const state: FlowSessionState = {
package/cli/hints.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  // suppression is keyed on `getCliIdentityKey()` so two agents / two terminals
15
15
  // don't share shown-sets.
16
16
 
17
- import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs'
17
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
18
18
  import { tmpdir } from 'os'
19
19
  import { dirname, join } from 'path'
20
20
  import { getCliIdentityKey } from './current-sim'
@@ -136,21 +136,6 @@ export function maybeHint(id: string, ...args: unknown[]): boolean {
136
136
  return true
137
137
  }
138
138
 
139
- export function resetHintsForIdentity() {
140
- rmSync(identityStatePath(), { force: true })
141
- }
142
-
143
- export function resetGlobalHints() {
144
- rmSync(globalStatePath(), { force: true })
145
- }
146
-
147
- export function listHints(): Array<{ id: string; frequency: HintFrequency }> {
148
- return Array.from(registry.entries()).map(([id, spec]) => ({
149
- id,
150
- frequency: spec.frequency,
151
- }))
152
- }
153
-
154
139
  // register the repo-wide hints here. keeping them centralised makes it easy
155
140
  // to audit wording + frequency policy in one place.
156
141
  defineHint<[number]>('app-still-loading', {
@@ -108,7 +108,3 @@ export function shouldPrintInspectNotice(
108
108
  writeInspectNoticeState(state)
109
109
  return true
110
110
  }
111
-
112
- export function clearInspectNoticeState() {
113
- rmSync(getInspectNoticeStatePath(), { force: true })
114
- }
package/cli/main.ts CHANGED
@@ -242,7 +242,6 @@ const port = parsed.globalFlags['port'] as number | undefined
242
242
  const verbose = parsed.verbose
243
243
  const device = parsed.globalFlags['device'] as string | undefined
244
244
  const theme = parsed.globalFlags['theme'] as string | undefined
245
- const homeScreen = parsed.globalFlags['home-screen'] as string | undefined
246
245
  const driver = parsed.globalFlags['driver'] as string | undefined
247
246
  const headless = (parsed.globalFlags['headless'] as boolean | undefined) === true
248
247
  const globalSimTarget = (parsed.globalFlags['sim'] ??
@@ -252,13 +251,12 @@ const commandArgsWithSim = globalSimTarget
252
251
  ? ['--sim', globalSimTarget, ...parsed.commandArgs]
253
252
  : parsed.commandArgs
254
253
 
255
- // apply device/theme/home-screen to settings store if provided
256
- if (device || theme || homeScreen) {
254
+ // apply device/theme to settings store if provided
255
+ if (device || theme) {
257
256
  const { settingsStore } = await import('peach-engine/settings/store')
258
257
  const overrides: Record<string, any> = {}
259
258
  if (device) overrides.deviceModel = device
260
259
  if (theme) overrides.colorScheme = theme
261
- if (homeScreen) overrides.homeScreenVersion = homeScreen
262
260
  settingsStore.apply(overrides)
263
261
  }
264
262
 
@@ -128,7 +128,7 @@ export const OUTBOUND_ENDPOINTS: Record<string, OutboundEndpointDeclaration> = {
128
128
  peach_cloud_sim: {
129
129
  category: 'explicit-user-action',
130
130
  destination:
131
- 'https://rnxsim.com by default, or the test or disposable origin named by PEACH_CLOUD_ORIGIN',
131
+ 'https://peachsim.com by default, or the test or disposable origin named by PEACH_CLOUD_ORIGIN',
132
132
  preConsent: false,
133
133
  payload:
134
134
  'the Metro JavaScript bundle selected by peach ios --remote, then structured simulator commands and lifecycle requests',
@@ -856,26 +856,3 @@ export function detectOutboundCalls(repositoryRoot: string): DetectedOutboundCal
856
856
  .map(([key, count]) => ({ key, count }))
857
857
  .sort((a, b) => a.key.localeCompare(b.key))
858
858
  }
859
-
860
- export function undeclaredOutboundCalls(repositoryRoot: string): string[] {
861
- const detected = detectOutboundCalls(repositoryRoot)
862
- const failures: string[] = []
863
- const seen = new Set<string>()
864
- for (const call of detected) {
865
- seen.add(call.key)
866
- const declaration = DECLARED_OUTBOUND_CALLS[call.key]
867
- if (!declaration) {
868
- failures.push(`undeclared: ${call.key} (count ${call.count})`)
869
- continue
870
- }
871
- if (declaration.count !== call.count) {
872
- failures.push(
873
- `count changed: ${call.key} (declared ${declaration.count}, found ${call.count})`,
874
- )
875
- }
876
- }
877
- for (const key of Object.keys(DECLARED_OUTBOUND_CALLS)) {
878
- if (!seen.has(key)) failures.push(`stale declaration: ${key}`)
879
- }
880
- return failures
881
- }
package/cli/parse-args.ts CHANGED
@@ -77,8 +77,7 @@ export const HYPHENATED_GROUPING_VERBS = new Set(['do', 'get', 'wait', 'shell',
77
77
 
78
78
  // known commands — if first positional arg matches, it's a command.
79
79
  // every entry here must have a `cliCommandMetas` entry in
80
- // `packages/peach-skills/src/cli/meta.ts` (or sit on the internal
81
- // allowlist in `test/sootsimCliRegistry.test.ts`), so help, website docs,
80
+ // `packages/peach-skills/src/cli/meta.ts`, so help, website docs,
82
81
  // and agent skills can never drift from what the parser accepts.
83
82
  export const COMMANDS = new Set(RNX_NODE_COMMAND_NAMES)
84
83
 
@@ -86,10 +86,12 @@ export async function runRnxInProcess(
86
86
  })
87
87
  }
88
88
 
89
+ export const runPeachInProcess = runRnxInProcess
90
+
89
91
  // the transport contract and the command's own name, re-exported so a host
90
92
  // that only wants to register this entry and supply a bridge (a Box Durable
91
93
  // Object, say) has one module to import.
92
- export { rnxPublicBrand } from '../src/public-brand'
94
+ export { peachPublicBrand, rnxPublicBrand } from '../src/public-brand'
93
95
  export type {
94
96
  BridgeClaimResult,
95
97
  BridgeSimInfo,
@@ -110,12 +110,14 @@ export async function runRnxCore(
110
110
  options.backend.kind,
111
111
  )
112
112
  if (!availability.available) {
113
- captured.writer.stderr(`${availability.reason ?? `peach ${command} is unavailable`}\n`)
113
+ captured.writer.stderr(
114
+ `${availability.reason ?? `peach ${command} is unavailable`}\n`,
115
+ )
114
116
  return result(captured, 1)
115
117
  }
116
118
 
117
119
  if (signal.aborted) {
118
- captured.writer.stderr('rnx: command cancelled\n')
120
+ captured.writer.stderr('peach: command cancelled\n')
119
121
  return result(captured, 1)
120
122
  }
121
123
 
package/cli/run-rnx.ts CHANGED
@@ -28,39 +28,45 @@ export interface RnxRunResult {
28
28
  exitCode: number
29
29
  }
30
30
 
31
+ export type PeachRunResult = RnxRunResult
32
+
31
33
  interface RnxSinks {
32
34
  stdout: string[]
33
35
  stderr: string[]
34
36
  }
35
37
 
36
- // thrown by `rnxExit` in place of process.exit. every catch that reports an
38
+ // thrown by `peachExit` in place of process.exit. every catch that reports an
37
39
  // error must re-throw this before reporting, or it turns a deliberate exit
38
40
  // into a spurious "command failed" line with the sentinel's own text.
39
- export class RnxExit extends Error {
41
+ export class PeachExit extends Error {
40
42
  readonly code: number
41
43
 
42
44
  constructor(code: number) {
43
45
  super(`peach exited with code ${code}`)
44
- this.name = 'RnxExit'
46
+ this.name = 'PeachExit'
45
47
  this.code = code
46
48
  }
47
49
  }
48
50
 
51
+ export const RnxExit = PeachExit
52
+
49
53
  const sinks = new AsyncLocalStorage<RnxSinks>()
50
54
 
51
55
  // stop a command's deliberate exit from being reported as a failure. the
52
56
  // guard every reporting catch needs, in one place so the call sites stay one
53
57
  // line.
54
58
  export function rethrowIfExit(error: unknown): void {
55
- if (error instanceof RnxExit) throw error
59
+ if (error instanceof PeachExit) throw error
56
60
  }
57
61
 
58
62
  // the in-process replacement for process.exit. returns `never` exactly as
59
63
  // process.exit does, so it substitutes without changing any call site's types.
60
- export function rnxExit(code: number): never {
61
- throw new RnxExit(code)
64
+ export function peachExit(code: number): never {
65
+ throw new PeachExit(code)
62
66
  }
63
67
 
68
+ export const rnxExit: (code: number) => never = peachExit
69
+
64
70
  function write(stream: 'stdout' | 'stderr', text: string): boolean {
65
71
  const current = sinks.getStore()
66
72
  if (!current) return false
@@ -112,20 +118,20 @@ function installCapture(): void {
112
118
  }
113
119
  }
114
120
 
115
- // run one rnx invocation and return what it wrote plus its exit code, instead
121
+ // run one peach invocation and return what it wrote plus its exit code, instead
116
122
  // of writing to the terminal and ending the process. `argv` is the command and
117
123
  // its arguments with no node/script prefix, e.g. ['describe', '--json'].
118
- export async function runRnx(
124
+ export async function runPeach(
119
125
  argv: string[],
120
126
  dispatch: (argv: string[]) => Promise<number>,
121
- ): Promise<RnxRunResult> {
127
+ ): Promise<PeachRunResult> {
122
128
  installCapture()
123
129
  const current: RnxSinks = { stdout: [], stderr: [] }
124
130
  const exitCode = await sinks.run(current, async () => {
125
131
  try {
126
132
  return await dispatch(argv)
127
133
  } catch (error) {
128
- if (error instanceof RnxExit) return error.code
134
+ if (error instanceof PeachExit) return error.code
129
135
  throw error
130
136
  }
131
137
  })
@@ -135,3 +141,5 @@ export async function runRnx(
135
141
  exitCode,
136
142
  }
137
143
  }
144
+
145
+ export const runRnx = runPeach
@@ -16,6 +16,7 @@ import { createCloudBox } from './cloud-client.ts'
16
16
  import type { StorageSnapshot } from '../../peach-engine/src/preview/storage-snapshot-contract.ts'
17
17
  import type { DeviceModel } from '../../peach-engine/src/settings/devices.ts'
18
18
  import type { BridgeCommandType } from '../src/bridge-contract.ts'
19
+ import { isRecord } from '../src/is-record'
19
20
 
20
21
  export interface SendToBoxOptions {
21
22
  /** runs one bridge command against the sim running the app. */
@@ -44,10 +45,6 @@ export interface SendToBoxResult {
44
45
  warnings: string[]
45
46
  }
46
47
 
47
- function isRecord(value: unknown): value is Record<string, unknown> {
48
- return !!value && typeof value === 'object' && !Array.isArray(value)
49
- }
50
-
51
48
  // the box opens on the device the sender was looking at. a remote simulator is
52
49
  // iOS only, so an Android sim sends to the default iPhone rather than refusing.
53
50
  function cloudDevice(capturedModel: unknown): DeviceModel {
package/cli/telemetry.ts CHANGED
@@ -46,7 +46,7 @@ export function buildCliDiagnosticEvent(diagnostic: CliDiagnostic) {
46
46
 
47
47
  return {
48
48
  event: diagnostic.kind,
49
- distinct_id: 'rnx-cli-diagnostic',
49
+ distinct_id: 'peach-cli-diagnostic',
50
50
  properties,
51
51
  }
52
52
  }
package/cli/ws-bridge.ts CHANGED
@@ -416,11 +416,7 @@ export function formatLockOwner(owner: string): string {
416
416
  const source = owner.slice(0, colon)
417
417
  const value = owner.slice(colon + 1)
418
418
  const short = value.length > 12 ? `${value.slice(0, 8)}…` : value
419
- if (
420
- source === 'TM_SESSION' ||
421
- source === 'CLAUDE_CODE_SESSION_ID' ||
422
- source === 'CODEX_THREAD_ID'
423
- ) {
419
+ if (source === 'CLAUDE_CODE_SESSION_ID' || source === 'CODEX_THREAD_ID') {
424
420
  return `agent ${short}`
425
421
  }
426
422
  return `${source} ${short}`
package/detox/gestures.ts CHANGED
@@ -1,11 +1,15 @@
1
+ import { scrollTouchSlop } from '../../peach-engine/src/engine/scroll-platform'
1
2
  import { pollUntil } from '../src/poll-until'
2
3
  // pull in @peach/globals so window.__sootsimTest and
3
4
  // window.__sootsimFindInteractiveCanvas are typed inside page.evaluate
4
5
  // bodies. all sootsim-namespaced globals are declared canonically there —
5
- // add new entries to packages/rnx-globals/src/index.ts, not here.
6
+ // add new entries to packages/peach-globals/src/index.ts, not here.
6
7
  import type {} from '@peach/globals'
7
8
  import type { Page } from 'playwright'
8
9
 
10
+ export const DETOX_PLATFORM =
11
+ process.env.PEACH_PLATFORM === 'android' ? ('android' as const) : ('ios' as const)
12
+
9
13
  // shape of the surface-metrics bridge response we care about. the bridge
10
14
  // types it as `Record<string, unknown> | null` (the contract is open) so we
11
15
  // narrow at the call sites by casting to this local view.
@@ -333,25 +337,28 @@ export async function dragScrollNode(
333
337
  const startSootSimY = node.absolutePosition.y + node.layout.height * normalizedStartY
334
338
  let endSootSimX = startSootSimX
335
339
  let endSootSimY = startSootSimY
340
+ // detox scrolls the content by exactly `pixels`, so the drag also covers the
341
+ // touch slop the scroll view consumes before it moves.
342
+ const travel = pixels + scrollTouchSlop(DETOX_PLATFORM, 1)
336
343
 
337
344
  switch (direction) {
338
345
  case 'down':
339
- endSootSimY -= pixels
346
+ endSootSimY -= travel
340
347
  break
341
348
  case 'up':
342
- endSootSimY += pixels
349
+ endSootSimY += travel
343
350
  break
344
351
  case 'left':
345
- endSootSimX += pixels
352
+ endSootSimX += travel
346
353
  break
347
354
  case 'right':
348
- endSootSimX -= pixels
355
+ endSootSimX -= travel
349
356
  break
350
357
  }
351
358
 
352
359
  const startPage = await sootsimToPage(page, startSootSimX, startSootSimY)
353
360
  const endPage = await sootsimToPage(page, endSootSimX, endSootSimY)
354
- const steps = Math.max(16, Math.ceil(pixels / 8))
361
+ const steps = Math.max(16, Math.ceil(travel / 8))
355
362
 
356
363
  await page.mouse.move(startPage.x, startPage.y)
357
364
  await page.mouse.down()
@@ -364,6 +371,13 @@ export async function dragScrollNode(
364
371
  await page.waitForTimeout(24)
365
372
  }
366
373
 
374
+ // detox's scroll never flings on device: ios turns deceleration off for the
375
+ // action's drag and android's linear swiper waits out any fling. this drag
376
+ // runs right at the momentum floor, so lifting straight off the last move
377
+ // flings or settles by event timing. resting past the release estimator's
378
+ // idle cutoff lifts with zero velocity, and the content stops where the
379
+ // drag ended.
380
+ await page.waitForTimeout(300)
367
381
  await page.mouse.up()
368
382
  await page.waitForTimeout(180)
369
383
  }