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