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,155 @@
1
+ import Foundation
2
+ import XCTest
3
+
4
+ enum RunnerCommandLifecycleState: String {
5
+ case notAccepted
6
+ case accepted
7
+ case started
8
+ case completed
9
+ case failed
10
+ }
11
+
12
+ struct RunnerCommandJournalEntry {
13
+ let commandId: String
14
+ let command: String
15
+ var state: RunnerCommandLifecycleState
16
+ var responseOk: Bool?
17
+ var responseJson: String?
18
+ var error: ErrorPayload?
19
+ }
20
+
21
+ final class RunnerCommandJournal {
22
+ private let lock = NSLock()
23
+ private let maxEntries = 64
24
+ private let maxResponseJsonBytes = 16 * 1024
25
+ private var entries: [String: RunnerCommandJournalEntry] = [:]
26
+ private var order: [String] = []
27
+
28
+ func accept(command: Command) {
29
+ guard let commandId = normalizedCommandId(command.commandId) else { return }
30
+ lock.lock()
31
+ defer { lock.unlock() }
32
+ entries[commandId] = RunnerCommandJournalEntry(
33
+ commandId: commandId,
34
+ command: command.command.rawValue,
35
+ state: .accepted,
36
+ responseOk: nil,
37
+ responseJson: nil,
38
+ error: nil
39
+ )
40
+ order.removeAll { $0 == commandId }
41
+ order.append(commandId)
42
+ pruneIfNeeded()
43
+ }
44
+
45
+ func start(command: Command) {
46
+ update(command: command, state: .started, responseOk: nil, responseJson: nil, error: nil)
47
+ }
48
+
49
+ func finish(command: Command, response: Response) {
50
+ update(
51
+ command: command,
52
+ state: response.ok ? .completed : .failed,
53
+ responseOk: response.ok,
54
+ responseJson: encodeResponseJson(command: command, response: response),
55
+ error: response.error
56
+ )
57
+ }
58
+
59
+ func fail(command: Command, error: Error) {
60
+ update(
61
+ command: command,
62
+ state: .failed,
63
+ responseOk: nil,
64
+ responseJson: nil,
65
+ error: ErrorPayload(message: error.localizedDescription)
66
+ )
67
+ }
68
+
69
+ func status(commandId: String) -> DataPayload {
70
+ guard let normalized = normalizedCommandId(commandId) else {
71
+ return DataPayload(lifecycleState: RunnerCommandLifecycleState.notAccepted.rawValue)
72
+ }
73
+ lock.lock()
74
+ let entry = entries[normalized]
75
+ lock.unlock()
76
+ guard let entry else {
77
+ return DataPayload(
78
+ commandId: normalized,
79
+ lifecycleState: RunnerCommandLifecycleState.notAccepted.rawValue
80
+ )
81
+ }
82
+ return DataPayload(
83
+ commandId: entry.commandId,
84
+ lifecycleState: entry.state.rawValue,
85
+ lifecycleCommand: entry.command,
86
+ lifecycleResponseOk: entry.responseOk,
87
+ lifecycleResponseJson: entry.responseJson,
88
+ lifecycleErrorCode: entry.error?.code,
89
+ lifecycleErrorMessage: entry.error?.message,
90
+ lifecycleErrorHint: entry.error?.hint
91
+ )
92
+ }
93
+
94
+ private func update(
95
+ command: Command,
96
+ state: RunnerCommandLifecycleState,
97
+ responseOk: Bool?,
98
+ responseJson: String?,
99
+ error: ErrorPayload?
100
+ ) {
101
+ guard let commandId = normalizedCommandId(command.commandId) else { return }
102
+ lock.lock()
103
+ defer { lock.unlock() }
104
+ var entry = entries[commandId] ?? RunnerCommandJournalEntry(
105
+ commandId: commandId,
106
+ command: command.command.rawValue,
107
+ state: .accepted,
108
+ responseOk: nil,
109
+ responseJson: nil,
110
+ error: nil
111
+ )
112
+ entry.state = state
113
+ entry.responseOk = responseOk
114
+ entry.responseJson = responseJson
115
+ entry.error = error
116
+ entries[commandId] = entry
117
+ order.removeAll { $0 == commandId }
118
+ order.append(commandId)
119
+ pruneIfNeeded()
120
+ }
121
+
122
+ private func pruneIfNeeded() {
123
+ while order.count > maxEntries {
124
+ let removed = order.removeFirst()
125
+ entries.removeValue(forKey: removed)
126
+ }
127
+ }
128
+
129
+ private func normalizedCommandId(_ value: String?) -> String? {
130
+ guard let value else { return nil }
131
+ let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
132
+ return trimmed.isEmpty ? nil : trimmed
133
+ }
134
+
135
+ private func encodeResponseJson(command: Command, response: Response) -> String? {
136
+ guard shouldRetainResponseJson(command: command) else { return nil }
137
+ guard let data = try? JSONEncoder().encode(response) else { return nil }
138
+ guard data.count <= maxResponseJsonBytes else { return nil }
139
+ return String(data: data, encoding: .utf8)
140
+ }
141
+
142
+ private func shouldRetainResponseJson(command: Command) -> Bool {
143
+ switch command.command {
144
+ case .snapshot, .screenshot:
145
+ return false
146
+ case .tap, .mouseClick, .longPress, .drag,
147
+ .remotePress, .type, .swipe, .scroll, .desktopScroll, .findText, .querySelector, .readText, .back,
148
+ .backInApp, .backSystem, .home, .rotate, .appSwitcher, .keyboardDismiss, .keyboardReturn,
149
+ .alert, .pinch, .sequence, .rotateGesture, .transformGesture, .recordStart, .recordStop,
150
+ .status, .uptime, .shutdown:
151
+ return true
152
+ }
153
+ }
154
+ }
155
+
@@ -0,0 +1,67 @@
1
+ import XCTest
2
+
3
+ struct FlatSnapshotFilterNode {
4
+ let isRoot: Bool
5
+ let label: String
6
+ let identifier: String
7
+ let valueText: String?
8
+ let visible: Bool
9
+
10
+ func matchesScope(_ scope: String) -> Bool {
11
+ let haystack = [label, identifier, valueText ?? ""].joined(separator: "\n")
12
+ return haystack.localizedCaseInsensitiveContains(scope)
13
+ }
14
+ }
15
+
16
+ struct FlatSnapshotFilterDecision {
17
+ let include: Bool
18
+ let insideMatchedScope: Bool
19
+ }
20
+
21
+ extension RunnerTests {
22
+ func flatSnapshotFilterDecision(
23
+ _ node: FlatSnapshotFilterNode,
24
+ options: SnapshotOptions,
25
+ insideMatchedScope: Bool
26
+ ) -> FlatSnapshotFilterDecision {
27
+ let scope = options.scope?.trimmingCharacters(in: .whitespacesAndNewlines)
28
+ let scopeActive = scope?.isEmpty == false
29
+ let matchesScope: Bool
30
+ if scopeActive, let scope {
31
+ matchesScope = node.matchesScope(scope)
32
+ } else {
33
+ matchesScope = false
34
+ }
35
+ let nowInsideScope = insideMatchedScope || matchesScope
36
+
37
+ let include: Bool
38
+ if node.isRoot {
39
+ include = true
40
+ } else if scopeActive && !nowInsideScope {
41
+ include = false
42
+ } else if options.interactiveOnly && !node.visible {
43
+ include = false
44
+ } else {
45
+ include = true
46
+ }
47
+
48
+ return FlatSnapshotFilterDecision(include: include, insideMatchedScope: nowInsideScope)
49
+ }
50
+
51
+ func privateAXInteractiveCandidate(rawElementType: Int) -> Bool {
52
+ guard let type = flatSnapshotElementType(rawElementType: rawElementType) else {
53
+ return false
54
+ }
55
+ return interactiveTypes.contains(type) || Self.scrollContainerTypes.contains(type)
56
+ }
57
+
58
+ func flatSnapshotElementType(rawElementType: Int) -> XCUIElement.ElementType? {
59
+ guard let raw = UInt(exactly: rawElementType),
60
+ let type = XCUIElement.ElementType(rawValue: raw)
61
+ else {
62
+ return nil
63
+ }
64
+ return type
65
+ }
66
+
67
+ }
@@ -36,34 +36,21 @@ extension RunnerTests {
36
36
  let y2: Double
37
37
  }
