blob: 51ad673334886919738bfaf7990f992e2ee9c0d5 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h"
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlatformViews.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h"
#import "flutter/shell/platform/darwin/ios/platform_view_ios.h"
FLUTTER_ASSERT_NOT_ARC
@class MockPlatformView;
static MockPlatformView* gMockPlatformView = nil;
@interface MockPlatformView : UIView
@end
@implementation MockPlatformView
- (instancetype)init {
self = [super init];
if (self) {
gMockPlatformView = self;
}
return self;
}
- (void)dealloc {
gMockPlatformView = nil;
[super dealloc];
}
@end
@interface MockFlutterPlatformView : NSObject <FlutterPlatformView>
@property(nonatomic, strong) UIView* view;
@end
@implementation MockFlutterPlatformView
- (instancetype)init {
if (self = [super init]) {
_view = [[MockPlatformView alloc] init];
}
return self;
}
- (void)dealloc {
[_view release];
_view = nil;
[super dealloc];
}
@end
@interface MockFlutterPlatformFactory : NSObject <FlutterPlatformViewFactory>
@end
@implementation MockFlutterPlatformFactory
- (NSObject<FlutterPlatformView>*)createWithFrame:(CGRect)frame
viewIdentifier:(int64_t)viewId
arguments:(id _Nullable)args {
return [[[MockFlutterPlatformView alloc] init] autorelease];
}
@end
namespace flutter {
namespace {
class MockDelegate : public PlatformView::Delegate {
void OnPlatformViewCreated(std::unique_ptr<Surface> surface) override {}
void OnPlatformViewDestroyed() override {}
void OnPlatformViewScheduleFrame() override {}
void OnPlatformViewSetNextFrameCallback(const fml::closure& closure) override {}
void OnPlatformViewSetViewportMetrics(const ViewportMetrics& metrics) override {}
void OnPlatformViewDispatchPlatformMessage(std::unique_ptr<PlatformMessage> message) override {}
void OnPlatformViewDispatchPointerDataPacket(std::unique_ptr<PointerDataPacket> packet) override {
}
void OnPlatformViewDispatchSemanticsAction(int32_t id,
SemanticsAction action,
fml::MallocMapping args) override {}
void OnPlatformViewSetSemanticsEnabled(bool enabled) override {}
void OnPlatformViewSetAccessibilityFeatures(int32_t flags) override {}
void OnPlatformViewRegisterTexture(std::shared_ptr<Texture> texture) override {}
void OnPlatformViewUnregisterTexture(int64_t texture_id) override {}
void OnPlatformViewMarkTextureFrameAvailable(int64_t texture_id) override {}
void LoadDartDeferredLibrary(intptr_t loading_unit_id,
std::unique_ptr<const fml::Mapping> snapshot_data,
std::unique_ptr<const fml::Mapping> snapshot_instructions) override {
}
void LoadDartDeferredLibraryError(intptr_t loading_unit_id,
const std::string error_message,
bool transient) override {}
void UpdateAssetResolverByType(std::unique_ptr<flutter::AssetResolver> updated_asset_resolver,
flutter::AssetResolver::AssetResolverType type) override {}
};
class MockIosDelegate : public AccessibilityBridge::IosDelegate {
public:
bool IsFlutterViewControllerPresentingModalViewController(
FlutterViewController* view_controller) override {
return result_IsFlutterViewControllerPresentingModalViewController_;
};
void PostAccessibilityNotification(UIAccessibilityNotifications notification,
id argument) override {
if (on_PostAccessibilityNotification_) {
on_PostAccessibilityNotification_(notification, argument);
}
}
std::function<void(UIAccessibilityNotifications, id)> on_PostAccessibilityNotification_;
bool result_IsFlutterViewControllerPresentingModalViewController_ = false;
};
} // namespace
} // namespace flutter
namespace {
fml::RefPtr<fml::TaskRunner> CreateNewThread(std::string name) {
auto thread = std::make_unique<fml::Thread>(name);
auto runner = thread->GetTaskRunner();
return runner;
}
} // namespace
@interface AccessibilityBridgeTest : XCTestCase
@end
@implementation AccessibilityBridgeTest
- (void)testCreate {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view=*/nil,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil);
XCTAssertTrue(bridge.get());
}
- (void)testUpdateSemanticsEmpty {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
id mockFlutterView = OCMClassMock([FlutterView class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
OCMExpect([mockFlutterView setAccessibilityElements:[OCMArg isNil]]);
auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil);
flutter::SemanticsNodeUpdates nodes;
flutter::CustomAccessibilityActionUpdates actions;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
OCMVerifyAll(mockFlutterView);
}
- (void)testUpdateSemanticsOneNode {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
id mockFlutterView = OCMClassMock([FlutterView class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
std::string label = "some label";
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil);
OCMExpect([mockFlutterView setAccessibilityElements:[OCMArg checkWithBlock:^BOOL(NSArray* value) {
if ([value count] != 1) {
return NO;
} else {
SemanticsObjectContainer* container = value[0];
SemanticsObject* object = container.semanticsObject;
return object.uid == kRootNodeId &&
object.bridge.get() == bridge.get() &&
object.node.label == label;
}
}]]);
flutter::SemanticsNodeUpdates nodes;
flutter::SemanticsNode semantics_node;
semantics_node.id = kRootNodeId;
semantics_node.label = label;
nodes[kRootNodeId] = semantics_node;
flutter::CustomAccessibilityActionUpdates actions;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
OCMVerifyAll(mockFlutterView);
}
- (void)testIsVoiceOverRunning {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
id mockFlutterView = OCMClassMock([FlutterView class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
OCMStub([mockFlutterViewController isVoiceOverRunning]).andReturn(YES);
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil);
XCTAssertTrue(bridge->isVoiceOverRunning());
}
- (void)testSemanticsDeallocated {
@autoreleasepool {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto flutterPlatformViewsController =
std::make_shared<flutter::FlutterPlatformViewsController>();
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/flutterPlatformViewsController,
/*task_runners=*/runners);
id mockFlutterView = OCMClassMock([FlutterView class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
std::string label = "some label";
flutterPlatformViewsController->SetFlutterView(mockFlutterView);
MockFlutterPlatformFactory* factory = [[MockFlutterPlatformFactory new] autorelease];
flutterPlatformViewsController->RegisterViewFactory(
factory, @"MockFlutterPlatformView",
FlutterPlatformViewGestureRecognizersBlockingPolicyEager);
FlutterResult result = ^(id result) {
};
flutterPlatformViewsController->OnMethodCall(
[FlutterMethodCall
methodCallWithMethodName:@"create"
arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}],
result);
auto bridge = std::make_unique<flutter::AccessibilityBridge>(
/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/flutterPlatformViewsController);
flutter::SemanticsNodeUpdates nodes;
flutter::SemanticsNode semantics_node;
semantics_node.id = 2;
semantics_node.platformViewId = 2;
semantics_node.label = label;
nodes[kRootNodeId] = semantics_node;
flutter::CustomAccessibilityActionUpdates actions;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
XCTAssertNotNil(gMockPlatformView);
flutterPlatformViewsController->Reset();
}
XCTAssertNil(gMockPlatformView);
}
- (void)testReplacedSemanticsDoesNotCleanupChildren {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto flutterPlatformViewsController = std::make_shared<flutter::FlutterPlatformViewsController>();
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/flutterPlatformViewsController,
/*task_runners=*/runners);
id engine = OCMClassMock([FlutterEngine class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
FlutterView* flutterView = [[FlutterView alloc] initWithDelegate:engine opaque:YES];
OCMStub([mockFlutterViewController view]).andReturn(flutterView);
std::string label = "some label";
auto bridge = std::make_unique<flutter::AccessibilityBridge>(
/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/flutterPlatformViewsController);
@autoreleasepool {
flutter::SemanticsNodeUpdates nodes;
flutter::SemanticsNode parent;
parent.id = 0;
parent.rect = SkRect::MakeXYWH(0, 0, 100, 200);
parent.label = "label";
parent.value = "value";
parent.hint = "hint";
flutter::SemanticsNode node;
node.id = 1;
node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
node.label = "label";
node.value = "value";
node.hint = "hint";
node.scrollExtentMax = 100.0;
node.scrollPosition = 0.0;
parent.childrenInTraversalOrder.push_back(1);
flutter::SemanticsNode child;
child.id = 2;
child.rect = SkRect::MakeXYWH(0, 0, 100, 200);
child.label = "label";
child.value = "value";
child.hint = "hint";
node.childrenInTraversalOrder.push_back(2);
nodes[0] = parent;
nodes[1] = node;
nodes[2] = child;
flutter::CustomAccessibilityActionUpdates actions;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
// Add implicit scroll from node 1 to cause replacement.
flutter::SemanticsNodeUpdates new_nodes;
flutter::SemanticsNode new_node;
new_node.id = 1;
new_node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
new_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling);
new_node.actions = flutter::kHorizontalScrollSemanticsActions;
new_node.label = "label";
new_node.value = "value";
new_node.hint = "hint";
new_node.scrollExtentMax = 100.0;
new_node.scrollPosition = 0.0;
new_node.childrenInTraversalOrder.push_back(2);
new_nodes[1] = new_node;
bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/actions);
}
/// The old node should be deallocated at this moment. Procced to check
/// accessibility tree integrity.
id rootContainer = flutterView.accessibilityElements[0];
XCTAssertTrue([rootContainer accessibilityElementCount] ==
2); // one for root, one for scrollable.
id scrollableContainer = [rootContainer accessibilityElementAtIndex:1];
XCTAssertTrue([scrollableContainer accessibilityElementCount] ==
2); // one for scrollable, one for scrollable child.
id child = [scrollableContainer accessibilityElementAtIndex:1];
/// Replacing node 1 should not accidentally clean up its child's container.
XCTAssertNotNil([child accessibilityContainer]);
}
- (void)testScrollableSemanticsDeallocated {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto flutterPlatformViewsController = std::make_shared<flutter::FlutterPlatformViewsController>();
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/flutterPlatformViewsController,
/*task_runners=*/runners);
id engine = OCMClassMock([FlutterEngine class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
FlutterView* flutterView = [[FlutterView alloc] initWithDelegate:engine opaque:YES];
OCMStub([mockFlutterViewController view]).andReturn(flutterView);
std::string label = "some label";
@autoreleasepool {
auto bridge = std::make_unique<flutter::AccessibilityBridge>(
/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/flutterPlatformViewsController);
flutter::SemanticsNodeUpdates nodes;
flutter::SemanticsNode parent;
parent.id = 0;
parent.rect = SkRect::MakeXYWH(0, 0, 100, 200);
parent.label = "label";
parent.value = "value";
parent.hint = "hint";
flutter::SemanticsNode node;
node.id = 1;
node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling);
node.actions = flutter::kHorizontalScrollSemanticsActions;
node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
node.label = "label";
node.value = "value";
node.hint = "hint";
node.scrollExtentMax = 100.0;
node.scrollPosition = 0.0;
parent.childrenInTraversalOrder.push_back(1);
nodes[0] = parent;
nodes[1] = node;
flutter::CustomAccessibilityActionUpdates actions;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
XCTAssertTrue([flutterView.subviews count] == 1);
XCTAssertTrue([flutterView.subviews[0] isKindOfClass:[FlutterSemanticsScrollView class]]);
XCTAssertTrue([flutterView.subviews[0].accessibilityLabel isEqualToString:@"label"]);
// Remove the scrollable from the tree.
flutter::SemanticsNodeUpdates new_nodes;
flutter::SemanticsNode new_parent;
new_parent.id = 0;
new_parent.rect = SkRect::MakeXYWH(0, 0, 100, 200);
new_parent.label = "label";
new_parent.value = "value";
new_parent.hint = "hint";
new_nodes[0] = new_parent;
bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/actions);
}
XCTAssertTrue([flutterView.subviews count] == 0);
}
- (void)testBridgeReplacesSemanticsNode {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto flutterPlatformViewsController = std::make_shared<flutter::FlutterPlatformViewsController>();
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/flutterPlatformViewsController,
/*task_runners=*/runners);
id engine = OCMClassMock([FlutterEngine class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
FlutterView* flutterView = [[FlutterView alloc] initWithDelegate:engine opaque:YES];
OCMStub([mockFlutterViewController view]).andReturn(flutterView);
std::string label = "some label";
@autoreleasepool {
auto bridge = std::make_unique<flutter::AccessibilityBridge>(
/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/flutterPlatformViewsController);
flutter::SemanticsNodeUpdates nodes;
flutter::SemanticsNode parent;
parent.id = 0;
parent.rect = SkRect::MakeXYWH(0, 0, 100, 200);
parent.label = "label";
parent.value = "value";
parent.hint = "hint";
flutter::SemanticsNode node;
node.id = 1;
node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling);
node.actions = flutter::kHorizontalScrollSemanticsActions;
node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
node.label = "label";
node.value = "value";
node.hint = "hint";
node.scrollExtentMax = 100.0;
node.scrollPosition = 0.0;
parent.childrenInTraversalOrder.push_back(1);
nodes[0] = parent;
nodes[1] = node;
flutter::CustomAccessibilityActionUpdates actions;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
XCTAssertTrue([flutterView.subviews count] == 1);
XCTAssertTrue([flutterView.subviews[0] isKindOfClass:[FlutterSemanticsScrollView class]]);
XCTAssertTrue([flutterView.subviews[0].accessibilityLabel isEqualToString:@"label"]);
// Remove implicit scroll from node 1.
flutter::SemanticsNodeUpdates new_nodes;
flutter::SemanticsNode new_node;
new_node.id = 1;
new_node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
new_node.label = "label";
new_node.value = "value";
new_node.hint = "hint";
new_node.scrollExtentMax = 100.0;
new_node.scrollPosition = 0.0;
new_nodes[1] = new_node;
bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/actions);
}
XCTAssertTrue([flutterView.subviews count] == 0);
}
- (void)testAnnouncesRouteChanges {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
id mockFlutterView = OCMClassMock([FlutterView class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
[[[NSMutableArray alloc] init] autorelease];
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
ios_delegate->on_PostAccessibilityNotification_ =
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
[accessibility_notifications addObject:@{
@"notification" : @(notification),
@"argument" : argument ? argument : [NSNull null],
}];
};
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
flutter::CustomAccessibilityActionUpdates actions;
flutter::SemanticsNodeUpdates nodes;
flutter::SemanticsNode node1;
node1.id = 1;
node1.label = "node1";
node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
node1.childrenInTraversalOrder = {2, 3};
node1.childrenInHitTestOrder = {2, 3};
nodes[node1.id] = node1;
flutter::SemanticsNode node2;
node2.id = 2;
node2.label = "node2";
nodes[node2.id] = node2;
flutter::SemanticsNode node3;
node3.id = 3;
node3.flags = static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
node3.label = "node3";
nodes[node3.id] = node3;
flutter::SemanticsNode root_node;
root_node.id = kRootNodeId;
root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
root_node.childrenInTraversalOrder = {1};
root_node.childrenInHitTestOrder = {1};
nodes[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
XCTAssertEqual([accessibility_notifications count], 1ul);
XCTAssertEqualObjects(accessibility_notifications[0][@"argument"], @"node3");
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
UIAccessibilityScreenChangedNotification);
}
- (void)testLayoutChangeWithNonAccessibilityElement {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
id mockFlutterView = OCMClassMock([FlutterView class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
[[[NSMutableArray alloc] init] autorelease];
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
ios_delegate->on_PostAccessibilityNotification_ =
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
[accessibility_notifications addObject:@{
@"notification" : @(notification),
@"argument" : argument ? argument : [NSNull null],
}];
};
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
flutter::CustomAccessibilityActionUpdates actions;
flutter::SemanticsNodeUpdates nodes;
flutter::SemanticsNode node1;
node1.id = 1;
node1.label = "node1";
node1.childrenInTraversalOrder = {2, 3};
node1.childrenInHitTestOrder = {2, 3};
nodes[node1.id] = node1;
flutter::SemanticsNode node2;
node2.id = 2;
node2.label = "node2";
nodes[node2.id] = node2;
flutter::SemanticsNode node3;
node3.id = 3;
node3.label = "node3";
nodes[node3.id] = node3;
flutter::SemanticsNode root_node;
root_node.id = kRootNodeId;
root_node.label = "root";
root_node.childrenInTraversalOrder = {1};
root_node.childrenInHitTestOrder = {1};
nodes[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
// Simulates the focusing on the node 1.
bridge->AccessibilityObjectDidBecomeFocused(1);
// In this update, we make node 1 unfocusable and trigger the
// layout change. The accessibility bridge should send layoutchange
// notification with the first focusable node under node 1
flutter::CustomAccessibilityActionUpdates new_actions;
flutter::SemanticsNodeUpdates new_nodes;
flutter::SemanticsNode new_node1;
new_node1.id = 1;
new_node1.childrenInTraversalOrder = {2};
new_node1.childrenInHitTestOrder = {2};
new_nodes[new_node1.id] = new_node1;
bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/new_actions);
XCTAssertEqual([accessibility_notifications count], 1ul);
SemanticsObject* focusObject = accessibility_notifications[0][@"argument"];
// Since node 1 is no longer focusable (no label), it will focus node 2 instead.
XCTAssertEqual([focusObject uid], 2);
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
UIAccessibilityLayoutChangedNotification);
}
- (void)testLayoutChangeDoesCallNativeAccessibility {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
id mockFlutterView = OCMClassMock([FlutterView class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
[[[NSMutableArray alloc] init] autorelease];
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
ios_delegate->on_PostAccessibilityNotification_ =
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
[accessibility_notifications addObject:@{
@"notification" : @(notification),
@"argument" : argument ? argument : [NSNull null],
}];
};
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
flutter::CustomAccessibilityActionUpdates actions;
flutter::SemanticsNodeUpdates nodes;
flutter::SemanticsNode node1;
node1.id = 1;
node1.label = "node1";
nodes[node1.id] = node1;
flutter::SemanticsNode root_node;
root_node.id = kRootNodeId;
root_node.label = "root";
root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling);
root_node.childrenInTraversalOrder = {1};
root_node.childrenInHitTestOrder = {1};
nodes[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
// Simulates the focusing on the node 0.
bridge->AccessibilityObjectDidBecomeFocused(0);
// Remove node 1 to trigger a layout change notification
flutter::CustomAccessibilityActionUpdates new_actions;
flutter::SemanticsNodeUpdates new_nodes;
flutter::SemanticsNode new_root_node;
new_root_node.id = kRootNodeId;
new_root_node.label = "root";
new_root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling);
new_nodes[new_root_node.id] = new_root_node;
bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/new_actions);
XCTAssertEqual([accessibility_notifications count], 1ul);
SemanticsObject* focusObject = accessibility_notifications[0][@"argument"];
// Make sure refocus event is sent with the nativeAccessibility of root node
// which is a FlutterSemanticsScrollView.
XCTAssertTrue([focusObject isKindOfClass:[FlutterSemanticsScrollView class]]);
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
UIAccessibilityLayoutChangedNotification);
}
- (void)testScrollableSemanticsContainerReturnsCorrectChildren {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
id mockFlutterView = OCMClassMock([FlutterView class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
OCMExpect([mockFlutterView
setAccessibilityElements:[OCMArg checkWithBlock:^BOOL(NSArray* value) {
if ([value count] != 1) {
return NO;
}
SemanticsObjectContainer* container = value[0];
SemanticsObject* object = container.semanticsObject;
FlutterScrollableSemanticsObject* scrollable =
(FlutterScrollableSemanticsObject*)object.children[0];
id nativeScrollable = scrollable.nativeAccessibility;
SemanticsObjectContainer* scrollableContainer = [nativeScrollable accessibilityContainer];
return [scrollableContainer indexOfAccessibilityElement:nativeScrollable] == 1;
}]]);
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
flutter::CustomAccessibilityActionUpdates actions;
flutter::SemanticsNodeUpdates nodes;
flutter::SemanticsNode node1;
node1.id = 1;
node1.label = "node1";
node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling);
nodes[node1.id] = node1;
flutter::SemanticsNode root_node;
root_node.id = kRootNodeId;
root_node.label = "root";
root_node.childrenInTraversalOrder = {1};
root_node.childrenInHitTestOrder = {1};
nodes[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
OCMVerifyAll(mockFlutterView);
}
- (void)testAnnouncesRouteChangesAndLayoutChangeInOneUpdate {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
id mockFlutterView = OCMClassMock([FlutterView class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
[[[NSMutableArray alloc] init] autorelease];
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
ios_delegate->on_PostAccessibilityNotification_ =
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
[accessibility_notifications addObject:@{
@"notification" : @(notification),
@"argument" : argument ? argument : [NSNull null],
}];
};
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
flutter::CustomAccessibilityActionUpdates actions;
flutter::SemanticsNodeUpdates nodes;
flutter::SemanticsNode node1;
node1.id = 1;
node1.label = "node1";
node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
nodes[node1.id] = node1;
flutter::SemanticsNode node3;
node3.id = 3;
node3.label = "node3";
nodes[node3.id] = node3;
flutter::SemanticsNode root_node;
root_node.id = kRootNodeId;
root_node.label = "root";
root_node.childrenInTraversalOrder = {1, 3};
root_node.childrenInHitTestOrder = {1, 3};
nodes[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
XCTAssertEqual([accessibility_notifications count], 1ul);
XCTAssertEqualObjects(accessibility_notifications[0][@"argument"], @"node1");
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
UIAccessibilityScreenChangedNotification);
// Simulates the focusing on the node 0.
bridge->AccessibilityObjectDidBecomeFocused(0);
flutter::SemanticsNodeUpdates new_nodes;
flutter::SemanticsNode new_node1;
new_node1.id = 1;
new_node1.label = "new_node1";
new_node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
new_node1.childrenInTraversalOrder = {2};
new_node1.childrenInHitTestOrder = {2};
new_nodes[new_node1.id] = new_node1;
flutter::SemanticsNode new_node2;
new_node2.id = 2;
new_node2.label = "new_node2";
new_node2.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
new_nodes[new_node2.id] = new_node2;
flutter::SemanticsNode new_root_node;
new_root_node.id = kRootNodeId;
new_root_node.label = "root";
new_root_node.childrenInTraversalOrder = {1};
new_root_node.childrenInHitTestOrder = {1};
new_nodes[new_root_node.id] = new_root_node;
bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/actions);
XCTAssertEqual([accessibility_notifications count], 3ul);
XCTAssertEqualObjects(accessibility_notifications[1][@"argument"], @"new_node2");
XCTAssertEqual([accessibility_notifications[1][@"notification"] unsignedIntValue],
UIAccessibilityScreenChangedNotification);
SemanticsObject* focusObject = accessibility_notifications[2][@"argument"];
// It should still focus the root.
XCTAssertEqual([focusObject uid], 0);
XCTAssertEqual([accessibility_notifications[2][@"notification"] unsignedIntValue],
UIAccessibilityLayoutChangedNotification);
}
- (void)testAnnouncesRouteChangesWhenAddAdditionalRoute {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
id mockFlutterView = OCMClassMock([FlutterView class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
[[[NSMutableArray alloc] init] autorelease];
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
ios_delegate->on_PostAccessibilityNotification_ =
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
[accessibility_notifications addObject:@{
@"notification" : @(notification),
@"argument" : argument ? argument : [NSNull null],
}];
};
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
flutter::CustomAccessibilityActionUpdates actions;
flutter::SemanticsNodeUpdates nodes;
flutter::SemanticsNode node1;
node1.id = 1;
node1.label = "node1";
node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
nodes[node1.id] = node1;
flutter::SemanticsNode root_node;
root_node.id = kRootNodeId;
root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
root_node.childrenInTraversalOrder = {1};
root_node.childrenInHitTestOrder = {1};
nodes[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
XCTAssertEqual([accessibility_notifications count], 1ul);
XCTAssertEqualObjects(accessibility_notifications[0][@"argument"], @"node1");
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
UIAccessibilityScreenChangedNotification);
flutter::SemanticsNodeUpdates new_nodes;
flutter::SemanticsNode new_node1;
new_node1.id = 1;
new_node1.label = "new_node1";
new_node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
new_node1.childrenInTraversalOrder = {2};
new_node1.childrenInHitTestOrder = {2};
new_nodes[new_node1.id] = new_node1;
flutter::SemanticsNode new_node2;
new_node2.id = 2;
new_node2.label = "new_node2";
new_node2.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
new_nodes[new_node2.id] = new_node2;
flutter::SemanticsNode new_root_node;
new_root_node.id = kRootNodeId;
new_root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
new_root_node.childrenInTraversalOrder = {1};
new_root_node.childrenInHitTestOrder = {1};
new_nodes[new_root_node.id] = new_root_node;
bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/actions);
XCTAssertEqual([accessibility_notifications count], 2ul);
XCTAssertEqualObjects(accessibility_notifications[1][@"argument"], @"new_node2");
XCTAssertEqual([accessibility_notifications[1][@"notification"] unsignedIntValue],
UIAccessibilityScreenChangedNotification);
}
- (void)testAnnouncesRouteChangesRemoveRouteInMiddle {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
id mockFlutterView = OCMClassMock([FlutterView class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
[[[NSMutableArray alloc] init] autorelease];
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
ios_delegate->on_PostAccessibilityNotification_ =
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
[accessibility_notifications addObject:@{
@"notification" : @(notification),
@"argument" : argument ? argument : [NSNull null],
}];
};
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
flutter::CustomAccessibilityActionUpdates actions;
flutter::SemanticsNodeUpdates nodes;
flutter::SemanticsNode node1;
node1.id = 1;
node1.label = "node1";
node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
node1.childrenInTraversalOrder = {2};
node1.childrenInHitTestOrder = {2};
nodes[node1.id] = node1;
flutter::SemanticsNode node2;
node2.id = 2;
node2.label = "node2";
node2.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
nodes[node2.id] = node2;
flutter::SemanticsNode root_node;
root_node.id = kRootNodeId;
root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
root_node.childrenInTraversalOrder = {1};
root_node.childrenInHitTestOrder = {1};
nodes[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
XCTAssertEqual([accessibility_notifications count], 1ul);
XCTAssertEqualObjects(accessibility_notifications[0][@"argument"], @"node2");
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
UIAccessibilityScreenChangedNotification);
flutter::SemanticsNodeUpdates new_nodes;
flutter::SemanticsNode new_node1;
new_node1.id = 1;
new_node1.label = "new_node1";
new_node1.childrenInTraversalOrder = {2};
new_node1.childrenInHitTestOrder = {2};
new_nodes[new_node1.id] = new_node1;
flutter::SemanticsNode new_node2;
new_node2.id = 2;
new_node2.label = "new_node2";
new_node2.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
new_nodes[new_node2.id] = new_node2;
flutter::SemanticsNode new_root_node;
new_root_node.id = kRootNodeId;
new_root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
new_root_node.childrenInTraversalOrder = {1};
new_root_node.childrenInHitTestOrder = {1};
new_nodes[new_root_node.id] = new_root_node;
bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/actions);
XCTAssertEqual([accessibility_notifications count], 2ul);
XCTAssertEqualObjects(accessibility_notifications[1][@"argument"], @"new_node2");
XCTAssertEqual([accessibility_notifications[1][@"notification"] unsignedIntValue],
UIAccessibilityScreenChangedNotification);
}
- (void)testAnnouncesRouteChangesWhenNoNamesRoute {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
id mockFlutterView = OCMClassMock([FlutterView class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
[[[NSMutableArray alloc] init] autorelease];
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
ios_delegate->on_PostAccessibilityNotification_ =
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
[accessibility_notifications addObject:@{
@"notification" : @(notification),
@"argument" : argument ? argument : [NSNull null],
}];
};
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
flutter::CustomAccessibilityActionUpdates actions;
flutter::SemanticsNodeUpdates nodes;
flutter::SemanticsNode node1;
node1.id = 1;
node1.label = "node1";
node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
node1.childrenInTraversalOrder = {2, 3};
node1.childrenInHitTestOrder = {2, 3};
nodes[node1.id] = node1;
flutter::SemanticsNode node2;
node2.id = 2;
node2.label = "node2";
nodes[node2.id] = node2;
flutter::SemanticsNode node3;
node3.id = 3;
node3.label = "node3";
nodes[node3.id] = node3;
flutter::SemanticsNode root_node;
root_node.id = kRootNodeId;
root_node.childrenInTraversalOrder = {1};
root_node.childrenInHitTestOrder = {1};
nodes[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
// Notification should focus first focusable node, which is node1.
XCTAssertEqual([accessibility_notifications count], 1ul);
id focusObject = accessibility_notifications[0][@"argument"];
XCTAssertTrue([focusObject isKindOfClass:[NSString class]]);
XCTAssertEqualObjects(focusObject, @"node1");
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
UIAccessibilityScreenChangedNotification);
}
- (void)testAnnouncesLayoutChangeWithNilIfLastFocusIsRemoved {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
id mockFlutterView = OCMClassMock([FlutterView class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
[[[NSMutableArray alloc] init] autorelease];
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
ios_delegate->on_PostAccessibilityNotification_ =
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
[accessibility_notifications addObject:@{
@"notification" : @(notification),
@"argument" : argument ? argument : [NSNull null],
}];
};
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
flutter::CustomAccessibilityActionUpdates actions;
flutter::SemanticsNodeUpdates first_update;
flutter::SemanticsNode route_node;
route_node.id = 1;
route_node.label = "route";
first_update[route_node.id] = route_node;
flutter::SemanticsNode root_node;
root_node.id = kRootNodeId;
root_node.label = "root";
root_node.childrenInTraversalOrder = {1};
root_node.childrenInHitTestOrder = {1};
first_update[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/first_update, /*actions=*/actions);
XCTAssertEqual([accessibility_notifications count], 0ul);
// Simulates the focusing on the node 1.
bridge->AccessibilityObjectDidBecomeFocused(1);
flutter::SemanticsNodeUpdates second_update;
// Simulates the removal of the node 1
flutter::SemanticsNode new_root_node;
new_root_node.id = kRootNodeId;
new_root_node.label = "root";
second_update[root_node.id] = new_root_node;
bridge->UpdateSemantics(/*nodes=*/second_update, /*actions=*/actions);
SemanticsObject* focusObject = accessibility_notifications[0][@"argument"];
// The node 1 was removed, so the bridge will set the focus object to root.
XCTAssertEqual([focusObject uid], 0);
XCTAssertEqualObjects([focusObject accessibilityLabel], @"root");
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
UIAccessibilityLayoutChangedNotification);
}
- (void)testAnnouncesLayoutChangeWithLastFocused {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
id mockFlutterView = OCMClassMock([FlutterView class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
[[[NSMutableArray alloc] init] autorelease];
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
ios_delegate->on_PostAccessibilityNotification_ =
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
[accessibility_notifications addObject:@{
@"notification" : @(notification),
@"argument" : argument ? argument : [NSNull null],
}];
};
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
flutter::CustomAccessibilityActionUpdates actions;
flutter::SemanticsNodeUpdates first_update;
flutter::SemanticsNode node_one;
node_one.id = 1;
node_one.label = "route1";
first_update[node_one.id] = node_one;
flutter::SemanticsNode node_two;
node_two.id = 2;
node_two.label = "route2";
first_update[node_two.id] = node_two;
flutter::SemanticsNode root_node;
root_node.id = kRootNodeId;
root_node.label = "root";
root_node.childrenInTraversalOrder = {1, 2};
root_node.childrenInHitTestOrder = {1, 2};
first_update[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/first_update, /*actions=*/actions);
XCTAssertEqual([accessibility_notifications count], 0ul);
// Simulates the focusing on the node 1.
bridge->AccessibilityObjectDidBecomeFocused(1);
flutter::SemanticsNodeUpdates second_update;
// Simulates the removal of the node 2.
flutter::SemanticsNode new_root_node;
new_root_node.id = kRootNodeId;
new_root_node.label = "root";
new_root_node.childrenInTraversalOrder = {1};
new_root_node.childrenInHitTestOrder = {1};
second_update[root_node.id] = new_root_node;
bridge->UpdateSemantics(/*nodes=*/second_update, /*actions=*/actions);
SemanticsObject* focusObject = accessibility_notifications[0][@"argument"];
// Since we have focused on the node 1 right before the layout changed, the bridge should refocus
// the node 1.
XCTAssertEqual([focusObject uid], 1);
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
UIAccessibilityLayoutChangedNotification);
}
- (void)testAnnouncesLayoutChangeWhenFocusMovedOutside {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
id mockFlutterView = OCMClassMock([FlutterView class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
[[[NSMutableArray alloc] init] autorelease];
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
ios_delegate->on_PostAccessibilityNotification_ =
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
[accessibility_notifications addObject:@{
@"notification" : @(notification),
@"argument" : argument ? argument : [NSNull null],
}];
};
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
flutter::CustomAccessibilityActionUpdates actions;
flutter::SemanticsNodeUpdates first_update;
flutter::SemanticsNode node_one;
node_one.id = 1;
node_one.label = "route1";
first_update[node_one.id] = node_one;
flutter::SemanticsNode node_two;
node_two.id = 2;
node_two.label = "route2";
first_update[node_two.id] = node_two;
flutter::SemanticsNode root_node;
root_node.id = kRootNodeId;
root_node.label = "root";
root_node.childrenInTraversalOrder = {1, 2};
root_node.childrenInHitTestOrder = {1, 2};
first_update[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/first_update, /*actions=*/actions);
XCTAssertEqual([accessibility_notifications count], 0ul);
// Simulates the focusing on the node 1.
bridge->AccessibilityObjectDidBecomeFocused(1);
// Simulates that the focus move outside of flutter.
bridge->AccessibilityObjectDidLoseFocus(1);
flutter::SemanticsNodeUpdates second_update;
// Simulates the removal of the node 2.
flutter::SemanticsNode new_root_node;
new_root_node.id = kRootNodeId;
new_root_node.label = "root";
new_root_node.childrenInTraversalOrder = {1};
new_root_node.childrenInHitTestOrder = {1};
second_update[root_node.id] = new_root_node;
bridge->UpdateSemantics(/*nodes=*/second_update, /*actions=*/actions);
NSNull* focusObject = accessibility_notifications[0][@"argument"];
// Since the focus is moved outside of the app right before the layout
// changed, the bridge should not try to refocus anything .
XCTAssertEqual(focusObject, [NSNull null]);
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
UIAccessibilityLayoutChangedNotification);
}
- (void)testAnnouncesScrollChangeWithLastFocused {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
id mockFlutterView = OCMClassMock([FlutterView class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
[[[NSMutableArray alloc] init] autorelease];
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
ios_delegate->on_PostAccessibilityNotification_ =
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
[accessibility_notifications addObject:@{
@"notification" : @(notification),
@"argument" : argument ? argument : [NSNull null],
}];
};
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
flutter::CustomAccessibilityActionUpdates actions;
flutter::SemanticsNodeUpdates first_update;
flutter::SemanticsNode node_one;
node_one.id = 1;
node_one.label = "route1";
node_one.scrollPosition = 0.0;
first_update[node_one.id] = node_one;
flutter::SemanticsNode root_node;
root_node.id = kRootNodeId;
root_node.label = "root";
root_node.childrenInTraversalOrder = {1};
root_node.childrenInHitTestOrder = {1};
first_update[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/first_update, /*actions=*/actions);
// The first update will trigger a scroll announcement, but we are not interested in it.
[accessibility_notifications removeAllObjects];
// Simulates the focusing on the node 1.
bridge->AccessibilityObjectDidBecomeFocused(1);
flutter::SemanticsNodeUpdates second_update;
// Simulates the scrolling on the node 1.
flutter::SemanticsNode new_node_one;
new_node_one.id = 1;
new_node_one.label = "route1";
new_node_one.scrollPosition = 1.0;
second_update[new_node_one.id] = new_node_one;
bridge->UpdateSemantics(/*nodes=*/second_update, /*actions=*/actions);
SemanticsObject* focusObject = accessibility_notifications[0][@"argument"];
// Since we have focused on the node 1 right before the scrolling, the bridge should refocus the
// node 1.
XCTAssertEqual([focusObject uid], 1);
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
UIAccessibilityPageScrolledNotification);
}
- (void)testAnnouncesScrollChangeDoesCallNativeAccessibility {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
id mockFlutterView = OCMClassMock([FlutterView class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
[[[NSMutableArray alloc] init] autorelease];
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
ios_delegate->on_PostAccessibilityNotification_ =
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
[accessibility_notifications addObject:@{
@"notification" : @(notification),
@"argument" : argument ? argument : [NSNull null],
}];
};
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
flutter::CustomAccessibilityActionUpdates actions;
flutter::SemanticsNodeUpdates first_update;
flutter::SemanticsNode node_one;
node_one.id = 1;
node_one.label = "route1";
node_one.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling);
node_one.scrollPosition = 0.0;
first_update[node_one.id] = node_one;
flutter::SemanticsNode root_node;
root_node.id = kRootNodeId;
root_node.label = "root";
root_node.childrenInTraversalOrder = {1};
root_node.childrenInHitTestOrder = {1};
first_update[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/first_update, /*actions=*/actions);
// The first update will trigger a scroll announcement, but we are not interested in it.
[accessibility_notifications removeAllObjects];
// Simulates the focusing on the node 1.
bridge->AccessibilityObjectDidBecomeFocused(1);
flutter::SemanticsNodeUpdates second_update;
// Simulates the scrolling on the node 1.
flutter::SemanticsNode new_node_one;
new_node_one.id = 1;
new_node_one.label = "route1";
new_node_one.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling);
new_node_one.scrollPosition = 1.0;
second_update[new_node_one.id] = new_node_one;
bridge->UpdateSemantics(/*nodes=*/second_update, /*actions=*/actions);
SemanticsObject* focusObject = accessibility_notifications[0][@"argument"];
// Make sure refocus event is sent with the nativeAccessibility of node_one
// which is a FlutterSemanticsScrollView.
XCTAssertTrue([focusObject isKindOfClass:[FlutterSemanticsScrollView class]]);
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
UIAccessibilityPageScrolledNotification);
}
- (void)testAnnouncesIgnoresRouteChangesWhenModal {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
id mockFlutterView = OCMClassMock([FlutterView class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
std::string label = "some label";
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
[[[NSMutableArray alloc] init] autorelease];
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
ios_delegate->on_PostAccessibilityNotification_ =
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
[accessibility_notifications addObject:@{
@"notification" : @(notification),
@"argument" : argument ? argument : [NSNull null],
}];
};
ios_delegate->result_IsFlutterViewControllerPresentingModalViewController_ = true;
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
flutter::CustomAccessibilityActionUpdates actions;
flutter::SemanticsNodeUpdates nodes;
flutter::SemanticsNode route_node;
route_node.id = 1;
route_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
route_node.label = "route";
nodes[route_node.id] = route_node;
flutter::SemanticsNode root_node;
root_node.id = kRootNodeId;
root_node.label = label;
root_node.childrenInTraversalOrder = {1};
root_node.childrenInHitTestOrder = {1};
nodes[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
XCTAssertEqual([accessibility_notifications count], 0ul);
}
- (void)testAnnouncesIgnoresLayoutChangeWhenModal {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
id mockFlutterView = OCMClassMock([FlutterView class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
[[[NSMutableArray alloc] init] autorelease];
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
ios_delegate->on_PostAccessibilityNotification_ =
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
[accessibility_notifications addObject:@{
@"notification" : @(notification),
@"argument" : argument ? argument : [NSNull null],
}];
};
ios_delegate->result_IsFlutterViewControllerPresentingModalViewController_ = true;
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
flutter::CustomAccessibilityActionUpdates actions;
flutter::SemanticsNodeUpdates nodes;
flutter::SemanticsNode child_node;
child_node.id = 1;
child_node.label = "child_node";
nodes[child_node.id] = child_node;
flutter::SemanticsNode root_node;
root_node.id = kRootNodeId;
root_node.label = "root";
root_node.childrenInTraversalOrder = {1};
root_node.childrenInHitTestOrder = {1};
nodes[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
// Removes child_node to simulate a layout change.
flutter::SemanticsNodeUpdates new_nodes;
flutter::SemanticsNode new_root_node;
new_root_node.id = kRootNodeId;
new_root_node.label = "root";
new_nodes[new_root_node.id] = new_root_node;
bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/actions);
XCTAssertEqual([accessibility_notifications count], 0ul);
}
- (void)testAnnouncesIgnoresScrollChangeWhenModal {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
id mockFlutterView = OCMClassMock([FlutterView class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
[[[NSMutableArray alloc] init] autorelease];
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
ios_delegate->on_PostAccessibilityNotification_ =
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
[accessibility_notifications addObject:@{
@"notification" : @(notification),
@"argument" : argument ? argument : [NSNull null],
}];
};
ios_delegate->result_IsFlutterViewControllerPresentingModalViewController_ = true;
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
flutter::CustomAccessibilityActionUpdates actions;
flutter::SemanticsNodeUpdates nodes;
flutter::SemanticsNode root_node;
root_node.id = kRootNodeId;
root_node.label = "root";
root_node.scrollPosition = 1;
nodes[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
// Removes child_node to simulate a layout change.
flutter::SemanticsNodeUpdates new_nodes;
flutter::SemanticsNode new_root_node;
new_root_node.id = kRootNodeId;
new_root_node.label = "root";
new_root_node.scrollPosition = 2;
new_nodes[new_root_node.id] = new_root_node;
bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/actions);
XCTAssertEqual([accessibility_notifications count], 0ul);
}
- (void)testAccessibilityMessageAfterDeletion {
flutter::MockDelegate mock_delegate;
auto thread = std::make_unique<fml::Thread>("AccessibilityBridgeTest");
auto thread_task_runner = thread->GetTaskRunner();
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
id messenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
id engine = OCMClassMock([FlutterEngine class]);
id flutterViewController = OCMClassMock([FlutterViewController class]);
OCMStub([flutterViewController engine]).andReturn(engine);
OCMStub([engine binaryMessenger]).andReturn(messenger);
FlutterBinaryMessengerConnection connection = 123;
OCMStub([messenger setMessageHandlerOnChannel:@"flutter/accessibility"
binaryMessageHandler:[OCMArg any]])
.andReturn(connection);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
fml::AutoResetWaitableEvent latch;
thread_task_runner->PostTask([&] {
auto weakFactory =
std::make_unique<fml::WeakPtrFactory<FlutterViewController>>(flutterViewController);
platform_view->SetOwnerViewController(weakFactory->GetWeakPtr());
auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view=*/nil,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil);
XCTAssertTrue(bridge.get());
OCMVerify([messenger setMessageHandlerOnChannel:@"flutter/accessibility"
binaryMessageHandler:[OCMArg isNotNil]]);
bridge.reset();
latch.Signal();
});
latch.Wait();
OCMVerify([messenger cleanUpConnection:connection]);
[engine stopMocking];
}
- (void)testFlutterSemanticsScrollViewManagedObjectLifecycleCorrectly {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
id mockFlutterView = OCMClassMock([FlutterView class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
FlutterSemanticsScrollView* flutterSemanticsScrollView;
@autoreleasepool {
FlutterScrollableSemanticsObject* semanticsObject =
[[[FlutterScrollableSemanticsObject alloc] initWithBridge:bridge->GetWeakPtr()
uid:1234] autorelease];
flutterSemanticsScrollView = semanticsObject.nativeAccessibility;
}
XCTAssertTrue(flutterSemanticsScrollView);
// If the _semanticsObject is not a weak pointer this (or any other method on
// flutterSemanticsScrollView) will cause an EXC_BAD_ACCESS.
XCTAssertFalse([flutterSemanticsScrollView isAccessibilityElement]);
}
- (void)testPlatformViewDestructorDoesNotCallSemanticsAPIs {
class TestDelegate : public flutter::MockDelegate {
public:
void OnPlatformViewSetSemanticsEnabled(bool enabled) override { set_semantics_enabled_calls++; }
int set_semantics_enabled_calls = 0;
};
TestDelegate test_delegate;
auto thread = std::make_unique<fml::Thread>("AccessibilityBridgeTest");
auto thread_task_runner = thread->GetTaskRunner();
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
fml::AutoResetWaitableEvent latch;
thread_task_runner->PostTask([&] {
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/test_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
auto flutterPlatformViewsController =
std::make_shared<flutter::FlutterPlatformViewsController>();
OCMStub([mockFlutterViewController platformViewsController])
.andReturn(flutterPlatformViewsController.get());
auto weakFactory =
std::make_unique<fml::WeakPtrFactory<FlutterViewController>>(mockFlutterViewController);
platform_view->SetOwnerViewController(weakFactory->GetWeakPtr());
platform_view->SetSemanticsEnabled(true);
XCTAssertNotEqual(test_delegate.set_semantics_enabled_calls, 0);
// Deleting PlatformViewIOS should not call OnPlatformViewSetSemanticsEnabled
test_delegate.set_semantics_enabled_calls = 0;
platform_view.reset();
XCTAssertEqual(test_delegate.set_semantics_enabled_calls, 0);
latch.Signal();
});
latch.Wait();
}
@end