diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index cd0ba041fa759cb49dfaee3dfd17a96c2dd80958..c891c35394c5772b5252fadfdba7d68e11da58f7 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -903,6 +903,7 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObjec FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/platform_message_response_darwin.h diff --git a/shell/platform/darwin/ios/BUILD.gn b/shell/platform/darwin/ios/BUILD.gn index f2eead712db4cc0b070380d12ac7ed7eaff7033e..a71cd79cc8ad01bf43f1659bce3c7dcc85215ddc 100644 --- a/shell/platform/darwin/ios/BUILD.gn +++ b/shell/platform/darwin/ios/BUILD.gn @@ -152,6 +152,28 @@ source_set("flutter_framework_source") { ios_test_flutter_path = rebase_path("$root_out_dir/libios_test_flutter.dylib") platform_frameworks_path = "$ios_sdk_path/../../Library/Frameworks/" +# For tests that rely on manual reference counting. +source_set("ios_test_flutter_mrc") { + visibility = [ ":*" ] + cflags = [ + "-fvisibility=default", + "-F$platform_frameworks_path", + "-mios-simulator-version-min=$ios_testing_deployment_target", + ] + sources = [ + "framework/Source/accessibility_bridge_test.mm", + ] + deps = [ + ":flutter_framework_source", + "//flutter/shell/platform/darwin/common:framework_shared", + "//flutter/third_party/tonic", + "//flutter/third_party/txt", + "//third_party/ocmock:ocmock", + "//third_party/rapidjson", + "//third_party/skia", + ] +} + # NOTE: This currently only supports simulator targets because of the install_name. # TODO(54504): Switch the install_name and make the test runner copy the dynamic # library into the testing bundle. @@ -182,8 +204,12 @@ shared_library("ios_test_flutter") { ] deps = [ ":flutter_framework_source", + ":ios_test_flutter_mrc", "//flutter/shell/platform/darwin/common:framework_shared", + "//flutter/third_party/tonic", + "//flutter/third_party/txt", "//third_party/ocmock:ocmock", + "//third_party/rapidjson", "//third_party/skia", ] public_configs = [ "//flutter:config" ] diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObject.h b/shell/platform/darwin/ios/framework/Source/SemanticsObject.h index ba7c4689be4639cb08f839ded9ab09197f3d3aba..ba8591efab62240047a10e28e770e82ffad23d5c 100644 --- a/shell/platform/darwin/ios/framework/Source/SemanticsObject.h +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObject.h @@ -155,4 +155,50 @@ constexpr int32_t kRootNodeId = 0; - (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject; @end +/** + * Represents a semantics object that has children and hence has to be presented to the OS as a + * UIAccessibilityContainer. + * + * The SemanticsObject class cannot implement the UIAccessibilityContainer protocol because an + * object that returns YES for isAccessibilityElement cannot also implement + * UIAccessibilityContainer. + * + * With the help of SemanticsObjectContainer, the hierarchy of semantic objects received from + * the framework, such as: + * + * SemanticsObject1 + * SemanticsObject2 + * SemanticsObject3 + * SemanticsObject4 + * + * is translated into the following hierarchy, which is understood by iOS: + * + * SemanticsObjectContainer1 + * SemanticsObject1 + * SemanticsObjectContainer2 + * SemanticsObject2 + * SemanticsObject3 + * SemanticsObject4 + * + * From Flutter's view of the world (the first tree seen above), we construct iOS's view of the + * world (second tree) as follows: We replace each SemanticsObjects that has children with a + * SemanticsObjectContainer, which has the original SemanticsObject and its children as children. + * + * SemanticsObjects have semantic information attached to them which is interpreted by + * VoiceOver (they return YES for isAccessibilityElement). The SemanticsObjectContainers are just + * there for structure and they don't provide any semantic information to VoiceOver (they return + * NO for isAccessibilityElement). + */ +@interface SemanticsObjectContainer : UIAccessibilityElement +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; +- (instancetype)initWithAccessibilityContainer:(id)container NS_UNAVAILABLE; +- (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject + bridge:(fml::WeakPtr)bridge + NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, weak) SemanticsObject* semanticsObject; + +@end + #endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_SEMANTICS_OBJECT_H_ diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm b/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm index 9f8f6b67aea41cd9f21a12ca2fcd4cfb3267c44f..229e3989a805fd0ec052ef470dc2e6d31b6df5a3 100644 --- a/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm @@ -103,50 +103,6 @@ flutter::SemanticsAction GetSemanticsActionForScrollDirection( } @end -/** - * Represents a semantics object that has children and hence has to be presented to the OS as a - * UIAccessibilityContainer. - * - * The SemanticsObject class cannot implement the UIAccessibilityContainer protocol because an - * object that returns YES for isAccessibilityElement cannot also implement - * UIAccessibilityContainer. - * - * With the help of SemanticsObjectContainer, the hierarchy of semantic objects received from - * the framework, such as: - * - * SemanticsObject1 - * SemanticsObject2 - * SemanticsObject3 - * SemanticsObject4 - * - * is translated into the following hierarchy, which is understood by iOS: - * - * SemanticsObjectContainer1 - * SemanticsObject1 - * SemanticsObjectContainer2 - * SemanticsObject2 - * SemanticsObject3 - * SemanticsObject4 - * - * From Flutter's view of the world (the first tree seen above), we construct iOS's view of the - * world (second tree) as follows: We replace each SemanticsObjects that has children with a - * SemanticsObjectContainer, which has the original SemanticsObject and its children as children. - * - * SemanticsObjects have semantic information attached to them which is interpreted by - * VoiceOver (they return YES for isAccessibilityElement). The SemanticsObjectContainers are just - * there for structure and they don't provide any semantic information to VoiceOver (they return - * NO for isAccessibilityElement). - */ -@interface SemanticsObjectContainer : UIAccessibilityElement -- (instancetype)init __attribute__((unavailable("Use initWithSemanticsObject instead"))); -- (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject - bridge:(fml::WeakPtr)bridge - NS_DESIGNATED_INITIALIZER; - -@property(nonatomic, weak) SemanticsObject* semanticsObject; - -@end - @interface SemanticsObject () /** Should only be called in conjunction with setting child/parent relationship. */ - (void)privateSetParent:(SemanticsObject*)parent; diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm new file mode 100644 index 0000000000000000000000000000000000000000..6f17747e4a310ca9ebf978bf39d38928453ee470 --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm @@ -0,0 +1,134 @@ +// 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 + +#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h" +#import "flutter/shell/platform/darwin/ios/platform_view_ios.h" +#import "third_party/ocmock/Source/OCMock/OCMock.h" + +FLUTTER_ASSERT_NOT_ARC + +namespace flutter { +namespace { +class MockDelegate : public PlatformView::Delegate { + void OnPlatformViewCreated(std::unique_ptr surface) override {} + void OnPlatformViewDestroyed() override {} + void OnPlatformViewSetNextFrameCallback(const fml::closure& closure) override {} + void OnPlatformViewSetViewportMetrics(const ViewportMetrics& metrics) override {} + void OnPlatformViewDispatchPlatformMessage(fml::RefPtr message) override {} + void OnPlatformViewDispatchPointerDataPacket(std::unique_ptr packet) override { + } + void OnPlatformViewDispatchSemanticsAction(int32_t id, + SemanticsAction action, + std::vector args) override {} + void OnPlatformViewSetSemanticsEnabled(bool enabled) override {} + void OnPlatformViewSetAccessibilityFeatures(int32_t flags) override {} + void OnPlatformViewRegisterTexture(std::shared_ptr texture) override {} + void OnPlatformViewUnregisterTexture(int64_t texture_id) override {} + void OnPlatformViewMarkTextureFrameAvailable(int64_t texture_id) override {} +}; +} // namespace +} // namespace flutter + +namespace { +fml::RefPtr CreateNewThread(std::string name) { + auto thread = std::make_unique(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( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + auto bridge = + std::make_unique(/*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( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + id mockFlutterView = OCMClassMock([FlutterView class]); + OCMExpect([mockFlutterView setAccessibilityElements:[OCMArg isNil]]); + auto bridge = + std::make_unique(/*view=*/mockFlutterView, + /*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( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + id mockFlutterView = OCMClassMock([FlutterView class]); + std::string label = "some label"; + + __block auto bridge = + std::make_unique(/*view=*/mockFlutterView, + /*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); +} + +@end