38
38
 
39
+ struct SynthesizedDragPlan {
40
+ let points: DragPoints
41
+ let context: SynthesizedCoordinateContext
42
+
43
+ var referenceFrame: CGRect {
44
+ context.referenceFrame
45
+ }
46
+ }
47
+
39
48
  struct SelectorElementMatch {
40
49
  let element: XCUIElement?
41
50
  let isAmbiguous: Bool
42
51
  let usedNonHittableFallback: Bool
43
52
  }
44
53
 
45
- // MARK: - Navigation Gestures
46
-
47
- func tapInAppBackControl(app: XCUIApplication) -> Bool {
48
- #if os(macOS)
49
- if let back = macOSNavigationBackElement(app: app) {
50
- tapElementCenter(app: app, element: back)
51
- return true
52
- }
53
- return false
54
- #elseif os(tvOS)
55
- _ = pressTvRemote(.menu)
56
- return true
57
- #else
58
- let buttons = app.navigationBars.buttons.allElementsBoundByIndex
59
- if let back = buttons.first(where: { $0.isHittable }) {
60
- back.tap()
61
- return true
62
- }
63
- return false
64
- #endif
65
- }
66
-
67
54
  func performBackGesture(app: XCUIApplication) {
68
55
  if pressTvRemote(.menu) {
69
56
  return
@@ -200,16 +187,32 @@ extension RunnerTests {
200
187
  )
201
188
  }
202
189
 
190
+ // Maestro-compat gate for the non-hittable coordinate fallback: an element
191
+ // with no frame at all cannot be coordinate-tapped, otherwise the decision
192
+ // is the shared TapPointPolicy center-in-frame rule (golden parity table
193
+ // with the TS twin). app.frame is the frame source here — replay taps
194
+ // resolved bounds Maestro-style, so the union frame is intentional.
203
195
  private func hasTappableFrame(app: XCUIApplication, element: XCUIElement) -> Bool {
204
196
  let frame = element.frame
205
197
  if frame.isEmpty {
206
198
  return false
207
199
  }
208
- let appFrame = app.frame
209
- if appFrame.isEmpty {
210
- return true
200
+ return TapPointPolicy.isAllowed(elementFrame: frame, windowFrame: app.frame)
201
+ }
202
+
203
+ // The tappable on-screen viewport. app.frame is unsuitable: it unions
204
+ // transformed subtrees, so a closed drawer at negative x inflates it and
205
+ // out-of-window coordinates still pass containment. Falls back to app.frame
206
+ // when no window frame is readable.
207
+ func onScreenWindowFrame(app: XCUIApplication) -> CGRect {
208
+ let window = app.windows.element(boundBy: 0)
209
+ if window.exists {
210
+ let frame = window.frame
211
+ if !frame.isEmpty {
212
+ return frame
213
+ }
211
214
  }
212
- return appFrame.contains(CGPoint(x: frame.midX, y: frame.midY))
215
+ return app.frame
213
216
  }
214
217
 
215
218
  func queryElement(app: XCUIApplication, selectorKey: String, selectorValue: String) -> Response {
@@ -771,11 +774,24 @@ extension RunnerTests {
771
774
  y: Double,
772
775
  x2: Double,
773
776
  y2: Double,
774
- durationMs: Double
777
+ durationMs: Double,
778
+ context: SynthesizedCoordinateContext? = nil
775
779
  ) -> RunnerInteractionOutcome {
776
780
  #if os(iOS)
781
+ guard x.isFinite, y.isFinite, x2.isFinite, y2.isFinite else {
782
+ return .unsupported(
783
+ message: "synthesized coordinate drag requires finite coordinates",
784
+ hint: "Retry with finite x, y, x2, and y2 values."
785
+ )
786
+ }
777
787
  let orientation = Int(RunnerSynthesizedGesture.interfaceOrientation(forApplication: app))
778
- let frame = app.frame
788
+ guard let context = context ?? synthesizedCoordinateContext(policy: synthesizedGesturePolicy(.synthesizedDrag)) else {
789
+ return .unsupported(
790
+ message: "synthesized coordinate drag could not resolve a finite screen frame",
791
+ hint: "Retry after the app is foregrounded, or use a plain screenshot to choose coordinates."
792
+ )
793
+ }
794
+ let frame = context.referenceFrame
779
795
  let start = nativeSynthesizedPoint(orientedX: x, orientedY: y, in: frame, interfaceOrientation: orientation)
780
796
  let end = nativeSynthesizedPoint(orientedX: x2, orientedY: y2, in: frame, interfaceOrientation: orientation)
781
797
  if let message = RunnerSynthesizedGesture.synthesizeSwipe(
@@ -788,7 +804,7 @@ extension RunnerTests {
788
804
  ) {
789
805
  return .unsupported(
790
806
  message: message,
791
- hint: "Falling back to XCTest coordinate drag may be slower; update Xcode if this persists."
807
+ hint: "Private XCTest event synthesis is required for AX-free coordinate drag on iOS; update Xcode if this persists."
792
808
  )
793
809
  }
794
810
  return .performed
@@ -805,10 +821,28 @@ extension RunnerTests {
805
821
  #endif
806
822
  }
807
823
 
808
- func synthesizedTapAt(app: XCUIApplication, x: Double, y: Double) -> RunnerInteractionOutcome {
824
+ func synthesizedTapAt(
825
+ app: XCUIApplication,
826
+ x: Double,
827
+ y: Double,
828
+ context: SynthesizedCoordinateContext? = nil
829
+ ) -> RunnerInteractionOutcome {
809
830
  #if os(iOS)
831
+ guard x.isFinite, y.isFinite else {
832
+ return .unsupported(
833
+ message: "synthesized coordinate tap requires finite coordinates",
834
+ hint: "Retry with finite x and y values."
835
+ )
836
+ }
810
837
  let orientation = Int(RunnerSynthesizedGesture.interfaceOrientation(forApplication: app))
811
- let point = nativeSynthesizedPoint(orientedX: x, orientedY: y, in: app.frame, interfaceOrientation: orientation)
838
+ guard let context = context ?? synthesizedCoordinateContext(policy: synthesizedGesturePolicy(.coordinateTap)) else {
839
+ return .unsupported(
840
+ message: "synthesized coordinate tap could not resolve a finite screen frame",
841
+ hint: "Retry after the app is foregrounded, or use a plain screenshot to choose coordinates."
842
+ )
843
+ }
844
+ let frame = context.referenceFrame
845
+ let point = nativeSynthesizedPoint(orientedX: x, orientedY: y, in: frame, interfaceOrientation: orientation)
812
846
  if let message = RunnerSynthesizedGesture.synthesizeTap(
813
847
  withApplication: app,
814
848
  x: Double(point.x),
@@ -978,6 +1012,114 @@ extension RunnerTests {
978
1012
  #endif
979
1013
  }
980
1014
 
1015
+ func axFreeSynthesizedDragPlan(
1016
+ app: XCUIApplication,
1017
+ x: Double,
1018
+ y: Double,
1019
+ x2: Double,
1020
+ y2: Double,
1021
+ context: SynthesizedCoordinateContext? = nil
1022
+ ) -> SynthesizedDragPlan? {
1023
+ #if os(iOS)
1024
+ let context = context ?? synthesizedCoordinateContext(policy: synthesizedGesturePolicy(.synthesizedDrag))
1025
+ guard x.isFinite, y.isFinite, x2.isFinite, y2.isFinite,
1026
+ let context
1027
+ else {
1028
+ return nil
1029
+ }
1030
+ let points = keyboardAvoidingSynthesizedDragPoints(
1031
+ app: app,
1032
+ x: x,
1033
+ y: y,
1034
+ x2: x2,
1035
+ y2: y2,
1036
+ context: context
1037
+ )
1038
+ return SynthesizedDragPlan(
1039
+ points: points,
1040
+ context: context
1041
+ )
1042
+ #else
1043
+ return nil
1044
+ #endif
1045
+ }
1046
+
1047
+ func axFreeDragVisualizationFrame(
1048
+ x: Double,
1049
+ y: Double,
1050
+ x2: Double,
1051
+ y2: Double,
1052
+ referenceFrame: CGRect
1053
+ ) -> DragVisualizationFrame {
1054
+ return DragVisualizationFrame(
1055
+ x: x,
1056
+ y: y,
1057
+ x2: x2,
1058
+ y2: y2,
1059
+ referenceWidth: Double(referenceFrame.width),
1060
+ referenceHeight: Double(referenceFrame.height)
1061
+ )
1062
+ }
1063
+
1064
+ func synthesizedCoordinateContext(policy: SynthesizedGesturePolicy) -> SynthesizedCoordinateContext? {
1065
+ #if os(iOS)
1066
+ let health = runnerAccessibilityHealth
1067
+ guard let referenceFrame = synthesizedScreenshotReferenceFrame(
1068
+ screenshotSize: { XCUIScreen.main.screenshot().image.size }
1069
+ ) else {
1070
+ return nil
1071
+ }
1072
+ return SynthesizedCoordinateContext(
1073
+ referenceFrame: referenceFrame,
1074
+ keyboardPolicy: policy.keyboardPolicy,
1075
+ fallbackPolicy: policy.fallbackPolicy,
1076
+ accessibilityHealth: health
1077
+ )
1078
+ #else
1079
+ return nil
1080
+ #endif
1081
+ }
1082
+
1083
+ func synthesizedScreenshotReferenceFrame(screenshotSize: () -> CGSize) -> CGRect? {
1084
+ let screenshotSize = screenshotSize()
1085
+ guard screenshotSize.width.isFinite, screenshotSize.height.isFinite,
1086
+ screenshotSize.width > 0,
1087
+ screenshotSize.height > 0
1088
+ else {
1089
+ return nil
1090
+ }
1091
+ return CGRect(x: 0, y: 0, width: screenshotSize.width, height: screenshotSize.height)
1092
+ }
1093
+
1094
+ func synthesizedFrameAvoidingKeyboardWhenAllowed(
1095
+ app: XCUIApplication,
1096
+ context: SynthesizedCoordinateContext
1097
+ ) -> CGRect {
1098
+ #if os(iOS)
1099
+ guard context.allowsKeyboardProbe else { return context.referenceFrame }
1100
+ return frameAvoidingKeyboard(app: app, frame: context.referenceFrame)
1101
+ #else
1102
+ return context.referenceFrame
1103
+ #endif
1104
+ }
1105
+
1106
+ func keyboardAvoidingSynthesizedDragPoints(
1107
+ app: XCUIApplication,
1108
+ x: Double,
1109
+ y: Double,
1110
+ x2: Double,
1111
+ y2: Double,
1112
+ context: SynthesizedCoordinateContext
1113
+ ) -> DragPoints {
1114
+ #if os(iOS)
1115
+ guard context.allowsKeyboardProbe else {
1116
+ return DragPoints(x: x, y: y, x2: x2, y2: y2)
1117
+ }
1118
+ return keyboardAvoidingDragPoints(app: app, x: x, y: y, x2: x2, y2: y2)
1119
+ #else
1120
+ return DragPoints(x: x, y: y, x2: x2, y2: y2)
1121
+ #endif
1122
+ }
981
1123
 
982
1124
  func swipe(app: XCUIApplication, direction: String) -> DragVisualizationFrame? {
983
1125
  if performTvRemoteSwipeIfAvailable(direction: direction) {
@@ -1211,95 +1353,4 @@ extension RunnerTests {
1211
1353
  }
1212
1354
  #endif
1213
1355
 
1214
- private func tapElementCenter(app: XCUIApplication, element: XCUIElement) {
1215
- let frame = element.frame
1216
- if !frame.isEmpty {
1217
- _ = tapAt(app: app, x: frame.midX, y: frame.midY)
1218
- return
1219
- }
1220
- #if !os(tvOS)
1221
- element.tap()
1222
- #endif
1223
- }
1224
-
1225
- private func macOSNavigationBackElement(app: XCUIApplication) -> XCUIElement? {
1226
- let predicate = NSPredicate(
1227
- format: "identifier == %@ OR label == %@",
1228
- "go back",
1229
- "Back"
1230
- )
1231
- let element = app.descendants(matching: .any).matching(predicate).firstMatch
1232
- return element.exists ? element : nil
1233
- }
1234
-
1235
- #if AGENT_DEVICE_RUNNER_UNIT_TESTS
1236
- // Identity in portrait/unknown, 90° per landscape, 180° upside-down.
1237
- func testNativeSynthesizedPointRotatesByInterfaceOrientation() {
1238
- let portrait = CGRect(x: 0, y: 0, width: 834, height: 1210)
1239
- let landscape = CGRect(x: 0, y: 0, width: 1210, height: 834)
1240
- let offsetLandscape = CGRect(x: 10, y: 20, width: 1210, height: 834)
1241
- // (frame, UIInterfaceOrientation, expected native point) for a tap at (170, 268).
1242
- let cases: [(CGRect, Int, CGPoint)] = [
1243
- (portrait, RunnerInterfaceOrientation.portrait, CGPoint(x: 170, y: 268)),
1244
- (landscape, RunnerInterfaceOrientation.landscapeRight, CGPoint(x: 566, y: 170)),
1245
- (landscape, RunnerInterfaceOrientation.landscapeLeft, CGPoint(x: 268, y: 1040)),
1246
- (portrait, RunnerInterfaceOrientation.portraitUpsideDown, CGPoint(x: 664, y: 942)),
1247
- (portrait, RunnerInterfaceOrientation.unknown, CGPoint(x: 170, y: 268)),
1248
- ]
1249
- for (frame, orientation, expected) in cases {
1250
- XCTAssertEqual(
1251
- nativeSynthesizedPoint(orientedX: 170, orientedY: 268, in: frame, interfaceOrientation: orientation),
1252
- expected,
1253
- "interfaceOrientation \(orientation)"
1254
- )
1255
- }
1256
- XCTAssertEqual(
1257
- nativeSynthesizedPoint(
1258
- orientedX: 180,
1259
- orientedY: 288,
1260
- in: offsetLandscape,
1261
- interfaceOrientation: RunnerInterfaceOrientation.landscapeLeft
1262
- ),
1263
- CGPoint(x: 268, y: 1040),
1264
- "non-zero frame origin is localized before rotation"
1265
- )
1266
- }
1267
-
1268
- func testNativeSynthesizedVectorRotatesByInterfaceOrientation() {
1269
- let cases: [(Int, CGVector)] = [
1270
- (RunnerInterfaceOrientation.portrait, CGVector(dx: 40, dy: -20)),
1271
- (RunnerInterfaceOrientation.landscapeRight, CGVector(dx: 20, dy: 40)),
1272
- (RunnerInterfaceOrientation.landscapeLeft, CGVector(dx: -20, dy: -40)),
1273
- (RunnerInterfaceOrientation.portraitUpsideDown, CGVector(dx: -40, dy: 20)),
1274
- (RunnerInterfaceOrientation.unknown, CGVector(dx: 40, dy: -20)),
1275
- ]
1276
- for (orientation, expected) in cases {
1277
- let vector = nativeSynthesizedVector(orientedDx: 40, orientedDy: -20, interfaceOrientation: orientation)
1278
- XCTAssertEqual(vector.dx, expected.dx, "dx interfaceOrientation \(orientation)")
1279
- XCTAssertEqual(vector.dy, expected.dy, "dy interfaceOrientation \(orientation)")
1280
- }
1281
- }
1282
-
1283
- func testDesktopScrollWheelDeltasMapDirections() throws {
1284
- XCTAssertEqual(try XCTUnwrap(desktopScrollWheelDeltas(direction: "up", pixels: 120)).vertical, 120)
1285
- XCTAssertEqual(try XCTUnwrap(desktopScrollWheelDeltas(direction: "down", pixels: 120)).vertical, -120)
1286
- XCTAssertEqual(try XCTUnwrap(desktopScrollWheelDeltas(direction: "left", pixels: 120)).horizontal, 120)
1287
- XCTAssertEqual(try XCTUnwrap(desktopScrollWheelDeltas(direction: "right", pixels: 120)).horizontal, -120)
1288
- XCTAssertNil(desktopScrollWheelDeltas(direction: "diagonal", pixels: 120))
1289
- }
1290
-
1291
- func testDesktopScrollWheelDeltaEventsHonorDurationAndPreservePixels() throws {
1292
- let events = try XCTUnwrap(desktopScrollWheelDeltaEvents(direction: "down", pixels: 200, durationMs: 50))
1293
- XCTAssertEqual(events.count, 4)
1294
- XCTAssertEqual(events.map(\.vertical).reduce(0, +), -200)
1295
- XCTAssertEqual(events.map(\.horizontal).reduce(0, +), 0)
1296
- XCTAssertEqual(desktopScrollEventIntervalSeconds(durationMs: 50, eventCount: events.count), 0.05 / 3.0)
1297
- }
1298
-
1299
- func testDesktopScrollWheelDeltaEventsKeepInstantScrollSingleEvent() throws {
1300
- let events = try XCTUnwrap(desktopScrollWheelDeltaEvents(direction: "down", pixels: 200, durationMs: 0))
1301
- XCTAssertEqual(events.count, 1)
1302
- XCTAssertEqual(events.first?.vertical, -200)
1303
- }
1304
- #endif
1305
1356
  }