agent-device 0.18.3 → 0.19.1

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 (219) hide show
  1. package/README.md +5 -1
  2. package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.18.3.apk → agent-device-android-multitouch-helper-0.19.1.apk} +0 -0
  3. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.1.apk.sha256 +1 -0
  4. package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.18.3.manifest.json → agent-device-android-multitouch-helper-0.19.1.manifest.json} +4 -4
  5. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.18.3.apk → agent-device-android-snapshot-helper-0.19.1.apk} +0 -0
  6. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.1.apk.sha256 +1 -0
  7. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.18.3.manifest.json → agent-device-android-snapshot-helper-0.19.1.manifest.json} +6 -6
  8. package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTapPointPolicy.swift +28 -0
  9. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +33 -127
  10. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +630 -177
  11. package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +155 -0
  12. package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +67 -0
  13. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +173 -122
  14. package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +172 -0
  15. package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +64 -0
  16. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +76 -182
  17. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +92 -51
  18. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +213 -137
  19. package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +142 -0
  20. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +208 -46
  21. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +82 -11
  22. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +74 -0
  23. package/dist/src/agent-browser-tool.js +1 -0
  24. package/dist/src/android-adb.d.ts +63 -234
  25. package/dist/src/android-adb.js +1 -1
  26. package/dist/src/android-input-ownership.js +6 -0
  27. package/dist/src/android.js +1 -5
  28. package/dist/src/app-lifecycle.js +7 -0
  29. package/dist/src/apple-runner-options.js +1 -0
  30. package/dist/src/apps.js +5 -11
  31. package/dist/src/artifacts.d.ts +4 -3
  32. package/dist/src/artifacts.js +1 -1
  33. package/dist/src/audio-probe.js +1 -1
  34. package/dist/src/batch.d.ts +2 -177
  35. package/dist/src/batch.js +1 -1
  36. package/dist/src/cli-help.js +178 -60
  37. package/dist/src/cli-output.js +1 -1
  38. package/dist/src/cli-runner.js +2 -0
  39. package/dist/src/cli.js +12 -41
  40. package/dist/src/client-metro-companion.js +1 -0
  41. package/dist/src/client.js +3 -0
  42. package/dist/src/command-metadata.js +1 -0
  43. package/dist/src/command-schema.js +22 -0
  44. package/dist/src/command-surface.js +1 -1
  45. package/dist/src/contracts.d.ts +2 -213
  46. package/dist/src/contracts.js +1 -1
  47. package/dist/src/debug-symbols.js +5 -3
  48. package/dist/src/device-input-state.js +2 -0
  49. package/dist/src/devices.js +1 -1
  50. package/dist/src/devices2.js +1 -0
  51. package/dist/src/devices3.js +1 -0
  52. package/dist/src/find.js +1 -1
  53. package/dist/src/finders.d.ts +2 -81
  54. package/dist/src/finders.js +1 -1
  55. package/dist/src/generic.js +1 -1
  56. package/dist/src/handler-utils.js +1 -1
  57. package/dist/src/helper.js +2 -0
  58. package/dist/src/host-process.js +2 -0
  59. package/dist/src/index.d.ts +1444 -1786
  60. package/dist/src/index.js +1 -1
  61. package/dist/src/input-actions.js +3 -1
  62. package/dist/src/input-actions2.js +1 -0
  63. package/dist/src/install-source.d.ts +13 -14
  64. package/dist/src/install-source.js +1 -1
  65. package/dist/src/install-source2.js +1 -0
  66. package/dist/src/interaction-snapshot.js +1 -0
  67. package/dist/src/interaction.js +1 -1
  68. package/dist/src/interactor.js +2 -1
  69. package/dist/src/internal/bin.d.ts +1 -1
  70. package/dist/src/internal/bin.js +2 -5
  71. package/dist/src/internal/companion-tunnel.d.ts +1 -1
  72. package/dist/src/internal/companion-tunnel.js +1 -1
  73. package/dist/src/internal/daemon.d.ts +1 -1
  74. package/dist/src/internal/daemon.js +18 -1
  75. package/dist/src/internal/png-worker.d.ts +29 -26
  76. package/dist/src/internal/png-worker.js +1 -1
  77. package/dist/src/internal/update-check-entry.d.ts +1 -1
  78. package/dist/src/internal/update-check-entry.js +1 -1
  79. package/dist/src/io.d.ts +2 -85
  80. package/dist/src/io.js +1 -1
  81. package/dist/src/lease.js +1 -1
  82. package/dist/src/linux.js +1 -1
  83. package/dist/src/manifest.js +1 -0
  84. package/dist/src/metro.d.ts +69 -113
  85. package/dist/src/metro.js +1 -1
  86. package/dist/src/metro2.js +1 -0
  87. package/dist/src/notifications.js +1 -1
  88. package/dist/src/output-path.js +1 -0
  89. package/dist/src/path-resolution.js +1 -0
  90. package/dist/src/png-worker-contract.js +1 -0
  91. package/dist/src/points.js +4 -0
  92. package/dist/src/project-runtime.js +1 -0
  93. package/dist/src/provider.js +1 -1
  94. package/dist/src/providers.js +1 -0
  95. package/dist/src/react-native.js +1 -1
  96. package/dist/src/record-trace-recording.js +2 -5
  97. package/dist/src/record-trace.js +1 -1
  98. package/dist/src/recording-provider.js +1 -1
  99. package/dist/src/registry.js +38 -0
  100. package/dist/src/remote-config.d.ts +2 -83
  101. package/dist/src/reporting.js +9 -0
  102. package/dist/src/rolldown-runtime.js +1 -0
  103. package/dist/src/runner-client.js +1 -0
  104. package/dist/src/runner-provider.js +1 -0
  105. package/dist/src/runner-sequence.js +1 -1
  106. package/dist/src/runtime-transport.js +1 -0
  107. package/dist/src/runtime.js +3 -0
  108. package/dist/src/scoped-provider.js +1 -0
  109. package/dist/src/sdk-android-adb.d.ts +144 -0
  110. package/dist/src/sdk-android-adb.js +4 -0
  111. package/dist/src/sdk-app-inventory.d.ts +4 -0
  112. package/dist/src/sdk-app-inventory.js +1 -0
  113. package/dist/src/sdk-batch-runner.d.ts +39 -0
  114. package/dist/src/sdk-batch-runner.js +2 -0
  115. package/dist/src/sdk-batch.js +2 -0
  116. package/dist/src/sdk-contracts.d.ts +386 -0
  117. package/dist/src/sdk-contracts.js +2 -0
  118. package/dist/src/sdk-finders.d.ts +60 -0
  119. package/dist/src/sdk-finders.js +1 -0
  120. package/dist/src/sdk-io.d.ts +78 -0
  121. package/dist/src/sdk-io.js +1 -0
  122. package/dist/src/sdk-remote-config.d.ts +139 -0
  123. package/dist/src/sdk-remote-config.js +1 -0
  124. package/dist/src/sdk-selectors.js +1 -0
  125. package/dist/src/selector-runtime.js +1 -1
  126. package/dist/src/selectors.d.ts +38 -137
  127. package/dist/src/selectors.js +1 -1
  128. package/dist/src/selectors2.js +1 -0
  129. package/dist/src/server.js +3 -2
  130. package/dist/src/session.js +10 -11
  131. package/dist/src/setting-state.js +1 -0
  132. package/dist/src/simctl.js +1 -0
  133. package/dist/src/simulator.js +1 -2
  134. package/dist/src/snapshot-diagnostics.js +3 -0
  135. package/dist/src/snapshot-helper.js +5 -0
  136. package/dist/src/snapshot-quality.js +1 -0
  137. package/dist/src/snapshot.js +1 -2
  138. package/dist/src/snapshot2.js +2 -0
  139. package/dist/src/{5560.js → timeouts.js} +1 -1
  140. package/dist/src/tool-provider.js +3 -0
  141. package/dist/src/tool-provider2.js +1 -0
  142. package/dist/src/ttl-memo.js +1 -0
  143. package/dist/src/tv-remote.js +14 -0
  144. package/dist/src/update-check.js +1 -0
  145. package/dist/src/url.js +1 -0
  146. package/dist/src/version.js +1 -0
  147. package/dist/src/web.js +1 -1
  148. package/dist/src/xml.js +2 -0
  149. package/package.json +13 -19
  150. package/server.json +2 -2
  151. package/skills/agent-device/SKILL.md +8 -7
  152. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.18.3.apk.sha256 +0 -1
  153. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.18.3.apk.sha256 +0 -1
  154. package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +0 -443
  155. package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +0 -169
  156. package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +0 -220
  157. package/dist/src/1404.js +0 -1
  158. package/dist/src/1986.js +0 -1
  159. package/dist/src/2748.js +0 -1
  160. package/dist/src/2948.js +0 -30
  161. package/dist/src/3006.js +0 -1
  162. package/dist/src/314.js +0 -1
  163. package/dist/src/3340.js +0 -8
  164. package/dist/src/4418.js +0 -1
  165. package/dist/src/4428.js +0 -3
  166. package/dist/src/485.js +0 -1
  167. package/dist/src/4917.js +0 -1
  168. package/dist/src/495.js +0 -5
  169. package/dist/src/5448.js +0 -3
  170. package/dist/src/5484.js +0 -1
  171. package/dist/src/5643.js +0 -1
  172. package/dist/src/587.js +0 -1
  173. package/dist/src/5913.js +0 -4
  174. package/dist/src/6849.js +0 -1
  175. package/dist/src/6918.js +0 -1
  176. package/dist/src/6967.js +0 -1
  177. package/dist/src/760.js +0 -1
  178. package/dist/src/7871.js +0 -1
  179. package/dist/src/8184.js +0 -1
  180. package/dist/src/8225.js +0 -1
  181. package/dist/src/8391.js +0 -1
  182. package/dist/src/8404.js +0 -2
  183. package/dist/src/8414.js +0 -2
  184. package/dist/src/8875.js +0 -4
  185. package/dist/src/893.js +0 -1
  186. package/dist/src/8936.js +0 -1
  187. package/dist/src/9414.js +0 -1
  188. package/dist/src/9581.js +0 -22
  189. package/dist/src/9612.js +0 -1
  190. package/dist/src/9624.js +0 -1
  191. package/dist/src/9722.js +0 -39
  192. package/dist/src/devices~1.js +0 -1
  193. package/dist/src/devices~2.js +0 -1
  194. package/dist/src/input-actions~1.js +0 -1
  195. package/dist/src/rslib-runtime.js +0 -1
  196. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +0 -0
  197. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj +0 -0
  198. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
  199. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcshareddata/xcschemes/AgentDeviceRunner.xcscheme +0 -0
  200. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +0 -0
  201. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests.entitlements +0 -0
  202. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +0 -0
  203. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +0 -0
  204. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.h +0 -0
  205. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.m +0 -0
  206. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +0 -0
  207. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +0 -0
  208. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +0 -0
  209. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Environment.swift +0 -0
  210. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift +0 -0
  211. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +0 -0
  212. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +0 -0
  213. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift +0 -0
  214. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +0 -0
  215. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TvRemote.swift +0 -0
  216. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests.xctestplan +0 -0
  217. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-overlay.swift +0 -0
  218. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-resize.swift +0 -0
  219. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-trim.swift +0 -0
