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