| Index: ios/chrome/browser/autofill/js_suggestion_manager_unittest.mm
 | 
| diff --git a/ios/chrome/browser/autofill/js_suggestion_manager_unittest.mm b/ios/chrome/browser/autofill/js_suggestion_manager_unittest.mm
 | 
| new file mode 100644
 | 
| index 0000000000000000000000000000000000000000..7f9c20ce3322763faa8f8d83a9c9638a43760a19
 | 
| --- /dev/null
 | 
| +++ b/ios/chrome/browser/autofill/js_suggestion_manager_unittest.mm
 | 
| @@ -0,0 +1,373 @@
 | 
| +// Copyright 2013 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 "components/autofill/ios/browser/js_suggestion_manager.h"
 | 
| +
 | 
| +#import <Foundation/Foundation.h>
 | 
| +
 | 
| +#include "base/strings/sys_string_conversions.h"
 | 
| +#import "base/test/ios/wait_util.h"
 | 
| +#include "ios/chrome/browser/web/chrome_web_test.h"
 | 
| +#import "ios/web/public/test/js_test_util.h"
 | 
| +#import "ios/web/public/test/web_js_test.h"
 | 
| +#import "ios/web/public/web_state/js/crw_js_injection_receiver.h"
 | 
| +#import "ios/web/public/web_state/web_state.h"
 | 
| +#import "testing/gtest_mac.h"
 | 
| +
 | 
| +#if !defined(__has_feature) || !__has_feature(objc_arc)
 | 
| +#error "This file requires ARC support."
 | 
| +#endif
 | 
| +
 | 
