| Flutter iOS Embedder |

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