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.
Files changed (171) hide show
  1. 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
  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.8.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.8.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.8.manifest.json → agent-device-android-snapshot-helper-0.21.9.manifest.json} +6 -6
  7. package/apple/macos-helper/Sources/AgentDeviceMacOSDeviceHub/DeviceHubWindowInventory.swift +65 -0
  8. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubPose.swift +45 -140
  9. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubSidebar.swift +57 -0
  10. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubWindowDiscovery.swift +108 -0
  11. package/apple/macos-helper/Tests/AgentDeviceMacOSDeviceHubTests/DeviceHubWindowInventoryTests.swift +65 -0
  12. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +1 -0
  13. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAppScreenCapture.swift +175 -0
  14. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +10 -2
  15. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +42 -15
  16. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +8 -0
  17. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +45 -22
  18. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +39 -734
  19. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Keyboard.swift +20 -4
  20. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +63 -1
  21. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +13 -0
  22. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +25 -10
  23. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +5 -562
  24. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotAcquisition.swift +566 -0
  25. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +5 -1
  26. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedInteraction.swift +594 -0
  27. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.h +35 -1
  28. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.m +160 -0
  29. package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/RecordingExportSupport.swift +3 -0
  30. package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/recording-frames.swift +203 -0
  31. package/dist/apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotCoordinateSpace.swift +5 -0
  32. package/dist/src/absence-observation-errors.js +1 -1
  33. package/dist/src/adapter.js +1 -1
  34. package/dist/src/agent-browser-lifecycle.js +1 -1
  35. package/dist/src/agent-browser-process-record.js +1 -1
  36. package/dist/src/agent-device-client.js +1 -1
  37. package/dist/src/alert-runtime.js +1 -1
  38. package/dist/src/android-adb.js +1 -1
  39. package/dist/src/android-input-ownership.js +1 -1
  40. package/dist/src/android.js +1 -1
  41. package/dist/src/app-launch.js +1 -1
  42. package/dist/src/app-log-runtime2.js +3 -3
  43. package/dist/src/app-resolution.js +2 -2
  44. package/dist/src/apple-runner-platform.js +1 -1
  45. package/dist/src/application-lifecycle-interaction.js +1 -1
  46. package/dist/src/artifact-paths.js +1 -0
  47. package/dist/src/back-mode.js +1 -1
  48. package/dist/src/cli-config.js +1 -1
  49. package/dist/src/cli-help.js +8 -7
  50. package/dist/src/cli.js +4 -4
  51. package/dist/src/client-types.d.ts +23 -5
  52. package/dist/src/command-tools.js +3 -3
  53. package/dist/src/connection-runtime.js +1 -1
  54. package/dist/src/device-claim-inspection.js +1 -1
  55. package/dist/src/device-claim-rule.js +2 -2
  56. package/dist/src/device-input-state.d.ts +1 -1
  57. package/dist/src/device-selection-resolver.js +1 -1
  58. package/dist/src/device.js +1 -1
  59. package/dist/src/device2.js +1 -1
  60. package/dist/src/dispatch-resolve.js +1 -1
  61. package/dist/src/doctor-facade.js +1 -1
  62. package/dist/src/durable-resource.js +1 -1
  63. package/dist/src/find.js +1 -1
  64. package/dist/src/finders.js +1 -1
  65. package/dist/src/fold-runtime.js +1 -0
  66. package/dist/src/gesture-admission.js +1 -1
  67. package/dist/src/interaction-positionals.js +1 -1
  68. package/dist/src/interaction-ref-policy.js +1 -1
  69. package/dist/src/interaction.js +1 -1
  70. package/dist/src/interaction2.js +1 -1
  71. package/dist/src/interactor-operation-catalog.js +1 -1
  72. package/dist/src/interactor.js +4 -4
  73. package/dist/src/interactors.js +1 -1
  74. package/dist/src/internal/bin.js +1 -1
  75. package/dist/src/internal/daemon.js +2 -2
  76. package/dist/src/inventory2.js +1 -1
  77. package/dist/src/ios-snapshot-acquisition.js +1 -1
  78. package/dist/src/ios-snapshot-runtime.js +1 -1
  79. package/dist/src/lease.js +1 -1
  80. package/dist/src/local-interactor-operation-set.js +1 -1
  81. package/dist/src/location-coordinates.js +1 -1
  82. package/dist/src/managed-device-scope.js +1 -1
  83. package/dist/src/mechanics.js +17 -17
  84. package/dist/src/mobile-snapshot-semantics.js +1 -1
  85. package/dist/src/mp4-duration.js +1 -0
  86. package/dist/src/multitouch-support.js +1 -1
  87. package/dist/src/owner-identity.js +1 -1
  88. package/dist/src/parameterized-recorded-fill.js +1 -1
  89. package/dist/src/perf-facade.js +2 -2
  90. package/dist/src/perf-runtime-plan.js +1 -1
  91. package/dist/src/platform-request-scope.js +1 -1
  92. package/dist/src/platform-runtime-managed-owner.js +1 -1
  93. package/dist/src/platform-runtime-open-target.js +1 -1
  94. package/dist/src/platform-runtime-operation-host.js +2 -2
  95. package/dist/src/platform-runtime-runtime-hints.js +1 -1
  96. package/dist/src/platform-runtime-screen-recording-finalizer-host.js +1 -1
  97. package/dist/src/platform-runtime-unavailable.js +1 -1
  98. package/dist/src/platform-runtime.js +1 -1
  99. package/dist/src/platform-runtime2.js +1 -1
  100. package/dist/src/png-crop-bytes.js +1 -1
  101. package/dist/src/png-encode.js +1 -0
  102. package/dist/src/png-size.js +1 -0
  103. package/dist/src/provider-webdriver.js +1 -1
  104. package/dist/src/quality-warnings.js +1 -1
  105. package/dist/src/react-native.js +1 -1
  106. package/dist/src/record-runtime.js +1 -1
  107. package/dist/src/recording-export-quality.js +1 -1
  108. package/dist/src/recording-scope.js +1 -0
  109. package/dist/src/recording.js +3 -0
  110. package/dist/src/registry.js +35 -35
  111. package/dist/src/replay.js +2 -2
  112. package/dist/src/request-admission.js +1 -1
  113. package/dist/src/request-runtime-binding.js +1 -1
  114. package/dist/src/result-serialization.js +1 -1
  115. package/dist/src/runner-cache-metadata.js +2 -2
  116. package/dist/src/runner-client.js +1 -1
  117. package/dist/src/runner-contract.js +1 -1
  118. package/dist/src/runner-dev-tools-security.js +1 -1
  119. package/dist/src/runner-device-readiness.js +1 -1
  120. package/dist/src/runner-disposal.js +1 -1
  121. package/dist/src/runner-operations-facade.js +1 -1
  122. package/dist/src/runner-presentation.js +1 -1
  123. package/dist/src/runner.js +1 -1
  124. package/dist/src/runtime.js +2 -2
  125. package/dist/src/runtime10.js +2 -1
  126. package/dist/src/runtime2.js +4 -4
  127. package/dist/src/runtime4.js +5 -5
  128. package/dist/src/runtime5.js +1 -1
  129. package/dist/src/runtime6.js +1 -1
  130. package/dist/src/runtime7.js +1 -1
  131. package/dist/src/runtime8.js +1 -1
  132. package/dist/src/runtime9.js +1 -1
  133. package/dist/src/screen-recording-admission-ledger.js +1 -1
  134. package/dist/src/screen-recording-resource-recovery.js +1 -1
  135. package/dist/src/screen-recording-transport.js +1 -1
  136. package/dist/src/screenshot-density.js +1 -1
  137. package/dist/src/screenshot-overlay-draw.js +1 -0
  138. package/dist/src/screenshot-runtime.js +1 -1
  139. package/dist/src/script.js +1 -1
  140. package/dist/src/scroll-gesture.js +1 -1
  141. package/dist/src/sdk-batch.js +2 -2
  142. package/dist/src/sdk-contracts.d.ts +7 -5
  143. package/dist/src/sdk-device.js +1 -1
  144. package/dist/src/sdk-finders.js +1 -1
  145. package/dist/src/sdk-selectors.d.ts +44 -1
  146. package/dist/src/sdk-selectors.js +1 -1
  147. package/dist/src/selection.js +0 -0
  148. package/dist/src/selector-runtime.js +1 -1
  149. package/dist/src/selectors.js +1 -1
  150. package/dist/src/semantic-index.js +1 -1
  151. package/dist/src/session-routing.js +1 -1
  152. package/dist/src/session-snapshot.js +1 -1
  153. package/dist/src/session-store.js +3 -3
  154. package/dist/src/session2.js +3 -3
  155. package/dist/src/simctl.js +1 -1
  156. package/dist/src/simulator-boot.js +1 -1
  157. package/dist/src/snapshot-runtime-binding.js +1 -1
  158. package/dist/src/snapshot-runtime.js +1 -1
  159. package/dist/src/snapshot-state.js +1 -1
  160. package/dist/src/snapshot4.js +1 -1
  161. package/dist/src/src.js +1 -1
  162. package/dist/src/src7.js +1 -1
  163. package/dist/src/stop-sequence.js +1 -1
  164. package/dist/src/text-entry-focus.js +1 -1
  165. package/dist/src/tree.js +1 -1
  166. package/dist/src/type-text-runtime.js +1 -1
  167. package/dist/src/video.js +2 -2
  168. package/package.json +1 -1
  169. package/server.json +2 -2
  170. package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.8.apk.sha256 +0 -1
  171. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.8.apk.sha256 +0 -1
