agent-device 0.16.14 → 0.17.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.
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.16.14.apk → agent-device-android-multitouch-helper-0.17.1.apk} +0 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.17.1.apk.sha256 +1 -0
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.16.14.manifest.json → agent-device-android-multitouch-helper-0.17.1.manifest.json} +4 -4
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.16.14.apk → agent-device-android-snapshot-helper-0.17.1.apk} +0 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.17.1.apk.sha256 +1 -0
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.16.14.manifest.json → agent-device-android-snapshot-helper-0.17.1.manifest.json} +6 -6
- package/dist/src/1352.js +1 -1
- package/dist/src/221.js +4 -4
- package/dist/src/2415.js +29 -29
- package/dist/src/2805.js +1 -1
- package/dist/src/6232.js +1 -1
- package/dist/src/7599.js +4 -3
- package/dist/src/8020.js +1 -0
- package/dist/src/8699.js +1 -1
- package/dist/src/9238.js +3 -3
- package/dist/src/940.js +1 -1
- package/dist/src/9533.js +1 -1
- package/dist/src/9542.js +3 -3
- package/dist/src/android-snapshot-helper.d.ts +1 -0
- package/dist/src/apple.js +1 -1
- package/dist/src/apps.js +1 -1
- package/dist/src/args.js +15 -10
- package/dist/src/cli.js +9 -9
- package/dist/src/command-metadata.js +1 -1
- package/dist/src/contracts.d.ts +1 -0
- package/dist/src/find.js +1 -1
- package/dist/src/finders.d.ts +1 -0
- package/dist/src/generic.js +12 -10
- package/dist/src/index.d.ts +20 -1
- package/dist/src/interaction.js +1 -1
- package/dist/src/record-trace-recording.js +26 -0
- package/dist/src/record-trace.js +1 -26
- package/dist/src/selectors.d.ts +1 -0
- package/dist/src/session.js +11 -11
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +4 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +71 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +41 -7
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +160 -13
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +11 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift +12 -4
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +26 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +8 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +7 -1
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +571 -56
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +21 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TvRemote.swift +11 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +13 -2
- package/ios-runner/README.md +13 -0
- package/package.json +2 -2
- package/server.json +2 -2
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.14.apk.sha256 +0 -1
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.16.14.apk.sha256 +0 -1
|
@@ -2,8 +2,15 @@ import XCTest
|
|
|
2
2
|
|
|
3
3
|
extension RunnerTests {
|
|
4
4
|
private static let axSnapshotErrorCode = "IOS_AX_SNAPSHOT_FAILED"
|
|
5
|
+
private static let axSnapshotFailureMessage =
|
|
6
|
+
"iOS XCTest snapshot failed while serializing the accessibility tree."
|
|
7
|
+
private static let axSnapshotUnavailableReason = "ax_snapshot_unavailable"
|
|
5
8
|
private static let axSnapshotHint =
|
|
6
|
-
"XCTest could not serialize this iOS accessibility tree.
|
|
9
|
+
"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."
|
|
10
|
+
private static let rawSnapshotTooLargeCode = "IOS_RAW_SNAPSHOT_TOO_LARGE"
|
|
11
|
+
private static let rawSnapshotMaxNodes = 5_000
|
|
12
|
+
private static let rawSnapshotTooLargeHint =
|
|
13
|
+
"Raw iOS snapshot exceeded the runner payload guard. Use regular snapshot for visible UI, or scope/depth-limit raw snapshot when inspecting a large accessibility tree."
|
|
7
14
|
private static let collapsedTabCandidateTypes: Set<XCUIElement.ElementType> = [
|
|
8
15
|
.button,
|
|
9
16
|
.link,
|
|
@@ -16,6 +23,7 @@ extension RunnerTests {
|
|
|
16
23
|
.scrollView,
|
|
17
24
|
.table
|
|
18
25
|
]
|
|
26
|
+
private static let flatInteractiveFallbackBudget: TimeInterval = 1.0
|
|
19
27
|
|
|
20
28
|
private struct SnapshotTraversalContext {
|
|
21
29
|
let queryRoot: XCUIElement
|
|
@@ -36,6 +44,12 @@ extension RunnerTests {
|
|
|
36
44
|
let visible: Bool
|
|
37
45
|
}
|
|
38
46
|
|
|
47
|
+
private enum SnapshotTraversalCapture {
|
|
48
|
+
case context(SnapshotTraversalContext)
|
|
49
|
+
case fallback(DataPayload)
|
|
50
|
+
case empty
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
struct SnapshotCaptureFailure: Error {
|
|
40
54
|
let code: String
|
|
41
55
|
let message: String
|
|
@@ -85,11 +99,25 @@ extension RunnerTests {
|
|
|
85
99
|
}
|
|
86
100
|
|
|
87
101
|
func snapshotFast(app: XCUIApplication, options: SnapshotOptions) throws -> DataPayload {
|
|
102
|
+
if options.interactiveOnly && options.compact {
|
|
103
|
+
return snapshotFlatInteractive(app: app, options: options)
|
|
104
|
+
}
|
|
88
105
|
if let blocking = blockingSystemAlertSnapshot() {
|
|
89
106
|
return blocking
|
|
90
107
|
}
|
|
91
108
|
|
|
92
|
-
|
|
109
|
+
let capture = try captureSnapshotTraversalContext(
|
|
110
|
+
app: app,
|
|
111
|
+
options: options,
|
|
112
|
+
allowInteractiveUnavailableFallback: true
|
|
113
|
+
)
|
|
114
|
+
let context: SnapshotTraversalContext
|
|
115
|
+
switch capture {
|
|
116
|
+
case .context(let traversalContext):
|
|
117
|
+
context = traversalContext
|
|
118
|
+
case .fallback(let fallback):
|
|
119
|
+
return fallback
|
|
120
|
+
case .empty:
|
|
93
121
|
return DataPayload(nodes: [], truncated: false)
|
|
94
122
|
}
|
|
95
123
|
|
|
@@ -98,15 +126,15 @@ extension RunnerTests {
|
|
|
98
126
|
if let cachedDescendantElements {
|
|
99
127
|
return cachedDescendantElements
|
|
100
128
|
}
|
|
101
|
-
let
|
|
129
|
+
let result = snapshotElementsQuery {
|
|
102
130
|
context.queryRoot.descendants(matching: .any).allElementsBoundByIndex
|
|
103
131
|
}
|
|
104
|
-
cachedDescendantElements =
|
|
105
|
-
return
|
|
132
|
+
cachedDescendantElements = result.elements
|
|
133
|
+
return result.elements
|
|
106
134
|
}
|
|
107
135
|
|
|
108
136
|
var nodes: [SnapshotNode] = []
|
|
109
|
-
var
|
|
137
|
+
var hiddenContentHintsByNodeIndex: [Int: (above: Bool, below: Bool)] = [:]
|
|
110
138
|
let rootEvaluation = evaluateSnapshot(context.rootSnapshot, in: context)
|
|
111
139
|
nodes.append(
|
|
112
140
|
makeSnapshotNode(
|
|
@@ -118,30 +146,42 @@ extension RunnerTests {
|
|
|
118
146
|
)
|
|
119
147
|
)
|
|
120
148
|
if context.maxDepth > 0 {
|
|
121
|
-
|
|
149
|
+
appendCollapsedTabFallbackNodes(
|
|
122
150
|
to: &nodes,
|
|
123
151
|
containerSnapshot: context.rootSnapshot,
|
|
124
152
|
resolveElements: collapsedTabDescendants,
|
|
125
153
|
depth: 1,
|
|
126
|
-
parentIndex: 0
|
|
127
|
-
nodeLimit: fastSnapshotLimit
|
|
154
|
+
parentIndex: 0
|
|
128
155
|
)
|
|
129
|
-
truncated = truncated || didTruncateFallback
|
|
130
156
|
}
|
|
131
157
|
|
|
132
158
|
var seen = Set<String>()
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
159
|
+
let rootScrollAnchor = scrollContainerAnchor(
|
|
160
|
+
for: context.rootSnapshot,
|
|
161
|
+
visible: rootEvaluation.visible,
|
|
162
|
+
nodeIndex: 0
|
|
163
|
+
)
|
|
164
|
+
var stack: [(XCUIElementSnapshot, Int, Int, Int?, (index: Int, rect: CGRect)?)] =
|
|
165
|
+
context.rootSnapshot.children.map {
|
|
166
|
+
($0, 1, 1, 0, rootScrollAnchor)
|
|
141
167
|
}
|
|
168
|
+
|
|
169
|
+
while let (snapshot, depth, visibleDepth, parentIndex, nearestScrollAnchor) = stack.popLast() {
|
|
142
170
|
if let limit = options.depth, depth > limit { continue }
|
|
143
171
|
|
|
144
172
|
let evaluation = evaluateSnapshot(snapshot, in: context)
|
|
173
|
+
let regularVisible = isVisibleInRegularSnapshot(
|
|
174
|
+
snapshot.frame,
|
|
175
|
+
viewport: context.viewport,
|
|
176
|
+
scrollContainerAnchor: nearestScrollAnchor
|
|
177
|
+
)
|
|
178
|
+
if !regularVisible, let nearestScrollAnchor {
|
|
179
|
+
rememberHiddenContentHint(
|
|
180
|
+
for: snapshot.frame,
|
|
181
|
+
relativeTo: nearestScrollAnchor,
|
|
182
|
+
hints: &hiddenContentHintsByNodeIndex
|
|
183
|
+
)
|
|
184
|
+
}
|
|
145
185
|
let include = shouldInclude(
|
|
146
186
|
snapshot: snapshot,
|
|
147
187
|
label: evaluation.label,
|
|
@@ -149,7 +189,8 @@ extension RunnerTests {
|
|
|
149
189
|
valueText: evaluation.valueText,
|
|
150
190
|
options: options,
|
|
151
191
|
hittable: evaluation.hittable,
|
|
152
|
-
visible:
|
|
192
|
+
visible: regularVisible,
|
|
193
|
+
regularSnapshot: true
|
|
153
194
|
)
|
|
154
195
|
|
|
155
196
|
let key = "\(snapshot.elementType)-\(evaluation.label)-\(evaluation.identifier)-\(snapshot.frame.origin.x)-\(snapshot.frame.origin.y)"
|
|
@@ -161,8 +202,20 @@ extension RunnerTests {
|
|
|
161
202
|
let currentIndex = include && !isDuplicate ? nodes.count : parentIndex
|
|
162
203
|
if depth < context.maxDepth {
|
|
163
204
|
let nextVisibleDepth = include && !isDuplicate ? visibleDepth + 1 : visibleDepth
|
|
205
|
+
let nextScrollContainerAnchor: (index: Int, rect: CGRect)?
|
|
206
|
+
if include && !isDuplicate {
|
|
207
|
+
nextScrollContainerAnchor =
|
|
208
|
+
scrollContainerAnchor(
|
|
209
|
+
for: snapshot,
|
|
210
|
+
visible: regularVisible,
|
|
211
|
+
nodeIndex: currentIndex
|
|
212
|
+
)
|
|
213
|
+
?? nearestScrollAnchor
|
|
214
|
+
} else {
|
|
215
|
+
nextScrollContainerAnchor = nearestScrollAnchor
|
|
216
|
+
}
|
|
164
217
|
for child in snapshot.children.reversed() {
|
|
165
|
-
stack.append((child, depth + 1, nextVisibleDepth, currentIndex))
|
|
218
|
+
stack.append((child, depth + 1, nextVisibleDepth, currentIndex, nextScrollContainerAnchor))
|
|
166
219
|
}
|
|
167
220
|
}
|
|
168
221
|
|
|
@@ -179,20 +232,21 @@ extension RunnerTests {
|
|
|
179
232
|
)
|
|
180
233
|
)
|
|
181
234
|
if visibleDepth < context.maxDepth {
|
|
182
|
-
|
|
235
|
+
appendCollapsedTabFallbackNodes(
|
|
183
236
|
to: &nodes,
|
|
184
237
|
containerSnapshot: snapshot,
|
|
185
238
|
resolveElements: collapsedTabDescendants,
|
|
186
239
|
depth: visibleDepth + 1,
|
|
187
|
-
parentIndex: index
|
|
188
|
-
nodeLimit: fastSnapshotLimit
|
|
240
|
+
parentIndex: index
|
|
189
241
|
)
|
|
190
|
-
truncated = truncated || didTruncateFallback
|
|
191
242
|
}
|
|
192
243
|
|
|
193
244
|
}
|
|
194
245
|
|
|
195
|
-
return DataPayload(
|
|
246
|
+
return DataPayload(
|
|
247
|
+
nodes: applyHiddenContentHints(hiddenContentHintsByNodeIndex, to: nodes),
|
|
248
|
+
truncated: false
|
|
249
|
+
)
|
|
196
250
|
}
|
|
197
251
|
|
|
198
252
|
func snapshotRaw(app: XCUIApplication, options: SnapshotOptions) throws -> DataPayload {
|
|
@@ -200,18 +254,24 @@ extension RunnerTests {
|
|
|
200
254
|
return blocking
|
|
201
255
|
}
|
|
202
256
|
|
|
203
|
-
|
|
257
|
+
let capture = try captureSnapshotTraversalContext(
|
|
258
|
+
app: app,
|
|
259
|
+
options: options,
|
|
260
|
+
allowInteractiveUnavailableFallback: false
|
|
261
|
+
)
|
|
262
|
+
let context: SnapshotTraversalContext
|
|
263
|
+
switch capture {
|
|
264
|
+
case .context(let traversalContext):
|
|
265
|
+
context = traversalContext
|
|
266
|
+
case .fallback(let fallback):
|
|
267
|
+
return fallback
|
|
268
|
+
case .empty:
|
|
204
269
|
return DataPayload(nodes: [], truncated: false)
|
|
205
270
|
}
|
|
206
271
|
|
|
207
272
|
var nodes: [SnapshotNode] = []
|
|
208
|
-
var truncated = false
|
|
209
273
|
|
|
210
|
-
func walk(_ snapshot: XCUIElementSnapshot, depth: Int, parentIndex: Int?) {
|
|
211
|
-
if nodes.count >= maxSnapshotElements {
|
|
212
|
-
truncated = true
|
|
213
|
-
return
|
|
214
|
-
}
|
|
274
|
+
func walk(_ snapshot: XCUIElementSnapshot, depth: Int, parentIndex: Int?) throws {
|
|
215
275
|
if let limit = options.depth, depth > limit { return }
|
|
216
276
|
|
|
217
277
|
let evaluation = evaluateSnapshot(snapshot, in: context)
|
|
@@ -226,6 +286,9 @@ extension RunnerTests {
|
|
|
226
286
|
)
|
|
227
287
|
let currentIndex = include ? nodes.count : parentIndex
|
|
228
288
|
if include {
|
|
289
|
+
if nodes.count >= Self.rawSnapshotMaxNodes {
|
|
290
|
+
throw rawSnapshotTooLargeFailure(nodeCount: nodes.count + 1)
|
|
291
|
+
}
|
|
229
292
|
nodes.append(
|
|
230
293
|
makeSnapshotNode(
|
|
231
294
|
snapshot: snapshot,
|
|
@@ -239,15 +302,286 @@ extension RunnerTests {
|
|
|
239
302
|
|
|
240
303
|
let children = snapshot.children
|
|
241
304
|
for child in children {
|
|
242
|
-
walk(child, depth: depth + 1, parentIndex: currentIndex)
|
|
243
|
-
if truncated { return }
|
|
305
|
+
try walk(child, depth: depth + 1, parentIndex: currentIndex)
|
|
244
306
|
}
|
|
245
307
|
}
|
|
246
308
|
|
|
247
|
-
walk(context.rootSnapshot, depth: 0, parentIndex: nil)
|
|
309
|
+
try walk(context.rootSnapshot, depth: 0, parentIndex: nil)
|
|
310
|
+
return DataPayload(nodes: nodes, truncated: false)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private func snapshotFlatInteractive(app: XCUIApplication, options: SnapshotOptions) -> DataPayload {
|
|
314
|
+
var nodes: [SnapshotNode] = [
|
|
315
|
+
compactInteractiveRootNode(rect: .zero)
|
|
316
|
+
]
|
|
317
|
+
if options.depth == 0 {
|
|
318
|
+
return DataPayload(nodes: nodes, truncated: false)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
let deadline = options.interactiveOnly
|
|
322
|
+
? Date().addingTimeInterval(Self.flatInteractiveFallbackBudget)
|
|
323
|
+
: Date.distantFuture
|
|
324
|
+
let viewport = safeSnapshotViewport(app: app)
|
|
325
|
+
var seen = Set<String>()
|
|
326
|
+
var candidates: [SnapshotNode] = []
|
|
327
|
+
let flatElements = flatInteractiveElements(app: app, deadline: deadline)
|
|
328
|
+
var truncated = flatElements.truncated
|
|
329
|
+
for element in flatElements.elements {
|
|
330
|
+
if Date() >= deadline {
|
|
331
|
+
NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_FLAT_FALLBACK_DEADLINE")
|
|
332
|
+
truncated = true
|
|
333
|
+
break
|
|
334
|
+
}
|
|
335
|
+
guard let node = flatSnapshotNode(
|
|
336
|
+
element: element,
|
|
337
|
+
index: 0,
|
|
338
|
+
parentIndex: 0,
|
|
339
|
+
viewport: viewport,
|
|
340
|
+
options: options
|
|
341
|
+
) else {
|
|
342
|
+
continue
|
|
343
|
+
}
|
|
344
|
+
let key = "\(node.type)-\(node.label ?? "")-\(node.identifier ?? "")-\(node.value ?? "")-\(node.rect.x)-\(node.rect.y)-\(node.rect.width)-\(node.rect.height)"
|
|
345
|
+
if seen.contains(key) { continue }
|
|
346
|
+
seen.insert(key)
|
|
347
|
+
candidates.append(node)
|
|
348
|
+
}
|
|
349
|
+
candidates.sort { left, right in
|
|
350
|
+
if left.rect.y != right.rect.y {
|
|
351
|
+
return left.rect.y < right.rect.y
|
|
352
|
+
}
|
|
353
|
+
if left.rect.x != right.rect.x {
|
|
354
|
+
return left.rect.x < right.rect.x
|
|
355
|
+
}
|
|
356
|
+
return left.type < right.type
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
nodes[0] = compactInteractiveRootNode(rect: compactInteractiveRootFrame(for: candidates))
|
|
360
|
+
for candidate in candidates {
|
|
361
|
+
nodes.append(
|
|
362
|
+
SnapshotNode(
|
|
363
|
+
index: nodes.count,
|
|
364
|
+
type: candidate.type,
|
|
365
|
+
label: candidate.label,
|
|
366
|
+
identifier: candidate.identifier,
|
|
367
|
+
value: candidate.value,
|
|
368
|
+
rect: candidate.rect,
|
|
369
|
+
enabled: candidate.enabled,
|
|
370
|
+
focused: candidate.focused,
|
|
371
|
+
selected: candidate.selected,
|
|
372
|
+
hittable: candidate.hittable,
|
|
373
|
+
depth: 1,
|
|
374
|
+
parentIndex: 0,
|
|
375
|
+
hiddenContentAbove: nil,
|
|
376
|
+
hiddenContentBelow: nil
|
|
377
|
+
)
|
|
378
|
+
)
|
|
379
|
+
}
|
|
248
380
|
return DataPayload(nodes: nodes, truncated: truncated)
|
|
249
381
|
}
|
|
250
382
|
|
|
383
|
+
private func snapshotAccessibilityUnavailable(failure: SnapshotCaptureFailure) -> DataPayload {
|
|
384
|
+
NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_AX_UNAVAILABLE=%@", failure.message)
|
|
385
|
+
invalidateCachedTarget(reason: Self.axSnapshotUnavailableReason)
|
|
386
|
+
return sparseTruncatedSnapshotPayload(
|
|
387
|
+
message: recoveredSnapshotMessage(failure),
|
|
388
|
+
runnerFatal: true,
|
|
389
|
+
runnerFatalReason: Self.axSnapshotUnavailableReason
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private func captureSnapshotTraversalContext(
|
|
394
|
+
app: XCUIApplication,
|
|
395
|
+
options: SnapshotOptions,
|
|
396
|
+
allowInteractiveUnavailableFallback: Bool
|
|
397
|
+
) throws -> SnapshotTraversalCapture {
|
|
398
|
+
do {
|
|
399
|
+
guard let context = try makeSnapshotTraversalContext(app: app, options: options) else {
|
|
400
|
+
return .empty
|
|
401
|
+
}
|
|
402
|
+
return .context(context)
|
|
403
|
+
} catch let failure as SnapshotCaptureFailure {
|
|
404
|
+
if let fallback = snapshotDepthLimitedAccessibilityFallback(
|
|
405
|
+
app: app,
|
|
406
|
+
options: options,
|
|
407
|
+
failure: failure
|
|
408
|
+
) {
|
|
409
|
+
return .fallback(fallback)
|
|
410
|
+
}
|
|
411
|
+
if allowInteractiveUnavailableFallback && options.interactiveOnly {
|
|
412
|
+
return .fallback(snapshotAccessibilityUnavailable(failure: failure))
|
|
413
|
+
}
|
|
414
|
+
throw failure
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private func snapshotDepthLimitedAccessibilityFallback(
|
|
419
|
+
app: XCUIApplication,
|
|
420
|
+
options: SnapshotOptions,
|
|
421
|
+
failure: SnapshotCaptureFailure
|
|
422
|
+
) -> DataPayload? {
|
|
423
|
+
guard let requestedDepth = options.depth else {
|
|
424
|
+
return nil
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
NSLog(
|
|
428
|
+
"AGENT_DEVICE_RUNNER_SNAPSHOT_DEPTH_FALLBACK=%@",
|
|
429
|
+
failure.message
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
if requestedDepth <= 0 {
|
|
433
|
+
return sparseTruncatedSnapshotPayload(message: recoveredSnapshotMessage(failure))
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Raw depth-limited recovery intentionally falls back to sparse interactive discovery because
|
|
437
|
+
// the raw AX tree is the failed operation.
|
|
438
|
+
let fallback = snapshotFlatInteractive(
|
|
439
|
+
app: app,
|
|
440
|
+
options: SnapshotOptions(
|
|
441
|
+
interactiveOnly: true,
|
|
442
|
+
compact: options.compact,
|
|
443
|
+
depth: requestedDepth,
|
|
444
|
+
scope: options.scope,
|
|
445
|
+
raw: false
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
return DataPayload(
|
|
449
|
+
message: recoveredSnapshotMessage(failure),
|
|
450
|
+
nodes: fallback.nodes,
|
|
451
|
+
truncated: true
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private func recoveredSnapshotMessage(_ failure: SnapshotCaptureFailure) -> String {
|
|
456
|
+
return "\(failure.message) Hint: \(failure.hint)"
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private func rawSnapshotTooLargeFailure(nodeCount: Int) -> SnapshotCaptureFailure {
|
|
460
|
+
SnapshotCaptureFailure(
|
|
461
|
+
code: Self.rawSnapshotTooLargeCode,
|
|
462
|
+
message: "iOS raw snapshot exceeded \(Self.rawSnapshotMaxNodes) nodes while walking node \(nodeCount).",
|
|
463
|
+
hint: Self.rawSnapshotTooLargeHint
|
|
464
|
+
)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private func sparseTruncatedSnapshotPayload(
|
|
468
|
+
message: String,
|
|
469
|
+
runnerFatal: Bool? = nil,
|
|
470
|
+
runnerFatalReason: String? = nil
|
|
471
|
+
) -> DataPayload {
|
|
472
|
+
return DataPayload(
|
|
473
|
+
message: message,
|
|
474
|
+
nodes: [compactInteractiveRootNode(rect: .zero)],
|
|
475
|
+
truncated: true,
|
|
476
|
+
runnerFatal: runnerFatal,
|
|
477
|
+
runnerFatalReason: runnerFatalReason
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
func testSnapshotAccessibilityUnavailableMarksSparseSnapshotRunnerFatal() {
|
|
482
|
+
currentApp = app
|
|
483
|
+
currentBundleId = "com.example.app"
|
|
484
|
+
|
|
485
|
+
let payload = snapshotAccessibilityUnavailable(
|
|
486
|
+
failure: SnapshotCaptureFailure(
|
|
487
|
+
code: Self.axSnapshotErrorCode,
|
|
488
|
+
message: Self.axSnapshotFailureMessage,
|
|
489
|
+
hint: Self.axSnapshotHint
|
|
490
|
+
)
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
XCTAssertEqual(payload.message, "\(Self.axSnapshotFailureMessage) Hint: \(Self.axSnapshotHint)")
|
|
494
|
+
XCTAssertEqual(payload.nodes?.count, 1)
|
|
495
|
+
XCTAssertEqual(payload.nodes?.first?.type, "Application")
|
|
496
|
+
XCTAssertEqual(payload.truncated, true)
|
|
497
|
+
XCTAssertEqual(payload.runnerFatal, true)
|
|
498
|
+
XCTAssertEqual(payload.runnerFatalReason, Self.axSnapshotUnavailableReason)
|
|
499
|
+
XCTAssertNil(currentApp)
|
|
500
|
+
XCTAssertNil(currentBundleId)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
func testRecoveredSnapshotMessagePreservesHint() {
|
|
504
|
+
let message = recoveredSnapshotMessage(
|
|
505
|
+
SnapshotCaptureFailure(
|
|
506
|
+
code: Self.axSnapshotErrorCode,
|
|
507
|
+
message: Self.axSnapshotFailureMessage,
|
|
508
|
+
hint: Self.axSnapshotHint
|
|
509
|
+
)
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
XCTAssertTrue(message.contains(Self.axSnapshotFailureMessage))
|
|
513
|
+
XCTAssertTrue(message.contains(Self.axSnapshotHint))
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
func testRawSnapshotTooLargeFailureIsStructured() {
|
|
517
|
+
let failure = rawSnapshotTooLargeFailure(nodeCount: Self.rawSnapshotMaxNodes + 1)
|
|
518
|
+
|
|
519
|
+
XCTAssertEqual(failure.code, Self.rawSnapshotTooLargeCode)
|
|
520
|
+
XCTAssertTrue(failure.message.contains("\(Self.rawSnapshotMaxNodes) nodes"))
|
|
521
|
+
XCTAssertEqual(failure.hint, Self.rawSnapshotTooLargeHint)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
func testDepthLimitedSnapshotFailureReturnsNonFatalFallback() {
|
|
525
|
+
currentApp = app
|
|
526
|
+
currentBundleId = "com.example.app"
|
|
527
|
+
|
|
528
|
+
let payload = snapshotDepthLimitedAccessibilityFallback(
|
|
529
|
+
app: app,
|
|
530
|
+
options: SnapshotOptions(
|
|
531
|
+
interactiveOnly: false,
|
|
532
|
+
compact: false,
|
|
533
|
+
depth: 0,
|
|
534
|
+
scope: nil,
|
|
535
|
+
raw: false
|
|
536
|
+
),
|
|
537
|
+
failure: SnapshotCaptureFailure(
|
|
538
|
+
code: Self.axSnapshotErrorCode,
|
|
539
|
+
message: "\(Self.axSnapshotFailureMessage) kAXErrorIllegalArgument.",
|
|
540
|
+
hint: Self.axSnapshotHint
|
|
541
|
+
)
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
XCTAssertEqual(
|
|
545
|
+
payload?.message,
|
|
546
|
+
"\(Self.axSnapshotFailureMessage) kAXErrorIllegalArgument. Hint: \(Self.axSnapshotHint)"
|
|
547
|
+
)
|
|
548
|
+
XCTAssertEqual(payload?.nodes?.count, 1)
|
|
549
|
+
XCTAssertEqual(payload?.nodes?.first?.type, "Application")
|
|
550
|
+
XCTAssertEqual(payload?.truncated, true)
|
|
551
|
+
XCTAssertNil(payload?.runnerFatal)
|
|
552
|
+
XCTAssertNil(payload?.runnerFatalReason)
|
|
553
|
+
XCTAssertNotNil(currentApp)
|
|
554
|
+
XCTAssertEqual(currentBundleId, "com.example.app")
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private func compactInteractiveRootNode(rect: CGRect) -> SnapshotNode {
|
|
558
|
+
SnapshotNode(
|
|
559
|
+
index: 0,
|
|
560
|
+
type: "Application",
|
|
561
|
+
label: nil,
|
|
562
|
+
identifier: nil,
|
|
563
|
+
value: nil,
|
|
564
|
+
rect: snapshotRect(from: rect),
|
|
565
|
+
enabled: true,
|
|
566
|
+
focused: nil,
|
|
567
|
+
selected: nil,
|
|
568
|
+
hittable: false,
|
|
569
|
+
depth: 0,
|
|
570
|
+
parentIndex: nil,
|
|
571
|
+
hiddenContentAbove: nil,
|
|
572
|
+
hiddenContentBelow: nil
|
|
573
|
+
)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private func compactInteractiveRootFrame(for candidates: [SnapshotNode]) -> CGRect {
|
|
577
|
+
guard !candidates.isEmpty else {
|
|
578
|
+
return .zero
|
|
579
|
+
}
|
|
580
|
+
let maxX = candidates.map { CGFloat($0.rect.x + $0.rect.width) }.max() ?? 0
|
|
581
|
+
let maxY = candidates.map { CGFloat($0.rect.y + $0.rect.height) }.max() ?? 0
|
|
582
|
+
return CGRect(x: 0, y: 0, width: max(1, maxX), height: max(1, maxY))
|
|
583
|
+
}
|
|
584
|
+
|
|
251
585
|
func snapshotRect(from frame: CGRect) -> SnapshotRect {
|
|
252
586
|
return SnapshotRect(
|
|
253
587
|
x: Double(frame.origin.x),
|
|
@@ -266,7 +600,8 @@ extension RunnerTests {
|
|
|
266
600
|
valueText: String?,
|
|
267
601
|
options: SnapshotOptions,
|
|
268
602
|
hittable: Bool,
|
|
269
|
-
visible: Bool
|
|
603
|
+
visible: Bool,
|
|
604
|
+
regularSnapshot: Bool = false
|
|
270
605
|
) -> Bool {
|
|
271
606
|
let type = snapshot.elementType
|
|
272
607
|
let hasContent = !label.isEmpty || !identifier.isEmpty || (valueText != nil)
|
|
@@ -288,6 +623,10 @@ extension RunnerTests {
|
|
|
288
623
|
if options.compact {
|
|
289
624
|
return hasContent || hittable
|
|
290
625
|
}
|
|
626
|
+
if regularSnapshot {
|
|
627
|
+
if type == .application || type == .window { return true }
|
|
628
|
+
return visible
|
|
629
|
+
}
|
|
291
630
|
return true
|
|
292
631
|
}
|
|
293
632
|
|
|
@@ -366,11 +705,12 @@ extension RunnerTests {
|
|
|
366
705
|
}
|
|
367
706
|
|
|
368
707
|
private func axSnapshotFailure(_ message: String) -> SnapshotCaptureFailure {
|
|
708
|
+
let detail = message.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
369
709
|
let failureMessage: String
|
|
370
|
-
if
|
|
371
|
-
failureMessage =
|
|
710
|
+
if detail.isEmpty {
|
|
711
|
+
failureMessage = Self.axSnapshotFailureMessage
|
|
372
712
|
} else {
|
|
373
|
-
failureMessage = "
|
|
713
|
+
failureMessage = "\(Self.axSnapshotFailureMessage) \(detail)"
|
|
374
714
|
}
|
|
375
715
|
return SnapshotCaptureFailure(
|
|
376
716
|
code: Self.axSnapshotErrorCode,
|
|
@@ -381,14 +721,10 @@ extension RunnerTests {
|
|
|
381
721
|
|
|
382
722
|
private static func isAxIllegalArgument(_ message: String) -> Bool {
|
|
383
723
|
let normalized = message.lowercased()
|
|
384
|
-
return
|
|
724
|
+
return normalized.contains("kaxerrorillegalargument")
|
|
385
725
|
|| (normalized.contains("illegal argument") && normalized.contains("snapshot"))
|
|
386
726
|
}
|
|
387
727
|
|
|
388
|
-
private static func hasAxIllegalArgumentCode(_ message: String) -> Bool {
|
|
389
|
-
return message.lowercased().contains("kaxerrorillegalargument")
|
|
390
|
-
}
|
|
391
|
-
|
|
392
728
|
private func evaluateSnapshot(
|
|
393
729
|
_ snapshot: XCUIElementSnapshot,
|
|
394
730
|
in context: SnapshotTraversalContext
|
|
@@ -491,8 +827,13 @@ extension RunnerTests {
|
|
|
491
827
|
|
|
492
828
|
private func snapshotViewport(app: XCUIApplication) -> CGRect {
|
|
493
829
|
let windows = app.windows.allElementsBoundByIndex
|
|
494
|
-
|
|
495
|
-
|
|
830
|
+
let windowFrames = windows
|
|
831
|
+
.filter { $0.exists && !$0.frame.isNull && !$0.frame.isEmpty }
|
|
832
|
+
.map(\.frame)
|
|
833
|
+
if let largestWindowFrame = windowFrames.max(by: { left, right in
|
|
834
|
+
left.width * left.height < right.width * right.height
|
|
835
|
+
}) {
|
|
836
|
+
return largestWindowFrame
|
|
496
837
|
}
|
|
497
838
|
let appFrame = app.frame
|
|
498
839
|
if !appFrame.isNull && !appFrame.isEmpty {
|
|
@@ -519,14 +860,23 @@ extension RunnerTests {
|
|
|
519
860
|
return rect.intersects(viewport)
|
|
520
861
|
}
|
|
521
862
|
|
|
863
|
+
private func isVisibleInRegularSnapshot(
|
|
864
|
+
_ rect: CGRect,
|
|
865
|
+
viewport: CGRect,
|
|
866
|
+
scrollContainerAnchor: (index: Int, rect: CGRect)?
|
|
867
|
+
) -> Bool {
|
|
868
|
+
if !isVisibleInViewport(rect, viewport) { return false }
|
|
869
|
+
guard let scrollContainerAnchor else { return true }
|
|
870
|
+
return isVisibleInViewport(rect, scrollContainerAnchor.rect)
|
|
871
|
+
}
|
|
872
|
+
|
|
522
873
|
private func appendCollapsedTabFallbackNodes(
|
|
523
874
|
to nodes: inout [SnapshotNode],
|
|
524
875
|
containerSnapshot: XCUIElementSnapshot,
|
|
525
876
|
resolveElements: () -> [XCUIElement],
|
|
526
877
|
depth: Int,
|
|
527
|
-
parentIndex: Int
|
|
528
|
-
|
|
529
|
-
) -> Bool {
|
|
878
|
+
parentIndex: Int
|
|
879
|
+
) {
|
|
530
880
|
let fallbackNodes = collapsedTabFallbackNodes(
|
|
531
881
|
for: containerSnapshot,
|
|
532
882
|
resolveElements: resolveElements,
|
|
@@ -534,11 +884,62 @@ extension RunnerTests {
|
|
|
534
884
|
depth: depth,
|
|
535
885
|
parentIndex: parentIndex
|
|
536
886
|
)
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
887
|
+
nodes.append(contentsOf: fallbackNodes)
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
private func scrollContainerAnchor(
|
|
891
|
+
for snapshot: XCUIElementSnapshot,
|
|
892
|
+
visible: Bool,
|
|
893
|
+
nodeIndex: Int?
|
|
894
|
+
) -> (index: Int, rect: CGRect)? {
|
|
895
|
+
guard let nodeIndex else { return nil }
|
|
896
|
+
if !isScrollableContainer(snapshot, visible: visible) { return nil }
|
|
897
|
+
return (nodeIndex, snapshot.frame)
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
private func rememberHiddenContentHint(
|
|
901
|
+
for frame: CGRect,
|
|
902
|
+
relativeTo scrollContainerAnchor: (index: Int, rect: CGRect),
|
|
903
|
+
hints: inout [Int: (above: Bool, below: Bool)]
|
|
904
|
+
) {
|
|
905
|
+
if frame.isNull || frame.isEmpty { return }
|
|
906
|
+
var hint = hints[scrollContainerAnchor.index] ?? (above: false, below: false)
|
|
907
|
+
if frame.maxY <= scrollContainerAnchor.rect.minY {
|
|
908
|
+
hint.above = true
|
|
909
|
+
} else if frame.minY >= scrollContainerAnchor.rect.maxY {
|
|
910
|
+
hint.below = true
|
|
911
|
+
} else {
|
|
912
|
+
return
|
|
913
|
+
}
|
|
914
|
+
hints[scrollContainerAnchor.index] = hint
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
private func applyHiddenContentHints(
|
|
918
|
+
_ hints: [Int: (above: Bool, below: Bool)],
|
|
919
|
+
to nodes: [SnapshotNode]
|
|
920
|
+
) -> [SnapshotNode] {
|
|
921
|
+
if hints.isEmpty { return nodes }
|
|
922
|
+
return nodes.map { node in
|
|
923
|
+
guard let hint = hints[node.index] else { return node }
|
|
924
|
+
let hiddenContentAbove: Bool? = (node.hiddenContentAbove == true || hint.above) ? true : nil
|
|
925
|
+
let hiddenContentBelow: Bool? = (node.hiddenContentBelow == true || hint.below) ? true : nil
|
|
926
|
+
return SnapshotNode(
|
|
927
|
+
index: node.index,
|
|
928
|
+
type: node.type,
|
|
929
|
+
label: node.label,
|
|
930
|
+
identifier: node.identifier,
|
|
931
|
+
value: node.value,
|
|
932
|
+
rect: node.rect,
|
|
933
|
+
enabled: node.enabled,
|
|
934
|
+
focused: node.focused,
|
|
935
|
+
selected: node.selected,
|
|
936
|
+
hittable: node.hittable,
|
|
937
|
+
depth: node.depth,
|
|
938
|
+
parentIndex: node.parentIndex,
|
|
939
|
+
hiddenContentAbove: hiddenContentAbove,
|
|
940
|
+
hiddenContentBelow: hiddenContentBelow
|
|
941
|
+
)
|
|
942
|
+
}
|
|
542
943
|
}
|
|
543
944
|
|
|
544
945
|
private func collapsedTabFallbackNodes(
|
|
@@ -710,8 +1111,122 @@ extension RunnerTests {
|
|
|
710
1111
|
return containerLabel == label && containerIdentifier == identifier
|
|
711
1112
|
}
|
|
712
1113
|
|
|
713
|
-
private func
|
|
714
|
-
|
|
1114
|
+
private func flatInteractiveElements(
|
|
1115
|
+
app: XCUIApplication,
|
|
1116
|
+
deadline: Date
|
|
1117
|
+
) -> (elements: [XCUIElement], truncated: Bool) {
|
|
1118
|
+
let queries: [XCUIElementQuery] = [
|
|
1119
|
+
app.buttons,
|
|
1120
|
+
app.links,
|
|
1121
|
+
app.textFields,
|
|
1122
|
+
app.secureTextFields,
|
|
1123
|
+
app.searchFields,
|
|
1124
|
+
app.textViews,
|
|
1125
|
+
app.switches,
|
|
1126
|
+
app.sliders,
|
|
1127
|
+
app.segmentedControls,
|
|
1128
|
+
app.cells,
|
|
1129
|
+
app.collectionViews,
|
|
1130
|
+
app.tables,
|
|
1131
|
+
app.scrollViews,
|
|
1132
|
+
app.pickers,
|
|
1133
|
+
app.steppers,
|
|
1134
|
+
app.tabBars,
|
|
1135
|
+
app.menuItems,
|
|
1136
|
+
app.staticTexts,
|
|
1137
|
+
app.images
|
|
1138
|
+
]
|
|
1139
|
+
|
|
1140
|
+
var elements: [XCUIElement] = []
|
|
1141
|
+
var truncated = false
|
|
1142
|
+
for query in queries {
|
|
1143
|
+
if Date() >= deadline {
|
|
1144
|
+
NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_FLAT_FALLBACK_DEADLINE")
|
|
1145
|
+
truncated = true
|
|
1146
|
+
break
|
|
1147
|
+
}
|
|
1148
|
+
let result = snapshotElementsQuery {
|
|
1149
|
+
query.allElementsBoundByIndex
|
|
1150
|
+
}
|
|
1151
|
+
elements.append(contentsOf: result.elements)
|
|
1152
|
+
if result.axUnavailable {
|
|
1153
|
+
break
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
return (elements, truncated)
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
private func snapshotElementsQuery(
|
|
1160
|
+
_ fetch: () -> [XCUIElement]
|
|
1161
|
+
) -> (elements: [XCUIElement], axUnavailable: Bool) {
|
|
1162
|
+
let (elements, exceptionMessage) = catchingObjCException(fallback: [], fetch)
|
|
1163
|
+
guard let exceptionMessage else {
|
|
1164
|
+
return (elements, false)
|
|
1165
|
+
}
|
|
1166
|
+
NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_QUERY_IGNORED_EXCEPTION=%@", exceptionMessage)
|
|
1167
|
+
if Self.isAxIllegalArgument(exceptionMessage) {
|
|
1168
|
+
invalidateCachedTarget(reason: "ax_snapshot_query_unavailable")
|
|
1169
|
+
return ([], true)
|
|
1170
|
+
}
|
|
1171
|
+
return ([], false)
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
private func flatSnapshotNode(
|
|
1175
|
+
element: XCUIElement,
|
|
1176
|
+
index: Int,
|
|
1177
|
+
parentIndex: Int?,
|
|
1178
|
+
viewport: CGRect,
|
|
1179
|
+
options: SnapshotOptions
|
|
1180
|
+
) -> SnapshotNode? {
|
|
1181
|
+
var node: SnapshotNode?
|
|
1182
|
+
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
1183
|
+
if !element.exists { return }
|
|
1184
|
+
let frame = element.frame
|
|
1185
|
+
if frame.isNull || frame.isEmpty { return }
|
|
1186
|
+
let visible = isVisibleInViewport(frame, viewport)
|
|
1187
|
+
if options.interactiveOnly && !visible { return }
|
|
1188
|
+
#if os(macOS)
|
|
1189
|
+
if !visible { return }
|
|
1190
|
+
#endif
|
|
1191
|
+
let label = element.label.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1192
|
+
let identifier = element.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1193
|
+
let valueText = snapshotValueText(element)
|
|
1194
|
+
let hasContent = !label.isEmpty || !identifier.isEmpty || valueText != nil
|
|
1195
|
+
let elementType = element.elementType
|
|
1196
|
+
let enabled = element.isEnabled
|
|
1197
|
+
let hittable = visible && enabled && element.isHittable
|
|
1198
|
+
if options.compact && !hasContent && !hittable && !interactiveTypes.contains(elementType) {
|
|
1199
|
+
return
|
|
1200
|
+
}
|
|
1201
|
+
if let scope = options.scope?.trimmingCharacters(in: .whitespacesAndNewlines), !scope.isEmpty {
|
|
1202
|
+
let haystack = [label, identifier, valueText ?? ""].joined(separator: "\n")
|
|
1203
|
+
if !haystack.localizedCaseInsensitiveContains(scope) {
|
|
1204
|
+
return
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
node = SnapshotNode(
|
|
1209
|
+
index: index,
|
|
1210
|
+
type: elementTypeName(elementType),
|
|
1211
|
+
label: label.isEmpty ? nil : label,
|
|
1212
|
+
identifier: identifier.isEmpty ? nil : identifier,
|
|
1213
|
+
value: valueText,
|
|
1214
|
+
rect: snapshotRect(from: frame),
|
|
1215
|
+
enabled: enabled,
|
|
1216
|
+
focused: elementHasFocus(element) ? true : nil,
|
|
1217
|
+
selected: element.isSelected ? true : nil,
|
|
1218
|
+
hittable: hittable,
|
|
1219
|
+
depth: 1,
|
|
1220
|
+
parentIndex: parentIndex,
|
|
1221
|
+
hiddenContentAbove: nil,
|
|
1222
|
+
hiddenContentBelow: nil
|
|
1223
|
+
)
|
|
1224
|
+
})
|
|
1225
|
+
if let exceptionMessage {
|
|
1226
|
+
NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_FLAT_IGNORED_EXCEPTION=%@", exceptionMessage)
|
|
1227
|
+
return nil
|
|
1228
|
+
}
|
|
1229
|
+
return node
|
|
715
1230
|
}
|
|
716
1231
|
|
|
717
1232
|
private func isScrollableContainer(_ snapshot: XCUIElementSnapshot, visible: Bool) -> Bool {
|