@@ -0,0 +1,172 @@
1
+ import XCTest
2
+
3
+ extension RunnerTests {
4
+ static let navigationBackKeywords = ["back", "close", "cancel"]
5
+ static let navigationFallbackVerificationDelay: TimeInterval = 0.25
6
+
7
+ func tapInAppBackControl(app: XCUIApplication) -> Bool {
8
+ #if os(macOS)
9
+ if let back = macOSNavigationBackElement(app: app) {
10
+ tapElementCenter(app: app, element: back)
11
+ return true
12
+ }
13
+ return false
14
+ #elseif os(tvOS)
15
+ _ = pressTvRemote(.menu)
16
+ return true
17
+ #else
18
+ let buttons = app.navigationBars.buttons.allElementsBoundByIndex
19
+ if let back = buttons.first(where: { $0.isHittable }) {
20
+ back.tap()
21
+ return true
22
+ }
23
+ if isSnapshotXCTestChannelPenalized(bundleId: currentBundleId) {
24
+ NSLog("AGENT_DEVICE_RUNNER_IN_APP_BACK_SKIPPED_XCTEST_ENUMERATION bundle=%@", currentBundleId ?? "")
25
+ } else if let back = topNavigationBackElement(app: app) {
26
+ tapElementCenter(app: app, element: back)
27
+ return true
28
+ }
29
+ return tapTopLeadingNavigationFallback(app: app)
30
+ #endif
31
+ }
32
+
33
+ private func tapElementCenter(app: XCUIApplication, element: XCUIElement) {
34
+ let frame = element.frame
35
+ if !frame.isEmpty {
36
+ _ = tapAt(app: app, x: frame.midX, y: frame.midY)
37
+ return
38
+ }
39
+ #if !os(tvOS)
40
+ element.tap()
41
+ #endif
42
+ }
43
+
44
+ private func topNavigationBackElement(app: XCUIApplication) -> XCUIElement? {
45
+ #if os(iOS)
46
+ let frame = onScreenWindowFrame(app: app)
47
+ let candidates = app.buttons.matching(Self.navigationBackPredicate()).allElementsBoundByIndex.compactMap {
48
+ element -> (XCUIElement, Int)? in
49
+ guard element.exists, element.isHittable else { return nil }
50
+ guard Self.isTopNavigationControlFrame(element.frame, in: frame) else { return nil }
51
+ guard let rank = Self.navigationBackControlRank(
52
+ label: element.label,
53
+ identifier: element.identifier
54
+ ) else {
55
+ return nil
56
+ }
57
+ return (element, rank)
58
+ }
59
+ return candidates.sorted { lhs, rhs in
60
+ if lhs.1 != rhs.1 { return lhs.1 < rhs.1 }
61
+ let leftFrame = lhs.0.frame
62
+ let rightFrame = rhs.0.frame
63
+ if leftFrame.minY != rightFrame.minY { return leftFrame.minY < rightFrame.minY }
64
+ return leftFrame.minX < rightFrame.minX
65
+ }.first?.0
66
+ #else
67
+ return nil
68
+ #endif
69
+ }
70
+
71
+ static func navigationBackPredicate() -> NSPredicate {
72
+ let clauses = navigationBackKeywords.flatMap { _ in
73
+ ["label CONTAINS[c] %@", "identifier CONTAINS[c] %@"]
74
+ }.joined(separator: " OR ")
75
+ let arguments = navigationBackKeywords.flatMap { [$0, $0] }
76
+ return NSPredicate(format: clauses, argumentArray: arguments)
77
+ }
78
+
79
+ static func navigationBackControlRank(label: String, identifier: String) -> Int? {
80
+ let text = "\(label) \(identifier)".lowercased()
81
+ return navigationBackKeywords.firstIndex { text.contains($0) }
82
+ }
83
+
84
+ static func isTopNavigationControlFrame(_ candidate: CGRect, in window: CGRect) -> Bool {
85
+ guard
86
+ candidate.width.isFinite,
87
+ candidate.height.isFinite,
88
+ window.width.isFinite,
89
+ window.height.isFinite,
90
+ candidate.width > 0,
91
+ candidate.height > 0,
92
+ window.width > 0,
93
+ window.height > 0
94
+ else {
95
+ return false
96
+ }
97
+ // Accept the compact navigation/search header band without matching deep content controls.
98
+ let maxY = window.minY + min(max(window.height * 0.22, 96), 180)
99
+ return candidate.midY >= window.minY && candidate.midY <= maxY
100
+ }
101
+
102
+ static func topLeadingNavigationFallbackPoint(in frame: CGRect) -> CGPoint? {
103
+ guard frame.width.isFinite, frame.height.isFinite, frame.width > 0, frame.height > 0 else {
104
+ return nil
105
+ }
106
+ // Aim at the standard leading navigation slot, bounded for compact and tablet widths.
107
+ let xOffset = min(max(frame.width * 0.08, 28), 44)
108
+ // Sit below the status/dynamic-island region and inside common custom RN search headers.
109
+ let yOffset = min(max(frame.height * 0.155, 56), 132)
110
+ return CGPoint(x: frame.minX + xOffset, y: frame.minY + yOffset)
111
+ }
112
+
113
+ private func tapTopLeadingNavigationFallback(app: XCUIApplication) -> Bool {
114
+ #if os(iOS)
115
+ let frame = onScreenWindowFrame(app: app)
116
+ guard let point = Self.topLeadingNavigationFallbackPoint(in: frame) else {
117
+ return false
118
+ }
119
+ let before = captureNavigationFallbackVisualState()
120
+ let context = synthesizedCoordinateContext(
121
+ policy: synthesizedGesturePolicy(.coordinateTap)
122
+ )?.withReferenceFrame(frame)
123
+ let synthesized = performGesture(app, idleTimeout: false) {
124
+ synthesizedTapAt(app: app, x: point.x, y: point.y, context: context)
125
+ }
126
+ if case .performed = synthesized.outcome {
127
+ return didNavigationFallbackChangeVisualState(before: before)
128
+ }
129
+ let fallback = performGesture(app) {
130
+ tapAt(app: app, x: point.x, y: point.y)
131
+ }
132
+ if case .performed = fallback.outcome {
133
+ return didNavigationFallbackChangeVisualState(before: before)
134
+ }
135
+ #endif
136
+ return false
137
+ }
138
+
139
+ private func captureNavigationFallbackVisualState() -> Data? {
140
+ #if os(iOS)
141
+ runnerPngData(for: XCUIScreen.main.screenshot().image)
142
+ #else
143
+ return nil
144
+ #endif
145
+ }
146
+
147
+ private func didNavigationFallbackChangeVisualState(before: Data?) -> Bool {
148
+ sleepFor(Self.navigationFallbackVerificationDelay)
149
+ let after = captureNavigationFallbackVisualState()
150
+ let changed = Self.didNavigationFallbackChangeVisualState(before: before, after: after)
151
+ if !changed {
152
+ NSLog("AGENT_DEVICE_RUNNER_IN_APP_BACK_FALLBACK_NO_STATE_CHANGE")
153
+ }
154
+ return changed
155
+ }
156
+
157
+ static func didNavigationFallbackChangeVisualState(before: Data?, after: Data?) -> Bool {
158
+ guard let before, let after else { return false }
159
+ return before != after
160
+ }
161
+
162
+ private func macOSNavigationBackElement(app: XCUIApplication) -> XCUIElement? {
163
+ let predicate = NSPredicate(
164
+ format: "identifier == %@ OR label == %@",
165
+ "go back",
166
+ "Back"
167
+ )
168
+ let element = app.descendants(matching: .any).matching(predicate).firstMatch
169
+ return element.exists ? element : nil
170
+ }
171
+
172
+ }
@@ -0,0 +1,64 @@
1
+ import XCTest
2
+
3
+ // Swift port of buildScrollGesturePlan from src/core/scroll-gesture.ts.
4
+ //
5
+ // This is a deliberate two-place invariant: the daemon keeps the TS implementation (for Android,
6
+ // recording, and reported-pixels), and the runner places the gesture with this Swift copy. The
7
+ // parity test vectors at the bottom of this file mirror src/core/__tests__/scroll-gesture.test.ts —
8
+ // if you change the math in either language, update the other and both vector sets.
9
+ //
10
+ // All inputs here are positive (reference dims, travel, center), so Swift's `.rounded()`
11
+ // (half away from zero) matches JS `Math.round` (half up) on every value computed below.
12
+
13
+ struct RunnerScrollGesturePlan {
14
+ let x1: Double
15
+ let y1: Double
16
+ let x2: Double
17
+ let y2: Double
18
+ let travelPixels: Double
19
+ }
20
+
21
+ private let runnerDefaultScrollAmount = 0.6
22
+ private let runnerDefaultEdgePaddingFraction = 0.05
23
+
24
+ func runnerScrollGesturePlan(
25
+ direction: String,
26
+ amount: Double?,
27
+ pixels: Double?,
28
+ referenceWidth: Double,
29
+ referenceHeight: Double
30
+ ) -> RunnerScrollGesturePlan? {
31
+ // Mirror the TS INVALID_ARGS contract: non-positive or non-finite amount/pixels are rejected
32
+ // rather than clamped into a journaled 1px scroll. The daemon validates before sending, so
33
+ // this only triggers for non-daemon wire clients.
34
+ if let amount, !(amount.isFinite && amount > 0) { return nil }
35
+ if let pixels, !(pixels.isFinite && pixels > 0) { return nil }
36
+ let axisLength = (direction == "up" || direction == "down") ? referenceHeight : referenceWidth
37
+ let requestedAmount = amount ?? runnerDefaultScrollAmount
38
+ let requestedPixels: Double =
39
+ pixels.map { max(1, $0.rounded()) } ?? (axisLength * requestedAmount).rounded()
40
+ let edgePadding = max(1, (axisLength * runnerDefaultEdgePaddingFraction).rounded())
41
+ let maxTravelPixels = max(1, axisLength - edgePadding * 2)
42
+ let travelPixels = max(1, min(requestedPixels, maxTravelPixels))
43
+ let halfTravel = (travelPixels / 2).rounded()
44
+ let centerX = (referenceWidth / 2).rounded()
45
+ let centerY = (referenceHeight / 2).rounded()
46
+
47
+ func plan(_ x1: Double, _ y1: Double, _ x2: Double, _ y2: Double) -> RunnerScrollGesturePlan {
48
+ RunnerScrollGesturePlan(x1: x1, y1: y1, x2: x2, y2: y2, travelPixels: travelPixels)
49
+ }
50
+
51
+ switch direction {
52
+ case "up":
53
+ return plan(centerX, centerY - halfTravel, centerX, centerY + halfTravel)
54
+ case "down":
55
+ return plan(centerX, centerY + halfTravel, centerX, centerY - halfTravel)
56
+ case "left":
57
+ return plan(centerX - halfTravel, centerY, centerX + halfTravel, centerY)
58
+ case "right":
59
+ return plan(centerX + halfTravel, centerY, centerX - halfTravel, centerY)
60
+ default:
61
+ return nil
62
+ }
63
+ }
64
+
@@ -40,8 +40,10 @@ extension RunnerTests {
40
40
  ? resolvedTouchVisualizationFrame(app: activeApp, x: firstStep.x!, y: firstStep.y!)
41
41
  : nil
42
42
 
43
+ let synthesizedContext = synthesizedSequenceCoordinateContext(steps: steps)
44
+
43
45
  let execution = assembleSequenceExecution(steps: steps) { _, step in
44
- performSequenceStep(step, activeApp: activeApp)
46
+ performSequenceStep(step, activeApp: activeApp, synthesizedContext: synthesizedContext)
45
47
  }
46
48
  return sequenceResponse(execution: execution, touchFrame: firstFrame)
47
49
  }
