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