| Flutter iOS Embedder |

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