agent-device 0.21.7 → 0.21.9

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 (204) hide show
  1. package/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.7.apk → agent-device-android-ime-helper-0.21.9.apk} +0 -0
  2. package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.9.apk.sha256 +1 -0
  3. package/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.7.manifest.json → agent-device-android-ime-helper-0.21.9.manifest.json} +4 -4
  4. package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.21.7.apk → agent-device-android-snapshot-helper-0.21.9.apk} +0 -0
  5. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.9.apk.sha256 +1 -0
  6. package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.21.7.manifest.json → agent-device-android-snapshot-helper-0.21.9.manifest.json} +6 -6
  7. package/apple/macos-helper/Package.swift +16 -1
  8. package/apple/macos-helper/Sources/AgentDeviceMacOSDeviceHub/DeviceHubPose.swift +47 -0
  9. package/apple/macos-helper/Sources/AgentDeviceMacOSDeviceHub/DeviceHubWindowInventory.swift +65 -0
  10. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubPose.swift +225 -0
  11. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubSidebar.swift +57 -0
  12. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubWindowDiscovery.swift +108 -0
  13. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/main.swift +66 -13
  14. package/apple/macos-helper/Sources/AgentDeviceMacOSInput/MouseClickDelivery.swift +116 -0
  15. package/apple/macos-helper/Sources/AgentDeviceMacOSInput/MouseClickSchedule.swift +61 -0
  16. package/apple/macos-helper/Tests/AgentDeviceMacOSDeviceHubTests/DeviceHubPoseTests.swift +30 -0
  17. package/apple/macos-helper/Tests/AgentDeviceMacOSDeviceHubTests/DeviceHubWindowInventoryTests.swift +65 -0
  18. package/apple/macos-helper/Tests/AgentDeviceMacOSInputTests/MouseClickScheduleTests.swift +58 -0
  19. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +1 -0
  20. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAppScreenCapture.swift +175 -0
  21. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +10 -2
  22. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +42 -15
  23. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +8 -0
  24. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +45 -22
  25. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +39 -734
  26. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Keyboard.swift +20 -4
  27. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +63 -1
  28. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +13 -0
  29. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +25 -10
  30. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +5 -562
  31. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotAcquisition.swift +566 -0
  32. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +5 -1
  33. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedInteraction.swift +594 -0
  34. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.h +35 -1
  35. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.m +160 -0
  36. package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/RecordingExportSupport.swift +3 -0
  37. package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/recording-frames.swift +203 -0
  38. package/dist/apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotCoordinateSpace.swift +5 -0
  39. package/dist/src/absence-observation-errors.js +1 -1
  40. package/dist/src/adapter.js +1 -1
  41. package/dist/src/agent-browser-lifecycle.js +1 -1
  42. package/dist/src/agent-browser-process-record.js +1 -1
  43. package/dist/src/agent-device-client.js +2 -2
  44. package/dist/src/alert-runtime.js +1 -1
  45. package/dist/src/android-adb.js +1 -1
  46. package/dist/src/android-input-ownership.js +1 -1
  47. package/dist/src/android.js +1 -1
  48. package/dist/src/app-launch.js +1 -1
  49. package/dist/src/app-log-runtime2.js +3 -3
  50. package/dist/src/app-resolution.js +2 -2
  51. package/dist/src/apple-runner-platform.js +1 -1
  52. package/dist/src/application-lifecycle-interaction.js +1 -1
  53. package/dist/src/artifact-paths.js +1 -0
  54. package/dist/src/back-mode.js +1 -1
  55. package/dist/src/cli-config.js +1 -1
  56. package/dist/src/cli-help.js +15 -11
  57. package/dist/src/cli.js +4 -4
  58. package/dist/src/client-types.d.ts +51 -1
  59. package/dist/src/command-tools.js +3 -3
  60. package/dist/src/config.js +1 -1
  61. package/dist/src/connection-runtime.js +1 -1
  62. package/dist/src/device-claim-inspection.js +1 -1
  63. package/dist/src/device-claim-rule.js +2 -2
  64. package/dist/src/device-input-state.d.ts +1 -1
  65. package/dist/src/device-rotation.js +1 -1
  66. package/dist/src/device-selection-resolver.js +1 -1
  67. package/dist/src/device-session.js +1 -1
  68. package/dist/src/device.js +1 -1
  69. package/dist/src/device2.js +1 -1
  70. package/dist/src/devicectl.js +1 -1
  71. package/dist/src/dispatch-resolve.js +1 -1
  72. package/dist/src/display-inventory.js +1 -1
  73. package/dist/src/doctor-facade.js +1 -1
  74. package/dist/src/durable-resource.js +1 -1
  75. package/dist/src/exec.js +1 -1
  76. package/dist/src/find.js +1 -1
  77. package/dist/src/finders.js +1 -1
  78. package/dist/src/fold-runtime.js +1 -0
  79. package/dist/src/gesture-admission.js +1 -1
  80. package/dist/src/gesture-runtime.js +1 -1
  81. package/dist/src/harmonyos.js +1 -1
  82. package/dist/src/helper.js +2 -2
  83. package/dist/src/host.js +1 -0
  84. package/dist/src/interaction-positionals.js +1 -1
  85. package/dist/src/interaction-ref-policy.js +1 -1
  86. package/dist/src/interaction.js +1 -1
  87. package/dist/src/interaction2.js +1 -1
  88. package/dist/src/interactor-operation-binding.js +1 -0
  89. package/dist/src/interactor-operation-catalog.js +1 -1
  90. package/dist/src/interactor.js +4 -5
  91. package/dist/src/interactor2.js +1 -1
  92. package/dist/src/interactors.js +1 -1
  93. package/dist/src/internal/bin.js +1 -1
  94. package/dist/src/internal/daemon.js +2 -2
  95. package/dist/src/internal/png-worker.d.ts +5 -0
  96. package/dist/src/internal/png-worker.js +1 -1
  97. package/dist/src/inventory2.js +1 -1
  98. package/dist/src/ios-snapshot-acquisition.js +1 -1
  99. package/dist/src/ios-snapshot-runtime.js +1 -1
  100. package/dist/src/lease.js +1 -1
  101. package/dist/src/linux.js +1 -1
  102. package/dist/src/local-interactor-operation-set.js +1 -1
  103. package/dist/src/location-coordinates.js +1 -1
  104. package/dist/src/macos-facade.js +1 -1
  105. package/dist/src/managed-device-scope.js +1 -1
  106. package/dist/src/mechanics.js +17 -16
  107. package/dist/src/mobile-snapshot-semantics.js +1 -1
  108. package/dist/src/mp4-duration.js +1 -0
  109. package/dist/src/multitouch-support.js +1 -1
  110. package/dist/src/owner-identity.js +1 -1
  111. package/dist/src/parameterized-recorded-fill.js +1 -1
  112. package/dist/src/perf-facade.js +2 -2
  113. package/dist/src/perf-runtime-plan.js +1 -1
  114. package/dist/src/physical-device-control.js +1 -1
  115. package/dist/src/platform-request-scope.js +1 -1
  116. package/dist/src/platform-runtime-managed-owner.js +1 -1
  117. package/dist/src/platform-runtime-open-target.js +1 -1
  118. package/dist/src/platform-runtime-operation-host.js +2 -2
  119. package/dist/src/platform-runtime-runtime-hints.js +1 -1
  120. package/dist/src/platform-runtime-screen-recording-finalizer-host.js +1 -1
  121. package/dist/src/platform-runtime-unavailable.js +1 -1
  122. package/dist/src/platform-runtime.js +1 -1
  123. package/dist/src/platform-runtime2.js +1 -1
  124. package/dist/src/png-crop-bytes.js +1 -1
  125. package/dist/src/png-encode.js +1 -0
  126. package/dist/src/png-size.js +1 -0
  127. package/dist/src/png-worker-client.js +1 -1
  128. package/dist/src/provider-webdriver.js +1 -1
  129. package/dist/src/quality-warnings.js +1 -1
  130. package/dist/src/react-native.js +1 -1
  131. package/dist/src/record-runtime.js +1 -1
  132. package/dist/src/recording-export-quality.js +1 -1
  133. package/dist/src/recording-scope.js +1 -0
  134. package/dist/src/recording.js +3 -0
  135. package/dist/src/registry.js +35 -35
  136. package/dist/src/replay.js +1 -1
  137. package/dist/src/request-admission.js +1 -1
  138. package/dist/src/request-runtime-binding.js +1 -1
  139. package/dist/src/result-serialization.js +1 -1
  140. package/dist/src/runner-cache-metadata.js +2 -2
  141. package/dist/src/runner-client.js +1 -1
  142. package/dist/src/runner-contract.js +3 -0
  143. package/dist/src/runner-dev-tools-security.js +1 -0
  144. package/dist/src/runner-device-readiness.js +1 -0
  145. package/dist/src/runner-disposal.js +1 -1
  146. package/dist/src/runner-operations-facade.js +1 -1
  147. package/dist/src/runner-presentation.js +1 -1
  148. package/dist/src/runner.js +1 -1
  149. package/dist/src/runtime.js +2 -2
  150. package/dist/src/runtime10.js +2 -1
  151. package/dist/src/runtime2.js +4 -4
  152. package/dist/src/runtime4.js +5 -5
  153. package/dist/src/runtime5.js +1 -1
  154. package/dist/src/runtime6.js +1 -1
  155. package/dist/src/runtime7.js +1 -1
  156. package/dist/src/runtime8.js +1 -1
  157. package/dist/src/runtime9.js +1 -1
  158. package/dist/src/screen-recording-admission-ledger.js +1 -1
  159. package/dist/src/screen-recording-resource-recovery.js +1 -1
  160. package/dist/src/screen-recording-transport.js +1 -1
  161. package/dist/src/screenshot-density.js +1 -1
  162. package/dist/src/screenshot-image.js +1 -0
  163. package/dist/src/screenshot-overlay-draw.js +1 -0
  164. package/dist/src/screenshot-runtime.js +1 -1
  165. package/dist/src/script.js +1 -1
  166. package/dist/src/scroll-gesture.js +1 -1
  167. package/dist/src/sdk-batch.js +2 -2
  168. package/dist/src/sdk-contracts.d.ts +7 -5
  169. package/dist/src/sdk-contracts.js +2 -2
  170. package/dist/src/sdk-device.js +1 -1
  171. package/dist/src/sdk-finders.js +1 -1
  172. package/dist/src/sdk-selectors.d.ts +58 -6
  173. package/dist/src/sdk-selectors.js +1 -1
  174. package/dist/src/selection.js +0 -0
  175. package/dist/src/selector-runtime.js +1 -1
  176. package/dist/src/selectors.js +1 -1
  177. package/dist/src/semantic-index.js +1 -1
  178. package/dist/src/session-routing.js +1 -1
  179. package/dist/src/session-snapshot.js +1 -1
  180. package/dist/src/session-store.js +3 -3
  181. package/dist/src/session2.js +3 -3
  182. package/dist/src/simctl.js +1 -1
  183. package/dist/src/simulator-boot.js +1 -1
  184. package/dist/src/simulator-facade.js +1 -1
  185. package/dist/src/simulator.js +1 -1
  186. package/dist/src/snapshot-runtime-binding.js +1 -1
  187. package/dist/src/snapshot-runtime.js +1 -1
  188. package/dist/src/snapshot-state.js +1 -1
  189. package/dist/src/snapshot4.js +1 -1
  190. package/dist/src/src.js +1 -1
  191. package/dist/src/src7.js +1 -1
  192. package/dist/src/src8.js +1 -1
  193. package/dist/src/stop-sequence.js +1 -1
  194. package/dist/src/text-entry-focus.js +1 -1
  195. package/dist/src/touch-runtime.js +1 -1
  196. package/dist/src/tree.js +1 -1
  197. package/dist/src/type-text-runtime.js +1 -1
  198. package/dist/src/video.js +2 -2
  199. package/dist/src/web2.js +1 -1
  200. package/package.json +3 -1
  201. package/server.json +2 -2
  202. package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.7.apk.sha256 +0 -1
  203. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.7.apk.sha256 +0 -1
  204. package/dist/src/png-transcode.js +0 -1
