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