| Flutter iOS Embedder |

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