@@ -0,0 +1 @@
1
+ abf226740c34ca523708302fae272010cc2616b651b6073737c50f909f672b38 agent-device-android-ime-helper-0.21.9.apk
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "android-ime-helper",
3
- "version": "0.21.7",
4
- "assetName": "agent-device-android-ime-helper-0.21.7.apk",
5
- "sha256": "76e2409c08e5fa41a217a01d0679ea8cfa567ba8828044bb38e02f95ac512248",
3
+ "version": "0.21.9",
4
+ "assetName": "agent-device-android-ime-helper-0.21.9.apk",
5
+ "sha256": "abf226740c34ca523708302fae272010cc2616b651b6073737c50f909f672b38",
6
6
  "packageName": "com.callstack.agentdevice.imehelper",
7
- "versionCode": 21007,
7
+ "versionCode": 21009,
8
8
  "serviceComponent": "com.callstack.agentdevice.imehelper/.TestInputMethodService",
9
9
  "broadcastProtocol": "android-ime-helper-v1"
10
10
  }
@@ -0,0 +1 @@
1
+ c73eed5a190579623e2c6b5baa2b3f7e760a5d41d4630e05d970826007d4ae55 agent-device-android-snapshot-helper-0.21.9.apk
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "android-snapshot-helper",
3
- "version": "0.21.7",
4
- "releaseTag": "v0.21.7",
5
- "assetName": "agent-device-android-snapshot-helper-0.21.7.apk",
3
+ "version": "0.21.9",
4
+ "releaseTag": "v0.21.9",
5
+ "assetName": "agent-device-android-snapshot-helper-0.21.9.apk",
6
6
  "apkUrl": null,
