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