chromium/ios/chrome/browser/autofill/model/autofill_controller_js_unittest.mm

// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import <UIKit/UIKit.h>

#import "base/format_macros.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "components/autofill/core/common/autofill_constants.h"
#import "components/autofill/ios/browser/autofill_java_script_feature.h"
#import "components/autofill/ios/form_util/form_util_java_script_feature.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/web/model/chrome_web_client.h"
#import "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/test/js_test_util.h"
#import "ios/web/public/test/scoped_testing_web_client.h"
#import "ios/web/public/test/web_state_test_util.h"
#import "ios/web/public/test/web_task_environment.h"
#import "ios/web/public/test/web_view_interaction_test_util.h"
#import "ios/web/public/web_state.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"

// Unit tests for ios/chrome/browser/web/model/resources/autofill_controller.js
namespace {

using base::test::ios::kWaitForJSCompletionTimeout;
using base::test::ios::WaitUntilConditionOrTimeout;

// Structure for getting element by name using JavaScripts.
struct ElementByName {
  // The name of the element.
  const char* element_name;
  // The index in the elements that have `element_name`.
  const int index;
  // The option index if the element is a select, -1 otherwise.
  const int option_index;
};

NSString* GetDefaultMaxLengthString() {
  return @"524288";
}

NSNumber* GetDefaultMaxLength() {
  return @524288;
}

// Generates the JavaScript that gets an element by name.
NSString* GetElementByNameJavaScript(ElementByName element) {
  NSString* query =
      [NSString stringWithFormat:@"window.document.getElementsByName('%s')[%d]",
                                 element.element_name, element.index];
  if (element.option_index >= 0) {
    query =
        [query stringByAppendingFormat:@".options[%d]", element.option_index];
  }
  return query;
}

// Generates an array of JavaScripts that get each element in `elements` by
// name.
NSArray* GetElementsByNameJavaScripts(const ElementByName elements[],
                                      size_t elements_size) {
  NSMutableArray* array = [NSMutableArray array];
  for (size_t i = 0; i < elements_size; ++i) {
    NSString* query = GetElementByNameJavaScript(elements[i]);
    [array addObject:query];
  }
  return array;
}

// clang-format off
NSString* kHTMLForTestingElements = @"<html><body>"
    "<input type=hidden name='gl' value='us'>"
    "<form name='testform'>"
    "  <input type=hidden name='hl' value='en'>"
    "  <input type='text' name='firstname'>"
    "  <input type='text' name='lastname'>"
    "  <input type='email' name='email'>"
    "  <input type='tel' name='phone'>"
    "  <input type='url' autocomplete='off' name='blog'>"
    "  <input type='number' name='expected number of clicks'>"
    "  <input type='password' autocomplete='off' name='pwd'>"
    "  <input type='checkbox' name='vehicle' value='Bike'>"
    "  <input type='checkbox' name='vehicle' value='Car'>"
    "  <input type='checkbox' name='vehicle' value='Rocket'>"
    "  <input type='radio' name='boolean' value='true'>"
    "  <input type='radio' name='boolean' value='false'>"
    "  <input type='radio' name='boolean' value='other'>"
    "  <label>State:"
    "    <select name='state'>"
    "      <option value='CA'>CA</option>"
    "      <option value='MA'>MA</option>"
    "    </select>"
    "   </label>"
    "  <label>Course:"
    "    <select name='course'>"
    "     <optgroup label='8.01 Physics I: Classical Mechanics'>"
    "      <option value='8.01.1'>Lecture 01: Powers of Ten"
    "      <option value='8.01.2'>Lecture 02: 1D Kinematics"
    "      <option value='8.01.3'>Lecture 03: Vectors"
    "     <optgroup label='8.02 Electricity and Magnestism'>"
    "      <option value='8.02.1'>Lecture 01: What holds our world together?"
    "      <option value='8.02.2'>Lecture 02: Electric Field"
    "      <option value='8.02.3'>Lecture 03: Electric Flux"
    "    </select>"
    "  </label>"
    "  <label>Cars:"
    "    <select name='cars' multiple>"
    "      <option value='volvo'>Volvo</option>"
    "      <option value='saab'>Saab</option>"
    "      <option value='opel'>Opel</option>"
    "      <option value='audi'>Audi</option>"
    "    </select>"
    "   </label>"
    "  <input type='submit' name='submit' value='Submit'>"
    "</form>"
    "</body></html>";
// clang-format on

// A bit field mask to extract data from WebFormControlElement. They are from
// autofill_controller.js
enum ExtractMask {
  EXTRACT_NONE = 0,
  EXTRACT_VALUE = 1 << 0,        // Extract value from WebFormControlElement.
  EXTRACT_OPTION_TEXT = 1 << 1,  // Extract option text from
                                 // WebFormSelectElement. Only valid when
                                 // `EXTRACT_VALUE` is set.
                                 // This is used for form submission where
                                 // human readable value is captured.
  EXTRACT_OPTIONS = 1 << 2,      // Extract options from
                                 // WebFormControlElement.
};

// Gets the attributes to check.
NSArray* GetFormFieldAttributeListsToCheck() {
  return @[
    @"identifier", @"name", @"form_control_type", @"autocomplete_attribute",
    @"max_length", @"should_autocomplete", @"is_checkable", @"value",
    @"option_values", @"option_texts"
  ];
}

// ***** Clang-formatting is disabled for the following block of testdata *****
// clang-format off

// Getters for form control element testing data. The returned data is
// an array consisting of an html fragment followed by an array
// of dictionaries containing the expected field attributes for elements in the
// given html fragment.
NSArray* GetTestFormInputElementWithLabelFromPrevious() {
  return @[
      @("* First name: "
          "<INPUT type='text' name='firstname' id='firstname' value='John'/>"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'* First name:'", @"label",
          @"'firstname'", @"identifier",
          @"'firstname'", @"name",
          @"'text'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          GetDefaultMaxLengthString(), @"max_length",
          @"true", @"should_autocomplete",
          @"false", @"is_checkable",
          @"'John'", @"value",
          @"'John'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil]];
}

NSArray* GetTestFormInputElementWithLabelFromEnclosingLabelBefore() {
  return @[
      @("<LABEL>* First name: "
          "<INPUT type='text' name='firstname' id='firstname' value='John'/>"
          "</LABEL>"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'* First name:'", @"label",
          @"'firstname'", @"name",
          @"'firstname'", @"identifier",
          @"'text'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          GetDefaultMaxLengthString(), @"max_length",
          @"true", @"should_autocomplete",
          @"false", @"is_checkable",
          @"'John'", @"value",
          @"'John'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil]];
}

NSArray* GetTestFormInputElementWithLabelFromPreviousSpan() {
  return @[
      @("* Last name<span>:</span> "
          "<INPUT type='text' name='lastname' id='lastname' value='John'/>"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'* Last name:'", @"label",
          @"'lastname'", @"identifier",
          @"'lastname'", @"name",
          @"'text'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          GetDefaultMaxLengthString(), @"max_length",
          @"true", @"should_autocomplete",
          @"false", @"is_checkable",
          @"'John'", @"value",
          @"'John'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil]];
}

NSArray* GetTestFormInputElementWithLabelFromPreviousParagraph() {
  return @[
      @("<p>* Email:</p> "
          "<INPUT type='email' name='email' id='email' "
          "value='[email protected]'/>"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'* Email:'", @"label",
          @"'email'", @"identifier",
          @"'email'", @"name",
          @"'email'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          GetDefaultMaxLengthString(), @"max_length",
          @"true", @"should_autocomplete",
          @"false", @"is_checkable",
          @"'[email protected]'", @"value",
          @"'[email protected]'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil]];
}

NSArray* GetTestFormInputElementWithLabelFromPreviousLabel() {
  return @[
      @("<label>* Telephone: </label> "
          "<INPUT type='tel' id='telephone' value='12345678'/>"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'* Telephone:'", @"label",
          @"'telephone'", @"identifier",
          @"'telephone'", @"name",
          @"'tel'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          GetDefaultMaxLengthString(), @"max_length",
          @"true", @"should_autocomplete",
          @"false", @"is_checkable",
          @"'12345678'", @"value",
          @"'12345678'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil]];
}

NSArray* GetTestFormInputElementWithLabelFromPreviousLabelOtherIgnored() {
  return @[
      @("Other Text <label>* Blog:</label> "
          "<INPUT type='url' id='blog' autocomplete='off' "
          "value='www.jogh.blog'/>"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'* Blog:'", @"label",
          @"'blog'", @"identifier",
          @"'blog'", @"name",
          @"'url'", @"form_control_type",
          @"'off'", @"autocomplete_attribute",
          GetDefaultMaxLengthString(), @"max_length",
          @"false", @"should_autocomplete",
          @"false", @"is_checkable",
          @"'www.jogh.blog'", @"value",
          @"'www.jogh.blog'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil]];
}

NSArray* GetTestFormInputElementWithLabelFromPreviousTextSpanBr() {
  return @[
      @("* Expected visits<span>:</span> <br>"
          "<INPUT type='number' id='number' "
          "name='expected number of clicks'/>"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'* Expected visits:'", @"label",
          @"'number'", @"identifier",
          @"'expected number of clicks'", @"name",
          @"'number'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          GetDefaultMaxLengthString(), @"max_length",
          @"true", @"should_autocomplete",
          @"false", @"is_checkable",
          @"''", @"value",
          @"''", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil]];
}

NSArray* GetTestFormInputElementWithLabelFromPreviousTextBrAndSpan() {
  return @[
      @("Other <br> * Password<span>:</span> "
          "<INPUT type='password' autocomplete='off' name='pwd' id='pwd'/>"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'* Password:'", @"label",
          @"'pwd'", @"identifier",
          @"'pwd'", @"name",
          @"'password'", @"form_control_type",
          @"'off'", @"autocomplete_attribute",
          GetDefaultMaxLengthString(), @"max_length",
          @"false", @"should_autocomplete",
          @"false", @"is_checkable",
          @"''", @"value",
          @"''", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil]];
}

NSArray* GetTestFormInputElementWithLabelFromListItem() {
  return @[
      @("<LI>"
          "<LABEL><EM>*</EM> Code:</LABEL>"
          "<INPUT type='text' id='first code' value='415'/>"
          "<INPUT type='text' id='middle code' value='555'/>"
          "<INPUT type='text' id='last code' value='1212'/>"
          "</LI>"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'* Code:'", @"label",
          @"'first code'", @"identifier",
          @"'first code'", @"name",
          @"'text'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          GetDefaultMaxLengthString(), @"max_length",
          @"true", @"should_autocomplete",
          @"false", @"is_checkable",
          @"'415'", @"value",
          @"'415'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil],
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'* Code:'", @"label",
          @"'middle code'", @"identifier",
          @"'middle code'", @"name",
          @"'text'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          GetDefaultMaxLengthString(), @"max_length",
          @"true", @"should_autocomplete",
          @"false", @"is_checkable",
          @"'555'", @"value",
          @"'555'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil],
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'* Code:'", @"label",
          @"'last code'", @"identifier",
          @"'last code'", @"name",
          @"'text'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          GetDefaultMaxLengthString(), @"max_length",
          @"true", @"should_autocomplete",
          @"false", @"is_checkable",
          @"'1212'", @"value",
          @"'1212'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil]];
}

NSArray* GetTestFormInputElementWithLabelFromTableColumnTD() {
  return @[
      @("<TABLE>"
          "<TR>"
          "  <TD>* First name:</TD>"
          "  <TD><INPUT type='text' id='tabletdname' value='John'/></TD>"
          "</TR>"
          "<TR>"
          "  <TD>Email:</TD>"
          "  <TD><INPUT type='email' id='tabletdemail'"
          "             value='[email protected]'/></TD>"
          "</TR>"
          "</TABLE>"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'* First name:'", @"label",
          @"'tabletdname'", @"identifier",
          @"'tabletdname'", @"name",
          @"'text'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          GetDefaultMaxLengthString(), @"max_length",
          @"true", @"should_autocomplete",
          @"false", @"is_checkable",
          @"'John'", @"value",
          @"'John'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil],
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'Email:'", @"label",
          @"'tabletdemail'", @"identifier",
          @"'tabletdemail'", @"name",
          @"'email'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          GetDefaultMaxLengthString(), @"max_length",
          @"true", @"should_autocomplete",
          @"false", @"is_checkable",
          @"'[email protected]'", @"value",
          @"'[email protected]'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil]];
}

NSArray* GetTestFormInputElementWithLabelFromTableColumnTH() {
  return @[
      @("<TABLE>"
          "<TR>"
          "  <TH>* First name:</TH>"
          "  <TD><INPUT type='text' name='nameintableth' id='nameintableth'"
          "             value='John'/></TD>"
          "</TR>"
          "<TR>"
          "  <TD>Email:</TD>"
          "  <TD><INPUT type='email' id='emailtableth'"
          "             value='[email protected]'/></TD>"
          "</TR>"
          "</TABLE>"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'* First name:'", @"label",
          @"'nameintableth'", @"identifier",
          @"'nameintableth'", @"name",
          @"'text'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          GetDefaultMaxLengthString(), @"max_length",
          @"true", @"should_autocomplete",
          @"false", @"is_checkable",
          @"'John'", @"value",
          @"'John'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil],
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'Email:'", @"label",
          @"'emailtableth'",  @"identifier",
          @"'emailtableth'",  @"name",
          @"'email'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          GetDefaultMaxLengthString(), @"max_length",
          @"true", @"should_autocomplete",
          @"false", @"is_checkable",
          @"'[email protected]'", @"value",
          @"'[email protected]'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil]];
}

NSArray* GetTestFormInputElementWithLabelFromTableNested() {
  return @[
      @("<TABLE>"
          "<TR>"
          "  <TD><FONT>* First </FONT><FONT>name:</FONT></TD>"
          "  <TD><INPUT type='text' id='nametablenested' value='John'/></TD>"
          "</TR>"
          "</TABLE>"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'* First name:'", @"label",
          @"'nametablenested'", @"identifier",
          @"'nametablenested'", @"name",
          @"'text'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          GetDefaultMaxLengthString(), @"max_length",
          @"true", @"should_autocomplete",
          @"false", @"is_checkable",
          @"'John'", @"value",
          @"'John'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil]];
}

NSArray* GetTestFormInputElementWithLabelFromTableRow() {
  return @[
      @("<TABLE>"
          "<TR>"
          "  <TD>* <FONT>First </FONT><FONT>name:</FONT></TD>"
          "</TR>"
          "<TR>"
          "  <TD><INPUT type='text'  name='nametablerow'  id='nametablerow'"
          "             value='John'/></TD>"
          "</TR>"
          "</TABLE>"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'* First name:'", @"label",
          @"'nametablerow'", @"identifier",
          @"'nametablerow'", @"name",
          @"'text'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          GetDefaultMaxLengthString(), @"max_length",
          @"true", @"should_autocomplete",
          @"false", @"is_checkable",
          @"'John'", @"value",
          @"'John'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil]];
}

NSArray* GetTestFormInputElementWithLabelFromDivTable() {
  return @[
      @("<DIV>* First name:<BR>"
          "<SPAN>"
          "<INPUT type='text' name='namedivtable' id='namedivtable'"
          "       value='John'>"
          "</SPAN>"
          "</DIV>"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'* First name:'", @"label",
          @"'namedivtable'", @"identifier",
          @"'namedivtable'", @"name",
          @"'text'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          GetDefaultMaxLengthString(), @"max_length",
          @"true", @"should_autocomplete",
          @"false", @"is_checkable",
          @"'John'", @"value",
          @"'John'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil]];
}

NSArray* GetTestFormInputElementWithLabelFromDefinitionList() {
  return @[
      @("<DL>"
          "  <DT>"
          "    <SPAN>"
          "      *"
          "    </SPAN>"
          "    <SPAN>"
          "      Favorite Sport"
          "    </SPAN>"
          "  </DT>"
          "  <DD>"
          "    <FONT>"
          "      <INPUT type='favorite sport' name='sport' id='sport'"
          "             value='Tennis'/>"
          "    </FONT>"
          "  </DD>"
          " </DL>"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'* Favorite Sport'", @"label",
          @"'sport'", @"identifier",
          @"'sport'", @"name",
          @"'text'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          GetDefaultMaxLengthString(), @"max_length",
          @"true", @"should_autocomplete",
          @"false", @"is_checkable",
          @"'Tennis'", @"value",
          @"'Tennis'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil]];
}

NSArray* GetTestInputRadio() {
  return @[
      @("<input type='radio' name='boolean'  id='boolean1' value='true'/> True"
          "<input type='radio' name='boolean'  id='boolean2' value='false'/> "
          "False"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'True'", @"label",
          @"'boolean1'", @"identifier",
          @"'boolean'", @"name",
          @"'radio'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"true", @"is_checkable",
          @"'true'", @"value",
          @"'true'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil],
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'False'", @"label",
          @"'boolean2'", @"identifier",
          @"'boolean'", @"name",
          @"'radio'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"true", @"is_checkable",
          @"'false'", @"value",
          @"'false'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil]];
}

NSArray* GetTestInputCheckbox() {
  return @[
      @("<input type='checkbox' name='vehicle' id='vehicle1' value='Bike'> "
          "Bicycle"
          "<input type='checkbox' name='vehicle' id='vehicle2' value='Car'> "
          "Automobile"
          "<input type='checkbox' name='vehicle' id='vehicle3' value='Rocket'> "
          "Missile"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'Bicycle'", @"label",
          @"'vehicle1'", @"identifier",
          @"'vehicle'", @"name",
          @"'checkbox'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"true", @"is_checkable",
          @"'Bike'", @"value",
          @"'Bike'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil],
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'Automobile'", @"label",
          @"'vehicle2'", @"identifier",
          @"'vehicle'", @"name",
          @"'checkbox'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"true", @"is_checkable",
          @"'Car'", @"value",
          @"'Car'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil],
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'Missile'", @"label",
          @"'vehicle3'", @"identifier",
          @"'vehicle'", @"name",
          @"'checkbox'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"true", @"is_checkable",
          @"'Rocket'", @"value",
          @"'Rocket'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          nil]];
}

NSArray* GetTestFormSelectElement() {
  return @[
      @("  <label>State:"
          "    <select name='state' id='state'>"
          "      <option value='CA'>California</option>"
          "      <option value='TX'>Texas</option>"
          "    </select>"
          "   </label>"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'State:'", @"label",
          @"'state'", @"identifier",
          @"'state'", @"name",
          @"'select-one'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"undefined", @"is_checkable",
          @"'CA'", @"value",
          @"'California'", @"value_option_text",
          @[@"'CA'", @"'TX'"], @"option_values",
          @[@"'California'", @"'Texas'"], @"option_texts",
          nil]];
}

NSArray* GetTestFormSelectElementWithOptgroup() {
  return @[
      @("  <label>Course:"
          "    <select name='course' id='course'>"
          "     <optgroup label='8.01 Physics I: Classical Mechanics'>"
          "      <option value='8.01.1'>Lecture 01: Powers of Ten"
          "      <option value='8.01.2'>Lecture 02: 1D Kinematics"
          "      <option value='8.01.3'>Lecture 03: Vectors"
          "     <optgroup label='8.02 Electricity and Magnestism'>"
          "      <option value='8.02.1'>Lecture 01: What holds world together?"
          "      <option value='8.02.2'>Lecture 02: Electric Field"
          "      <option value='8.02.3'>Lecture 03: Electric Flux"
          "    </select>"
          "  </label>"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'Course:'", @"label",
          @"'course'", @"identifier",
          @"'course'", @"name",
          @"'select-one'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"undefined", @"is_checkable",
          @"'8.01.1'", @"value",
          @"'Lecture 01: Powers of Ten'", @"value_option_text",
          @[@"'8.01.1'",
              @"'8.01.2'",
              @"'8.01.3'",
              @"'8.02.1'",
              @"'8.02.2'",
              @"'8.02.3'"], @"option_values",
          @[@"'Lecture 01: Powers of Ten'",
              @"'Lecture 02: 1D Kinematics'",
              @"'Lecture 03: Vectors'",
              @"'Lecture 01: What holds world together?'",
              @"'Lecture 02: Electric Field'",
              @"'Lecture 03: Electric Flux'"], @"option_texts",
          nil]];
}

// clang-format on

// Generates JavaScripts to check a JavaScripts object `results` with the
// expected values given in `expected`, which is a dictionary with string
// values for all the keys other than @"option_values" and @"option_texts";
// the values of @"option_values" and @"option_texts" are arrays of
// strings or undefined. Only attributes in `attributes_to_check` are checked.
// `index` is the index of the control element in the form. If it is >0, it will
// be used to generate a name for nameless elements.
NSString* GenerateElementItemVerifyingJavaScripts(NSString* results,
                                                  NSDictionary* expected,
                                                  NSArray* attributes_to_check,
                                                  int index) {
  NSMutableArray* verifying_javascripts = [NSMutableArray array];

  for (NSString* attribute in attributes_to_check) {
    if ([attribute isEqualToString:@"option_values"] ||
        [attribute isEqualToString:@"option_texts"]) {
      id expected_value = [expected objectForKey:attribute];
      if ([expected_value isKindOfClass:[NSString class]]) {
        [verifying_javascripts
            addObject:[NSString
                          stringWithFormat:@"%@['%@']===%@", results, attribute,
                                           [expected objectForKey:attribute]]];
      } else {
        for (NSUInteger i = 0; i < [(NSArray*)expected_value count]; ++i) {
          [verifying_javascripts
              addObject:[NSString
                            stringWithFormat:@"%@['%@'][%" PRIuNS "] === %@",
                                             results, attribute, i,
                                             [expected_value objectAtIndex:i]]];
        }
      }
    } else {
      NSString* expected_value = [expected objectForKey:attribute];
      if ([attribute isEqualToString:@"name"] &&
          [expected_value isEqualToString:@"''"] && index >= 0) {
        expected_value =
            [NSString stringWithFormat:@"'gChrome~field~%d'", index];
      }
      [verifying_javascripts
          addObject:[NSString stringWithFormat:@"%@['%@']===%@", results,
                                               attribute, expected_value]];
    }
  }

  return [verifying_javascripts componentsJoinedByString:@"&&"];
}

// Generates JavaScripts to check a JavaScripts array `results` with the
// expected values given in `expected`, which is an array of dictionaries; each
// dictionary is the expected values of the corresponding item in `results`.
// Only attributes in `attributes_to_check` are checked.
NSString* GenerateTestItemVerifyingJavaScripts(NSString* results,
                                               NSArray* expected,
                                               NSArray* attributed_to_check) {
  NSMutableArray* verifying_javascripts = [NSMutableArray array];

  NSUInteger controlCount = 0;
  for (NSUInteger indexOfTestData = 0; indexOfTestData < [expected count];
       ++indexOfTestData) {
    NSArray* expectedData = [expected objectAtIndex:indexOfTestData];
    for (NSUInteger i = 1; i < [expectedData count]; ++i, ++controlCount) {
      NSDictionary* expectedDict = [expectedData objectAtIndex:i];
      NSString* itemVerifyingJavaScripts =
          GenerateElementItemVerifyingJavaScripts(
              [NSString stringWithFormat:@"%@['fields'][%" PRIuNS "]", results,
                                         controlCount],
              expectedDict, attributed_to_check, controlCount);
      [verifying_javascripts addObject:itemVerifyingJavaScripts];
    }
  }
  return [verifying_javascripts componentsJoinedByString:@"&&"];
}

// Test fixture to test autofill controller.
class AutofillControllerJsTest : public PlatformTest {
 public:
  AutofillControllerJsTest()
      : web_client_(std::make_unique<ChromeWebClient>()) {
    browser_state_ = TestChromeBrowserState::Builder().Build();

    web::WebState::CreateParams params(browser_state_.get());
    web_state_ = web::WebState::Create(params);
    web_state_->GetView();
    web_state_->SetKeepRenderProcessAlive(true);
  }

 protected:
  web::WebState* web_state() { return web_state_.get(); }

  web::WebFrame* WaitForMainFrame() {
    __block web::WebFrame* main_frame = nullptr;
    EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
      web::WebFramesManager* frames_manager =
          autofill::FormUtilJavaScriptFeature::GetInstance()
              ->GetWebFramesManager(web_state());
      main_frame = frames_manager->GetMainWebFrame();
      return main_frame != nullptr;
    }));
    return main_frame;
  }

  // Helper method that EXPECTs `javascript` evaluation on page
  // `kHTMLForTestingElements` with expectation given by
  // `elements_with_true_expected`.
  void TestExecutingBooleanJavaScriptOnElement(
      NSString* javascript,
      const ElementByName elements_with_true_expected[],
      size_t size_elements_with_true_expected);

  // Helper method that EXPECTs
  // `__gCrWeb.fill.webFormControlElementToFormField`. This method applies
  // `__gCrWeb.fill.webFormControlElementToFormField` on each element in
  // `test_data` with all possible extract masks and verify the results.
  void TestWebFormControlElementToFormField(NSArray* test_data,
                                            NSString* tag_name);

  // Helper method for testing `javascripts_statement` that evalutate
  // `attribute_name` of the elements in `test_data` which has tag name
  // `tag_name`. EXPECTs JavaScript evaluation on
  // "window.document.getElementsByTagName()"
  void TestInputElementDataEvaluation(NSString* javascripts_statement,
                                      NSString* attribute_name,
                                      NSArray* test_data,
                                      NSString* tag_name);

  // Helper method that EXPECTs `__gCrWeb.fill.webFormElementToFormData` on
  // a form element obtained by `get_form_element_javascripts`. The results
  // are verified with `verifying_java_scripts`.
  void TestWebFormElementToFormDataForOneForm(
      NSString* get_form_element_javascripts,
      NSString* expected_result,
      NSString* verifying_javascripts);

  // EXPECTs `__gCrWeb.fill.webFormElementToFormData` on all the test data.
  void TestWebFormElementToFormData(NSArray* test_items);

  // EXPECTs `__gCrWeb.autofill.extractNewForms` on `html`.
  void TestExtractNewForms(NSString* html,
                           BOOL is_origin_window_location,
                           NSArray* expected_items);

  // Helper method that EXPECTs the `java_script` evaluation results on each
  // element obtained by JavaScripts in `get_element_java_scripts`. The
  // expected results are boolean and are true only for elements in
  // `get_element_java_scripts_expecting_true` which is subset of
  // `get_element_java_scripts`.
  void ExecuteBooleanJavaScriptOnElementsAndCheck(
      NSString* java_script,
      NSArray* get_element_java_scripts,
      NSArray* get_element_java_scripts_expecting_true);

  // Helper method that EXPECTs the `java_script` evaluation results on each
  // element obtained by scripts in `get_element_javas_cripts`; the expected
  // result is the corresponding entry in `expected_results`.
  void ExecuteJavaScriptOnElementsAndCheck(NSString* java_script,
                                           NSArray* get_element_java_scripts,
                                           NSArray* expected_results);

  id ExecuteJavaScript(NSString* java_script);

  std::unique_ptr<base::Value> CallJavaScriptFunction(
      const std::string& function,
      const base::Value::List& parameters);

  web::ScopedTestingWebClient web_client_;
  web::WebTaskEnvironment task_environment_;
  std::unique_ptr<TestChromeBrowserState> browser_state_;
  std::unique_ptr<web::WebState> web_state_;
};

void AutofillControllerJsTest::TestExecutingBooleanJavaScriptOnElement(
    NSString* javascript,
    const ElementByName elements_with_true_expected[],
    size_t size_elements_with_true_expected) {
  // Elements in `kHTMLForTestingElements`.
  const ElementByName elementsByName[] = {
      {"hl", 0, -1},
      {"firstname", 0, -1},
      {"lastname", 0, -1},
      {"email", 0, -1},
      {"phone", 0, -1},
      {"blog", 0, -1},
      {"expected number of clicks", 0, -1},
      {"pwd", 0, -1},
      {"vehicle", 0, -1},
      {"vehicle", 1, -1},
      {"vehicle", 2, -1},
      {"boolean", 0, -1},
      {"boolean", 1, -1},
      {"boolean", 2, -1},
      {"state", 0, -1},
      {"state", 0, 0},
      {"state", 0, 1},
      {"course", 0, -1},
      {"course", 0, 0},
      {"course", 0, 1},
      {"course", 0, 2},
      {"course", 0, 3},
      {"course", 0, 4},
      {"course", 0, 5},
      {"cars", 0, -1},
      {"cars", 0, 0},
      {"cars", 0, 1},
      {"cars", 0, 2},
      {"cars", 0, 3},
      {"submit", 0, -1},
  };

  web::test::LoadHtml(kHTMLForTestingElements, web_state());
  ExecuteBooleanJavaScriptOnElementsAndCheck(
      javascript,
      GetElementsByNameJavaScripts(elementsByName, std::size(elementsByName)),
      GetElementsByNameJavaScripts(elements_with_true_expected,
                                   size_elements_with_true_expected));
}

void AutofillControllerJsTest::ExecuteBooleanJavaScriptOnElementsAndCheck(
    NSString* java_script,
    NSArray* get_element_java_scripts,
    NSArray* get_element_java_scripts_expecting_true) {
  for (NSString* get_element_java_script in get_element_java_scripts) {
    NSString* js_to_execute =
        [NSString stringWithFormat:java_script, get_element_java_script];
    BOOL expected = [get_element_java_scripts_expecting_true
        containsObject:get_element_java_script];
    EXPECT_NSEQ(@(expected), ExecuteJavaScript(js_to_execute))
        << [NSString stringWithFormat:@"%@ on %@ should return %d", java_script,
                                      get_element_java_script, expected];
  }
}

void AutofillControllerJsTest::ExecuteJavaScriptOnElementsAndCheck(
    NSString* java_script,
    NSArray* get_element_java_scripts,
    NSArray* expected_results) {
  for (NSUInteger i = 0; i < get_element_java_scripts.count; ++i) {
    NSString* js_to_execute =
        [NSString stringWithFormat:java_script, get_element_java_scripts[i]];
    EXPECT_NSEQ(expected_results[i], ExecuteJavaScript(js_to_execute));
  }
}

id AutofillControllerJsTest::ExecuteJavaScript(NSString* java_script) {
  return web::test::ExecuteJavaScriptForFeature(
      web_state(), java_script,
      autofill::AutofillJavaScriptFeature::GetInstance());
}

std::unique_ptr<base::Value> AutofillControllerJsTest::CallJavaScriptFunction(
    const std::string& function,
    const base::Value::List& parameters) {
  return web::test::CallJavaScriptFunctionForFeature(
      web_state(), function, parameters,
      autofill::AutofillJavaScriptFeature::GetInstance());
}

TEST_F(AutofillControllerJsTest, HasTagName) {
  const ElementByName elements_expecting_true[] = {
      {"hl", 0, -1},
      {"firstname", 0, -1},
      {"lastname", 0, -1},
      {"email", 0, -1},
      {"phone", 0, -1},
      {"blog", 0, -1},
      {"expected number of clicks", 0, -1},
      {"pwd", 0, -1},
      {"vehicle", 0, -1},
      {"vehicle", 1, -1},
      {"vehicle", 2, -1},
      {"boolean", 0, -1},
      {"boolean", 1, -1},
      {"boolean", 2, -1},
      {"submit", 0, -1},
  };

  TestExecutingBooleanJavaScriptOnElement(
      @"__gCrWeb.fill.hasTagName(%@, 'input')", elements_expecting_true,
      std::size(elements_expecting_true));
}

TEST_F(AutofillControllerJsTest, CombineAndCollapseWhitespace) {
  web::test::LoadHtml(@"<html><body></body></html>", web_state());

  base::Value::List params;

  params.Append("foo");
  params.Append("bar");
  params.Append(false);
  auto result =
      CallJavaScriptFunction("fill.combineAndCollapseWhitespace", params);
  ASSERT_TRUE(result->is_string());
  EXPECT_EQ("foobar", result->GetString());
  params.clear();

  params.Append("foo");
  params.Append("bar");
  params.Append(true);
  result = CallJavaScriptFunction("fill.combineAndCollapseWhitespace", params);
  ASSERT_TRUE(result->is_string());
  EXPECT_EQ("foo bar", result->GetString());
  params.clear();

  params.Append("foo ");
  params.Append("bar");
  params.Append(false);
  result = CallJavaScriptFunction("fill.combineAndCollapseWhitespace", params);
  ASSERT_TRUE(result->is_string());
  EXPECT_EQ("foo bar", result->GetString());
  params.clear();

  params.Append("foo");
  params.Append(" bar");
  params.Append(false);
  result = CallJavaScriptFunction("fill.combineAndCollapseWhitespace", params);
  ASSERT_TRUE(result->is_string());
  EXPECT_EQ("foo bar", result->GetString());
  params.clear();

  params.Append("foo");
  params.Append(" bar");
  params.Append(true);
  result = CallJavaScriptFunction("fill.combineAndCollapseWhitespace", params);
  ASSERT_TRUE(result->is_string());
  EXPECT_EQ("foo bar", result->GetString());
  params.clear();

  params.Append("foo  ");
  params.Append("  bar");
  params.Append(false);
  result = CallJavaScriptFunction("fill.combineAndCollapseWhitespace", params);
  ASSERT_TRUE(result->is_string());
  EXPECT_EQ("foo bar", result->GetString());
  params.clear();

  params.Append("foo");
  params.Append("bar ");
  params.Append(false);
  result = CallJavaScriptFunction("fill.combineAndCollapseWhitespace", params);
  ASSERT_TRUE(result->is_string());
  EXPECT_EQ("foobar ", result->GetString());
  params.clear();

  params.Append(" foo");
  params.Append("bar");
  params.Append(true);
  result = CallJavaScriptFunction("fill.combineAndCollapseWhitespace", params);
  ASSERT_TRUE(result->is_string());
  EXPECT_EQ(" foo bar", result->GetString());
}

void AutofillControllerJsTest::TestInputElementDataEvaluation(
    NSString* javascripts_statement,
    NSString* attribute_name,
    NSArray* test_data,
    NSString* tag_name) {
  NSString* html_fragment = [test_data objectAtIndex:0U];
  web::test::LoadHtml(html_fragment, web_state());

  for (NSUInteger i = 1; i < [test_data count]; ++i) {
    NSString* get_element_javascripts = [NSString
        stringWithFormat:@"window.document.getElementsByTagName('%@')[%" PRIuNS
                          "]",
                         tag_name, i - 1];
    id actual = ExecuteJavaScript([NSString
        stringWithFormat:@"%@(%@).label === %@", javascripts_statement,
                         get_element_javascripts,
                         [[test_data objectAtIndex:i]
                             objectForKey:attribute_name]]);
    EXPECT_NSEQ(@YES, actual);
  }
}

TEST_F(AutofillControllerJsTest, InferLabelFromPrevious) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.fill.inferLabelFromPrevious", @"label",
      GetTestFormInputElementWithLabelFromPrevious(), @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromPreviousSpan) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.fill.inferLabelFromPrevious", @"label",
      GetTestFormInputElementWithLabelFromPreviousSpan(), @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromPreviousParagraph) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.fill.inferLabelFromPrevious", @"label",
      GetTestFormInputElementWithLabelFromPreviousParagraph(), @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromPreviousLabel) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.fill.inferLabelFromPrevious", @"label",
      GetTestFormInputElementWithLabelFromPreviousLabel(), @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromPreviousLabelOtherIgnored) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.fill.inferLabelFromPrevious", @"label",
      GetTestFormInputElementWithLabelFromPreviousLabelOtherIgnored(),
      @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromEnclosingLabelBefore) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.fill.inferLabelFromEnclosingLabel", @"label",
      GetTestFormInputElementWithLabelFromEnclosingLabelBefore(), @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromPreviousTextBrAndSpan) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.fill.inferLabelFromPrevious", @"label",
      GetTestFormInputElementWithLabelFromPreviousTextBrAndSpan(), @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromListItem) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.fill.inferLabelFromListItem", @"label",
      GetTestFormInputElementWithLabelFromListItem(), @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromTableColumnTD) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.fill.inferLabelFromTableColumn", @"label",
      GetTestFormInputElementWithLabelFromTableColumnTD(), @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromTableColumnTH) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.fill.inferLabelFromTableColumn", @"label",
      GetTestFormInputElementWithLabelFromTableColumnTH(), @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromTableColumnNested) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.fill.inferLabelFromTableColumn", @"label",
      GetTestFormInputElementWithLabelFromTableNested(), @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromTableRow) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.fill.inferLabelFromTableRow", @"label",
      GetTestFormInputElementWithLabelFromTableRow(), @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromDivTable) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.fill.inferLabelFromDivTable", @"label",
      GetTestFormInputElementWithLabelFromDivTable(), @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromDefinitionList) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.fill.inferLabelFromDefinitionList", @"label",
      GetTestFormInputElementWithLabelFromDefinitionList(), @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelForElement) {
  NSArray* testingElements = @[
    GetTestFormInputElementWithLabelFromPrevious(),
    GetTestFormInputElementWithLabelFromPreviousSpan(),
    GetTestFormInputElementWithLabelFromPreviousParagraph(),
    GetTestFormInputElementWithLabelFromPreviousLabel(),
    GetTestFormInputElementWithLabelFromPreviousLabelOtherIgnored(),
    GetTestFormInputElementWithLabelFromEnclosingLabelBefore(),
    GetTestFormInputElementWithLabelFromPreviousTextSpanBr(),
    GetTestFormInputElementWithLabelFromPreviousTextBrAndSpan(),
    GetTestFormInputElementWithLabelFromListItem(),
    GetTestFormInputElementWithLabelFromTableColumnTD(),
    GetTestFormInputElementWithLabelFromTableColumnTH(),
    GetTestFormInputElementWithLabelFromTableNested(),
    GetTestFormInputElementWithLabelFromTableRow(),
    GetTestFormInputElementWithLabelFromDivTable(),
    GetTestFormInputElementWithLabelFromDefinitionList(), GetTestInputRadio(),
    GetTestInputCheckbox()
  ];
  for (NSArray* testingElement in testingElements) {
    TestInputElementDataEvaluation(@"__gCrWeb.fill.inferLabelForElement",
                                   @"label", testingElement, @"input");
  }

  TestInputElementDataEvaluation(@"__gCrWeb.fill.inferLabelForElement",
                                 @"label", GetTestFormSelectElement(),
                                 @"select");

  TestInputElementDataEvaluation(
      @"__gCrWeb.fill.inferLabelForElement", @"label",
      GetTestFormSelectElementWithOptgroup(), @"select");
}

TEST_F(AutofillControllerJsTest, IsAutofillableElement) {
  const ElementByName elements_expecting_true[] = {
      {"firstname", 0, -1}, {"lastname", 0, -1},
      {"email", 0, -1},     {"phone", 0, -1},
      {"blog", 0, -1},      {"expected number of clicks", 0, -1},
      {"pwd", 0, -1},       {"vehicle", 0, -1},
      {"vehicle", 1, -1},   {"vehicle", 2, -1},
      {"boolean", 0, -1},   {"boolean", 1, -1},
      {"boolean", 2, -1},   {"state", 0, -1},
      {"course", 0, -1},
  };

  TestExecutingBooleanJavaScriptOnElement(
      @"__gCrWeb.fill.isAutofillableElement(%@)", elements_expecting_true,
      std::size(elements_expecting_true));
}

TEST_F(AutofillControllerJsTest, GetOptionStringsFromElement) {
  ElementByName testing_elements[] = {
      {"state", 0, -1}, {"course", 0, -1}, {"cars", 0, -1}};

  web::test::LoadHtml(kHTMLForTestingElements, web_state());
  ExecuteJavaScriptOnElementsAndCheck(
      @"var field = {};"
       "__gCrWeb.fill.getOptionStringsFromElement(%@, field);"
       "__gCrWeb.stringify(field);",
      GetElementsByNameJavaScripts(testing_elements,
                                   std::size(testing_elements)),
      @[
        @("{\"option_values\":[\"CA\",\"MA\"],"
          "\"option_texts\":[\"CA\",\"MA\"]}"),
        @("{\"option_values\":["
          "\"8.01.1\",\"8.01.2\",\"8.01.3\","
          "\"8.02.1\",\"8.02.2\",\"8.02.3\"],"
          "\"option_texts\":["
          "\"Lecture 01: Powers of Ten\","
          "\"Lecture 02: 1D Kinematics\","
          "\"Lecture 03: Vectors\","
          "\"Lecture 01: What holds our world together?\","
          "\"Lecture 02: Electric Field\","
          "\"Lecture 03: Electric Flux\""
          "]}"),
        @("{\"option_values\":[\"volvo\",\"saab\",\"opel\",\"audi\"],"
          "\"option_texts\":[\"Volvo\",\"Saab\",\"Opel\",\"Audi\"]}")
      ]);
}

TEST_F(AutofillControllerJsTest, FillFormField) {
  web::test::LoadHtml(kHTMLForTestingElements, web_state());

  // Test text and select elements of which the value should be changed.
  const ElementByName elements[] = {
      {"firstname", 0, -1},
      {"state", 0, -1},
  };
  NSArray* values = @[
    @"new name",
    @"MA",
  ];
  for (size_t i = 0; i < std::size(elements); ++i) {
    NSString* get_element_javascript = GetElementByNameJavaScript(elements[i]);
    NSString* new_value = [values objectAtIndex:i];
    EXPECT_NSEQ(
        new_value,
        ExecuteJavaScript([NSString
            stringWithFormat:@"var element=%@;var data={'value':'%@'};"
                             @"__gCrWeb.autofill.fillFormField(data, element);"
                             @"element.value",
                             get_element_javascript, new_value]));
  }

  // Test clickable elements, of which 'checked' should be updated.
  ElementByName checkable_elements[] = {
      {"vehicle", 0, -1}, {"vehicle", 1, -1}, {"vehicle", 2, -1},
      {"boolean", 0, -1}, {"boolean", 1, -1}, {"boolean", 2, -1},
  };
  const bool final_is_checked_values[] = {
      true, false, true, false, true, true,
  };
  for (size_t i = 0; i < std::size(checkable_elements); ++i) {
    NSString* get_element_javascript =
        GetElementByNameJavaScript(checkable_elements[i]);
    bool is_checked = final_is_checked_values[i];

    EXPECT_NSEQ(
        @(is_checked),
        ExecuteJavaScript([NSString
            stringWithFormat:@"var element=%@; var value=element.value; "
                             @"var data={'value':value,'is_checked':%@};"
                             @"__gCrWeb.autofill.fillFormField(data, element); "
                             @"element.checked",
                             get_element_javascript,
                             is_checked ? @"true" : @"false"]));
  }

  // Test elements of which the value should not be changed.
  ElementByName unchanged_elements[] = {
      {"hl", 0, -1},    // hidden element
      {"state", 0, 0},  // option element
      {"state", 0, 1},  // option element
  };
  for (size_t i = 0; i < std::size(unchanged_elements); ++i) {
    NSString* get_element_javascript =
        GetElementByNameJavaScript(unchanged_elements[i]);
    NSString* actual = ExecuteJavaScript(
        [NSString stringWithFormat:
                      @"var element=%@;"
                      @"var oldValue=element.value; var data={'value':'new'};"
                      @"__gCrWeb.autofill.fillFormField(data, element);"
                      @"element.value === oldValue",
                      get_element_javascript]);
    EXPECT_NSEQ(@YES, actual);
  }
}

TEST_F(AutofillControllerJsTest, IsTextInput) {
  const ElementByName elements_expecting_true[] = {
      {"firstname", 0, -1}, {"lastname", 0, -1},
      {"email", 0, -1},     {"phone", 0, -1},
      {"blog", 0, -1},      {"expected number of clicks", 0, -1},
      {"pwd", 0, -1},
  };

  TestExecutingBooleanJavaScriptOnElement(@"__gCrWeb.fill.isTextInput(%@)",
                                          elements_expecting_true,
                                          std::size(elements_expecting_true));
}

TEST_F(AutofillControllerJsTest, IsSelectElement) {
  const ElementByName elements_expecting_true[] = {
      {"state", 0, -1},
      {"course", 0, -1},
  };

  TestExecutingBooleanJavaScriptOnElement(@"__gCrWeb.fill.isSelectElement(%@)",
                                          elements_expecting_true,
                                          std::size(elements_expecting_true));
}

TEST_F(AutofillControllerJsTest, IsCheckableElement) {
  const ElementByName elements_expecting_true[] = {
      {"vehicle", 0, -1}, {"vehicle", 1, -1}, {"vehicle", 2, -1},
      {"boolean", 0, -1}, {"boolean", 1, -1}, {"boolean", 2, -1},
  };

  TestExecutingBooleanJavaScriptOnElement(
      @"__gCrWeb.fill.isCheckableElement(%@)", elements_expecting_true,
      std::size(elements_expecting_true));
}

TEST_F(AutofillControllerJsTest, IsAutofillableInputElement) {
  const ElementByName elements_expecting_true[] = {
      {"firstname", 0, -1}, {"lastname", 0, -1},
      {"email", 0, -1},     {"phone", 0, -1},
      {"blog", 0, -1},      {"expected number of clicks", 0, -1},
      {"pwd", 0, -1},       {"vehicle", 0, -1},
      {"vehicle", 1, -1},   {"vehicle", 2, -1},
      {"boolean", 0, -1},   {"boolean", 1, -1},
      {"boolean", 2, -1},
  };

  TestExecutingBooleanJavaScriptOnElement(
      @"__gCrWeb.fill.isAutofillableInputElement(%@)", elements_expecting_true,
      std::size(elements_expecting_true));
}

TEST_F(AutofillControllerJsTest, ExtractAutofillableElements) {
  web::test::LoadHtml(kHTMLForTestingElements, web_state());
  ElementByName expected_elements[] = {
      {"firstname", 0, -1}, {"lastname", 0, -1},
      {"email", 0, -1},     {"phone", 0, -1},
      {"blog", 0, -1},      {"expected number of clicks", 0, -1},
      {"pwd", 0, -1},       {"vehicle", 0, -1},
      {"vehicle", 1, -1},   {"vehicle", 2, -1},
      {"boolean", 0, -1},   {"boolean", 1, -1},
      {"boolean", 2, -1},   {"state", 0, -1},
  };
  NSArray* expected = GetElementsByNameJavaScripts(
      expected_elements, std::size(expected_elements));

  NSString* parameter = @"window.document.getElementsByTagName('form')[0]";
  for (NSUInteger index = 0; index < [expected count]; index++) {
    EXPECT_NSEQ(
        @YES,
        ExecuteJavaScript([NSString
            stringWithFormat:
                @"var controlElements="
                 "__gCrWeb.autofill.extractAutofillableElementsInForm(%@);"
                 "controlElements[%" PRIuNS "] === %@",
                parameter, index, expected[index]]));
  }
}

void AutofillControllerJsTest::TestWebFormControlElementToFormField(
    NSArray* test_data,
    NSString* tag_name) {
  web::test::LoadHtml([test_data firstObject], web_state());

  NSArray* attributes_to_check = GetFormFieldAttributeListsToCheck();

  for (NSUInteger j = 1; j < [test_data count]; ++j) {
    NSString* get_element_to_test =
        [NSString stringWithFormat:@"var element = "
                                    "window.document.getElementsByTagName('%"
                                    "@')[%" PRIuNS "]",
                                   tag_name, j - 1];
    NSDictionary* expected = [test_data objectAtIndex:j];
    // Generates JavaScripts to verify the results. Parameter `results` is
    // @"field" as in the evaluation JavaScripts the results are returned in
    // `field`.
    NSString* verifying_javascripts = GenerateElementItemVerifyingJavaScripts(
        @"field", expected, attributes_to_check, -1);
    EXPECT_NSEQ(
        @YES,
        ExecuteJavaScript([NSString
            stringWithFormat:@"%@; var field = {};"
                              "__gCrWeb.fill.webFormControlElementToFormField("
                              "    element, field);"
                              "%@",
                             get_element_to_test, verifying_javascripts]))
        << base::SysNSStringToUTF8([NSString
               stringWithFormat:
                   @"webFormControlElementToFormField actual results are: "
                   @"%@, \n"
                    "expected to be verified by %@",
                   ExecuteJavaScript([NSString
                       stringWithFormat:@"%@; var field = {};"
                                         "__gCrWeb.fill."
                                         "webFormControlElementToFormField("
                                         "    element, "
                                         "field);__gCrWeb.stringify(field);",
                                        get_element_to_test]),
                   verifying_javascripts]);
  }
}

TEST_F(AutofillControllerJsTest, WebFormControlElementToFormField) {
  NSArray* test_input_elements = @[
    GetTestFormInputElementWithLabelFromPrevious(),
    GetTestFormInputElementWithLabelFromPreviousSpan(),
    GetTestFormInputElementWithLabelFromPreviousParagraph(),
    GetTestFormInputElementWithLabelFromPreviousLabel(),
    GetTestFormInputElementWithLabelFromPreviousLabelOtherIgnored(),
    GetTestFormInputElementWithLabelFromEnclosingLabelBefore(),
    GetTestFormInputElementWithLabelFromPreviousTextBrAndSpan(),
    GetTestFormInputElementWithLabelFromPreviousTextSpanBr(),
    GetTestFormInputElementWithLabelFromListItem(),
    GetTestFormInputElementWithLabelFromTableColumnTD(),
    GetTestFormInputElementWithLabelFromTableColumnTH(),
    GetTestFormInputElementWithLabelFromTableNested(),
    GetTestFormInputElementWithLabelFromTableRow(),
    GetTestFormInputElementWithLabelFromDivTable(),
    GetTestFormInputElementWithLabelFromDefinitionList(), GetTestInputRadio(),
    GetTestInputCheckbox()
  ];
  for (NSArray* test_element in test_input_elements) {
    TestWebFormControlElementToFormField(test_element, @"input");
  }

  TestWebFormControlElementToFormField(GetTestFormSelectElement(), @"select");
  TestWebFormControlElementToFormField(GetTestFormSelectElementWithOptgroup(),
                                       @"select");
}

void AutofillControllerJsTest::TestWebFormElementToFormDataForOneForm(
    NSString* get_form_element_javascripts,
    NSString* expected_result,
    NSString* verifying_javascripts) {
  NSString* actual = ExecuteJavaScript(
      [NSString stringWithFormat:@"var form={}; var field={};"
                                  "(__gCrWeb.fill.webFormElementToFormData("
                                  "window, %@, null, form, field) "
                                  "=== %@) && %@",
                                 get_form_element_javascripts, expected_result,
                                 verifying_javascripts]);

  EXPECT_NSEQ(@YES, actual) << base::SysNSStringToUTF8([NSString
      stringWithFormat:
          @"Actual:\n%@; expected to be verifyied by\n%@",
          ExecuteJavaScript([NSString
              stringWithFormat:@"var form={};"
                                "__gCrWeb.fill."
                                "webFormElementToFormData(window, %@, null,"
                                "form, null);"
                                "__gCrWeb.stringify(form);",
                               get_form_element_javascripts]),
          verifying_javascripts]);
}

void AutofillControllerJsTest::TestWebFormElementToFormData(
    NSArray* test_items) {
  NSString* form_html_fragment =
      @"<form name='TestForm' action='http://cnn.com' method='post'>";
  for (NSUInteger i = 0; i < [test_items count]; ++i) {
    form_html_fragment = [form_html_fragment
        stringByAppendingString:[[test_items objectAtIndex:i]
                                    objectAtIndex:0U]];
  }
  form_html_fragment = [form_html_fragment stringByAppendingString:@"</form>"];
  web::test::LoadHtml(form_html_fragment, web_state());

  NSString* parameter = @"document.getElementsByTagName('form')[0]";
  NSString* expected_result = @"true";
  // We don't verify 'action' here as action is generated as a complete url
  // and here data url is used.
  NSMutableArray* verifying_javascripts = [NSMutableArray
      arrayWithObjects:@"form['name'] === 'TestForm'",
                       @"form['origin'] === window.location.href", nil];
  [verifying_javascripts
      addObject:GenerateTestItemVerifyingJavaScripts(
                    @"form", test_items, GetFormFieldAttributeListsToCheck())];
  TestWebFormElementToFormDataForOneForm(
      parameter, expected_result,
      [verifying_javascripts componentsJoinedByString:@"&&"]);
}

TEST_F(AutofillControllerJsTest, WebFormElementToFormData) {
  NSArray* test_elements = @[
    GetTestFormInputElementWithLabelFromPrevious(),
    GetTestFormInputElementWithLabelFromPreviousSpan(),
    GetTestFormInputElementWithLabelFromPreviousParagraph(),
    GetTestFormInputElementWithLabelFromPreviousLabel(),
    GetTestFormInputElementWithLabelFromPreviousLabelOtherIgnored(),
    GetTestFormInputElementWithLabelFromEnclosingLabelBefore(),
    GetTestFormInputElementWithLabelFromPreviousTextSpanBr(),
    GetTestFormInputElementWithLabelFromPreviousTextBrAndSpan(),
    GetTestFormInputElementWithLabelFromListItem(),
    GetTestFormInputElementWithLabelFromTableColumnTD(),
    GetTestFormInputElementWithLabelFromTableColumnTH(),
    GetTestFormInputElementWithLabelFromTableNested(),
    GetTestFormInputElementWithLabelFromTableRow(),
    GetTestFormInputElementWithLabelFromDivTable(),
    GetTestFormInputElementWithLabelFromDefinitionList(), GetTestInputRadio(),
    GetTestInputCheckbox(), GetTestFormSelectElement(),
    GetTestFormSelectElementWithOptgroup()
  ];
  // Test a form that has a signle item in the array.
  for (NSArray* testElement in test_elements) {
    TestWebFormElementToFormData(@[ testElement ]);
  }

  // Also test a form that has all the above items.
  TestWebFormElementToFormData(test_elements);
}

TEST_F(AutofillControllerJsTest, WebFormElementToFormDataTooManyFields) {
  NSString* html_fragment = @"<FORM name='Test' action='http://c.com'>";
  // In autofill_controller.js, the maximum number of parsable element is 200
  // (MAX_EXTRACTABLE_FIELDS = 200). Here an HTML page with 201
  // elements is generated for testing.
  for (NSUInteger index = 0; index < 201; ++index) {
    html_fragment =
        [html_fragment stringByAppendingFormat:@"<INPUT type='text'/>"];
  }
  html_fragment = [html_fragment stringByAppendingFormat:@"</FORM>"];

  web::test::LoadHtml(html_fragment, web_state());
  TestWebFormElementToFormDataForOneForm(
      @"document.getElementsByTagName('form')[0]", @"false", @"true");
}

TEST_F(AutofillControllerJsTest, WebFormElementToFormEmpty) {
  NSString* html_fragment = @"<FORM name='Test' action='http://c.com'>";
  html_fragment = [html_fragment stringByAppendingFormat:@"</FORM>"];

  web::test::LoadHtml(html_fragment, web_state());
  TestWebFormElementToFormDataForOneForm(
      @"document.getElementsByTagName('form')[0]", @"false", @"true");
}

void AutofillControllerJsTest::TestExtractNewForms(
    NSString* html,
    BOOL is_origin_window_location,
    NSArray* expected_items) {
  web::test::LoadHtml(html, web_state());
  // Generates verifying javascripts.
  NSMutableArray* verifying_javascripts = [NSMutableArray array];
  for (NSUInteger i = 0U; i < [expected_items count]; ++i) {
    // All forms created in this test suite are named "TestForm".
    // If a page contains more than one of these forms, ExtractForms will rename
    // all forms but the fist one.
    NSString* formName =
        (i == 0) ? @"TestForm"
                 : [NSString stringWithFormat:@"gChrome~form~%" PRIuNS, i];
    [verifying_javascripts
        addObject:[NSString stringWithFormat:@"forms[%" PRIuNS
                                              "]['name'] === '%@'",
                                             i, formName]];
    if (is_origin_window_location) {
      [verifying_javascripts
          addObject:[NSString stringWithFormat:
                                  @"forms[%" PRIuNS
                                   "]['origin'] === window.location.href",
                                  i]];
    }
    // This is the extract mask used by
    // __gCrWeb.autofill.extractNewForms.
    [verifying_javascripts
        addObject:GenerateTestItemVerifyingJavaScripts(
                      [NSString stringWithFormat:@"forms[%" PRIuNS "]", i],
                      [expected_items objectAtIndex:i],
                      GetFormFieldAttributeListsToCheck())];
  }

  NSString* actual = ExecuteJavaScript([NSString
      stringWithFormat:@"var forms = __gCrWeb.autofill.extractNewForms("
                        "true); %@",
                       [verifying_javascripts componentsJoinedByString:@"&&"]]);

  EXPECT_NSEQ(@YES, actual) << base::SysNSStringToUTF8([NSString
      stringWithFormat:@"actually forms = %@, "
                        "but it is expected to be verified by %@",
                       ExecuteJavaScript(
                           @"var forms = __gCrWeb.autofill.extractNewForms("
                            "true); __gCrWeb.stringify(forms)"),
                       verifying_javascripts]);
}

TEST_F(AutofillControllerJsTest, ExtractFormsAndFormElements) {
  NSArray* testFirstFormItems = @[
    GetTestFormInputElementWithLabelFromPrevious(),
    GetTestFormInputElementWithLabelFromPreviousSpan(),
    GetTestFormInputElementWithLabelFromPreviousParagraph(),
    GetTestFormInputElementWithLabelFromPreviousLabel(),
    GetTestFormInputElementWithLabelFromPreviousLabelOtherIgnored(),
    GetTestFormInputElementWithLabelFromEnclosingLabelBefore(),
    GetTestFormInputElementWithLabelFromPreviousTextSpanBr(),
    GetTestFormInputElementWithLabelFromPreviousTextBrAndSpan(),
    GetTestFormInputElementWithLabelFromListItem(),
    GetTestFormInputElementWithLabelFromTableColumnTD(),
    GetTestFormInputElementWithLabelFromTableColumnTH(),
    GetTestFormInputElementWithLabelFromTableNested(),
    GetTestFormInputElementWithLabelFromTableRow(),
    GetTestFormInputElementWithLabelFromDivTable(),
    GetTestFormInputElementWithLabelFromDefinitionList(), GetTestInputRadio(),
    GetTestInputCheckbox()
  ];
  NSArray* testSecondFormItems = @[
    GetTestFormInputElementWithLabelFromDivTable(), GetTestFormSelectElement(),
    GetTestFormSelectElementWithOptgroup()
  ];
  NSArray* test_forms = @[ testFirstFormItems, testSecondFormItems ];

  NSString* html = @"<html><body>";
  for (NSArray* testFormItems in test_forms) {
    html = [html stringByAppendingString:
                     @"<form name='TestForm' action='http://c.com'>"];
    for (NSArray* testItem in testFormItems) {
      html = [html stringByAppendingString:[testItem objectAtIndex:0U]];
    }
    html = [html stringByAppendingFormat:@"</form>"];
  }
  html = [html stringByAppendingFormat:@"</body></html>"];
  TestExtractNewForms(html, true, test_forms);
}

TEST_F(AutofillControllerJsTest,
       ExtractFormsAndFormElementsWithFormAssociatedElementsOutOfForm) {
  NSString* html =
      @"<html><body>"
       "<form id='testform'></form>"
       "1 <input type='text' name='name1' id='name1' form='testform'></input>"
       "2 <input type='text' name='name2' id='name2' form='testform'></input>"
       "3 <input type='text' name='name3' id='name3' form='testform'></input>"
       "4 <input type='text' name='name4' id='name4' form='testform'></input>"
       "</body></html>";
  web::test::LoadHtml(html, web_state());

  NSString* verifying_javascript = @"forms[0]['fields'][0]['name']==='name1' &&"
                                   @"forms[0]['fields'][0]['label']==='1' &&"
                                   @"forms[0]['fields'][1]['name']==='name2' &&"
                                   @"forms[0]['fields'][1]['label']==='2' &&"
                                   @"forms[0]['fields'][2]['name']==='name3' &&"
                                   @"forms[0]['fields'][2]['label']==='3' &&"
                                   @"forms[0]['fields'][3]['name']==='name4' &&"
                                   @"forms[0]['fields'][3]['label']==='4'";
  EXPECT_NSEQ(
      @YES, ExecuteJavaScript([NSString
                stringWithFormat:@"var forms = "
                                  "__gCrWeb.autofill.extractNewForms(true); %@",
                                 verifying_javascript]));
}

TEST_F(AutofillControllerJsTest, ExtractForms) {
  NSString* html = @"<html><body>";
  html = [html
      stringByAppendingString:@"<form name='TestForm' action='http://c.com'>"];
  html = [html
      stringByAppendingString:[GetTestFormInputElementWithLabelFromPrevious()
                                  objectAtIndex:0U]];
  html =
      [html stringByAppendingString:[GetTestInputCheckbox() objectAtIndex:0U]];
  html = [html stringByAppendingString:
                   [GetTestFormInputElementWithLabelFromTableColumnTH()
                       objectAtIndex:0U]];
  html = [html stringByAppendingString:
                   [GetTestFormInputElementWithLabelFromPreviousTextBrAndSpan()
                       objectAtIndex:0U]];
  html = [html
      stringByAppendingString:[GetTestFormSelectElement() objectAtIndex:0U]];
  html = [html stringByAppendingFormat:@"</form>"];
  html = [html stringByAppendingFormat:@"</body></html>"];

  web::test::LoadHtml(html, web_state());

  web::WebFrame* main_frame = WaitForMainFrame();
  ASSERT_TRUE(main_frame);

  NSDictionary* expected = @{
    @"name" : @"TestForm",
    @"fields" : @[
      @{
        @"aria_description" : @"",
        @"aria_label" : @"",
        @"name" : @"firstname",
        @"name_attribute" : @"firstname",
        @"id_attribute" : @"firstname",
        @"identifier" : @"firstname",
        @"renderer_id" : @"2",
        @"form_control_type" : @"text",
        @"max_length" : GetDefaultMaxLength(),
        @"placeholder_attribute" : @"",
        @"should_autocomplete" : @true,
        @"is_checkable" : @false,
        @"is_focusable" : @true,
        @"is_user_edited" : @true,
        @"value" : @"John",
        @"label" : @"* First name:"
      },
      @{
        @"aria_description" : @"",
        @"aria_label" : @"",
        @"name" : @"vehicle",
        @"name_attribute" : @"vehicle",
        @"id_attribute" : @"vehicle1",
        @"identifier" : @"vehicle1",
        @"renderer_id" : @"3",
        @"form_control_type" : @"checkbox",
        @"placeholder_attribute" : @"",
        @"should_autocomplete" : @true,
        @"is_checkable" : @true,
        @"is_focusable" : @true,
        @"is_user_edited" : @true,
        @"value" : @"Bike",
        @"label" : @"Bicycle"
      },
      @{
        @"aria_description" : @"",
        @"aria_label" : @"",
        @"name" : @"vehicle",
        @"name_attribute" : @"vehicle",
        @"id_attribute" : @"vehicle2",
        @"identifier" : @"vehicle2",
        @"renderer_id" : @"4",
        @"form_control_type" : @"checkbox",
        @"placeholder_attribute" : @"",
        @"should_autocomplete" : @true,
        @"is_checkable" : @true,
        @"is_focusable" : @true,
        @"is_user_edited" : @true,
        @"value" : @"Car",
        @"label" : @"Automobile"
      },
      @{
        @"aria_description" : @"",
        @"aria_label" : @"",
        @"name" : @"vehicle",
        @"name_attribute" : @"vehicle",
        @"id_attribute" : @"vehicle3",
        @"identifier" : @"vehicle3",
        @"renderer_id" : @"5",
        @"form_control_type" : @"checkbox",
        @"placeholder_attribute" : @"",
        @"should_autocomplete" : @true,
        @"is_checkable" : @true,
        @"is_focusable" : @true,
        @"is_user_edited" : @true,
        @"value" : @"Rocket",
        @"label" : @"Missile"
      },
      @{
        @"aria_description" : @"",
        @"aria_label" : @"",
        @"name" : @"nameintableth",
        @"name_attribute" : @"nameintableth",
        @"id_attribute" : @"nameintableth",
        @"identifier" : @"nameintableth",
        @"renderer_id" : @"6",
        @"form_control_type" : @"text",
        @"placeholder_attribute" : @"",
        @"max_length" : GetDefaultMaxLength(),
        @"should_autocomplete" : @true,
        @"is_checkable" : @false,
        @"is_focusable" : @true,
        @"is_user_edited" : @true,
        @"value" : @"John",
        @"label" : @"* First name:"
      },
      @{
        @"aria_description" : @"",
        @"aria_label" : @"",
        @"name" : @"emailtableth",
        @"name_attribute" : @"",
        @"id_attribute" : @"emailtableth",
        @"identifier" : @"emailtableth",
        @"renderer_id" : @"7",
        @"form_control_type" : @"email",
        @"placeholder_attribute" : @"",
        @"max_length" : GetDefaultMaxLength(),
        @"should_autocomplete" : @true,
        @"is_checkable" : @false,
        @"is_focusable" : @true,
        @"is_user_edited" : @true,
        @"value" : @"[email protected]",
        @"label" : @"Email:"
      },
      @{
        @"aria_description" : @"",
        @"aria_label" : @"",
        @"name" : @"pwd",
        @"name_attribute" : @"pwd",
        @"id_attribute" : @"pwd",
        @"identifier" : @"pwd",
        @"renderer_id" : @"8",
        @"form_control_type" : @"password",
        @"placeholder_attribute" : @"",
        @"autocomplete_attribute" : @"off",
        @"max_length" : GetDefaultMaxLength(),
        @"should_autocomplete" : @false,
        @"is_checkable" : @false,
        @"is_focusable" : @true,
        @"is_user_edited" : @true,
        @"value" : @"",
        @"label" : @"* Password:"
      },
      @{
        @"aria_description" : @"",
        @"aria_label" : @"",
        @"name" : @"state",
        @"name_attribute" : @"state",
        @"id_attribute" : @"state",
        @"identifier" : @"state",
        @"renderer_id" : @"9",
        @"form_control_type" : @"select-one",
        @"placeholder_attribute" : @"",
        @"is_focusable" : @1,
        @"is_user_edited" : @true,
        @"option_values" : @[ @"CA", @"TX" ],
        @"option_texts" : @[ @"California", @"Texas" ],
        @"should_autocomplete" : @1,
        @"value" : @"CA",
        @"label" : @"State:"
      }
    ]
  };

  NSString* result = ExecuteJavaScript(
      [NSString stringWithFormat:@"__gCrWeb.autofill.extractForms(%zu, true)",
                                 autofill::kMinRequiredFieldsForHeuristics]);
  NSArray* resultArray = [NSJSONSerialization
      JSONObjectWithData:[result dataUsingEncoding:NSUTF8StringEncoding]
                 options:0
                   error:nil];
  ASSERT_NSNE(nil, resultArray);
  EXPECT_EQ(1u, [resultArray count]);

  NSDictionary* form = [resultArray objectAtIndex:0];
  [expected enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL* stop) {
    EXPECT_NSEQ(form[key], obj);
  }];

  // Test with Object.prototype.toJSON override.
  result = ExecuteJavaScript([NSString
      stringWithFormat:@"Object.prototype.toJSON=function(){return 'abcde';};"
                        "__gCrWeb.autofill.extractForms(%zu, true)",
                       autofill::kMinRequiredFieldsForHeuristics]);
  resultArray = [NSJSONSerialization
      JSONObjectWithData:[result dataUsingEncoding:NSUTF8StringEncoding]
                 options:0
                   error:nil];
  ASSERT_NSNE(nil, resultArray);

  form = [resultArray objectAtIndex:0];
  [expected enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL* stop) {
    EXPECT_NSEQ(form[key], obj);
  }];

  // Test with Array.prototype.toJSON override.
  result = ExecuteJavaScript([NSString
      stringWithFormat:@"Array.prototype.toJSON=function(){return 'abcde';};"
                        "__gCrWeb.autofill.extractForms(%zu, true)",
                       autofill::kMinRequiredFieldsForHeuristics]);
  resultArray = [NSJSONSerialization
      JSONObjectWithData:[result dataUsingEncoding:NSUTF8StringEncoding]
                 options:0
                   error:nil];
  ASSERT_NSNE(nil, resultArray);

  form = [resultArray objectAtIndex:0];
  [expected enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL* stop) {
    EXPECT_NSEQ(form[key], obj);
  }];
}

// Test that, when xframes is enabled, forms that do not have input fields but
// have child frames are still extracted because their child frames may contain
// input fields.
TEST_F(AutofillControllerJsTest,
       ExtractForms_NoInputFieldsButChildFrames_WhenXframeEnabled) {
  NSString* html = @"<html><body>"
                    "<form id='testform'>"
                    "<iframe></iframe>"
                    "</form>"
                    "</body></html>";
  web::test::LoadHtml(html, web_state());

  autofill::FormUtilJavaScriptFeature::GetInstance()->SetAutofillAcrossIframes(
      WaitForMainFrame(), /*enabled=*/true);

  // Verify that the form with child frames was extracted.
  NSString* verifying_javascript =
      @"forms.length === 1 && forms[0].id_attribute === 'testform' && "
      @"forms[0].child_frames.length === 1; ";
  EXPECT_NSEQ(
      @YES,
      ExecuteJavaScript([NSString
          stringWithFormat:@"var forms = "
                            "__gCrWeb.autofill.extractNewForms(false); %@",
                           verifying_javascript]));
}

// Test that forms that don't have input fields and have child frames aren't
// extracted when xframes is disabled.
TEST_F(AutofillControllerJsTest,
       ExtractForms_NoInputFieldsButChildFrames_WhenXframeDisabled) {
  NSString* html = @"<html><body>"
                    "<form id='testform'>"
                    "<iframe></iframe>"
                    "</form>"
                    "</body></html>";
  web::test::LoadHtml(html, web_state());

  // Verify that the form with only child frames isn't eligible when the xframe
  // feature is disabled.
  NSString* verifying_javascript = @"forms.length === 0;";
  EXPECT_NSEQ(
      @YES,
      ExecuteJavaScript([NSString
          stringWithFormat:@"var forms = "
                            "__gCrWeb.autofill.extractNewForms(false); %@",
                           verifying_javascript]));
}

// Test that, when xframes is enabled, child frames outside forms are still
// extracted in a synthetic form because they may contain input fields and be
// part of a xframes form.
TEST_F(AutofillControllerJsTest,
       ExtractSyntheticForm_NoInputFieldsButChildFrames_WhenXframeEnabled) {
  NSString* html = @"<html><body>"
                    "Name <input id='name' type='text' name='name' />"
                    "<iframe></iframe>"
                    "</body></html>";
  web::test::LoadHtml(html, web_state());

  autofill::FormUtilJavaScriptFeature::GetInstance()->SetAutofillAcrossIframes(
      WaitForMainFrame(), /*enabled=*/true);

  // Verify that the form with child frames was extracted.
  NSString* verifying_javascript =
      @"forms.length === 1 && forms[0].child_frames.length === 1;";
  EXPECT_NSEQ(
      @YES,
      ExecuteJavaScript([NSString
          stringWithFormat:@"var forms = "
                            "__gCrWeb.autofill.extractNewForms(false); %@",
                           verifying_javascript]));
}

// Test that, when xframes is diabled, child frames outside of forms are not
// extracted.
TEST_F(AutofillControllerJsTest,
       ExtractSyntheticForm_NoInputFieldsButChildFrames_WhenXframeDisabled) {
  NSString* html = @"<html><body>"
                    "<iframe></iframe>"
                    "</body></html>";
  web::test::LoadHtml(html, web_state());

  // Verify that the form with child frames was extracted.
  NSString* verifying_javascript = @"forms.length === 0;";
  EXPECT_NSEQ(
      @YES,
      ExecuteJavaScript([NSString
          stringWithFormat:@"var forms = "
                            "__gCrWeb.autofill.extractNewForms(false); %@",
                           verifying_javascript]));
}

TEST_F(AutofillControllerJsTest, FillActiveFormField) {
  web::test::LoadHtml(kHTMLForTestingElements, web_state());

  web::WebFrame* main_frame = WaitForMainFrame();
  ASSERT_TRUE(main_frame);

  // Simulate form parsing to set renderer IDs.
  ExecuteJavaScript(@"__gCrWeb.autofill.extractForms(0, true)");

  NSString* newValue = @"new value";
  EXPECT_NSEQ(newValue,
              ExecuteJavaScript([NSString
                  stringWithFormat:
                      @"var element=document.getElementsByName('lastname')[0];"
                       "element.focus();"
                       "var "
                       "data={\"name\":\"lastname\",\"value\":\"%@\","
                       "\"identifier\":\"lastname\",\"renderer_id\":3};"
                       "__gCrWeb.autofill.fillActiveFormField(data);"
                       "element.value",
                      newValue]));

  EXPECT_NSEQ(
      @YES,
      ExecuteJavaScript([NSString
          stringWithFormat:@"var element=document.getElementsByName('gl')[0];"
                            "element.focus();"
                            "var oldValue = element.value;"
                            "var "
                            "data={\"name\":\"lastname\",\"value\":\"%@\","
                            "\"identifier\":\"lastname\",\"renderer_id\":3};"
                            "__gCrWeb.autofill.fillActiveFormField(data);"
                            "element.value === oldValue",
                           newValue]))
      << "A non-form element's value should changed.";
}

TEST_F(AutofillControllerJsTest, FillSpecificFormField) {
  web::test::LoadHtml(kHTMLForTestingElements, web_state());

  web::WebFrame* main_frame = WaitForMainFrame();
  ASSERT_TRUE(main_frame);

  // Simulate form parsing to set renderer IDs.
  ExecuteJavaScript(@"__gCrWeb.autofill.extractForms(0, true)");

  NSString* new_value = @"new value";
  EXPECT_NSEQ(new_value,
              ExecuteJavaScript([NSString
                  stringWithFormat:
                      @"var element=document.getElementsByName('lastname')[0];"
                       "var "
                       "data={\"name\":\"lastname\",\"value\":\"%@\","
                       "\"identifier\":\"lastname\",\"renderer_id\":3};"
                       "__gCrWeb.autofill.fillSpecificFormField(data);"
                       "element.value",
                      new_value]));

  EXPECT_NSEQ(
      @YES,
      ExecuteJavaScript([NSString
          stringWithFormat:@"var element=document.getElementsByName('gl')[0];"
                            "var oldValue = element.value;"
                            "var "
                            "data={\"name\":\"lastname\",\"value\":\"%@\","
                            "\"identifier\":\"lastname\",\"renderer_id\":3};"
                            "__gCrWeb.autofill.fillSpecificFormField(data);"
                            "element.value === oldValue",
                           new_value]))
      << "A non-form element's value should changed.";
}

// iOS version of FormAutofillTest.FormCache_ExtractNewForms from
// chrome/renderer/autofill/form_autofill_browsertest.cc
TEST_F(AutofillControllerJsTest, ExtractNewForms) {
  NSArray* testCases = @[
    // An empty form should not be extracted
    @{
      @"html" : @"<FORM name='TestForm' action='http://buh.com'>"
                 "</FORM>",
      @"expected_forms" : @0
    },
    // A form with at least one autofill field is extracted.
    @{
      @"html" : @"<FORM name='TestForm' action='http://buh.com'>"
                 "  <INPUT type='name' id='firstname'/>"
                 "</FORM>",
      @"expected_forms" : @1
    },
    // A form with less than three fields with at least one autocomplete type
    // should be extracted.
    @{
      @"html" : @"<FORM name='TestForm' action='http://buh.com'>"
                 "  <INPUT type='name' id='firstname'"
                 "         autocomplete='given-name'/>"
                 "</FORM>",
      @"expected_forms" : @1
    },
    // A form with three or more fields should be extracted.
    @{
      @"html" : @"<FORM name='TestForm' action='http://buh.com'>"
                 "  <INPUT type='text' id='firstname'/>"
                 "  <INPUT type='text' id='lastname'/>"
                 "  <INPUT type='text' id='email'/>"
                 "  <INPUT type='submit' value='Send'/>"
                 "</FORM>",
      @"expected_forms" : @1
    }
  ];

  for (NSDictionary* testCase in testCases) {
    web::test::LoadHtml(testCase[@"html"], web_state());

    NSString* result =
        ExecuteJavaScript(@"__gCrWeb.autofill.extractForms(true)");
    NSArray* resultArray = [NSJSONSerialization
        JSONObjectWithData:[result dataUsingEncoding:NSUTF8StringEncoding]
                   options:0
                     error:nil];
    ASSERT_NSNE(nil, resultArray);
    NSUInteger expectedCount =
        [testCase[@"expected_forms"] unsignedIntegerValue];
    EXPECT_EQ(expectedCount, [resultArray count])
        << base::SysNSStringToUTF8(testCase[@"html"]);
  }
}

// Test sanitizedFieldIsEmpty
TEST_F(AutofillControllerJsTest, SanitizedFieldIsEmpty) {
  web::test::LoadHtml(@"<html></html>", web_state());
  NSArray* tests = @[
    @[ @"--  (())//||__", @YES ], @[ @"  --  (())__  ", @YES ],
    @[ @"  --  (()c)__  ", @NO ], @[ @"123-456-7890", @NO ], @[ @"", @YES ]
  ];
  for (NSArray* test in tests) {
    NSString* result = ExecuteJavaScript([NSString
        stringWithFormat:@"__gCrWeb.autofill.sanitizedFieldIsEmpty('%@');",
                         test[0]]);
    EXPECT_NSEQ(result, test[1]);
  }
}

}  // namespace