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.
- 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
- 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.7.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.7.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.7.manifest.json → agent-device-android-snapshot-helper-0.21.9.manifest.json} +6 -6
- package/apple/macos-helper/Package.swift +16 -1
- package/apple/macos-helper/Sources/AgentDeviceMacOSDeviceHub/DeviceHubPose.swift +47 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSDeviceHub/DeviceHubWindowInventory.swift +65 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubPose.swift +225 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubSidebar.swift +57 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubWindowDiscovery.swift +108 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/main.swift +66 -13
- package/apple/macos-helper/Sources/AgentDeviceMacOSInput/MouseClickDelivery.swift +116 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSInput/MouseClickSchedule.swift +61 -0
- package/apple/macos-helper/Tests/AgentDeviceMacOSDeviceHubTests/DeviceHubPoseTests.swift +30 -0
- package/apple/macos-helper/Tests/AgentDeviceMacOSDeviceHubTests/DeviceHubWindowInventoryTests.swift +65 -0
- package/apple/macos-helper/Tests/AgentDeviceMacOSInputTests/MouseClickScheduleTests.swift +58 -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 +2 -2
- 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 +15 -11
- package/dist/src/cli.js +4 -4
- package/dist/src/client-types.d.ts +51 -1
- package/dist/src/command-tools.js +3 -3
- package/dist/src/config.js +1 -1
- 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-rotation.js +1 -1
- package/dist/src/device-selection-resolver.js +1 -1
- package/dist/src/device-session.js +1 -1
- package/dist/src/device.js +1 -1
- package/dist/src/device2.js +1 -1
- package/dist/src/devicectl.js +1 -1
- package/dist/src/dispatch-resolve.js +1 -1
- package/dist/src/display-inventory.js +1 -1
- package/dist/src/doctor-facade.js +1 -1
- package/dist/src/durable-resource.js +1 -1
- package/dist/src/exec.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/gesture-runtime.js +1 -1
- package/dist/src/harmonyos.js +1 -1
- package/dist/src/helper.js +2 -2
- package/dist/src/host.js +1 -0
- 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-binding.js +1 -0
- package/dist/src/interactor-operation-catalog.js +1 -1
- package/dist/src/interactor.js +4 -5
- package/dist/src/interactor2.js +1 -1
- 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/internal/png-worker.d.ts +5 -0
- package/dist/src/internal/png-worker.js +1 -1
- 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/linux.js +1 -1
- package/dist/src/local-interactor-operation-set.js +1 -1
- package/dist/src/location-coordinates.js +1 -1
- package/dist/src/macos-facade.js +1 -1
- package/dist/src/managed-device-scope.js +1 -1
- package/dist/src/mechanics.js +17 -16
- 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/physical-device-control.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/png-worker-client.js +1 -1
- 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 +1 -1
- 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 +3 -0
- package/dist/src/runner-dev-tools-security.js +1 -0
- package/dist/src/runner-device-readiness.js +1 -0
- 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-image.js +1 -0
- 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-contracts.js +2 -2
- package/dist/src/sdk-device.js +1 -1
- package/dist/src/sdk-finders.js +1 -1
- package/dist/src/sdk-selectors.d.ts +58 -6
- 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/simulator-facade.js +1 -1
- package/dist/src/simulator.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/src8.js +1 -1
- package/dist/src/stop-sequence.js +1 -1
- package/dist/src/text-entry-focus.js +1 -1
- package/dist/src/touch-runtime.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/dist/src/web2.js +1 -1
- package/package.json +3 -1
- package/server.json +2 -2
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.7.apk.sha256 +0 -1
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.7.apk.sha256 +0 -1
- package/dist/src/png-transcode.js +0 -1
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
import AgentDeviceSnapshotPresentation
|
|
3
|
+
|
|
4
|
+
extension RunnerTests {
|
|
5
|
+
func interactiveRootNode(rect: CGRect) -> RawAXNode {
|
|
6
|
+
RawAXNode(
|
|
7
|
+
index: 0,
|
|
8
|
+
type: "Application",
|
|
9
|
+
label: nil,
|
|
10
|
+
identifier: nil,
|
|
11
|
+
value: nil,
|
|
12
|
+
rect: SnapshotRect(rect),
|
|
13
|
+
enabled: true,
|
|
14
|
+
focused: nil,
|
|
15
|
+
selected: nil,
|
|
16
|
+
hittable: false,
|
|
17
|
+
depth: 0,
|
|
18
|
+
parentIndex: nil,
|
|
19
|
+
hiddenContentAbove: nil,
|
|
20
|
+
hiddenContentBelow: nil
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func interactiveRootFrame(for candidates: [RawAXNode]) -> CGRect {
|
|
25
|
+
guard !candidates.isEmpty else {
|
|
26
|
+
return .zero
|
|
27
|
+
}
|
|
28
|
+
let maxX = candidates.map { CGFloat($0.rect.x + $0.rect.width) }.max() ?? 0
|
|
29
|
+
let maxY = candidates.map { CGFloat($0.rect.y + $0.rect.height) }.max() ?? 0
|
|
30
|
+
return CGRect(x: 0, y: 0, width: max(1, maxX), height: max(1, maxY))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
func makeSnapshotTraversalContext(
|
|
36
|
+
app: XCUIApplication,
|
|
37
|
+
hint: CaptureHint,
|
|
38
|
+
captureDeadline: Date = .distantFuture,
|
|
39
|
+
treeCaptureSliceBudgetOverride: TimeInterval? = nil
|
|
40
|
+
) throws -> SnapshotTraversalContext? {
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
let geometry = try runMainThreadWork(
|
|
45
|
+
"snapshot_viewport",
|
|
46
|
+
timeout: min(1.0, max(0.1, captureDeadline.timeIntervalSinceNow)),
|
|
47
|
+
timeoutError: snapshotMainThreadTimeoutError("preparing tree snapshot")
|
|
48
|
+
) {
|
|
49
|
+
(
|
|
50
|
+
viewport: self.safeSnapshotViewport(app: app),
|
|
51
|
+
interfaceOrientation: self.capturedInterfaceOrientation(app: app)
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
let viewport = geometry.viewport
|
|
55
|
+
let interfaceOrientation = geometry.interfaceOrientation
|
|
56
|
+
let treeSliceBudget = treeCaptureSliceBudgetOverride ?? treeCaptureSliceBudget
|
|
57
|
+
let slice = min(treeSliceBudget, max(0.5, captureDeadline.timeIntervalSinceNow))
|
|
58
|
+
guard let rootSnapshot = try captureSnapshotRootBounded(app, sliceSeconds: slice) else {
|
|
59
|
+
return nil
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
let keyboardBand = captureKeyboardBandFact(app: app, deadline: captureDeadline)
|
|
68
|
+
|
|
69
|
+
return SnapshotTraversalContext(
|
|
70
|
+
queryRoot: app,
|
|
71
|
+
rootSnapshot: rootSnapshot,
|
|
72
|
+
viewport: viewport,
|
|
73
|
+
interfaceOrientation: interfaceOrientation,
|
|
74
|
+
keyboardBand: keyboardBand
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static let xCTestSnapshotTimeoutCode = "IOS_TREE_CAPTURE_TIMEOUT"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
private func captureSnapshotRootBounded(
|
|
87
|
+
_ element: XCUIElement,
|
|
88
|
+
sliceSeconds: TimeInterval
|
|
89
|
+
) throws -> XCUIElementSnapshot? {
|
|
90
|
+
if Thread.isMainThread {
|
|
91
|
+
return try captureSnapshotRoot(element)
|
|
92
|
+
}
|
|
93
|
+
return try runMainThreadWork(
|
|
94
|
+
"tree_capture",
|
|
95
|
+
timeout: sliceSeconds,
|
|
96
|
+
timeoutError: treeCaptureTimeoutError(sliceSeconds: sliceSeconds)
|
|
97
|
+
) {
|
|
98
|
+
try self.captureSnapshotRoot(element)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private func treeCaptureTimeoutError(sliceSeconds: TimeInterval) -> () -> Error {
|
|
103
|
+
{
|
|
104
|
+
SnapshotCaptureFailure(
|
|
105
|
+
code: Self.xCTestSnapshotTimeoutCode,
|
|
106
|
+
message: "the XCTest tree capture exceeded its \(Int(sliceSeconds))s time slice",
|
|
107
|
+
hint: "The capture plan will avoid or tightly bound XCTest-backed snapshot tiers on this screen."
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
func snapshotMainThreadTimeoutError(_ operation: String) -> () -> Error {
|
|
113
|
+
{
|
|
114
|
+
SnapshotCaptureFailure(
|
|
115
|
+
code: Self.xCTestSnapshotTimeoutCode,
|
|
116
|
+
message: "timed out while \(operation) on the XCTest main thread",
|
|
117
|
+
hint: "The capture plan will skip XCTest-backed snapshot tiers while the previous main-thread work drains."
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private func captureSnapshotRoot(_ element: XCUIElement) throws -> XCUIElementSnapshot? {
|
|
123
|
+
var rootSnapshot: XCUIElementSnapshot?
|
|
124
|
+
var swiftErrorMessage: String?
|
|
125
|
+
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
126
|
+
do {
|
|
127
|
+
rootSnapshot = try element.snapshot()
|
|
128
|
+
} catch {
|
|
129
|
+
swiftErrorMessage = describeSnapshotError(error)
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
if let rootSnapshot {
|
|
134
|
+
return rootSnapshot
|
|
135
|
+
}
|
|
136
|
+
let message = exceptionMessage ?? swiftErrorMessage ?? "snapshot returned no root"
|
|
137
|
+
if Self.isAxIllegalArgument(message) {
|
|
138
|
+
throw axSnapshotFailure(message)
|
|
139
|
+
}
|
|
140
|
+
return nil
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func safeSnapshotViewport(app: XCUIApplication) -> CGRect {
|
|
144
|
+
safely("SNAPSHOT_VIEWPORT", CGRect.infinite) { snapshotViewport(app: app) }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private func describeSnapshotError(_ error: Error) -> String {
|
|
148
|
+
let localized = error.localizedDescription
|
|
149
|
+
let debug = String(describing: error)
|
|
150
|
+
if localized.isEmpty { return debug }
|
|
151
|
+
if debug == localized { return localized }
|
|
152
|
+
return "\(localized) (\(debug))"
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private func axSnapshotFailure(_ message: String) -> SnapshotCaptureFailure {
|
|
156
|
+
let detail = message.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
157
|
+
let failureMessage: String
|
|
158
|
+
if detail.isEmpty {
|
|
159
|
+
failureMessage = Self.axSnapshotFailureMessage
|
|
160
|
+
} else {
|
|
161
|
+
failureMessage = "\(Self.axSnapshotFailureMessage) \(detail)"
|
|
162
|
+
}
|
|
163
|
+
return SnapshotCaptureFailure(
|
|
164
|
+
code: Self.axSnapshotErrorCode,
|
|
165
|
+
message: failureMessage,
|
|
166
|
+
hint: Self.axSnapshotHint
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private static func isAxIllegalArgument(_ message: String) -> Bool {
|
|
171
|
+
let normalized = message.lowercased()
|
|
172
|
+
return normalized.contains("kaxerrorillegalargument")
|
|
173
|
+
|| (normalized.contains("illegal argument") && normalized.contains("snapshot"))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
static func isAxSnapshotFailure(_ failure: SnapshotCaptureFailure) -> Bool {
|
|
177
|
+
failure.code == Self.axSnapshotErrorCode || isAxIllegalArgument(failure.message)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
func evaluateSnapshot(_ snapshot: XCUIElementSnapshot) -> SnapshotEvaluation {
|
|
181
|
+
let label = aggregatedLabel(for: snapshot) ?? snapshot.label.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
182
|
+
let identifier = snapshot.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
183
|
+
let valueText = snapshotValueText(snapshot)
|
|
184
|
+
return SnapshotEvaluation(
|
|
185
|
+
label: label,
|
|
186
|
+
identifier: identifier,
|
|
187
|
+
valueText: valueText,
|
|
188
|
+
focused: snapshotHasFocus(snapshot),
|
|
189
|
+
selected: snapshotIsSelected(snapshot)
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
func makeSnapshotNode(
|
|
194
|
+
snapshot: XCUIElementSnapshot,
|
|
195
|
+
evaluation: SnapshotEvaluation,
|
|
196
|
+
depth: Int,
|
|
197
|
+
index: Int,
|
|
198
|
+
parentIndex: Int?
|
|
199
|
+
) -> RawAXNode {
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
return RawAXNode(
|
|
203
|
+
index: index,
|
|
204
|
+
type: elementTypeName(snapshot.elementType),
|
|
205
|
+
label: evaluation.label.isEmpty ? nil : evaluation.label,
|
|
206
|
+
identifier: evaluation.identifier.isEmpty ? nil : evaluation.identifier,
|
|
207
|
+
value: evaluation.valueText,
|
|
208
|
+
rect: SnapshotRect(snapshot.frame),
|
|
209
|
+
enabled: snapshot.isEnabled,
|
|
210
|
+
focused: evaluation.focused ? true : nil,
|
|
211
|
+
selected: evaluation.selected ? true : nil,
|
|
212
|
+
hittable: false,
|
|
213
|
+
depth: depth,
|
|
214
|
+
parentIndex: parentIndex,
|
|
215
|
+
hiddenContentAbove: nil,
|
|
216
|
+
hiddenContentBelow: nil
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
func capturedInterfaceOrientation(app: XCUIApplication) -> Int {
|
|
223
|
+
safely("SNAPSHOT_INTERFACE_ORIENTATION", RunnerInterfaceOrientation.unknown) {
|
|
224
|
+
Int(RunnerSynthesizedGesture.interfaceOrientation(forApplication: app))
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private func snapshotValueText(_ snapshot: XCUIElementSnapshot) -> String? {
|
|
229
|
+
guard let value = snapshot.value else { return nil }
|
|
230
|
+
let text = String(describing: value).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
231
|
+
return text.isEmpty ? nil : text
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private func snapshotViewport(app: XCUIApplication) -> CGRect {
|
|
235
|
+
#if os(iOS)
|
|
236
|
+
let appFrame = onScreenWindowFrame(app: app)
|
|
237
|
+
#else
|
|
238
|
+
let appFrame = app.frame
|
|
239
|
+
#endif
|
|
240
|
+
if !appFrame.isNull && !appFrame.isEmpty {
|
|
241
|
+
return appFrame
|
|
242
|
+
}
|
|
243
|
+
return .infinite
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
static func snapshotTraversalIdentity(
|
|
247
|
+
elementType: XCUIElement.ElementType,
|
|
248
|
+
label: String,
|
|
249
|
+
identifier: String,
|
|
250
|
+
frame: CGRect
|
|
251
|
+
) -> String {
|
|
252
|
+
#if os(iOS)
|
|
253
|
+
"\(elementType)-\(label)-\(identifier)-\(frame.origin.x)-\(frame.origin.y)-\(frame.width)-\(frame.height)"
|
|
254
|
+
#else
|
|
255
|
+
return "\(elementType)-\(label)-\(identifier)-\(frame.origin.x)-\(frame.origin.y)"
|
|
256
|
+
#endif
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private func aggregatedLabel(for snapshot: XCUIElementSnapshot, depth: Int = 0) -> String? {
|
|
260
|
+
if depth > 4 { return nil }
|
|
261
|
+
let text = snapshot.label.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
262
|
+
if !text.isEmpty { return text }
|
|
263
|
+
if let valueText = snapshotValueText(snapshot) { return valueText }
|
|
264
|
+
for child in snapshot.children {
|
|
265
|
+
if let childLabel = aggregatedLabel(for: child, depth: depth + 1) {
|
|
266
|
+
return childLabel
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return nil
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
func appendCollapsedTabFallbackNodes(
|
|
273
|
+
to nodes: inout [RawAXNode],
|
|
274
|
+
containerSnapshot: XCUIElementSnapshot,
|
|
275
|
+
resolveElements: () -> [XCUIElement],
|
|
276
|
+
depth: Int,
|
|
277
|
+
parentIndex: Int
|
|
278
|
+
) {
|
|
279
|
+
let fallbackNodes = collapsedTabFallbackNodes(
|
|
280
|
+
for: containerSnapshot,
|
|
281
|
+
resolveElements: resolveElements,
|
|
282
|
+
startingIndex: nodes.count,
|
|
283
|
+
depth: depth,
|
|
284
|
+
parentIndex: parentIndex
|
|
285
|
+
)
|
|
286
|
+
nodes.append(contentsOf: fallbackNodes)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private func collapsedTabFallbackNodes(
|
|
290
|
+
for containerSnapshot: XCUIElementSnapshot,
|
|
291
|
+
resolveElements: () -> [XCUIElement],
|
|
292
|
+
startingIndex: Int,
|
|
293
|
+
depth: Int,
|
|
294
|
+
parentIndex: Int
|
|
295
|
+
) -> [RawAXNode] {
|
|
296
|
+
if !containerSnapshot.children.isEmpty { return [] }
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
guard shouldExpandCollapsedTabContainer(containerSnapshot) else { return [] }
|
|
302
|
+
let containerFrame = containerSnapshot.frame
|
|
303
|
+
if containerFrame.isNull || containerFrame.isEmpty { return [] }
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
let elements = resolveElements()
|
|
308
|
+
let candidates = elements.compactMap { element in
|
|
309
|
+
collapsedTabCandidateNode(
|
|
310
|
+
element: element,
|
|
311
|
+
containerSnapshot: containerSnapshot,
|
|
312
|
+
containerFrame: containerFrame
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
.sorted { left, right in
|
|
316
|
+
if left.rect.x != right.rect.x {
|
|
317
|
+
return left.rect.x < right.rect.x
|
|
318
|
+
}
|
|
319
|
+
return left.rect.y < right.rect.y
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if candidates.count < 2 { return [] }
|
|
323
|
+
let rowMidpoints = candidates.map { $0.rect.y + ($0.rect.height / 2) }
|
|
324
|
+
let rowSpread = (rowMidpoints.max() ?? 0) - (rowMidpoints.min() ?? 0)
|
|
325
|
+
|
|
326
|
+
if rowSpread > max(24.0, Double(containerFrame.height) * 0.6) { return [] }
|
|
327
|
+
|
|
328
|
+
var seen = Set<String>()
|
|
329
|
+
let uniqueCandidates = candidates.filter { node in
|
|
330
|
+
let key = "\(node.type)-\(node.label ?? "")-\(node.identifier ?? "")-\(node.value ?? "")-\(node.rect.x)-\(node.rect.y)-\(node.rect.width)-\(node.rect.height)"
|
|
331
|
+
if seen.contains(key) { return false }
|
|
332
|
+
seen.insert(key)
|
|
333
|
+
return true
|
|
334
|
+
}
|
|
335
|
+
if uniqueCandidates.count < 2 { return [] }
|
|
336
|
+
|
|
337
|
+
return uniqueCandidates.enumerated().map { offset, node in
|
|
338
|
+
RawAXNode(
|
|
339
|
+
index: startingIndex + offset,
|
|
340
|
+
type: node.type,
|
|
341
|
+
label: node.label,
|
|
342
|
+
identifier: node.identifier,
|
|
343
|
+
value: node.value,
|
|
344
|
+
rect: node.rect,
|
|
345
|
+
enabled: node.enabled,
|
|
346
|
+
focused: node.focused,
|
|
347
|
+
selected: node.selected,
|
|
348
|
+
hittable: node.hittable,
|
|
349
|
+
depth: depth,
|
|
350
|
+
parentIndex: parentIndex,
|
|
351
|
+
hiddenContentAbove: nil,
|
|
352
|
+
hiddenContentBelow: nil
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private func collapsedTabCandidateNode(
|
|
358
|
+
element: XCUIElement,
|
|
359
|
+
containerSnapshot: XCUIElementSnapshot,
|
|
360
|
+
containerFrame: CGRect
|
|
361
|
+
) -> RawAXNode? {
|
|
362
|
+
var node: RawAXNode?
|
|
363
|
+
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
364
|
+
if !element.exists { return }
|
|
365
|
+
let elementType = element.elementType
|
|
366
|
+
if !Self.collapsedTabCandidateTypes.contains(elementType) { return }
|
|
367
|
+
let frame = element.frame
|
|
368
|
+
if frame.isNull || frame.isEmpty { return }
|
|
369
|
+
if frame.equalTo(containerFrame) { return }
|
|
370
|
+
let area = max(CGFloat(1), frame.width * frame.height)
|
|
371
|
+
let containerArea = max(CGFloat(1), containerFrame.width * containerFrame.height)
|
|
372
|
+
if area >= containerArea * 0.9 { return }
|
|
373
|
+
let center = CGPoint(x: frame.midX, y: frame.midY)
|
|
374
|
+
if !containerFrame.contains(center) { return }
|
|
375
|
+
|
|
376
|
+
let label = element.label.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
377
|
+
let identifier = element.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
378
|
+
let valueText = snapshotValueText(element)
|
|
379
|
+
let hasContent = !label.isEmpty || !identifier.isEmpty || valueText != nil
|
|
380
|
+
if !hasContent { return }
|
|
381
|
+
if sameSemanticElement(
|
|
382
|
+
containerSnapshot: containerSnapshot,
|
|
383
|
+
elementType: elementType,
|
|
384
|
+
label: label,
|
|
385
|
+
identifier: identifier
|
|
386
|
+
) {
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
node = RawAXNode(
|
|
393
|
+
index: 0,
|
|
394
|
+
type: elementTypeName(elementType),
|
|
395
|
+
label: label.isEmpty ? nil : label,
|
|
396
|
+
identifier: identifier.isEmpty ? nil : identifier,
|
|
397
|
+
value: valueText,
|
|
398
|
+
rect: SnapshotRect(frame),
|
|
399
|
+
enabled: element.isEnabled,
|
|
400
|
+
focused: elementHasFocus(element) ? true : nil,
|
|
401
|
+
selected: element.isSelected ? true : nil,
|
|
402
|
+
hittable: false,
|
|
403
|
+
depth: 0,
|
|
404
|
+
parentIndex: nil,
|
|
405
|
+
hiddenContentAbove: nil,
|
|
406
|
+
hiddenContentBelow: nil
|
|
407
|
+
)
|
|
408
|
+
})
|
|
409
|
+
if let exceptionMessage {
|
|
410
|
+
NSLog(
|
|
411
|
+
"AGENT_DEVICE_RUNNER_SNAPSHOT_TAB_FALLBACK_IGNORED_EXCEPTION=%@",
|
|
412
|
+
exceptionMessage
|
|
413
|
+
)
|
|
414
|
+
return nil
|
|
415
|
+
}
|
|
416
|
+
return node
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private func snapshotHasFocus(_ snapshot: XCUIElementSnapshot) -> Bool {
|
|
420
|
+
var focused = false
|
|
421
|
+
_ = RunnerObjCExceptionCatcher.catchException({
|
|
422
|
+
if let value = (snapshot as! NSObject).value(forKey: "hasFocus") as? Bool {
|
|
423
|
+
focused = value
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
return focused
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
private func snapshotIsSelected(_ snapshot: XCUIElementSnapshot) -> Bool {
|
|
430
|
+
return snapshot.isSelected
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private func shouldExpandCollapsedTabContainer(_ snapshot: XCUIElementSnapshot) -> Bool {
|
|
434
|
+
let frame = snapshot.frame
|
|
435
|
+
if frame.isNull || frame.isEmpty { return false }
|
|
436
|
+
if frame.width < max(CGFloat(160), frame.height * 1.75) { return false }
|
|
437
|
+
switch snapshot.elementType {
|
|
438
|
+
case .tabBar, .segmentedControl, .slider:
|
|
439
|
+
return true
|
|
440
|
+
default:
|
|
441
|
+
return false
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private func snapshotValueText(_ element: XCUIElement) -> String? {
|
|
446
|
+
let text = String(describing: element.value ?? "")
|
|
447
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
448
|
+
return text.isEmpty ? nil : text
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private func sameSemanticElement(
|
|
452
|
+
containerSnapshot: XCUIElementSnapshot,
|
|
453
|
+
elementType: XCUIElement.ElementType,
|
|
454
|
+
label: String,
|
|
455
|
+
identifier: String
|
|
456
|
+
) -> Bool {
|
|
457
|
+
if containerSnapshot.elementType != elementType { return false }
|
|
458
|
+
let containerLabel = containerSnapshot.label.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
459
|
+
let containerIdentifier = containerSnapshot.identifier
|
|
460
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
461
|
+
return containerLabel == label && containerIdentifier == identifier
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
func flatInteractiveElements(
|
|
465
|
+
app: XCUIApplication,
|
|
466
|
+
deadline: Date
|
|
467
|
+
) -> (elements: [XCUIElement], truncated: Bool) {
|
|
468
|
+
let queries: [XCUIElementQuery] = [
|
|
469
|
+
app.buttons,
|
|
470
|
+
app.links,
|
|
471
|
+
app.textFields,
|
|
472
|
+
app.secureTextFields,
|
|
473
|
+
app.searchFields,
|
|
474
|
+
app.textViews,
|
|
475
|
+
app.switches,
|
|
476
|
+
app.sliders,
|
|
477
|
+
app.segmentedControls,
|
|
478
|
+
app.cells,
|
|
479
|
+
app.collectionViews,
|
|
480
|
+
app.tables,
|
|
481
|
+
app.scrollViews,
|
|
482
|
+
app.pickers,
|
|
483
|
+
app.steppers,
|
|
484
|
+
app.tabBars,
|
|
485
|
+
app.menuItems,
|
|
486
|
+
app.staticTexts,
|
|
487
|
+
app.images
|
|
488
|
+
]
|
|
489
|
+
|
|
490
|
+
var elements: [XCUIElement] = []
|
|
491
|
+
var truncated = false
|
|
492
|
+
for query in queries {
|
|
493
|
+
if Date() >= deadline {
|
|
494
|
+
NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_FLAT_FALLBACK_DEADLINE")
|
|
495
|
+
truncated = true
|
|
496
|
+
break
|
|
497
|
+
}
|
|
498
|
+
let result = snapshotElementsQuery {
|
|
499
|
+
query.allElementsBoundByIndex
|
|
500
|
+
}
|
|
501
|
+
elements.append(contentsOf: result.elements)
|
|
502
|
+
if result.axUnavailable {
|
|
503
|
+
break
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return (elements, truncated)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
func snapshotElementsQuery(
|
|
510
|
+
_ fetch: () -> [XCUIElement]
|
|
511
|
+
) -> (elements: [XCUIElement], axUnavailable: Bool) {
|
|
512
|
+
let (elements, exceptionMessage) = catchingObjCException(fallback: [], fetch)
|
|
513
|
+
guard let exceptionMessage else {
|
|
514
|
+
return (elements, false)
|
|
515
|
+
}
|
|
516
|
+
NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_QUERY_IGNORED_EXCEPTION=%@", exceptionMessage)
|
|
517
|
+
if Self.isAxIllegalArgument(exceptionMessage) {
|
|
518
|
+
invalidateCachedTarget(reason: "ax_snapshot_query_unavailable")
|
|
519
|
+
return ([], true)
|
|
520
|
+
}
|
|
521
|
+
return ([], false)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
func flatSnapshotNode(
|
|
525
|
+
element: XCUIElement,
|
|
526
|
+
index: Int,
|
|
527
|
+
parentIndex: Int?
|
|
528
|
+
) -> RawAXNode? {
|
|
529
|
+
var node: RawAXNode?
|
|
530
|
+
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
531
|
+
if !element.exists { return }
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
let frame = element.frame
|
|
535
|
+
if frame.isNull || frame.isEmpty { return }
|
|
536
|
+
let label = element.label.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
537
|
+
let identifier = element.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
538
|
+
let valueText = snapshotValueText(element)
|
|
539
|
+
let elementType = element.elementType
|
|
540
|
+
let enabled = element.isEnabled
|
|
541
|
+
|
|
542
|
+
node = RawAXNode(
|
|
543
|
+
index: index,
|
|
544
|
+
type: elementTypeName(elementType),
|
|
545
|
+
label: label.isEmpty ? nil : label,
|
|
546
|
+
identifier: identifier.isEmpty ? nil : identifier,
|
|
547
|
+
value: valueText,
|
|
548
|
+
rect: SnapshotRect(frame),
|
|
549
|
+
enabled: enabled,
|
|
550
|
+
focused: elementHasFocus(element) ? true : nil,
|
|
551
|
+
selected: element.isSelected ? true : nil,
|
|
552
|
+
hittable: false,
|
|
553
|
+
depth: 1,
|
|
554
|
+
parentIndex: parentIndex,
|
|
555
|
+
hiddenContentAbove: nil,
|
|
556
|
+
hiddenContentBelow: nil
|
|
557
|
+
)
|
|
558
|
+
})
|
|
559
|
+
if let exceptionMessage {
|
|
560
|
+
NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_FLAT_IGNORED_EXCEPTION=%@", exceptionMessage)
|
|
561
|
+
return nil
|
|
562
|
+
}
|
|
563
|
+
return node
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
}
|
|
@@ -59,6 +59,9 @@ struct SynthesizedGesturePolicy: Equatable, Hashable {
|
|
|
59
59
|
|
|
60
60
|
struct SynthesizedCoordinateContext {
|
|
61
61
|
let referenceFrame: CGRect
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
let resolvedWindow: XCUIElement
|
|
62
65
|
let keyboardPolicy: SynthesizedKeyboardPolicy
|
|
63
66
|
let fallbackPolicy: SynthesizedFallbackPolicy
|
|
64
67
|
let accessibilityHealth: RunnerAccessibilityHealth
|
|
@@ -66,6 +69,7 @@ struct SynthesizedCoordinateContext {
|
|
|
66
69
|
func withReferenceFrame(_ frame: CGRect) -> SynthesizedCoordinateContext {
|
|
67
70
|
SynthesizedCoordinateContext(
|
|
68
71
|
referenceFrame: frame,
|
|
72
|
+
resolvedWindow: resolvedWindow,
|
|
69
73
|
keyboardPolicy: keyboardPolicy,
|
|
70
74
|
fallbackPolicy: fallbackPolicy,
|
|
71
75
|
accessibilityHealth: accessibilityHealth
|
|
@@ -144,7 +148,7 @@ extension RunnerTests {
|
|
|
144
148
|
return
|
|
145
149
|
}
|
|
146
150
|
NSLog(
|
|
147
|
-
"AGENT_DEVICE_RUNNER_SYNTHESIZED_GESTURE_POLICY kind=%@ axHealth=%@ frameSource=
|
|
151
|
+
"AGENT_DEVICE_RUNNER_SYNTHESIZED_GESTURE_POLICY kind=%@ axHealth=%@ frameSource=window keyboardPolicy=%@ fallbackPolicy=%@ fallbackAllowed=%@ fallbackAttempted=%@",
|
|
148
152
|
kind.rawValue,
|
|
149
153
|
context.accessibilityHealth.rawValue,
|
|
150
154
|
context.keyboardPolicy.rawValue,
|