Added a unit test for the iOS AccessibilityBridge. (#18281)
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index cd0ba04..c891c35 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/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 f2eead7..a71cd79 100644
--- a/shell/platform/darwin/ios/BUILD.gn
+++ b/shell/platform/darwin/ios/BUILD.gn
@@ -152,6 +152,28 @@
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 @@
]
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 ba7c468..ba8591e 100644
--- a/shell/platform/darwin/ios/framework/Source/SemanticsObject.h
+++ b/shell/platform/darwin/ios/framework/Source/SemanticsObject.h
@@ -155,4 +155,50 @@
- (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<flutter::AccessibilityBridgeIos>)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 9f8f6b6..229e398 100644
--- a/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm
+++ b/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm
@@ -103,50 +103,6 @@
}
@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<flutter::AccessibilityBridgeIos>)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 0000000..6f17747
--- /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 <XCTest/XCTest.h>
+
+#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> surface) override {}
+ void OnPlatformViewDestroyed() override {}
+ void OnPlatformViewSetNextFrameCallback(const fml::closure& closure) override {}
+ void OnPlatformViewSetViewportMetrics(const ViewportMetrics& metrics) override {}
+ void OnPlatformViewDispatchPlatformMessage(fml::RefPtr<PlatformMessage> message) override {}
+ void OnPlatformViewDispatchPointerDataPacket(std::unique_ptr<PointerDataPacket> packet) override {
+ }
+ void OnPlatformViewDispatchSemanticsAction(int32_t id,
+ SemanticsAction action,
+ std::vector<uint8_t> 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 {}
+};
+} // 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,
+ /*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,
+ /*task_runners=*/runners);
+ id mockFlutterView = OCMClassMock([FlutterView class]);
+ OCMExpect([mockFlutterView setAccessibilityElements:[OCMArg isNil]]);
+ auto bridge =
+ std::make_unique<flutter::AccessibilityBridge>(/*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<flutter::PlatformViewIOS>(
+ /*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<flutter::AccessibilityBridge>(/*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