agent-device 0.10.3 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,14 +17,9 @@ extension RunnerTests {
17
17
  let referenceHeight: Double
18
18
  }
19
19
 
20
- struct GestureReferenceFrame {
21
- let referenceWidth: Double
22
- let referenceHeight: Double
23
- }
24
-
25
20
  // MARK: - Navigation Gestures
26
21
 
27
- func tapNavigationBack(app: XCUIApplication) -> Bool {
22
+ func tapInAppBackControl(app: XCUIApplication) -> Bool {
28
23
  #if os(macOS)
29
24
  if let back = macOSNavigationBackElement(app: app) {
30
25
  tapElementCenter(app: app, element: back)
@@ -37,7 +32,7 @@ extension RunnerTests {
37
32
  back.tap()
38
33
  return true
39
34
  }
40
- return pressTvRemoteMenuIfAvailable()
35
+ return false
41
36
  #endif
42
37
  }
43
38
 
@@ -51,6 +46,18 @@ extension RunnerTests {
51
46
  start.press(forDuration: 0.05, thenDragTo: end)
52
47
  }
53
48
 
49
+ func performSystemBackAction(app: XCUIApplication) -> Bool {
50
+ #if os(macOS)
51
+ return false
52
+ #else
53
+ if pressTvRemoteMenuIfAvailable() {
54
+ return true
55
+ }
56
+ performBackGesture(app: app)
57
+ return true
58
+ #endif
59
+ }
60
+
54
61
  func performAppSwitcherGesture(app: XCUIApplication) {
55
62
  if performTvRemoteAppSwitcherIfAvailable() {
56
63
  return
@@ -161,19 +168,114 @@ extension RunnerTests {
161
168
  element.typeText(deletes)
162
169
  }
163
170
 
164
- func focusedTextInput(app: XCUIApplication) -> XCUIElement? {
165
- let focused = app
166
- .descendants(matching: .any)
167
- .matching(NSPredicate(format: "hasKeyboardFocus == 1"))
168
- .firstMatch
169
- guard focused.exists else { return nil }
171
+ func textInputAt(app: XCUIApplication, x: Double, y: Double) -> XCUIElement? {
172
+ let point = CGPoint(x: x, y: y)
173
+ var matched: XCUIElement?
174
+ let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
175
+ // Prefer the smallest matching field so nested editable controls win over large containers.
176
+ let candidates = app.descendants(matching: .any).allElementsBoundByIndex
177
+ .filter { element in
178
+ guard element.exists else { return false }
179
+ switch element.elementType {
180
+ case .textField, .secureTextField, .searchField, .textView:
181
+ let frame = element.frame
182
+ return !frame.isEmpty && frame.contains(point)
183
+ default:
184
+ return false
185
+ }
186
+ }
187
+ .sorted { left, right in
188
+ let leftArea = max(1, left.frame.width * left.frame.height)
189
+ let rightArea = max(1, right.frame.width * right.frame.height)
190
+ if leftArea != rightArea {
191
+ return leftArea < rightArea
192
+ }
193
+ if left.frame.minY != right.frame.minY {
194
+ return left.frame.minY < right.frame.minY
195
+ }
196
+ if left.frame.minX != right.frame.minX {
197
+ return left.frame.minX < right.frame.minX
198
+ }
199
+ return left.elementType.rawValue < right.elementType.rawValue
200
+ }
201
+ matched = candidates.first
202
+ })
203
+ if let exceptionMessage {
204
+ NSLog(
205
+ "AGENT_DEVICE_RUNNER_TEXT_INPUT_AT_POINT_IGNORED_EXCEPTION=%@",
206
+ exceptionMessage
207
+ )
208
+ return nil
209
+ }
210
+ return matched
211
+ }
170
212
 
171
- switch focused.elementType {
172
- case .textField, .secureTextField, .searchField, .textView:
173
- return focused
174
- default:
213
+ func focusedTextInput(app: XCUIApplication) -> XCUIElement? {
214
+ var focused: XCUIElement?
215
+ let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
216
+ let candidate = app
217
+ .descendants(matching: .any)
218
+ .matching(NSPredicate(format: "hasKeyboardFocus == 1"))
219
+ .firstMatch
220
+ guard candidate.exists else { return }
221
+
222
+ switch candidate.elementType {
223
+ case .textField, .secureTextField, .searchField, .textView:
224
+ focused = candidate
225
+ default:
226
+ return
227
+ }
228
+ })
229
+ if let exceptionMessage {
230
+ NSLog(
231
+ "AGENT_DEVICE_RUNNER_FOCUSED_INPUT_QUERY_IGNORED_EXCEPTION=%@",
232
+ exceptionMessage
233
+ )
175
234
  return nil
176
235
  }
236
+ return focused
237
+ }
238
+
239
+ func isKeyboardVisible(app: XCUIApplication) -> Bool {
240
+ let keyboard = app.keyboards.firstMatch
241
+ return keyboard.exists && !keyboard.frame.isEmpty
242
+ }
243
+
244
+ func dismissKeyboard(app: XCUIApplication) -> (wasVisible: Bool, dismissed: Bool, visible: Bool) {
245
+ let wasVisible = isKeyboardVisible(app: app)
246
+ guard wasVisible else {
247
+ return (wasVisible: false, dismissed: false, visible: false)
248
+ }
249
+
250
+ let keyboard = app.keyboards.firstMatch
251
+ keyboard.swipeDown()
252
+ sleepFor(0.2)
253
+ if !isKeyboardVisible(app: app) {
254
+ return (wasVisible: true, dismissed: true, visible: false)
255
+ }
256
+
257
+ if tapKeyboardDismissControl(app: app) {
258
+ sleepFor(0.2)
259
+ let visible = isKeyboardVisible(app: app)
260
+ return (wasVisible: true, dismissed: !visible, visible: visible)
261
+ }
262
+
263
+ return (wasVisible: true, dismissed: false, visible: isKeyboardVisible(app: app))
264
+ }
265
+
266
+ private func tapKeyboardDismissControl(app: XCUIApplication) -> Bool {
267
+ for label in ["Hide keyboard", "Dismiss keyboard"] {
268
+ let candidates = [
269
+ app.keyboards.buttons[label],
270
+ app.keyboards.keys[label],
271
+ app.toolbars.buttons[label],
272
+ ]
273
+ if let hittable = candidates.first(where: { $0.exists && $0.isHittable }) {
274
+ hittable.tap()
275
+ return true
276
+ }
277
+ }
278
+ return false
177
279
  }
178
280
 
179
281
  private func moveCaretToEnd(element: XCUIElement) {
@@ -322,7 +424,7 @@ extension RunnerTests {
322
424
  )
323
425
  }
324
426
 
325
- private func resolvedTouchReferenceFrame(app: XCUIApplication, appFrame: CGRect) -> CGRect {
427
+ func resolvedTouchReferenceFrame(app: XCUIApplication, appFrame: CGRect) -> CGRect {
326
428
  let window = app.windows.firstMatch
327
429
  let windowFrame = window.frame
328
430
  if window.exists && !windowFrame.isEmpty {
@@ -334,14 +436,6 @@ extension RunnerTests {
334
436
  return CGRect(x: 0, y: 0, width: 0, height: 0)
335
437
  }
336
438
 
337
- func resolvedGestureReferenceFrame(app: XCUIApplication) -> GestureReferenceFrame {
338
- let frame = resolvedTouchReferenceFrame(app: app, appFrame: app.frame)
339
- return GestureReferenceFrame(
340
- referenceWidth: frame.width,
341
- referenceHeight: frame.height
342
- )
343
- }
344
-
345
439
  func runSeries(count: Int, pauseMs: Double, operation: (Int) -> Void) {
346
440
  let total = max(count, 1)
347
441
  let pause = max(pauseMs, 0)
@@ -353,39 +447,36 @@ extension RunnerTests {
353
447
  }
354
448
  }
355
449
 
356
- func swipe(app: XCUIApplication, direction: SwipeDirection) {
450
+ func swipe(app: XCUIApplication, direction: String) -> DragVisualizationFrame? {
357
451
  if performTvRemoteSwipeIfAvailable(direction: direction) {
358
- return
359
- }
360
- let target = app.windows.firstMatch.exists ? app.windows.firstMatch : app
361
- let start = target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.2))
362
- let end = target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.8))
363
- let left = target.coordinate(withNormalizedOffset: CGVector(dx: 0.2, dy: 0.5))
364
- let right = target.coordinate(withNormalizedOffset: CGVector(dx: 0.8, dy: 0.5))
365
-
366
- switch direction {
367
- case .up:
368
- end.press(forDuration: 0.1, thenDragTo: start)
369
- case .down:
370
- start.press(forDuration: 0.1, thenDragTo: end)
371
- case .left:
372
- right.press(forDuration: 0.1, thenDragTo: left)
373
- case .right:
374
- left.press(forDuration: 0.1, thenDragTo: right)
452
+ let frame = resolvedTouchReferenceFrame(app: app, appFrame: app.frame)
453
+ let midX = frame.midX
454
+ let midY = frame.midY
455
+ return DragVisualizationFrame(
456
+ x: midX,
457
+ y: midY,
458
+ x2: midX,
459
+ y2: midY,
460
+ referenceWidth: frame.width,
461
+ referenceHeight: frame.height
462
+ )
375
463
  }
464
+ return nil
376
465
  }
377
466
 
378
- private func performTvRemoteSwipeIfAvailable(direction: SwipeDirection) -> Bool {
467
+ private func performTvRemoteSwipeIfAvailable(direction: String) -> Bool {
379
468
  #if os(tvOS)
380
469
  switch direction {
381
- case .up:
470
+ case "up":
382
471
  XCUIRemote.shared.press(.up)
383
- case .down:
472
+ case "down":
384
473
  XCUIRemote.shared.press(.down)
385
- case .left:
474
+ case "left":
386
475
  XCUIRemote.shared.press(.left)
387
- case .right:
476
+ case "right":
388
477
  XCUIRemote.shared.press(.right)
478
+ default:
479
+ return false
389
480
  }
390
481
  return true
391
482
  #else
@@ -461,5 +552,4 @@ extension RunnerTests {
461
552
  let element = app.descendants(matching: .any).matching(predicate).firstMatch
462
553
  return element.exists ? element : nil
463
554
  }
464
-
465
555
  }
@@ -152,7 +152,7 @@ extension RunnerTests {
152
152
 
153
153
  func isReadOnlyCommand(_ command: Command) -> Bool {
154
154
  switch command.command {
155
- case .findText, .readText, .snapshot, .screenshot:
155
+ case .interactionFrame, .findText, .readText, .snapshot, .screenshot:
156
156
  return true
157
157
  case .alert:
158
158
  let action = (command.action ?? "get").lowercased()
@@ -170,7 +170,18 @@ extension RunnerTests {
170
170
 
171
171
  func isInteractionCommand(_ command: CommandType) -> Bool {
172
172
  switch command {
173
- case .tap, .longPress, .drag, .type, .swipe, .back, .appSwitcher, .pinch:
173
+ case
174
+ .tap,
175
+ .longPress,
176
+ .drag,
177
+ .type,
178
+ .swipe,
179
+ .back,
180
+ .backInApp,
181
+ .backSystem,
182
+ .appSwitcher,
183
+ .keyboardDismiss,
184
+ .pinch:
174
185
  return true
175
186
  default:
176
187
  return false
@@ -5,6 +5,7 @@ enum CommandType: String, Codable {
5
5
  case mouseClick
6
6
  case tapSeries
7
7
  case longPress
8
+ case interactionFrame
8
9
  case drag
9
10
  case dragSeries
10
11
  case type
@@ -14,8 +15,11 @@ enum CommandType: String, Codable {
14
15
  case snapshot
15
16
  case screenshot
16
17
  case back
18
+ case backInApp
19
+ case backSystem
17
20
  case home
18
21
  case appSwitcher
22
+ case keyboardDismiss
19
23
  case alert
20
24
  case pinch
21
25
  case recordStart
@@ -24,17 +28,11 @@ enum CommandType: String, Codable {
24
28
  case shutdown
25
29
  }
26
30
 
27
- enum SwipeDirection: String, Codable {
28
- case up
29
- case down
30
- case left
31
- case right
32
- }
33
-
34
31
  struct Command: Codable {
35
32
  let command: CommandType
36
33
  let appBundleId: String?
37
34
  let text: String?
35
+ let delayMs: Int?
38
36
  let clearFirst: Bool?
39
37
  let action: String?
40
38
  let x: Double?
@@ -48,7 +46,7 @@ struct Command: Codable {
48
46
  let x2: Double?
49
47
  let y2: Double?
50
48
  let durationMs: Double?
51
- let direction: SwipeDirection?
49
+ let direction: String?
52
50
  let scale: Double?
53
51
  let outPath: String?
54
52
  let fps: Int?
@@ -87,6 +85,9 @@ struct DataPayload: Codable {
87
85
  let referenceWidth: Double?
88
86
  let referenceHeight: Double?
89
87
  let currentUptimeMs: Double?
88
+ let visible: Bool?
89
+ let wasVisible: Bool?
90
+ let dismissed: Bool?
90
91
 
91
92
  init(
92
93
  message: String? = nil,
@@ -103,7 +104,10 @@ struct DataPayload: Codable {
103
104
  y2: Double? = nil,
104
105
  referenceWidth: Double? = nil,
105
106
  referenceHeight: Double? = nil,
106
- currentUptimeMs: Double? = nil
107
+ currentUptimeMs: Double? = nil,
108
+ visible: Bool? = nil,
109
+ wasVisible: Bool? = nil,
110
+ dismissed: Bool? = nil
107
111
  ) {
108
112
  self.message = message
109
113
  self.text = text
@@ -120,11 +124,20 @@ struct DataPayload: Codable {
120
124
  self.referenceWidth = referenceWidth
121
125
  self.referenceHeight = referenceHeight
122
126
  self.currentUptimeMs = currentUptimeMs
127
+ self.visible = visible
128
+ self.wasVisible = wasVisible
129
+ self.dismissed = dismissed
123
130
  }
124
131
  }
125
132
 
126
133
  struct ErrorPayload: Codable {
134
+ let code: String?
127
135
  let message: String
136
+
137
+ init(code: String? = nil, message: String) {
138
+ self.code = code
139
+ self.message = message
140
+ }
128
141
  }
129
142
 
130
143
  struct SnapshotRect: Codable {
@@ -1,7 +1,16 @@
1
1
  import XCTest
2
2
 
3
3
  extension RunnerTests {
4
+ private static let collapsedTabCandidateTypes: Set<XCUIElement.ElementType> = [
5
+ .button,
6
+ .link,
7
+ .menuItem,
8
+ .other,
9
+ .staticText
10
+ ]
11
+
4
12
  private struct SnapshotTraversalContext {
13
+ let queryRoot: XCUIElement
5
14
  let rootSnapshot: XCUIElementSnapshot
6
15
  let viewport: CGRect
7
16
  let flatSnapshots: [XCUIElementSnapshot]
@@ -68,12 +77,34 @@ extension RunnerTests {
68
77
  return DataPayload(nodes: [], truncated: false)
69
78
  }
70
79
 
80
+ var cachedDescendantElements: [XCUIElement]?
81
+ func collapsedTabDescendants() -> [XCUIElement] {
82
+ if let cachedDescendantElements {
83
+ return cachedDescendantElements
84
+ }
85
+ let fetched = safeSnapshotElementsQuery {
86
+ context.queryRoot.descendants(matching: .any).allElementsBoundByIndex
87
+ }
88
+ cachedDescendantElements = fetched
89
+ return fetched
90
+ }
91
+
71
92
  var nodes: [SnapshotNode] = []
72
93
  var truncated = false
73
94
  let rootEvaluation = evaluateSnapshot(context.rootSnapshot, in: context)
74
95
  nodes.append(
75
96
  makeSnapshotNode(snapshot: context.rootSnapshot, evaluation: rootEvaluation, depth: 0, index: 0)
76
97
  )
98
+ if context.maxDepth > 0 {
99
+ let didTruncateFallback = appendCollapsedTabFallbackNodes(
100
+ to: &nodes,
101
+ containerSnapshot: context.rootSnapshot,
102
+ resolveElements: collapsedTabDescendants,
103
+ depth: 1,
104
+ nodeLimit: fastSnapshotLimit
105
+ )
106
+ truncated = truncated || didTruncateFallback
107
+ }
77
108
 
78
109
  var seen = Set<String>()
79
110
  var stack: [(XCUIElementSnapshot, Int, Int)] = context.rootSnapshot.children.map { ($0, 1, 1) }
@@ -119,6 +150,16 @@ extension RunnerTests {
119
150
  index: nodes.count
120
151
  )
121
152
  )
153
+ if visibleDepth < context.maxDepth {
154
+ let didTruncateFallback = appendCollapsedTabFallbackNodes(
155
+ to: &nodes,
156
+ containerSnapshot: snapshot,
157
+ resolveElements: collapsedTabDescendants,
158
+ depth: visibleDepth + 1,
159
+ nodeLimit: fastSnapshotLimit
160
+ )
161
+ truncated = truncated || didTruncateFallback
162
+ }
122
163
 
123
164
  }
124
165
 
@@ -247,6 +288,7 @@ extension RunnerTests {
247
288
 
248
289
  let (flatSnapshots, snapshotRanges) = flattenedSnapshots(rootSnapshot)
249
290
  return SnapshotTraversalContext(
291
+ queryRoot: queryRoot,
250
292
  rootSnapshot: rootSnapshot,
251
293
  viewport: viewport,
252
294
  flatSnapshots: flatSnapshots,
@@ -376,4 +418,183 @@ extension RunnerTests {
376
418
  if rect.isNull || rect.isEmpty { return false }
377
419
  return rect.intersects(viewport)
378
420
  }
421
+
422
+ private func appendCollapsedTabFallbackNodes(
423
+ to nodes: inout [SnapshotNode],
424
+ containerSnapshot: XCUIElementSnapshot,
425
+ resolveElements: () -> [XCUIElement],
426
+ depth: Int,
427
+ nodeLimit: Int
428
+ ) -> Bool {
429
+ let fallbackNodes = collapsedTabFallbackNodes(
430
+ for: containerSnapshot,
431
+ resolveElements: resolveElements,
432
+ startingIndex: nodes.count,
433
+ depth: depth
434
+ )
435
+ if fallbackNodes.isEmpty { return false }
436
+ let remaining = max(0, nodeLimit - nodes.count)
437
+ if remaining == 0 { return true }
438
+ nodes.append(contentsOf: fallbackNodes.prefix(remaining))
439
+ return fallbackNodes.count > remaining
440
+ }
441
+
442
+ private func collapsedTabFallbackNodes(
443
+ for containerSnapshot: XCUIElementSnapshot,
444
+ resolveElements: () -> [XCUIElement],
445
+ startingIndex: Int,
446
+ depth: Int
447
+ ) -> [SnapshotNode] {
448
+ if !containerSnapshot.children.isEmpty { return [] }
449
+ guard shouldExpandCollapsedTabContainer(containerSnapshot) else { return [] }
450
+ let containerFrame = containerSnapshot.frame
451
+ if containerFrame.isNull || containerFrame.isEmpty { return [] }
452
+
453
+ // Collapsed tab containers should be rare, so a full descendant scan is acceptable once per
454
+ // snapshot as a fallback for XCTest omitting the tab children from the snapshot tree.
455
+ let elements = resolveElements()
456
+ let candidates = elements.compactMap { element in
457
+ collapsedTabCandidateNode(
458
+ element: element,
459
+ containerSnapshot: containerSnapshot,
460
+ containerFrame: containerFrame
461
+ )
462
+ }
463
+ .sorted { left, right in
464
+ if left.rect.x != right.rect.x {
465
+ return left.rect.x < right.rect.x
466
+ }
467
+ return left.rect.y < right.rect.y
468
+ }
469
+
470
+ if candidates.count < 2 { return [] }
471
+ let rowMidpoints = candidates.map { $0.rect.y + ($0.rect.height / 2) }
472
+ let rowSpread = (rowMidpoints.max() ?? 0) - (rowMidpoints.min() ?? 0)
473
+ // Allow modest vertical jitter and short two-row wraps while still rejecting unrelated controls.
474
+ if rowSpread > max(24.0, Double(containerFrame.height) * 0.6) { return [] }
475
+
476
+ var seen = Set<String>()
477
+ let uniqueCandidates = candidates.filter { node in
478
+ let key = "\(node.type)-\(node.label ?? "")-\(node.identifier ?? "")-\(node.value ?? "")-\(node.rect.x)-\(node.rect.y)-\(node.rect.width)-\(node.rect.height)"
479
+ if seen.contains(key) { return false }
480
+ seen.insert(key)
481
+ return true
482
+ }
483
+ if uniqueCandidates.count < 2 { return [] }
484
+
485
+ return uniqueCandidates.enumerated().map { offset, node in
486
+ SnapshotNode(
487
+ index: startingIndex + offset,
488
+ type: node.type,
489
+ label: node.label,
490
+ identifier: node.identifier,
491
+ value: node.value,
492
+ rect: node.rect,
493
+ enabled: node.enabled,
494
+ hittable: node.hittable,
495
+ depth: depth
496
+ )
497
+ }
498
+ }
499
+
500
+ private func collapsedTabCandidateNode(
501
+ element: XCUIElement,
502
+ containerSnapshot: XCUIElementSnapshot,
503
+ containerFrame: CGRect
504
+ ) -> SnapshotNode? {
505
+ var node: SnapshotNode?
506
+ let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
507
+ if !element.exists { return }
508
+ let elementType = element.elementType
509
+ if !Self.collapsedTabCandidateTypes.contains(elementType) { return }
510
+ let frame = element.frame
511
+ if frame.isNull || frame.isEmpty { return }
512
+ if frame.equalTo(containerFrame) { return }
513
+ let area = max(CGFloat(1), frame.width * frame.height)
514
+ let containerArea = max(CGFloat(1), containerFrame.width * containerFrame.height)
515
+ if area >= containerArea * 0.9 { return }
516
+ let center = CGPoint(x: frame.midX, y: frame.midY)
517
+ if !containerFrame.contains(center) { return }
518
+
519
+ let label = element.label.trimmingCharacters(in: .whitespacesAndNewlines)
520
+ let identifier = element.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
521
+ let valueText = snapshotValueText(element)
522
+ let hasContent = !label.isEmpty || !identifier.isEmpty || valueText != nil
523
+ if !hasContent { return }
524
+ if sameSemanticElement(
525
+ containerSnapshot: containerSnapshot,
526
+ elementType: elementType,
527
+ label: label,
528
+ identifier: identifier
529
+ ) {
530
+ return
531
+ }
532
+
533
+ node = SnapshotNode(
534
+ index: 0,
535
+ type: elementTypeName(elementType),
536
+ label: label.isEmpty ? nil : label,
537
+ identifier: identifier.isEmpty ? nil : identifier,
538
+ value: valueText,
539
+ rect: snapshotRect(from: frame),
540
+ enabled: element.isEnabled,
541
+ hittable: element.isHittable,
542
+ depth: 0
543
+ )
544
+ })
545
+ if let exceptionMessage {
546
+ NSLog(
547
+ "AGENT_DEVICE_RUNNER_SNAPSHOT_TAB_FALLBACK_IGNORED_EXCEPTION=%@",
548
+ exceptionMessage
549
+ )
550
+ return nil
551
+ }
552
+ return node
553
+ }
554
+
555
+ private func shouldExpandCollapsedTabContainer(_ snapshot: XCUIElementSnapshot) -> Bool {
556
+ let frame = snapshot.frame
557
+ if frame.isNull || frame.isEmpty { return false }
558
+ if frame.width < max(CGFloat(160), frame.height * 1.75) { return false }
559
+ switch snapshot.elementType {
560
+ case .tabBar, .segmentedControl, .slider:
561
+ return true
562
+ default:
563
+ return false
564
+ }
565
+ }
566
+
567
+ private func snapshotValueText(_ element: XCUIElement) -> String? {
568
+ let text = String(describing: element.value ?? "")
569
+ .trimmingCharacters(in: .whitespacesAndNewlines)
570
+ return text.isEmpty ? nil : text
571
+ }
572
+
573
+ private func sameSemanticElement(
574
+ containerSnapshot: XCUIElementSnapshot,
575
+ elementType: XCUIElement.ElementType,
576
+ label: String,
577
+ identifier: String
578
+ ) -> Bool {
579
+ if containerSnapshot.elementType != elementType { return false }
580
+ let containerLabel = containerSnapshot.label.trimmingCharacters(in: .whitespacesAndNewlines)
581
+ let containerIdentifier = containerSnapshot.identifier
582
+ .trimmingCharacters(in: .whitespacesAndNewlines)
583
+ return containerLabel == label && containerIdentifier == identifier
584
+ }
585
+
586
+ private func safeSnapshotElementsQuery(_ fetch: () -> [XCUIElement]) -> [XCUIElement] {
587
+ var elements: [XCUIElement] = []
588
+ let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
589
+ elements = fetch()
590
+ })
591
+ if let exceptionMessage {
592
+ NSLog(
593
+ "AGENT_DEVICE_RUNNER_SNAPSHOT_QUERY_IGNORED_EXCEPTION=%@",
594
+ exceptionMessage
595
+ )
596
+ return []
597
+ }
598
+ return elements
599
+ }
379
600
  }
@@ -21,27 +21,39 @@ extension RunnerTests {
21
21
  status: 413,
22
22
  response: Response(ok: false, error: ErrorPayload(message: "request too large"))
23
23
  )
24
- connection.send(content: response, completion: .contentProcessed { [weak self] _ in
25
- connection.cancel()
24
+ self.sendResponse(response, over: connection) { [weak self] in
26
25
  self?.finish()
27
- })
26
+ }
28
27
  return
29
28
  }
30
29
  let combined = buffer + data
31
30
  if let body = self.parseRequest(data: combined) {
32
31
  let result = self.handleRequestBody(body)
33
- connection.send(content: result.data, completion: .contentProcessed { _ in
34
- connection.cancel()
32
+ self.sendResponse(result.data, over: connection) { [weak self] in
35
33
  if result.shouldFinish {
36
- self.finish()
34
+ self?.finish()
37
35
  }
38
- })
36
+ }
39
37
  } else {
40
38
  self.receiveRequest(connection: connection, buffer: combined)
41
39
  }
42
40
  }
43
41
  }
44
42
 
43
+ private func sendResponse(
44
+ _ response: Data,
45
+ over connection: NWConnection,
46
+ afterSend: @escaping () -> Void = {}
47
+ ) {
48
+ connection.send(content: response, isComplete: true, completion: .contentProcessed { error in
49
+ if let error {
50
+ NSLog("AGENT_DEVICE_RUNNER_SEND_FAILED=%@", String(describing: error))
51
+ }
52
+ connection.cancel()
53
+ afterSend()
54
+ })
55
+ }
56
+
45
57
  private func parseRequest(data: Data) -> Data? {
46
58
  guard let headerEnd = data.range(of: Data("\r\n\r\n".utf8)) else {
47
59
  return nil