@@ -141,7 +143,8 @@ extension RunnerTests {
141
143
 
142
144
  private func performSequenceStep(
143
145
  _ step: SequenceStep,
144
- activeApp: XCUIApplication
146
+ activeApp: XCUIApplication,
147
+ synthesizedContext: SynthesizedCoordinateContext? = nil
145
148
  ) -> SequenceStepOutcome {
146
149
  let x = step.x ?? 0
147
150
  let y = step.y ?? 0
@@ -149,10 +152,26 @@ extension RunnerTests {
149
152
  // a tapAt fallback when synthesis is unsupported), so fusing a jittered tap series does not
150
153
  // change the touch mechanism for these inputs.
151
154
  if step.kind == "tap", step.synthesized == true {
155
+ let policyKind = SynthesizedGesturePolicyKind.synthesizedDrag
156
+ #if os(iOS)
157
+ guard let synthesizedContext else {
158
+ let nowMs = ProcessInfo.processInfo.systemUptime * 1000
159
+ logSynthesizedGesturePolicyDecision(kind: policyKind, context: nil, fallbackAttempted: false)
160
+ return SequenceStepOutcome(
161
+ outcome: .unsupported(
162
+ message: "synthesized coordinate tap could not resolve a finite coordinate frame",
163
+ hint: "Retry after the app is foregrounded, or use a plain screenshot to choose coordinates."
164
+ ),
165
+ gestureStartUptimeMs: nowMs,
166
+ gestureEndUptimeMs: nowMs
167
+ )
168
+ }
169
+ #endif
152
170
  let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
153
- synthesizedTapAt(app: activeApp, x: x, y: y)
171
+ synthesizedTapAt(app: activeApp, x: x, y: y, context: synthesizedContext)
154
172
  }
155
173
  if case .performed = outcome {
174
+ logSynthesizedGesturePolicyDecision(kind: policyKind, context: synthesizedContext, fallbackAttempted: false)
156
175
  if let pauseMs = step.pauseMs, pauseMs > 0 {
157
176
  sleepFor(min(max(pauseMs, 0), 10000) / 1000.0)
158
177
  }
@@ -162,11 +181,50 @@ extension RunnerTests {
162
181
  gestureEndUptimeMs: timing.gestureEndUptimeMs
163
182
  )
164
183
  }
184
+ #if os(iOS)
185
+ guard synthesizedContext.allowsXCTestCoordinateFallback else {
186
+ logSynthesizedGesturePolicyDecision(kind: policyKind, context: synthesizedContext, fallbackAttempted: false)
187
+ return SequenceStepOutcome(
188
+ outcome: outcome,
189
+ gestureStartUptimeMs: timing.gestureStartUptimeMs,
190
+ gestureEndUptimeMs: timing.gestureEndUptimeMs
191
+ )
192
+ }
193
+ logSynthesizedGesturePolicyDecision(kind: policyKind, context: synthesizedContext, fallbackAttempted: true)
194
+ #endif
165
195
  // Synthesis unsupported (e.g. macOS) — fall through to the drag-based tapAt below.
166
196
  }
167
197
  if step.kind == "drag", step.synthesized == true {
168
- let dragPoints = keyboardAvoidingDragPoints(
198
+ let policyKind = SynthesizedGesturePolicyKind.synthesizedDrag
199
+ let dragPoints: DragPoints
200
+ let dragContext: SynthesizedCoordinateContext?
201
+ #if os(iOS)
202
+ guard let dragPlan = axFreeSynthesizedDragPlan(
203
+ app: activeApp,
204
+ x: x,
205
+ y: y,
206
+ x2: step.x2 ?? x,
207
+ y2: step.y2 ?? y,
208
+ context: synthesizedContext
209
+ ) else {
210
+ let nowMs = ProcessInfo.processInfo.systemUptime * 1000
211
+ logSynthesizedGesturePolicyDecision(kind: policyKind, context: synthesizedContext, fallbackAttempted: false)
212
+ return SequenceStepOutcome(
213
+ outcome: .unsupported(
214
+ message: "synthesized coordinate drag could not resolve a finite coordinate frame",
215
+ hint: "Retry after the app is foregrounded, or use a plain screenshot to choose coordinates."
216
+ ),
217
+ gestureStartUptimeMs: nowMs,
218
+ gestureEndUptimeMs: nowMs
219
+ )
220
+ }
221
+ dragPoints = dragPlan.points
222
+ dragContext = dragPlan.context
223
+ #else
224
+ dragPoints = keyboardAvoidingDragPoints(
169
225
  app: activeApp, x: x, y: y, x2: step.x2 ?? x, y2: step.y2 ?? y)
226
+ dragContext = nil
227
+ #endif
170
228
  let durationMs = min(max(step.durationMs ?? 250, 16), 10000)
171
229
  let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
172
230
  synthesizedDragAt(
@@ -175,10 +233,12 @@ extension RunnerTests {
175
233
  y: dragPoints.y,
176
234
  x2: dragPoints.x2,
177
235
  y2: dragPoints.y2,
178
- durationMs: durationMs
236
+ durationMs: durationMs,
237
+ context: dragContext
179
238
  )
180
239
  }
181
240
  if case .performed = outcome {
241
+ logSynthesizedGesturePolicyDecision(kind: policyKind, context: dragContext, fallbackAttempted: false)
182
242
  if let pauseMs = step.pauseMs, pauseMs > 0 {
183
243
  sleepFor(min(max(pauseMs, 0), 10000) / 1000.0)
184
244
  }
@@ -188,6 +248,17 @@ extension RunnerTests {
188
248
  gestureEndUptimeMs: timing.gestureEndUptimeMs
189
249
  )
190
250
  }
251
+ #if os(iOS)
252
+ guard dragContext?.allowsXCTestCoordinateFallback == true else {
253
+ logSynthesizedGesturePolicyDecision(kind: policyKind, context: dragContext, fallbackAttempted: false)
254
+ return SequenceStepOutcome(
255
+ outcome: outcome,
256
+ gestureStartUptimeMs: timing.gestureStartUptimeMs,
257
+ gestureEndUptimeMs: timing.gestureEndUptimeMs
258
+ )
259
+ }
260
+ #endif
261
+ logSynthesizedGesturePolicyDecision(kind: policyKind, context: dragContext, fallbackAttempted: true)
191
262
  let fallbackHoldDuration = synthesizedSwipeFallbackHoldDuration(durationMs: step.durationMs ?? 250)
192
263
  let (fallbackTiming, fallbackOutcome) = performGesture(activeApp) {
193
264
  dragAt(
@@ -273,180 +344,3 @@ extension RunnerTests {
273
344
  }
274
345
  }
275
346
 
276
- #if AGENT_DEVICE_RUNNER_UNIT_TESTS
277
- // MARK: - In-bundle unit tests (device-free)
278
-
279
- extension RunnerTests {
280
- func testSequenceDecodesStepsFromWire() throws {
281
- let json = """
282
- {"command":"sequence","commandId":"seq-1","steps":[
283
- {"kind":"tap","x":100,"y":200},
284
- {"kind":"doubleTap","x":101,"y":200},
285
- {"kind":"longPress","x":102,"y":200,"durationMs":300},
286
- {"kind":"drag","x":10,"y":600,"x2":10,"y2":200,"durationMs":250,"pauseMs":50}
287
- ]}
288
- """
289
- let command = try JSONDecoder().decode(Command.self, from: Data(json.utf8))
290
- XCTAssertEqual(command.command, .sequence)
291
- XCTAssertEqual(command.steps?.count, 4)
292
- XCTAssertEqual(command.steps?[0].kind, "tap")
293
- XCTAssertEqual(command.steps?[1].kind, "doubleTap")
294
- XCTAssertEqual(command.steps?[3].x2, 10)
295
- XCTAssertEqual(command.steps?[3].pauseMs, 50)
296
- }
297
-
298
- func testSequenceAcceptsDoubleTapKind() {
299
- // A doubleTap step missing coords must fail on the coords check, not the kind allowlist —
300
- // proving "doubleTap" passes validateSequenceStep without needing a device to execute on.
301
- let response = executeSequenceForTest(steps: [
302
- sequenceStep(kind: "doubleTap", x: nil)
303
- ])
304
- XCTAssertEqual(response.ok, false)
305
- XCTAssertEqual(response.error?.code, "INVALID_ARGS")
306
- XCTAssertTrue(response.error?.message.contains("requires finite x and y") ?? false)
307
- XCTAssertFalse(response.error?.message.contains("unsupported kind") ?? true)
308
- }
309
-
310
- func testSequenceRejectsUnknownKind() throws {
311
- let response = executeSequenceForTest(steps: [
312
- sequenceStep(kind: "tap", x: 1, y: 2),
313
- sequenceStep(kind: "pinch", x: 3, y: 4),
314
- ])
315
- XCTAssertEqual(response.ok, false)
316
- XCTAssertEqual(response.error?.code, "INVALID_ARGS")
317
- XCTAssertTrue(response.error?.message.contains("step 1") ?? false)
318
- XCTAssertTrue(response.error?.message.contains("pinch") ?? false)
319
- }
320
-
321
- func testSequenceRejectsEmpty() {
322
- let response = executeSequenceForTest(steps: [])
323
- XCTAssertEqual(response.ok, false)
324
- XCTAssertEqual(response.error?.code, "INVALID_ARGS")
325
- }
326
-
327
- func testSequenceRejectsTooManySteps() {
328
- let steps = (0..<21).map { _ in sequenceStep(kind: "tap", x: 1, y: 2) }
329
- let response = executeSequenceForTest(steps: steps)
330
- XCTAssertEqual(response.ok, false)
331
- XCTAssertEqual(response.error?.code, "INVALID_ARGS")
332
- XCTAssertTrue(response.error?.message.contains("at most 20") ?? false)
333
- }
334
-
335
- func testSequenceRejectsDragMissingSecondPoint() {
336
- let response = executeSequenceForTest(steps: [
337
- sequenceStep(kind: "tap", x: 1, y: 2),
338
- sequenceStep(kind: "drag", x: 3, y: 4),
339
- ])
340
- XCTAssertEqual(response.ok, false)
341
- XCTAssertEqual(response.error?.code, "INVALID_ARGS")
342
- XCTAssertTrue(response.error?.message.contains("step 1") ?? false)
343
- }
344
-
345
- func testAssembleSequencePreservesOrderOnSuccess() {
346
- let steps = [
347
- sequenceStep(kind: "tap", x: 1, y: 1),
348
- sequenceStep(kind: "longPress", x: 2, y: 2),
349
- sequenceStep(kind: "tap", x: 3, y: 3),
350
- ]
351
- var calls: [Int] = []
352
- let execution = assembleSequenceExecution(steps: steps) { index, _ in
353
- calls.append(index)
354
- return SequenceStepOutcome(
355
- outcome: .performed,
356
- gestureStartUptimeMs: Double(index * 10),
357
- gestureEndUptimeMs: Double(index * 10 + 5)
358
- )
359
- }
360
- XCTAssertEqual(calls, [0, 1, 2])
361
- XCTAssertEqual(execution.completedSteps, 3)
362
- XCTAssertNil(execution.failedStepIndex)
363
- XCTAssertEqual(execution.results.map { $0.kind }, ["tap", "longPress", "tap"])
364
- XCTAssertEqual(execution.gestureStartUptimeMs, 0)
365
- XCTAssertEqual(execution.gestureEndUptimeMs, 25)
366
- }
367
-
368
- func testAssembleSequenceStopsAtFirstFailure() {
369
- let steps = [
370
- sequenceStep(kind: "tap", x: 1, y: 1),
371
- sequenceStep(kind: "drag", x: 2, y: 2),
372
- sequenceStep(kind: "tap", x: 3, y: 3),
373
- ]
374
- var calls: [Int] = []
375
- let execution = assembleSequenceExecution(steps: steps) { index, _ in
376
- calls.append(index)
377
- if index == 1 {
378
- return SequenceStepOutcome(
379
- outcome: .unsupported(message: "drag unsupported", hint: nil),
380
- gestureStartUptimeMs: 10,
381
- gestureEndUptimeMs: 15
382
- )
383
- }
384
- return SequenceStepOutcome(outcome: .performed, gestureStartUptimeMs: 0, gestureEndUptimeMs: 5)
385
- }
386
- // Step 2 is never invoked.
387
- XCTAssertEqual(calls, [0, 1])
388
- XCTAssertEqual(execution.completedSteps, 1)
389
- XCTAssertEqual(execution.failedStepIndex, 1)
390
- // results.count == completedSteps + 1 (the failed step).
391
- XCTAssertEqual(execution.results.count, 2)
392
- XCTAssertEqual(execution.results[1].ok, false)
393
- XCTAssertEqual(execution.results[1].errorCode, "UNSUPPORTED_OPERATION")
394
- XCTAssertEqual(execution.results[1].errorMessage, "drag unsupported")
395
- }
396
-
397
- func testSequenceWorstCaseResponseStaysUnderJournalCap() throws {
398
- let longMessage = String(repeating: "e", count: 200)
399
- let results = (0..<20).map { index in
400
- SequenceStepResult(
401
- ok: index < 19,
402
- kind: "drag",
403
- errorCode: index < 19 ? nil : "UNSUPPORTED_OPERATION",
404
- errorMessage: index < 19 ? nil : longMessage,
405
- gestureStartUptimeMs: 123456.789,
406
- gestureEndUptimeMs: 123466.789
407
- )
408
- }
409
- let response = Response(
410
- ok: true,
411
- data: DataPayload(
412
- message: "sequence",
413
- completedSteps: 19,
414
- failedStepIndex: 19,
415
- sequenceResults: results
416
- )
417
- )
418
- let encoded = try JSONEncoder().encode(response)
419
- XCTAssertLessThan(encoded.count, 16 * 1024)
420
- }
421
-
422
- private func sequenceStep(
423
- kind: String,
424
- x: Double?,
425
- y: Double? = nil,
426
- x2: Double? = nil,
427
- y2: Double? = nil
428
- ) -> SequenceStep {
429
- SequenceStep(
430
- kind: kind, x: x, y: y, x2: x2, y2: y2, durationMs: nil, pauseMs: nil, synthesized: nil)
431
- }
432
-
433
- /// Validation runs before any executor call, so the INVALID_ARGS paths are exercised without
434
- /// reaching the device executor (which is never invoked when validation rejects).
435
- private func executeSequenceForTest(steps: [SequenceStep]) -> Response {
436
- let command = makeSequenceCommand(steps: steps)
437
- return executeSequence(command: command, activeApp: app)
438
- }
439
-
440
- /// Build a sequence Command via JSON so the test does not depend on the memberwise init's
441
- /// parameter order.
442
- private func makeSequenceCommand(steps: [SequenceStep]) -> Command {
443
- struct SequenceCommandFixture: Encodable {
444
- let command = "sequence"
445
- let commandId = "seq-test"
446
- let steps: [SequenceStep]
447
- }
448
- let data = try! JSONEncoder().encode(SequenceCommandFixture(steps: steps))
449
- return try! JSONDecoder().decode(Command.self, from: data)
450
- }
451
- }
452
- #endif