Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(445)

Side by Side Diff: ios/chrome/browser/ui/activity_services/activity_service_controller_unittest.mm

Issue 2585233003: Upstream Chrome on iOS source code [2/11]. (Closed)
Patch Set: Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « ios/chrome/browser/ui/activity_services/activity_service_controller_egtest.mm ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #import "ios/chrome/browser/ui/activity_services/activity_service_controller.h"
6
7 #import <MobileCoreServices/MobileCoreServices.h>
8
9 #include "base/mac/scoped_nsobject.h"
10 #import "base/test/ios/wait_util.h"
11 #include "components/reading_list/core/reading_list_switches.h"
12 #import "ios/chrome/browser/ui/activity_services/activity_type_util.h"
13 #import "ios/chrome/browser/ui/activity_services/appex_constants.h"
14 #import "ios/chrome/browser/ui/activity_services/chrome_activity_item_source.h"
15 #import "ios/chrome/browser/ui/activity_services/print_activity.h"
16 #import "ios/chrome/browser/ui/activity_services/share_to_data.h"
17 #include "ios/web/public/test/test_web_thread_bundle.h"
18 #include "testing/gtest_mac.h"
19 #include "testing/platform_test.h"
20 #import "third_party/ocmock/OCMock/OCMock.h"
21 #import "third_party/ocmock/gtest_support.h"
22
23 @interface ActivityServiceController (CrVisibleForTesting)
24 - (NSArray*)activityItemsForData:(ShareToData*)data;
25 - (NSArray*)applicationActivitiesForData:(ShareToData*)data
26 controller:(UIViewController*)controller;
27 - (BOOL)processItemsReturnedFromActivity:(NSString*)activityType
28 status:(ShareTo::ShareResult)result
29 items:(NSArray*)extensionItems;
30 // Setter function for mocking during testing
31 - (void)setShareToDelegateForTesting:(id<ShareToDelegate>)delegate;
32 @end
33
34 namespace {
35
36 class ActivityServiceControllerTest : public PlatformTest {
37 protected:
38 void SetUp() override {
39 PlatformTest::SetUp();
40 parentController_.reset(
41 [[UIViewController alloc] initWithNibName:nil bundle:nil]);
42 [[UIApplication sharedApplication] keyWindow].rootViewController =
43 parentController_;
44 shareToDelegate_.reset(
45 [[OCMockObject mockForProtocol:@protocol(ShareToDelegate)] retain]);
46 shareData_.reset([[ShareToData alloc]
47 initWithURL:GURL("https://chromium.org")
48 title:@""
49 isOriginalTitle:YES
50 isPagePrintable:YES]);
51 }
52
53 void TearDown() override {
54 [[UIApplication sharedApplication] keyWindow].rootViewController = nil;
55 PlatformTest::TearDown();
56 }
57
58 id<ShareToDelegate> GetShareToDelegate() {
59 return static_cast<id<ShareToDelegate>>(shareToDelegate_.get());
60 }
61
62 CGRect AnchorRect() {
63 // On iPad, UIPopovers must be anchored to rectangles that have a non zero
64 // size.
65 return CGRectMake(0, 0, 1, 1);
66 }
67
68 UIView* AnchorView() {
69 // On iPad, UIPopovers must be anchored to non nil views.
70 return [parentController_.get() view];
71 }
72
73 BOOL ArrayContainsImageSource(NSArray* array) {
74 for (NSObject* item in array) {
75 if ([item class] == [UIActivityImageSource class]) {
76 return YES;
77 }
78 }
79 return NO;
80 }
81
82 // Search |array| for id<UIActivityItemSource> objects. Returns an array of
83 // matching NSExtensionItem objects returned by calling them.
84 NSArray* FindItemsForActivityType(NSArray* array, NSString* activityType) {
85 id mockActivityViewController =
86 [OCMockObject niceMockForClass:[UIActivityViewController class]];
87 NSMutableArray* result = [NSMutableArray array];
88 for (id item in array) {
89 if ([item respondsToSelector:@selector(activityViewController:
90 itemForActivityType:)]) {
91 id resultItem = [item activityViewController:mockActivityViewController
92 itemForActivityType:activityType];
93 if ([resultItem isKindOfClass:[NSExtensionItem class]])
94 [result addObject:resultItem];
95 }
96 }
97 return result;
98 }
99
100 // Searches |array| for objects of class |klass| and returns them in an
101 // autoreleased array.
102 NSArray* FindItemsOfClass(NSArray* array, Class klass) {
103 NSMutableArray* result = [NSMutableArray array];
104 for (id item in array) {
105 if ([item isKindOfClass:klass])
106 [result addObject:item];
107 }
108 return result;
109 }
110
111 // Searches |array| for objects returning |inUTType| conforming objects.
112 // Returns an autoreleased array of conforming objects.
113 NSArray* FindItemsEqualsToUTType(NSArray* array,
114 NSString* activityType,
115 NSString* inUTType) {
116 id mockActivityViewController =
117 [OCMockObject niceMockForClass:[UIActivityViewController class]];
118 NSMutableArray* result = [NSMutableArray array];
119 for (id item in array) {
120 if (![item conformsToProtocol:@protocol(UIActivityItemSource)])
121 continue;
122 SEL dataTypeSelector =
123 @selector(activityViewController:dataTypeIdentifierForActivityType:);
124 if (![item respondsToSelector:dataTypeSelector])
125 continue;
126 NSString* itemDataType =
127 [item activityViewController:mockActivityViewController
128 dataTypeIdentifierForActivityType:activityType];
129 if ([itemDataType isEqualToString:inUTType]) {
130 [result addObject:item];
131 }
132 }
133 return result;
134 }
135
136 // Calls -processItemsReturnedFromActivity:status:items: with the provided
137 // |extensionItem| and expects failure.
138 void ProcessItemsReturnedFromActivityFailure(NSArray* extensionItems,
139 BOOL expectedResetUI) {
140 base::scoped_nsobject<ActivityServiceController> activityController(
141 [[ActivityServiceController alloc] init]);
142
143 // Sets up a Mock ShareToDelegate object to check that the ShareToDelegate
144 // callback function is not called.
145 OCMockObject* shareToDelegateMock =
146 [OCMockObject mockForProtocol:@protocol(ShareToDelegate)];
147 __block bool blockCalled = false;
148 void (^validationBlock)(NSInvocation*) = ^(NSInvocation* invocation) {
149 blockCalled = true;
150 };
151 // OCMock does not allow "any" specification for non-object parameters.
152 // To implement something that accept any non-SHARE_SUCCESS parameter
153 // to calling this method, all the non-success values have to be
154 // enumerated.
155 [[[shareToDelegateMock stub] andDo:validationBlock]
156 passwordAppExDidFinish:ShareTo::ShareResult::SHARE_CANCEL
157 username:OCMOCK_ANY
158 password:OCMOCK_ANY
159 successMessage:OCMOCK_ANY];
160 [[[shareToDelegateMock stub] andDo:validationBlock]
161 passwordAppExDidFinish:ShareTo::ShareResult::SHARE_NETWORK_FAILURE
162 username:OCMOCK_ANY
163 password:OCMOCK_ANY
164 successMessage:OCMOCK_ANY];
165 [[[shareToDelegateMock stub] andDo:validationBlock]
166 passwordAppExDidFinish:ShareTo::ShareResult::SHARE_SIGN_IN_FAILURE
167 username:OCMOCK_ANY
168 password:OCMOCK_ANY
169 successMessage:OCMOCK_ANY];
170 [[[shareToDelegateMock stub] andDo:validationBlock]
171 passwordAppExDidFinish:ShareTo::ShareResult::SHARE_ERROR
172 username:OCMOCK_ANY
173 password:OCMOCK_ANY
174 successMessage:OCMOCK_ANY];
175 [[[shareToDelegateMock stub] andDo:validationBlock]
176 passwordAppExDidFinish:ShareTo::ShareResult::SHARE_UNKNOWN_RESULT
177 username:OCMOCK_ANY
178 password:OCMOCK_ANY
179 successMessage:OCMOCK_ANY];
180 [activityController setShareToDelegateForTesting:(id)shareToDelegateMock];
181
182 // Sets up the returned item from a Password Management App Extension.
183 NSString* activityType = activity_services::kAppExtensionLastPass;
184 ShareTo::ShareResult result = ShareTo::ShareResult::SHARE_SUCCESS;
185 BOOL resetUI =
186 [activityController processItemsReturnedFromActivity:activityType
187 status:result
188 items:extensionItems];
189 ASSERT_EQ(expectedResetUI, resetUI);
190 base::test::ios::WaitUntilCondition(^{
191 return blockCalled;
192 });
193 EXPECT_OCMOCK_VERIFY(shareToDelegateMock);
194 }
195
196 web::TestWebThreadBundle thread_bundle_;
197 base::scoped_nsobject<UIViewController> parentController_;
198 base::scoped_nsobject<OCMockObject> shareToDelegate_;
199 base::scoped_nsobject<ShareToData> shareData_;
200 };
201
202 TEST_F(ActivityServiceControllerTest, PresentAndDismissController) {
203 [[shareToDelegate_ expect] shareDidComplete:ShareTo::ShareResult::SHARE_CANCEL
204 successMessage:[OCMArg isNil]];
205
206 UIViewController* parentController =
207 static_cast<UIViewController*>(parentController_.get());
208 base::scoped_nsobject<ActivityServiceController> activityController(
209 [[ActivityServiceController alloc] init]);
210 EXPECT_FALSE([activityController isActive]);
211
212 // Test sharing.
213 [activityController shareWithData:shareData_
214 controller:parentController
215 browserState:nullptr
216 shareToDelegate:GetShareToDelegate()
217 fromRect:AnchorRect()
218 inView:AnchorView()];
219 EXPECT_TRUE([activityController isActive]);
220
221 // Cancels sharing and isActive flag should be turned off.
222 [activityController cancelShareAnimated:NO];
223 base::test::ios::WaitUntilCondition(^bool() {
224 return ![activityController isActive];
225 });
226 EXPECT_OCMOCK_VERIFY(shareToDelegate_);
227 }
228
229 // Verifies that an UIActivityImageSource is sent to the
230 // UIActivityViewController if and only if the ShareToData contains an image.
231 TEST_F(ActivityServiceControllerTest, ActivityItemsForData) {
232 base::scoped_nsobject<ActivityServiceController> activityController(
233 [[ActivityServiceController alloc] init]);
234
235 // ShareToData does not contain an image, so the result items array will not
236 // contain an image source.
237 base::scoped_nsobject<ShareToData> data([[ShareToData alloc]
238 initWithURL:GURL("https://chromium.org")
239 title:@"foo"
240 isOriginalTitle:YES
241 isPagePrintable:YES]);
242 NSArray* items = [activityController activityItemsForData:data];
243 EXPECT_FALSE(ArrayContainsImageSource(items));
244
245 // Adds an image to the ShareToData object and call -activityItemsForData:
246 // again. Verifies that the result items array contains an image source.
247 [data setImage:[UIImage imageNamed:@"activity_services_print"]];
248 items = [activityController activityItemsForData:data];
249 EXPECT_TRUE(ArrayContainsImageSource(items));
250 }
251
252 // Verifies that when App Extension support is enabled, the URL string is
253 // passed in a dictionary as part of the Activity Items to the App Extension.
254 TEST_F(ActivityServiceControllerTest, ActivityItemsForDataWithPasswordAppEx) {
255 base::scoped_nsobject<ActivityServiceController> activityController(
256 [[ActivityServiceController alloc] init]);
257 base::scoped_nsobject<ShareToData> data([[ShareToData alloc]
258 initWithURL:GURL("https://chromium.org/login.html")
259 title:@"kung fu fighting"
260 isOriginalTitle:YES
261 isPagePrintable:YES]);
262 NSArray* items = [activityController activityItemsForData:data];
263 NSString* findLoginAction =
264 (NSString*)activity_services::kUTTypeAppExtensionFindLoginAction;
265 // Gets the list of NSExtensionItem objects returned by the array of
266 // id<UIActivityItemSource> objects returned by -activityItemsForData:.
267 NSArray* extensionItems = FindItemsForActivityType(
268 items, activity_services::kAppExtensionOnePassword);
269 ASSERT_EQ(1U, [extensionItems count]);
270 NSExtensionItem* item = extensionItems[0];
271 EXPECT_EQ(1U, item.attachments.count);
272 NSItemProvider* itemProvider = item.attachments[0];
273 // Extracts the dictionary back from the ItemProvider and then check that
274 // it has the expected version and the page's URL.
275 __block base::scoped_nsobject<NSDictionary> result;
276 [itemProvider
277 loadItemForTypeIdentifier:findLoginAction
278 options:nil
279 completionHandler:^(id item, NSError* error) {
280 if (error || ![item isKindOfClass:[NSDictionary class]]) {
281 result.reset([[NSDictionary dictionary] retain]);
282 } else {
283 result.reset([item retain]);
284 }
285 }];
286 base::test::ios::WaitUntilCondition(^{
287 return result.get() != nil;
288 });
289 EXPECT_EQ(2U, [result count]);
290 // Checks version.
291 NSNumber* version =
292 [result objectForKey:activity_services::kPasswordAppExVersionNumberKey];
293 EXPECT_NSEQ(activity_services::kPasswordAppExVersionNumber, version);
294 // Checks URL.
295 NSString* appExUrlString =
296 [result objectForKey:activity_services::kPasswordAppExURLStringKey];
297 EXPECT_NSEQ(@"https://chromium.org/login.html", appExUrlString);
298
299 // Checks that the list includes the page's title.
300 NSArray* sources =
301 FindItemsOfClass(items, [UIActivityFindLoginActionSource class]);
302 EXPECT_EQ(1U, [sources count]);
303 UIActivityFindLoginActionSource* actionSource = sources[0];
304 id mockActivityViewController =
305 [OCMockObject niceMockForClass:[UIActivityViewController class]];
306 NSString* title = [actionSource
307 activityViewController:mockActivityViewController
308 subjectForActivityType:activity_services::kAppExtensionOnePassword];
309 EXPECT_NSEQ(@"kung fu fighting", title);
310 }
311
312 // Verifies that a Share extension can fetch a URL when Password App Extension
313 // is enabled.
314 TEST_F(ActivityServiceControllerTest,
315 ActivityItemsForDataWithPasswordAppExReturnsURL) {
316 base::scoped_nsobject<ActivityServiceController> activityController(
317 [[ActivityServiceController alloc] init]);
318 base::scoped_nsobject<ShareToData> data([[ShareToData alloc]
319 initWithURL:GURL("https://chromium.org/login.html")
320 title:@"kung fu fighting"
321 isOriginalTitle:YES
322 isPagePrintable:YES]);
323 NSArray* items = [activityController activityItemsForData:data];
324 NSString* shareAction = @"com.apple.UIKit.activity.PostToFacebook";
325 NSArray* urlItems =
326 FindItemsEqualsToUTType(items, shareAction, @"public.url");
327 ASSERT_EQ(1U, [urlItems count]);
328 id<UIActivityItemSource> itemSource = urlItems[0];
329 id mockActivityViewController =
330 [OCMockObject niceMockForClass:[UIActivityViewController class]];
331 id item = [itemSource activityViewController:mockActivityViewController
332 itemForActivityType:shareAction];
333 ASSERT_TRUE([item isKindOfClass:[NSURL class]]);
334 EXPECT_NSEQ(@"https://chromium.org/login.html", [item absoluteString]);
335 }
336
337 // Verifies that -processItemsReturnedFromActivity:status:item: contains
338 // the username and password.
339 TEST_F(ActivityServiceControllerTest, ProcessItemsReturnedSuccessfully) {
340 base::scoped_nsobject<ActivityServiceController> activityController(
341 [[ActivityServiceController alloc] init]);
342
343 // Sets up a Mock ShareToDelegate object to check that the callback function
344 // -passwordAppExDidFinish:username:password:successMessage:
345 // is correct with the correct username and password.
346 OCMockObject* shareToDelegateMock =
347 [OCMockObject mockForProtocol:@protocol(ShareToDelegate)];
348 NSString* const kSecretUsername = @"john.doe";
349 NSString* const kSecretPassword = @"super!secret";
350 __block bool blockCalled = false;
351 void (^validationBlock)(NSInvocation*) = ^(NSInvocation* invocation) {
352 NSString* username;
353 NSString* password;
354 // Skips 0 and 1 index because they are |self| and |cmd|.
355 [invocation getArgument:&username atIndex:3];
356 [invocation getArgument:&password atIndex:4];
357 EXPECT_NSEQ(kSecretUsername, username);
358 EXPECT_NSEQ(kSecretPassword, password);
359 blockCalled = true;
360 };
361 [[[shareToDelegateMock stub] andDo:validationBlock]
362 passwordAppExDidFinish:ShareTo::ShareResult::SHARE_SUCCESS
363 username:OCMOCK_ANY
364 password:OCMOCK_ANY
365 successMessage:OCMOCK_ANY];
366 [activityController setShareToDelegateForTesting:(id)shareToDelegateMock];
367
368 // Sets up the returned item from a Password Management App Extension.
369 NSString* activityType = @"com.software.find-login-action.extension";
370 ShareTo::ShareResult result = ShareTo::ShareResult::SHARE_SUCCESS;
371 NSDictionary* dictionaryFromAppEx =
372 @{ @"username" : kSecretUsername,
373 @"password" : kSecretPassword };
374 base::scoped_nsobject<NSItemProvider> itemProvider([[NSItemProvider alloc]
375 initWithItem:dictionaryFromAppEx
376 typeIdentifier:(NSString*)kUTTypePropertyList]);
377 base::scoped_nsobject<NSExtensionItem> extensionItem(
378 [[NSExtensionItem alloc] init]);
379 [extensionItem setAttachments:@[ itemProvider.get() ]];
380
381 BOOL resetUI =
382 [activityController processItemsReturnedFromActivity:activityType
383 status:result
384 items:@[ extensionItem ]];
385 ASSERT_FALSE(resetUI);
386 // Wait for -passwordAppExDidFinish:username:password:successMessage:
387 // to be called.
388 base::test::ios::WaitUntilCondition(^{
389 return blockCalled;
390 });
391 EXPECT_OCMOCK_VERIFY(shareToDelegateMock);
392 }
393
394 // Verifies that -processItemsReturnedFromActivity:status:item: fails when
395 // called with invalid NSExtensionItem.
396 TEST_F(ActivityServiceControllerTest, ProcessItemsReturnedFailures) {
397 ProcessItemsReturnedFromActivityFailure(@[], YES);
398
399 // Extension Item is empty.
400 base::scoped_nsobject<NSExtensionItem> extensionItem(
401 [[NSExtensionItem alloc] init]);
402 [extensionItem setAttachments:@[]];
403 ProcessItemsReturnedFromActivityFailure(@[ extensionItem ], YES);
404
405 // Extension Item does not have a property list provider as the first
406 // attachment.
407 base::scoped_nsobject<NSItemProvider> itemProvider([[NSItemProvider alloc]
408 initWithItem:@"some arbitrary garbage"
409 typeIdentifier:(NSString*)kUTTypeText]);
410 [extensionItem setAttachments:@[ itemProvider.get() ]];
411 ProcessItemsReturnedFromActivityFailure(@[ extensionItem ], YES);
412
413 // Property list provider did not return a dictionary object.
414 itemProvider.reset([[NSItemProvider alloc]
415 initWithItem:@[ @"foo", @"bar" ]
416 typeIdentifier:(NSString*)kUTTypePropertyList]);
417 [extensionItem setAttachments:@[ itemProvider.get() ]];
418 ProcessItemsReturnedFromActivityFailure(@[ extensionItem ], NO);
419 }
420
421 // Verifies that the PrintActivity is sent to the UIActivityViewController if
422 // and only if the activity is "printable".
423 TEST_F(ActivityServiceControllerTest, ApplicationActivitiesForData) {
424 base::scoped_nsobject<ActivityServiceController> activityController(
425 [[ActivityServiceController alloc] init]);
426
427 // Verify printable data.
428 base::scoped_nsobject<ShareToData> data([[ShareToData alloc]
429 initWithURL:GURL("https://chromium.org/printable")
430 title:@"bar"
431 isOriginalTitle:YES
432 isPagePrintable:YES]);
433
434 NSArray* items =
435 [activityController applicationActivitiesForData:data controller:nil];
436 NSUInteger expected_items_count =
437 reading_list::switches::IsReadingListEnabled() ? 2U : 1U;
438 ASSERT_EQ(expected_items_count, [items count]);
439 EXPECT_EQ([PrintActivity class], [[items objectAtIndex:0] class]);
440
441 // Verify non-printable data.
442 data.reset([[ShareToData alloc]
443 initWithURL:GURL("https://chromium.org/unprintable")
444 title:@"baz"
445 isOriginalTitle:YES
446 isPagePrintable:NO]);
447 items = [activityController applicationActivitiesForData:data controller:nil];
448 EXPECT_EQ(expected_items_count - 1, [items count]);
449 }
450
451 TEST_F(ActivityServiceControllerTest, FindLoginActionTypeConformsToPublicURL) {
452 // If this test fails, it is probably due to missing or incorrect
453 // UTImportedTypeDeclarations in Info.plist. Note that there are
454 // two Info.plist,
455 // - ios/chrome/app/resources/Info.plist for Chrome app
456 // - testing/gtest_ios/unittest-Info.plist for ios_chrome_unittests
457 // Both of them must be changed.
458
459 // 1Password defined the type @"org.appextension.find-login-action" so
460 // any app can launch the 1Password app extension to fill in username and
461 // password. This is being used by iOS native apps to launch 1Password app
462 // extension and show *only* 1Password app extension as an option.
463 // Therefore, this data type should *not* conform to public.url.
464 // During the transition period, this test:
465 // EXPECT_FALSE(UTTypeConformsTo(onePasswordFindLoginAction, kUTTypeURL));
466 // is not possible due to backward compatibility configurations.
467 CFStringRef onePasswordFindLoginAction =
468 reinterpret_cast<CFStringRef>(@"org.appextension.find-login-action");
469
470 // Chrome defines kUTTypeAppExtensionFindLoginAction which conforms to
471 // public.url UTType in order to allow Share actions (e.g. Facebook, Twitter,
472 // etc) to appear on UIActivityViewController opened by Chrome).
473 CFStringRef chromeFindLoginAction = reinterpret_cast<CFStringRef>(
474 activity_services::kUTTypeAppExtensionFindLoginAction);
475 EXPECT_TRUE(UTTypeConformsTo(chromeFindLoginAction, kUTTypeURL));
476 EXPECT_TRUE(
477 UTTypeConformsTo(chromeFindLoginAction, onePasswordFindLoginAction));
478 }
479
480 } // namespace
OLDNEW
« no previous file with comments | « ios/chrome/browser/ui/activity_services/activity_service_controller_egtest.mm ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698