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