| +namespace {
 | 
| +
 | 
| +// Test fixture to test suggestions.
 | 
| +class JsSuggestionManagerTest : public web::WebJsTest<ChromeWebTest> {
 | 
| + protected:
 | 
| +  JsSuggestionManagerTest()
 | 
| +      : web::WebJsTest<ChromeWebTest>(@[ @"suggestion_controller" ]) {}
 | 
| +  // Loads the given HTML and initializes the Autofill JS scripts.
 | 
| +  void LoadHtml(NSString* html);
 | 
| +  // Helper method that initializes a form with three fields. Can be used to
 | 
| +  // test whether adding an attribute on the second field causes it to be
 | 
| +  // skipped (or not, as is appropriate) by selectNextElement.
 | 
| +  void SequentialNavigationSkipCheck(NSString* attribute, BOOL shouldSkip);
 | 
| +  // Returns the active element name from the JS side.
 | 
| +  NSString* GetActiveElementName() {
 | 
| +    return ExecuteJavaScript(@"document.activeElement.name");
 | 
| +  }
 | 
| +  JsSuggestionManager* manager_;
 | 
| +};
 | 
| +
 | 
| +void JsSuggestionManagerTest::LoadHtml(NSString* html) {
 | 
| +  WebJsTest<ChromeWebTest>::LoadHtml(html);
 | 
| +  manager_ =
 | 
| +      static_cast<JsSuggestionManager*>([web_state()->GetJSInjectionReceiver()
 | 
| +          instanceOfClass:[JsSuggestionManager class]]);
 | 
| +  [manager_ inject];
 | 
| +}
 | 
| +
 | 
| +TEST_F(JsSuggestionManagerTest, InitAndInject) {
 | 
| +  LoadHtml(@"<html></html>");
 | 
| +  EXPECT_TRUE([manager_ hasBeenInjected]);
 | 
| +}
 | 
| +
 | 
| +TEST_F(JsSuggestionManagerTest, SelectElementInTabOrder) {
 | 
| +  NSString* htmlFragment =
 | 
| +      @"<html> <body>"
 | 
| +       "<input id='1 (0)' tabIndex=1 href='http://www.w3schools.com'>1 (0)</a>"
 | 
| +       "<input id='0 (0)' tabIndex=0 href='http://www.w3schools.com'>0 (0)</a>"
 | 
| +       "<input id='2' tabIndex=2 href='http://www.w3schools.com'>2</a>"
 | 
| +       "<input id='0 (1)' tabIndex=0 href='http://www.w3schools.com'>0 (1)</a>"
 | 
| +       "<input id='-2' tabIndex=-2 href='http://www.w3schools.com'>-2</a>"
 | 
| +       "<a href='http://www.w3schools.com'></a>"
 | 
| +       "<input id='-1 (0)' tabIndex=-1 href='http://www.w3schools.com'>-1</a>"
 | 
| +       "<input id='-2 (2)' tabIndex=-2 href='http://www.w3schools.com'>-2</a>"
 | 
| +       "<input id='0 (2)' tabIndex=0 href='http://www.w3schools.com'>0 - 2</a>"
 | 
| +       "<input id='3' tabIndex=3 href='http://www.w3schools.com'>3</a>"
 | 
| +       "<input id='1 (1)' tabIndex=1 href='http://www.w3schools.com'>1 (1)</a>"
 | 
| +       "<input id='-1 (1)' tabIndex=-1 href='http://www.w3schools.com'>-1 </a>"
 | 
| +       "<input id='0 (3)' tabIndex=0 href='http://www.w3schools.com'>0 (3)</a>"
 | 
| +       "</body></html>";
 | 
| +  LoadHtmlAndInject(htmlFragment);
 | 
| +
 | 
| +  // clang-format off
 | 
| +  NSDictionary* next_expected_ids = @ {
 | 
| +      @"1 (0)"  : @"1 (1)",
 | 
| +      @"0 (0)"  : @"0 (1)",
 | 
| +      @"2"      : @"3",
 | 
| +      @"0 (1)"  : @"0 (2)",
 | 
| +      @"-2"     : @"0 (2)",
 | 
| +      @"-1 (0)" : @"0 (2)",
 | 
| +      @"-2 (2)" : @"0 (2)",
 | 
| +      @"0 (2)"  : @"0 (3)",
 | 
| +      @"3"      : @"0 (0)",
 | 
| +      @"1 (1)"  : @"2",
 | 
| +      @"-1 (1)" : @"0 (3)",
 | 
| +      @"0 (3)"  : @"null"
 | 
| +  };
 | 
| +  // clang-format on
 | 
| +
 | 
| +  for (NSString* element_id : next_expected_ids) {
 | 
| +    NSString* expected_id = [next_expected_ids objectForKey:element_id];
 | 
| +    EXPECT_NSEQ(expected_id,
 | 
| +                ExecuteJavaScriptWithFormat(
 | 
| +                    @"var elements=document.getElementsByTagName('input');"
 | 
| +                     "var element=document.getElementById('%@');"
 | 
| +                     "var next = __gCrWeb.suggestion.getNextElementInTabOrder("
 | 
| +                     "    element, elements);"
 | 
| +                     "next ? next.id : 'null';",
 | 
| +                    element_id))
 | 
| +        << "Wrong when selecting next element of element with element id "
 | 
| +        << base::SysNSStringToUTF8(element_id);
 | 
| +  }
 | 
| +  EXPECT_NSEQ(@YES,
 | 
| +              ExecuteJavaScriptWithFormat(
 | 
| +                  @"var elements=document.getElementsByTagName('input');"
 | 
| +                   "var element=document.getElementsByTagName('a')[0];"
 | 
| +                   "var next = __gCrWeb.suggestion.getNextElementInTabOrder("
 | 
| +                   "    element, elements); next===null"))
 | 
| +      << "Wrong when selecting the next element of an element not in the "
 | 
| +      << "element list.";
 | 
| +
 | 
| +  for (NSString* element_id : next_expected_ids) {
 | 
| +    NSString* expected_id = [next_expected_ids objectForKey:element_id];
 | 
| +    if ([expected_id isEqualToString:@"null"]) {
 | 
| +      // If the expected next element is null, the focus is not moved.
 | 
| +      expected_id = element_id;
 | 
| +    }
 | 
| +    EXPECT_NSEQ(expected_id, ExecuteJavaScriptWithFormat(
 | 
| +                                 @"document.getElementById('%@').focus();"
 | 
| +                                  "__gCrWeb.suggestion.selectNextElement();"
 | 
| +                                  "document.activeElement.id",
 | 
| +                                 element_id))
 | 
| +        << "Wrong when selecting next element with active element "
 | 
| +        << base::SysNSStringToUTF8(element_id);
 | 
| +  }
 | 
| +
 | 
| +  for (NSString* element_id : next_expected_ids) {
 | 
| +    // If the expected next element is null, there is no next element.
 | 
| +    BOOL expected = ![next_expected_ids[element_id] isEqualToString:@"null"];
 | 
| +    EXPECT_NSEQ(@(expected), ExecuteJavaScriptWithFormat(
 | 
| +                                 @"document.getElementById('%@').focus();"
 | 
| +                                  "__gCrWeb.suggestion.hasNextElement()",
 | 
| +                                 element_id))
 | 
| +        << "Wrong when checking hasNextElement() for "
 | 
| +        << base::SysNSStringToUTF8(element_id);
 | 
| +  }
 | 
| +
 | 
| +  // clang-format off
 | 
| +  NSDictionary* prev_expected_ids = @{
 | 
| +      @"1 (0)" : @"null",
 | 
| +      @"0 (0)" : @"3",
 | 
| +      @"2"     : @"1 (1)",
 | 
| +      @"0 (1)" : @"0 (0)",
 | 
| +      @"-2"    : @"0 (1)",
 | 
| +      @"-1 (0)": @"0 (1)",
 | 
| +      @"-2 (2)": @"0 (1)",
 | 
| +      @"0 (2)" : @"0 (1)",
 | 
| +      @"3"     : @"2",
 | 
| +      @"1 (1)" : @"1 (0)",
 | 
| +      @"-1 (1)": @"1 (1)",
 | 
| +      @"0 (3)" : @"0 (2)",
 | 
| +  };
 | 
| +  // clang-format on
 | 
| +
 | 
| +  for (NSString* element_id : prev_expected_ids) {
 | 
| +    NSString* expected_id = [prev_expected_ids objectForKey:element_id];
 | 
| +    EXPECT_NSEQ(
 | 
| +        expected_id,
 | 
| +        ExecuteJavaScriptWithFormat(
 | 
| +            @"var elements=document.getElementsByTagName('input');"
 | 
| +             "var element=document.getElementById('%@');"
 | 
| +             "var prev = __gCrWeb.suggestion.getPreviousElementInTabOrder("
 | 
| +             "    element, elements);"
 | 
| +             "prev ? prev.id : 'null';",
 | 
| +            element_id))
 | 
| +        << "Wrong when selecting prev element of element with element id "
 | 
| +        << base::SysNSStringToUTF8(element_id);
 | 
| +  }
 | 
| +  EXPECT_NSEQ(
 | 
| +      @YES, ExecuteJavaScriptWithFormat(
 | 
| +                @"var elements=document.getElementsByTagName('input');"
 | 
| +                 "var element=document.getElementsByTagName('a')[0];"
 | 
| +                 "var prev = __gCrWeb.suggestion.getPreviousElementInTabOrder("
 | 
| +                 "    element, elements); prev===null"))
 | 
| +      << "Wrong when selecting the previous element of an element not in the "
 | 
| +      << "element list";
 | 
| +
 | 
| +  for (NSString* element_id : prev_expected_ids) {
 | 
| +    NSString* expected_id = [prev_expected_ids objectForKey:element_id];
 | 
| +    if ([expected_id isEqualToString:@"null"]) {
 | 
| +      // If the expected previous element is null, the focus is not moved.
 | 
| +      expected_id = element_id;
 | 
| +    }
 | 
| +    EXPECT_NSEQ(expected_id, ExecuteJavaScriptWithFormat(
 | 
| +                                 @"document.getElementById('%@').focus();"
 | 
| +                                  "__gCrWeb.suggestion.selectPreviousElement();"
 | 
| +                                  "document.activeElement.id",
 | 
| +                                 element_id))
 | 
| +        << "Wrong when selecting previous element with active element "
 | 
| +        << base::SysNSStringToUTF8(element_id);
 | 
| +  }
 | 
| +
 | 
| +  for (NSString* element_id : prev_expected_ids) {
 | 
| +    // If the expected next element is null, there is no next element.
 | 
| +    BOOL expected = ![prev_expected_ids[element_id] isEqualToString:@"null"];
 | 
| +    EXPECT_NSEQ(@(expected), ExecuteJavaScriptWithFormat(
 | 
| +                                 @"document.getElementById('%@').focus();"
 | 
| +                                  "__gCrWeb.suggestion.hasPreviousElement()",
 | 
| +                                 element_id))
 | 
| +        << "Wrong when checking hasPreviousElement() for "
 | 
| +        << base::SysNSStringToUTF8(element_id);
 | 
| +  }
 | 
| +}
 | 
| +
 | 
| +TEST_F(JsSuggestionManagerTest, SequentialNavigation) {
 | 
| +  LoadHtml(@"<html><body><form name='testform' method='post'>"
 | 
| +            "<input type='text' name='firstname'/>"
 | 
| +            "<input type='text' name='lastname'/>"
 | 
| +            "<input type='email' name='email'/>"
 | 
| +            "</form></body></html>");
 | 
| +
 | 
| +  [manager_
 | 
| +      executeJavaScript:@"document.getElementsByName('firstname')[0].focus()"
 | 
| +      completionHandler:nil];
 | 
| +  [manager_ selectNextElement];
 | 
| +  EXPECT_NSEQ(@"lastname", GetActiveElementName());
 | 
| +  __block BOOL block_was_called = NO;
 | 
| +  [manager_ fetchPreviousAndNextElementsPresenceWithCompletionHandler:^void(
 | 
| +                BOOL has_previous_element, BOOL has_next_element) {
 | 
| +    block_was_called = YES;
 | 
| +    EXPECT_TRUE(has_previous_element);
 | 
| +    EXPECT_TRUE(has_next_element);
 | 
| +  }];
 | 
| +  base::test::ios::WaitUntilCondition(^bool() {
 | 
| +    return block_was_called;
 | 
| +  });
 | 
| +  [manager_ selectNextElement];
 | 
| +  EXPECT_NSEQ(@"email", GetActiveElementName());
 | 
| +  [manager_ selectPreviousElement];
 | 
| +  EXPECT_NSEQ(@"lastname", GetActiveElementName());
 | 
| +}
 | 
| +
 | 
| +void JsSuggestionManagerTest::SequentialNavigationSkipCheck(NSString* attribute,
 | 
| +                                                            BOOL shouldSkip) {
 | 
| +  LoadHtml([NSString stringWithFormat:@"<html><body>"
 | 
| +                                       "<form name='testform' method='post'>"
 | 
| +                                       "<input type='text' name='firstname'/>"
 | 
| +                                       "<%@ name='middlename'/>"
 | 
| +                                       "<input type='text' name='lastname'/>"
 | 
| +                                       "</form></body></html>",
 | 
| +                                      attribute]);
 | 
| +  [manager_
 | 
| +      executeJavaScript:@"document.getElementsByName('firstname')[0].focus()"
 | 
| +      completionHandler:nil];
 | 
| +  NSString* const kActiveElementNameJS = @"document.activeElement.name";
 | 
| +  EXPECT_NSEQ(@"firstname",
 | 
| +              web::ExecuteJavaScript(manager_, kActiveElementNameJS));
 | 
| +  [manager_ selectNextElement];
 | 
| +  NSString* activeElementNameJS = GetActiveElementName();
 | 
| +  if (shouldSkip)
 | 
| +    EXPECT_NSEQ(@"lastname", activeElementNameJS);
 | 
| +  else
 | 
| +    EXPECT_NSEQ(@"middlename", activeElementNameJS);
 | 
| +}
 | 
| +
 | 
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationNoSkipText) {
 | 
| +  SequentialNavigationSkipCheck(@"input type='text'", NO);
 | 
| +}
 | 
