Flutter iOS Embedder
accessibility_bridge_test.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 
16 
18 
19 @class MockPlatformView;
20 __weak static MockPlatformView* gMockPlatformView = nil;
21 
22 @interface MockPlatformView : UIView
23 @end
24 @implementation MockPlatformView
25 
26 - (instancetype)init {
27  self = [super init];
28  if (self) {
29  gMockPlatformView = self;
30  }
31  return self;
32 }
33 
34 - (void)dealloc {
35  gMockPlatformView = nil;
36 }
37 
38 @end
39 
41 @property(nonatomic, strong) UIView* view;
42 @end
43 
44 @implementation MockFlutterPlatformView
45 
46 - (instancetype)init {
47  if (self = [super init]) {
48  _view = [[MockPlatformView alloc] init];
49  }
50  return self;
51 }
52 
53 @end
54 
56 @end
57 
58 @implementation MockFlutterPlatformFactory
59 - (NSObject<FlutterPlatformView>*)createWithFrame:(CGRect)frame
60  viewIdentifier:(int64_t)viewId
61  arguments:(id _Nullable)args {
62  return [[MockFlutterPlatformView alloc] init];
63 }
64 
65 @end
66 
67 namespace flutter {
68 namespace {
69 class MockDelegate : public PlatformView::Delegate {
70  public:
71  void OnPlatformViewCreated(std::unique_ptr<Surface> surface) override {}
72  void OnPlatformViewDestroyed() override {}
73  void OnPlatformViewScheduleFrame() override {}
74  void OnPlatformViewSetNextFrameCallback(const fml::closure& closure) override {}
75  void OnPlatformViewSetViewportMetrics(int64_t view_id, const ViewportMetrics& metrics) override {}
76  const flutter::Settings& OnPlatformViewGetSettings() const override { return settings_; }
77  void OnPlatformViewDispatchPlatformMessage(std::unique_ptr<PlatformMessage> message) override {}
78  void OnPlatformViewDispatchPointerDataPacket(std::unique_ptr<PointerDataPacket> packet) override {
79  }
80  void OnPlatformViewDispatchSemanticsAction(int32_t id,
81  SemanticsAction action,
82  fml::MallocMapping args) override {}
83  void OnPlatformViewSetSemanticsEnabled(bool enabled) override {}
84  void OnPlatformViewSetAccessibilityFeatures(int32_t flags) override {}
85  void OnPlatformViewRegisterTexture(std::shared_ptr<Texture> texture) override {}
86  void OnPlatformViewUnregisterTexture(int64_t texture_id) override {}
87  void OnPlatformViewMarkTextureFrameAvailable(int64_t texture_id) override {}
88 
89  void LoadDartDeferredLibrary(intptr_t loading_unit_id,
90  std::unique_ptr<const fml::Mapping> snapshot_data,
91  std::unique_ptr<const fml::Mapping> snapshot_instructions) override {
92  }
93  void LoadDartDeferredLibraryError(intptr_t loading_unit_id,
94  const std::string error_message,
95  bool transient) override {}
96  void UpdateAssetResolverByType(std::unique_ptr<flutter::AssetResolver> updated_asset_resolver,
97  flutter::AssetResolver::AssetResolverType type) override {}
98 
99  flutter::Settings settings_;
100 };
101 
102 class MockIosDelegate : public AccessibilityBridge::IosDelegate {
103  public:
104  bool IsFlutterViewControllerPresentingModalViewController(
105  FlutterViewController* view_controller) override {
106  return result_IsFlutterViewControllerPresentingModalViewController_;
107  };
108 
109  void PostAccessibilityNotification(UIAccessibilityNotifications notification,
110  id argument) override {
111  if (on_PostAccessibilityNotification_) {
112  on_PostAccessibilityNotification_(notification, argument);
113  }
114  }
115  std::function<void(UIAccessibilityNotifications, id)> on_PostAccessibilityNotification_;
116  bool result_IsFlutterViewControllerPresentingModalViewController_ = false;
117 };
118 } // namespace
119 } // namespace flutter
120 
121 namespace {
122 fml::RefPtr<fml::TaskRunner> CreateNewThread(const std::string& name) {
123  auto thread = std::make_unique<fml::Thread>(name);
124  auto runner = thread->GetTaskRunner();
125  return runner;
126 }
127 } // namespace
128 
129 @interface AccessibilityBridgeTest : XCTestCase
130 @end
131 
132 @implementation AccessibilityBridgeTest
133 
134 - (void)testCreate {
135  flutter::MockDelegate mock_delegate;
136  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
137  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
138  /*platform=*/thread_task_runner,
139  /*raster=*/thread_task_runner,
140  /*ui=*/thread_task_runner,
141  /*io=*/thread_task_runner);
142  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
143  /*delegate=*/mock_delegate,
144  /*rendering_api=*/mock_delegate.settings_.enable_impeller
147  /*platform_views_controller=*/nil,
148  /*task_runners=*/runners,
149  /*worker_task_runner=*/nil,
150  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
151  auto bridge =
152  std::make_unique<flutter::AccessibilityBridge>(/*view=*/nil,
153  /*platform_view=*/platform_view.get(),
154  /*platform_views_controller=*/nil);
155  XCTAssertTrue(bridge.get());
156 }
157 
158 - (void)testUpdateSemanticsEmpty {
159  flutter::MockDelegate mock_delegate;
160  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
161  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
162  /*platform=*/thread_task_runner,
163  /*raster=*/thread_task_runner,
164  /*ui=*/thread_task_runner,
165  /*io=*/thread_task_runner);
166  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
167  /*delegate=*/mock_delegate,
168  /*rendering_api=*/mock_delegate.settings_.enable_impeller
171  /*platform_views_controller=*/nil,
172  /*task_runners=*/runners,
173  /*worker_task_runner=*/nil,
174  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
175  id mockFlutterView = OCMClassMock([FlutterView class]);
176  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
177  OCMStub([mockFlutterViewController viewIfLoaded]).andReturn(mockFlutterView);
178  OCMExpect([mockFlutterView setAccessibilityElements:[OCMArg isNil]]);
179  auto bridge =
180  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
181  /*platform_view=*/platform_view.get(),
182  /*platform_views_controller=*/nil);
183  flutter::SemanticsNodeUpdates nodes;
184  flutter::CustomAccessibilityActionUpdates actions;
185  bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
186  OCMVerifyAll(mockFlutterView);
187 }
188 
189 - (void)testUpdateSemanticsOneNode {
190  flutter::MockDelegate mock_delegate;
191  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
192  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
193  /*platform=*/thread_task_runner,
194  /*raster=*/thread_task_runner,
195  /*ui=*/thread_task_runner,
196  /*io=*/thread_task_runner);
197  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
198  /*delegate=*/mock_delegate,
199  /*rendering_api=*/mock_delegate.settings_.enable_impeller
202  /*platform_views_controller=*/nil,
203  /*task_runners=*/runners,
204  /*worker_task_runner=*/nil,
205  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
206  id mockFlutterView = OCMClassMock([FlutterView class]);
207  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
208  OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
209  std::string label = "some label";
210 
211  __block auto bridge =
212  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
213  /*platform_view=*/platform_view.get(),
214  /*platform_views_controller=*/nil);
215 
216  OCMExpect([mockFlutterView setAccessibilityElements:[OCMArg checkWithBlock:^BOOL(NSArray* value) {
217  if ([value count] != 1) {
218  return NO;
219  } else {
220  SemanticsObjectContainer* container = value[0];
221  SemanticsObject* object = container.semanticsObject;
222  return object.uid == kRootNodeId &&
223  object.bridge.get() == bridge.get() &&
224  object.node.label == label;
225  }
226  }]]);
227 
228  flutter::SemanticsNodeUpdates nodes;
229  flutter::SemanticsNode semantics_node;
230  semantics_node.id = kRootNodeId;
231  semantics_node.label = label;
232  nodes[kRootNodeId] = semantics_node;
233  flutter::CustomAccessibilityActionUpdates actions;
234  bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
235  OCMVerifyAll(mockFlutterView);
236 }
237 
238 - (void)testIsVoiceOverRunning {
239  flutter::MockDelegate mock_delegate;
240  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
241  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
242  /*platform=*/thread_task_runner,
243  /*raster=*/thread_task_runner,
244  /*ui=*/thread_task_runner,
245  /*io=*/thread_task_runner);
246  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
247  /*delegate=*/mock_delegate,
248  /*rendering_api=*/mock_delegate.settings_.enable_impeller
251  /*platform_views_controller=*/nil,
252  /*task_runners=*/runners,
253  /*worker_task_runner=*/nil,
254  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
255  id mockFlutterView = OCMClassMock([FlutterView class]);
256  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
257  OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
258  OCMStub([mockFlutterViewController isVoiceOverRunning]).andReturn(YES);
259 
260  __block auto bridge =
261  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
262  /*platform_view=*/platform_view.get(),
263  /*platform_views_controller=*/nil);
264 
265  XCTAssertTrue(bridge->isVoiceOverRunning());
266 }
267 
268 - (void)testSemanticsDeallocated {
269  @autoreleasepool {
270  flutter::MockDelegate mock_delegate;
271  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
272  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
273  /*platform=*/thread_task_runner,
274  /*raster=*/thread_task_runner,
275  /*ui=*/thread_task_runner,
276  /*io=*/thread_task_runner);
277 
278  auto flutterPlatformViewsController =
279  std::make_shared<flutter::FlutterPlatformViewsController>();
280  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
281  /*delegate=*/mock_delegate,
282  /*rendering_api=*/mock_delegate.settings_.enable_impeller
285  /*platform_views_controller=*/flutterPlatformViewsController,
286  /*task_runners=*/runners,
287  /*worker_task_runner=*/nil,
288  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
289  id mockFlutterView = OCMClassMock([FlutterView class]);
290  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
291  OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
292  std::string label = "some label";
293  flutterPlatformViewsController->SetFlutterView(mockFlutterView);
294 
295  MockFlutterPlatformFactory* factory = [[MockFlutterPlatformFactory alloc] init];
296  flutterPlatformViewsController->RegisterViewFactory(
297  factory, @"MockFlutterPlatformView",
299  FlutterResult result = ^(id result) {
300  };
301  flutterPlatformViewsController->OnMethodCall(
303  methodCallWithMethodName:@"create"
304  arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}],
305  result);
306 
307  auto bridge = std::make_unique<flutter::AccessibilityBridge>(
308  /*view_controller=*/mockFlutterViewController,
309  /*platform_view=*/platform_view.get(),
310  /*platform_views_controller=*/flutterPlatformViewsController);
311 
312  flutter::SemanticsNodeUpdates nodes;
313  flutter::SemanticsNode semantics_node;
314  semantics_node.id = 2;
315  semantics_node.platformViewId = 2;
316  semantics_node.label = label;
317  nodes[kRootNodeId] = semantics_node;
318  flutter::CustomAccessibilityActionUpdates actions;
319  bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
320  XCTAssertNotNil(gMockPlatformView);
321  flutterPlatformViewsController->Reset();
322  }
323  XCTAssertNil(gMockPlatformView);
324 }
325 
326 - (void)testSemanticsDeallocatedWithoutLoadingView {
327  id engine = OCMClassMock([FlutterEngine class]);
328  FlutterViewController* flutterViewController =
329  [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
330  @autoreleasepool {
331  flutter::MockDelegate mock_delegate;
332  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
333  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
334  /*platform=*/thread_task_runner,
335  /*raster=*/thread_task_runner,
336  /*ui=*/thread_task_runner,
337  /*io=*/thread_task_runner);
338 
339  auto flutterPlatformViewsController =
340  std::make_shared<flutter::FlutterPlatformViewsController>();
341  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
342  /*delegate=*/mock_delegate,
343  /*rendering_api=*/mock_delegate.settings_.enable_impeller
346  /*platform_views_controller=*/flutterPlatformViewsController,
347  /*task_runners=*/runners,
348  /*worker_task_runner=*/nil,
349  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
350 
351  MockFlutterPlatformFactory* factory = [[MockFlutterPlatformFactory alloc] init];
352  flutterPlatformViewsController->RegisterViewFactory(
353  factory, @"MockFlutterPlatformView",
355  FlutterResult result = ^(id result) {
356  };
357  flutterPlatformViewsController->OnMethodCall(
359  methodCallWithMethodName:@"create"
360  arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}],
361  result);
362 
363  auto bridge = std::make_unique<flutter::AccessibilityBridge>(
364  /*view_controller=*/flutterViewController,
365  /*platform_view=*/platform_view.get(),
366  /*platform_views_controller=*/flutterPlatformViewsController);
367 
368  XCTAssertNotNil(gMockPlatformView);
369  flutterPlatformViewsController->Reset();
370  platform_view->NotifyDestroyed();
371  }
372  XCTAssertNil(gMockPlatformView);
373  XCTAssertNil(flutterViewController.viewIfLoaded);
374  [flutterViewController deregisterNotifications];
375 }
376 
377 - (void)testReplacedSemanticsDoesNotCleanupChildren {
378  flutter::MockDelegate mock_delegate;
379  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
380  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
381  /*platform=*/thread_task_runner,
382  /*raster=*/thread_task_runner,
383  /*ui=*/thread_task_runner,
384  /*io=*/thread_task_runner);
385 
386  auto flutterPlatformViewsController = std::make_shared<flutter::FlutterPlatformViewsController>();
387  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
388  /*delegate=*/mock_delegate,
389  /*rendering_api=*/mock_delegate.settings_.enable_impeller
392  /*platform_views_controller=*/flutterPlatformViewsController,
393  /*task_runners=*/runners,
394  /*worker_task_runner=*/nil,
395  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
396  id engine = OCMClassMock([FlutterEngine class]);
397  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
398  FlutterView* flutterView = [[FlutterView alloc] initWithDelegate:engine
399  opaque:YES
400  enableWideGamut:NO];
401  OCMStub([mockFlutterViewController view]).andReturn(flutterView);
402  std::string label = "some label";
403  auto bridge = std::make_unique<flutter::AccessibilityBridge>(
404  /*view_controller=*/mockFlutterViewController,
405  /*platform_view=*/platform_view.get(),
406  /*platform_views_controller=*/flutterPlatformViewsController);
407  @autoreleasepool {
408  flutter::SemanticsNodeUpdates nodes;
409  flutter::SemanticsNode parent;
410  parent.id = 0;
411  parent.rect = SkRect::MakeXYWH(0, 0, 100, 200);
412  parent.label = "label";
413  parent.value = "value";
414  parent.hint = "hint";
415 
416  flutter::SemanticsNode node;
417  node.id = 1;
418  node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
419  node.label = "label";
420  node.value = "value";
421  node.hint = "hint";
422  node.scrollExtentMax = 100.0;
423  node.scrollPosition = 0.0;
424  parent.childrenInTraversalOrder.push_back(1);
425  parent.childrenInHitTestOrder.push_back(1);
426 
427  flutter::SemanticsNode child;
428  child.id = 2;
429  child.rect = SkRect::MakeXYWH(0, 0, 100, 200);
430  child.label = "label";
431  child.value = "value";
432  child.hint = "hint";
433  node.childrenInTraversalOrder.push_back(2);
434  node.childrenInHitTestOrder.push_back(2);
435 
436  nodes[0] = parent;
437  nodes[1] = node;
438  nodes[2] = child;
439  flutter::CustomAccessibilityActionUpdates actions;
440  bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
441 
442  // Add implicit scroll from node 1 to cause replacement.
443  flutter::SemanticsNodeUpdates new_nodes;
444  flutter::SemanticsNode new_node;
445  new_node.id = 1;
446  new_node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
447  new_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling);
448  new_node.actions = flutter::kHorizontalScrollSemanticsActions;
449  new_node.label = "label";
450  new_node.value = "value";
451  new_node.hint = "hint";
452  new_node.scrollExtentMax = 100.0;
453  new_node.scrollPosition = 0.0;
454  new_node.childrenInTraversalOrder.push_back(2);
455  new_node.childrenInHitTestOrder.push_back(2);
456 
457  new_nodes[1] = new_node;
458  bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/actions);
459  }
460  /// The old node should be deallocated at this moment. Procced to check
461  /// accessibility tree integrity.
462  id rootContainer = flutterView.accessibilityElements[0];
463  XCTAssertTrue([rootContainer accessibilityElementCount] ==
464  2); // one for root, one for scrollable.
465  id scrollableContainer = [rootContainer accessibilityElementAtIndex:1];
466  XCTAssertTrue([scrollableContainer accessibilityElementCount] ==
467  2); // one for scrollable, one for scrollable child.
468  id child = [scrollableContainer accessibilityElementAtIndex:1];
469  /// Replacing node 1 should not accidentally clean up its child's container.
470  XCTAssertNotNil([child accessibilityContainer]);
471 }
472 
473 - (void)testScrollableSemanticsDeallocated {
474  flutter::MockDelegate mock_delegate;
475  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
476  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
477  /*platform=*/thread_task_runner,
478  /*raster=*/thread_task_runner,
479  /*ui=*/thread_task_runner,
480  /*io=*/thread_task_runner);
481 
482  auto flutterPlatformViewsController = std::make_shared<flutter::FlutterPlatformViewsController>();
483  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
484  /*delegate=*/mock_delegate,
485  /*rendering_api=*/mock_delegate.settings_.enable_impeller
488  /*platform_views_controller=*/flutterPlatformViewsController,
489  /*task_runners=*/runners,
490  /*worker_task_runner=*/nil,
491  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
492  id engine = OCMClassMock([FlutterEngine class]);
493  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
494  FlutterView* flutterView = [[FlutterView alloc] initWithDelegate:engine
495  opaque:YES
496  enableWideGamut:NO];
497  OCMStub([mockFlutterViewController view]).andReturn(flutterView);
498  std::string label = "some label";
499  @autoreleasepool {
500  auto bridge = std::make_unique<flutter::AccessibilityBridge>(
501  /*view_controller=*/mockFlutterViewController,
502  /*platform_view=*/platform_view.get(),
503  /*platform_views_controller=*/flutterPlatformViewsController);
504 
505  flutter::SemanticsNodeUpdates nodes;
506  flutter::SemanticsNode parent;
507  parent.id = 0;
508  parent.rect = SkRect::MakeXYWH(0, 0, 100, 200);
509  parent.label = "label";
510  parent.value = "value";
511  parent.hint = "hint";
512 
513  flutter::SemanticsNode node;
514  node.id = 1;
515  node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling);
516  node.actions = flutter::kHorizontalScrollSemanticsActions;
517  node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
518  node.label = "label";
519  node.value = "value";
520  node.hint = "hint";
521  node.scrollExtentMax = 100.0;
522  node.scrollPosition = 0.0;
523  parent.childrenInTraversalOrder.push_back(1);
524  parent.childrenInHitTestOrder.push_back(1);
525  nodes[0] = parent;
526  nodes[1] = node;
527  flutter::CustomAccessibilityActionUpdates actions;
528  bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
529  XCTAssertTrue([flutterView.subviews count] == 1);
530  XCTAssertTrue([flutterView.subviews[0] isKindOfClass:[FlutterSemanticsScrollView class]]);
531  XCTAssertTrue([flutterView.subviews[0].accessibilityLabel isEqualToString:@"label"]);
532 
533  // Remove the scrollable from the tree.
534  flutter::SemanticsNodeUpdates new_nodes;
535  flutter::SemanticsNode new_parent;
536  new_parent.id = 0;
537  new_parent.rect = SkRect::MakeXYWH(0, 0, 100, 200);
538  new_parent.label = "label";
539  new_parent.value = "value";
540  new_parent.hint = "hint";
541  new_nodes[0] = new_parent;
542  bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/actions);
543  }
544  XCTAssertTrue([flutterView.subviews count] == 0);
545 }
546 
547 - (void)testBridgeReplacesSemanticsNode {
548  flutter::MockDelegate mock_delegate;
549  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
550  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
551  /*platform=*/thread_task_runner,
552  /*raster=*/thread_task_runner,
553  /*ui=*/thread_task_runner,
554  /*io=*/thread_task_runner);
555 
556  auto flutterPlatformViewsController = std::make_shared<flutter::FlutterPlatformViewsController>();
557  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
558  /*delegate=*/mock_delegate,
559  /*rendering_api=*/mock_delegate.settings_.enable_impeller
562  /*platform_views_controller=*/flutterPlatformViewsController,
563  /*task_runners=*/runners,
564  /*worker_task_runner=*/nil,
565  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
566  id engine = OCMClassMock([FlutterEngine class]);
567  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
568  FlutterView* flutterView = [[FlutterView alloc] initWithDelegate:engine
569  opaque:YES
570  enableWideGamut:NO];
571  OCMStub([mockFlutterViewController view]).andReturn(flutterView);
572  std::string label = "some label";
573  @autoreleasepool {
574  auto bridge = std::make_unique<flutter::AccessibilityBridge>(
575  /*view_controller=*/mockFlutterViewController,
576  /*platform_view=*/platform_view.get(),
577  /*platform_views_controller=*/flutterPlatformViewsController);
578 
579  flutter::SemanticsNodeUpdates nodes;
580  flutter::SemanticsNode parent;
581  parent.id = 0;
582  parent.rect = SkRect::MakeXYWH(0, 0, 100, 200);
583  parent.label = "label";
584  parent.value = "value";
585  parent.hint = "hint";
586 
587  flutter::SemanticsNode node;
588  node.id = 1;
589  node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling);
590  node.actions = flutter::kHorizontalScrollSemanticsActions;
591  node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
592  node.label = "label";
593  node.value = "value";
594  node.hint = "hint";
595  node.scrollExtentMax = 100.0;
596  node.scrollPosition = 0.0;
597  parent.childrenInTraversalOrder.push_back(1);
598  parent.childrenInHitTestOrder.push_back(1);
599  nodes[0] = parent;
600  nodes[1] = node;
601  flutter::CustomAccessibilityActionUpdates actions;
602  bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
603  XCTAssertTrue([flutterView.subviews count] == 1);
604  XCTAssertTrue([flutterView.subviews[0] isKindOfClass:[FlutterSemanticsScrollView class]]);
605  XCTAssertTrue([flutterView.subviews[0].accessibilityLabel isEqualToString:@"label"]);
606 
607  // Remove implicit scroll from node 1.
608  flutter::SemanticsNodeUpdates new_nodes;
609  flutter::SemanticsNode new_node;
610  new_node.id = 1;
611  new_node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
612  new_node.label = "label";
613  new_node.value = "value";
614  new_node.hint = "hint";
615  new_node.scrollExtentMax = 100.0;
616  new_node.scrollPosition = 0.0;
617  new_nodes[1] = new_node;
618  bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/actions);
619  }
620  XCTAssertTrue([flutterView.subviews count] == 0);
621 }
622 
623 - (void)testAnnouncesRouteChanges {
624  flutter::MockDelegate mock_delegate;
625  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
626  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
627  /*platform=*/thread_task_runner,
628  /*raster=*/thread_task_runner,
629  /*ui=*/thread_task_runner,
630  /*io=*/thread_task_runner);
631  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
632  /*delegate=*/mock_delegate,
633  /*rendering_api=*/mock_delegate.settings_.enable_impeller
636  /*platform_views_controller=*/nil,
637  /*task_runners=*/runners,
638  /*worker_task_runner=*/nil,
639  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
640  id mockFlutterView = OCMClassMock([FlutterView class]);
641  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
642  OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
643 
644  NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
645  [[NSMutableArray alloc] init];
646  auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
647  ios_delegate->on_PostAccessibilityNotification_ =
648  [accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
649  [accessibility_notifications addObject:@{
650  @"notification" : @(notification),
651  @"argument" : argument ? argument : [NSNull null],
652  }];
653  };
654  __block auto bridge =
655  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
656  /*platform_view=*/platform_view.get(),
657  /*platform_views_controller=*/nil,
658  /*ios_delegate=*/std::move(ios_delegate));
659 
660  flutter::CustomAccessibilityActionUpdates actions;
661  flutter::SemanticsNodeUpdates nodes;
662 
663  flutter::SemanticsNode node1;
664  node1.id = 1;
665  node1.label = "node1";
666  node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
667  node1.childrenInTraversalOrder = {2, 3};
668  node1.childrenInHitTestOrder = {2, 3};
669  nodes[node1.id] = node1;
670  flutter::SemanticsNode node2;
671  node2.id = 2;
672  node2.label = "node2";
673  nodes[node2.id] = node2;
674  flutter::SemanticsNode node3;
675  node3.id = 3;
676  node3.flags = static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
677  node3.label = "node3";
678  nodes[node3.id] = node3;
679  flutter::SemanticsNode root_node;
680  root_node.id = kRootNodeId;
681  root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
682  root_node.childrenInTraversalOrder = {1};
683  root_node.childrenInHitTestOrder = {1};
684  nodes[root_node.id] = root_node;
685  bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
686 
687  XCTAssertEqual([accessibility_notifications count], 1ul);
688  XCTAssertEqualObjects(accessibility_notifications[0][@"argument"], @"node3");
689  XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
690  UIAccessibilityScreenChangedNotification);
691 }
692 
693 - (void)testRadioButtonIsNotSwitchButton {
694  flutter::MockDelegate mock_delegate;
695  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
696  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
697  /*platform=*/thread_task_runner,
698  /*raster=*/thread_task_runner,
699  /*ui=*/thread_task_runner,
700  /*io=*/thread_task_runner);
701  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
702  /*delegate=*/mock_delegate,
703  /*rendering_api=*/mock_delegate.settings_.enable_impeller
706  /*platform_views_controller=*/nil,
707  /*task_runners=*/runners,
708  /*worker_task_runner=*/nil,
709  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
710  id engine = OCMClassMock([FlutterEngine class]);
711  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
712  FlutterView* flutterView = [[FlutterView alloc] initWithDelegate:engine
713  opaque:YES
714  enableWideGamut:NO];
715  OCMStub([mockFlutterViewController view]).andReturn(flutterView);
716  auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
717  __block auto bridge =
718  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
719  /*platform_view=*/platform_view.get(),
720  /*platform_views_controller=*/nil,
721  /*ios_delegate=*/std::move(ios_delegate));
722 
723  flutter::CustomAccessibilityActionUpdates actions;
724  flutter::SemanticsNodeUpdates nodes;
725 
726  flutter::SemanticsNode root_node;
727  root_node.id = kRootNodeId;
728  root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kIsInMutuallyExclusiveGroup) |
729  static_cast<int32_t>(flutter::SemanticsFlags::kIsEnabled) |
730  static_cast<int32_t>(flutter::SemanticsFlags::kHasCheckedState) |
731  static_cast<int32_t>(flutter::SemanticsFlags::kHasEnabledState);
732  nodes[root_node.id] = root_node;
733  bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
734 
735  SemanticsObjectContainer* rootContainer = flutterView.accessibilityElements[0];
736  FlutterSemanticsObject* rootNode = [rootContainer accessibilityElementAtIndex:0];
737 
738  XCTAssertTrue((rootNode.accessibilityTraits & UIAccessibilityTraitButton) > 0);
739  XCTAssertNil(rootNode.accessibilityValue);
740 }
741 
742 - (void)testLayoutChangeWithNonAccessibilityElement {
743  flutter::MockDelegate mock_delegate;
744  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
745  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
746  /*platform=*/thread_task_runner,
747  /*raster=*/thread_task_runner,
748  /*ui=*/thread_task_runner,
749  /*io=*/thread_task_runner);
750  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
751  /*delegate=*/mock_delegate,
752  /*rendering_api=*/mock_delegate.settings_.enable_impeller
755  /*platform_views_controller=*/nil,
756  /*task_runners=*/runners,
757  /*worker_task_runner=*/nil,
758  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
759  id mockFlutterView = OCMClassMock([FlutterView class]);
760  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
761  OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
762 
763  NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
764  [[NSMutableArray alloc] init];
765  auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
766  ios_delegate->on_PostAccessibilityNotification_ =
767  [accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
768  [accessibility_notifications addObject:@{
769  @"notification" : @(notification),
770  @"argument" : argument ? argument : [NSNull null],
771  }];
772  };
773  __block auto bridge =
774  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
775  /*platform_view=*/platform_view.get(),
776  /*platform_views_controller=*/nil,
777  /*ios_delegate=*/std::move(ios_delegate));
778 
779  flutter::CustomAccessibilityActionUpdates actions;
780  flutter::SemanticsNodeUpdates nodes;
781 
782  flutter::SemanticsNode node1;
783  node1.id = 1;
784  node1.label = "node1";
785  node1.childrenInTraversalOrder = {2, 3};
786  node1.childrenInHitTestOrder = {2, 3};
787  nodes[node1.id] = node1;
788  flutter::SemanticsNode node2;
789  node2.id = 2;
790  node2.label = "node2";
791  nodes[node2.id] = node2;
792  flutter::SemanticsNode node3;
793  node3.id = 3;
794  node3.label = "node3";
795  nodes[node3.id] = node3;
796  flutter::SemanticsNode root_node;
797  root_node.id = kRootNodeId;
798  root_node.label = "root";
799  root_node.childrenInTraversalOrder = {1};
800  root_node.childrenInHitTestOrder = {1};
801  nodes[root_node.id] = root_node;
802  bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
803 
804  // Simulates the focusing on the node 1.
805  bridge->AccessibilityObjectDidBecomeFocused(1);
806 
807  // In this update, we make node 1 unfocusable and trigger the
808  // layout change. The accessibility bridge should send layoutchange
809  // notification with the first focusable node under node 1
810  flutter::CustomAccessibilityActionUpdates new_actions;
811  flutter::SemanticsNodeUpdates new_nodes;
812 
813  flutter::SemanticsNode new_node1;
814  new_node1.id = 1;
815  new_node1.childrenInTraversalOrder = {2};
816  new_node1.childrenInHitTestOrder = {2};
817  new_nodes[new_node1.id] = new_node1;
818  bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/new_actions);
819 
820  XCTAssertEqual([accessibility_notifications count], 1ul);
821  SemanticsObject* focusObject = accessibility_notifications[0][@"argument"];
822  // Since node 1 is no longer focusable (no label), it will focus node 2 instead.
823  XCTAssertEqual([focusObject uid], 2);
824  XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
825  UIAccessibilityLayoutChangedNotification);
826 }
827 
828 - (void)testLayoutChangeDoesCallNativeAccessibility {
829  flutter::MockDelegate mock_delegate;
830  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
831  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
832  /*platform=*/thread_task_runner,
833  /*raster=*/thread_task_runner,
834  /*ui=*/thread_task_runner,
835  /*io=*/thread_task_runner);
836  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
837  /*delegate=*/mock_delegate,
838  /*rendering_api=*/mock_delegate.settings_.enable_impeller
841  /*platform_views_controller=*/nil,
842  /*task_runners=*/runners,
843  /*worker_task_runner=*/nil,
844  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
845  id mockFlutterView = OCMClassMock([FlutterView class]);
846  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
847  OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
848 
849  NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
850  [[NSMutableArray alloc] init];
851  auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
852  ios_delegate->on_PostAccessibilityNotification_ =
853  [accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
854  [accessibility_notifications addObject:@{
855  @"notification" : @(notification),
856  @"argument" : argument ? argument : [NSNull null],
857  }];
858  };
859  __block auto bridge =
860  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
861  /*platform_view=*/platform_view.get(),
862  /*platform_views_controller=*/nil,
863  /*ios_delegate=*/std::move(ios_delegate));
864 
865  flutter::CustomAccessibilityActionUpdates actions;
866  flutter::SemanticsNodeUpdates nodes;
867 
868  flutter::SemanticsNode node1;
869  node1.id = 1;
870  node1.label = "node1";
871  nodes[node1.id] = node1;
872  flutter::SemanticsNode root_node;
873  root_node.id = kRootNodeId;
874  root_node.label = "root";
875  root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling);
876  root_node.childrenInTraversalOrder = {1};
877  root_node.childrenInHitTestOrder = {1};
878  nodes[root_node.id] = root_node;
879  bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
880 
881  // Simulates the focusing on the node 0.
882  bridge->AccessibilityObjectDidBecomeFocused(0);
883 
884  // Remove node 1 to trigger a layout change notification
885  flutter::CustomAccessibilityActionUpdates new_actions;
886  flutter::SemanticsNodeUpdates new_nodes;
887 
888  flutter::SemanticsNode new_root_node;
889  new_root_node.id = kRootNodeId;
890  new_root_node.label = "root";
891  new_root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling);
892  new_nodes[new_root_node.id] = new_root_node;
893  bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/new_actions);
894 
895  XCTAssertEqual([accessibility_notifications count], 1ul);
896  id focusObject = accessibility_notifications[0][@"argument"];
897 
898  // Make sure the focused item is not specificed when it stays the same.
899  // See: https://github.com/flutter/flutter/issues/104176
900  XCTAssertEqualObjects(focusObject, [NSNull null]);
901  XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
902  UIAccessibilityLayoutChangedNotification);
903 }
904 
905 - (void)testLayoutChangeDoesCallNativeAccessibilityWhenFocusChanged {
906  flutter::MockDelegate mock_delegate;
907  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
908  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
909  /*platform=*/thread_task_runner,
910  /*raster=*/thread_task_runner,
911  /*ui=*/thread_task_runner,
912  /*io=*/thread_task_runner);
913  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
914  /*delegate=*/mock_delegate,
915  /*rendering_api=*/mock_delegate.settings_.enable_impeller
918  /*platform_views_controller=*/nil,
919  /*task_runners=*/runners,
920  /*worker_task_runner=*/nil,
921  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
922  id mockFlutterView = OCMClassMock([FlutterView class]);
923  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
924  OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
925 
926  NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
927  [[NSMutableArray alloc] init];
928  auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
929  ios_delegate->on_PostAccessibilityNotification_ =
930  [accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
931  [accessibility_notifications addObject:@{
932  @"notification" : @(notification),
933  @"argument" : argument ? argument : [NSNull null],
934  }];
935  };
936  __block auto bridge =
937  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
938  /*platform_view=*/platform_view.get(),
939  /*platform_views_controller=*/nil,
940  /*ios_delegate=*/std::move(ios_delegate));
941 
942  flutter::CustomAccessibilityActionUpdates actions;
943  flutter::SemanticsNodeUpdates nodes;
944 
945  flutter::SemanticsNode node1;
946  node1.id = 1;
947  node1.label = "node1";
948  nodes[node1.id] = node1;
949  flutter::SemanticsNode root_node;
950  root_node.id = kRootNodeId;
951  root_node.label = "root";
952  root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling);
953  root_node.childrenInTraversalOrder = {1};
954  root_node.childrenInHitTestOrder = {1};
955  nodes[root_node.id] = root_node;
956  bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
957 
958  // Simulates the focusing on the node 1.
959  bridge->AccessibilityObjectDidBecomeFocused(1);
960 
961  // Remove node 1 to trigger a layout change notification, and focus should be one root
962  flutter::CustomAccessibilityActionUpdates new_actions;
963  flutter::SemanticsNodeUpdates new_nodes;
964 
965  flutter::SemanticsNode new_root_node;
966  new_root_node.id = kRootNodeId;
967  new_root_node.label = "root";
968  new_root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling);
969  new_nodes[new_root_node.id] = new_root_node;
970  bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/new_actions);
971 
972  XCTAssertEqual([accessibility_notifications count], 1ul);
973  SemanticsObject* focusObject2 = accessibility_notifications[0][@"argument"];
974 
975  // Bridge should ask accessibility to focus on root because node 1 is moved from screen.
976  XCTAssertTrue([focusObject2 isKindOfClass:[FlutterSemanticsScrollView class]]);
977  XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
978  UIAccessibilityLayoutChangedNotification);
979 }
980 
981 - (void)testScrollableSemanticsContainerReturnsCorrectChildren {
982  flutter::MockDelegate mock_delegate;
983  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
984  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
985  /*platform=*/thread_task_runner,
986  /*raster=*/thread_task_runner,
987  /*ui=*/thread_task_runner,
988  /*io=*/thread_task_runner);
989  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
990  /*delegate=*/mock_delegate,
991  /*rendering_api=*/mock_delegate.settings_.enable_impeller
994  /*platform_views_controller=*/nil,
995  /*task_runners=*/runners,
996  /*worker_task_runner=*/nil,
997  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
998  id mockFlutterView = OCMClassMock([FlutterView class]);
999  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
1000  OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
1001 
1002  OCMExpect([mockFlutterView
1003  setAccessibilityElements:[OCMArg checkWithBlock:^BOOL(NSArray* value) {
1004  if ([value count] != 1) {
1005  return NO;
1006  }
1007  SemanticsObjectContainer* container = value[0];
1008  SemanticsObject* object = container.semanticsObject;
1009  FlutterScrollableSemanticsObject* scrollable =
1010  (FlutterScrollableSemanticsObject*)object.children[0];
1011  id nativeScrollable = scrollable.nativeAccessibility;
1012  SemanticsObjectContainer* scrollableContainer = [nativeScrollable accessibilityContainer];
1013  return [scrollableContainer indexOfAccessibilityElement:nativeScrollable] == 1;
1014  }]]);
1015  auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
1016  __block auto bridge =
1017  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
1018  /*platform_view=*/platform_view.get(),
1019  /*platform_views_controller=*/nil,
1020  /*ios_delegate=*/std::move(ios_delegate));
1021 
1022  flutter::CustomAccessibilityActionUpdates actions;
1023  flutter::SemanticsNodeUpdates nodes;
1024 
1025  flutter::SemanticsNode node1;
1026  node1.id = 1;
1027  node1.label = "node1";
1028  node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling);
1029  nodes[node1.id] = node1;
1030  flutter::SemanticsNode root_node;
1031  root_node.id = kRootNodeId;
1032  root_node.label = "root";
1033  root_node.childrenInTraversalOrder = {1};
1034  root_node.childrenInHitTestOrder = {1};
1035  nodes[root_node.id] = root_node;
1036  bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
1037  OCMVerifyAll(mockFlutterView);
1038 }
1039 
1040 - (void)testAnnouncesRouteChangesAndLayoutChangeInOneUpdate {
1041  flutter::MockDelegate mock_delegate;
1042  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
1043  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
1044  /*platform=*/thread_task_runner,
1045  /*raster=*/thread_task_runner,
1046  /*ui=*/thread_task_runner,
1047  /*io=*/thread_task_runner);
1048  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
1049  /*delegate=*/mock_delegate,
1050  /*rendering_api=*/mock_delegate.settings_.enable_impeller
1053  /*platform_views_controller=*/nil,
1054  /*task_runners=*/runners,
1055  /*worker_task_runner=*/nil,
1056  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
1057  id mockFlutterView = OCMClassMock([FlutterView class]);
1058  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
1059  OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
1060 
1061  NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
1062  [[NSMutableArray alloc] init];
1063  auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
1064  ios_delegate->on_PostAccessibilityNotification_ =
1065  [accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
1066  [accessibility_notifications addObject:@{
1067  @"notification" : @(notification),
1068  @"argument" : argument ? argument : [NSNull null],
1069  }];
1070  };
1071  __block auto bridge =
1072  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
1073  /*platform_view=*/platform_view.get(),
1074  /*platform_views_controller=*/nil,
1075  /*ios_delegate=*/std::move(ios_delegate));
1076 
1077  flutter::CustomAccessibilityActionUpdates actions;
1078  flutter::SemanticsNodeUpdates nodes;
1079 
1080  flutter::SemanticsNode node1;
1081  node1.id = 1;
1082  node1.label = "node1";
1083  node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
1084  static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
1085  nodes[node1.id] = node1;
1086  flutter::SemanticsNode node3;
1087  node3.id = 3;
1088  node3.label = "node3";
1089  nodes[node3.id] = node3;
1090  flutter::SemanticsNode root_node;
1091  root_node.id = kRootNodeId;
1092  root_node.label = "root";
1093  root_node.childrenInTraversalOrder = {1, 3};
1094  root_node.childrenInHitTestOrder = {1, 3};
1095  nodes[root_node.id] = root_node;
1096  bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
1097 
1098  XCTAssertEqual([accessibility_notifications count], 1ul);
1099  XCTAssertEqualObjects(accessibility_notifications[0][@"argument"], @"node1");
1100  XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
1101  UIAccessibilityScreenChangedNotification);
1102 
1103  // Simulates the focusing on the node 0.
1104  bridge->AccessibilityObjectDidBecomeFocused(0);
1105 
1106  flutter::SemanticsNodeUpdates new_nodes;
1107 
1108  flutter::SemanticsNode new_node1;
1109  new_node1.id = 1;
1110  new_node1.label = "new_node1";
1111  new_node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
1112  static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
1113  new_node1.childrenInTraversalOrder = {2};
1114  new_node1.childrenInHitTestOrder = {2};
1115  new_nodes[new_node1.id] = new_node1;
1116  flutter::SemanticsNode new_node2;
1117  new_node2.id = 2;
1118  new_node2.label = "new_node2";
1119  new_node2.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
1120  static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
1121  new_nodes[new_node2.id] = new_node2;
1122  flutter::SemanticsNode new_root_node;
1123  new_root_node.id = kRootNodeId;
1124  new_root_node.label = "root";
1125  new_root_node.childrenInTraversalOrder = {1};
1126  new_root_node.childrenInHitTestOrder = {1};
1127  new_nodes[new_root_node.id] = new_root_node;
1128  bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/actions);
1129  XCTAssertEqual([accessibility_notifications count], 3ul);
1130  XCTAssertEqualObjects(accessibility_notifications[1][@"argument"], @"new_node2");
1131  XCTAssertEqual([accessibility_notifications[1][@"notification"] unsignedIntValue],
1132  UIAccessibilityScreenChangedNotification);
1133  SemanticsObject* focusObject = accessibility_notifications[2][@"argument"];
1134  XCTAssertEqual([focusObject uid], 0);
1135  XCTAssertEqual([accessibility_notifications[2][@"notification"] unsignedIntValue],
1136  UIAccessibilityLayoutChangedNotification);
1137 }
1138 
1139 - (void)testAnnouncesRouteChangesWhenAddAdditionalRoute {
1140  flutter::MockDelegate mock_delegate;
1141  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
1142  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
1143  /*platform=*/thread_task_runner,
1144  /*raster=*/thread_task_runner,
1145  /*ui=*/thread_task_runner,
1146  /*io=*/thread_task_runner);
1147  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
1148  /*delegate=*/mock_delegate,
1149  /*rendering_api=*/mock_delegate.settings_.enable_impeller
1152  /*platform_views_controller=*/nil,
1153  /*task_runners=*/runners,
1154  /*worker_task_runner=*/nil,
1155  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
1156  id mockFlutterView = OCMClassMock([FlutterView class]);
1157  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
1158  OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
1159 
1160  NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
1161  [[NSMutableArray alloc] init];
1162  auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
1163  ios_delegate->on_PostAccessibilityNotification_ =
1164  [accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
1165  [accessibility_notifications addObject:@{
1166  @"notification" : @(notification),
1167  @"argument" : argument ? argument : [NSNull null],
1168  }];
1169  };
1170  __block auto bridge =
1171  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
1172  /*platform_view=*/platform_view.get(),
1173  /*platform_views_controller=*/nil,
1174  /*ios_delegate=*/std::move(ios_delegate));
1175 
1176  flutter::CustomAccessibilityActionUpdates actions;
1177  flutter::SemanticsNodeUpdates nodes;
1178 
1179  flutter::SemanticsNode node1;
1180  node1.id = 1;
1181  node1.label = "node1";
1182  node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
1183  static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
1184  nodes[node1.id] = node1;
1185  flutter::SemanticsNode root_node;
1186  root_node.id = kRootNodeId;
1187  root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
1188  root_node.childrenInTraversalOrder = {1};
1189  root_node.childrenInHitTestOrder = {1};
1190  nodes[root_node.id] = root_node;
1191  bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
1192 
1193  XCTAssertEqual([accessibility_notifications count], 1ul);
1194  XCTAssertEqualObjects(accessibility_notifications[0][@"argument"], @"node1");
1195  XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
1196  UIAccessibilityScreenChangedNotification);
1197 
1198  flutter::SemanticsNodeUpdates new_nodes;
1199 
1200  flutter::SemanticsNode new_node1;
1201  new_node1.id = 1;
1202  new_node1.label = "new_node1";
1203  new_node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
1204  static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
1205  new_node1.childrenInTraversalOrder = {2};
1206  new_node1.childrenInHitTestOrder = {2};
1207  new_nodes[new_node1.id] = new_node1;
1208  flutter::SemanticsNode new_node2;
1209  new_node2.id = 2;
1210  new_node2.label = "new_node2";
1211  new_node2.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
1212  static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
1213  new_nodes[new_node2.id] = new_node2;
1214  flutter::SemanticsNode new_root_node;
1215  new_root_node.id = kRootNodeId;
1216  new_root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
1217  new_root_node.childrenInTraversalOrder = {1};
1218  new_root_node.childrenInHitTestOrder = {1};
1219  new_nodes[new_root_node.id] = new_root_node;
1220  bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/actions);
1221  XCTAssertEqual([accessibility_notifications count], 2ul);
1222  XCTAssertEqualObjects(accessibility_notifications[1][@"argument"], @"new_node2");
1223  XCTAssertEqual([accessibility_notifications[1][@"notification"] unsignedIntValue],
1224  UIAccessibilityScreenChangedNotification);
1225 }
1226 
1227 - (void)testAnnouncesRouteChangesRemoveRouteInMiddle {
1228  flutter::MockDelegate mock_delegate;
1229  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
1230  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
1231  /*platform=*/thread_task_runner,
1232  /*raster=*/thread_task_runner,
1233  /*ui=*/thread_task_runner,
1234  /*io=*/thread_task_runner);
1235  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
1236  /*delegate=*/mock_delegate,
1237  /*rendering_api=*/mock_delegate.settings_.enable_impeller
1240  /*platform_views_controller=*/nil,
1241  /*task_runners=*/runners,
1242  /*worker_task_runner=*/nil,
1243  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
1244  id mockFlutterView = OCMClassMock([FlutterView class]);
1245  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
1246  OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
1247 
1248  NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
1249  [[NSMutableArray alloc] init];
1250  auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
1251  ios_delegate->on_PostAccessibilityNotification_ =
1252  [accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
1253  [accessibility_notifications addObject:@{
1254  @"notification" : @(notification),
1255  @"argument" : argument ? argument : [NSNull null],
1256  }];
1257  };
1258  __block auto bridge =
1259  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
1260  /*platform_view=*/platform_view.get(),
1261  /*platform_views_controller=*/nil,
1262  /*ios_delegate=*/std::move(ios_delegate));
1263 
1264  flutter::CustomAccessibilityActionUpdates actions;
1265  flutter::SemanticsNodeUpdates nodes;
1266 
1267  flutter::SemanticsNode node1;
1268  node1.id = 1;
1269  node1.label = "node1";
1270  node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
1271  static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
1272  node1.childrenInTraversalOrder = {2};
1273  node1.childrenInHitTestOrder = {2};
1274  nodes[node1.id] = node1;
1275  flutter::SemanticsNode node2;
1276  node2.id = 2;
1277  node2.label = "node2";
1278  node2.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
1279  static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
1280  nodes[node2.id] = node2;
1281  flutter::SemanticsNode root_node;
1282  root_node.id = kRootNodeId;
1283  root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
1284  root_node.childrenInTraversalOrder = {1};
1285  root_node.childrenInHitTestOrder = {1};
1286  nodes[root_node.id] = root_node;
1287  bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
1288 
1289  XCTAssertEqual([accessibility_notifications count], 1ul);
1290  XCTAssertEqualObjects(accessibility_notifications[0][@"argument"], @"node2");
1291  XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
1292  UIAccessibilityScreenChangedNotification);
1293 
1294  flutter::SemanticsNodeUpdates new_nodes;
1295 
1296  flutter::SemanticsNode new_node1;
1297  new_node1.id = 1;
1298  new_node1.label = "new_node1";
1299  new_node1.childrenInTraversalOrder = {2};
1300  new_node1.childrenInHitTestOrder = {2};
1301  new_nodes[new_node1.id] = new_node1;
1302  flutter::SemanticsNode new_node2;
1303  new_node2.id = 2;
1304  new_node2.label = "new_node2";
1305  new_node2.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
1306  static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
1307  new_nodes[new_node2.id] = new_node2;
1308  flutter::SemanticsNode new_root_node;
1309  new_root_node.id = kRootNodeId;
1310  new_root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
1311  new_root_node.childrenInTraversalOrder = {1};
1312  new_root_node.childrenInHitTestOrder = {1};
1313  new_nodes[new_root_node.id] = new_root_node;
1314  bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/actions);
1315  XCTAssertEqual([accessibility_notifications count], 2ul);
1316  XCTAssertEqualObjects(accessibility_notifications[1][@"argument"], @"new_node2");
1317  XCTAssertEqual([accessibility_notifications[1][@"notification"] unsignedIntValue],
1318  UIAccessibilityScreenChangedNotification);
1319 }
1320 
1321 - (void)testHandleEvent {
1322  flutter::MockDelegate mock_delegate;
1323  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
1324  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
1325  /*platform=*/thread_task_runner,
1326  /*raster=*/thread_task_runner,
1327  /*ui=*/thread_task_runner,
1328  /*io=*/thread_task_runner);
1329  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
1330  /*delegate=*/mock_delegate,
1331  /*rendering_api=*/mock_delegate.settings_.enable_impeller
1334  /*platform_views_controller=*/nil,
1335  /*task_runners=*/runners,
1336  /*worker_task_runner=*/nil,
1337  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
1338  id mockFlutterView = OCMClassMock([FlutterView class]);
1339  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
1340  OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
1341 
1342  NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
1343  [[NSMutableArray alloc] init];
1344  auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
1345  ios_delegate->on_PostAccessibilityNotification_ =
1346  [accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
1347  [accessibility_notifications addObject:@{
1348  @"notification" : @(notification),
1349  @"argument" : argument ? argument : [NSNull null],
1350  }];
1351  };
1352  __block auto bridge =
1353  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
1354  /*platform_view=*/platform_view.get(),
1355  /*platform_views_controller=*/nil,
1356  /*ios_delegate=*/std::move(ios_delegate));
1357 
1358  NSDictionary<NSString*, id>* annotatedEvent = @{@"type" : @"focus", @"nodeId" : @123};
1359 
1360  bridge->HandleEvent(annotatedEvent);
1361 
1362  XCTAssertEqual([accessibility_notifications count], 1ul);
1363  XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
1364  UIAccessibilityLayoutChangedNotification);
1365 }
1366 
1367 - (void)testAccessibilityObjectDidBecomeFocused {
1368  flutter::MockDelegate mock_delegate;
1369  auto thread = std::make_unique<fml::Thread>("AccessibilityBridgeTest");
1370  auto thread_task_runner = thread->GetTaskRunner();
1371  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
1372  /*platform=*/thread_task_runner,
1373  /*raster=*/thread_task_runner,
1374  /*ui=*/thread_task_runner,
1375  /*io=*/thread_task_runner);
1376  id messenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
1377  id engine = OCMClassMock([FlutterEngine class]);
1378  id flutterViewController = OCMClassMock([FlutterViewController class]);
1379 
1380  OCMStub([flutterViewController engine]).andReturn(engine);
1381  OCMStub([engine binaryMessenger]).andReturn(messenger);
1382  FlutterBinaryMessengerConnection connection = 123;
1383  OCMStub([messenger setMessageHandlerOnChannel:@"flutter/accessibility"
1384  binaryMessageHandler:[OCMArg any]])
1385  .andReturn(connection);
1386 
1387  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
1388  /*delegate=*/mock_delegate,
1389  /*rendering_api=*/mock_delegate.settings_.enable_impeller
1392  /*platform_views_controller=*/nil,
1393  /*task_runners=*/runners,
1394  /*worker_task_runner=*/nil,
1395  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
1396  fml::AutoResetWaitableEvent latch;
1397  thread_task_runner->PostTask([&] {
1398  auto weakFactory =
1399  std::make_unique<fml::WeakNSObjectFactory<FlutterViewController>>(flutterViewController);
1400  platform_view->SetOwnerViewController(weakFactory->GetWeakNSObject());
1401  auto bridge =
1402  std::make_unique<flutter::AccessibilityBridge>(/*view=*/nil,
1403  /*platform_view=*/platform_view.get(),
1404  /*platform_views_controller=*/nil);
1405  XCTAssertTrue(bridge.get());
1406  OCMVerify([messenger setMessageHandlerOnChannel:@"flutter/accessibility"
1407  binaryMessageHandler:[OCMArg isNotNil]]);
1408 
1409  bridge->AccessibilityObjectDidBecomeFocused(123);
1410 
1411  NSDictionary<NSString*, id>* annotatedEvent = @{@"type" : @"didGainFocus", @"nodeId" : @123};
1412  NSData* encodedMessage = [[FlutterStandardMessageCodec sharedInstance] encode:annotatedEvent];
1413 
1414  OCMVerify([messenger sendOnChannel:@"flutter/accessibility" message:encodedMessage]);
1415  latch.Signal();
1416  });
1417  latch.Wait();
1418 
1419  [engine stopMocking];
1420 }
1421 
1422 - (void)testAnnouncesRouteChangesWhenNoNamesRoute {
1423  flutter::MockDelegate mock_delegate;
1424  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
1425  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
1426  /*platform=*/thread_task_runner,
1427  /*raster=*/thread_task_runner,
1428  /*ui=*/thread_task_runner,
1429  /*io=*/thread_task_runner);
1430  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
1431  /*delegate=*/mock_delegate,
1432  /*rendering_api=*/mock_delegate.settings_.enable_impeller
1435  /*platform_views_controller=*/nil,
1436  /*task_runners=*/runners,
1437  /*worker_task_runner=*/nil,
1438  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
1439  id mockFlutterView = OCMClassMock([FlutterView class]);
1440  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
1441  OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
1442 
1443  NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
1444  [[NSMutableArray alloc] init];
1445  auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
1446  ios_delegate->on_PostAccessibilityNotification_ =
1447  [accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
1448  [accessibility_notifications addObject:@{
1449  @"notification" : @(notification),
1450  @"argument" : argument ? argument : [NSNull null],
1451  }];
1452  };
1453  __block auto bridge =
1454  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
1455  /*platform_view=*/platform_view.get(),
1456  /*platform_views_controller=*/nil,
1457  /*ios_delegate=*/std::move(ios_delegate));
1458 
1459  flutter::CustomAccessibilityActionUpdates actions;
1460  flutter::SemanticsNodeUpdates nodes;
1461 
1462  flutter::SemanticsNode node1;
1463  node1.id = 1;
1464  node1.label = "node1";
1465  node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
1466  static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
1467  node1.childrenInTraversalOrder = {2, 3};
1468  node1.childrenInHitTestOrder = {2, 3};
1469  nodes[node1.id] = node1;
1470  flutter::SemanticsNode node2;
1471  node2.id = 2;
1472  node2.label = "node2";
1473  nodes[node2.id] = node2;
1474  flutter::SemanticsNode node3;
1475  node3.id = 3;
1476  node3.label = "node3";
1477  nodes[node3.id] = node3;
1478  flutter::SemanticsNode root_node;
1479  root_node.id = kRootNodeId;
1480  root_node.childrenInTraversalOrder = {1};
1481  root_node.childrenInHitTestOrder = {1};
1482  nodes[root_node.id] = root_node;
1483  bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
1484 
1485  // Notification should focus first focusable node, which is node1.
1486  XCTAssertEqual([accessibility_notifications count], 1ul);
1487  id focusObject = accessibility_notifications[0][@"argument"];
1488  XCTAssertTrue([focusObject isKindOfClass:[NSString class]]);
1489  XCTAssertEqualObjects(focusObject, @"node1");
1490  XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
1491  UIAccessibilityScreenChangedNotification);
1492 }
1493 
1494 - (void)testAnnouncesLayoutChangeWithNilIfLastFocusIsRemoved {
1495  flutter::MockDelegate mock_delegate;
1496  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
1497  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
1498  /*platform=*/thread_task_runner,
1499  /*raster=*/thread_task_runner,
1500  /*ui=*/thread_task_runner,
1501  /*io=*/thread_task_runner);
1502  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
1503  /*delegate=*/mock_delegate,
1504  /*rendering_api=*/mock_delegate.settings_.enable_impeller
1507  /*platform_views_controller=*/nil,
1508  /*task_runners=*/runners,
1509  /*worker_task_runner=*/nil,
1510  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
1511  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
1512  id mockFlutterView = OCMClassMock([FlutterView class]);
1513  OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
1514 
1515  NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
1516  [[NSMutableArray alloc] init];
1517  auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
1518  ios_delegate->on_PostAccessibilityNotification_ =
1519  [accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
1520  [accessibility_notifications addObject:@{
1521  @"notification" : @(notification),
1522  @"argument" : argument ? argument : [NSNull null],
1523  }];
1524  };
1525  __block auto bridge =
1526  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
1527  /*platform_view=*/platform_view.get(),
1528  /*platform_views_controller=*/nil,
1529  /*ios_delegate=*/std::move(ios_delegate));
1530 
1531  flutter::CustomAccessibilityActionUpdates actions;
1532  flutter::SemanticsNodeUpdates first_update;
1533 
1534  flutter::SemanticsNode route_node;
1535  route_node.id = 1;
1536  route_node.label = "route";
1537  first_update[route_node.id] = route_node;
1538  flutter::SemanticsNode root_node;
1539  root_node.id = kRootNodeId;
1540  root_node.label = "root";
1541  root_node.childrenInTraversalOrder = {1};
1542  root_node.childrenInHitTestOrder = {1};
1543  first_update[root_node.id] = root_node;
1544  bridge->UpdateSemantics(/*nodes=*/first_update, /*actions=*/actions);
1545 
1546  XCTAssertEqual([accessibility_notifications count], 0ul);
1547  // Simulates the focusing on the node 1.
1548  bridge->AccessibilityObjectDidBecomeFocused(1);
1549 
1550  flutter::SemanticsNodeUpdates second_update;
1551  // Simulates the removal of the node 1
1552  flutter::SemanticsNode new_root_node;
1553  new_root_node.id = kRootNodeId;
1554  new_root_node.label = "root";
1555  second_update[root_node.id] = new_root_node;
1556  bridge->UpdateSemantics(/*nodes=*/second_update, /*actions=*/actions);
1557  SemanticsObject* focusObject = accessibility_notifications[0][@"argument"];
1558  // The node 1 was removed, so the bridge will set the focus object to root.
1559  XCTAssertEqual([focusObject uid], 0);
1560  XCTAssertEqualObjects([focusObject accessibilityLabel], @"root");
1561  XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
1562  UIAccessibilityLayoutChangedNotification);
1563 }
1564 
1565 - (void)testAnnouncesLayoutChangeWithTheSameItemFocused {
1566  flutter::MockDelegate mock_delegate;
1567  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
1568  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
1569  /*platform=*/thread_task_runner,
1570  /*raster=*/thread_task_runner,
1571  /*ui=*/thread_task_runner,
1572  /*io=*/thread_task_runner);
1573  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
1574  /*delegate=*/mock_delegate,
1575  /*rendering_api=*/mock_delegate.settings_.enable_impeller
1578  /*platform_views_controller=*/nil,
1579  /*task_runners=*/runners,
1580  /*worker_task_runner=*/nil,
1581  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
1582  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
1583  id mockFlutterView = OCMClassMock([FlutterView class]);
1584  OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
1585 
1586  NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
1587  [[NSMutableArray alloc] init];
1588  auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
1589  ios_delegate->on_PostAccessibilityNotification_ =
1590  [accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
1591  [accessibility_notifications addObject:@{
1592  @"notification" : @(notification),
1593  @"argument" : argument ? argument : [NSNull null],
1594  }];
1595  };
1596  __block auto bridge =
1597  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
1598  /*platform_view=*/platform_view.get(),
1599  /*platform_views_controller=*/nil,
1600  /*ios_delegate=*/std::move(ios_delegate));
1601 
1602  flutter::CustomAccessibilityActionUpdates actions;
1603  flutter::SemanticsNodeUpdates first_update;
1604 
1605  flutter::SemanticsNode node_one;
1606  node_one.id = 1;
1607  node_one.label = "route1";
1608  first_update[node_one.id] = node_one;
1609  flutter::SemanticsNode node_two;
1610  node_two.id = 2;
1611  node_two.label = "route2";
1612  first_update[node_two.id] = node_two;
1613  flutter::SemanticsNode root_node;
1614  root_node.id = kRootNodeId;
1615  root_node.label = "root";
1616  root_node.childrenInTraversalOrder = {1, 2};
1617  root_node.childrenInHitTestOrder = {1, 2};
1618  first_update[root_node.id] = root_node;
1619  bridge->UpdateSemantics(/*nodes=*/first_update, /*actions=*/actions);
1620 
1621  XCTAssertEqual([accessibility_notifications count], 0ul);
1622  // Simulates the focusing on the node 1.
1623  bridge->AccessibilityObjectDidBecomeFocused(1);
1624 
1625  flutter::SemanticsNodeUpdates second_update;
1626  // Simulates the removal of the node 2.
1627  flutter::SemanticsNode new_root_node;
1628  new_root_node.id = kRootNodeId;
1629  new_root_node.label = "root";
1630  new_root_node.childrenInTraversalOrder = {1};
1631  new_root_node.childrenInHitTestOrder = {1};
1632  second_update[root_node.id] = new_root_node;
1633  bridge->UpdateSemantics(/*nodes=*/second_update, /*actions=*/actions);
1634  id focusObject = accessibility_notifications[0][@"argument"];
1635  // Since we have focused on the node 1 right before the layout changed, the bridge should not ask
1636  // to refocus again on the same node.
1637  XCTAssertEqualObjects(focusObject, [NSNull null]);
1638  XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
1639  UIAccessibilityLayoutChangedNotification);
1640 }
1641 
1642 - (void)testAnnouncesLayoutChangeWhenFocusMovedOutside {
1643  flutter::MockDelegate mock_delegate;
1644  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
1645  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
1646  /*platform=*/thread_task_runner,
1647  /*raster=*/thread_task_runner,
1648  /*ui=*/thread_task_runner,
1649  /*io=*/thread_task_runner);
1650  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
1651  /*delegate=*/mock_delegate,
1652  /*rendering_api=*/mock_delegate.settings_.enable_impeller
1655  /*platform_views_controller=*/nil,
1656  /*task_runners=*/runners,
1657  /*worker_task_runner=*/nil,
1658  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
1659  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
1660  id mockFlutterView = OCMClassMock([FlutterView class]);
1661  OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
1662 
1663  NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
1664  [[NSMutableArray alloc] init];
1665  auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
1666  ios_delegate->on_PostAccessibilityNotification_ =
1667  [accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
1668  [accessibility_notifications addObject:@{
1669  @"notification" : @(notification),
1670  @"argument" : argument ? argument : [NSNull null],
1671  }];
1672  };
1673  __block auto bridge =
1674  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
1675  /*platform_view=*/platform_view.get(),
1676  /*platform_views_controller=*/nil,
1677  /*ios_delegate=*/std::move(ios_delegate));
1678 
1679  flutter::CustomAccessibilityActionUpdates actions;
1680  flutter::SemanticsNodeUpdates first_update;
1681 
1682  flutter::SemanticsNode node_one;
1683  node_one.id = 1;
1684  node_one.label = "route1";
1685  first_update[node_one.id] = node_one;
1686  flutter::SemanticsNode node_two;
1687  node_two.id = 2;
1688  node_two.label = "route2";
1689  first_update[node_two.id] = node_two;
1690  flutter::SemanticsNode root_node;
1691  root_node.id = kRootNodeId;
1692  root_node.label = "root";
1693  root_node.childrenInTraversalOrder = {1, 2};
1694  root_node.childrenInHitTestOrder = {1, 2};
1695  first_update[root_node.id] = root_node;
1696  bridge->UpdateSemantics(/*nodes=*/first_update, /*actions=*/actions);
1697 
1698  XCTAssertEqual([accessibility_notifications count], 0ul);
1699  // Simulates the focusing on the node 1.
1700  bridge->AccessibilityObjectDidBecomeFocused(1);
1701  // Simulates that the focus move outside of flutter.
1702  bridge->AccessibilityObjectDidLoseFocus(1);
1703 
1704  flutter::SemanticsNodeUpdates second_update;
1705  // Simulates the removal of the node 2.
1706  flutter::SemanticsNode new_root_node;
1707  new_root_node.id = kRootNodeId;
1708  new_root_node.label = "root";
1709  new_root_node.childrenInTraversalOrder = {1};
1710  new_root_node.childrenInHitTestOrder = {1};
1711  second_update[root_node.id] = new_root_node;
1712  bridge->UpdateSemantics(/*nodes=*/second_update, /*actions=*/actions);
1713  NSNull* focusObject = accessibility_notifications[0][@"argument"];
1714  // Since the focus is moved outside of the app right before the layout
1715  // changed, the bridge should not try to refocus anything .
1716  XCTAssertEqual(focusObject, [NSNull null]);
1717  XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
1718  UIAccessibilityLayoutChangedNotification);
1719 }
1720 
1721 - (void)testAnnouncesScrollChangeWithLastFocused {
1722  flutter::MockDelegate mock_delegate;
1723  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
1724  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
1725  /*platform=*/thread_task_runner,
1726  /*raster=*/thread_task_runner,
1727  /*ui=*/thread_task_runner,
1728  /*io=*/thread_task_runner);
1729  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
1730  /*delegate=*/mock_delegate,
1731  /*rendering_api=*/mock_delegate.settings_.enable_impeller
1734  /*platform_views_controller=*/nil,
1735  /*task_runners=*/runners,
1736  /*worker_task_runner=*/nil,
1737  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
1738  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
1739  id mockFlutterView = OCMClassMock([FlutterView class]);
1740  OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
1741 
1742  NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
1743  [[NSMutableArray alloc] init];
1744  auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
1745  ios_delegate->on_PostAccessibilityNotification_ =
1746  [accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
1747  [accessibility_notifications addObject:@{
1748  @"notification" : @(notification),
1749  @"argument" : argument ? argument : [NSNull null],
1750  }];
1751  };
1752  __block auto bridge =
1753  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
1754  /*platform_view=*/platform_view.get(),
1755  /*platform_views_controller=*/nil,
1756  /*ios_delegate=*/std::move(ios_delegate));
1757 
1758  flutter::CustomAccessibilityActionUpdates actions;
1759  flutter::SemanticsNodeUpdates first_update;
1760 
1761  flutter::SemanticsNode node_one;
1762  node_one.id = 1;
1763  node_one.label = "route1";
1764  node_one.scrollPosition = 0.0;
1765  first_update[node_one.id] = node_one;
1766  flutter::SemanticsNode root_node;
1767  root_node.id = kRootNodeId;
1768  root_node.label = "root";
1769  root_node.childrenInTraversalOrder = {1};
1770  root_node.childrenInHitTestOrder = {1};
1771  first_update[root_node.id] = root_node;
1772  bridge->UpdateSemantics(/*nodes=*/first_update, /*actions=*/actions);
1773 
1774  // The first update will trigger a scroll announcement, but we are not interested in it.
1775  [accessibility_notifications removeAllObjects];
1776 
1777  // Simulates the focusing on the node 1.
1778  bridge->AccessibilityObjectDidBecomeFocused(1);
1779 
1780  flutter::SemanticsNodeUpdates second_update;
1781  // Simulates the scrolling on the node 1.
1782  flutter::SemanticsNode new_node_one;
1783  new_node_one.id = 1;
1784  new_node_one.label = "route1";
1785  new_node_one.scrollPosition = 1.0;
1786  second_update[new_node_one.id] = new_node_one;
1787  bridge->UpdateSemantics(/*nodes=*/second_update, /*actions=*/actions);
1788  SemanticsObject* focusObject = accessibility_notifications[0][@"argument"];
1789  // Since we have focused on the node 1 right before the scrolling, the bridge should refocus the
1790  // node 1.
1791  XCTAssertEqual([focusObject uid], 1);
1792  XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
1793  UIAccessibilityPageScrolledNotification);
1794 }
1795 
1796 - (void)testAnnouncesScrollChangeDoesCallNativeAccessibility {
1797  flutter::MockDelegate mock_delegate;
1798  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
1799  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
1800  /*platform=*/thread_task_runner,
1801  /*raster=*/thread_task_runner,
1802  /*ui=*/thread_task_runner,
1803  /*io=*/thread_task_runner);
1804  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
1805  /*delegate=*/mock_delegate,
1806  /*rendering_api=*/mock_delegate.settings_.enable_impeller
1809  /*platform_views_controller=*/nil,
1810  /*task_runners=*/runners,
1811  /*worker_task_runner=*/nil,
1812  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
1813  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
1814  id mockFlutterView = OCMClassMock([FlutterView class]);
1815  OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
1816 
1817  NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
1818  [[NSMutableArray alloc] init];
1819  auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
1820  ios_delegate->on_PostAccessibilityNotification_ =
1821  [accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
1822  [accessibility_notifications addObject:@{
1823  @"notification" : @(notification),
1824  @"argument" : argument ? argument : [NSNull null],
1825  }];
1826  };
1827  __block auto bridge =
1828  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
1829  /*platform_view=*/platform_view.get(),
1830  /*platform_views_controller=*/nil,
1831  /*ios_delegate=*/std::move(ios_delegate));
1832 
1833  flutter::CustomAccessibilityActionUpdates actions;
1834  flutter::SemanticsNodeUpdates first_update;
1835 
1836  flutter::SemanticsNode node_one;
1837  node_one.id = 1;
1838  node_one.label = "route1";
1839  node_one.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling);
1840  node_one.scrollPosition = 0.0;
1841  first_update[node_one.id] = node_one;
1842  flutter::SemanticsNode root_node;
1843  root_node.id = kRootNodeId;
1844  root_node.label = "root";
1845  root_node.childrenInTraversalOrder = {1};
1846  root_node.childrenInHitTestOrder = {1};
1847  first_update[root_node.id] = root_node;
1848  bridge->UpdateSemantics(/*nodes=*/first_update, /*actions=*/actions);
1849 
1850  // The first update will trigger a scroll announcement, but we are not interested in it.
1851  [accessibility_notifications removeAllObjects];
1852 
1853  // Simulates the focusing on the node 1.
1854  bridge->AccessibilityObjectDidBecomeFocused(1);
1855 
1856  flutter::SemanticsNodeUpdates second_update;
1857  // Simulates the scrolling on the node 1.
1858  flutter::SemanticsNode new_node_one;
1859  new_node_one.id = 1;
1860  new_node_one.label = "route1";
1861  new_node_one.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling);
1862  new_node_one.scrollPosition = 1.0;
1863  second_update[new_node_one.id] = new_node_one;
1864  bridge->UpdateSemantics(/*nodes=*/second_update, /*actions=*/actions);
1865  SemanticsObject* focusObject = accessibility_notifications[0][@"argument"];
1866  // Make sure refocus event is sent with the nativeAccessibility of node_one
1867  // which is a FlutterSemanticsScrollView.
1868  XCTAssertTrue([focusObject isKindOfClass:[FlutterSemanticsScrollView class]]);
1869  XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
1870  UIAccessibilityPageScrolledNotification);
1871 }
1872 
1873 - (void)testAnnouncesIgnoresRouteChangesWhenModal {
1874  flutter::MockDelegate mock_delegate;
1875  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
1876  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
1877  /*platform=*/thread_task_runner,
1878  /*raster=*/thread_task_runner,
1879  /*ui=*/thread_task_runner,
1880  /*io=*/thread_task_runner);
1881  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
1882  /*delegate=*/mock_delegate,
1883  /*rendering_api=*/mock_delegate.settings_.enable_impeller
1886  /*platform_views_controller=*/nil,
1887  /*task_runners=*/runners,
1888  /*worker_task_runner=*/nil,
1889  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
1890  id mockFlutterView = OCMClassMock([FlutterView class]);
1891  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
1892  OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
1893  std::string label = "some label";
1894 
1895  NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
1896  [[NSMutableArray alloc] init];
1897  auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
1898  ios_delegate->on_PostAccessibilityNotification_ =
1899  [accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
1900  [accessibility_notifications addObject:@{
1901  @"notification" : @(notification),
1902  @"argument" : argument ? argument : [NSNull null],
1903  }];
1904  };
1905  ios_delegate->result_IsFlutterViewControllerPresentingModalViewController_ = true;
1906  __block auto bridge =
1907  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
1908  /*platform_view=*/platform_view.get(),
1909  /*platform_views_controller=*/nil,
1910  /*ios_delegate=*/std::move(ios_delegate));
1911 
1912  flutter::CustomAccessibilityActionUpdates actions;
1913  flutter::SemanticsNodeUpdates nodes;
1914 
1915  flutter::SemanticsNode route_node;
1916  route_node.id = 1;
1917  route_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
1918  static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
1919  route_node.label = "route";
1920  nodes[route_node.id] = route_node;
1921  flutter::SemanticsNode root_node;
1922  root_node.id = kRootNodeId;
1923  root_node.label = label;
1924  root_node.childrenInTraversalOrder = {1};
1925  root_node.childrenInHitTestOrder = {1};
1926  nodes[root_node.id] = root_node;
1927  bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
1928 
1929  XCTAssertEqual([accessibility_notifications count], 0ul);
1930 }
1931 
1932 - (void)testAnnouncesIgnoresLayoutChangeWhenModal {
1933  flutter::MockDelegate mock_delegate;
1934  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
1935  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
1936  /*platform=*/thread_task_runner,
1937  /*raster=*/thread_task_runner,
1938  /*ui=*/thread_task_runner,
1939  /*io=*/thread_task_runner);
1940  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
1941  /*delegate=*/mock_delegate,
1942  /*rendering_api=*/mock_delegate.settings_.enable_impeller
1945  /*platform_views_controller=*/nil,
1946  /*task_runners=*/runners,
1947  /*worker_task_runner=*/nil,
1948  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
1949  id mockFlutterView = OCMClassMock([FlutterView class]);
1950  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
1951  OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
1952 
1953  NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
1954  [[NSMutableArray alloc] init];
1955  auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
1956  ios_delegate->on_PostAccessibilityNotification_ =
1957  [accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
1958  [accessibility_notifications addObject:@{
1959  @"notification" : @(notification),
1960  @"argument" : argument ? argument : [NSNull null],
1961  }];
1962  };
1963  ios_delegate->result_IsFlutterViewControllerPresentingModalViewController_ = true;
1964  __block auto bridge =
1965  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
1966  /*platform_view=*/platform_view.get(),
1967  /*platform_views_controller=*/nil,
1968  /*ios_delegate=*/std::move(ios_delegate));
1969 
1970  flutter::CustomAccessibilityActionUpdates actions;
1971  flutter::SemanticsNodeUpdates nodes;
1972 
1973  flutter::SemanticsNode child_node;
1974  child_node.id = 1;
1975  child_node.label = "child_node";
1976  nodes[child_node.id] = child_node;
1977  flutter::SemanticsNode root_node;
1978  root_node.id = kRootNodeId;
1979  root_node.label = "root";
1980  root_node.childrenInTraversalOrder = {1};
1981  root_node.childrenInHitTestOrder = {1};
1982  nodes[root_node.id] = root_node;
1983  bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
1984 
1985  // Removes child_node to simulate a layout change.
1986  flutter::SemanticsNodeUpdates new_nodes;
1987  flutter::SemanticsNode new_root_node;
1988  new_root_node.id = kRootNodeId;
1989  new_root_node.label = "root";
1990  new_nodes[new_root_node.id] = new_root_node;
1991  bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/actions);
1992 
1993  XCTAssertEqual([accessibility_notifications count], 0ul);
1994 }
1995 
1996 - (void)testAnnouncesIgnoresScrollChangeWhenModal {
1997  flutter::MockDelegate mock_delegate;
1998  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
1999  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
2000  /*platform=*/thread_task_runner,
2001  /*raster=*/thread_task_runner,
2002  /*ui=*/thread_task_runner,
2003  /*io=*/thread_task_runner);
2004  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
2005  /*delegate=*/mock_delegate,
2006  /*rendering_api=*/mock_delegate.settings_.enable_impeller
2009  /*platform_views_controller=*/nil,
2010  /*task_runners=*/runners,
2011  /*worker_task_runner=*/nil,
2012  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
2013  id mockFlutterView = OCMClassMock([FlutterView class]);
2014  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
2015  OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
2016 
2017  NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
2018  [[NSMutableArray alloc] init];
2019  auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
2020  ios_delegate->on_PostAccessibilityNotification_ =
2021  [accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
2022  [accessibility_notifications addObject:@{
2023  @"notification" : @(notification),
2024  @"argument" : argument ? argument : [NSNull null],
2025  }];
2026  };
2027  ios_delegate->result_IsFlutterViewControllerPresentingModalViewController_ = true;
2028  __block auto bridge =
2029  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
2030  /*platform_view=*/platform_view.get(),
2031  /*platform_views_controller=*/nil,
2032  /*ios_delegate=*/std::move(ios_delegate));
2033 
2034  flutter::CustomAccessibilityActionUpdates actions;
2035  flutter::SemanticsNodeUpdates nodes;
2036 
2037  flutter::SemanticsNode root_node;
2038  root_node.id = kRootNodeId;
2039  root_node.label = "root";
2040  root_node.scrollPosition = 1;
2041  nodes[root_node.id] = root_node;
2042  bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
2043 
2044  // Removes child_node to simulate a layout change.
2045  flutter::SemanticsNodeUpdates new_nodes;
2046  flutter::SemanticsNode new_root_node;
2047  new_root_node.id = kRootNodeId;
2048  new_root_node.label = "root";
2049  new_root_node.scrollPosition = 2;
2050  new_nodes[new_root_node.id] = new_root_node;
2051  bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/actions);
2052 
2053  XCTAssertEqual([accessibility_notifications count], 0ul);
2054 }
2055 
2056 - (void)testAccessibilityMessageAfterDeletion {
2057  flutter::MockDelegate mock_delegate;
2058  auto thread = std::make_unique<fml::Thread>("AccessibilityBridgeTest");
2059  auto thread_task_runner = thread->GetTaskRunner();
2060  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
2061  /*platform=*/thread_task_runner,
2062  /*raster=*/thread_task_runner,
2063  /*ui=*/thread_task_runner,
2064  /*io=*/thread_task_runner);
2065  id messenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
2066  id engine = OCMClassMock([FlutterEngine class]);
2067  id flutterViewController = OCMClassMock([FlutterViewController class]);
2068 
2069  OCMStub([flutterViewController engine]).andReturn(engine);
2070  OCMStub([engine binaryMessenger]).andReturn(messenger);
2071  FlutterBinaryMessengerConnection connection = 123;
2072  OCMStub([messenger setMessageHandlerOnChannel:@"flutter/accessibility"
2073  binaryMessageHandler:[OCMArg any]])
2074  .andReturn(connection);
2075 
2076  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
2077  /*delegate=*/mock_delegate,
2078  /*rendering_api=*/mock_delegate.settings_.enable_impeller
2081  /*platform_views_controller=*/nil,
2082  /*task_runners=*/runners,
2083  /*worker_task_runner=*/nil,
2084  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
2085  fml::AutoResetWaitableEvent latch;
2086  thread_task_runner->PostTask([&] {
2087  auto weakFactory =
2088  std::make_unique<fml::WeakNSObjectFactory<FlutterViewController>>(flutterViewController);
2089  platform_view->SetOwnerViewController(weakFactory->GetWeakNSObject());
2090  auto bridge =
2091  std::make_unique<flutter::AccessibilityBridge>(/*view=*/nil,
2092  /*platform_view=*/platform_view.get(),
2093  /*platform_views_controller=*/nil);
2094  XCTAssertTrue(bridge.get());
2095  OCMVerify([messenger setMessageHandlerOnChannel:@"flutter/accessibility"
2096  binaryMessageHandler:[OCMArg isNotNil]]);
2097  bridge.reset();
2098  latch.Signal();
2099  });
2100  latch.Wait();
2101  OCMVerify([messenger cleanUpConnection:connection]);
2102  [engine stopMocking];
2103 }
2104 
2105 - (void)testFlutterSemanticsScrollViewManagedObjectLifecycleCorrectly {
2106  flutter::MockDelegate mock_delegate;
2107  auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
2108  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
2109  /*platform=*/thread_task_runner,
2110  /*raster=*/thread_task_runner,
2111  /*ui=*/thread_task_runner,
2112  /*io=*/thread_task_runner);
2113  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
2114  /*delegate=*/mock_delegate,
2115  /*rendering_api=*/mock_delegate.settings_.enable_impeller
2118  /*platform_views_controller=*/nil,
2119  /*task_runners=*/runners,
2120  /*worker_task_runner=*/nil,
2121  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
2122  id mockFlutterView = OCMClassMock([FlutterView class]);
2123  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
2124  OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
2125 
2126  auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
2127  __block auto bridge =
2128  std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
2129  /*platform_view=*/platform_view.get(),
2130  /*platform_views_controller=*/nil,
2131  /*ios_delegate=*/std::move(ios_delegate));
2132 
2133  FlutterSemanticsScrollView* flutterSemanticsScrollView;
2134  @autoreleasepool {
2135  FlutterScrollableSemanticsObject* semanticsObject =
2136  [[FlutterScrollableSemanticsObject alloc] initWithBridge:bridge->GetWeakPtr() uid:1234];
2137 
2138  flutterSemanticsScrollView = semanticsObject.nativeAccessibility;
2139  }
2140  XCTAssertTrue(flutterSemanticsScrollView);
2141  // If the _semanticsObject is not a weak pointer this (or any other method on
2142  // flutterSemanticsScrollView) will cause an EXC_BAD_ACCESS.
2143  XCTAssertFalse([flutterSemanticsScrollView isAccessibilityElement]);
2144 }
2145 
2146 - (void)testPlatformViewDestructorDoesNotCallSemanticsAPIs {
2147  class TestDelegate : public flutter::MockDelegate {
2148  public:
2149  void OnPlatformViewSetSemanticsEnabled(bool enabled) override { set_semantics_enabled_calls++; }
2150  int set_semantics_enabled_calls = 0;
2151  };
2152 
2153  TestDelegate test_delegate;
2154  auto thread = std::make_unique<fml::Thread>("AccessibilityBridgeTest");
2155  auto thread_task_runner = thread->GetTaskRunner();
2156  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
2157  /*platform=*/thread_task_runner,
2158  /*raster=*/thread_task_runner,
2159  /*ui=*/thread_task_runner,
2160  /*io=*/thread_task_runner);
2161 
2162  fml::AutoResetWaitableEvent latch;
2163  thread_task_runner->PostTask([&] {
2164  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
2165  /*delegate=*/test_delegate,
2166  /*rendering_api=*/test_delegate.settings_.enable_impeller
2169  /*platform_views_controller=*/nil,
2170  /*task_runners=*/runners,
2171  /*worker_task_runner=*/nil,
2172  /*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
2173 
2174  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
2175  auto flutterPlatformViewsController =
2176  std::make_shared<flutter::FlutterPlatformViewsController>();
2177  OCMStub([mockFlutterViewController platformViewsController])
2178  .andReturn(flutterPlatformViewsController.get());
2179  auto weakFactory = std::make_unique<fml::WeakNSObjectFactory<FlutterViewController>>(
2180  mockFlutterViewController);
2181  platform_view->SetOwnerViewController(weakFactory->GetWeakNSObject());
2182 
2183  platform_view->SetSemanticsEnabled(true);
2184  XCTAssertNotEqual(test_delegate.set_semantics_enabled_calls, 0);
2185 
2186  // Deleting PlatformViewIOS should not call OnPlatformViewSetSemanticsEnabled
2187  test_delegate.set_semantics_enabled_calls = 0;
2188  platform_view.reset();
2189  XCTAssertEqual(test_delegate.set_semantics_enabled_calls, 0);
2190 
2191  latch.Signal();
2192  });
2193  latch.Wait();
2194 }
2195 
2196 @end
FlutterEngine
Definition: FlutterEngine.h:61
FlutterPlatformViews.h
FlutterViewController
Definition: FlutterViewController.h:56
MockFlutterPlatformFactory
Definition: accessibility_bridge_test.mm:55
FlutterSemanticsScrollView.h
FLUTTER_ASSERT_ARC::CreateNewThread
fml::RefPtr< fml::TaskRunner > CreateNewThread(const std::string &name)
Definition: VsyncWaiterIosTest.mm:16
SemanticsObjectContainer::semanticsObject
SemanticsObject * semanticsObject
Definition: SemanticsObject.h:234
MockPlatformView
Definition: accessibility_bridge_test.mm:22
FlutterMacros.h
platform_view
std::unique_ptr< flutter::PlatformViewIOS > platform_view
Definition: FlutterEnginePlatformViewTest.mm:61
FlutterSemanticsScrollView
Definition: FlutterSemanticsScrollView.h:21
FlutterStandardMessageCodec
Definition: FlutterCodecs.h:209
FlutterSemanticsObject
Definition: SemanticsObject.h:154
FlutterMethodCall
Definition: FlutterCodecs.h:220
FlutterPlatformViewGestureRecognizersBlockingPolicyEager
@ FlutterPlatformViewGestureRecognizersBlockingPolicyEager
Definition: FlutterPlugin.h:261
flutter
Definition: accessibility_bridge.h:28
accessibility_bridge.h
FlutterBinaryMessenger.h
FlutterPlatformViews_Internal.h
settings_
flutter::Settings settings_
Definition: FlutterEnginePlatformViewTest.mm:51
FlutterResult
void(^ FlutterResult)(id _Nullable result)
Definition: FlutterChannels.h:194
kRootNodeId
constexpr int32_t kRootNodeId
Definition: SemanticsObject.h:15
flutter::IOSRenderingAPI::kMetal
@ kMetal
FlutterPlatformViewFactory-p
Definition: FlutterPlatformViews.h:26
engine
id engine
Definition: FlutterTextInputPluginTest.mm:89
FlutterViewController_Internal.h
SemanticsObject::nativeAccessibility
id nativeAccessibility
Definition: SemanticsObject.h:82
FlutterView
Definition: FlutterView.h:39
SemanticsObject::uid
int32_t uid
Definition: SemanticsObject.h:35
platform_view_ios.h
AccessibilityBridgeTest
Definition: accessibility_bridge_test.mm:129
FlutterPlatformView-p
Definition: FlutterPlatformViews.h:18
SemanticsObjectContainer
Definition: SemanticsObject.h:226
gMockPlatformView
static __weak MockPlatformView * gMockPlatformView
Definition: accessibility_bridge_test.mm:20
FlutterBinaryMessenger-p
Definition: FlutterBinaryMessenger.h:49
texture_id
int64_t texture_id
Definition: texture_registrar_unittests.cc:24
flutter::IOSRenderingAPI::kSoftware
@ kSoftware
FLUTTER_ASSERT_ARC
Definition: VsyncWaiterIosTest.mm:15
FlutterBinaryMessengerConnection
int64_t FlutterBinaryMessengerConnection
Definition: FlutterBinaryMessenger.h:32
FlutterScrollableSemanticsObject
Definition: SemanticsObject.h:188
MockFlutterPlatformView
Definition: accessibility_bridge_test.mm:40
+[FlutterMessageCodec-p sharedInstance]
instancetype sharedInstance()
SemanticsObject
Definition: SemanticsObject.h:30