Index: ios/chrome/browser/autofill/form_suggestion_controller_unittest.mm |
diff --git a/ios/chrome/browser/autofill/form_suggestion_controller_unittest.mm b/ios/chrome/browser/autofill/form_suggestion_controller_unittest.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..68250b9e4c198689c9b45e4106d64a05d6371c3c |
--- /dev/null |
+++ b/ios/chrome/browser/autofill/form_suggestion_controller_unittest.mm |
@@ -0,0 +1,451 @@ |
+// 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/autofill/form_suggestion_controller.h" |
+ |
+#include <utility> |
+#include <vector> |
+ |
+#include "base/ios/ios_util.h" |
+#include "base/mac/foundation_util.h" |
+#import "components/autofill/ios/browser/form_suggestion.h" |
+#import "ios/chrome/browser/autofill/form_input_accessory_view_controller.h" |
+#import "ios/chrome/browser/autofill/form_suggestion_provider.h" |
+#import "ios/chrome/browser/autofill/form_suggestion_view.h" |
+#include "ios/chrome/browser/ui/ui_util.h" |
+#import "ios/chrome/browser/web/chrome_web_test.h" |
+#include "ios/chrome/test/base/scoped_block_swizzler.h" |
+#import "ios/web/public/web_state/crw_web_view_proxy.h" |
+#import "ios/web/web_state/ui/crw_web_controller.h" |
+#import "testing/gtest_mac.h" |
+#import "third_party/ocmock/OCMock/OCMock.h" |
+#include "third_party/ocmock/gtest_support.h" |
+ |
+#if !defined(__has_feature) || !__has_feature(objc_arc) |
+#error "This file requires ARC support." |
+#endif |
+ |
+@interface FormInputAccessoryViewController (Testing) |
+- (instancetype)initWithWebState:(web::WebState*)webState |
+ JSSuggestionManager:(JsSuggestionManager*)JSSuggestionManager |
+ providers:(NSArray*)providers; |
+@end |
+ |
+// Test provider that records invocations of its interface methods. |
+@interface TestSuggestionProvider : NSObject<FormSuggestionProvider> |
+ |
+@property(weak, nonatomic, readonly) FormSuggestion* suggestion; |
+@property(weak, nonatomic, readonly) NSString* formName; |
+@property(weak, nonatomic, readonly) NSString* fieldName; |
+@property(nonatomic, assign) BOOL selected; |
+@property(nonatomic, assign) BOOL askedIfSuggestionsAvailable; |
+@property(nonatomic, assign) BOOL askedForSuggestions; |
+ |
+- (instancetype)initWithSuggestions:(NSArray*)suggestions; |
+ |
+@end |
+ |
+@implementation TestSuggestionProvider { |
+ NSArray* _suggestions; |
+ NSString* _formName; |
+ NSString* _fieldName; |
+ FormSuggestion* _suggestion; |
+} |
+ |
+@synthesize selected = _selected; |
+@synthesize askedIfSuggestionsAvailable = _askedIfSuggestionsAvailable; |
+@synthesize askedForSuggestions = _askedForSuggestions; |
+ |
+- (instancetype)initWithSuggestions:(NSArray*)suggestions { |
+ self = [super init]; |
+ if (self) |
+ _suggestions = [suggestions copy]; |
+ return self; |
+} |
+ |
+- (NSString*)formName { |
+ return _formName; |
+} |
+ |
+- (NSString*)fieldName { |
+ return _fieldName; |
+} |
+ |
+- (FormSuggestion*)suggestion { |
+ return _suggestion; |
+} |
+ |
+- (void)checkIfSuggestionsAvailableForForm:(NSString*)formName |
+ field:(NSString*)fieldName |
+ type:(NSString*)type |
+ typedValue:(NSString*)typedValue |
+ webState:(web::WebState*)webState |
+ completionHandler: |
+ (SuggestionsAvailableCompletion)completion { |
+ self.askedIfSuggestionsAvailable = YES; |
+ completion([_suggestions count] > 0); |
+} |
+ |
+- (void)retrieveSuggestionsForForm:(NSString*)formName |
+ field:(NSString*)fieldName |
+ type:(NSString*)type |
+ typedValue:(NSString*)typedValue |
+ webState:(web::WebState*)webState |
+ completionHandler:(SuggestionsReadyCompletion)completion { |
+ self.askedForSuggestions = YES; |
+ completion(_suggestions, self); |
+} |
+ |
+- (void)didSelectSuggestion:(FormSuggestion*)suggestion |
+ forField:(NSString*)fieldName |
+ form:(NSString*)formName |
+ completionHandler:(SuggestionHandledCompletion)completion { |
+ self.selected = YES; |
+ _suggestion = suggestion; |
+ _formName = [formName copy]; |
+ _fieldName = [fieldName copy]; |
+ completion(); |
+} |
+ |
+@end |
+ |
+namespace { |
+ |
+// Finds the FormSuggestionView in |parent|'s view hierarchy, if it exists. |
+FormSuggestionView* GetSuggestionView(UIView* parent) { |
+ if ([parent isKindOfClass:[FormSuggestionView class]]) |
+ return base::mac::ObjCCastStrict<FormSuggestionView>(parent); |
+ for (UIView* child in parent.subviews) { |
+ UIView* suggestion_view = GetSuggestionView(child); |
+ if (suggestion_view) |
+ return base::mac::ObjCCastStrict<FormSuggestionView>(suggestion_view); |
+ } |
+ return nil; |
+} |
+ |
+// Test fixture for FormSuggestionController testing. |
+class FormSuggestionControllerTest : public ChromeWebTest { |
+ public: |
+ FormSuggestionControllerTest() {} |
+ |
+ void SetUp() override { |
+ ChromeWebTest::SetUp(); |
+ |
+ // Mock out the JsSuggestionManager. |
+ mock_js_suggestion_manager_ = |
+ [OCMockObject niceMockForClass:[JsSuggestionManager class]]; |
+ |
+ // Set up a fake keyboard accessory view. It is expected to have two |
+ // subviews. |
+ input_accessory_view_ = [[UIView alloc] init]; |
+ UIView* fake_view_1 = [[UIView alloc] init]; |
+ [input_accessory_view_ addSubview:fake_view_1]; |
+ UIView* fake_view_2 = [[UIView alloc] init]; |
+ [input_accessory_view_ addSubview:fake_view_2]; |
+ |
+ // Return the fake keyboard accessory view from the mock CRWWebViewProxy. |
+ mock_web_view_proxy_ = |
+ [OCMockObject niceMockForProtocol:@protocol(CRWWebViewProxy)]; |
+ [[[mock_web_view_proxy_ stub] andReturn:input_accessory_view_] |
+ keyboardAccessory]; |
+ } |
+ |
+ void TearDown() override { |
+ [suggestion_controller_ detachFromWebState]; |
+ ChromeWebTest::TearDown(); |
+ } |
+ |
+ // Sets |url| to be current for WebState. |
+ void SetCurrentUrl(const std::string& url) { LoadHtml(@"", GURL(url)); } |
+ |
+ // Swizzles the current web controller to set whether the content is HTML. |
+ void SetContentIsHtml(BOOL content_is_html) { |
+ id content_is_html_block = ^BOOL(CRWWebController* webController) { |
+ return content_is_html; |
+ }; |
+ content_is_html_swizzler_.reset(new ScopedBlockSwizzler( |
+ [CRWWebController class], @selector(contentIsHTML), |
+ content_is_html_block)); |
+ } |
+ |
+ protected: |
+ // Sets up |suggestion_controller_| with the specified array of |
+ // FormSuggestionProviders. |
+ void SetUpController(NSArray* providers) { |
+ suggestion_controller_ = [[FormSuggestionController alloc] |
+ initWithWebState:web_state() |
+ providers:providers |
+ JsSuggestionManager:mock_js_suggestion_manager_]; |
+ [suggestion_controller_ setWebViewProxy:mock_web_view_proxy_]; |
+ @autoreleasepool { |
+ accessory_controller_ = [[FormInputAccessoryViewController alloc] |
+ initWithWebState:web_state() |
+ JSSuggestionManager:mock_js_suggestion_manager_ |
+ providers:@[ |
+ [suggestion_controller_ accessoryViewProvider] |
+ ]]; |
+ } |
+ // Mock out the FormInputAccessoryViewController so it can use the fake |
+ // CRWWebViewProxy |
+ id mock_accessory_controller = |
+ [OCMockObject partialMockForObject:accessory_controller_]; |
+ [[[mock_accessory_controller stub] andReturn:mock_web_view_proxy_] |
+ webViewProxy]; |
+ |
+ // On iPad devices, the suggestion view is added directly to the |
+ // keyboard view instead of to the input accessory view which is no longer |
+ // available on iPad devices. The following code mocks out the methods on |
+ // FormInputAccessoryViewController that add and remove the suggestion view. |
+ // The mocks now just add and remove it directly to and from |
+ // input_accessory_view_ so that the tests can locate it with |
+ // GetSuggestionView (defined above). |
+ // TODO(crbug.com/661622): Revisit this to see if there's a better way to |
+ // test the iPad case. At a minimum, the name 'input_accessory_view_' should |
+ // be made more generic. |
+ if (IsIPadIdiom()) { |
+ void (^mockShow)(NSInvocation*) = ^(NSInvocation* invocation) { |
+ __unsafe_unretained UIView* view; |
+ [invocation getArgument:&view atIndex:2]; |
+ for (UIView* view in [input_accessory_view_ subviews]) { |
+ [view removeFromSuperview]; |
+ } |
+ [input_accessory_view_ addSubview:view]; |
+ }; |
+ [[[mock_accessory_controller stub] andDo:mockShow] |
+ showCustomInputAccessoryView:[OCMArg any]]; |
+ |
+ void (^mockRestore)(NSInvocation*) = ^(NSInvocation* invocation) { |
+ for (UIView* view in [input_accessory_view_ subviews]) { |
+ [view removeFromSuperview]; |
+ } |
+ }; |
+ [[[mock_accessory_controller stub] andDo:mockRestore] |
+ restoreDefaultInputAccessoryView]; |
+ } |
+ } |
+ |
+ // Swizzler for [CRWWebController contentIsHTML]. |
+ std::unique_ptr<ScopedBlockSwizzler> content_is_html_swizzler_; |
+ |
+ // The FormSuggestionController under test. |
+ FormSuggestionController* suggestion_controller_; |
+ |
+ // A fake keyboard accessory view. |
+ UIView* input_accessory_view_; |
+ |
+ // Mock JsSuggestionManager for verifying interactions. |
+ id mock_js_suggestion_manager_; |
+ |
+ // Mock CRWWebViewProxy for verifying interactions. |
+ id mock_web_view_proxy_; |
+ |
+ // Accessory view controller. |
+ FormInputAccessoryViewController* accessory_controller_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(FormSuggestionControllerTest); |
+}; |
+ |
+// Tests that pages whose URLs don't have a web scheme aren't processed. |
+TEST_F(FormSuggestionControllerTest, PageLoadShouldBeIgnoredWhenNotWebScheme) { |
+ SetUpController(@[]); |
+ SetCurrentUrl("data:text/html;charset=utf8;base64,"); |
+ [suggestion_controller_ webStateDidLoadPage:web_state()]; |
+ |
+ EXPECT_FALSE(GetSuggestionView(input_accessory_view_)); |
+ EXPECT_OCMOCK_VERIFY(mock_js_suggestion_manager_); |
+} |
+ |
+// Tests that pages whose content isn't HTML aren't processed. |
+TEST_F(FormSuggestionControllerTest, PageLoadShouldBeIgnoredWhenNotHtml) { |
+ SetUpController(@[]); |
+ SetCurrentUrl("http://foo.com"); |
+ SetContentIsHtml(NO); |
+ [suggestion_controller_ webStateDidLoadPage:web_state()]; |
+ |
+ EXPECT_FALSE(GetSuggestionView(input_accessory_view_)); |
+ EXPECT_OCMOCK_VERIFY(mock_js_suggestion_manager_); |
+} |
+ |
+// Tests that the keyboard accessory view is reset and JavaScript is injected |
+// when a page is loaded. |
+TEST_F(FormSuggestionControllerTest, |
+ PageLoadShouldRestoreKeyboardAccessoryViewAndInjectJavaScript) { |
+ SetUpController(@[]); |
+ SetCurrentUrl("http://foo.com"); |
+ |
+ // Load the page. The JS should be injected. |
+ [[mock_js_suggestion_manager_ expect] inject]; |
+ [suggestion_controller_ webStateDidLoadPage:web_state()]; |
+ EXPECT_OCMOCK_VERIFY(mock_js_suggestion_manager_); |
+ |
+ // Trigger form activity, which should set up the suggestions view. |
+ [accessory_controller_ webState:web_state() |
+ didRegisterFormActivityWithFormNamed:"form" |
+ fieldName:"field" |
+ type:"type" |
+ value:"value" |
+ keyCode:web::WebStateObserver:: |
+ kInvalidFormKeyCode |
+ inputMissing:false]; |
+ EXPECT_TRUE(GetSuggestionView(input_accessory_view_)); |
+ |
+ // Trigger another page load. The suggestions accessory view should |
+ // not be present. |
+ [accessory_controller_ webStateDidLoadPage:web_state()]; |
+ EXPECT_FALSE(GetSuggestionView(input_accessory_view_)); |
+} |
+ |
+// Tests that "blur" events are ignored. |
+TEST_F(FormSuggestionControllerTest, FormActivityBlurShouldBeIgnored) { |
+ [accessory_controller_ webState:web_state() |
+ didRegisterFormActivityWithFormNamed:"form" |
+ fieldName:"field" |
+ type:"blur" // blur! |
+ value:"value" |
+ keyCode:web::WebStateObserver:: |
+ kInvalidFormKeyCode |
+ inputMissing:false]; |
+ EXPECT_FALSE(GetSuggestionView(input_accessory_view_)); |
+} |
+ |
+// Tests that no suggestions are displayed when no providers are registered. |
+TEST_F(FormSuggestionControllerTest, |
+ FormActivityShouldRetrieveSuggestions_NoProvidersAvailable) { |
+ // Set up the controller without any providers. |
+ SetUpController(@[]); |
+ SetCurrentUrl("http://foo.com"); |
+ [accessory_controller_ webState:web_state() |
+ didRegisterFormActivityWithFormNamed:"form" |
+ fieldName:"field" |
+ type:"type" |
+ value:"value" |
+ keyCode:web::WebStateObserver:: |
+ kInvalidFormKeyCode |
+ inputMissing:false]; |
+ |
+ // The suggestions accessory view should be empty. |
+ FormSuggestionView* suggestionView = GetSuggestionView(input_accessory_view_); |
+ EXPECT_TRUE(suggestionView); |
+ EXPECT_EQ(0U, [suggestionView.suggestions count]); |
+} |
+ |
+// Tests that, when no providers have suggestions to offer for a form/field, |
+// they aren't asked and no suggestions are displayed. |
+TEST_F(FormSuggestionControllerTest, |
+ FormActivityShouldRetrieveSuggestions_NoSuggestionsAvailable) { |
+ // Set up the controller with some providers, but none of them will |
+ // have suggestions available. |
+ TestSuggestionProvider* provider1 = |
+ [[TestSuggestionProvider alloc] initWithSuggestions:@[]]; |
+ TestSuggestionProvider* provider2 = |
+ [[TestSuggestionProvider alloc] initWithSuggestions:@[]]; |
+ SetUpController(@[ provider1, provider2 ]); |
+ SetCurrentUrl("http://foo.com"); |
+ |
+ [accessory_controller_ webState:web_state() |
+ didRegisterFormActivityWithFormNamed:"form" |
+ fieldName:"field" |
+ type:"type" |
+ value:"value" |
+ keyCode:web::WebStateObserver:: |
+ kInvalidFormKeyCode |
+ inputMissing:false]; |
+ |
+ // The providers should each be asked if they have suggestions for the |
+ // form in question. |
+ EXPECT_TRUE([provider1 askedIfSuggestionsAvailable]); |
+ EXPECT_TRUE([provider2 askedIfSuggestionsAvailable]); |
+ |
+ // Since none of the providers had suggestions available, none of them |
+ // should have been asked for suggestions. |
+ EXPECT_FALSE([provider1 askedForSuggestions]); |
+ EXPECT_FALSE([provider2 askedForSuggestions]); |
+ |
+ // The accessory view should be empty. |
+ FormSuggestionView* suggestionView = GetSuggestionView(input_accessory_view_); |
+ EXPECT_TRUE(suggestionView); |
+ EXPECT_EQ(0U, [suggestionView.suggestions count]); |
+} |
+ |
+// Tests that, once a provider is asked if it has suggestions for a form/field, |
+// it and only it is asked to provide them, and that they are then displayed |
+// in the keyboard accessory view. |
+TEST_F(FormSuggestionControllerTest, |
+ FormActivityShouldRetrieveSuggestions_SuggestionsAddedToAccessoryView) { |
+ // Set up the controller with some providers, one of which can provide |
+ // suggestions. |
+ NSArray* suggestions = @[ |
+ [FormSuggestion suggestionWithValue:@"foo" |
+ displayDescription:nil |
+ icon:@"" |
+ identifier:0], |
+ [FormSuggestion suggestionWithValue:@"bar" |
+ displayDescription:nil |
+ icon:@"" |
+ identifier:1] |
+ ]; |
+ TestSuggestionProvider* provider1 = |
+ [[TestSuggestionProvider alloc] initWithSuggestions:suggestions]; |
+ TestSuggestionProvider* provider2 = |
+ [[TestSuggestionProvider alloc] initWithSuggestions:@[]]; |
+ SetUpController(@[ provider1, provider2 ]); |
+ SetCurrentUrl("http://foo.com"); |
+ |
+ [accessory_controller_ webState:web_state() |
+ didRegisterFormActivityWithFormNamed:"form" |
+ fieldName:"field" |
+ type:"type" |
+ value:"value" |
+ keyCode:web::WebStateObserver:: |
+ kInvalidFormKeyCode |
+ inputMissing:false]; |
+ |
+ // Since the first provider has suggestions available, it and only it |
+ // should have been asked. |
+ EXPECT_TRUE([provider1 askedIfSuggestionsAvailable]); |
+ EXPECT_FALSE([provider2 askedIfSuggestionsAvailable]); |
+ |
+ // Since the first provider said it had suggestions, it and only it |
+ // should have been asked to provide them. |
+ EXPECT_TRUE([provider1 askedForSuggestions]); |
+ EXPECT_FALSE([provider2 askedForSuggestions]); |
+ |
+ // The accessory view should show the suggestions. |
+ FormSuggestionView* suggestionView = GetSuggestionView(input_accessory_view_); |
+ EXPECT_TRUE(suggestionView); |
+ EXPECT_NSEQ(suggestions, suggestionView.suggestions); |
+} |
+ |
+// Tests that selecting a suggestion from the accessory view informs the |
+// specified delegate for that suggestion. |
+TEST_F(FormSuggestionControllerTest, SelectingSuggestionShouldNotifyDelegate) { |
+ // Send some suggestions to the controller and then tap one. |
+ NSArray* suggestions = @[ |
+ [FormSuggestion suggestionWithValue:@"foo" |
+ displayDescription:nil |
+ icon:@"" |
+ identifier:0], |
+ ]; |
+ TestSuggestionProvider* provider = |
+ [[TestSuggestionProvider alloc] initWithSuggestions:suggestions]; |
+ SetUpController(@[ provider ]); |
+ SetCurrentUrl("http://foo.com"); |
+ [accessory_controller_ webState:web_state() |
+ didRegisterFormActivityWithFormNamed:"form" |
+ fieldName:"field" |
+ type:"type" |
+ value:"value" |
+ keyCode:web::WebStateObserver:: |
+ kInvalidFormKeyCode |
+ inputMissing:false]; |
+ |
+ // Selecting a suggestion should notify the delegate. |
+ [suggestion_controller_ didSelectSuggestion:suggestions[0]]; |
+ EXPECT_TRUE([provider selected]); |
+ EXPECT_NSEQ(@"form", [provider formName]); |
+ EXPECT_NSEQ(@"field", [provider fieldName]); |
+ EXPECT_NSEQ(suggestions[0], [provider suggestion]); |
+} |
+ |
+} // namespace |