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