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