| +
 | 
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationNoSkipTextArea) {
 | 
| +  SequentialNavigationSkipCheck(@"input type='textarea'", NO);
 | 
| +}
 | 
| +
 | 
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationOverInvisibleElement) {
 | 
| +  SequentialNavigationSkipCheck(@"input type='text' style='display:none'", YES);
 | 
| +}
 | 
| +
 | 
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationOverHiddenElement) {
 | 
| +  SequentialNavigationSkipCheck(@"input type='text' style='visibility:hidden'",
 | 
| +                                YES);
 | 
| +}
 | 
| +
 | 
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationOverDisabledElement) {
 | 
| +  SequentialNavigationSkipCheck(@"type='text' disabled", YES);
 | 
| +}
 | 
| +
 | 
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationNoSkipPassword) {
 | 
| +  SequentialNavigationSkipCheck(@"input type='password'", NO);
 | 
| +}
 | 
| +
 | 
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationSkipSubmit) {
 | 
| +  SequentialNavigationSkipCheck(@"input type='submit'", YES);
 | 
| +}
 | 
| +
 | 
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationSkipImage) {
 | 
| +  SequentialNavigationSkipCheck(@"input type='image'", YES);
 | 
| +}
 | 
| +
 | 
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationSkipButton) {
 | 
| +  SequentialNavigationSkipCheck(@"input type='button'", YES);
 | 
| +}
 | 
