@@ -138,6 +138,288 @@ final class AccessibilityHierarchyParserTests: XCTestCase {
138138 XCTAssertEqual ( padAgain, [ " C " , " D " , " B " , " A " ] )
139139
140140 }
141+
142+ // MARK: - Hierarchy API Tests
143+
144+ func testHierarchyStructure_FlatElements( ) {
145+ // Test that flat list of elements produces correct hierarchy
146+ let containerView = UIView ( frame: . init( x: 0 , y: 0 , width: 100 , height: 100 ) )
147+
148+ let elementA = UIView ( frame: . init( x: 0 , y: 0 , width: 50 , height: 50 ) )
149+ elementA. isAccessibilityElement = true
150+ elementA. accessibilityLabel = " Element A "
151+ containerView. addSubview ( elementA)
152+
153+ let elementB = UIView ( frame: . init( x: 50 , y: 0 , width: 50 , height: 50 ) )
154+ elementB. isAccessibilityElement = true
155+ elementB. accessibilityLabel = " Element B "
156+ containerView. addSubview ( elementB)
157+
158+ let parser = AccessibilityHierarchyParser ( )
159+ let hierarchy = parser. parseAccessibilityHierarchy (
160+ in: containerView,
161+ userInterfaceLayoutDirectionProvider: TestUserInterfaceLayoutDirectionProvider ( userInterfaceLayoutDirection: . leftToRight) ,
162+ userInterfaceIdiomProvider: TestUserInterfaceIdiomProvider ( userInterfaceIdiom: . phone)
163+ )
164+
165+ // Should have 2 element nodes at root level (no containers)
166+ XCTAssertEqual ( hierarchy. count, 2 )
167+
168+ // Both should be element nodes
169+ guard case . element( let markerA, let indexA) = hierarchy [ 0 ] else {
170+ XCTFail ( " Expected element node " )
171+ return
172+ }
173+ XCTAssertEqual ( markerA. label, " Element A " )
174+ XCTAssertEqual ( indexA, 0 )
175+
176+ guard case . element( let markerB, let indexB) = hierarchy [ 1 ] else {
177+ XCTFail ( " Expected element node " )
178+ return
179+ }
180+ XCTAssertEqual ( markerB. label, " Element B " )
181+ XCTAssertEqual ( indexB, 1 )
182+ }
183+
184+ func testHierarchyStructure_ListContainer( ) {
185+ // Test that list container appears as explicit node
186+ let containerView = UIView ( frame: . init( x: 0 , y: 0 , width: 100 , height: 200 ) )
187+
188+ let listContainer = UIView ( frame: . init( x: 0 , y: 0 , width: 100 , height: 100 ) )
189+ listContainer. accessibilityContainerType = . list
190+ listContainer. accessibilityLabel = " My List "
191+ containerView. addSubview ( listContainer)
192+
193+ let item1 = UIView ( frame: . init( x: 0 , y: 0 , width: 100 , height: 50 ) )
194+ item1. isAccessibilityElement = true
195+ item1. accessibilityLabel = " Item 1 "
196+ listContainer. addSubview ( item1)
197+
198+ let item2 = UIView ( frame: . init( x: 0 , y: 50 , width: 100 , height: 50 ) )
199+ item2. isAccessibilityElement = true
200+ item2. accessibilityLabel = " Item 2 "
201+ listContainer. addSubview ( item2)
202+
203+ let parser = AccessibilityHierarchyParser ( )
204+ let hierarchy = parser. parseAccessibilityHierarchy (
205+ in: containerView,
206+ userInterfaceLayoutDirectionProvider: TestUserInterfaceLayoutDirectionProvider ( userInterfaceLayoutDirection: . leftToRight) ,
207+ userInterfaceIdiomProvider: TestUserInterfaceIdiomProvider ( userInterfaceIdiom: . phone)
208+ )
209+
210+ // Should have 1 container node at root
211+ XCTAssertEqual ( hierarchy. count, 1 )
212+
213+ // Should be a container node
214+ guard case . container( let containerInfo, let containerChildren) = hierarchy [ 0 ] else {
215+ XCTFail ( " Expected container node " )
216+ return
217+ }
218+
219+ XCTAssertEqual ( containerInfo. type, . list)
220+ XCTAssertEqual ( containerInfo. label, " My List " )
221+ XCTAssertNil ( hierarchy [ 0 ] . traversalIndex) // Container nodes don't have traversal index
222+
223+ // Should have 2 children
224+ XCTAssertEqual ( containerChildren. count, 2 )
225+
226+ guard case . element( let marker1, let index1) = containerChildren [ 0 ] else {
227+ XCTFail ( " Expected element node " )
228+ return
229+ }
230+ XCTAssertEqual ( marker1. label, " Item 1 " )
231+ XCTAssertEqual ( index1, 0 )
232+
233+ guard case . element( let marker2, let index2) = containerChildren [ 1 ] else {
234+ XCTFail ( " Expected element node " )
235+ return
236+ }
237+ XCTAssertEqual ( marker2. label, " Item 2 " )
238+ XCTAssertEqual ( index2, 1 )
239+ }
240+
241+ func testHierarchyStructure_SemanticGroupFlattening( ) {
242+ // Test that semantic groups without labels are flattened
243+ let containerView = UIView ( frame: . init( x: 0 , y: 0 , width: 100 , height: 200 ) )
244+
245+ let semanticGroup = UIView ( frame: . init( x: 0 , y: 0 , width: 100 , height: 100 ) )
246+ semanticGroup. shouldGroupAccessibilityChildren = true
247+ // No label - should be flattened
248+ containerView. addSubview ( semanticGroup)
249+
250+ let item1 = UIView ( frame: . init( x: 0 , y: 0 , width: 100 , height: 50 ) )
251+ item1. isAccessibilityElement = true
252+ item1. accessibilityLabel = " Item 1 "
253+ semanticGroup. addSubview ( item1)
254+
255+ let item2 = UIView ( frame: . init( x: 0 , y: 50 , width: 100 , height: 50 ) )
256+ item2. isAccessibilityElement = true
257+ item2. accessibilityLabel = " Item 2 "
258+ semanticGroup. addSubview ( item2)
259+
260+ let parser = AccessibilityHierarchyParser ( )
261+ let hierarchy = parser. parseAccessibilityHierarchy (
262+ in: containerView,
263+ userInterfaceLayoutDirectionProvider: TestUserInterfaceLayoutDirectionProvider ( userInterfaceLayoutDirection: . leftToRight) ,
264+ userInterfaceIdiomProvider: TestUserInterfaceIdiomProvider ( userInterfaceIdiom: . phone)
265+ )
266+
267+ // Semantic group should be flattened - elements appear at root
268+ XCTAssertEqual ( hierarchy. count, 2 )
269+
270+ guard case . element( let marker1, _) = hierarchy [ 0 ] else {
271+ XCTFail ( " Expected element node " )
272+ return
273+ }
274+ XCTAssertEqual ( marker1. label, " Item 1 " )
275+
276+ guard case . element( let marker2, _) = hierarchy [ 1 ] else {
277+ XCTFail ( " Expected element node " )
278+ return
279+ }
280+ XCTAssertEqual ( marker2. label, " Item 2 " )
281+ }
282+
283+ func testHierarchyFlatteningMatchesOriginalOutput( ) {
284+ // Test that flattening hierarchy produces same result as flat array
285+ let containerView = UIView ( frame: . init( x: 0 , y: 0 , width: 100 , height: 300 ) )
286+
287+ let listContainer = UIView ( frame: . init( x: 0 , y: 0 , width: 100 , height: 100 ) )
288+ listContainer. accessibilityContainerType = . list
289+ containerView. addSubview ( listContainer)
290+
291+ let item1 = UIView ( frame: . init( x: 0 , y: 0 , width: 100 , height: 50 ) )
292+ item1. isAccessibilityElement = true
293+ item1. accessibilityLabel = " Item 1 "
294+ listContainer. addSubview ( item1)
295+
296+ let item2 = UIView ( frame: . init( x: 0 , y: 50 , width: 100 , height: 50 ) )
297+ item2. isAccessibilityElement = true
298+ item2. accessibilityLabel = " Item 2 "
299+ listContainer. addSubview ( item2)
300+
301+ let standaloneElement = UIView ( frame: . init( x: 0 , y: 100 , width: 100 , height: 50 ) )
302+ standaloneElement. isAccessibilityElement = true
303+ standaloneElement. accessibilityLabel = " Standalone "
304+ containerView. addSubview ( standaloneElement)
305+
306+ let parser = AccessibilityHierarchyParser ( )
307+
308+ // Get flat array
309+ let flatMarkers = parser. parseAccessibilityElements (
310+ in: containerView,
311+ userInterfaceLayoutDirectionProvider: TestUserInterfaceLayoutDirectionProvider ( userInterfaceLayoutDirection: . leftToRight) ,
312+ userInterfaceIdiomProvider: TestUserInterfaceIdiomProvider ( userInterfaceIdiom: . phone)
313+ )
314+
315+ // Get hierarchy and flatten it
316+ let hierarchy = parser. parseAccessibilityHierarchy (
317+ in: containerView,
318+ userInterfaceLayoutDirectionProvider: TestUserInterfaceLayoutDirectionProvider ( userInterfaceLayoutDirection: . leftToRight) ,
319+ userInterfaceIdiomProvider: TestUserInterfaceIdiomProvider ( userInterfaceIdiom: . phone)
320+ )
321+ let flattenedMarkers = hierarchy. flattenToMarkers ( )
322+
323+ // Should have same count
324+ XCTAssertEqual ( flatMarkers. count, flattenedMarkers. count)
325+
326+ // Should have same labels in same order
327+ XCTAssertEqual ( flatMarkers. map { $0. label } , flattenedMarkers. map { $0. label } )
328+ }
329+
330+ func testHierarchyTraversalOrder( ) {
331+ // Test that traversal indices are sequential
332+ let containerView = UIView ( frame: . init( x: 0 , y: 0 , width: 100 , height: 100 ) )
333+
334+ let elementA = UIView ( frame: . init( x: 0 , y: 0 , width: 50 , height: 50 ) )
335+ elementA. isAccessibilityElement = true
336+ elementA. accessibilityLabel = " A "
337+ containerView. addSubview ( elementA)
338+
339+ let elementB = UIView ( frame: . init( x: 50 , y: 0 , width: 50 , height: 50 ) )
340+ elementB. isAccessibilityElement = true
341+ elementB. accessibilityLabel = " B "
342+ containerView. addSubview ( elementB)
343+
344+ let elementC = UIView ( frame: . init( x: 0 , y: 50 , width: 50 , height: 50 ) )
345+ elementC. isAccessibilityElement = true
346+ elementC. accessibilityLabel = " C "
347+ containerView. addSubview ( elementC)
348+
349+ let parser = AccessibilityHierarchyParser ( )
350+ let hierarchy = parser. parseAccessibilityHierarchy (
351+ in: containerView,
352+ userInterfaceLayoutDirectionProvider: TestUserInterfaceLayoutDirectionProvider ( userInterfaceLayoutDirection: . leftToRight) ,
353+ userInterfaceIdiomProvider: TestUserInterfaceIdiomProvider ( userInterfaceIdiom: . phone)
354+ )
355+
356+ // Collect all traversal indices
357+ var indices : [ Int ] = [ ]
358+ for node in hierarchy {
359+ if let index = node. traversalIndex {
360+ indices. append ( index)
361+ }
362+ }
363+
364+ // Should be sequential: 0, 1, 2
365+ XCTAssertEqual ( indices, [ 0 , 1 , 2 ] )
366+ }
367+
368+ func testHierarchyContextInformation( ) {
369+ // Test that context information is populated correctly
370+ let containerView = UIView ( frame: . init( x: 0 , y: 0 , width: 100 , height: 200 ) )
371+
372+ let listContainer = UIView ( frame: . init( x: 0 , y: 0 , width: 100 , height: 100 ) )
373+ listContainer. accessibilityContainerType = . list
374+ containerView. addSubview ( listContainer)
375+
376+ let item1 = UIView ( frame: . init( x: 0 , y: 0 , width: 100 , height: 50 ) )
377+ item1. isAccessibilityElement = true
378+ item1. accessibilityLabel = " Item 1 "
379+ listContainer. addSubview ( item1)
380+
381+ let item2 = UIView ( frame: . init( x: 0 , y: 50 , width: 100 , height: 50 ) )
382+ item2. isAccessibilityElement = true
383+ item2. accessibilityLabel = " Item 2 "
384+ listContainer. addSubview ( item2)
385+
386+ let parser = AccessibilityHierarchyParser ( )
387+ let hierarchy = parser. parseAccessibilityHierarchy (
388+ in: containerView,
389+ userInterfaceLayoutDirectionProvider: TestUserInterfaceLayoutDirectionProvider ( userInterfaceLayoutDirection: . leftToRight) ,
390+ userInterfaceIdiomProvider: TestUserInterfaceIdiomProvider ( userInterfaceIdiom: . phone)
391+ )
392+
393+ // Check first item has list start context
394+ guard case . container( let containerInfo, let containerChildren) = hierarchy [ 0 ] else {
395+ XCTFail ( " Expected container node " )
396+ return
397+ }
398+ XCTAssertEqual ( containerInfo. type, . list)
399+
400+ guard case . element( let marker1, _) = containerChildren [ 0 ] else {
401+ XCTFail ( " Expected element node " )
402+ return
403+ }
404+
405+ if case . listBoundary( let position) = marker1. context {
406+ XCTAssertEqual ( position, . start)
407+ } else {
408+ XCTFail ( " Expected list boundary context " )
409+ }
410+
411+ // Check last item has list end context
412+ guard case . element( let marker2, _) = containerChildren [ 1 ] else {
413+ XCTFail ( " Expected element node " )
414+ return
415+ }
416+
417+ if case . listBoundary( let position) = marker2. context {
418+ XCTAssertEqual ( position, . end)
419+ } else {
420+ XCTFail ( " Expected list boundary context " )
421+ }
422+ }
141423}
142424
143425// MARK: -
0 commit comments