Index: ios/chrome/browser/ui/activity_services/activity_service_controller_unittest.mm |
diff --git a/ios/chrome/browser/ui/activity_services/activity_service_controller_unittest.mm b/ios/chrome/browser/ui/activity_services/activity_service_controller_unittest.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..2784e0578e218e0bfecb6e13eb828b69a89854ca |
--- /dev/null |
+++ b/ios/chrome/browser/ui/activity_services/activity_service_controller_unittest.mm |
@@ -0,0 +1,480 @@ |
+// Copyright 2014 The Chromium 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 "ios/chrome/browser/ui/activity_services/activity_service_controller.h" |
+ |
+#import <MobileCoreServices/MobileCoreServices.h> |
+ |
+#include "base/mac/scoped_nsobject.h" |
+#import "base/test/ios/wait_util.h" |
+#include "components/reading_list/core/reading_list_switches.h" |
+#import "ios/chrome/browser/ui/activity_services/activity_type_util.h" |
+#import "ios/chrome/browser/ui/activity_services/appex_constants.h" |
+#import "ios/chrome/browser/ui/activity_services/chrome_activity_item_source.h" |
+#import "ios/chrome/browser/ui/activity_services/print_activity.h" |
+#import "ios/chrome/browser/ui/activity_services/share_to_data.h" |
+#include "ios/web/public/test/test_web_thread_bundle.h" |
+#include "testing/gtest_mac.h" |
+#include "testing/platform_test.h" |
+#import "third_party/ocmock/OCMock/OCMock.h" |
+#import "third_party/ocmock/gtest_support.h" |
+ |
+@interface ActivityServiceController (CrVisibleForTesting) |
+- (NSArray*)activityItemsForData:(ShareToData*)data; |
+- (NSArray*)applicationActivitiesForData:(ShareToData*)data |
+ controller:(UIViewController*)controller; |
+- (BOOL)processItemsReturnedFromActivity:(NSString*)activityType |
+ status:(ShareTo::ShareResult)result |
+ items:(NSArray*)extensionItems; |
+// Setter function for mocking during testing |
+- (void)setShareToDelegateForTesting:(id<ShareToDelegate>)delegate; |
+@end |
+ |
+namespace { |
+ |
+class ActivityServiceControllerTest : public PlatformTest { |
+ protected: |
+ void SetUp() override { |
+ PlatformTest::SetUp(); |
+ parentController_.reset( |
+ [[UIViewController alloc] initWithNibName:nil bundle:nil]); |
+ [[UIApplication sharedApplication] keyWindow].rootViewController = |
+ parentController_; |
+ shareToDelegate_.reset( |
+ [[OCMockObject mockForProtocol:@protocol(ShareToDelegate)] retain]); |
+ shareData_.reset([[ShareToData alloc] |
+ initWithURL:GURL("https://chromium.org") |
+ title:@"" |
+ isOriginalTitle:YES |
+ isPagePrintable:YES]); |
+ } |
+ |
+ void TearDown() override { |
+ [[UIApplication sharedApplication] keyWindow].rootViewController = nil; |
+ PlatformTest::TearDown(); |
+ } |
+ |
+ id<ShareToDelegate> GetShareToDelegate() { |
+ return static_cast<id<ShareToDelegate>>(shareToDelegate_.get()); |
+ } |
+ |
+ CGRect AnchorRect() { |
+ // On iPad, UIPopovers must be anchored to rectangles that have a non zero |
+ // size. |
+ return CGRectMake(0, 0, 1, 1); |
+ } |
+ |
+ UIView* AnchorView() { |
+ // On iPad, UIPopovers must be anchored to non nil views. |
+ return [parentController_.get() view]; |
+ } |
+ |
+ BOOL ArrayContainsImageSource(NSArray* array) { |
+ for (NSObject* item in array) { |
+ if ([item class] == [UIActivityImageSource class]) { |
+ return YES; |
+ } |
+ } |
+ return NO; |
+ } |
+ |
+ // Search |array| for id<UIActivityItemSource> objects. Returns an array of |
+ // matching NSExtensionItem objects returned by calling them. |
+ NSArray* FindItemsForActivityType(NSArray* array, NSString* activityType) { |
+ id mockActivityViewController = |
+ [OCMockObject niceMockForClass:[UIActivityViewController class]]; |
+ NSMutableArray* result = [NSMutableArray array]; |
+ for (id item in array) { |
+ if ([item respondsToSelector:@selector(activityViewController: |
+ itemForActivityType:)]) { |
+ id resultItem = [item activityViewController:mockActivityViewController |
+ itemForActivityType:activityType]; |
+ if ([resultItem isKindOfClass:[NSExtensionItem class]]) |
+ [result addObject:resultItem]; |
+ } |
+ } |
+ return result; |
+ } |
+ |
+ // Searches |array| for objects of class |klass| and returns them in an |
+ // autoreleased array. |
+ NSArray* FindItemsOfClass(NSArray* array, Class klass) { |
+ NSMutableArray* result = [NSMutableArray array]; |
+ for (id item in array) { |
+ if ([item isKindOfClass:klass]) |
+ [result addObject:item]; |
+ } |
+ return result; |
+ } |
+ |
+ // Searches |array| for objects returning |inUTType| conforming objects. |
+ // Returns an autoreleased array of conforming objects. |
+ NSArray* FindItemsEqualsToUTType(NSArray* array, |
+ NSString* activityType, |
+ NSString* inUTType) { |
+ id mockActivityViewController = |
+ [OCMockObject niceMockForClass:[UIActivityViewController class]]; |
+ NSMutableArray* result = [NSMutableArray array]; |
+ for (id item in array) { |
+ if (![item conformsToProtocol:@protocol(UIActivityItemSource)]) |
+ continue; |
+ SEL dataTypeSelector = |
+ @selector(activityViewController:dataTypeIdentifierForActivityType:); |
+ if (![item respondsToSelector:dataTypeSelector]) |
+ continue; |
+ NSString* itemDataType = |
+ [item activityViewController:mockActivityViewController |
+ dataTypeIdentifierForActivityType:activityType]; |
+ if ([itemDataType isEqualToString:inUTType]) { |
+ [result addObject:item]; |
+ } |
+ } |
+ return result; |
+ } |
+ |
+ // Calls -processItemsReturnedFromActivity:status:items: with the provided |
+ // |extensionItem| and expects failure. |
+ void ProcessItemsReturnedFromActivityFailure(NSArray* extensionItems, |
+ BOOL expectedResetUI) { |
+ base::scoped_nsobject<ActivityServiceController> activityController( |
+ [[ActivityServiceController alloc] init]); |
+ |
+ // Sets up a Mock ShareToDelegate object to check that the ShareToDelegate |
+ // callback function is not called. |
+ OCMockObject* shareToDelegateMock = |
+ [OCMockObject mockForProtocol:@protocol(ShareToDelegate)]; |
+ __block bool blockCalled = false; |
+ void (^validationBlock)(NSInvocation*) = ^(NSInvocation* invocation) { |
+ blockCalled = true; |
+ }; |
+ // OCMock does not allow "any" specification for non-object parameters. |
+ // To implement something that accept any non-SHARE_SUCCESS parameter |
+ // to calling this method, all the non-success values have to be |
+ // enumerated. |
+ [[[shareToDelegateMock stub] andDo:validationBlock] |
+ passwordAppExDidFinish:ShareTo::ShareResult::SHARE_CANCEL |
+ username:OCMOCK_ANY |
+ password:OCMOCK_ANY |
+ successMessage:OCMOCK_ANY]; |
+ [[[shareToDelegateMock stub] andDo:validationBlock] |
+ passwordAppExDidFinish:ShareTo::ShareResult::SHARE_NETWORK_FAILURE |
+ username:OCMOCK_ANY |
+ password:OCMOCK_ANY |
+ successMessage:OCMOCK_ANY]; |
+ [[[shareToDelegateMock stub] andDo:validationBlock] |
+ passwordAppExDidFinish:ShareTo::ShareResult::SHARE_SIGN_IN_FAILURE |
+ username:OCMOCK_ANY |
+ password:OCMOCK_ANY |
+ successMessage:OCMOCK_ANY]; |
+ [[[shareToDelegateMock stub] andDo:validationBlock] |
+ passwordAppExDidFinish:ShareTo::ShareResult::SHARE_ERROR |
+ username:OCMOCK_ANY |
+ password:OCMOCK_ANY |
+ successMessage:OCMOCK_ANY]; |
+ [[[shareToDelegateMock stub] andDo:validationBlock] |
+ passwordAppExDidFinish:ShareTo::ShareResult::SHARE_UNKNOWN_RESULT |
+ username:OCMOCK_ANY |
+ password:OCMOCK_ANY |
+ successMessage:OCMOCK_ANY]; |
+ [activityController setShareToDelegateForTesting:(id)shareToDelegateMock]; |
+ |
+ // Sets up the returned item from a Password Management App Extension. |
+ NSString* activityType = activity_services::kAppExtensionLastPass; |
+ ShareTo::ShareResult result = ShareTo::ShareResult::SHARE_SUCCESS; |
+ BOOL resetUI = |
+ [activityController processItemsReturnedFromActivity:activityType |
+ status:result |
+ items:extensionItems]; |
+ ASSERT_EQ(expectedResetUI, resetUI); |
+ base::test::ios::WaitUntilCondition(^{ |
+ return blockCalled; |
+ }); |
+ EXPECT_OCMOCK_VERIFY(shareToDelegateMock); |
+ } |
+ |
+ web::TestWebThreadBundle thread_bundle_; |
+ base::scoped_nsobject<UIViewController> parentController_; |
+ base::scoped_nsobject<OCMockObject> shareToDelegate_; |
+ base::scoped_nsobject<ShareToData> shareData_; |
+}; |
+ |
+TEST_F(ActivityServiceControllerTest, PresentAndDismissController) { |
+ [[shareToDelegate_ expect] shareDidComplete:ShareTo::ShareResult::SHARE_CANCEL |
+ successMessage:[OCMArg isNil]]; |
+ |
+ UIViewController* parentController = |
+ static_cast<UIViewController*>(parentController_.get()); |
+ base::scoped_nsobject<ActivityServiceController> activityController( |
+ [[ActivityServiceController alloc] init]); |
+ EXPECT_FALSE([activityController isActive]); |
+ |
+ // Test sharing. |
+ [activityController shareWithData:shareData_ |
+ controller:parentController |
+ browserState:nullptr |
+ shareToDelegate:GetShareToDelegate() |
+ fromRect:AnchorRect() |
+ inView:AnchorView()]; |
+ EXPECT_TRUE([activityController isActive]); |
+ |
+ // Cancels sharing and isActive flag should be turned off. |
+ [activityController cancelShareAnimated:NO]; |
+ base::test::ios::WaitUntilCondition(^bool() { |
+ return ![activityController isActive]; |
+ }); |
+ EXPECT_OCMOCK_VERIFY(shareToDelegate_); |
+} |
+ |
+// Verifies that an UIActivityImageSource is sent to the |
+// UIActivityViewController if and only if the ShareToData contains an image. |
+TEST_F(ActivityServiceControllerTest, ActivityItemsForData) { |
+ base::scoped_nsobject<ActivityServiceController> activityController( |
+ [[ActivityServiceController alloc] init]); |
+ |
+ // ShareToData does not contain an image, so the result items array will not |
+ // contain an image source. |
+ base::scoped_nsobject<ShareToData> data([[ShareToData alloc] |
+ initWithURL:GURL("https://chromium.org") |
+ title:@"foo" |
+ isOriginalTitle:YES |
+ isPagePrintable:YES]); |
+ NSArray* items = [activityController activityItemsForData:data]; |
+ EXPECT_FALSE(ArrayContainsImageSource(items)); |
+ |
+ // Adds an image to the ShareToData object and call -activityItemsForData: |
+ // again. Verifies that the result items array contains an image source. |
+ [data setImage:[UIImage imageNamed:@"activity_services_print"]]; |
+ items = [activityController activityItemsForData:data]; |
+ EXPECT_TRUE(ArrayContainsImageSource(items)); |
+} |
+ |
+// Verifies that when App Extension support is enabled, the URL string is |
+// passed in a dictionary as part of the Activity Items to the App Extension. |
+TEST_F(ActivityServiceControllerTest, ActivityItemsForDataWithPasswordAppEx) { |
+ base::scoped_nsobject<ActivityServiceController> activityController( |
+ [[ActivityServiceController alloc] init]); |
+ base::scoped_nsobject<ShareToData> data([[ShareToData alloc] |
+ initWithURL:GURL("https://chromium.org/login.html") |
+ title:@"kung fu fighting" |
+ isOriginalTitle:YES |
+ isPagePrintable:YES]); |
+ NSArray* items = [activityController activityItemsForData:data]; |
+ NSString* findLoginAction = |
+ (NSString*)activity_services::kUTTypeAppExtensionFindLoginAction; |
+ // Gets the list of NSExtensionItem objects returned by the array of |
+ // id<UIActivityItemSource> objects returned by -activityItemsForData:. |
+ NSArray* extensionItems = FindItemsForActivityType( |
+ items, activity_services::kAppExtensionOnePassword); |
+ ASSERT_EQ(1U, [extensionItems count]); |
+ NSExtensionItem* item = extensionItems[0]; |
+ EXPECT_EQ(1U, item.attachments.count); |
+ NSItemProvider* itemProvider = item.attachments[0]; |
+ // Extracts the dictionary back from the ItemProvider and then check that |
+ // it has the expected version and the page's URL. |
+ __block base::scoped_nsobject<NSDictionary> result; |
+ [itemProvider |
+ loadItemForTypeIdentifier:findLoginAction |
+ options:nil |
+ completionHandler:^(id item, NSError* error) { |
+ if (error || ![item isKindOfClass:[NSDictionary class]]) { |
+ result.reset([[NSDictionary dictionary] retain]); |
+ } else { |
+ result.reset([item retain]); |
+ } |
+ }]; |
+ base::test::ios::WaitUntilCondition(^{ |
+ return result.get() != nil; |
+ }); |
+ EXPECT_EQ(2U, [result count]); |
+ // Checks version. |
+ NSNumber* version = |
+ [result objectForKey:activity_services::kPasswordAppExVersionNumberKey]; |
+ EXPECT_NSEQ(activity_services::kPasswordAppExVersionNumber, version); |
+ // Checks URL. |
+ NSString* appExUrlString = |
+ [result objectForKey:activity_services::kPasswordAppExURLStringKey]; |
+ EXPECT_NSEQ(@"https://chromium.org/login.html", appExUrlString); |
+ |
+ // Checks that the list includes the page's title. |
+ NSArray* sources = |
+ FindItemsOfClass(items, [UIActivityFindLoginActionSource class]); |
+ EXPECT_EQ(1U, [sources count]); |
+ UIActivityFindLoginActionSource* actionSource = sources[0]; |
+ id mockActivityViewController = |
+ [OCMockObject niceMockForClass:[UIActivityViewController class]]; |
+ NSString* title = [actionSource |
+ activityViewController:mockActivityViewController |
+ subjectForActivityType:activity_services::kAppExtensionOnePassword]; |
+ EXPECT_NSEQ(@"kung fu fighting", title); |
+} |
+ |
+// Verifies that a Share extension can fetch a URL when Password App Extension |
+// is enabled. |
+TEST_F(ActivityServiceControllerTest, |
+ ActivityItemsForDataWithPasswordAppExReturnsURL) { |
+ base::scoped_nsobject<ActivityServiceController> activityController( |
+ [[ActivityServiceController alloc] init]); |
+ base::scoped_nsobject<ShareToData> data([[ShareToData alloc] |
+ initWithURL:GURL("https://chromium.org/login.html") |
+ title:@"kung fu fighting" |
+ isOriginalTitle:YES |
+ isPagePrintable:YES]); |
+ NSArray* items = [activityController activityItemsForData:data]; |
+ NSString* shareAction = @"com.apple.UIKit.activity.PostToFacebook"; |
+ NSArray* urlItems = |
+ FindItemsEqualsToUTType(items, shareAction, @"public.url"); |
+ ASSERT_EQ(1U, [urlItems count]); |
+ id<UIActivityItemSource> itemSource = urlItems[0]; |
+ id mockActivityViewController = |
+ [OCMockObject niceMockForClass:[UIActivityViewController class]]; |
+ id item = [itemSource activityViewController:mockActivityViewController |
+ itemForActivityType:shareAction]; |
+ ASSERT_TRUE([item isKindOfClass:[NSURL class]]); |
+ EXPECT_NSEQ(@"https://chromium.org/login.html", [item absoluteString]); |
+} |
+ |
+// Verifies that -processItemsReturnedFromActivity:status:item: contains |
+// the username and password. |
+TEST_F(ActivityServiceControllerTest, ProcessItemsReturnedSuccessfully) { |
+ base::scoped_nsobject<ActivityServiceController> activityController( |
+ [[ActivityServiceController alloc] init]); |
+ |
+ // Sets up a Mock ShareToDelegate object to check that the callback function |
+ // -passwordAppExDidFinish:username:password:successMessage: |
+ // is correct with the correct username and password. |
+ OCMockObject* shareToDelegateMock = |
+ [OCMockObject mockForProtocol:@protocol(ShareToDelegate)]; |
+ NSString* const kSecretUsername = @"john.doe"; |
+ NSString* const kSecretPassword = @"super!secret"; |
+ __block bool blockCalled = false; |
+ void (^validationBlock)(NSInvocation*) = ^(NSInvocation* invocation) { |
+ NSString* username; |
+ NSString* password; |
+ // Skips 0 and 1 index because they are |self| and |cmd|. |
+ [invocation getArgument:&username atIndex:3]; |
+ [invocation getArgument:&password atIndex:4]; |
+ EXPECT_NSEQ(kSecretUsername, username); |
+ EXPECT_NSEQ(kSecretPassword, password); |
+ blockCalled = true; |
+ }; |
+ [[[shareToDelegateMock stub] andDo:validationBlock] |
+ passwordAppExDidFinish:ShareTo::ShareResult::SHARE_SUCCESS |
+ username:OCMOCK_ANY |
+ password:OCMOCK_ANY |
+ successMessage:OCMOCK_ANY]; |
+ [activityController setShareToDelegateForTesting:(id)shareToDelegateMock]; |
+ |
+ // Sets up the returned item from a Password Management App Extension. |
+ NSString* activityType = @"com.software.find-login-action.extension"; |
+ ShareTo::ShareResult result = ShareTo::ShareResult::SHARE_SUCCESS; |
+ NSDictionary* dictionaryFromAppEx = |
+ @{ @"username" : kSecretUsername, |
+ @"password" : kSecretPassword }; |
+ base::scoped_nsobject<NSItemProvider> itemProvider([[NSItemProvider alloc] |
+ initWithItem:dictionaryFromAppEx |
+ typeIdentifier:(NSString*)kUTTypePropertyList]); |
+ base::scoped_nsobject<NSExtensionItem> extensionItem( |
+ [[NSExtensionItem alloc] init]); |
+ [extensionItem setAttachments:@[ itemProvider.get() ]]; |
+ |
+ BOOL resetUI = |
+ [activityController processItemsReturnedFromActivity:activityType |
+ status:result |
+ items:@[ extensionItem ]]; |
+ ASSERT_FALSE(resetUI); |
+ // Wait for -passwordAppExDidFinish:username:password:successMessage: |
+ // to be called. |
+ base::test::ios::WaitUntilCondition(^{ |
+ return blockCalled; |
+ }); |
+ EXPECT_OCMOCK_VERIFY(shareToDelegateMock); |
+} |
+ |
+// Verifies that -processItemsReturnedFromActivity:status:item: fails when |
+// called with invalid NSExtensionItem. |
+TEST_F(ActivityServiceControllerTest, ProcessItemsReturnedFailures) { |
+ ProcessItemsReturnedFromActivityFailure(@[], YES); |
+ |
+ // Extension Item is empty. |
+ base::scoped_nsobject<NSExtensionItem> extensionItem( |
+ [[NSExtensionItem alloc] init]); |
+ [extensionItem setAttachments:@[]]; |
+ ProcessItemsReturnedFromActivityFailure(@[ extensionItem ], YES); |
+ |
+ // Extension Item does not have a property list provider as the first |
+ // attachment. |
+ base::scoped_nsobject<NSItemProvider> itemProvider([[NSItemProvider alloc] |
+ initWithItem:@"some arbitrary garbage" |
+ typeIdentifier:(NSString*)kUTTypeText]); |
+ [extensionItem setAttachments:@[ itemProvider.get() ]]; |
+ ProcessItemsReturnedFromActivityFailure(@[ extensionItem ], YES); |
+ |
+ // Property list provider did not return a dictionary object. |
+ itemProvider.reset([[NSItemProvider alloc] |
+ initWithItem:@[ @"foo", @"bar" ] |
+ typeIdentifier:(NSString*)kUTTypePropertyList]); |
+ [extensionItem setAttachments:@[ itemProvider.get() ]]; |
+ ProcessItemsReturnedFromActivityFailure(@[ extensionItem ], NO); |
+} |
+ |
+// Verifies that the PrintActivity is sent to the UIActivityViewController if |
+// and only if the activity is "printable". |
+TEST_F(ActivityServiceControllerTest, ApplicationActivitiesForData) { |
+ base::scoped_nsobject<ActivityServiceController> activityController( |
+ [[ActivityServiceController alloc] init]); |
+ |
+ // Verify printable data. |
+ base::scoped_nsobject<ShareToData> data([[ShareToData alloc] |
+ initWithURL:GURL("https://chromium.org/printable") |
+ title:@"bar" |
+ isOriginalTitle:YES |
+ isPagePrintable:YES]); |
+ |
+ NSArray* items = |
+ [activityController applicationActivitiesForData:data controller:nil]; |
+ NSUInteger expected_items_count = |
+ reading_list::switches::IsReadingListEnabled() ? 2U : 1U; |
+ ASSERT_EQ(expected_items_count, [items count]); |
+ EXPECT_EQ([PrintActivity class], [[items objectAtIndex:0] class]); |
+ |
+ // Verify non-printable data. |
+ data.reset([[ShareToData alloc] |
+ initWithURL:GURL("https://chromium.org/unprintable") |
+ title:@"baz" |
+ isOriginalTitle:YES |
+ isPagePrintable:NO]); |
+ items = [activityController applicationActivitiesForData:data controller:nil]; |
+ EXPECT_EQ(expected_items_count - 1, [items count]); |
+} |
+ |
+TEST_F(ActivityServiceControllerTest, FindLoginActionTypeConformsToPublicURL) { |
+ // If this test fails, it is probably due to missing or incorrect |
+ // UTImportedTypeDeclarations in Info.plist. Note that there are |
+ // two Info.plist, |
+ // - ios/chrome/app/resources/Info.plist for Chrome app |
+ // - testing/gtest_ios/unittest-Info.plist for ios_chrome_unittests |
+ // Both of them must be changed. |
+ |
+ // 1Password defined the type @"org.appextension.find-login-action" so |
+ // any app can launch the 1Password app extension to fill in username and |
+ // password. This is being used by iOS native apps to launch 1Password app |
+ // extension and show *only* 1Password app extension as an option. |
+ // Therefore, this data type should *not* conform to public.url. |
+ // During the transition period, this test: |
+ // EXPECT_FALSE(UTTypeConformsTo(onePasswordFindLoginAction, kUTTypeURL)); |
+ // is not possible due to backward compatibility configurations. |
+ CFStringRef onePasswordFindLoginAction = |
+ reinterpret_cast<CFStringRef>(@"org.appextension.find-login-action"); |
+ |
+ // Chrome defines kUTTypeAppExtensionFindLoginAction which conforms to |
+ // public.url UTType in order to allow Share actions (e.g. Facebook, Twitter, |
+ // etc) to appear on UIActivityViewController opened by Chrome). |
+ CFStringRef chromeFindLoginAction = reinterpret_cast<CFStringRef>( |
+ activity_services::kUTTypeAppExtensionFindLoginAction); |
+ EXPECT_TRUE(UTTypeConformsTo(chromeFindLoginAction, kUTTypeURL)); |
+ EXPECT_TRUE( |
+ UTTypeConformsTo(chromeFindLoginAction, onePasswordFindLoginAction)); |
+} |
+ |
+} // namespace |