| +
 | 
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationSkipRange) {
 | 
| +  SequentialNavigationSkipCheck(@"input type='range'", YES);
 | 
| +}
 | 
| +
 | 
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationSkipRadio) {
 | 
| +  SequentialNavigationSkipCheck(@"type='radio'", YES);
 | 
| +}
 | 
| +
 | 
| +TEST_F(JsSuggestionManagerTest, SequentialNavigationSkipCheckbox) {
 | 
| +  SequentialNavigationSkipCheck(@"type='checkbox'", YES);
 | 
| +}
 | 
| +
 | 
| +// Special test for a condition where the closeKeyboard script would cause an
 | 
| +// illegal JS recursion if a blur event results in an event that triggers a
 | 
| +// crwebinvoke:// back, such as a page change.
 | 
| +TEST_F(JsSuggestionManagerTest, CloseKeyboardSafetyTest) {
 | 
| +  LoadHtml(@"<select id='select'>Select</select>");
 | 
| +  ExecuteJavaScript(
 | 
| +      @"select.onblur = function(){window.location.href = '#test'}");
 | 
| +  ExecuteJavaScript(@"select.focus()");
 | 
| +  // In the failure condition the app will crash during the next line.
 | 
| +  [manager_ closeKeyboard];
 | 
| +  // TODO(crbug.com/661624): add a check for the keyboard actually being
 | 
| +  // dismissed; unfortunately it is not known how to adapt
 | 
| +  // WaitForBackgroundTasks to yield for events wrapped with window.setTimeout()
 | 
| +  // or other deferred events.
 | 
| +}
 | 
