| 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
 | 
| 
 |