7
- "sha256": "19690a5cc158aafbe0dd2bf33cd58950ef1d40ad3b52fd9e5014578ca64a7d13",
8
- "checksumName": "agent-device-android-snapshot-helper-0.21.7.apk.sha256",
7
+ "sha256": "c73eed5a190579623e2c6b5baa2b3f7e760a5d41d4630e05d970826007d4ae55",
8
+ "checksumName": "agent-device-android-snapshot-helper-0.21.9.apk.sha256",
9
9
  "packageName": "com.callstack.agentdevice.snapshothelper",
10
- "versionCode": 21007,
10
+ "versionCode": 21009,
11
11
  "instrumentationRunner": "com.callstack.agentdevice.snapshothelper/.SnapshotInstrumentation",
12
12
  "minSdk": 23,
13
13
  "targetSdk": 36,
@@ -11,8 +11,23 @@ let package = Package(
11
11
  ),
12
12
  ],
13
13
  targets: [
14
+ .target(
15
+ name: "AgentDeviceMacOSInput"
16
+ ),
17
+ .target(
18
+ name: "AgentDeviceMacOSDeviceHub"
19
+ ),
14
20
  .executableTarget(
15
- name: "AgentDeviceMacOSHelper"
21
+ name: "AgentDeviceMacOSHelper",
22
+ dependencies: ["AgentDeviceMacOSInput", "AgentDeviceMacOSDeviceHub"]
23
+ ),
24
+ .testTarget(
25
+ name: "AgentDeviceMacOSInputTests",
26
+ dependencies: ["AgentDeviceMacOSInput"]
27
+ ),
28
+ .testTarget(
29
+ name: "AgentDeviceMacOSDeviceHubTests",
30
+ dependencies: ["AgentDeviceMacOSDeviceHub"]
16
31
  ),
17
32
  ]