| +
 | 
| +// Test fixture to test
 | 
| +// |fetchPreviousAndNextElementsPresenceWithCompletionHandler|.
 | 
| +class FetchPreviousAndNextExceptionTest : public JsSuggestionManagerTest {
 | 
| + public:
 | 
| +  void SetUp() override {
 | 
| +    JsSuggestionManagerTest::SetUp();
 | 
| +    LoadHtml(@"<html></html>");
 | 
| +  }
 | 
| +
 | 
| + protected:
 | 
| +  // Evaluates JS and tests that the completion handler passed to
 | 
| +  // |fetchPreviousAndNextElementsPresenceWithCompletionHandler| is called with
 | 
| +  // (NO, NO) indicating no previous and next element.
 | 
| +  void EvaluateJavaScriptAndExpectNoPreviousAndNextElement(NSString* js) {
 | 
| +    ExecuteJavaScript(js);
 | 
| +    __block BOOL block_was_called = NO;
 | 
| +    id completionHandler = ^(BOOL hasPreviousElement, BOOL hasNextElement) {
 | 
| +      EXPECT_FALSE(hasPreviousElement);
 | 
| +      EXPECT_FALSE(hasNextElement);
 | 
| +      block_was_called = YES;
 | 
| +    };
 | 
| +    [manager_ fetchPreviousAndNextElementsPresenceWithCompletionHandler:
 | 
| +                  completionHandler];
 | 
| +    base::test::ios::WaitUntilCondition(^bool() {
 | 
| +      return block_was_called;
 | 
| +    });
 | 
| +  }
 | 
| +};
 | 
| +
 | 
| +// Tests that |fetchPreviousAndNextElementsPresenceWithCompletionHandler| works
 | 
| +// when |__gCrWeb.suggestion.hasPreviousElement| throws an exception.
 | 
| +TEST_F(FetchPreviousAndNextExceptionTest, HasPreviousElementException) {
 | 
| +  EvaluateJavaScriptAndExpectNoPreviousAndNextElement(
 | 
| +      @"__gCrWeb.suggestion.hasPreviousElement = function() { bar.foo1; }");
 | 
| +}
 | 
| +
 | 
| +// Tests that |fetchPreviousAndNextElementsPresenceWithCompletionHandler| works
 | 
| +// when |__gCrWeb.suggestion.hasNextElement| throws an exception.
 | 
| +TEST_F(FetchPreviousAndNextExceptionTest, HasNextElementException) {
 | 
| +  EvaluateJavaScriptAndExpectNoPreviousAndNextElement(
 | 
| +      @"__gCrWeb.suggestion.hasNextElement = function() { bar.foo1; }");
 | 
| +}
 | 
| +
 | 
| +// Tests that |fetchPreviousAndNextElementsPresenceWithCompletionHandler| works
 | 
| +// when |Array.toString| has been overridden to return a malformed string
 | 
| +// without a ",".
 | 
| +TEST_F(FetchPreviousAndNextExceptionTest, HasPreviousElementNull) {
 | 
| +  EvaluateJavaScriptAndExpectNoPreviousAndNextElement(
 | 
| +      @"Array.prototype.toString = function() { return 'Hello'; }");
 | 
| +}
 | 
| +
 | 
| +}  // namespace
 | 
| 
 |