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