Skip to content

Commit 930b45e

Browse files
adding hierarchy parser
1 parent dc50558 commit 930b45e

2 files changed

Lines changed: 861 additions & 13 deletions

File tree

Example/UnitTests/AccessibilityHierarchyParserTests.swift

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)