Flutter iOS Embedder
SemanticsObjectTest.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #import <OCMock/OCMock.h>
6 #import <XCTest/XCTest.h>
7 
15 
17 
18 const float kFloatCompareEpsilon = 0.001;
19 
20 @interface SemanticsObject (UIFocusSystem) <UIFocusItem, UIFocusItemContainer>
21 @end
22 
24  UIFocusItemScrollableContainer>
25 @end
26 
28 - (UIView<UITextInput>*)textInputSurrogate;
29 @end
30 
31 @interface SemanticsObjectTest : XCTestCase
32 @end
33 
34 @implementation SemanticsObjectTest
35 
36 - (void)testCreate {
37  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
39  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
40  SemanticsObject* object = [[SemanticsObject alloc] initWithBridge:bridge uid:0];
41  XCTAssertNotNil(object);
42 }
43 
44 - (void)testSetChildren {
45  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
47  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
48  SemanticsObject* parent = [[SemanticsObject alloc] initWithBridge:bridge uid:0];
49  SemanticsObject* child = [[SemanticsObject alloc] initWithBridge:bridge uid:1];
50  parent.children = @[ child ];
51  XCTAssertEqual(parent, child.parent);
52  parent.children = @[];
53  XCTAssertNil(child.parent);
54 }
55 
56 - (void)testAccessibilityHitTestFocusAtLeaf {
57  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
59  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
60  SemanticsObject* object0 = [[SemanticsObject alloc] initWithBridge:bridge uid:0];
61  SemanticsObject* object1 = [[SemanticsObject alloc] initWithBridge:bridge uid:1];
62  SemanticsObject* object2 = [[SemanticsObject alloc] initWithBridge:bridge uid:2];
63  SemanticsObject* object3 = [[SemanticsObject alloc] initWithBridge:bridge uid:3];
64  object0.children = @[ object1 ];
65  object0.childrenInHitTestOrder = @[ object1 ];
66  object1.children = @[ object2, object3 ];
67  object1.childrenInHitTestOrder = @[ object2, object3 ];
68 
69  flutter::SemanticsNode node0;
70  node0.id = 0;
71  node0.rect = SkRect::MakeXYWH(0, 0, 200, 200);
72  node0.label = "0";
73  [object0 setSemanticsNode:&node0];
74 
75  flutter::SemanticsNode node1;
76  node1.id = 1;
77  node1.rect = SkRect::MakeXYWH(0, 0, 200, 200);
78  node1.label = "1";
79  [object1 setSemanticsNode:&node1];
80 
81  flutter::SemanticsNode node2;
82  node2.id = 2;
83  node2.rect = SkRect::MakeXYWH(0, 0, 100, 100);
84  node2.label = "2";
85  [object2 setSemanticsNode:&node2];
86 
87  flutter::SemanticsNode node3;
88  node3.id = 3;
89  node3.rect = SkRect::MakeXYWH(0, 0, 200, 200);
90  node3.label = "3";
91  [object3 setSemanticsNode:&node3];
92 
93  CGPoint point = CGPointMake(10, 10);
94  id hitTestResult = [object0 _accessibilityHitTest:point withEvent:nil];
95 
96  // Focus to object2 because it's the first object in hit test order
97  XCTAssertEqual(hitTestResult, object2);
98 }
99 
100 - (void)testAccessibilityHitTestNoFocusableItem {
101  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
103  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
104  SemanticsObject* object0 = [[SemanticsObject alloc] initWithBridge:bridge uid:0];
105  SemanticsObject* object1 = [[SemanticsObject alloc] initWithBridge:bridge uid:1];
106  SemanticsObject* object2 = [[SemanticsObject alloc] initWithBridge:bridge uid:2];
107  SemanticsObject* object3 = [[SemanticsObject alloc] initWithBridge:bridge uid:3];
108  object0.children = @[ object1 ];
109  object0.childrenInHitTestOrder = @[ object1 ];
110  object1.children = @[ object2, object3 ];
111  object1.childrenInHitTestOrder = @[ object2, object3 ];
112 
113  flutter::SemanticsNode node0;
114  node0.id = 0;
115  node0.rect = SkRect::MakeXYWH(0, 0, 200, 200);
116  [object0 setSemanticsNode:&node0];
117 
118  flutter::SemanticsNode node1;
119  node1.id = 1;
120  node1.rect = SkRect::MakeXYWH(0, 0, 200, 200);
121  [object1 setSemanticsNode:&node1];
122 
123  flutter::SemanticsNode node2;
124  node2.id = 2;
125  node2.rect = SkRect::MakeXYWH(0, 0, 100, 100);
126  [object2 setSemanticsNode:&node2];
127 
128  flutter::SemanticsNode node3;
129  node3.id = 3;
130  node3.rect = SkRect::MakeXYWH(0, 0, 200, 200);
131  [object3 setSemanticsNode:&node3];
132 
133  CGPoint point = CGPointMake(10, 10);
134  id hitTestResult = [object0 _accessibilityHitTest:point withEvent:nil];
135 
136  XCTAssertNil(hitTestResult);
137 }
138 
139 - (void)testAccessibilityScrollToVisible {
140  fml::WeakPtrFactory<flutter::testing::MockAccessibilityBridge> factory(
142  fml::WeakPtr<flutter::testing::MockAccessibilityBridge> bridge = factory.GetWeakPtr();
143  SemanticsObject* object3 = [[SemanticsObject alloc] initWithBridge:bridge uid:3];
144 
145  flutter::SemanticsNode node3;
146  node3.id = 3;
147  node3.rect = SkRect::MakeXYWH(0, 0, 200, 200);
148  [object3 setSemanticsNode:&node3];
149 
151 
152  XCTAssertTrue(bridge->observations.size() == 1);
153  XCTAssertTrue(bridge->observations[0].id == 3);
154  XCTAssertTrue(bridge->observations[0].action == flutter::SemanticsAction::kShowOnScreen);
155 }
156 
157 - (void)testFlutterScrollableSemanticsObjectIsNotAccessibilityElementWhenVoiceOverIsRunning {
159  mock->isVoiceOverRunningValue = true;
160  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(mock);
161  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
162 
163  flutter::SemanticsNode node;
164  node.flags.hasImplicitScrolling = true;
165 
166  node.actions = flutter::kHorizontalScrollSemanticsActions |
167  static_cast<int32_t>(flutter::SemanticsAction::kCustomAction);
168 
169  node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
170  node.scrollExtentMax = 100.0;
171  node.scrollPosition = 0.0;
172 
174  [[FlutterScrollableSemanticsObject alloc] initWithBridge:bridge uid:0];
175  [scrollable setSemanticsNode:&node];
177 
178  UIScrollView* scrollView = [scrollable nativeAccessibility];
179 
180  XCTAssertFalse(scrollView.isAccessibilityElement);
181 }
182 
183 - (void)testAccessibilityScrollToVisibleWithChild {
184  fml::WeakPtrFactory<flutter::testing::MockAccessibilityBridge> factory(
186  fml::WeakPtr<flutter::testing::MockAccessibilityBridge> bridge = factory.GetWeakPtr();
187  SemanticsObject* object3 = [[SemanticsObject alloc] initWithBridge:bridge uid:3];
188 
189  flutter::SemanticsNode node3;
190  node3.id = 3;
191  node3.rect = SkRect::MakeXYWH(0, 0, 200, 200);
192  [object3 setSemanticsNode:&node3];
193 
194  [object3 accessibilityScrollToVisibleWithChild:object3];
195 
196  XCTAssertTrue(bridge->observations.size() == 1);
197  XCTAssertTrue(bridge->observations[0].id == 3);
198  XCTAssertTrue(bridge->observations[0].action == flutter::SemanticsAction::kShowOnScreen);
199 }
200 
201 - (void)testAccessibilityHitTestOutOfRect {
202  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
204  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
205  SemanticsObject* object0 = [[SemanticsObject alloc] initWithBridge:bridge uid:0];
206  SemanticsObject* object1 = [[SemanticsObject alloc] initWithBridge:bridge uid:1];
207  SemanticsObject* object2 = [[SemanticsObject alloc] initWithBridge:bridge uid:2];
208  SemanticsObject* object3 = [[SemanticsObject alloc] initWithBridge:bridge uid:3];
209  object0.children = @[ object1 ];
210  object0.childrenInHitTestOrder = @[ object1 ];
211  object1.children = @[ object2, object3 ];
212  object1.childrenInHitTestOrder = @[ object2, object3 ];
213 
214  flutter::SemanticsNode node0;
215  node0.id = 0;
216  node0.rect = SkRect::MakeXYWH(0, 0, 200, 200);
217  node0.label = "0";
218  [object0 setSemanticsNode:&node0];
219 
220  flutter::SemanticsNode node1;
221  node1.id = 1;
222  node1.rect = SkRect::MakeXYWH(0, 0, 200, 200);
223  node1.label = "1";
224  [object1 setSemanticsNode:&node1];
225 
226  flutter::SemanticsNode node2;
227  node2.id = 2;
228  node2.rect = SkRect::MakeXYWH(0, 0, 100, 100);
229  node2.label = "2";
230  [object2 setSemanticsNode:&node2];
231 
232  flutter::SemanticsNode node3;
233  node3.id = 3;
234  node3.rect = SkRect::MakeXYWH(0, 0, 200, 200);
235  node3.label = "3";
236  [object3 setSemanticsNode:&node3];
237 
238  CGPoint point = CGPointMake(300, 300);
239  id hitTestResult = [object0 _accessibilityHitTest:point withEvent:nil];
240 
241  XCTAssertNil(hitTestResult);
242 }
243 
244 - (void)testReplaceChildAtIndex {
245  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
247  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
248  SemanticsObject* parent = [[SemanticsObject alloc] initWithBridge:bridge uid:0];
249  SemanticsObject* child1 = [[SemanticsObject alloc] initWithBridge:bridge uid:1];
250  SemanticsObject* child2 = [[SemanticsObject alloc] initWithBridge:bridge uid:2];
251  parent.children = @[ child1 ];
252  [parent replaceChildAtIndex:0 withChild:child2];
253  XCTAssertNil(child1.parent);
254  XCTAssertEqual(parent, child2.parent);
255  XCTAssertEqualObjects(parent.children, @[ child2 ]);
256 }
257 
258 - (void)testPlainSemanticsObjectWithLabelHasStaticTextTrait {
259  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
261  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
262  flutter::SemanticsNode node;
263  node.label = "foo";
264  FlutterSemanticsObject* object = [[FlutterSemanticsObject alloc] initWithBridge:bridge uid:0];
265  [object setSemanticsNode:&node];
266  XCTAssertEqual([object accessibilityTraits], UIAccessibilityTraitStaticText);
267 }
268 
269 - (void)testNodeWithImplicitScrollIsAnAccessibilityElementWhenItisHidden {
270  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
272  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
273  flutter::SemanticsNode node;
274 
275  node.flags.hasImplicitScrolling = true;
276  node.flags.isHidden = true;
277  FlutterSemanticsObject* object = [[FlutterSemanticsObject alloc] initWithBridge:bridge uid:0];
278  [object setSemanticsNode:&node];
279  XCTAssertEqual(object.isAccessibilityElement, YES);
280 }
281 
282 - (void)testNodeWithImplicitScrollIsNotAnAccessibilityElementWhenItisNotHidden {
283  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
285  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
286  flutter::SemanticsNode node;
287  node.flags.hasImplicitScrolling = true;
288  FlutterSemanticsObject* object = [[FlutterSemanticsObject alloc] initWithBridge:bridge uid:0];
289  [object setSemanticsNode:&node];
290  XCTAssertEqual(object.isAccessibilityElement, NO);
291 }
292 
293 - (void)testIntresetingSemanticsObjectWithLabelHasStaticTextTrait {
294  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
296  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
297  flutter::SemanticsNode node;
298  node.label = "foo";
299  FlutterSemanticsObject* object = [[FlutterSemanticsObject alloc] initWithBridge:bridge uid:0];
300  SemanticsObject* child1 = [[SemanticsObject alloc] initWithBridge:bridge uid:1];
301  object.children = @[ child1 ];
302  [object setSemanticsNode:&node];
303  XCTAssertEqual([object accessibilityTraits], UIAccessibilityTraitNone);
304 }
305 
306 - (void)testIntresetingSemanticsObjectWithLabelHasStaticTextTrait1 {
307  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
309  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
310  flutter::SemanticsNode node;
311  node.label = "foo";
312  node.flags.isTextField = true;
313  FlutterSemanticsObject* object = [[FlutterSemanticsObject alloc] initWithBridge:bridge uid:0];
314  [object setSemanticsNode:&node];
315  XCTAssertEqual([object accessibilityTraits], UIAccessibilityTraitNone);
316 }
317 
318 - (void)testIntresetingSemanticsObjectWithLabelHasStaticTextTrait2 {
319  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
321  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
322  flutter::SemanticsNode node;
323  node.label = "foo";
324 
325  node.flags.isButton = true;
326  FlutterSemanticsObject* object = [[FlutterSemanticsObject alloc] initWithBridge:bridge uid:0];
327  [object setSemanticsNode:&node];
328  XCTAssertEqual([object accessibilityTraits], UIAccessibilityTraitButton);
329 }
330 
331 - (void)testVerticalFlutterScrollableSemanticsObject {
332  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
334  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
335 
336  float transformScale = 0.5f;
337  float screenScale = [[bridge->view() window] screen].scale;
338  float effectivelyScale = transformScale / screenScale;
339  float x = 10;
340  float y = 10;
341  float w = 100;
342  float h = 200;
343  float scrollExtentMax = 500.0;
344  float scrollPosition = 150.0;
345 
346  flutter::SemanticsNode node;
347  node.flags.hasImplicitScrolling = true;
348  node.actions = flutter::kVerticalScrollSemanticsActions;
349  node.rect = SkRect::MakeXYWH(x, y, w, h);
350  node.scrollExtentMax = scrollExtentMax;
351  node.scrollPosition = scrollPosition;
352  node.transform = {
353  transformScale, 0, 0, 0, 0, transformScale, 0, 0, 0, 0, transformScale, 0, 0, 0, 0, 1.0};
355  [[FlutterScrollableSemanticsObject alloc] initWithBridge:bridge uid:0];
356  [scrollable setSemanticsNode:&node];
358  UIScrollView* scrollView = [scrollable nativeAccessibility];
359 
360  XCTAssertEqualWithAccuracy(scrollView.frame.origin.x, x * effectivelyScale, kFloatCompareEpsilon);
361  XCTAssertEqualWithAccuracy(scrollView.frame.origin.y, y * effectivelyScale, kFloatCompareEpsilon);
362  XCTAssertEqualWithAccuracy(scrollView.frame.size.width, w * effectivelyScale,
364  XCTAssertEqualWithAccuracy(scrollView.frame.size.height, h * effectivelyScale,
366 
367  XCTAssertEqualWithAccuracy(scrollView.contentSize.width, w * effectivelyScale,
369  XCTAssertEqualWithAccuracy(scrollView.contentSize.height,
370  (h + scrollExtentMax) * effectivelyScale, kFloatCompareEpsilon);
371 
372  XCTAssertEqual(scrollView.contentOffset.x, 0);
373  XCTAssertEqualWithAccuracy(scrollView.contentOffset.y, scrollPosition * effectivelyScale,
375 }
376 
377 - (void)testVerticalFlutterScrollableSemanticsObjectNoWindowDoesNotCrash {
378  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
380  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
381 
382  float transformScale = 0.5f;
383  float x = 10;
384  float y = 10;
385  float w = 100;
386  float h = 200;
387  float scrollExtentMax = 500.0;
388  float scrollPosition = 150.0;
389 
390  flutter::SemanticsNode node;
391  node.flags.hasImplicitScrolling = true;
392  node.actions = flutter::kVerticalScrollSemanticsActions;
393  node.rect = SkRect::MakeXYWH(x, y, w, h);
394  node.scrollExtentMax = scrollExtentMax;
395  node.scrollPosition = scrollPosition;
396  node.transform = {
397  transformScale, 0, 0, 0, 0, transformScale, 0, 0, 0, 0, transformScale, 0, 0, 0, 0, 1.0};
399  [[FlutterScrollableSemanticsObject alloc] initWithBridge:bridge uid:0];
400  [scrollable setSemanticsNode:&node];
402  XCTAssertNoThrow([scrollable accessibilityBridgeDidFinishUpdate]);
403 }
404 
405 - (void)testHorizontalFlutterScrollableSemanticsObject {
406  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
408  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
409 
410  float transformScale = 0.5f;
411  float screenScale = [[bridge->view() window] screen].scale;
412  float effectivelyScale = transformScale / screenScale;
413  float x = 10;
414  float y = 10;
415  float w = 100;
416  float h = 200;
417  float scrollExtentMax = 500.0;
418  float scrollPosition = 150.0;
419 
420  flutter::SemanticsNode node;
421  node.flags.hasImplicitScrolling = true;
422  node.actions = flutter::kHorizontalScrollSemanticsActions;
423  node.rect = SkRect::MakeXYWH(x, y, w, h);
424  node.scrollExtentMax = scrollExtentMax;
425  node.scrollPosition = scrollPosition;
426  node.transform = {
427  transformScale, 0, 0, 0, 0, transformScale, 0, 0, 0, 0, transformScale, 0, 0, 0, 0, 1.0};
429  [[FlutterScrollableSemanticsObject alloc] initWithBridge:bridge uid:0];
430  [scrollable setSemanticsNode:&node];
432  UIScrollView* scrollView = [scrollable nativeAccessibility];
433 
434  XCTAssertEqualWithAccuracy(scrollView.frame.origin.x, x * effectivelyScale, kFloatCompareEpsilon);
435  XCTAssertEqualWithAccuracy(scrollView.frame.origin.y, y * effectivelyScale, kFloatCompareEpsilon);
436  XCTAssertEqualWithAccuracy(scrollView.frame.size.width, w * effectivelyScale,
438  XCTAssertEqualWithAccuracy(scrollView.frame.size.height, h * effectivelyScale,
440 
441  XCTAssertEqualWithAccuracy(scrollView.contentSize.width, (w + scrollExtentMax) * effectivelyScale,
443  XCTAssertEqualWithAccuracy(scrollView.contentSize.height, h * effectivelyScale,
445 
446  XCTAssertEqualWithAccuracy(scrollView.contentOffset.x, scrollPosition * effectivelyScale,
448  XCTAssertEqual(scrollView.contentOffset.y, 0);
449 }
450 
451 - (void)testCanHandleInfiniteScrollExtent {
452  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
454  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
455 
456  float transformScale = 0.5f;
457  float screenScale = [[bridge->view() window] screen].scale;
458  float effectivelyScale = transformScale / screenScale;
459  float x = 10;
460  float y = 10;
461  float w = 100;
462  float h = 200;
463  float scrollExtentMax = INFINITY;
464  float scrollPosition = 150.0;
465 
466  flutter::SemanticsNode node;
467  node.flags.hasImplicitScrolling = true;
468  node.actions = flutter::kVerticalScrollSemanticsActions;
469  node.rect = SkRect::MakeXYWH(x, y, w, h);
470  node.scrollExtentMax = scrollExtentMax;
471  node.scrollPosition = scrollPosition;
472  node.transform = {
473  transformScale, 0, 0, 0, 0, transformScale, 0, 0, 0, 0, transformScale, 0, 0, 0, 0, 1.0};
475  [[FlutterScrollableSemanticsObject alloc] initWithBridge:bridge uid:0];
476  [scrollable setSemanticsNode:&node];
478  UIScrollView* scrollView = [scrollable nativeAccessibility];
479  XCTAssertEqualWithAccuracy(scrollView.frame.origin.x, x * effectivelyScale, kFloatCompareEpsilon);
480  XCTAssertEqualWithAccuracy(scrollView.frame.origin.y, y * effectivelyScale, kFloatCompareEpsilon);
481  XCTAssertEqualWithAccuracy(scrollView.frame.size.width, w * effectivelyScale,
483  XCTAssertEqualWithAccuracy(scrollView.frame.size.height, h * effectivelyScale,
485 
486  XCTAssertEqualWithAccuracy(scrollView.contentSize.width, w * effectivelyScale,
488  XCTAssertEqualWithAccuracy(scrollView.contentSize.height,
489  (h + kScrollExtentMaxForInf + scrollPosition) * effectivelyScale,
491 
492  XCTAssertEqual(scrollView.contentOffset.x, 0);
493  XCTAssertEqualWithAccuracy(scrollView.contentOffset.y, scrollPosition * effectivelyScale,
495 }
496 
497 - (void)testCanHandleNaNScrollExtentAndScrollPoisition {
498  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
500  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
501 
502  float transformScale = 0.5f;
503  float screenScale = [[bridge->view() window] screen].scale;
504  float effectivelyScale = transformScale / screenScale;
505  float x = 10;
506  float y = 10;
507  float w = 100;
508  float h = 200;
509  float scrollExtentMax = std::nan("");
510  float scrollPosition = std::nan("");
511 
512  flutter::SemanticsNode node;
513  node.flags.hasImplicitScrolling = true;
514  node.actions = flutter::kVerticalScrollSemanticsActions;
515  node.rect = SkRect::MakeXYWH(x, y, w, h);
516  node.scrollExtentMax = scrollExtentMax;
517  node.scrollPosition = scrollPosition;
518  node.transform = {
519  transformScale, 0, 0, 0, 0, transformScale, 0, 0, 0, 0, transformScale, 0, 0, 0, 0, 1.0};
521  [[FlutterScrollableSemanticsObject alloc] initWithBridge:bridge uid:0];
522  [scrollable setSemanticsNode:&node];
524  UIScrollView* scrollView = [scrollable nativeAccessibility];
525 
526  XCTAssertEqualWithAccuracy(scrollView.frame.origin.x, x * effectivelyScale, kFloatCompareEpsilon);
527  XCTAssertEqualWithAccuracy(scrollView.frame.origin.y, y * effectivelyScale, kFloatCompareEpsilon);
528  XCTAssertEqualWithAccuracy(scrollView.frame.size.width, w * effectivelyScale,
530  XCTAssertEqualWithAccuracy(scrollView.frame.size.height, h * effectivelyScale,
532 
533  // Content size equal to the scrollable size.
534  XCTAssertEqualWithAccuracy(scrollView.contentSize.width, w * effectivelyScale,
536  XCTAssertEqualWithAccuracy(scrollView.contentSize.height, h * effectivelyScale,
538 
539  XCTAssertEqual(scrollView.contentOffset.x, 0);
540  XCTAssertEqual(scrollView.contentOffset.y, 0);
541 }
542 
543 - (void)testFlutterScrollableSemanticsObjectIsNotHittestable {
544  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
546  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
547 
548  flutter::SemanticsNode node;
549  node.flags.hasImplicitScrolling = true;
550  node.actions = flutter::kHorizontalScrollSemanticsActions;
551  node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
552  node.scrollExtentMax = 100.0;
553  node.scrollPosition = 0.0;
554 
556  [[FlutterScrollableSemanticsObject alloc] initWithBridge:bridge uid:0];
557  [scrollable setSemanticsNode:&node];
559  UIScrollView* scrollView = [scrollable nativeAccessibility];
560  XCTAssertEqual([scrollView hitTest:CGPointMake(10, 10) withEvent:nil], nil);
561 }
562 
563 - (void)testFlutterScrollableSemanticsObjectIsHiddenWhenVoiceOverIsRunning {
565  mock->isVoiceOverRunningValue = false;
566  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(mock);
567  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
568 
569  flutter::SemanticsNode node;
570  node.flags.hasImplicitScrolling = true;
571  node.actions = flutter::kHorizontalScrollSemanticsActions;
572  node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
573  node.scrollExtentMax = 100.0;
574  node.scrollPosition = 0.0;
575 
577  [[FlutterScrollableSemanticsObject alloc] initWithBridge:bridge uid:0];
578  [scrollable setSemanticsNode:&node];
580  UIScrollView* scrollView = [scrollable nativeAccessibility];
581  XCTAssertTrue(scrollView.isAccessibilityElement);
582  mock->isVoiceOverRunningValue = true;
583  XCTAssertFalse(scrollView.isAccessibilityElement);
584 }
585 
586 - (void)testFlutterSemanticsObjectHasIdentifier {
588  mock->isVoiceOverRunningValue = true;
589  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(mock);
590  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
591 
592  flutter::SemanticsNode node;
593  node.identifier = "identifier";
594 
595  FlutterSemanticsObject* object = [[FlutterSemanticsObject alloc] initWithBridge:bridge uid:0];
596  [object setSemanticsNode:&node];
597  XCTAssertTrue([object.accessibilityIdentifier isEqualToString:@"identifier"]);
598 }
599 
600 - (void)testFlutterSemanticsObjectHasLocale {
602  mock->isVoiceOverRunningValue = true;
603  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(mock);
604  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
605 
606  flutter::SemanticsNode node;
607  node.locale = "es-MX";
608 
609  FlutterSemanticsObject* object = [[FlutterSemanticsObject alloc] initWithBridge:bridge uid:0];
610  [object setSemanticsNode:&node];
611  XCTAssertTrue([object.accessibilityLanguage isEqualToString:@"es-MX"]);
612 }
613 
614 - (void)testFlutterSemanticsObjectUseDefaultLocale {
616  mock->isVoiceOverRunningValue = true;
617  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(mock);
618  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
619 
620  flutter::SemanticsNode node;
621  mock->mockedLocale = @"es-MX";
622 
623  FlutterSemanticsObject* object = [[FlutterSemanticsObject alloc] initWithBridge:bridge uid:0];
624  [object setSemanticsNode:&node];
625  XCTAssertTrue([object.accessibilityLanguage isEqualToString:@"es-MX"]);
626 }
627 
628 - (void)testFlutterSemanticsObjectPrioritizedSectionLocale {
630  mock->isVoiceOverRunningValue = true;
631  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(mock);
632  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
633 
634  flutter::SemanticsNode node;
635  // Set both locales.
636  mock->mockedLocale = @"es-MX";
637  node.locale = "zh-TW";
638 
639  FlutterSemanticsObject* object = [[FlutterSemanticsObject alloc] initWithBridge:bridge uid:0];
640  [object setSemanticsNode:&node];
641  // node.locale takes priority.
642  XCTAssertTrue([object.accessibilityLanguage isEqualToString:@"zh-TW"]);
643 }
644 
645 - (void)testFlutterSemanticsObjectLocaleNil {
647  mock->isVoiceOverRunningValue = true;
648  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(mock);
649  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
650 
651  flutter::SemanticsNode node;
652 
653  FlutterSemanticsObject* object = [[FlutterSemanticsObject alloc] initWithBridge:bridge uid:0];
654  [object setSemanticsNode:&node];
655  XCTAssertTrue(object.accessibilityLanguage == nil);
656 }
657 
658 - (void)testFlutterScrollableSemanticsObjectWithLabelValueHintIsNotHiddenWhenVoiceOverIsRunning {
660  mock->isVoiceOverRunningValue = true;
661  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(mock);
662  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
663 
664  flutter::SemanticsNode node;
665  node.flags.hasImplicitScrolling = true;
666  node.actions = flutter::kHorizontalScrollSemanticsActions;
667  node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
668  node.label = "label";
669  node.value = "value";
670  node.hint = "hint";
671  node.scrollExtentMax = 100.0;
672  node.scrollPosition = 0.0;
673 
675  [[FlutterScrollableSemanticsObject alloc] initWithBridge:bridge uid:0];
676  [scrollable setSemanticsNode:&node];
678  UIScrollView* scrollView = [scrollable nativeAccessibility];
679  XCTAssertTrue(scrollView.isAccessibilityElement);
680  XCTAssertTrue(
681  [scrollView.accessibilityLabel isEqualToString:NSLocalizedString(@"label", @"test")]);
682  XCTAssertTrue(
683  [scrollView.accessibilityValue isEqualToString:NSLocalizedString(@"value", @"test")]);
684  XCTAssertTrue([scrollView.accessibilityHint isEqualToString:NSLocalizedString(@"hint", @"test")]);
685 }
686 
687 - (void)testFlutterSemanticsObjectMergeTooltipToLabel {
689  mock->isVoiceOverRunningValue = true;
690  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(mock);
691  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
692 
693  flutter::SemanticsNode node;
694  node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
695  node.label = "label";
696  node.tooltip = "tooltip";
697  FlutterSemanticsObject* object = [[FlutterSemanticsObject alloc] initWithBridge:bridge uid:0];
698  [object setSemanticsNode:&node];
699  XCTAssertTrue(object.isAccessibilityElement);
700  XCTAssertTrue([object.accessibilityLabel isEqualToString:@"label\ntooltip"]);
701 }
702 
703 - (void)testSemanticsObjectContainerAccessibilityFrameCoversEntireScreen {
705  mock->isVoiceOverRunningValue = true;
706  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(mock);
707  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
708 
709  flutter::SemanticsNode parent;
710  parent.id = 0;
711  parent.actions = static_cast<int32_t>(flutter::SemanticsAction::kTap);
712 
713  flutter::SemanticsNode child;
714  child.id = 1;
715  child.actions = static_cast<int32_t>(flutter::SemanticsAction::kTap);
716  child.rect = SkRect::MakeXYWH(0, 0, 100, 100);
717  parent.childrenInTraversalOrder.push_back(1);
718 
719  FlutterSemanticsObject* parentObject = [[FlutterSemanticsObject alloc] initWithBridge:bridge
720  uid:0];
721  [parentObject setSemanticsNode:&parent];
722 
723  FlutterSemanticsObject* childObject = [[FlutterSemanticsObject alloc] initWithBridge:bridge
724  uid:1];
725  [childObject setSemanticsNode:&child];
726 
727  parentObject.children = @[ childObject ];
728  [parentObject accessibilityBridgeDidFinishUpdate];
730 
731  SemanticsObjectContainer* container =
732  static_cast<SemanticsObjectContainer*>(parentObject.accessibilityContainer);
733 
734  XCTAssertTrue(childObject.accessibilityRespondsToUserInteraction);
735  XCTAssertTrue(CGRectEqualToRect(container.accessibilityFrame, UIScreen.mainScreen.bounds));
736 }
737 
738 - (void)testFlutterSemanticsObjectAttributedStringsDoNotCrashWhenEmpty {
740  mock->isVoiceOverRunningValue = true;
741  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(mock);
742  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
743 
744  flutter::SemanticsNode node;
745  node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
746  FlutterSemanticsObject* object = [[FlutterSemanticsObject alloc] initWithBridge:bridge uid:0];
747  [object setSemanticsNode:&node];
748  XCTAssertTrue(object.accessibilityAttributedLabel == nil);
749 }
750 
751 - (void)testFlutterScrollableSemanticsObjectReturnsParentContainerIfNoChildren {
753  mock->isVoiceOverRunningValue = true;
754  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(mock);
755  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
756 
757  flutter::SemanticsNode parent;
758  parent.id = 0;
759  parent.rect = SkRect::MakeXYWH(0, 0, 100, 200);
760  parent.label = "label";
761  parent.value = "value";
762  parent.hint = "hint";
763 
764  flutter::SemanticsNode node;
765  node.id = 1;
766  node.flags.hasImplicitScrolling = true;
767  node.actions = flutter::kHorizontalScrollSemanticsActions;
768  node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
769  node.label = "label";
770  node.value = "value";
771  node.hint = "hint";
772  node.scrollExtentMax = 100.0;
773  node.scrollPosition = 0.0;
774  parent.childrenInTraversalOrder.push_back(1);
775 
776  FlutterSemanticsObject* parentObject = [[FlutterSemanticsObject alloc] initWithBridge:bridge
777  uid:0];
778  [parentObject setSemanticsNode:&parent];
779 
781  [[FlutterScrollableSemanticsObject alloc] initWithBridge:bridge uid:1];
782  [scrollable setSemanticsNode:&node];
783  UIScrollView* scrollView = [scrollable nativeAccessibility];
784 
785  parentObject.children = @[ scrollable ];
786  [parentObject accessibilityBridgeDidFinishUpdate];
788  XCTAssertTrue(scrollView.isAccessibilityElement);
789  SemanticsObjectContainer* container =
790  static_cast<SemanticsObjectContainer*>(scrollable.accessibilityContainer);
791  XCTAssertEqual(container.semanticsObject, parentObject);
792 }
793 
794 - (void)testFlutterScrollableSemanticsObjectNoScrollBarOrContentInsets {
795  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
797  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
798 
799  flutter::SemanticsNode node;
800  node.flags.hasImplicitScrolling = true;
801  node.actions = flutter::kHorizontalScrollSemanticsActions;
802  node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
803  node.scrollExtentMax = 100.0;
804  node.scrollPosition = 0.0;
805 
807  [[FlutterScrollableSemanticsObject alloc] initWithBridge:bridge uid:0];
808  [scrollable setSemanticsNode:&node];
810  UIScrollView* scrollView = [scrollable nativeAccessibility];
811 
812  XCTAssertFalse(scrollView.showsHorizontalScrollIndicator);
813  XCTAssertFalse(scrollView.showsVerticalScrollIndicator);
814  XCTAssertEqual(scrollView.contentInsetAdjustmentBehavior,
815  UIScrollViewContentInsetAdjustmentNever);
816  XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(scrollView.contentInset, UIEdgeInsetsZero));
817 }
818 
819 - (void)testSemanticsObjectBuildsAttributedString {
820  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
822  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
823  flutter::SemanticsNode node;
824  node.label = "label";
825  std::shared_ptr<flutter::SpellOutStringAttribute> attribute =
826  std::make_shared<flutter::SpellOutStringAttribute>();
827  attribute->start = 1;
828  attribute->end = 2;
829  attribute->type = flutter::StringAttributeType::kSpellOut;
830  node.labelAttributes.push_back(attribute);
831  node.value = "value";
832  attribute = std::make_shared<flutter::SpellOutStringAttribute>();
833  attribute->start = 2;
834  attribute->end = 3;
835  attribute->type = flutter::StringAttributeType::kSpellOut;
836  node.valueAttributes.push_back(attribute);
837  node.hint = "hint";
838  std::shared_ptr<flutter::LocaleStringAttribute> local_attribute =
839  std::make_shared<flutter::LocaleStringAttribute>();
840  local_attribute->start = 3;
841  local_attribute->end = 4;
842  local_attribute->type = flutter::StringAttributeType::kLocale;
843  local_attribute->locale = "en-MX";
844  node.hintAttributes.push_back(local_attribute);
845  FlutterSemanticsObject* object = [[FlutterSemanticsObject alloc] initWithBridge:bridge uid:0];
846  [object setSemanticsNode:&node];
847  NSMutableAttributedString* expectedAttributedLabel =
848  [[NSMutableAttributedString alloc] initWithString:NSLocalizedString(@"label", @"test")];
849  NSDictionary* attributeDict = @{
850  UIAccessibilitySpeechAttributeSpellOut : @YES,
851  };
852  [expectedAttributedLabel setAttributes:attributeDict range:NSMakeRange(1, 1)];
853  XCTAssertTrue(
854  [object.accessibilityAttributedLabel isEqualToAttributedString:expectedAttributedLabel]);
855 
856  NSMutableAttributedString* expectedAttributedValue =
857  [[NSMutableAttributedString alloc] initWithString:NSLocalizedString(@"value", @"test")];
858  attributeDict = @{
859  UIAccessibilitySpeechAttributeSpellOut : @YES,
860  };
861  [expectedAttributedValue setAttributes:attributeDict range:NSMakeRange(2, 1)];
862  XCTAssertTrue(
863  [object.accessibilityAttributedValue isEqualToAttributedString:expectedAttributedValue]);
864 
865  NSMutableAttributedString* expectedAttributedHint =
866  [[NSMutableAttributedString alloc] initWithString:NSLocalizedString(@"hint", @"test")];
867  attributeDict = @{
868  UIAccessibilitySpeechAttributeLanguage : @"en-MX",
869  };
870  [expectedAttributedHint setAttributes:attributeDict range:NSMakeRange(3, 1)];
871  XCTAssertTrue(
872  [object.accessibilityAttributedHint isEqualToAttributedString:expectedAttributedHint]);
873 }
874 
875 - (void)testShouldTriggerAnnouncement {
876  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
878  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
879  SemanticsObject* object = [[SemanticsObject alloc] initWithBridge:bridge uid:0];
880 
881  // Handle nil with no node set.
882  XCTAssertFalse([object nodeShouldTriggerAnnouncement:nil]);
883 
884  // Handle initial setting of node with liveRegion
885  flutter::SemanticsNode node;
886  node.flags.isLiveRegion = true;
887  node.label = "foo";
888  XCTAssertTrue([object nodeShouldTriggerAnnouncement:&node]);
889 
890  // Handle nil with node set.
891  [object setSemanticsNode:&node];
892  XCTAssertFalse([object nodeShouldTriggerAnnouncement:nil]);
893 
894  // Handle new node, still has live region, same label.
895  XCTAssertFalse([object nodeShouldTriggerAnnouncement:&node]);
896 
897  // Handle update node with new label, still has live region.
898  flutter::SemanticsNode updatedNode;
899  updatedNode.flags.isLiveRegion = true;
900  updatedNode.label = "bar";
901  XCTAssertTrue([object nodeShouldTriggerAnnouncement:&updatedNode]);
902 
903  // Handle dropping the live region flag.
904  updatedNode.flags = flutter::SemanticsFlags{};
905  XCTAssertFalse([object nodeShouldTriggerAnnouncement:&updatedNode]);
906 
907  // Handle adding the flag when the label has not changed.
908  updatedNode.label = "foo";
909  [object setSemanticsNode:&updatedNode];
910  XCTAssertTrue([object nodeShouldTriggerAnnouncement:&node]);
911 }
912 
913 - (void)testShouldDispatchShowOnScreenActionForHeader {
914  fml::WeakPtrFactory<flutter::testing::MockAccessibilityBridge> factory(
916  fml::WeakPtr<flutter::testing::MockAccessibilityBridge> bridge = factory.GetWeakPtr();
917  SemanticsObject* object = [[SemanticsObject alloc] initWithBridge:bridge uid:1];
918 
919  // Handle initial setting of node with header.
920  flutter::SemanticsNode node;
921  node.flags.isHeader = true;
922  node.label = "foo";
923 
924  [object setSemanticsNode:&node];
925 
926  // Simulate accessibility focus.
927  [object accessibilityElementDidBecomeFocused];
928 
929  XCTAssertTrue(bridge->observations.size() == 1);
930  XCTAssertTrue(bridge->observations[0].id == 1);
931  XCTAssertTrue(bridge->observations[0].action == flutter::SemanticsAction::kShowOnScreen);
932 }
933 
934 - (void)testShouldDispatchShowOnScreenActionForHidden {
935  fml::WeakPtrFactory<flutter::testing::MockAccessibilityBridge> factory(
937  fml::WeakPtr<flutter::testing::MockAccessibilityBridge> bridge = factory.GetWeakPtr();
938  SemanticsObject* object = [[SemanticsObject alloc] initWithBridge:bridge uid:1];
939 
940  // Handle initial setting of node with hidden.
941  flutter::SemanticsNode node;
942  node.flags.isHidden = true;
943  node.label = "foo";
944 
945  [object setSemanticsNode:&node];
946 
947  // Simulate accessibility focus.
948  [object accessibilityElementDidBecomeFocused];
949 
950  XCTAssertTrue(bridge->observations.size() == 1);
951  XCTAssertTrue(bridge->observations[0].id == 1);
952  XCTAssertTrue(bridge->observations[0].action == flutter::SemanticsAction::kShowOnScreen);
953 }
954 
955 - (void)testFlutterSwitchSemanticsObjectMatchesUISwitch {
956  fml::WeakPtrFactory<flutter::testing::MockAccessibilityBridge> factory(
958  fml::WeakPtr<flutter::testing::MockAccessibilityBridge> bridge = factory.GetWeakPtr();
959  FlutterSwitchSemanticsObject* object = [[FlutterSwitchSemanticsObject alloc] initWithBridge:bridge
960  uid:1];
961 
962  // Handle initial setting of node with header.
963  flutter::SemanticsNode node;
964  node.flags.isToggled = flutter::SemanticsTristate::kTrue;
965  node.flags.isEnabled = flutter::SemanticsTristate::kTrue;
966  node.label = "foo";
967  [object setSemanticsNode:&node];
968  // Create ab real UISwitch to compare the FlutterSwitchSemanticsObject with.
969  UISwitch* nativeSwitch = [[UISwitch alloc] init];
970  nativeSwitch.on = YES;
971 
972  XCTAssertEqual(object.accessibilityTraits, nativeSwitch.accessibilityTraits);
973  XCTAssertEqualObjects(object.accessibilityValue, nativeSwitch.accessibilityValue);
974 
975  // Set the toggled to false;
976  flutter::SemanticsNode update;
977  update.flags.isToggled = flutter::SemanticsTristate::kFalse;
978  update.flags.isEnabled = flutter::SemanticsTristate::kTrue;
979 
980  update.label = "foo";
981  [object setSemanticsNode:&update];
982 
983  nativeSwitch.on = NO;
984 
985  XCTAssertEqual(object.accessibilityTraits, nativeSwitch.accessibilityTraits);
986  XCTAssertEqualObjects(object.accessibilityValue, nativeSwitch.accessibilityValue);
987 }
988 
989 - (void)testFlutterSemanticsObjectOfRadioButton {
990  fml::WeakPtrFactory<flutter::testing::MockAccessibilityBridge> factory(
992  fml::WeakPtr<flutter::testing::MockAccessibilityBridge> bridge = factory.GetWeakPtr();
993  FlutterSemanticsObject* object = [[FlutterSemanticsObject alloc] initWithBridge:bridge uid:0];
994 
995  // Handle initial setting of node with header.
996  flutter::SemanticsNode node;
997  node.flags.isInMutuallyExclusiveGroup = true;
998  node.flags.isChecked = flutter::SemanticsCheckState::kFalse;
999  node.flags.isEnabled = flutter::SemanticsTristate::kTrue;
1000  node.label = "foo";
1001  [object setSemanticsNode:&node];
1002  XCTAssertTrue((object.accessibilityTraits & UIAccessibilityTraitButton) > 0);
1003  XCTAssertNil(object.accessibilityValue);
1004 }
1005 
1006 - (void)testFlutterSwitchSemanticsObjectMatchesUISwitchDisabled {
1007  fml::WeakPtrFactory<flutter::testing::MockAccessibilityBridge> factory(
1009  fml::WeakPtr<flutter::testing::MockAccessibilityBridge> bridge = factory.GetWeakPtr();
1010  FlutterSwitchSemanticsObject* object = [[FlutterSwitchSemanticsObject alloc] initWithBridge:bridge
1011  uid:1];
1012 
1013  // Handle initial setting of node with header.
1014  flutter::SemanticsNode node;
1015  node.flags.isToggled = flutter::SemanticsTristate::kTrue;
1016  node.label = "foo";
1017  [object setSemanticsNode:&node];
1018  // Create ab real UISwitch to compare the FlutterSwitchSemanticsObject with.
1019  UISwitch* nativeSwitch = [[UISwitch alloc] init];
1020  nativeSwitch.on = YES;
1021  nativeSwitch.enabled = NO;
1022 
1023  XCTAssertEqual(object.accessibilityTraits, nativeSwitch.accessibilityTraits);
1024  XCTAssertEqualObjects(object.accessibilityValue, nativeSwitch.accessibilityValue);
1025 }
1026 
1027 - (void)testSemanticsObjectDeallocated {
1028  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
1030  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
1031  SemanticsObject* parent = [[SemanticsObject alloc] initWithBridge:bridge uid:0];
1032  SemanticsObject* child = [[SemanticsObject alloc] initWithBridge:bridge uid:1];
1033  parent.children = @[ child ];
1034  // Validate SemanticsObject deallocation does not crash.
1035  // https://github.com/flutter/flutter/issues/66032
1036  __weak SemanticsObject* weakObject = parent;
1037  parent = nil;
1038  XCTAssertNil(weakObject);
1039 }
1040 
1041 - (void)testFlutterSemanticsObjectReturnsNilContainerWhenBridgeIsNotAlive {
1042  FlutterSemanticsObject* parentObject;
1044  FlutterSemanticsObject* object2;
1045 
1046  flutter::SemanticsNode parent;
1047  parent.id = 0;
1048  parent.rect = SkRect::MakeXYWH(0, 0, 100, 200);
1049  parent.label = "label";
1050  parent.value = "value";
1051  parent.hint = "hint";
1052 
1053  flutter::SemanticsNode node;
1054  node.id = 1;
1055  node.flags.hasImplicitScrolling = true;
1056  node.actions = flutter::kHorizontalScrollSemanticsActions;
1057  node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
1058  node.label = "label";
1059  node.value = "value";
1060  node.hint = "hint";
1061  node.scrollExtentMax = 100.0;
1062  node.scrollPosition = 0.0;
1063  parent.childrenInTraversalOrder.push_back(1);
1064 
1065  flutter::SemanticsNode node2;
1066  node2.id = 2;
1067  node2.rect = SkRect::MakeXYWH(0, 0, 100, 200);
1068  node2.label = "label";
1069  node2.value = "value";
1070  node2.hint = "hint";
1071  node2.scrollExtentMax = 100.0;
1072  node2.scrollPosition = 0.0;
1073  parent.childrenInTraversalOrder.push_back(2);
1074 
1075  {
1078  mock->isVoiceOverRunningValue = true;
1079  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(mock);
1080  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
1081 
1082  parentObject = [[FlutterSemanticsObject alloc] initWithBridge:bridge uid:0];
1083  [parentObject setSemanticsNode:&parent];
1084 
1085  scrollable = [[FlutterScrollableSemanticsObject alloc] initWithBridge:bridge uid:1];
1086  [scrollable setSemanticsNode:&node];
1088 
1089  object2 = [[FlutterSemanticsObject alloc] initWithBridge:bridge uid:2];
1090  [object2 setSemanticsNode:&node2];
1091 
1092  parentObject.children = @[ scrollable, object2 ];
1093  [parentObject accessibilityBridgeDidFinishUpdate];
1096 
1097  // Returns the correct container if the bridge is alive.
1098  SemanticsObjectContainer* container =
1099  static_cast<SemanticsObjectContainer*>(scrollable.accessibilityContainer);
1100  XCTAssertEqual(container.semanticsObject, parentObject);
1101  SemanticsObjectContainer* container2 =
1102  static_cast<SemanticsObjectContainer*>(object2.accessibilityContainer);
1103  XCTAssertEqual(container2.semanticsObject, parentObject);
1104  }
1105  // The bridge pointer went out of scope and was deallocated.
1106 
1107  XCTAssertNil(scrollable.accessibilityContainer);
1108  XCTAssertNil(object2.accessibilityContainer);
1109 }
1110 
1111 - (void)testAccessibilityHitTestSearchCanReturnPlatformView {
1112  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
1114  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
1115  SemanticsObject* object0 = [[SemanticsObject alloc] initWithBridge:bridge uid:0];
1116  SemanticsObject* object1 = [[SemanticsObject alloc] initWithBridge:bridge uid:1];
1117  SemanticsObject* object3 = [[SemanticsObject alloc] initWithBridge:bridge uid:3];
1118  FlutterTouchInterceptingView* platformView =
1119  [[FlutterTouchInterceptingView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
1120  FlutterPlatformViewSemanticsContainer* platformViewSemanticsContainer =
1121  [[FlutterPlatformViewSemanticsContainer alloc] initWithBridge:bridge
1122  uid:1
1123  platformView:platformView];
1124 
1125  object0.children = @[ object1 ];
1126  object0.childrenInHitTestOrder = @[ object1 ];
1127  object1.children = @[ platformViewSemanticsContainer, object3 ];
1128  object1.childrenInHitTestOrder = @[ platformViewSemanticsContainer, object3 ];
1129 
1130  flutter::SemanticsNode node0;
1131  node0.id = 0;
1132  node0.rect = SkRect::MakeXYWH(0, 0, 200, 200);
1133  node0.label = "0";
1134  [object0 setSemanticsNode:&node0];
1135 
1136  flutter::SemanticsNode node1;
1137  node1.id = 1;
1138  node1.rect = SkRect::MakeXYWH(0, 0, 200, 200);
1139  node1.label = "1";
1140  [object1 setSemanticsNode:&node1];
1141 
1142  flutter::SemanticsNode node2;
1143  node2.id = 2;
1144  node2.rect = SkRect::MakeXYWH(0, 0, 100, 100);
1145  node2.label = "2";
1146  [platformViewSemanticsContainer setSemanticsNode:&node2];
1147 
1148  flutter::SemanticsNode node3;
1149  node3.id = 3;
1150  node3.rect = SkRect::MakeXYWH(0, 0, 200, 200);
1151  node3.label = "3";
1152  [object3 setSemanticsNode:&node3];
1153 
1154  CGPoint point = CGPointMake(10, 10);
1155  id hitTestResult = [object0 _accessibilityHitTest:point withEvent:nil];
1156 
1157  XCTAssertEqual(hitTestResult, platformView);
1158 }
1159 
1160 - (void)testFlutterPlatformViewSemanticsContainer {
1161  fml::WeakPtrFactory<flutter::testing::MockAccessibilityBridge> factory(
1163  fml::WeakPtr<flutter::testing::MockAccessibilityBridge> bridge = factory.GetWeakPtr();
1164  __weak FlutterTouchInterceptingView* weakPlatformView;
1165  __weak FlutterPlatformViewSemanticsContainer* weakContainer;
1166  @autoreleasepool {
1167  FlutterTouchInterceptingView* platformView = [[FlutterTouchInterceptingView alloc] init];
1168  weakPlatformView = platformView;
1169 
1170  @autoreleasepool {
1172  [[FlutterPlatformViewSemanticsContainer alloc] initWithBridge:bridge
1173  uid:1
1174  platformView:platformView];
1175  weakContainer = container;
1176  XCTAssertEqualObjects(platformView.accessibilityContainer, container);
1177  XCTAssertNotNil(weakPlatformView);
1178  XCTAssertNotNil(weakContainer);
1179  }
1180  // Check the variables are still lived.
1181  // `container` is `retain` in `platformView`, so it will not be nil here.
1182  XCTAssertNotNil(weakPlatformView);
1183  XCTAssertNotNil(weakContainer);
1184  }
1185  // Check if there's no more strong references to `platformView` after container and platformView
1186  // are released.
1187  XCTAssertNil(weakPlatformView);
1188  XCTAssertNil(weakContainer);
1189 }
1190 
1191 - (void)testTextInputSemanticsObject {
1192  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
1194  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
1195 
1196  flutter::SemanticsNode node;
1197  node.label = "foo";
1198  node.flags.isTextField = true;
1199  node.flags.isReadOnly = true;
1200  TextInputSemanticsObject* object = [[TextInputSemanticsObject alloc] initWithBridge:bridge uid:0];
1201  [object setSemanticsNode:&node];
1203  XCTAssertEqual([object accessibilityTraits], UIAccessibilityTraitNone);
1204 }
1205 
1206 - (void)testTextInputSemanticsObject_canPerformAction {
1207  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
1209  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
1210 
1211  flutter::SemanticsNode node;
1212  node.label = "foo";
1213  node.flags.isTextField = true;
1214  node.flags.isReadOnly = true;
1215  TextInputSemanticsObject* object = [[TextInputSemanticsObject alloc] initWithBridge:bridge uid:0];
1216  [object setSemanticsNode:&node];
1218 
1219  id textInputSurrogate = OCMClassMock([UIResponder class]);
1220  id partialSemanticsObject = OCMPartialMock(object);
1221  OCMStub([partialSemanticsObject textInputSurrogate]).andReturn(textInputSurrogate);
1222 
1223  OCMExpect([textInputSurrogate canPerformAction:[OCMArg anySelector] withSender:OCMOCK_ANY])
1224  .andReturn(YES);
1225  XCTAssertTrue([partialSemanticsObject canPerformAction:@selector(copy:) withSender:nil]);
1226 
1227  OCMExpect([textInputSurrogate canPerformAction:[OCMArg anySelector] withSender:OCMOCK_ANY])
1228  .andReturn(NO);
1229  XCTAssertFalse([partialSemanticsObject canPerformAction:@selector(copy:) withSender:nil]);
1230 }
1231 
1232 - (void)testTextInputSemanticsObject_editActions {
1233  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
1235  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
1236 
1237  flutter::SemanticsNode node;
1238  node.label = "foo";
1239 
1240  node.flags.isTextField = true;
1241  node.flags.isReadOnly = true;
1242  TextInputSemanticsObject* object = [[TextInputSemanticsObject alloc] initWithBridge:bridge uid:0];
1243  [object setSemanticsNode:&node];
1245 
1246  id textInputSurrogate = OCMClassMock([UIResponder class]);
1247  id partialSemanticsObject = OCMPartialMock(object);
1248  OCMStub([partialSemanticsObject textInputSurrogate]).andReturn(textInputSurrogate);
1249 
1250  XCTestExpectation* copyExpectation =
1251  [self expectationWithDescription:@"Surrogate's copy method is called."];
1252  XCTestExpectation* cutExpectation =
1253  [self expectationWithDescription:@"Surrogate's cut method is called."];
1254  XCTestExpectation* pasteExpectation =
1255  [self expectationWithDescription:@"Surrogate's paste method is called."];
1256  XCTestExpectation* selectAllExpectation =
1257  [self expectationWithDescription:@"Surrogate's selectAll method is called."];
1258  XCTestExpectation* deleteExpectation =
1259  [self expectationWithDescription:@"Surrogate's delete method is called."];
1260 
1261  OCMStub([textInputSurrogate copy:OCMOCK_ANY]).andDo(^(NSInvocation* invocation) {
1262  [copyExpectation fulfill];
1263  });
1264  OCMStub([textInputSurrogate cut:OCMOCK_ANY]).andDo(^(NSInvocation* invocation) {
1265  [cutExpectation fulfill];
1266  });
1267  OCMStub([textInputSurrogate paste:OCMOCK_ANY]).andDo(^(NSInvocation* invocation) {
1268  [pasteExpectation fulfill];
1269  });
1270  OCMStub([textInputSurrogate selectAll:OCMOCK_ANY]).andDo(^(NSInvocation* invocation) {
1271  [selectAllExpectation fulfill];
1272  });
1273  OCMStub([textInputSurrogate delete:OCMOCK_ANY]).andDo(^(NSInvocation* invocation) {
1274  [deleteExpectation fulfill];
1275  });
1276 
1277  [partialSemanticsObject copy:nil];
1278  [partialSemanticsObject cut:nil];
1279  [partialSemanticsObject paste:nil];
1280  [partialSemanticsObject selectAll:nil];
1281  [partialSemanticsObject delete:nil];
1282 
1283  [self waitForExpectationsWithTimeout:1 handler:nil];
1284 }
1285 
1286 - (void)testSliderSemanticsObject {
1287  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
1289  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
1290 
1291  flutter::SemanticsNode node;
1292  node.flags.isSlider = true;
1293  SemanticsObject* object = [[SemanticsObject alloc] initWithBridge:bridge uid:0];
1294  [object setSemanticsNode:&node];
1296  XCTAssertEqual([object accessibilityActivate], YES);
1297 }
1298 
1299 - (void)testUIFocusItemConformance {
1300  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
1302  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
1303  SemanticsObject* parent = [[SemanticsObject alloc] initWithBridge:bridge uid:0];
1304  SemanticsObject* child = [[SemanticsObject alloc] initWithBridge:bridge uid:1];
1305  parent.children = @[ child ];
1306 
1307  // parentFocusEnvironment
1308  XCTAssertTrue([parent.parentFocusEnvironment isKindOfClass:[UIView class]]);
1309  XCTAssertEqual(child.parentFocusEnvironment, child.parent);
1310 
1311  // canBecomeFocused
1312  flutter::SemanticsNode childNode;
1313  childNode.flags.isHidden = true;
1314  childNode.actions = static_cast<int32_t>(flutter::SemanticsAction::kTap);
1315  [child setSemanticsNode:&childNode];
1316  XCTAssertFalse(child.canBecomeFocused);
1317  childNode.flags = flutter::SemanticsFlags{};
1318  [child setSemanticsNode:&childNode];
1319  XCTAssertTrue(child.canBecomeFocused);
1320  childNode.actions = 0;
1321  [child setSemanticsNode:&childNode];
1322  XCTAssertFalse(child.canBecomeFocused);
1323 
1324  CGFloat scale = ((bridge->view().window.screen ?: UIScreen.mainScreen)).scale;
1325 
1326  childNode.rect = SkRect::MakeXYWH(0, 0, 100 * scale, 100 * scale);
1327  [child setSemanticsNode:&childNode];
1328  flutter::SemanticsNode parentNode;
1329  parentNode.rect = SkRect::MakeXYWH(0, 0, 200, 200);
1330  [parent setSemanticsNode:&parentNode];
1331 
1332  XCTAssertTrue(CGRectEqualToRect(child.frame, CGRectMake(0, 0, 100, 100)));
1333 }
1334 
1335 - (void)testUIFocusItemContainerConformance {
1336  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
1338  fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
1339  SemanticsObject* parent = [[SemanticsObject alloc] initWithBridge:bridge uid:0];
1340  SemanticsObject* child1 = [[SemanticsObject alloc] initWithBridge:bridge uid:1];
1341  SemanticsObject* child2 = [[SemanticsObject alloc] initWithBridge:bridge uid:2];
1342  parent.childrenInHitTestOrder = @[ child1, child2 ];
1343 
1344  // focusItemsInRect
1345  NSArray<id<UIFocusItem>>* itemsInRect = [parent focusItemsInRect:CGRectMake(0, 0, 100, 100)];
1346  XCTAssertEqual(itemsInRect.count, (unsigned long)2);
1347  XCTAssertTrue([itemsInRect containsObject:child1]);
1348  XCTAssertTrue([itemsInRect containsObject:child2]);
1349 }
1350 
1351 - (void)testUIFocusItemScrollableContainerConformance {
1352  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
1354  fml::WeakPtr<flutter::testing::MockAccessibilityBridge> bridge = factory.GetWeakPtr();
1355  FlutterScrollableSemanticsObject* scrollable =
1356  [[FlutterScrollableSemanticsObject alloc] initWithBridge:bridge uid:5];
1357 
1358  // setContentOffset
1359  CGPoint p = CGPointMake(123.0, 456.0);
1360  [scrollable.scrollView scrollViewWillEndDragging:scrollable.scrollView
1361  withVelocity:CGPointZero
1362  targetContentOffset:&p];
1363  scrollable.scrollView.contentOffset = p;
1364  [scrollable.scrollView scrollViewDidEndDecelerating:scrollable.scrollView];
1365  XCTAssertEqual(bridge->observations.size(), (size_t)1);
1366  XCTAssertEqual(bridge->observations[0].id, 5);
1367  XCTAssertEqual(bridge->observations[0].action, flutter::SemanticsAction::kScrollToOffset);
1368 
1369  std::vector<uint8_t> args = bridge->observations[0].args;
1370  XCTAssertEqual(args.size(), 3 * sizeof(CGFloat));
1371 
1372  NSData* encoded = [NSData dataWithBytes:args.data() length:args.size()];
1374  CGPoint point = CGPointZero;
1375  memcpy(&point, decoded.data.bytes, decoded.data.length);
1376  XCTAssertTrue(CGPointEqualToPoint(point, p));
1377 }
1378 
1379 - (void)testUIFocusItemScrollableContainerNoFeedbackLoops {
1380  fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
1382  fml::WeakPtr<flutter::testing::MockAccessibilityBridge> bridge = factory.GetWeakPtr();
1383  FlutterScrollableSemanticsObject* scrollable =
1384  [[FlutterScrollableSemanticsObject alloc] initWithBridge:bridge uid:5];
1385 
1386  // setContentOffset
1387  const CGPoint p = CGPointMake(0.0, 456.0);
1388  scrollable.scrollView.contentOffset = p;
1389  bridge->observations.clear();
1390 
1391  const SkScalar scrollPosition = p.y + 0.0000000000000001;
1392  flutter::SemanticsNode node;
1393  node.flags.hasImplicitScrolling = true;
1394  node.actions = flutter::kVerticalScrollSemanticsActions;
1395  node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
1396  node.scrollExtentMax = 10000;
1397  node.scrollPosition = scrollPosition;
1398  node.transform = {1.0, 0, 0, 0, 0, 1.0, 0, 0, 0, 0, 1.0, 0, 0, scrollPosition, 0, 1.0};
1399  [scrollable setSemanticsNode:&node];
1401 
1402  XCTAssertEqual(bridge->observations.size(), (size_t)0);
1403 }
1404 @end
constexpr float kScrollExtentMaxForInf
FLUTTER_ASSERT_ARC const float kFloatCompareEpsilon
UIView< UITextInput > * textInputSurrogate()
FlutterSemanticsScrollView * scrollView
SemanticsObject * semanticsObject
id _accessibilityHitTest:withEvent:(CGPoint point,[withEvent] UIEvent *event)
SemanticsObject * parent
NSArray< SemanticsObject * > * childrenInHitTestOrder
BOOL accessibilityScrollToVisibleWithChild:(id child)
void accessibilityBridgeDidFinishUpdate()
BOOL accessibilityScrollToVisible()
NSArray< SemanticsObject * > * children
void replaceChildAtIndex:withChild:(NSInteger index,[withChild] SemanticsObject *child)
void setSemanticsNode:(const flutter::SemanticsNode *NS_REQUIRES_SUPER)
instancetype sharedInstance()