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