18
33
  )
@@ -0,0 +1,47 @@
1
+ import Foundation
2
+
3
+ /// The decisions the Device Hub pose press makes without a window server: which control a pose
4
+ /// names, which window belongs to a device, and which sidebar row identifies it. They live apart
5
+ /// from the helper executable so the tests can exercise them without an accessibility session.
6
+
7
+ /// Device Hub's three pose presets for a foldable simulator, named as the helper's `--pose`
8
+ /// argument spells them. Each maps to the accessibility description of its action-bar button.
9
+ public enum DeviceHubPose: String, CaseIterable, Sendable {
10
+ case closed
11
+ case book
12
+ case open
13
+
14
+ public init?(argument: String) {
15
+ self.init(rawValue: argument.lowercased())
16
+ }
17
+
18
+ /// The `AXDescription` Device Hub gives the button for this pose.
19
+ public var controlDescription: String {
20
+ switch self {
21
+ case .closed: return "Closed"
22
+ case .book: return "Book"
23
+ case .open: return "Open"
24
+ }
25
+ }
26
+ }
27
+
28
+ /// Device Hub is launched through a trampoline, so LaunchServices registers it with no process
29
+ /// identifier; the process table is matched on the executable path instead.
30
+ public let deviceHubExecutableSuffix = "/DeviceHub.app/Contents/MacOS/DeviceHub"
31
+
32
+ /// The separator Device Hub puts between the device name and the OS in a window title.
33
+ public let deviceHubWindowTitleSeparator = " – "
34
+
35
+ /// Device Hub titles a device window `<device name> – <OS> <version>`, so a window shows the
36
+ /// device when its title is the name or the name followed by that exact separator. A bare
37
+ /// space would also match a simulator whose name merely extends this one ("iPhone Duo Lab").
38
+ public func deviceHubWindowShows(deviceName: String, title: String?) -> Bool {
39
+ guard let title else { return false }
40
+ return title == deviceName || title.hasPrefix(deviceName + deviceHubWindowTitleSeparator)
41
+ }
42
+
43
+ /// The accessibility identifier of the sidebar row for one simulator. The UDID is the one device
44
+ /// identity Device Hub exposes that two simulators sharing a name cannot confuse.
45
+ public func deviceHubDeviceRowIdentifier(udid: String) -> String {
46
+ return "TableRow.Device.\(udid)"
47
+ }
@@ -0,0 +1,65 @@
1
+ import ApplicationServices
2
+
3
+ package enum DeviceHubWindowRead {
4
+ case available([AXUIElement])
5
+ case failed(Int32)
6
+ }
7
+
8
+ package struct DeviceHubWindowCandidate {
9
+ package let processID: Int32
10
+ package let window: AXUIElement
11
+ }
12
+
13
+ package enum DeviceHubRowLookup {
14
+ case identified(AXUIElement, restoreSidebar: Bool)
15
+ case sidebarUnavailable
16
+ case unconfirmed
17
+ case timedOut
18
+
19
+ package var diagnostic: String {
20
+ switch self {
21
+ case .identified: return "identified"
22
+ case .sidebarUnavailable: return "sidebar-unavailable"
23
+ case .unconfirmed: return "row-unconfirmed"
24
+ case .timedOut: return "timed-out"
25
+ }
26
+ }
27
+ }
28
+
29
+ package struct DeviceHubWindowInventory {
30
+ package private(set) var candidates: [DeviceHubWindowCandidate] = []
31
+ package private(set) var emptyProcessIDs: [Int32] = []
32
+ package private(set) var failures: [Int32: Int32] = [:]
33
+ package private(set) var candidateOutcomes: [String] = []
34
+ package private(set) var budgetExpired = false
35
+
36
+ package init() {}
37
+
38
+ package mutating func record(processID: Int32, read: DeviceHubWindowRead) {
39
+ switch read {
40
+ case .available(let windows):
41
+ if windows.isEmpty { emptyProcessIDs.append(processID) }
42
+ candidates.append(contentsOf: windows.map { DeviceHubWindowCandidate(processID: processID, window: $0) })
43
+ case .failed(let status):
44
+ failures[processID] = status
45
+ }
46
+ }
47
+
48
+ package mutating func record(candidate: Int, outcome: DeviceHubRowLookup) {
49
+ candidateOutcomes.append("\(candidates[candidate].processID):\(candidate):\(outcome.diagnostic)")
50
+ }
51
+
52
+ package mutating func expireBudget() { budgetExpired = true }
53
+
54
+ package func processesToReopen(attempted: Set<Int32>) -> [Int32] {
55
+ budgetExpired ? [] : emptyProcessIDs.filter { !attempted.contains($0) }
56
+ }
57
+
58
+ package func failureReason(reopenFailures: [Int32: String] = [:]) -> String {
59
+ if !failures.isEmpty { return "device-hub-window-read-failed" }
60
+ if budgetExpired { return "device-hub-discovery-timeout" }
61
+ if !candidates.isEmpty { return "device-hub-identity-unconfirmed" }
62
+ if let reason = reopenFailures.sorted(by: { $0.key < $1.key }).first?.value { return reason }
63
+ return "device-hub-window-missing"
64
+ }
65
+ }
@@ -0,0 +1,225 @@
1
+ import AgentDeviceMacOSDeviceHub
2
+ import AppKit
3
+ import ApplicationServices
4
+ import Darwin
5
+ import Foundation
6
+
7
+ /// How long sidebar selection and pose-control discovery may each wait before refusing a press.
8
+ private let deviceHubSettleDeadline: TimeInterval = 8
9
+ private let deviceHubPoll: TimeInterval = 0.25
10
+
11
+ struct DeviceHubPoseResponse: Encodable {
12
+ let pose: String
13
+ let control: String
14
+ let windowTitle: String
15
+ /** Whether Device Hub had to be asked to reopen a window before the device could be selected. */
16
+ let reopened: Bool
17
+ /** Whether the window was switched to the device through its sidebar row. */
18
+ let selected: Bool
19
+ }
20
+
21
+ func handleDeviceHub(arguments: [String]) throws -> any Encodable {
22
+ guard arguments.first == "pose" else {
23
+ throw HelperError.invalidArgs("device-hub requires pose")
24
+ }
25
+ let rest = Array(arguments.dropFirst())
26
+ guard let udid = helperOptionValue(arguments: rest, name: "--udid")?
27
+ .trimmingCharacters(in: .whitespacesAndNewlines),
28
+ !udid.isEmpty
29
+ else {
30
+ throw HelperError.invalidArgs("device-hub pose requires --udid <udid>")
31
+ }
32
+ guard let deviceName = helperOptionValue(arguments: rest, name: "--device-name")?
33
+ .trimmingCharacters(in: .whitespacesAndNewlines),
34
+ !deviceName.isEmpty
35
+ else {
36
+ throw HelperError.invalidArgs("device-hub pose requires --device-name <name>")
37
+ }
38
+ guard let pose = helperOptionValue(arguments: rest, name: "--pose").flatMap(DeviceHubPose.init(argument:))
39
+ else {
40
+ throw HelperError.invalidArgs("device-hub pose requires --pose <closed|book|open>")
41
+ }
42
+ let control = pose.controlDescription
43
+ guard AXIsProcessTrusted() else {
44
+ throw HelperError.commandFailed(
45
+ "fold needs Accessibility permission to press the Device Hub pose control",
46
+ details: ["reason": "accessibility-permission", "permission": "accessibility"]
47
+ )
48
+ }
49
+ let discovery = try discoverDeviceHubWindow(udid: udid, deviceName: deviceName)
50
+ let window = discovery.window
51
+ defer {
52
+ if discovery.revealedSidebar {
53
+ _ = setDeviceHubSidebar(visible: false, window: window,
54
+ deadline: ProcessInfo.processInfo.systemUptime + 0.5)
55
+ }
56
+ }
57
+ let selected = try selectDevice(row: discovery.row, deviceName: deviceName, in: window)
58
+ let windowTitle = stringAttribute(window, attribute: kAXTitleAttribute as String) ?? ""
59
+ guard let button = awaitPoseButton(in: window, description: control) else {
60
+ throw HelperError.commandFailed(
61
+ "Device Hub shows no \(control) pose control for \(deviceName)",
62
+ details: [
63
+ "reason": "device-hub-pose-control-missing",
64
+ "windowTitle": windowTitle,
65
+ "control": control,
66
+ ]
67
+ )
68
+ }
69
+ let status = AXUIElementPerformAction(button, kAXPressAction as CFString)
70
+ guard status == .success else {
71
+ throw HelperError.commandFailed(
72
+ "Device Hub refused the \(control) pose press",
73
+ details: ["reason": "device-hub-press-failed", "status": "\(status.rawValue)"]
74
+ )
75
+ }
76
+ return SuccessEnvelope(
77
+ data: DeviceHubPoseResponse(
78
+ pose: pose.rawValue,
79
+ control: control,
80
+ windowTitle: windowTitle,
81
+ reopened: discovery.reopened,
82
+ selected: selected
83
+ )
84
+ )
85
+ }
86
+
87
+ private func windowShows(deviceName: String, _ window: AXUIElement) -> Bool {
88
+ return deviceHubWindowShows(
89
+ deviceName: deviceName,
90
+ title: stringAttribute(window, attribute: kAXTitleAttribute as String)
91
+ )
92
+ }
93
+
94
+ /// `kAEReopenApplication` is what the Dock sends when an app with no open windows is clicked, and
95
+ /// it is the one event Device Hub answers by restoring the device window. Sent to the process
96
+ /// directly, because LaunchServices cannot address a trampolined app by bundle identifier.
97
+ func sendDeviceHubReopenEvent(to pid: pid_t, timeout: TimeInterval) throws {
98
+ let target = NSAppleEventDescriptor(processIdentifier: pid)
99
+ let event = NSAppleEventDescriptor(
100
+ eventClass: AEEventClass(kCoreEventClass),
101
+ eventID: AEEventID(kAEReopenApplication),
102
+ targetDescriptor: target,
103
+ returnID: AEReturnID(kAutoGenerateReturnID),
104
+ transactionID: AETransactionID(kAnyTransactionID)
105
+ )
106
+ do {
107
+ _ = try event.sendEvent(options: [.noReply], timeout: min(5, max(0.001, timeout)))
108
+ } catch {
109
+ let code = (error as NSError).code
110
+ throw HelperError.commandFailed(
111
+ "fold could not ask Device Hub to reopen its device window",
112
+ details: [
113
+ "reason": code == -1743 ? "automation-permission" : "device-hub-reopen-failed",
114
+ "error": String(describing: error),
115
+ ]
116
+ )
117
+ }
118
+ }
119
+
120
+ /// Switches the window to the device through its sidebar row, whose accessibility identifier is
121
+ /// `TableRow.Device.<udid>` — the one place Device Hub exposes a device identity that two
122
+ /// simulators sharing a name cannot confuse. A window already titled with the device still gets
123
+ /// the selection, because the title alone cannot tell such twins apart.
124
+ private func selectDevice(
125
+ row: AXUIElement,
126
+ deviceName: String,
127
+ in window: AXUIElement
128
+ ) throws -> Bool {
129
+ let deadline = ProcessInfo.processInfo.systemUptime + deviceHubSettleDeadline
130
+ AXUIElementSetMessagingTimeout(row, 0.25)
131
+ AXUIElementSetMessagingTimeout(window, 0.25)
132
+ let status = AXUIElementSetAttributeValue(row, kAXSelectedAttribute as CFString, kCFBooleanTrue)
133
+ guard status == .success || status == .cannotComplete else {
134
+ throw HelperError.commandFailed(
135
+ "Device Hub refused to select \(deviceName) in its sidebar",
136
+ details: ["reason": "device-hub-select-failed", "status": "\(status.rawValue)"]
137
+ )
138
+ }
139
+ while ProcessInfo.processInfo.systemUptime < deadline {
140
+ var selected: CFTypeRef?
141
+ if AXUIElementCopyAttributeValue(row, kAXSelectedAttribute as CFString, &selected) == .success,
142
+ (selected as? Bool) == true, windowShows(deviceName: deviceName, window) { return true }
143
+ Thread.sleep(forTimeInterval: deviceHubPoll)
144
+ }
145
+ throw HelperError.commandFailed(
146
+ "Device Hub did not confirm selection of \(deviceName)",
147
+ details: ["reason": "device-hub-select-unconfirmed", "status": "\(status.rawValue)"]
148
+ )
149
+ }
150
+
151
+ func deviceHubDeviceRow(udid: String, in window: AXUIElement, deadline: TimeInterval) -> AXUIElement? {
152
+ guard let label = findDeviceHubElement(root: window, depth: 0, deadline: deadline, where: {
153
+ stringAttribute($0, attribute: "AXIdentifier") == deviceHubDeviceRowIdentifier(udid: udid)
154
+ }) else {
155
+ return nil
156
+ }
157
+ var current: AXUIElement? = label
158
+ while let element = current, ProcessInfo.processInfo.systemUptime < deadline {
159
+ AXUIElementSetMessagingTimeout(element, Float(max(0.001, min(0.25, deadline - ProcessInfo.processInfo.systemUptime))))
160
+ if stringAttribute(element, attribute: kAXRoleAttribute as String) == "AXRow" {
161
+ return element
162
+ }
163
+ current = elementAttribute(element, attribute: kAXParentAttribute as String)
164
+ }
165
+ return nil
166
+ }
167
+
168
+ /// The action bar is rebuilt for the device the window shows, so right after a sidebar selection
169
+ /// the window already carries the new title while the pose controls are still being laid out.
170
+ /// The controls are therefore awaited, not looked up once.
171
+ private func awaitPoseButton(in window: AXUIElement, description: String) -> AXUIElement? {
172
+ let deadline = ProcessInfo.processInfo.systemUptime + deviceHubSettleDeadline
173
+ while true {
174
+ if let button = poseButton(in: window, description: description, deadline: deadline) {
175
+ return button
176
+ }
177
+ guard ProcessInfo.processInfo.systemUptime < deadline else { return nil }
178
+ Thread.sleep(forTimeInterval: deviceHubPoll)
179
+ }
180
+ }
181
+
182
+ /// The pose controls sit in the window's action bar as `AXButton`s described by their preset
183
+ /// name; the simulated screen inside the same window is an iOS content group whose own buttons
184
+ /// carry app labels, never these three.
185
+ private func poseButton(in window: AXUIElement, description: String, deadline: TimeInterval) -> AXUIElement? {
186
+ return findDeviceHubElement(root: window, depth: 0, deadline: deadline) {
187
+ stringAttribute($0, attribute: kAXRoleAttribute as String) == "AXButton"
188
+ && stringAttribute($0, attribute: kAXDescriptionAttribute as String) == description
189
+ }
190
+ }
191
+
192
+ func findDeviceHubElement(
193
+ root: AXUIElement,
194
+ depth: Int,
195
+ deadline: TimeInterval,
196
+ where matches: (AXUIElement) -> Bool
197
+ ) -> AXUIElement? {
198
+ let remaining = deadline - ProcessInfo.processInfo.systemUptime
199
+ if depth > 14 || remaining <= 0 {
200
+ return nil
201
+ }
202
+ AXUIElementSetMessagingTimeout(root, Float(min(0.25, remaining)))
203
+ for child in children(of: root) {
204
+ let remaining = deadline - ProcessInfo.processInfo.systemUptime
205
+ guard remaining > 0 else { return nil }
206
+ AXUIElementSetMessagingTimeout(child, Float(min(0.25, remaining)))
207
+ if matches(child) {
208
+ return child
209
+ }
210
+ if stringAttribute(child, attribute: kAXSubroleAttribute as String) == "iOSContentGroup" {
211
+ continue
212
+ }
213
+ if let nested = findDeviceHubElement(root: child, depth: depth + 1, deadline: deadline, where: matches) {
214
+ return nested
215
+ }
216
+ }
217
+ return nil
218
+ }
219
+
220
+ func helperOptionValue(arguments: [String], name: String) -> String? {
221
+ guard let index = arguments.firstIndex(of: name), arguments.indices.contains(index + 1) else {
222
+ return nil
223
+ }
224
+ return arguments[index + 1]
225
+ }
@@ -0,0 +1,57 @@
1
+ import AgentDeviceMacOSDeviceHub
2
+ import ApplicationServices
3
+ import Foundation
4
+
5
+ func identifyDeviceHubRow(
6
+ udid: String,
7
+ candidate: DeviceHubWindowCandidate,
8
+ deadline: TimeInterval
9
+ ) -> DeviceHubRowLookup {
10
+ let now = ProcessInfo.processInfo.systemUptime
11
+ let visibleRowDeadline = now + max(0, deadline - now) / 3
12
+ if let row = deviceHubDeviceRow(udid: udid, in: candidate.window, deadline: visibleRowDeadline) {
13
+ return .identified(row, restoreSidebar: false)
14
+ }
15
+ let searchDeadline = deadline - 0.25
16
+ guard let button = deviceHubSidebarButton(visible: true, window: candidate.window, deadline: searchDeadline) else {
17
+ return ProcessInfo.processInfo.systemUptime >= searchDeadline ? .timedOut : .sidebarUnavailable
18
+ }
19
+ var transferCleanup = false
20
+ defer {
21
+ if !transferCleanup {
22
+ _ = setDeviceHubSidebar(visible: false, window: candidate.window, deadline: deadline)
23
+ }
24
+ }
25
+ let status = pressDeviceHubSidebarButton(button, deadline: searchDeadline)
26
+ guard status == .success || status == .cannotComplete else { return .unconfirmed }
27
+ repeat {
28
+ if let row = deviceHubDeviceRow(udid: udid, in: candidate.window, deadline: searchDeadline) {
29
+ transferCleanup = true
30
+ return .identified(row, restoreSidebar: true)
31
+ }
32
+ let remaining = searchDeadline - ProcessInfo.processInfo.systemUptime
33
+ if remaining <= 0 { return .timedOut }
34
+ Thread.sleep(forTimeInterval: min(0.05, remaining))
35
+ } while ProcessInfo.processInfo.systemUptime < searchDeadline
36
+ return .timedOut
37
+ }
38
+
39
+ func setDeviceHubSidebar(visible: Bool, window: AXUIElement, deadline: TimeInterval) -> Bool {
40
+ guard let button = deviceHubSidebarButton(visible: visible, window: window, deadline: deadline) else { return false }
41
+ return pressDeviceHubSidebarButton(button, deadline: deadline) == .success
42
+ }
43
+
44
+ private func deviceHubSidebarButton(visible: Bool, window: AXUIElement, deadline: TimeInterval) -> AXUIElement? {
45
+ let description = visible ? "Show Sidebar" : "Hide Sidebar"
46
+ return findDeviceHubElement(root: window, depth: 0, deadline: deadline) {
47
+ stringAttribute($0, attribute: kAXRoleAttribute as String) == "AXButton"
48
+ && stringAttribute($0, attribute: kAXDescriptionAttribute as String) == description
49
+ }
50
+ }
51
+
52
+ private func pressDeviceHubSidebarButton(_ button: AXUIElement, deadline: TimeInterval) -> AXError {
53
+ let remaining = deadline - ProcessInfo.processInfo.systemUptime
54
+ guard remaining > 0 else { return .cannotComplete }
55
+ AXUIElementSetMessagingTimeout(button, Float(min(0.1, remaining)))
56
+ return AXUIElementPerformAction(button, kAXPressAction as CFString)
57
+ }
@@ -0,0 +1,108 @@
1
+ import AgentDeviceMacOSDeviceHub
2
+ import AppKit
3
+ import ApplicationServices
4
+ import Darwin
5
+
6
+ struct DiscoveredDeviceHubWindow {
7
+ let window: AXUIElement
8
+ let reopened: Bool
9
+ let row: AXUIElement
10
+ let revealedSidebar: Bool
11
+ }
12
+
13
+ func discoverDeviceHubWindow(udid: String, deviceName: String) throws -> DiscoveredDeviceHubWindow {
14
+ let deadline = ProcessInfo.processInfo.systemUptime + 8
15
+ var reopened = Set<pid_t>()
16
+ var reopenFailures: [pid_t: String] = [:]
17
+ var inventory = DeviceHubWindowInventory()
18
+ var processIDs: [pid_t] = []
19
+ repeat {
20
+ processIDs = deviceHubProcessIdentifiers()
21
+ if processIDs.isEmpty {
22
+ throw HelperError.commandFailed(
23
+ "Xcode Device Hub is not running",
24
+ details: ["reason": "device-hub-not-running", "bundleId": "com.apple.dt.Devices"]
25
+ )
26
+ }
27
+ inventory = DeviceHubWindowInventory()
28
+ for pid in processIDs {
29
+ let remaining = deadline - ProcessInfo.processInfo.systemUptime
30
+ guard remaining > 0 else { inventory.expireBudget(); break }
31
+ let app = AXUIElementCreateApplication(pid)
32
+ AXUIElementSetMessagingTimeout(app, Float(min(0.5, remaining)))
33
+ var value: CFTypeRef?
34
+ let status = AXUIElementCopyAttributeValue(app, kAXWindowsAttribute as CFString, &value)
35
+ if status == .success, let windows = value as? [AXUIElement] {
36
+ for window in windows { AXUIElementSetMessagingTimeout(window, 0.5) }
37
+ inventory.record(processID: pid, read: .available(windows))
38
+ } else {
39
+ inventory.record(processID: pid, read: .failed(
40
+ status == .success ? AXError.illegalArgument.rawValue : status.rawValue
41
+ ))
42
+ }
43
+ }
44
+ for (index, candidate) in inventory.candidates.enumerated() {
45
+ let now = ProcessInfo.processInfo.systemUptime
46
+ guard now < deadline else { inventory.expireBudget(); break }
47
+ let candidateDeadline = now + (deadline - now) / Double(inventory.candidates.count - index) / 2
48
+ let outcome = identifyDeviceHubRow(udid: udid, candidate: candidate, deadline: candidateDeadline)
49
+ inventory.record(candidate: index, outcome: outcome)
50
+ if case .identified(let row, let restoreSidebar) = outcome {
51
+ return DiscoveredDeviceHubWindow(window: candidate.window,
52
+ reopened: reopened.contains(candidate.processID), row: row, revealedSidebar: restoreSidebar)
53
+ }
54
+ }
55
+ let attempted = reopened.union(reopenFailures.keys)
56
+ for pid in inventory.processesToReopen(attempted: attempted) {
57
+ guard ProcessInfo.processInfo.systemUptime < deadline else { inventory.expireBudget(); break }
58
+ do {
59
+ try sendDeviceHubReopenEvent(to: pid, timeout: deadline - ProcessInfo.processInfo.systemUptime)
60
+ reopened.insert(pid)
61
+ } catch let error as HelperError {
62
+ guard case .commandFailed(_, let details) = error else { throw error }
63
+ reopenFailures[pid] = details["reason"] ?? "device-hub-reopen-failed"
64
+ }
65
+ }
66
+ let remaining = deadline - ProcessInfo.processInfo.systemUptime
67
+ if remaining <= 0 { inventory.expireBudget(); break }
68
+ Thread.sleep(forTimeInterval: min(0.25, remaining))
69
+ } while ProcessInfo.processInfo.systemUptime < deadline
70
+
71
+ let statuses = inventory.failures.sorted { $0.key < $1.key }
72
+ .map { "\($0.key):\($0.value)" }.joined(separator: ",")
73
+ throw HelperError.commandFailed(
74
+ "Could not confirm a Device Hub window for the requested device",
75
+ details: [
76
+ "reason": inventory.failureReason(reopenFailures: reopenFailures),
77
+ "reopenFailures": reopenFailures.sorted { $0.key < $1.key }.map { "\($0.key):\($0.value)" }.joined(separator: ","),
78
+ "candidateOutcomes": inventory.candidateOutcomes.joined(separator: ","),
79
+ "udid": udid,
80
+ "deviceName": deviceName,
81
+ "processIDs": processIDs.map(String.init).joined(separator: ","),
82
+ "axWindowReadStatuses": statuses,
83
+ "hostDisplayIDs": deviceHubHostDisplays().map { String($0) }.joined(separator: ","),
84
+ ]
85
+ )
86
+ }
87
+
88
+ private func deviceHubProcessIdentifiers() -> [pid_t] {
89
+ let byteCount = proc_listpids(UInt32(PROC_ALL_PIDS), 0, nil, 0)
90
+ guard byteCount > 0 else { return [] }
91
+ var pids = [pid_t](repeating: 0, count: Int(byteCount) / MemoryLayout<pid_t>.size + 64)
92
+ let filled = proc_listpids(UInt32(PROC_ALL_PIDS), 0, &pids, Int32(pids.count * MemoryLayout<pid_t>.size))
93
+ guard filled > 0 else { return [] }
94
+ return pids.prefix(Int(filled) / MemoryLayout<pid_t>.size).filter { pid in
95
+ guard pid > 0 else { return false }
96
+ var path = [CChar](repeating: 0, count: 4096)
97
+ guard proc_pidpath(pid, &path, UInt32(path.count)) > 0 else { return false }
98
+ return String(cString: path).hasSuffix(deviceHubExecutableSuffix)
99
+ }.sorted()
100
+ }
101
+
102
+ private func deviceHubHostDisplays() -> [CGDirectDisplayID] {
103
+ var count: UInt32 = 0
104
+ guard CGGetActiveDisplayList(0, nil, &count) == .success else { return [] }
105
+ var displays = [CGDirectDisplayID](repeating: 0, count: Int(count))
106
+ guard CGGetActiveDisplayList(count, &displays, &count) == .success else { return [] }
107
+ return Array(displays.prefix(Int(count)))
108
+ }