@@ -89,7 +89,15 @@ extension RunnerTests {
8989 return blocking
9090 }
9191
92- guard let context = try makeSnapshotTraversalContext ( app: app, options: options) else {
92+ let context : SnapshotTraversalContext ?
93+ do {
94+ context = try makeSnapshotTraversalContext ( app: app, options: options)
95+ } catch let failure as SnapshotCaptureFailure where options. interactiveOnly {
96+ NSLog ( " AGENT_DEVICE_RUNNER_SNAPSHOT_FLAT_FALLBACK=%@ " , failure. message)
97+ return snapshotFlatInteractive ( app: app, options: options)
98+ }
99+
100+ guard let context else {
93101 return DataPayload ( nodes: [ ] , truncated: false )
94102 }
95103
@@ -248,6 +256,82 @@ extension RunnerTests {
248256 return DataPayload ( nodes: nodes, truncated: truncated)
249257 }
250258
259+ private func snapshotFlatInteractive( app: XCUIApplication , options: SnapshotOptions ) -> DataPayload {
260+ let viewport = safeSnapshotViewport ( app: app)
261+ var nodes : [ SnapshotNode ] = [
262+ SnapshotNode (
263+ index: 0 ,
264+ type: " Application " ,
265+ label: nonEmptyElementText { app. label } ,
266+ identifier: nonEmptyElementText { app. identifier } ,
267+ value: nil ,
268+ rect: snapshotRect ( from: safely ( " SNAPSHOT_FLAT_APP_FRAME " , CGRect . zero) { app. frame } ) ,
269+ enabled: true ,
270+ focused: nil ,
271+ selected: nil ,
272+ hittable: false ,
273+ depth: 0 ,
274+ parentIndex: nil ,
275+ hiddenContentAbove: nil ,
276+ hiddenContentBelow: nil
277+ )
278+ ]
279+ if options. depth == 0 {
280+ return DataPayload ( nodes: nodes, truncated: false )
281+ }
282+
283+ var seen = Set < String > ( )
284+ let candidates = flatFallbackElements ( app: app)
285+ . compactMap { element in
286+ flatSnapshotNode (
287+ element: element,
288+ index: 0 ,
289+ parentIndex: 0 ,
290+ viewport: viewport,
291+ options: options
292+ )
293+ }
294+ . filter { node in
295+ let key = " \( node. type) - \( node. label ?? " " ) - \( node. identifier ?? " " ) - \( node. value ?? " " ) - \( node. rect. x) - \( node. rect. y) - \( node. rect. width) - \( node. rect. height) "
296+ if seen. contains ( key) { return false }
297+ seen. insert ( key)
298+ return true
299+ }
300+ . sorted { left, right in
301+ if left. rect. y != right. rect. y {
302+ return left. rect. y < right. rect. y
303+ }
304+ if left. rect. x != right. rect. x {
305+ return left. rect. x < right. rect. x
306+ }
307+ return left. type < right. type
308+ }
309+
310+ let remaining = max ( 0 , fastSnapshotLimit - nodes. count)
311+ let truncated = candidates. count > remaining
312+ for candidate in candidates. prefix ( remaining) {
313+ nodes. append (
314+ SnapshotNode (
315+ index: nodes. count,
316+ type: candidate. type,
317+ label: candidate. label,
318+ identifier: candidate. identifier,
319+ value: candidate. value,
320+ rect: candidate. rect,
321+ enabled: candidate. enabled,
322+ focused: candidate. focused,
323+ selected: candidate. selected,
324+ hittable: candidate. hittable,
325+ depth: 1 ,
326+ parentIndex: 0 ,
327+ hiddenContentAbove: nil ,
328+ hiddenContentBelow: nil
329+ )
330+ )
331+ }
332+ return DataPayload ( nodes: nodes, truncated: truncated)
333+ }
334+
251335 func snapshotRect( from frame: CGRect ) -> SnapshotRect {
252336 return SnapshotRect (
253337 x: Double ( frame. origin. x) ,
@@ -714,6 +798,95 @@ extension RunnerTests {
714798 safely ( " SNAPSHOT_QUERY " , [ ] , fetch)
715799 }
716800
801+ private func flatFallbackElements( app: XCUIApplication ) -> [ XCUIElement ] {
802+ let queries : [ XCUIElementQuery ] = [
803+ app. buttons,
804+ app. cells,
805+ app. collectionViews,
806+ app. images,
807+ app. links,
808+ app. scrollViews,
809+ app. searchFields,
810+ app. secureTextFields,
811+ app. segmentedControls,
812+ app. sliders,
813+ app. staticTexts,
814+ app. switches,
815+ app. tables,
816+ app. textFields,
817+ app. textViews
818+ ]
819+ return queries. flatMap { query in
820+ safeSnapshotElementsQuery {
821+ query. allElementsBoundByIndex
822+ }
823+ }
824+ }
825+
826+ private func flatSnapshotNode(
827+ element: XCUIElement ,
828+ index: Int ,
829+ parentIndex: Int ? ,
830+ viewport: CGRect ,
831+ options: SnapshotOptions
832+ ) -> SnapshotNode ? {
833+ var node : SnapshotNode ?
834+ let exceptionMessage = RunnerObjCExceptionCatcher . catchException ( {
835+ if !element. exists { return }
836+ let frame = element. frame
837+ if frame. isNull || frame. isEmpty { return }
838+ let visible = isVisibleInViewport ( frame, viewport)
839+ #if os(macOS)
840+ if !visible { return }
841+ #endif
842+ let label = element. label. trimmingCharacters ( in: . whitespacesAndNewlines)
843+ let identifier = element. identifier. trimmingCharacters ( in: . whitespacesAndNewlines)
844+ let valueText = snapshotValueText ( element)
845+ let hasContent = !label. isEmpty || !identifier. isEmpty || valueText != nil
846+ let elementType = element. elementType
847+ let enabled = element. isEnabled
848+ let hittable = visible && enabled && element. isHittable
849+ if options. compact && !hasContent && !hittable && !interactiveTypes. contains ( elementType) {
850+ return
851+ }
852+ if let scope = options. scope? . trimmingCharacters ( in: . whitespacesAndNewlines) , !scope. isEmpty {
853+ let haystack = [ label, identifier, valueText ?? " " ] . joined ( separator: " \n " )
854+ if !haystack. localizedCaseInsensitiveContains ( scope) {
855+ return
856+ }
857+ }
858+
859+ node = SnapshotNode (
860+ index: index,
861+ type: elementTypeName ( elementType) ,
862+ label: label. isEmpty ? nil : label,
863+ identifier: identifier. isEmpty ? nil : identifier,
864+ value: valueText,
865+ rect: snapshotRect ( from: frame) ,
866+ enabled: enabled,
867+ focused: elementHasFocus ( element) ? true : nil ,
868+ selected: element. isSelected ? true : nil ,
869+ hittable: hittable,
870+ depth: 1 ,
871+ parentIndex: parentIndex,
872+ hiddenContentAbove: nil ,
873+ hiddenContentBelow: nil
874+ )
875+ } )
876+ if let exceptionMessage {
877+ NSLog ( " AGENT_DEVICE_RUNNER_SNAPSHOT_FLAT_IGNORED_EXCEPTION=%@ " , exceptionMessage)
878+ return nil
879+ }
880+ return node
881+ }
882+
883+ private func nonEmptyElementText( _ read: ( ) -> String ) -> String ? {
884+ let value = safely ( " SNAPSHOT_FLAT_TEXT " , " " ) {
885+ read ( )
886+ } . trimmingCharacters ( in: . whitespacesAndNewlines)
887+ return value. isEmpty ? nil : value
888+ }
889+
717890 private func isScrollableContainer( _ snapshot: XCUIElementSnapshot , visible: Bool ) -> Bool {
718891 if !visible { return false }
719892 if !Self. scrollContainerTypes. contains ( snapshot. elementType) { return false }
0 commit comments