@@ -2,11 +2,11 @@ import XCTest
2
2
  import AgentDeviceSnapshotPresentation
3
3
 
4
4
  extension RunnerTests {
5
- private static let axSnapshotErrorCode = "IOS_AX_SNAPSHOT_FAILED"
6
- private static let axSnapshotFailureMessage =
5
+ static let axSnapshotErrorCode = "IOS_AX_SNAPSHOT_FAILED"
6
+ static let axSnapshotFailureMessage =
7
7
  "iOS XCTest snapshot failed while serializing the accessibility tree."
8
8
  private static let axSnapshotUnavailableReason = "ax_snapshot_unavailable"
9
- private static let axSnapshotHint =
9
+ static let axSnapshotHint =
10
10
  "Snapshot state is unavailable because XCTest could not serialize this iOS accessibility tree. This can be specific to the current screen. Use plain screenshot, not screenshot --overlay-refs, as visual truth; navigate with coordinate commands if needed; then retry snapshot -i after reaching another screen. If you own the app and need full-tree inspection, simplify this screen's accessibility tree and expose stable ids on actionable controls."
11
11
  private static let rawSnapshotTooLargeCode = "IOS_RAW_SNAPSHOT_TOO_LARGE"
12
12
  private static let rawSnapshotMaxNodes = 5_000
@@ -32,7 +32,7 @@ extension RunnerTests {
32
32
  let keyboardBand: RunnerKeyboardBandFact?
33
33
  }
34
34
 
35
- private struct SnapshotEvaluation {
35
+ struct SnapshotEvaluation {
36
36
  let label: String
37
37
  let identifier: String
38
38
  let valueText: String?
@@ -117,7 +117,7 @@ extension RunnerTests {
117
117
  "ScrollView"
118
118
  ]
119
119
 
120
- private static let collapsedTabCandidateTypes: Set<XCUIElement.ElementType> = [
120
+ static let collapsedTabCandidateTypes: Set<XCUIElement.ElementType> = [
121
121
  .button,
122
122
  .link,
123
123
  .menuItem,
@@ -799,562 +799,5 @@ extension RunnerTests {
799
799
 
800
800
 
801
801
 
802
-
803
- private func interactiveRootNode(rect: CGRect) -> RawAXNode {
804
- RawAXNode(
805
- index: 0,
806
- type: "Application",
807
- label: nil,
808
- identifier: nil,
809
- value: nil,
810
- rect: SnapshotRect(rect),
811
- enabled: true,
812
- focused: nil,
813
- selected: nil,
814
- hittable: false,
815
- depth: 0,
816
- parentIndex: nil,
817
- hiddenContentAbove: nil,
818
- hiddenContentBelow: nil
819
- )
820
- }
821
-
822
- private func interactiveRootFrame(for candidates: [RawAXNode]) -> CGRect {
823
- guard !candidates.isEmpty else {
824
- return .zero
825
- }
826
- let maxX = candidates.map { CGFloat($0.rect.x + $0.rect.width) }.max() ?? 0
827
- let maxY = candidates.map { CGFloat($0.rect.y + $0.rect.height) }.max() ?? 0
828
- return CGRect(x: 0, y: 0, width: max(1, maxX), height: max(1, maxY))
829
- }
830
-
831
-
832
-
833
- func makeSnapshotTraversalContext(
834
- app: XCUIApplication,
835
- hint: CaptureHint,
836
- captureDeadline: Date = .distantFuture,
837
- treeCaptureSliceBudgetOverride: TimeInterval? = nil
838
- ) throws -> SnapshotTraversalContext? {
839
-
840
-
841
-
842
- let geometry = try runMainThreadWork(
843
- "snapshot_viewport",
844
- timeout: min(1.0, max(0.1, captureDeadline.timeIntervalSinceNow)),
845
- timeoutError: snapshotMainThreadTimeoutError("preparing tree snapshot")
846
- ) {
847
- (
848
- viewport: self.safeSnapshotViewport(app: app),
849
- interfaceOrientation: self.capturedInterfaceOrientation(app: app)
850
- )
851
- }
852
- let viewport = geometry.viewport
853
- let interfaceOrientation = geometry.interfaceOrientation
854
- let treeSliceBudget = treeCaptureSliceBudgetOverride ?? treeCaptureSliceBudget
855
- let slice = min(treeSliceBudget, max(0.5, captureDeadline.timeIntervalSinceNow))
856
- guard let rootSnapshot = try captureSnapshotRootBounded(app, sliceSeconds: slice) else {
857
- return nil
858
- }
859
-
860
-
861
-
862
-
863
-
864
-
865
- let keyboardBand = captureKeyboardBandFact(app: app, deadline: captureDeadline)
866
-
867
- return SnapshotTraversalContext(
868
- queryRoot: app,
869
- rootSnapshot: rootSnapshot,
870
- viewport: viewport,
871
- interfaceOrientation: interfaceOrientation,
872
- keyboardBand: keyboardBand
873
- )
874
- }
875
-
876
- static let xCTestSnapshotTimeoutCode = "IOS_TREE_CAPTURE_TIMEOUT"
877
-
878
-
879
-
880
-
881
-
882
-
883
-
884
- private func captureSnapshotRootBounded(
885
- _ element: XCUIElement,
886
- sliceSeconds: TimeInterval
887
- ) throws -> XCUIElementSnapshot? {
888
- if Thread.isMainThread {
889
- return try captureSnapshotRoot(element)
890
- }
891
- return try runMainThreadWork(
892
- "tree_capture",
893
- timeout: sliceSeconds,
894
- timeoutError: treeCaptureTimeoutError(sliceSeconds: sliceSeconds)
895
- ) {
896
- try self.captureSnapshotRoot(element)
897
- }
898
- }
899
-
900
- private func treeCaptureTimeoutError(sliceSeconds: TimeInterval) -> () -> Error {
901
- {
902
- SnapshotCaptureFailure(
903
- code: Self.xCTestSnapshotTimeoutCode,
904
- message: "the XCTest tree capture exceeded its \(Int(sliceSeconds))s time slice",
905
- hint: "The capture plan will avoid or tightly bound XCTest-backed snapshot tiers on this screen."
906
- )
907
- }
908
- }
909
-
910
- func snapshotMainThreadTimeoutError(_ operation: String) -> () -> Error {
911
- {
912
- SnapshotCaptureFailure(
913
- code: Self.xCTestSnapshotTimeoutCode,
914
- message: "timed out while \(operation) on the XCTest main thread",
915
- hint: "The capture plan will skip XCTest-backed snapshot tiers while the previous main-thread work drains."
916
- )
917
- }
918
- }
919
-
920
- private func captureSnapshotRoot(_ element: XCUIElement) throws -> XCUIElementSnapshot? {
921
- var rootSnapshot: XCUIElementSnapshot?
922
- var swiftErrorMessage: String?
923
- let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
924
- do {
925
- rootSnapshot = try element.snapshot()
926
- } catch {
927
- swiftErrorMessage = describeSnapshotError(error)
928
- }
929
- })
930
-
931
- if let rootSnapshot {
932
- return rootSnapshot
933
- }
934
- let message = exceptionMessage ?? swiftErrorMessage ?? "snapshot returned no root"
935
- if Self.isAxIllegalArgument(message) {
936
- throw axSnapshotFailure(message)
937
- }
938
- return nil
939
- }
940
-
941
- func safeSnapshotViewport(app: XCUIApplication) -> CGRect {
942
- safely("SNAPSHOT_VIEWPORT", CGRect.infinite) { snapshotViewport(app: app) }
943
- }
944
-
945
- private func describeSnapshotError(_ error: Error) -> String {
946
- let localized = error.localizedDescription
947
- let debug = String(describing: error)
948
- if localized.isEmpty { return debug }
949
- if debug == localized { return localized }
950
- return "\(localized) (\(debug))"
951
- }
952
-
953
- private func axSnapshotFailure(_ message: String) -> SnapshotCaptureFailure {
954
- let detail = message.trimmingCharacters(in: .whitespacesAndNewlines)
955
- let failureMessage: String
956
- if detail.isEmpty {
957
- failureMessage = Self.axSnapshotFailureMessage
958
- } else {
959
- failureMessage = "\(Self.axSnapshotFailureMessage) \(detail)"
960
- }
961
- return SnapshotCaptureFailure(
962
- code: Self.axSnapshotErrorCode,
963
- message: failureMessage,
964
- hint: Self.axSnapshotHint
965
- )
966
- }
967
-
968
- private static func isAxIllegalArgument(_ message: String) -> Bool {
969
- let normalized = message.lowercased()
970
- return normalized.contains("kaxerrorillegalargument")
971
- || (normalized.contains("illegal argument") && normalized.contains("snapshot"))
972
- }
973
-
974
- static func isAxSnapshotFailure(_ failure: SnapshotCaptureFailure) -> Bool {
975
- failure.code == Self.axSnapshotErrorCode || isAxIllegalArgument(failure.message)
976
- }
977
-
978
- private func evaluateSnapshot(_ snapshot: XCUIElementSnapshot) -> SnapshotEvaluation {
979
- let label = aggregatedLabel(for: snapshot) ?? snapshot.label.trimmingCharacters(in: .whitespacesAndNewlines)
980
- let identifier = snapshot.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
981
- let valueText = snapshotValueText(snapshot)
982
- return SnapshotEvaluation(
983
- label: label,
984
- identifier: identifier,
985
- valueText: valueText,
986
- focused: snapshotHasFocus(snapshot),
987
- selected: snapshotIsSelected(snapshot)
988
- )
989
- }
990
-
991
- private func makeSnapshotNode(
992
- snapshot: XCUIElementSnapshot,
993
- evaluation: SnapshotEvaluation,
994
- depth: Int,
995
- index: Int,
996
- parentIndex: Int?
997
- ) -> RawAXNode {
998
-
999
-
1000
- return RawAXNode(
1001
- index: index,
1002
- type: elementTypeName(snapshot.elementType),
1003
- label: evaluation.label.isEmpty ? nil : evaluation.label,
1004
- identifier: evaluation.identifier.isEmpty ? nil : evaluation.identifier,
1005
- value: evaluation.valueText,
1006
- rect: SnapshotRect(snapshot.frame),
1007
- enabled: snapshot.isEnabled,
1008
- focused: evaluation.focused ? true : nil,
1009
- selected: evaluation.selected ? true : nil,
1010
- hittable: false,
1011
- depth: depth,
1012
- parentIndex: parentIndex,
1013
- hiddenContentAbove: nil,
1014
- hiddenContentBelow: nil
1015
- )
1016
- }
1017
-
1018
-
1019
-
1020
- func capturedInterfaceOrientation(app: XCUIApplication) -> Int {
1021
- safely("SNAPSHOT_INTERFACE_ORIENTATION", RunnerInterfaceOrientation.unknown) {
1022
- Int(RunnerSynthesizedGesture.interfaceOrientation(forApplication: app))
1023
- }
1024
- }
1025
-
1026
- private func snapshotValueText(_ snapshot: XCUIElementSnapshot) -> String? {
1027
- guard let value = snapshot.value else { return nil }
1028
- let text = String(describing: value).trimmingCharacters(in: .whitespacesAndNewlines)
1029
- return text.isEmpty ? nil : text
1030
- }
1031
-
1032
- private func snapshotViewport(app: XCUIApplication) -> CGRect {
1033
- let appFrame = app.frame
1034
- if !appFrame.isNull && !appFrame.isEmpty {
1035
- return appFrame
1036
- }
1037
- return .infinite
1038
- }
1039
-
1040
- static func snapshotTraversalIdentity(
1041
- elementType: XCUIElement.ElementType,
1042
- label: String,
1043
- identifier: String,
1044
- frame: CGRect
1045
- ) -> String {
1046
- #if os(iOS)
1047
- "\(elementType)-\(label)-\(identifier)-\(frame.origin.x)-\(frame.origin.y)-\(frame.width)-\(frame.height)"
1048
- #else
1049
- return "\(elementType)-\(label)-\(identifier)-\(frame.origin.x)-\(frame.origin.y)"
1050
- #endif
1051
- }
1052
-
1053
- private func aggregatedLabel(for snapshot: XCUIElementSnapshot, depth: Int = 0) -> String? {
1054
- if depth > 4 { return nil }
1055
- let text = snapshot.label.trimmingCharacters(in: .whitespacesAndNewlines)
1056
- if !text.isEmpty { return text }
1057
- if let valueText = snapshotValueText(snapshot) { return valueText }
1058
- for child in snapshot.children {
1059
- if let childLabel = aggregatedLabel(for: child, depth: depth + 1) {
1060
- return childLabel
1061
- }
1062
- }
1063
- return nil
1064
- }
1065
-
1066
- private func appendCollapsedTabFallbackNodes(
1067
- to nodes: inout [RawAXNode],
1068
- containerSnapshot: XCUIElementSnapshot,
1069
- resolveElements: () -> [XCUIElement],
1070
- depth: Int,
1071
- parentIndex: Int
1072
- ) {
1073
- let fallbackNodes = collapsedTabFallbackNodes(
1074
- for: containerSnapshot,
1075
- resolveElements: resolveElements,
1076
- startingIndex: nodes.count,
1077
- depth: depth,
1078
- parentIndex: parentIndex
1079
- )
1080
- nodes.append(contentsOf: fallbackNodes)
1081
- }
1082
-
1083
- private func collapsedTabFallbackNodes(
1084
- for containerSnapshot: XCUIElementSnapshot,
1085
- resolveElements: () -> [XCUIElement],
1086
- startingIndex: Int,
1087
- depth: Int,
1088
- parentIndex: Int
1089
- ) -> [RawAXNode] {
1090
- if !containerSnapshot.children.isEmpty { return [] }
1091
-
1092
-
1093
-
1094
-
1095
- guard shouldExpandCollapsedTabContainer(containerSnapshot) else { return [] }
1096
- let containerFrame = containerSnapshot.frame
1097
- if containerFrame.isNull || containerFrame.isEmpty { return [] }
1098
-
1099
-
1100
-
1101
- let elements = resolveElements()
1102
- let candidates = elements.compactMap { element in
1103
- collapsedTabCandidateNode(
1104
- element: element,
1105
- containerSnapshot: containerSnapshot,
1106
- containerFrame: containerFrame
1107
- )
1108
- }
1109
- .sorted { left, right in
1110
- if left.rect.x != right.rect.x {
1111
- return left.rect.x < right.rect.x
1112
- }
1113
- return left.rect.y < right.rect.y
1114
- }
1115
-
1116
- if candidates.count < 2 { return [] }
1117
- let rowMidpoints = candidates.map { $0.rect.y + ($0.rect.height / 2) }
1118
- let rowSpread = (rowMidpoints.max() ?? 0) - (rowMidpoints.min() ?? 0)
1119
-
1120
- if rowSpread > max(24.0, Double(containerFrame.height) * 0.6) { return [] }
1121
-
1122
- var seen = Set<String>()
1123
- let uniqueCandidates = candidates.filter { node in
1124
- let key = "\(node.type)-\(node.label ?? "")-\(node.identifier ?? "")-\(node.value ?? "")-\(node.rect.x)-\(node.rect.y)-\(node.rect.width)-\(node.rect.height)"
1125
- if seen.contains(key) { return false }
1126
- seen.insert(key)
1127
- return true
1128
- }
1129
- if uniqueCandidates.count < 2 { return [] }
1130
-
1131
- return uniqueCandidates.enumerated().map { offset, node in
1132
- RawAXNode(
1133
- index: startingIndex + offset,
1134
- type: node.type,
1135
- label: node.label,
1136
- identifier: node.identifier,
1137
- value: node.value,
1138
- rect: node.rect,
1139
- enabled: node.enabled,
1140
- focused: node.focused,
1141
- selected: node.selected,
1142
- hittable: node.hittable,
1143
- depth: depth,
1144
- parentIndex: parentIndex,
1145
- hiddenContentAbove: nil,
1146
- hiddenContentBelow: nil
1147
- )
1148
- }
1149
- }
1150
-
1151
- private func collapsedTabCandidateNode(
1152
- element: XCUIElement,
1153
- containerSnapshot: XCUIElementSnapshot,
1154
- containerFrame: CGRect
1155
- ) -> RawAXNode? {
1156
- var node: RawAXNode?
1157
- let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
1158
- if !element.exists { return }
1159
- let elementType = element.elementType
1160
- if !Self.collapsedTabCandidateTypes.contains(elementType) { return }
1161
- let frame = element.frame
1162
- if frame.isNull || frame.isEmpty { return }
1163
- if frame.equalTo(containerFrame) { return }
1164
- let area = max(CGFloat(1), frame.width * frame.height)
1165
- let containerArea = max(CGFloat(1), containerFrame.width * containerFrame.height)
1166
- if area >= containerArea * 0.9 { return }
1167
- let center = CGPoint(x: frame.midX, y: frame.midY)
1168
- if !containerFrame.contains(center) { return }
1169
-
1170
- let label = element.label.trimmingCharacters(in: .whitespacesAndNewlines)
1171
- let identifier = element.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
1172
- let valueText = snapshotValueText(element)
1173
- let hasContent = !label.isEmpty || !identifier.isEmpty || valueText != nil
1174
- if !hasContent { return }
1175
- if sameSemanticElement(
1176
- containerSnapshot: containerSnapshot,
1177
- elementType: elementType,
1178
- label: label,
1179
- identifier: identifier
1180
- ) {
1181
- return
1182
- }
1183
-
1184
-
1185
-
1186
- node = RawAXNode(
1187
- index: 0,
1188
- type: elementTypeName(elementType),
1189
- label: label.isEmpty ? nil : label,
1190
- identifier: identifier.isEmpty ? nil : identifier,
1191
- value: valueText,
1192
- rect: SnapshotRect(frame),
1193
- enabled: element.isEnabled,
1194
- focused: elementHasFocus(element) ? true : nil,
1195
- selected: element.isSelected ? true : nil,
1196
- hittable: false,
1197
- depth: 0,
1198
- parentIndex: nil,
1199
- hiddenContentAbove: nil,
1200
- hiddenContentBelow: nil
1201
- )
1202
- })
1203
- if let exceptionMessage {
1204
- NSLog(
1205
- "AGENT_DEVICE_RUNNER_SNAPSHOT_TAB_FALLBACK_IGNORED_EXCEPTION=%@",
1206
- exceptionMessage
1207
- )
1208
- return nil
1209
- }
1210
- return node
1211
- }
1212
-
1213
- private func snapshotHasFocus(_ snapshot: XCUIElementSnapshot) -> Bool {
1214
- var focused = false
1215
- _ = RunnerObjCExceptionCatcher.catchException({
1216
- if let value = (snapshot as! NSObject).value(forKey: "hasFocus") as? Bool {
1217
- focused = value
1218
- }
1219
- })
1220
- return focused
1221
- }
1222
-
1223
- private func snapshotIsSelected(_ snapshot: XCUIElementSnapshot) -> Bool {
1224
- return snapshot.isSelected
1225
- }
1226
-
1227
- private func shouldExpandCollapsedTabContainer(_ snapshot: XCUIElementSnapshot) -> Bool {
1228
- let frame = snapshot.frame
1229
- if frame.isNull || frame.isEmpty { return false }
1230
- if frame.width < max(CGFloat(160), frame.height * 1.75) { return false }
1231
- switch snapshot.elementType {
1232
- case .tabBar, .segmentedControl, .slider:
1233
- return true
1234
- default:
1235
- return false
1236
- }
1237
- }
1238
-
1239
- private func snapshotValueText(_ element: XCUIElement) -> String? {
1240
- let text = String(describing: element.value ?? "")
1241
- .trimmingCharacters(in: .whitespacesAndNewlines)
1242
- return text.isEmpty ? nil : text
1243
- }
1244
-
1245
- private func sameSemanticElement(
1246
- containerSnapshot: XCUIElementSnapshot,
1247
- elementType: XCUIElement.ElementType,
1248
- label: String,
1249
- identifier: String
1250
- ) -> Bool {
1251
- if containerSnapshot.elementType != elementType { return false }
1252
- let containerLabel = containerSnapshot.label.trimmingCharacters(in: .whitespacesAndNewlines)
1253
- let containerIdentifier = containerSnapshot.identifier
1254
- .trimmingCharacters(in: .whitespacesAndNewlines)
1255
- return containerLabel == label && containerIdentifier == identifier
1256
- }
1257
-
1258
- private func flatInteractiveElements(
1259
- app: XCUIApplication,
1260
- deadline: Date
1261
- ) -> (elements: [XCUIElement], truncated: Bool) {
1262
- let queries: [XCUIElementQuery] = [
1263
- app.buttons,
1264
- app.links,
1265
- app.textFields,
1266
- app.secureTextFields,
1267
- app.searchFields,
1268
- app.textViews,
1269
- app.switches,
1270
- app.sliders,
1271
- app.segmentedControls,
1272
- app.cells,
1273
- app.collectionViews,
1274
- app.tables,
1275
- app.scrollViews,
1276
- app.pickers,
1277
- app.steppers,
1278
- app.tabBars,
1279
- app.menuItems,
1280
- app.staticTexts,
1281
- app.images
1282
- ]
1283
-
1284
- var elements: [XCUIElement] = []
1285
- var truncated = false
1286
- for query in queries {
1287
- if Date() >= deadline {
1288
- NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_FLAT_FALLBACK_DEADLINE")
1289
- truncated = true
1290
- break
1291
- }
1292
- let result = snapshotElementsQuery {
1293
- query.allElementsBoundByIndex
1294
- }
1295
- elements.append(contentsOf: result.elements)
1296
- if result.axUnavailable {
1297
- break
1298
- }
1299
- }
1300
- return (elements, truncated)
1301
- }
1302
-
1303
- private func snapshotElementsQuery(
1304
- _ fetch: () -> [XCUIElement]
1305
- ) -> (elements: [XCUIElement], axUnavailable: Bool) {
1306
- let (elements, exceptionMessage) = catchingObjCException(fallback: [], fetch)
1307
- guard let exceptionMessage else {
1308
- return (elements, false)
1309
- }
1310
- NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_QUERY_IGNORED_EXCEPTION=%@", exceptionMessage)
1311
- if Self.isAxIllegalArgument(exceptionMessage) {
1312
- invalidateCachedTarget(reason: "ax_snapshot_query_unavailable")
1313
- return ([], true)
1314
- }
1315
- return ([], false)
1316
- }
1317
-
1318
- private func flatSnapshotNode(
1319
- element: XCUIElement,
1320
- index: Int,
1321
- parentIndex: Int?
1322
- ) -> RawAXNode? {
1323
- var node: RawAXNode?
1324
- let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
1325
- if !element.exists { return }
1326
-
1327
-
1328
- let frame = element.frame
1329
- if frame.isNull || frame.isEmpty { return }
1330
- let label = element.label.trimmingCharacters(in: .whitespacesAndNewlines)
1331
- let identifier = element.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
1332
- let valueText = snapshotValueText(element)
1333
- let elementType = element.elementType
1334
- let enabled = element.isEnabled
1335
-
1336
- node = RawAXNode(
1337
- index: index,
1338
- type: elementTypeName(elementType),
1339
- label: label.isEmpty ? nil : label,
1340
- identifier: identifier.isEmpty ? nil : identifier,
1341
- value: valueText,
1342
- rect: SnapshotRect(frame),
1343
- enabled: enabled,
1344
- focused: elementHasFocus(element) ? true : nil,
1345
- selected: element.isSelected ? true : nil,
1346
- hittable: false,
1347
- depth: 1,
1348
- parentIndex: parentIndex,
1349
- hiddenContentAbove: nil,
1350
- hiddenContentBelow: nil
1351
- )
1352
- })
1353
- if let exceptionMessage {
1354
- NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_FLAT_IGNORED_EXCEPTION=%@", exceptionMessage)
1355
- return nil
1356
- }
1357
- return node
1358
- }
1359
802
 
1360
803
  }