agent-device 0.21.8 → 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.
- package/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.8.apk → agent-device-android-ime-helper-0.21.9.apk} +0 -0
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.9.apk.sha256 +1 -0
- package/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.8.manifest.json → agent-device-android-ime-helper-0.21.9.manifest.json} +4 -4
- package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.21.8.apk → agent-device-android-snapshot-helper-0.21.9.apk} +0 -0
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.9.apk.sha256 +1 -0
- package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.21.8.manifest.json → agent-device-android-snapshot-helper-0.21.9.manifest.json} +6 -6
- package/apple/macos-helper/Sources/AgentDeviceMacOSDeviceHub/DeviceHubWindowInventory.swift +65 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubPose.swift +45 -140
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubSidebar.swift +57 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubWindowDiscovery.swift +108 -0
- package/apple/macos-helper/Tests/AgentDeviceMacOSDeviceHubTests/DeviceHubWindowInventoryTests.swift +65 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +1 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAppScreenCapture.swift +175 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +10 -2
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +42 -15
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +8 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +45 -22
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +39 -734
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Keyboard.swift +20 -4
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +63 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +13 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +25 -10
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +5 -562
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotAcquisition.swift +566 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +5 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedInteraction.swift +594 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.h +35 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.m +160 -0
- package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/RecordingExportSupport.swift +3 -0
- package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/recording-frames.swift +203 -0
- package/dist/apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotCoordinateSpace.swift +5 -0
- package/dist/src/absence-observation-errors.js +1 -1
- package/dist/src/adapter.js +1 -1
- package/dist/src/agent-browser-lifecycle.js +1 -1
- package/dist/src/agent-browser-process-record.js +1 -1
- package/dist/src/agent-device-client.js +1 -1
- package/dist/src/alert-runtime.js +1 -1
- package/dist/src/android-adb.js +1 -1
- package/dist/src/android-input-ownership.js +1 -1
- package/dist/src/android.js +1 -1
- package/dist/src/app-launch.js +1 -1
- package/dist/src/app-log-runtime2.js +3 -3
- package/dist/src/app-resolution.js +2 -2
- package/dist/src/apple-runner-platform.js +1 -1
- package/dist/src/application-lifecycle-interaction.js +1 -1
- package/dist/src/artifact-paths.js +1 -0
- package/dist/src/back-mode.js +1 -1
- package/dist/src/cli-config.js +1 -1
- package/dist/src/cli-help.js +8 -7
- package/dist/src/cli.js +4 -4
- package/dist/src/client-types.d.ts +23 -5
- package/dist/src/command-tools.js +3 -3
- package/dist/src/connection-runtime.js +1 -1
- package/dist/src/device-claim-inspection.js +1 -1
- package/dist/src/device-claim-rule.js +2 -2
- package/dist/src/device-input-state.d.ts +1 -1
- package/dist/src/device-selection-resolver.js +1 -1
- package/dist/src/device.js +1 -1
- package/dist/src/device2.js +1 -1
- package/dist/src/dispatch-resolve.js +1 -1
- package/dist/src/doctor-facade.js +1 -1
- package/dist/src/durable-resource.js +1 -1
- package/dist/src/find.js +1 -1
- package/dist/src/finders.js +1 -1
- package/dist/src/fold-runtime.js +1 -0
- package/dist/src/gesture-admission.js +1 -1
- package/dist/src/interaction-positionals.js +1 -1
- package/dist/src/interaction-ref-policy.js +1 -1
- package/dist/src/interaction.js +1 -1
- package/dist/src/interaction2.js +1 -1
- package/dist/src/interactor-operation-catalog.js +1 -1
- package/dist/src/interactor.js +4 -4
- package/dist/src/interactors.js +1 -1
- package/dist/src/internal/bin.js +1 -1
- package/dist/src/internal/daemon.js +2 -2
- package/dist/src/inventory2.js +1 -1
- package/dist/src/ios-snapshot-acquisition.js +1 -1
- package/dist/src/ios-snapshot-runtime.js +1 -1
- package/dist/src/lease.js +1 -1
- package/dist/src/local-interactor-operation-set.js +1 -1
- package/dist/src/location-coordinates.js +1 -1
- package/dist/src/managed-device-scope.js +1 -1
- package/dist/src/mechanics.js +17 -17
- package/dist/src/mobile-snapshot-semantics.js +1 -1
- package/dist/src/mp4-duration.js +1 -0
- package/dist/src/multitouch-support.js +1 -1
- package/dist/src/owner-identity.js +1 -1
- package/dist/src/parameterized-recorded-fill.js +1 -1
- package/dist/src/perf-facade.js +2 -2
- package/dist/src/perf-runtime-plan.js +1 -1
- package/dist/src/platform-request-scope.js +1 -1
- package/dist/src/platform-runtime-managed-owner.js +1 -1
- package/dist/src/platform-runtime-open-target.js +1 -1
- package/dist/src/platform-runtime-operation-host.js +2 -2
- package/dist/src/platform-runtime-runtime-hints.js +1 -1
- package/dist/src/platform-runtime-screen-recording-finalizer-host.js +1 -1
- package/dist/src/platform-runtime-unavailable.js +1 -1
- package/dist/src/platform-runtime.js +1 -1
- package/dist/src/platform-runtime2.js +1 -1
- package/dist/src/png-crop-bytes.js +1 -1
- package/dist/src/png-encode.js +1 -0
- package/dist/src/png-size.js +1 -0
- package/dist/src/provider-webdriver.js +1 -1
- package/dist/src/quality-warnings.js +1 -1
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-runtime.js +1 -1
- package/dist/src/recording-export-quality.js +1 -1
- package/dist/src/recording-scope.js +1 -0
- package/dist/src/recording.js +3 -0
- package/dist/src/registry.js +35 -35
- package/dist/src/replay.js +2 -2
- package/dist/src/request-admission.js +1 -1
- package/dist/src/request-runtime-binding.js +1 -1
- package/dist/src/result-serialization.js +1 -1
- package/dist/src/runner-cache-metadata.js +2 -2
- package/dist/src/runner-client.js +1 -1
- package/dist/src/runner-contract.js +1 -1
- package/dist/src/runner-dev-tools-security.js +1 -1
- package/dist/src/runner-device-readiness.js +1 -1
- package/dist/src/runner-disposal.js +1 -1
- package/dist/src/runner-operations-facade.js +1 -1
- package/dist/src/runner-presentation.js +1 -1
- package/dist/src/runner.js +1 -1
- package/dist/src/runtime.js +2 -2
- package/dist/src/runtime10.js +2 -1
- package/dist/src/runtime2.js +4 -4
- package/dist/src/runtime4.js +5 -5
- package/dist/src/runtime5.js +1 -1
- package/dist/src/runtime6.js +1 -1
- package/dist/src/runtime7.js +1 -1
- package/dist/src/runtime8.js +1 -1
- package/dist/src/runtime9.js +1 -1
- package/dist/src/screen-recording-admission-ledger.js +1 -1
- package/dist/src/screen-recording-resource-recovery.js +1 -1
- package/dist/src/screen-recording-transport.js +1 -1
- package/dist/src/screenshot-density.js +1 -1
- package/dist/src/screenshot-overlay-draw.js +1 -0
- package/dist/src/screenshot-runtime.js +1 -1
- package/dist/src/script.js +1 -1
- package/dist/src/scroll-gesture.js +1 -1
- package/dist/src/sdk-batch.js +2 -2
- package/dist/src/sdk-contracts.d.ts +7 -5
- package/dist/src/sdk-device.js +1 -1
- package/dist/src/sdk-finders.js +1 -1
- package/dist/src/sdk-selectors.d.ts +44 -1
- package/dist/src/sdk-selectors.js +1 -1
- package/dist/src/selection.js +0 -0
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/selectors.js +1 -1
- package/dist/src/semantic-index.js +1 -1
- package/dist/src/session-routing.js +1 -1
- package/dist/src/session-snapshot.js +1 -1
- package/dist/src/session-store.js +3 -3
- package/dist/src/session2.js +3 -3
- package/dist/src/simctl.js +1 -1
- package/dist/src/simulator-boot.js +1 -1
- package/dist/src/snapshot-runtime-binding.js +1 -1
- package/dist/src/snapshot-runtime.js +1 -1
- package/dist/src/snapshot-state.js +1 -1
- package/dist/src/snapshot4.js +1 -1
- package/dist/src/src.js +1 -1
- package/dist/src/src7.js +1 -1
- package/dist/src/stop-sequence.js +1 -1
- package/dist/src/text-entry-focus.js +1 -1
- package/dist/src/tree.js +1 -1
- package/dist/src/type-text-runtime.js +1 -1
- package/dist/src/video.js +2 -2
- package/package.json +1 -1
- package/server.json +2 -2
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.8.apk.sha256 +0 -1
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.8.apk.sha256 +0 -1
|
Binary file
|
|
@@ -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.
|
|
4
|
-
"assetName": "agent-device-android-ime-helper-0.21.
|
|
5
|
-
"sha256": "
|
|
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":
|
|
7
|
+
"versionCode": 21009,
|
|
8
8
|
"serviceComponent": "com.callstack.agentdevice.imehelper/.TestInputMethodService",
|
|
9
9
|
"broadcastProtocol": "android-ime-helper-v1"
|
|
10
10
|
}
|
|
Binary file
|
|
@@ -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.
|
|
4
|
-
"releaseTag": "v0.21.
|
|
5
|
-
"assetName": "agent-device-android-snapshot-helper-0.21.
|
|
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": "
|
|
8
|
-
"checksumName": "agent-device-android-snapshot-helper-0.21.
|
|
7
|
+
"sha256": "c73eed5a190579623e2c6b5baa2b3f7e760a5d41d4630e05d970826007d4ae55",
|
|
8
|
+
"checksumName": "agent-device-android-snapshot-helper-0.21.9.apk.sha256",
|
|
9
9
|
"packageName": "com.callstack.agentdevice.snapshothelper",
|
|
10
|
-
"versionCode":
|
|
10
|
+
"versionCode": 21009,
|
|
11
11
|
"instrumentationRunner": "com.callstack.agentdevice.snapshothelper/.SnapshotInstrumentation",
|
|
12
12
|
"minSdk": 23,
|
|
13
13
|
"targetSdk": 36,
|
|
@@ -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
|
+
}
|
|
@@ -4,12 +4,7 @@ import ApplicationServices
|
|
|
4
4
|
import Darwin
|
|
5
5
|
import Foundation
|
|
6
6
|
|
|
7
|
-
///
|
|
8
|
-
/// are the only public seam onto the private channel that folds a simulator.
|
|
9
|
-
private let deviceHubBundleId = "com.apple.dt.Devices"
|
|
10
|
-
|
|
11
|
-
/// How long a reopen gets to restore a window, and a sidebar selection to switch the window's
|
|
12
|
-
/// device, before the press is refused.
|
|
7
|
+
/// How long sidebar selection and pose-control discovery may each wait before refusing a press.
|
|
13
8
|
private let deviceHubSettleDeadline: TimeInterval = 8
|
|
14
9
|
private let deviceHubPoll: TimeInterval = 0.25
|
|
15
10
|
|
|
@@ -51,16 +46,15 @@ func handleDeviceHub(arguments: [String]) throws -> any Encodable {
|
|
|
51
46
|
details: ["reason": "accessibility-permission", "permission": "accessibility"]
|
|
52
47
|
)
|
|
53
48
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
+
}
|
|
59
56
|
}
|
|
60
|
-
|
|
61
|
-
let appElement = AXUIElementCreateApplication(pid)
|
|
62
|
-
let (window, reopened) = try deviceHubWindow(in: appElement, pid: pid, deviceName: deviceName)
|
|
63
|
-
let selected = try selectDevice(udid: udid, deviceName: deviceName, in: window, appElement: appElement)
|
|
57
|
+
let selected = try selectDevice(row: discovery.row, deviceName: deviceName, in: window)
|
|
64
58
|
let windowTitle = stringAttribute(window, attribute: kAXTitleAttribute as String) ?? ""
|
|
65
59
|
guard let button = awaitPoseButton(in: window, description: control) else {
|
|
66
60
|
throw HelperError.commandFailed(
|
|
@@ -84,64 +78,12 @@ func handleDeviceHub(arguments: [String]) throws -> any Encodable {
|
|
|
84
78
|
pose: pose.rawValue,
|
|
85
79
|
control: control,
|
|
86
80
|
windowTitle: windowTitle,
|
|
87
|
-
reopened: reopened,
|
|
81
|
+
reopened: discovery.reopened,
|
|
88
82
|
selected: selected
|
|
89
83
|
)
|
|
90
84
|
)
|
|
91
85
|
}
|
|
92
86
|
|
|
93
|
-
/// Device Hub is launched through a trampoline, so LaunchServices registers it with no process
|
|
94
|
-
/// identifier (`NSRunningApplication.processIdentifier` is -1) and an accessibility element built
|
|
95
|
-
/// from that identifier is invalid. The process table is the only place its real pid appears.
|
|
96
|
-
private func deviceHubProcessIdentifier() -> pid_t? {
|
|
97
|
-
let executableSuffix = deviceHubExecutableSuffix
|
|
98
|
-
let byteCount = proc_listpids(UInt32(PROC_ALL_PIDS), 0, nil, 0)
|
|
99
|
-
guard byteCount > 0 else { return nil }
|
|
100
|
-
var pids = [pid_t](repeating: 0, count: Int(byteCount) / MemoryLayout<pid_t>.size + 64)
|
|
101
|
-
let filled = proc_listpids(
|
|
102
|
-
UInt32(PROC_ALL_PIDS), 0, &pids, Int32(pids.count * MemoryLayout<pid_t>.size)
|
|
103
|
-
)
|
|
104
|
-
guard filled > 0 else { return nil }
|
|
105
|
-
var path = [CChar](repeating: 0, count: 4096)
|
|
106
|
-
for pid in pids.prefix(Int(filled) / MemoryLayout<pid_t>.size) where pid > 0 {
|
|
107
|
-
guard proc_pidpath(pid, &path, UInt32(path.count)) > 0 else { continue }
|
|
108
|
-
if String(cString: path).hasSuffix(executableSuffix) {
|
|
109
|
-
return pid
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return nil
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/// A device window to drive: the one already showing this device when there is one, otherwise
|
|
116
|
-
/// any device window, since its sidebar can switch it to the device. A Device Hub with no window
|
|
117
|
-
/// at all — a simulator booted headlessly leaves it that way — is asked to reopen one.
|
|
118
|
-
private func deviceHubWindow(
|
|
119
|
-
in appElement: AXUIElement,
|
|
120
|
-
pid: pid_t,
|
|
121
|
-
deviceName: String
|
|
122
|
-
) throws -> (window: AXUIElement, reopened: Bool) {
|
|
123
|
-
if let window = preferredWindow(in: appElement, deviceName: deviceName) {
|
|
124
|
-
return (window, false)
|
|
125
|
-
}
|
|
126
|
-
try sendReopenEvent(to: pid)
|
|
127
|
-
let deadline = Date().addingTimeInterval(deviceHubSettleDeadline)
|
|
128
|
-
while Date() < deadline {
|
|
129
|
-
Thread.sleep(forTimeInterval: deviceHubPoll)
|
|
130
|
-
if let window = preferredWindow(in: appElement, deviceName: deviceName) {
|
|
131
|
-
return (window, true)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
throw HelperError.commandFailed(
|
|
135
|
-
"Device Hub shows no device window to drive",
|
|
136
|
-
details: ["reason": "device-hub-window-missing", "deviceName": deviceName]
|
|
137
|
-
)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
private func preferredWindow(in appElement: AXUIElement, deviceName: String) -> AXUIElement? {
|
|
141
|
-
let candidates = windows(of: appElement)
|
|
142
|
-
return candidates.first { windowShows(deviceName: deviceName, $0) } ?? candidates.first
|
|
143
|
-
}
|
|
144
|
-
|
|
145
87
|
private func windowShows(deviceName: String, _ window: AXUIElement) -> Bool {
|
|
146
88
|
return deviceHubWindowShows(
|
|
147
89
|
deviceName: deviceName,
|
|
@@ -152,7 +94,7 @@ private func windowShows(deviceName: String, _ window: AXUIElement) -> Bool {
|
|
|
152
94
|
/// `kAEReopenApplication` is what the Dock sends when an app with no open windows is clicked, and
|
|
153
95
|
/// it is the one event Device Hub answers by restoring the device window. Sent to the process
|
|
154
96
|
/// directly, because LaunchServices cannot address a trampolined app by bundle identifier.
|
|
155
|
-
|
|
97
|
+
func sendDeviceHubReopenEvent(to pid: pid_t, timeout: TimeInterval) throws {
|
|
156
98
|
let target = NSAppleEventDescriptor(processIdentifier: pid)
|
|
157
99
|
let event = NSAppleEventDescriptor(
|
|
158
100
|
eventClass: AEEventClass(kCoreEventClass),
|
|
@@ -162,7 +104,7 @@ private func sendReopenEvent(to pid: pid_t) throws {
|
|
|
162
104
|
transactionID: AETransactionID(kAnyTransactionID)
|
|
163
105
|
)
|
|
164
106
|
do {
|
|
165
|
-
_ = try event.sendEvent(options: [.noReply], timeout: 5)
|
|
107
|
+
_ = try event.sendEvent(options: [.noReply], timeout: min(5, max(0.001, timeout)))
|
|
166
108
|
} catch {
|
|
167
109
|
let code = (error as NSError).code
|
|
168
110
|
throw HelperError.commandFailed(
|
|
@@ -178,66 +120,43 @@ private func sendReopenEvent(to pid: pid_t) throws {
|
|
|
178
120
|
/// Switches the window to the device through its sidebar row, whose accessibility identifier is
|
|
179
121
|
/// `TableRow.Device.<udid>` — the one place Device Hub exposes a device identity that two
|
|
180
122
|
/// simulators sharing a name cannot confuse. A window already titled with the device still gets
|
|
181
|
-
/// the selection
|
|
123
|
+
/// the selection, because the title alone cannot tell such twins apart.
|
|
182
124
|
private func selectDevice(
|
|
183
|
-
|
|
125
|
+
row: AXUIElement,
|
|
184
126
|
deviceName: String,
|
|
185
|
-
in window: AXUIElement
|
|
186
|
-
appElement: AXUIElement
|
|
127
|
+
in window: AXUIElement
|
|
187
128
|
) throws -> Bool {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
shownSidebar = true
|
|
192
|
-
let deadline = Date().addingTimeInterval(deviceHubSettleDeadline)
|
|
193
|
-
while row == nil, Date() < deadline {
|
|
194
|
-
Thread.sleep(forTimeInterval: deviceHubPoll)
|
|
195
|
-
row = deviceRow(udid: udid, in: window)
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
defer {
|
|
199
|
-
if shownSidebar { _ = pressMenuItem(appElement: appElement, menu: "View", item: "Hide Sidebar") }
|
|
200
|
-
}
|
|
201
|
-
guard let row else {
|
|
202
|
-
if windowShows(deviceName: deviceName, window) {
|
|
203
|
-
return false
|
|
204
|
-
}
|
|
205
|
-
throw HelperError.commandFailed(
|
|
206
|
-
"Device Hub lists no device \(udid) in its sidebar",
|
|
207
|
-
details: ["reason": "device-hub-device-missing", "udid": udid, "deviceName": deviceName]
|
|
208
|
-
)
|
|
209
|
-
}
|
|
129
|
+
let deadline = ProcessInfo.processInfo.systemUptime + deviceHubSettleDeadline
|
|
130
|
+
AXUIElementSetMessagingTimeout(row, 0.25)
|
|
131
|
+
AXUIElementSetMessagingTimeout(window, 0.25)
|
|
210
132
|
let status = AXUIElementSetAttributeValue(row, kAXSelectedAttribute as CFString, kCFBooleanTrue)
|
|
211
|
-
guard status == .success else {
|
|
133
|
+
guard status == .success || status == .cannotComplete else {
|
|
212
134
|
throw HelperError.commandFailed(
|
|
213
135
|
"Device Hub refused to select \(deviceName) in its sidebar",
|
|
214
136
|
details: ["reason": "device-hub-select-failed", "status": "\(status.rawValue)"]
|
|
215
137
|
)
|
|
216
138
|
}
|
|
217
|
-
|
|
218
|
-
|
|
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 }
|
|
219
143
|
Thread.sleep(forTimeInterval: deviceHubPoll)
|
|
220
144
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
"reason": "device-hub-select-unconfirmed",
|
|
226
|
-
"windowTitle": stringAttribute(window, attribute: kAXTitleAttribute as String) ?? "",
|
|
227
|
-
]
|
|
228
|
-
)
|
|
229
|
-
}
|
|
230
|
-
return true
|
|
145
|
+
throw HelperError.commandFailed(
|
|
146
|
+
"Device Hub did not confirm selection of \(deviceName)",
|
|
147
|
+
details: ["reason": "device-hub-select-unconfirmed", "status": "\(status.rawValue)"]
|
|
148
|
+
)
|
|
231
149
|
}
|
|
232
150
|
|
|
233
|
-
|
|
234
|
-
guard let label =
|
|
151
|
+
func deviceHubDeviceRow(udid: String, in window: AXUIElement, deadline: TimeInterval) -> AXUIElement? {
|
|
152
|
+
guard let label = findDeviceHubElement(root: window, depth: 0, deadline: deadline, where: {
|
|
235
153
|
stringAttribute($0, attribute: "AXIdentifier") == deviceHubDeviceRowIdentifier(udid: udid)
|
|
236
154
|
}) else {
|
|
237
155
|
return nil
|
|
238
156
|
}
|
|
239
157
|
var current: AXUIElement? = label
|
|
240
|
-
while let element = current {
|
|
158
|
+
while let element = current, ProcessInfo.processInfo.systemUptime < deadline {
|
|
159
|
+
AXUIElementSetMessagingTimeout(element, Float(max(0.001, min(0.25, deadline - ProcessInfo.processInfo.systemUptime))))
|
|
241
160
|
if stringAttribute(element, attribute: kAXRoleAttribute as String) == "AXRow" {
|
|
242
161
|
return element
|
|
243
162
|
}
|
|
@@ -246,36 +165,16 @@ private func deviceRow(udid: String, in window: AXUIElement) -> AXUIElement? {
|
|
|
246
165
|
return nil
|
|
247
166
|
}
|
|
248
167
|
|
|
249
|
-
private func showSidebar(appElement: AXUIElement) -> Bool {
|
|
250
|
-
return pressMenuItem(appElement: appElement, menu: "View", item: "Show Sidebar")
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
private func pressMenuItem(appElement: AXUIElement, menu: String, item: String) -> Bool {
|
|
254
|
-
guard let menuBar = elementAttribute(appElement, attribute: kAXMenuBarAttribute as String) else {
|
|
255
|
-
return false
|
|
256
|
-
}
|
|
257
|
-
for menuBarItem in children(of: menuBar)
|
|
258
|
-
where stringAttribute(menuBarItem, attribute: kAXTitleAttribute as String) == menu {
|
|
259
|
-
for submenu in children(of: menuBarItem) {
|
|
260
|
-
for menuItem in children(of: submenu)
|
|
261
|
-
where stringAttribute(menuItem, attribute: kAXTitleAttribute as String) == item {
|
|
262
|
-
return AXUIElementPerformAction(menuItem, kAXPressAction as CFString) == .success
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
return false
|
|
267
|
-
}
|
|
268
|
-
|
|
269
168
|
/// The action bar is rebuilt for the device the window shows, so right after a sidebar selection
|
|
270
169
|
/// the window already carries the new title while the pose controls are still being laid out.
|
|
271
170
|
/// The controls are therefore awaited, not looked up once.
|
|
272
171
|
private func awaitPoseButton(in window: AXUIElement, description: String) -> AXUIElement? {
|
|
273
|
-
let deadline =
|
|
172
|
+
let deadline = ProcessInfo.processInfo.systemUptime + deviceHubSettleDeadline
|
|
274
173
|
while true {
|
|
275
|
-
if let button = poseButton(in: window, description: description) {
|
|
174
|
+
if let button = poseButton(in: window, description: description, deadline: deadline) {
|
|
276
175
|
return button
|
|
277
176
|
}
|
|
278
|
-
guard
|
|
177
|
+
guard ProcessInfo.processInfo.systemUptime < deadline else { return nil }
|
|
279
178
|
Thread.sleep(forTimeInterval: deviceHubPoll)
|
|
280
179
|
}
|
|
281
180
|
}
|
|
@@ -283,29 +182,35 @@ private func awaitPoseButton(in window: AXUIElement, description: String) -> AXU
|
|
|
283
182
|
/// The pose controls sit in the window's action bar as `AXButton`s described by their preset
|
|
284
183
|
/// name; the simulated screen inside the same window is an iOS content group whose own buttons
|
|
285
184
|
/// carry app labels, never these three.
|
|
286
|
-
private func poseButton(in window: AXUIElement, description: String) -> AXUIElement? {
|
|
287
|
-
return
|
|
185
|
+
private func poseButton(in window: AXUIElement, description: String, deadline: TimeInterval) -> AXUIElement? {
|
|
186
|
+
return findDeviceHubElement(root: window, depth: 0, deadline: deadline) {
|
|
288
187
|
stringAttribute($0, attribute: kAXRoleAttribute as String) == "AXButton"
|
|
289
188
|
&& stringAttribute($0, attribute: kAXDescriptionAttribute as String) == description
|
|
290
189
|
}
|
|
291
190
|
}
|
|
292
191
|
|
|
293
|
-
|
|
192
|
+
func findDeviceHubElement(
|
|
294
193
|
root: AXUIElement,
|
|
295
194
|
depth: Int,
|
|
195
|
+
deadline: TimeInterval,
|
|
296
196
|
where matches: (AXUIElement) -> Bool
|
|
297
197
|
) -> AXUIElement? {
|
|
298
|
-
|
|
198
|
+
let remaining = deadline - ProcessInfo.processInfo.systemUptime
|
|
199
|
+
if depth > 14 || remaining <= 0 {
|
|
299
200
|
return nil
|
|
300
201
|
}
|
|
202
|
+
AXUIElementSetMessagingTimeout(root, Float(min(0.25, remaining)))
|
|
301
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)))
|
|
302
207
|
if matches(child) {
|
|
303
208
|
return child
|
|
304
209
|
}
|
|
305
210
|
if stringAttribute(child, attribute: kAXSubroleAttribute as String) == "iOSContentGroup" {
|
|
306
211
|
continue
|
|
307
212
|
}
|
|
308
|
-
if let nested =
|
|
213
|
+
if let nested = findDeviceHubElement(root: child, depth: depth + 1, deadline: deadline, where: matches) {
|
|
309
214
|
return nested
|
|
310
215
|
}
|
|
311
216
|
}
|
|
@@ -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
|
+
}
|
package/apple/macos-helper/Tests/AgentDeviceMacOSDeviceHubTests/DeviceHubWindowInventoryTests.swift
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import ApplicationServices
|
|
2
|
+
import XCTest
|
|
3
|
+
@testable import AgentDeviceMacOSDeviceHub
|
|
4
|
+
|
|
5
|
+
final class DeviceHubWindowInventoryTests: XCTestCase {
|
|
6
|
+
func testLaterWindowKeepsItsProcessAndDoesNotPreventEmptyProcessReopen() {
|
|
7
|
+
var inventory = DeviceHubWindowInventory()
|
|
8
|
+
let window = AXUIElementCreateApplication(20)
|
|
9
|
+
inventory.record(processID: 10, read: .available([]))
|
|
10
|
+
inventory.record(processID: 20, read: .available([window]))
|
|
11
|
+
XCTAssertEqual(inventory.candidates.map(\.processID), [20])
|
|
12
|
+
XCTAssertTrue(CFEqual(inventory.candidates[0].window, window))
|
|
13
|
+
inventory.record(candidate: 0, outcome: .sidebarUnavailable)
|
|
14
|
+
XCTAssertEqual(inventory.processesToReopen(attempted: []), [10])
|
|
15
|
+
XCTAssertEqual(inventory.failureReason(), "device-hub-identity-unconfirmed")
|
|
16
|
+
XCTAssertEqual(inventory.candidateOutcomes, ["20:0:sidebar-unavailable"])
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func testFailedReadsAreNotReopenedOrMisreportedAsMissingWindows() {
|
|
20
|
+
var inventory = DeviceHubWindowInventory()
|
|
21
|
+
inventory.record(processID: 10, read: .failed(-25204))
|
|
22
|
+
inventory.record(processID: 20, read: .available([]))
|
|
23
|
+
XCTAssertEqual(inventory.processesToReopen(attempted: []), [20])
|
|
24
|
+
XCTAssertEqual(inventory.failures, [10: -25204])
|
|
25
|
+
XCTAssertEqual(inventory.failureReason(), "device-hub-window-read-failed")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func testSuccessfulEmptyReadIsMissingAndReopenIsAttemptedOnlyOnce() {
|
|
29
|
+
var inventory = DeviceHubWindowInventory()
|
|
30
|
+
inventory.record(processID: 10, read: .available([]))
|
|
31
|
+
XCTAssertEqual(inventory.failureReason(), "device-hub-window-missing")
|
|
32
|
+
XCTAssertEqual(inventory.processesToReopen(attempted: []), [10])
|
|
33
|
+
XCTAssertEqual(inventory.processesToReopen(attempted: [10]), [])
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func testSharedBudgetExhaustionStopsReopenWithoutClaimingMissingDevice() {
|
|
37
|
+
var inventory = DeviceHubWindowInventory()
|
|
38
|
+
inventory.record(processID: 10, read: .available([]))
|
|
39
|
+
inventory.expireBudget()
|
|
40
|
+
XCTAssertEqual(inventory.processesToReopen(attempted: []), [])
|
|
41
|
+
XCTAssertEqual(inventory.failureReason(), "device-hub-discovery-timeout")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func testCandidateOutcomesRetainUncertaintyAndOwningProcess() {
|
|
45
|
+
var inventory = DeviceHubWindowInventory()
|
|
46
|
+
inventory.record(processID: 10, read: .available([AXUIElementCreateApplication(10)]))
|
|
47
|
+
inventory.record(processID: 20, read: .available([AXUIElementCreateApplication(20)]))
|
|
48
|
+
inventory.record(candidate: 0, outcome: .timedOut)
|
|
49
|
+
inventory.record(candidate: 1, outcome: .unconfirmed)
|
|
50
|
+
XCTAssertEqual(inventory.candidateOutcomes, ["10:0:timed-out", "20:1:row-unconfirmed"])
|
|
51
|
+
XCTAssertEqual(inventory.failureReason(), "device-hub-identity-unconfirmed")
|
|
52
|
+
}
|
|
53
|
+
func testUnrelatedReopenFailureDoesNotMaskCandidateUncertainty() {
|
|
54
|
+
var inventory = DeviceHubWindowInventory()
|
|
55
|
+
inventory.record(processID: 10, read: .available([]))
|
|
56
|
+
let failures: [Int32: String] = [10: "automation-permission"]
|
|
57
|
+
XCTAssertEqual(inventory.failureReason(reopenFailures: failures), "automation-permission")
|
|
58
|
+
inventory.record(processID: 20, read: .available([AXUIElementCreateApplication(20)]))
|
|
59
|
+
inventory.record(candidate: 0, outcome: .timedOut)
|
|
60
|
+
XCTAssertEqual(inventory.failureReason(reopenFailures: failures), "device-hub-identity-unconfirmed")
|
|
61
|
+
inventory.expireBudget()
|
|
62
|
+
XCTAssertEqual(inventory.failureReason(reopenFailures: failures), "device-hub-discovery-timeout")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
}
|