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