11import XCTest
22
33extension RunnerTests {
4+ private static let axSnapshotErrorCode = " IOS_AX_SNAPSHOT_FAILED "
5+ private static let axSnapshotHint =
6+ " XCTest could not serialize this iOS accessibility tree. Try a smaller read such as snapshot -s <visible label or id> -d 8, use direct selector commands such as find id <value> click, or use screenshot/logs/appstate in the same session. If you own the app and need full-tree inspection, consider flagging this screen for accessibility-tree simplification: reduce unnecessary accessible wrapper nesting and expose stable ids on actionable controls. "
47 private static let collapsedTabCandidateTypes : Set < XCUIElement . ElementType > = [
58 . button,
69 . link,
@@ -33,6 +36,12 @@ extension RunnerTests {
3336 let visible : Bool
3437 }
3538
39+ struct SnapshotCaptureFailure : Error {
40+ let code : String
41+ let message : String
42+ let hint : String
43+ }
44+
3645 // MARK: - Snapshot Entry
3746
3847 func elementTypeName( _ type: XCUIElement . ElementType ) -> String {
@@ -75,12 +84,12 @@ extension RunnerTests {
7584 }
7685 }
7786
78- func snapshotFast( app: XCUIApplication , options: SnapshotOptions ) -> DataPayload {
87+ func snapshotFast( app: XCUIApplication , options: SnapshotOptions ) throws -> DataPayload {
7988 if let blocking = blockingSystemAlertSnapshot ( ) {
8089 return blocking
8190 }
8291
83- guard let context = makeSnapshotTraversalContext ( app: app, options: options) else {
92+ guard let context = try makeSnapshotTraversalContext ( app: app, options: options) else {
8493 return DataPayload ( nodes: [ ] , truncated: false )
8594 }
8695
@@ -186,12 +195,12 @@ extension RunnerTests {
186195 return DataPayload ( nodes: nodes, truncated: truncated)
187196 }
188197
189- func snapshotRaw( app: XCUIApplication , options: SnapshotOptions ) -> DataPayload {
198+ func snapshotRaw( app: XCUIApplication , options: SnapshotOptions ) throws -> DataPayload {
190199 if let blocking = blockingSystemAlertSnapshot ( ) {
191200 return blocking
192201 }
193202
194- guard let context = makeSnapshotTraversalContext ( app: app, options: options) else {
203+ guard let context = try makeSnapshotTraversalContext ( app: app, options: options) else {
195204 return DataPayload ( nodes: [ ] , truncated: false )
196205 }
197206
@@ -304,14 +313,11 @@ extension RunnerTests {
304313 private func makeSnapshotTraversalContext(
305314 app: XCUIApplication ,
306315 options: SnapshotOptions
307- ) -> SnapshotTraversalContext ? {
308- let viewport = snapshotViewport ( app: app)
316+ ) throws -> SnapshotTraversalContext ? {
317+ let viewport = safeSnapshotViewport ( app: app)
309318 let queryRoot = options. scope. flatMap { findScopeElement ( app: app, scope: $0) } ?? app
310319
311- let rootSnapshot : XCUIElementSnapshot
312- do {
313- rootSnapshot = try queryRoot. snapshot ( )
314- } catch {
320+ guard let rootSnapshot = try captureSnapshotRoot ( queryRoot) else {
315321 return nil
316322 }
317323
@@ -326,6 +332,70 @@ extension RunnerTests {
326332 )
327333 }
328334
335+ private func captureSnapshotRoot( _ element: XCUIElement ) throws -> XCUIElementSnapshot ? {
336+ var rootSnapshot : XCUIElementSnapshot ?
337+ var swiftErrorMessage : String ?
338+ let exceptionMessage = RunnerObjCExceptionCatcher . catchException ( {
339+ do {
340+ rootSnapshot = try element. snapshot ( )
341+ } catch {
342+ swiftErrorMessage = describeSnapshotError ( error)
343+ }
344+ } )
345+
346+ if let rootSnapshot {
347+ return rootSnapshot
348+ }
349+ let message = exceptionMessage ?? swiftErrorMessage ?? " snapshot returned no root "
350+ if Self . isAxIllegalArgument ( message) {
351+ throw axSnapshotFailure ( message)
352+ }
353+ return nil
354+ }
355+
356+ private func safeSnapshotViewport( app: XCUIApplication ) -> CGRect {
357+ var viewport = CGRect . infinite
358+ let exceptionMessage = RunnerObjCExceptionCatcher . catchException ( {
359+ viewport = snapshotViewport ( app: app)
360+ } )
361+ if let exceptionMessage {
362+ NSLog ( " AGENT_DEVICE_RUNNER_SNAPSHOT_VIEWPORT_IGNORED_EXCEPTION=%@ " , exceptionMessage)
363+ }
364+ return viewport
365+ }
366+
367+ private func describeSnapshotError( _ error: Error ) -> String {
368+ let localized = error. localizedDescription
369+ let debug = String ( describing: error)
370+ if localized. isEmpty { return debug }
371+ if debug == localized { return localized }
372+ return " \( localized) ( \( debug) ) "
373+ }
374+
375+ private func axSnapshotFailure( _ message: String ) -> SnapshotCaptureFailure {
376+ let failureMessage : String
377+ if Self . hasAxIllegalArgumentCode ( message) {
378+ failureMessage = " iOS XCTest snapshot failed with kAXErrorIllegalArgument. \( message) "
379+ } else {
380+ failureMessage = " iOS XCTest snapshot failed while serializing the accessibility tree. \( message) "
381+ }
382+ return SnapshotCaptureFailure (
383+ code: Self . axSnapshotErrorCode,
384+ message: failureMessage,
385+ hint: Self . axSnapshotHint
386+ )
387+ }
388+
389+ private static func isAxIllegalArgument( _ message: String ) -> Bool {
390+ let normalized = message. lowercased ( )
391+ return hasAxIllegalArgumentCode ( normalized)
392+ || ( normalized. contains ( " illegal argument " ) && normalized. contains ( " snapshot " ) )
393+ }
394+
395+ private static func hasAxIllegalArgumentCode( _ message: String ) -> Bool {
396+ return message. lowercased ( ) . contains ( " kaxerrorillegalargument " )
397+ }
398+
329399 private func evaluateSnapshot(
330400 _ snapshot: XCUIElementSnapshot ,
331401 in context: SnapshotTraversalContext
0 commit comments