OLD | NEW |
(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/autofill/form_suggestion_controller.h" |
| 6 |
| 7 #include <utility> |
| 8 #include <vector> |
| 9 |
| 10 #include "base/ios/ios_util.h" |
| 11 #include "base/mac/foundation_util.h" |
| 12 #import "components/autofill/ios/browser/form_suggestion.h" |
| 13 #import "ios/chrome/browser/autofill/form_input_accessory_view_controller.h" |
| 14 #import "ios/chrome/browser/autofill/form_suggestion_provider.h" |
| 15 #import "ios/chrome/browser/autofill/form_suggestion_view.h" |
| 16 #include "ios/chrome/browser/ui/ui_util.h" |
| 17 #import "ios/chrome/browser/web/chrome_web_test.h" |
| 18 #include "ios/chrome/test/base/scoped_block_swizzler.h" |
| 19 #import "ios/web/public/web_state/crw_web_view_proxy.h" |
| 20 #import "ios/web/web_state/ui/crw_web_controller.h" |
| 21 #import "testing/gtest_mac.h" |
| 22 #import "third_party/ocmock/OCMock/OCMock.h" |
| 23 #include "third_party/ocmock/gtest_support.h" |
| 24 |
| 25 #if !defined(__has_feature) || !__has_feature(objc_arc) |
| 26 #error "This file requires ARC support." |
| 27 #endif |
| 28 |
| 29 @interface FormInputAccessoryViewController (Testing) |
| 30 - (instancetype)initWithWebState:(web::WebState*)webState |
| 31 JSSuggestionManager:(JsSuggestionManager*)JSSuggestionManager |
| 32 providers:(NSArray*)providers; |
| 33 @end |
| 34 |
| 35 // Test provider that records invocations of its interface methods. |
| 36 @interface TestSuggestionProvider : NSObject<FormSuggestionProvider> |
| 37 |
| 38 @property(weak, nonatomic, readonly) FormSuggestion* suggestion; |
| 39 @property(weak, nonatomic, readonly) NSString* formName; |
| 40 @property(weak, nonatomic, readonly) NSString* fieldName; |
| 41 @property(nonatomic, assign) BOOL selected; |
| 42 @property(nonatomic, assign) BOOL askedIfSuggestionsAvailable; |
| 43 @property(nonatomic, assign) BOOL askedForSuggestions; |
| 44 |
| 45 - (instancetype)initWithSuggestions:(NSArray*)suggestions; |
| 46 |
| 47 @end |
| 48 |
| 49 @implementation TestSuggestionProvider { |
| 50 NSArray* _suggestions; |
| 51 NSString* _formName; |
| 52 NSString* _fieldName; |
| 53 FormSuggestion* _suggestion; |
| 54 } |
| 55 |
| 56 @synthesize selected = _selected; |
| 57 @synthesize askedIfSuggestionsAvailable = _askedIfSuggestionsAvailable; |
| 58 @synthesize askedForSuggestions = _askedForSuggestions; |
| 59 |
| 60 - (instancetype)initWithSuggestions:(NSArray*)suggestions { |
| 61 self = [super init]; |
| 62 if (self) |
| 63 _suggestions = [suggestions copy]; |
| 64 return self; |
| 65 } |
| 66 |
| 67 - (NSString*)formName { |
| 68 return _formName; |
| 69 } |
| 70 |
| 71 - (NSString*)fieldName { |
| 72 return _fieldName; |
| 73 } |
| 74 |
| 75 - (FormSuggestion*)suggestion { |
| 76 return _suggestion; |
| 77 } |
| 78 |
| 79 - (void)checkIfSuggestionsAvailableForForm:(NSString*)formName |
| 80 field:(NSString*)fieldName |
| 81 type:(NSString*)type |
| 82 typedValue:(NSString*)typedValue |
| 83 webState:(web::WebState*)webState |
| 84 completionHandler: |
| 85 (SuggestionsAvailableCompletion)completion { |
| 86 self.askedIfSuggestionsAvailable = YES; |
| 87 completion([_suggestions count] > 0); |
| 88 } |
| 89 |
| 90 - (void)retrieveSuggestionsForForm:(NSString*)formName |
| 91 field:(NSString*)fieldName |
| 92 type:(NSString*)type |
| 93 typedValue:(NSString*)typedValue |
| 94 webState:(web::WebState*)webState |
| 95 completionHandler:(SuggestionsReadyCompletion)completion { |
| 96 self.askedForSuggestions = YES; |
| 97 completion(_suggestions, self); |
| 98 } |
| 99 |
| 100 - (void)didSelectSuggestion:(FormSuggestion*)suggestion |
| 101 forField:(NSString*)fieldName |
| 102 form:(NSString*)formName |
| 103 completionHandler:(SuggestionHandledCompletion)completion { |
| 104 self.selected = YES; |
| 105 _suggestion = suggestion; |
| 106 _formName = [formName copy]; |
| 107 _fieldName = [fieldName copy]; |
| 108 completion(); |
| 109 } |
| 110 |
| 111 @end |
| 112 |
| 113 namespace { |
| 114 |
| 115 // Finds the FormSuggestionView in |parent|'s view hierarchy, if it exists. |
| 116 FormSuggestionView* GetSuggestionView(UIView* parent) { |
| 117 if ([parent isKindOfClass:[FormSuggestionView class]]) |
| 118 return base::mac::ObjCCastStrict<FormSuggestionView>(parent); |
| 119 for (UIView* child in parent.subviews) { |
| 120 UIView* suggestion_view = GetSuggestionView(child); |
| 121 if (suggestion_view) |
| 122 return base::mac::ObjCCastStrict<FormSuggestionView>(suggestion_view); |
| 123 } |
| 124 return nil; |
| 125 } |
| 126 |
| 127 // Test fixture for FormSuggestionController testing. |
| 128 class FormSuggestionControllerTest : public ChromeWebTest { |
| 129 public: |
| 130 FormSuggestionControllerTest() {} |
| 131 |
| 132 void SetUp() override { |
| 133 ChromeWebTest::SetUp(); |
| 134 |
| 135 // Mock out the JsSuggestionManager. |
| 136 mock_js_suggestion_manager_ = |
| 137 [OCMockObject niceMockForClass:[JsSuggestionManager class]]; |
| 138 |
| 139 // Set up a fake keyboard accessory view. It is expected to have two |
| 140 // subviews. |
| 141 input_accessory_view_ = [[UIView alloc] init]; |
| 142 UIView* fake_view_1 = [[UIView alloc] init]; |
| 143 [input_accessory_view_ addSubview:fake_view_1]; |
| 144 UIView* fake_view_2 = [[UIView alloc] init]; |
| 145 [input_accessory_view_ addSubview:fake_view_2]; |
| 146 |
| 147 // Return the fake keyboard accessory view from the mock CRWWebViewProxy. |
| 148 mock_web_view_proxy_ = |
| 149 [OCMockObject niceMockForProtocol:@protocol(CRWWebViewProxy)]; |
| 150 [[[mock_web_view_proxy_ stub] andReturn:input_accessory_view_] |
| 151 keyboardAccessory]; |
| 152 } |
| 153 |
| 154 void TearDown() override { |
| 155 [suggestion_controller_ detachFromWebState]; |
| 156 ChromeWebTest::TearDown(); |
| 157 } |
| 158 |
| 159 // Sets |url| to be current for WebState. |
| 160 void SetCurrentUrl(const std::string& url) { LoadHtml(@"", GURL(url)); } |
| 161 |
| 162 // Swizzles the current web controller to set whether the content is HTML. |
| 163 void SetContentIsHtml(BOOL content_is_html) { |
| 164 id content_is_html_block = ^BOOL(CRWWebController* webController) { |
| 165 return content_is_html; |
| 166 }; |
| 167 content_is_html_swizzler_.reset(new ScopedBlockSwizzler( |
| 168 [CRWWebController class], @selector(contentIsHTML), |
| 169 content_is_html_block)); |
| 170 } |
| 171 |
| 172 protected: |
| 173 // Sets up |suggestion_controller_| with the specified array of |
| 174 // FormSuggestionProviders. |
| 175 void SetUpController(NSArray* providers) { |
| 176 suggestion_controller_ = [[FormSuggestionController alloc] |
| 177 initWithWebState:web_state() |
| 178 providers:providers |
| 179 JsSuggestionManager:mock_js_suggestion_manager_]; |
| 180 [suggestion_controller_ setWebViewProxy:mock_web_view_proxy_]; |
| 181 @autoreleasepool { |
| 182 accessory_controller_ = [[FormInputAccessoryViewController alloc] |
| 183 initWithWebState:web_state() |
| 184 JSSuggestionManager:mock_js_suggestion_manager_ |
| 185 providers:@[ |
| 186 [suggestion_controller_ accessoryViewProvider] |
| 187 ]]; |
| 188 } |
| 189 // Mock out the FormInputAccessoryViewController so it can use the fake |
| 190 // CRWWebViewProxy |
| 191 id mock_accessory_controller = |
| 192 [OCMockObject partialMockForObject:accessory_controller_]; |
| 193 [[[mock_accessory_controller stub] andReturn:mock_web_view_proxy_] |
| 194 webViewProxy]; |
| 195 |
| 196 // On iPad devices, the suggestion view is added directly to the |
| 197 // keyboard view instead of to the input accessory view which is no longer |
| 198 // available on iPad devices. The following code mocks out the methods on |
| 199 // FormInputAccessoryViewController that add and remove the suggestion view. |
| 200 // The mocks now just add and remove it directly to and from |
| 201 // input_accessory_view_ so that the tests can locate it with |
| 202 // GetSuggestionView (defined above). |
| 203 // TODO(crbug.com/661622): Revisit this to see if there's a better way to |
| 204 // test the iPad case. At a minimum, the name 'input_accessory_view_' should |
| 205 // be made more generic. |
| 206 if (IsIPadIdiom()) { |
| 207 void (^mockShow)(NSInvocation*) = ^(NSInvocation* invocation) { |
| 208 __unsafe_unretained UIView* view; |
| 209 [invocation getArgument:&view atIndex:2]; |
| 210 for (UIView* view in [input_accessory_view_ subviews]) { |
| 211 [view removeFromSuperview]; |
| 212 } |
| 213 [input_accessory_view_ addSubview:view]; |
| 214 }; |
| 215 [[[mock_accessory_controller stub] andDo:mockShow] |
| 216 showCustomInputAccessoryView:[OCMArg any]]; |
| 217 |
| 218 void (^mockRestore)(NSInvocation*) = ^(NSInvocation* invocation) { |
| 219 for (UIView* view in [input_accessory_view_ subviews]) { |
| 220 [view removeFromSuperview]; |
| 221 } |
| 222 }; |
| 223 [[[mock_accessory_controller stub] andDo:mockRestore] |
| 224 restoreDefaultInputAccessoryView]; |
| 225 } |
| 226 } |
| 227 |
| 228 // Swizzler for [CRWWebController contentIsHTML]. |
| 229 std::unique_ptr<ScopedBlockSwizzler> content_is_html_swizzler_; |
| 230 |
| 231 // The FormSuggestionController under test. |
| 232 FormSuggestionController* suggestion_controller_; |
| 233 |
| 234 // A fake keyboard accessory view. |
| 235 UIView* input_accessory_view_; |
| 236 |
| 237 // Mock JsSuggestionManager for verifying interactions. |
| 238 id mock_js_suggestion_manager_; |
| 239 |
| 240 // Mock CRWWebViewProxy for verifying interactions. |
| 241 id mock_web_view_proxy_; |
| 242 |
| 243 // Accessory view controller. |
| 244 FormInputAccessoryViewController* accessory_controller_; |
| 245 |
| 246 DISALLOW_COPY_AND_ASSIGN(FormSuggestionControllerTest); |
| 247 }; |
| 248 |
| 249 // Tests that pages whose URLs don't have a web scheme aren't processed. |
| 250 TEST_F(FormSuggestionControllerTest, PageLoadShouldBeIgnoredWhenNotWebScheme) { |
| 251 SetUpController(@[]); |
| 252 SetCurrentUrl("data:text/html;charset=utf8;base64,"); |
| 253 [suggestion_controller_ webStateDidLoadPage:web_state()]; |
| 254 |
| 255 EXPECT_FALSE(GetSuggestionView(input_accessory_view_)); |
| 256 EXPECT_OCMOCK_VERIFY(mock_js_suggestion_manager_); |
| 257 } |
| 258 |
| 259 // Tests that pages whose content isn't HTML aren't processed. |
| 260 TEST_F(FormSuggestionControllerTest, PageLoadShouldBeIgnoredWhenNotHtml) { |
| 261 SetUpController(@[]); |
| 262 SetCurrentUrl("http://foo.com"); |
| 263 SetContentIsHtml(NO); |
| 264 [suggestion_controller_ webStateDidLoadPage:web_state()]; |
| 265 |
| 266 EXPECT_FALSE(GetSuggestionView(input_accessory_view_)); |
| 267 EXPECT_OCMOCK_VERIFY(mock_js_suggestion_manager_); |
| 268 } |
| 269 |
| 270 // Tests that the keyboard accessory view is reset and JavaScript is injected |
| 271 // when a page is loaded. |
| 272 TEST_F(FormSuggestionControllerTest, |
| 273 PageLoadShouldRestoreKeyboardAccessoryViewAndInjectJavaScript) { |
| 274 SetUpController(@[]); |
| 275 SetCurrentUrl("http://foo.com"); |
| 276 |
| 277 // Load the page. The JS should be injected. |
| 278 [[mock_js_suggestion_manager_ expect] inject]; |
| 279 [suggestion_controller_ webStateDidLoadPage:web_state()]; |
| 280 EXPECT_OCMOCK_VERIFY(mock_js_suggestion_manager_); |
| 281 |
| 282 // Trigger form activity, which should set up the suggestions view. |
| 283 [accessory_controller_ webState:web_state() |
| 284 didRegisterFormActivityWithFormNamed:"form" |
| 285 fieldName:"field" |
| 286 type:"type" |
| 287 value:"value" |
| 288 keyCode:web::WebStateObserver:: |
| 289 kInvalidFormKeyCode |
| 290 inputMissing:false]; |
| 291 EXPECT_TRUE(GetSuggestionView(input_accessory_view_)); |
| 292 |
| 293 // Trigger another page load. The suggestions accessory view should |
| 294 // not be present. |
| 295 [accessory_controller_ webStateDidLoadPage:web_state()]; |
| 296 EXPECT_FALSE(GetSuggestionView(input_accessory_view_)); |
| 297 } |
| 298 |
| 299 // Tests that "blur" events are ignored. |
| 300 TEST_F(FormSuggestionControllerTest, FormActivityBlurShouldBeIgnored) { |
| 301 [accessory_controller_ webState:web_state() |
| 302 didRegisterFormActivityWithFormNamed:"form" |
| 303 fieldName:"field" |
| 304 type:"blur" // blur! |
| 305 value:"value" |
| 306 keyCode:web::WebStateObserver:: |
| 307 kInvalidFormKeyCode |
| 308 inputMissing:false]; |
| 309 EXPECT_FALSE(GetSuggestionView(input_accessory_view_)); |
| 310 } |
| 311 |
| 312 // Tests that no suggestions are displayed when no providers are registered. |
| 313 TEST_F(FormSuggestionControllerTest, |
| 314 FormActivityShouldRetrieveSuggestions_NoProvidersAvailable) { |
| 315 // Set up the controller without any providers. |
| 316 SetUpController(@[]); |
| 317 SetCurrentUrl("http://foo.com"); |
| 318 [accessory_controller_ webState:web_state() |
| 319 didRegisterFormActivityWithFormNamed:"form" |
| 320 fieldName:"field" |
| 321 type:"type" |
| 322 value:"value" |
| 323 keyCode:web::WebStateObserver:: |
| 324 kInvalidFormKeyCode |
| 325 inputMissing:false]; |
| 326 |
| 327 // The suggestions accessory view should be empty. |
| 328 FormSuggestionView* suggestionView = GetSuggestionView(input_accessory_view_); |
| 329 EXPECT_TRUE(suggestionView); |
| 330 EXPECT_EQ(0U, [suggestionView.suggestions count]); |
| 331 } |
| 332 |
| 333 // Tests that, when no providers have suggestions to offer for a form/field, |
| 334 // they aren't asked and no suggestions are displayed. |
| 335 TEST_F(FormSuggestionControllerTest, |
| 336 FormActivityShouldRetrieveSuggestions_NoSuggestionsAvailable) { |
| 337 // Set up the controller with some providers, but none of them will |
| 338 // have suggestions available. |
| 339 TestSuggestionProvider* provider1 = |
| 340 [[TestSuggestionProvider alloc] initWithSuggestions:@[]]; |
| 341 TestSuggestionProvider* provider2 = |
| 342 [[TestSuggestionProvider alloc] initWithSuggestions:@[]]; |
| 343 SetUpController(@[ provider1, provider2 ]); |
| 344 SetCurrentUrl("http://foo.com"); |
| 345 |
| 346 [accessory_controller_ webState:web_state() |
| 347 didRegisterFormActivityWithFormNamed:"form" |
| 348 fieldName:"field" |
| 349 type:"type" |
| 350 value:"value" |
| 351 keyCode:web::WebStateObserver:: |
| 352 kInvalidFormKeyCode |
| 353 inputMissing:false]; |
| 354 |
| 355 // The providers should each be asked if they have suggestions for the |
| 356 // form in question. |
| 357 EXPECT_TRUE([provider1 askedIfSuggestionsAvailable]); |
| 358 EXPECT_TRUE([provider2 askedIfSuggestionsAvailable]); |
| 359 |
| 360 // Since none of the providers had suggestions available, none of them |
| 361 // should have been asked for suggestions. |
| 362 EXPECT_FALSE([provider1 askedForSuggestions]); |
| 363 EXPECT_FALSE([provider2 askedForSuggestions]); |
| 364 |
| 365 // The accessory view should be empty. |
| 366 FormSuggestionView* suggestionView = GetSuggestionView(input_accessory_view_); |
| 367 EXPECT_TRUE(suggestionView); |
| 368 EXPECT_EQ(0U, [suggestionView.suggestions count]); |
| 369 } |
| 370 |
| 371 // Tests that, once a provider is asked if it has suggestions for a form/field, |
| 372 // it and only it is asked to provide them, and that they are then displayed |
| 373 // in the keyboard accessory view. |
| 374 TEST_F(FormSuggestionControllerTest, |
| 375 FormActivityShouldRetrieveSuggestions_SuggestionsAddedToAccessoryView) { |
| 376 // Set up the controller with some providers, one of which can provide |
| 377 // suggestions. |
| 378 NSArray* suggestions = @[ |
| 379 [FormSuggestion suggestionWithValue:@"foo" |
| 380 displayDescription:nil |
| 381 icon:@"" |
| 382 identifier:0], |
| 383 [FormSuggestion suggestionWithValue:@"bar" |
| 384 displayDescription:nil |
| 385 icon:@"" |
| 386 identifier:1] |
| 387 ]; |
| 388 TestSuggestionProvider* provider1 = |
| 389 [[TestSuggestionProvider alloc] initWithSuggestions:suggestions]; |
| 390 TestSuggestionProvider* provider2 = |
| 391 [[TestSuggestionProvider alloc] initWithSuggestions:@[]]; |
| 392 SetUpController(@[ provider1, provider2 ]); |
| 393 SetCurrentUrl("http://foo.com"); |
| 394 |
| 395 [accessory_controller_ webState:web_state() |
| 396 didRegisterFormActivityWithFormNamed:"form" |
| 397 fieldName:"field" |
| 398 type:"type" |
| 399 value:"value" |
| 400 keyCode:web::WebStateObserver:: |
| 401 kInvalidFormKeyCode |
| 402 inputMissing:false]; |
| 403 |
| 404 // Since the first provider has suggestions available, it and only it |
| 405 // should have been asked. |
| 406 EXPECT_TRUE([provider1 askedIfSuggestionsAvailable]); |
| 407 EXPECT_FALSE([provider2 askedIfSuggestionsAvailable]); |
| 408 |
| 409 // Since the first provider said it had suggestions, it and only it |
| 410 // should have been asked to provide them. |
| 411 EXPECT_TRUE([provider1 askedForSuggestions]); |
| 412 EXPECT_FALSE([provider2 askedForSuggestions]); |
| 413 |
| 414 // The accessory view should show the suggestions. |
| 415 FormSuggestionView* suggestionView = GetSuggestionView(input_accessory_view_); |
| 416 EXPECT_TRUE(suggestionView); |
| 417 EXPECT_NSEQ(suggestions, suggestionView.suggestions); |
| 418 } |
| 419 |
| 420 // Tests that selecting a suggestion from the accessory view informs the |
| 421 // specified delegate for that suggestion. |
| 422 TEST_F(FormSuggestionControllerTest, SelectingSuggestionShouldNotifyDelegate) { |
| 423 // Send some suggestions to the controller and then tap one. |
| 424 NSArray* suggestions = @[ |
| 425 [FormSuggestion suggestionWithValue:@"foo" |
| 426 displayDescription:nil |
| 427 icon:@"" |
| 428 identifier:0], |
| 429 ]; |
| 430 TestSuggestionProvider* provider = |
| 431 [[TestSuggestionProvider alloc] initWithSuggestions:suggestions]; |
| 432 SetUpController(@[ provider ]); |
| 433 SetCurrentUrl("http://foo.com"); |
| 434 [accessory_controller_ webState:web_state() |
| 435 didRegisterFormActivityWithFormNamed:"form" |
| 436 fieldName:"field" |
| 437 type:"type" |
| 438 value:"value" |
| 439 keyCode:web::WebStateObserver:: |
| 440 kInvalidFormKeyCode |
| 441 inputMissing:false]; |
| 442 |
| 443 // Selecting a suggestion should notify the delegate. |
| 444 [suggestion_controller_ didSelectSuggestion:suggestions[0]]; |
| 445 EXPECT_TRUE([provider selected]); |
| 446 EXPECT_NSEQ(@"form", [provider formName]); |
| 447 EXPECT_NSEQ(@"field", [provider fieldName]); |
| 448 EXPECT_NSEQ(suggestions[0], [provider suggestion]); |
| 449 } |
| 450 |
| 451 } // namespace |
OLD | NEW |