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