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