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