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