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

#import "base/containers/span.h"
#import "base/format_macros.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "base/test/scoped_feature_list.h"
#import "components/autofill/core/common/autofill_constants.h"
#import "components/autofill/core/common/autofill_features.h"
#import "components/autofill/ios/browser/autofill_java_script_feature.h"
#import "components/autofill/ios/browser/autofill_util.h"
#import "components/autofill/ios/common/features.h"
#import "components/autofill/ios/form_util/autofill_form_features_java_script_feature.h"
#import "components/autofill/ios/form_util/autofill_test_with_web_state.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/chrome/test/ios_chrome_scoped_testing_local_state.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/javascript_test.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;
};

// 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(
    base::span<const ElementByName> elements) {
  NSMutableArray* array = [NSMutableArray array];
  for (const ElementByName& element : elements) {
    NSString* query = GetElementByNameJavaScript(element);
    [array addObject:query];
  }
  return array;
}

// clang-format off
NSString* const 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='date' name='bday'>"
    "  <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", @"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' "
          "pattern='.*'/>"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'* First name:'", @"label",
          @"'firstname'", @"identifier",
          @"'firstname'", @"name",
          @"'text'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"'John'", @"value",
          @"'John'", @"value_option_text",
          @"undefined", @"option_values",
          @"undefined", @"option_texts",
          @".*", @"pattern_attribute",
          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",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"'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",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"'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='john@example.com'/>"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'* Email:'", @"label",
          @"'email'", @"identifier",
          @"'email'", @"name",
          @"'email'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"'john@example.com'", @"value",
          @"'john@example.com'", @"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",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"'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",
          @"undefined", @"max_length",
          @"false", @"should_autocomplete",
          @"'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",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"''", @"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",
          @"undefined", @"max_length",
          @"false", @"should_autocomplete",
          @"''", @"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",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"'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",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"'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",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"'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='john@example.com'/></TD>"
          "</TR>"
          "</TABLE>"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'* First name:'", @"label",
          @"'tabletdname'", @"identifier",
          @"'tabletdname'", @"name",
          @"'text'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"'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",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"'john@example.com'", @"value",
          @"'john@example.com'", @"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='john@example.com'/></TD>"
          "</TR>"
          "</TABLE>"),
      [NSDictionary dictionaryWithObjectsAndKeys:
          @"'* First name:'", @"label",
          @"'nameintableth'", @"identifier",
          @"'nameintableth'", @"name",
          @"'text'", @"form_control_type",
          @"undefined", @"autocomplete_attribute",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"'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",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"'john@example.com'", @"value",
          @"'john@example.com'", @"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",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"'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",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"'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",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"'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",
          @"undefined", @"max_length",
          @"true", @"should_autocomplete",
          @"'Tennis'", @"value",
          @"'Tennis'", @"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",
          @"0", @"max_length",
          @"true", @"should_autocomplete",
          @"'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",
          @"0", @"max_length",
          @"true", @"should_autocomplete",
          @"'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:@"%@['%@'][%lu] === %@", results,
                                             attribute,
                                             static_cast<unsigned long>(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'][%lu]", results,
                                   static_cast<unsigned long>(controlCount)],
              expectedDict, attributed_to_check, controlCount);
      [verifying_javascripts addObject:itemVerifyingJavaScripts];
    }
  }
  return [verifying_javascripts componentsJoinedByString:@"&&"];
}

// Test fixture to test autofill controller.
class AutofillControllerJsTest : public web::JavascriptTest {
 public:
  AutofillControllerJsTest()
      : web_client_(std::make_unique<ChromeWebClient>()) {
    profile_ = TestProfileIOS::Builder().Build();

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

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

  void SetUp() override {
    JavascriptTest::SetUp();

    // Inject default feature flag placeholders before loading scripts.
    NSString* const placeholders = autofill::test::GetAutofillTestPlaceholders();
    WKUserScript* script = [[WKUserScript alloc]
          initWithSource:placeholders
           injectionTime:WKUserScriptInjectionTimeAtDocumentStart
        forMainFrameOnly:NO];
    [web_view().configuration.userContentController addUserScript:script];

    AddGCrWebScript();
    AddUserScript(@"autofill_form_features");
    AddUserScript(@"fill_util_test");

    ASSERT_TRUE(
        web::test::LoadHtml(web_view(), kHTMLForTestingElements,
                            [NSURL URLWithString:@"https://chromium.test/"]));
  }

  web::WebFrame* WaitForMainFrame() {
    __block web::WebFrame* main_frame = nullptr;
    EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
      web::WebFramesManager* frames_manager =
          autofill::GetWebFramesManagerForAutofill(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,
      base::span<const ElementByName> elements_with_true_expected);

  // Helper method that EXPECTs
  // `webFormControlElementToFormField` method from fill_test_api API. This
  // method applies `webFormControlElementToFormField` method from fill_test_api
  // API 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 `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 `webFormElementToFormData` method from fill_test_api API on all the
  // test data.
  void TestWebFormElementToFormData(NSArray* test_items);

  // EXPECTs `extractNewForms` function from autofill API 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);

  // Rolls up the `user_script`, and its dependencies, with `java_script` into
  // one NSString.
  NSString* RollupJavaScriptWithUserScript(NSString* java_script,
                                           NSString* user_script);

  // Re-injects user scripts with specific feature flag placeholder values.
  void InjectUserScriptsWithPlaceholders(
      const autofill::test::AutofillPlaceholderConfig& config);

  web::ScopedTestingWebClient web_client_;
  web::WebTaskEnvironment task_environment_;
  std::unique_ptr<TestProfileIOS> profile_;
  std::unique_ptr<web::WebState> web_state_;
};

void AutofillControllerJsTest::TestExecutingBooleanJavaScriptOnElement(
    NSString* javascript,
    base::span<const ElementByName> elements_with_true_expected) {
  // Elements in `kHTMLForTestingElements`.
  constexpr auto kElementsByName = std::to_array<ElementByName>({
      {"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},
      {"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},
  });

  ASSERT_TRUE(
      web::test::LoadHtml(web_view(), kHTMLForTestingElements,
                          [NSURL URLWithString:@"https://chromium.test/"]));
  ExecuteBooleanJavaScriptOnElementsAndCheck(
      javascript, GetElementsByNameJavaScripts(kElementsByName),
      GetElementsByNameJavaScripts(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),
                web::test::ExecuteJavaScript(web_view(), 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],
                web::test::ExecuteJavaScript(web_view(), js_to_execute));
  }
}

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

NSString* AutofillControllerJsTest::RollupJavaScriptWithUserScript(
    NSString* java_script,
    NSString* user_script) {
  NSArray<NSString*>* user_scripts = @[ @"gcrweb", user_script ];
  NSMutableString* rollup_script = [NSMutableString string];
  for (NSString* script in user_scripts) {
    [rollup_script appendString:web::test::GetPageScript(script)];
  }
  [rollup_script appendString:java_script];
  return rollup_script;
}

void AutofillControllerJsTest::InjectUserScriptsWithPlaceholders(
    const autofill::test::AutofillPlaceholderConfig& config) {
  [web_view().configuration.userContentController removeAllUserScripts];

  NSString* const placeholders =
      autofill::test::GetAutofillTestPlaceholders(config);
  WKUserScript* script = [[WKUserScript alloc]
        initWithSource:placeholders
         injectionTime:WKUserScriptInjectionTimeAtDocumentStart
      forMainFrameOnly:NO];
  [web_view().configuration.userContentController addUserScript:script];

  AddGCrWebScript();
  AddUserScript(@"autofill_form_features");
  AddUserScript(@"fill_util_test");
}

TEST_F(AutofillControllerJsTest, HasTagName) {
  constexpr auto kElementsExpectingTrue = std::to_array<ElementByName>({
      {"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},
      {"submit", 0, -1},
  });

  TestExecutingBooleanJavaScriptOnElement(
      @"__gCrWeb.getRegisteredApi('fill_test_api')."
      @"getFunction('hasTagName')(%@, 'input')",
      kElementsExpectingTrue);
}

NSString* GetCombineAndCollapseWhitespaceScript(NSString* str1,
                                                NSString* str2,
                                                BOOL separate_with_space) {
  return
      [NSString stringWithFormat:
                    @"__gCrWeb.getRegisteredApi('fill_test_api')."
                    @"getFunction('combineAndCollapseWhitespace')(%@, %@, %@)",
                    str1, str2, separate_with_space ? @"true" : @"false"];
}

TEST_F(AutofillControllerJsTest, CombineAndCollapseWhitespace) {
  NSString* js_to_execute =
      GetCombineAndCollapseWhitespaceScript(@"'foo'", @"'bar'", false);
  EXPECT_NSEQ(@"foobar",
              web::test::ExecuteJavaScript(web_view(), js_to_execute));

  js_to_execute =
      GetCombineAndCollapseWhitespaceScript(@"'foo'", @"'bar'", true);
  EXPECT_NSEQ(@"foo bar",
              web::test::ExecuteJavaScript(web_view(), js_to_execute));

  js_to_execute =
      GetCombineAndCollapseWhitespaceScript(@"'foo '", @"'bar'", false);
  EXPECT_NSEQ(@"foo bar",
              web::test::ExecuteJavaScript(web_view(), js_to_execute));

  js_to_execute =
      GetCombineAndCollapseWhitespaceScript(@"'foo'", @"' bar'", false);
  EXPECT_NSEQ(@"foo bar",
              web::test::ExecuteJavaScript(web_view(), js_to_execute));

  js_to_execute =
      GetCombineAndCollapseWhitespaceScript(@"'foo'", @"' bar'", true);
  EXPECT_NSEQ(@"foo bar",
              web::test::ExecuteJavaScript(web_view(), js_to_execute));

  js_to_execute =
      GetCombineAndCollapseWhitespaceScript(@"'foo  '", @"'  bar'", false);
  EXPECT_NSEQ(@"foo bar",
              web::test::ExecuteJavaScript(web_view(), js_to_execute));

  js_to_execute =
      GetCombineAndCollapseWhitespaceScript(@"'foo'", @"'bar '", false);
  EXPECT_NSEQ(@"foobar ",
              web::test::ExecuteJavaScript(web_view(), js_to_execute));

  js_to_execute =
      GetCombineAndCollapseWhitespaceScript(@"' foo'", @"'bar'", true);
  EXPECT_NSEQ(@" foo bar",
              web::test::ExecuteJavaScript(web_view(), js_to_execute));
}

void AutofillControllerJsTest::TestInputElementDataEvaluation(
    NSString* javascripts_statement,
    NSString* attribute_name,
    NSArray* test_data,
    NSString* tag_name) {
  NSString* html_fragment = [test_data objectAtIndex:0U];
  ASSERT_TRUE(
      web::test::LoadHtml(web_view(), html_fragment,
                          [NSURL URLWithString:@"https://chromium.test/"]));

  for (NSUInteger i = 1; i < [test_data count]; ++i) {
    NSString* get_element_javascripts = [NSString
        stringWithFormat:@"window.document.getElementsByTagName('%@')[%lu]",
                         tag_name, static_cast<unsigned long>(i - 1)];
    id actual = web::test::ExecuteJavaScript(
        web_view(),
        [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.getRegisteredApi('fill_test_api').getFunction('"
      @"inferLabelFromPrevious')",
      @"label", GetTestFormInputElementWithLabelFromPrevious(), @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromPreviousSpan) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.getRegisteredApi('fill_test_api')."
      @"getFunction('inferLabelFromPrevious')",
      @"label", GetTestFormInputElementWithLabelFromPreviousSpan(), @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromPreviousParagraph) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.getRegisteredApi('fill_test_api')."
      @"getFunction('inferLabelFromPrevious')",
      @"label", GetTestFormInputElementWithLabelFromPreviousParagraph(),
      @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromPreviousLabel) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.getRegisteredApi('fill_test_api')."
      @"getFunction('inferLabelFromPrevious')",
      @"label", GetTestFormInputElementWithLabelFromPreviousLabel(), @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromPreviousLabelOtherIgnored) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.getRegisteredApi('fill_test_api')."
      @"getFunction('inferLabelFromPrevious')",
      @"label", GetTestFormInputElementWithLabelFromPreviousLabelOtherIgnored(),
      @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromEnclosingLabelBefore) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.getRegisteredApi('fill_test_api')."
      @"getFunction('inferLabelFromEnclosingLabel')",
      @"label", GetTestFormInputElementWithLabelFromEnclosingLabelBefore(),
      @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromPreviousTextBrAndSpan) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.getRegisteredApi('fill_test_api')."
      @"getFunction('inferLabelFromPrevious')",
      @"label", GetTestFormInputElementWithLabelFromPreviousTextBrAndSpan(),
      @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromListItem) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.getRegisteredApi('fill_test_api')."
      @"getFunction('inferLabelFromListItem')",
      @"label", GetTestFormInputElementWithLabelFromListItem(), @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromTableColumnTD) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.getRegisteredApi('fill_test_api')."
      @"getFunction('inferLabelFromTableColumn')",
      @"label", GetTestFormInputElementWithLabelFromTableColumnTD(), @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromTableColumnTH) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.getRegisteredApi('fill_test_api')."
      @"getFunction('inferLabelFromTableColumn')",
      @"label", GetTestFormInputElementWithLabelFromTableColumnTH(), @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromTableColumnNested) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.getRegisteredApi('fill_test_api')."
      @"getFunction('inferLabelFromTableColumn')",
      @"label", GetTestFormInputElementWithLabelFromTableNested(), @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromTableRow) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.getRegisteredApi('fill_test_api')."
      @"getFunction('inferLabelFromTableRow')",
      @"label", GetTestFormInputElementWithLabelFromTableRow(), @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromDivTable) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.getRegisteredApi('fill_test_api')."
      @"getFunction('inferLabelFromDivTable')",
      @"label", GetTestFormInputElementWithLabelFromDivTable(), @"input");
}

TEST_F(AutofillControllerJsTest, InferLabelFromDefinitionList) {
  TestInputElementDataEvaluation(
      @"__gCrWeb.getRegisteredApi('fill_test_api').getFunction('"
      @"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(),
  ];
  for (NSArray* testingElement in testingElements) {
    TestInputElementDataEvaluation(
        @"__gCrWeb.getRegisteredApi('fill_test_api')."
        @"getFunction('inferLabelForElement')",
        @"label", testingElement, @"input");
  }

  TestInputElementDataEvaluation(@"__gCrWeb.getRegisteredApi('fill_test_api')."
                                 @"getFunction('inferLabelForElement')",
                                 @"label", GetTestFormSelectElement(),
                                 @"select");

  TestInputElementDataEvaluation(
      @"__gCrWeb.getRegisteredApi('fill_test_api')."
      @"getFunction('inferLabelForElement')",
      @"label", GetTestFormSelectElementWithOptgroup(), @"select");
}

TEST_F(AutofillControllerJsTest, IsAutofillableElement) {
  constexpr auto kElementsExpectingTrue = std::to_array<ElementByName>({
      {"firstname", 0, -1},
      {"lastname", 0, -1},
      {"email", 0, -1},
      {"phone", 0, -1},
      {"blog", 0, -1},
      {"expected number of clicks", 0, -1},
      {"pwd", 0, -1},
      {"state", 0, -1},
      {"course", 0, -1},
  });

  TestExecutingBooleanJavaScriptOnElement(
      @"__gCrWeb.getRegisteredApi('fill_test_api')."
      @"getFunction('isAutofillableElement')(%@)",
      kElementsExpectingTrue);
}

TEST_F(AutofillControllerJsTest, GetOptionStringsFromElement) {
  constexpr auto kTestingElements = std::to_array<ElementByName>(
      {{"state", 0, -1}, {"course", 0, -1}, {"cars", 0, -1}});

  ExecuteJavaScriptOnElementsAndCheck(
      @"var field = {};"
       "__gCrWeb.getRegisteredApi('fill_test_api').getFunction('"
       "getOptionStringsFromElement')(%@, field);"
       "__gCrWeb.getRegisteredApi('fill_test_api').getFunction('stringify')("
       "field);",
      GetElementsByNameJavaScripts(kTestingElements), @[
        @("{\"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.
  struct ElementByNameWithNewValue {
    ElementByName element;
    const char* new_value;
  };

  constexpr auto kElements = std::to_array<ElementByNameWithNewValue>({
      {{"firstname", 0, -1}, "new name"},
      {{"state", 0, -1}, "MA"},
  });

  for (const ElementByNameWithNewValue& item : kElements) {
    NSString* get_element_javascript = GetElementByNameJavaScript(item.element);
    NSString* new_value = base::SysUTF8ToNSString(item.new_value);
    EXPECT_NSEQ(
        new_value,
        ExecuteJavaScript([NSString
            stringWithFormat:@"var element=%@;var data={'value':'%@'};"
                             @"__gCrWeb.getRegisteredApi('autofill')."
                             @"getFunction('fillFormField')(data, element);"
                             @"element.value",
                             get_element_javascript, new_value]));
  }

  // Test elements of which the value should not be changed.
  constexpr auto kUnchangedElements = std::to_array<ElementByName>({
      {"hl", 0, -1},    // hidden element
      {"state", 0, 0},  // option element
      {"state", 0, 1},  // option element
  });

  for (const ElementByName& element : kUnchangedElements) {
    NSString* get_element_javascript = GetElementByNameJavaScript(element);
    NSString* actual = ExecuteJavaScript(
        [NSString stringWithFormat:
                      @"var element=%@;"
                      @"var oldValue=element.value; var data={'value':'new'};"
                      @"__gCrWeb.getRegisteredApi('autofill')."
                      @"getFunction('fillFormField')(data, element);"
                      @"element.value === oldValue",
                      get_element_javascript]);
    EXPECT_NSEQ(@YES, actual);
  }
}

TEST_F(AutofillControllerJsTest, IsSelectElement) {
  constexpr auto kElementsExpectingTrue = std::to_array<ElementByName>({
      {"state", 0, -1},
      {"course", 0, -1},
  });

  TestExecutingBooleanJavaScriptOnElement(
      @"__gCrWeb.getRegisteredApi('fill_test_api')."
      @"getFunction('isSelectElement')(%@)",
      kElementsExpectingTrue);
}

TEST_F(AutofillControllerJsTest, ExtractAutofillableElements_Date) {
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitAndEnableFeature(kAutofillSupportDateInput);

  autofill::test::AutofillPlaceholderConfig config;
  config.autofill_support_date_input = true;
  InjectUserScriptsWithPlaceholders(config);

  NSString* html = @"<html><body><form><input type='date' name='bday' "
                   @"id='bday'></form></body></html>";
  web::test::LoadHtml(html, web_state());

  NSString* parameter = @"window.document.getElementsByTagName('form')[0]";
  id result = ExecuteJavaScript([NSString
      stringWithFormat:@"var controlElements="
                        "__gCrWeb.getRegisteredApi('autofill')."
                        "getFunction('extractAutofillableElementsInForm')(%@);"
                        "controlElements[0].id === 'bday'",
                       parameter]);
  EXPECT_NSEQ(@YES, result);
}

TEST_F(AutofillControllerJsTest, ExtractAutofillableElements_Date_Disabled) {
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitAndDisableFeature(kAutofillSupportDateInput);

  autofill::test::AutofillPlaceholderConfig config;
  config.autofill_support_date_input = false;
  InjectUserScriptsWithPlaceholders(config);

  NSString* html = @"<html><body><form><input type='date' name='bday' "
                   @"id='bday'></form></body></html>";
  web::test::LoadHtml(html, web_state());

  NSString* parameter = @"window.document.getElementsByTagName('form')[0]";
  id result = ExecuteJavaScript([NSString
      stringWithFormat:@"var controlElements="
                        "__gCrWeb.getRegisteredApi('autofill')."
                        "getFunction('extractAutofillableElementsInForm')(%@);"
                        "controlElements.length === 0",
                       parameter]);
  EXPECT_NSEQ(@YES, result);
}

TEST_F(AutofillControllerJsTest, IsAutofillableInputElement) {
  constexpr auto kElementsExpectingTrue = std::to_array<ElementByName>({
      {"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.getRegisteredApi('fill_test_api')."
      @"getFunction('isAutofillableInputElement')(%@)",
      kElementsExpectingTrue);
}

TEST_F(AutofillControllerJsTest, ExtractAutofillableElements) {
  web::test::LoadHtml(kHTMLForTestingElements, web_state());
  constexpr auto kExpectedElements = std::to_array<ElementByName>({
      {"firstname", 0, -1},
      {"lastname", 0, -1},
      {"email", 0, -1},
      {"phone", 0, -1},
      {"blog", 0, -1},
      {"expected number of clicks", 0, -1},
      {"pwd", 0, -1},
      {"state", 0, -1},
  });
  NSArray* expected = GetElementsByNameJavaScripts(kExpectedElements);

  NSString* parameter = @"window.document.getElementsByTagName('form')[0]";
  for (NSUInteger index = 0; index < [expected count]; index++) {
    EXPECT_NSEQ(@YES,
                ExecuteJavaScript([NSString
                    stringWithFormat:
                        @"var controlElements="
                         "__gCrWeb.getRegisteredApi('autofill')."
                         "getFunction('extractAutofillableElementsInForm')(%@);"
                         "controlElements[%lu] === %@",
                        parameter, static_cast<unsigned long>(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('%@')[%lu]",
                         tag_name, static_cast<unsigned long>(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);
    NSString* java_script = [NSString
        stringWithFormat:@"%@; var field = {};"
                          "__gCrWeb.getRegisteredApi('fill_test_api')."
                          "getFunction('webFormControlElementToFormField')"
                          "(element, field);%@",
                         get_element_to_test, verifying_javascripts];
    NSString* script =
        j == 1 ? RollupJavaScriptWithUserScript(java_script, @"fill_util_test")
               : java_script;
    NSString* actual = ExecuteJavaScript(script);
    java_script = [NSString
        stringWithFormat:
            @"%@; var field = {};"
             "__gCrWeb.getRegisteredApi('fill_test_api')."
             "getFunction('webFormControlElementToFormField')(element, field);"
             "__gCrWeb.getRegisteredApi('fill_test_api').getFunction('"
             "stringify')(field);",
            get_element_to_test];
    EXPECT_NSEQ(@YES, actual) << base::SysNSStringToUTF8(
        [NSString stringWithFormat:
                      @"webFormControlElementToFormField actual results are: "
                      @"%@, \n"
                       "expected to be verified by %@",
                      ExecuteJavaScript(java_script), 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(),
  ];
  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* java_script =
      [NSString stringWithFormat:@"var form={}; var field={};"
                                 @"(__gCrWeb.getRegisteredApi('fill_test_api')."
                                 @"getFunction('webFormElementToFormData')"
                                 @"(window, %@, null, form, field) "
                                  "=== %@) && %@",
                                 get_form_element_javascripts, expected_result,
                                 verifying_javascripts];
  NSString* script =
      RollupJavaScriptWithUserScript(java_script, @"fill_util_test");
  NSString* actual = ExecuteJavaScript(script);

  java_script =
      [NSString stringWithFormat:@"var form={};"
                                 @"__gCrWeb.getRegisteredApi('fill_test_api')."
                                 @"getFunction('webFormElementToFormData')"
                                 @"(window, %@, null, form, null);"
                                  "__gCrWeb.getRegisteredApi('fill_test_api')."
                                  "getFunction('stringify')(form);",
                                 get_form_element_javascripts];
  script = RollupJavaScriptWithUserScript(java_script, @"fill_util_test");
  EXPECT_NSEQ(@YES, actual) << base::SysNSStringToUTF8([NSString
      stringWithFormat:@"Actual:\n%@; expected to be verifyied by\n%@",
                       ExecuteJavaScript(script), 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(),
    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~%lu",
                                              static_cast<unsigned long>(i)];
    [verifying_javascripts
        addObject:[NSString stringWithFormat:@"forms[%lu]['name'] === '%@'",
                                             static_cast<unsigned long>(i),
                                             formName]];
    if (is_origin_window_location) {
      [verifying_javascripts
          addObject:[NSString
                        stringWithFormat:
                            @"forms[%lu]['origin'] === window.location.href",
                            static_cast<unsigned long>(i)]];
    }
    // This is the extract mask used by `extractNewForms` function
    // from autofill API.
    [verifying_javascripts
        addObject:GenerateTestItemVerifyingJavaScripts(
                      [NSString stringWithFormat:@"forms[%lu]",
                                                 static_cast<unsigned long>(i)],
                      [expected_items objectAtIndex:i],
                      GetFormFieldAttributeListsToCheck())];
  }

  NSString* actual = ExecuteJavaScript([NSString
      stringWithFormat:@"var forms = "
                       @"__gCrWeb.getRegisteredApi('autofill')."
                       @"getFunction('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.getRegisteredApi('autofill')."
                                     @"getFunction('extractNewForms')(true);"
                                     @"__gCrWeb.getRegisteredApi('fill_test_"
                                     @"api').getFunction('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(),
  ];
  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.getRegisteredApi('autofill')."
                                    "getFunction('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:
                   [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",
        @"pattern_attribute" : @".*",
        @"placeholder_attribute" : @"",
        @"should_autocomplete" : @true,
        @"is_focusable" : @true,
        @"is_user_edited_deprecated" : @false,
        @"value" : @"John",
        @"label" : @"* First name:"
      },
      @{
        @"aria_description" : @"",
        @"aria_label" : @"",
        @"name" : @"nameintableth",
        @"name_attribute" : @"nameintableth",
        @"id_attribute" : @"nameintableth",
        @"identifier" : @"nameintableth",
        @"renderer_id" : @"3",
        @"form_control_type" : @"text",
        @"pattern_attribute" : @"",
        @"placeholder_attribute" : @"",
        @"should_autocomplete" : @true,
        @"is_focusable" : @true,
        @"is_user_edited_deprecated" : @false,
        @"value" : @"John",
        @"label" : @"* First name:"
      },
      @{
        @"aria_description" : @"",
        @"aria_label" : @"",
        @"name" : @"emailtableth",
        @"name_attribute" : @"",
        @"id_attribute" : @"emailtableth",
        @"identifier" : @"emailtableth",
        @"renderer_id" : @"4",
        @"form_control_type" : @"email",
        @"pattern_attribute" : @"",
        @"placeholder_attribute" : @"",
        @"should_autocomplete" : @true,
        @"is_focusable" : @true,
        @"is_user_edited_deprecated" : @false,
        @"value" : @"john@example.com",
        @"label" : @"Email:"
      },
      @{
        @"aria_description" : @"",
        @"aria_label" : @"",
        @"name" : @"pwd",
        @"name_attribute" : @"pwd",
        @"id_attribute" : @"pwd",
        @"identifier" : @"pwd",
        @"renderer_id" : @"5",
        @"form_control_type" : @"password",
        @"pattern_attribute" : @"",
        @"placeholder_attribute" : @"",
        @"autocomplete_attribute" : @"off",
        @"should_autocomplete" : @false,
        @"is_focusable" : @true,
        @"is_user_edited_deprecated" : @false,
        @"value" : @"",
        @"label" : @"* Password:"
      },
      @{
        @"aria_description" : @"",
        @"aria_label" : @"",
        @"name" : @"state",
        @"name_attribute" : @"state",
        @"id_attribute" : @"state",
        @"identifier" : @"state",
        @"renderer_id" : @"6",
        @"form_control_type" : @"select-one",
        @"pattern_attribute" : @"",
        @"placeholder_attribute" : @"",
        @"max_length" : @0,
        @"is_focusable" : @1,
        @"is_user_edited_deprecated" : @false,
        @"option_values" : @[ @"CA", @"TX" ],
        @"option_texts" : @[ @"California", @"Texas" ],
        @"should_autocomplete" : @1,
        @"value" : @"CA",
        @"label" : @"State:"
      }
    ]
  };

  NSString* result = ExecuteJavaScript(
      [NSString stringWithFormat:@"__gCrWeb.getRegisteredApi('autofill')."
                                 @"getFunction('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.getRegisteredApi('autofill')."
                        "getFunction('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.getRegisteredApi('autofill').getFunction('"
                        "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 the is_user_edited_deprecated bit is correctly set in the extracted
// fields when the fix is enabled. This test is limited as it can't test if
// is_user_edited_deprecated can be set to true because there is no way to
// emulate an input from the user in the unittest (i.e. Event.isTrusted set to
// true) - this would required popping up a keyboard.
TEST_F(AutofillControllerJsTest, ExtractForms_UserEdited_FixEnabled) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeature(kAutofillCorrectUserEditedBitInParsedField);

  // Load html form that consist of 2 plain text inputs that the user can type
  // in.
  NSString* html = @"<html><body>"
                    "<form id='form1'>"
                    "<input type='text' id='input1' />"
                    "<input type='text' id='input2' />"
                    "</form>"
                    "</body></html>";
  web::test::LoadHtml(html, web_state());

  // Emulate a user input on the first input element.
  EXPECT_NSEQ(@YES, ExecuteJavaScript(
                        @"document.getElementById('input1').dispatchEvent(new "
                        @"Event('input', { bubbles: true }))"));

  // Verify that the first <input> element that received the scripted input
  // event has is_user_edited_deprecated still set to false because the user
  // input wasn't trusted, and that the second <input> has
  // is_user_edited_deprecated set to false because it didn't receive any user
  // input event.
  NSString* verifying_javascript =
      @"!forms[0].fields[0].is_user_edited_deprecated && "
      @"!forms[0].fields[1].is_user_edited_deprecated;";
  EXPECT_NSEQ(@YES,
              ExecuteJavaScript([NSString
                  stringWithFormat:@"var forms = "
                                    "__gCrWeb.getRegisteredApi('autofill')."
                                    "getFunction('extractNewForms')(false); %@",
                                   verifying_javascript]));
}

// 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());

  // 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.getRegisteredApi('autofill')."
                                    "getFunction('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());

  // 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.getRegisteredApi('autofill')."
                                    "getFunction('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.getRegisteredApi('autofill').getFunction('"
                    @"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.getRegisteredApi('autofill')."
                       "getFunction('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.getRegisteredApi('autofill')."
                            "getFunction('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.getRegisteredApi('autofill')."
                    @"getFunction('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.getRegisteredApi('autofill')."
                       "getFunction('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.getRegisteredApi('autofill')."
                            "getFunction('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.getRegisteredApi('autofill')."
                          @"getFunction('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.getRegisteredApi('autofill')."
                         @"getFunction('sanitizedFieldIsEmpty')('%@');",
                         test[0]]);
    EXPECT_NSEQ(result, test[1]);
  }
}

// Tests that scrollFieldIntoView scrolls the target field into view.
TEST_F(AutofillControllerJsTest, ScrollFieldIntoView) {
  web::test::LoadHtml(@"<html><body>"
                       "<input type='text' name='first' id='first' />"
                       "<div style='height: 3000px;'></div>"
                       "<input type='text' name='second' id='second' />"
                       "</body></html>",
                      web_state());

  // Extract forms to assign unique renderer IDs.
  ExecuteJavaScript(@"__gCrWeb.getRegisteredApi('autofill')."
                    @"getFunction('extractForms')(false)");

  // Initial scroll position should be at the top.
  NSNumber* initialY = ExecuteJavaScript(@"window.scrollY");
  EXPECT_EQ(0, [initialY intValue]);

  // Scroll the second element (renderer ID 2) into view.
  ExecuteJavaScript(@"__gCrWeb.getRegisteredApi('autofill')."
                    @"getFunction('scrollFieldIntoView')(2)");

  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    NSNumber* scrolledY = ExecuteJavaScript(@"window.scrollY");
    return [scrolledY intValue] > 0;
  }));
}

// Tests that scrollFieldIntoView safely no-ops for a non-existent renderer ID.
TEST_F(AutofillControllerJsTest, ScrollFieldIntoViewNonExistentRendererId) {
  web::test::LoadHtml(@"<html><body>"
                       "<input type='text' name='first' id='first' />"
                       "<div style='height: 3000px;'></div>"
                       "<input type='text' name='second' id='second' />"
                       "</body></html>",
                      web_state());

  // Extract forms to assign unique renderer IDs.
  ExecuteJavaScript(@"__gCrWeb.getRegisteredApi('autofill')."
                    @"getFunction('extractForms')(false)");

  // Attempt to scroll a non-existent element (renderer ID 99999) into view.
  ExecuteJavaScript(@"__gCrWeb.getRegisteredApi('autofill')."
                    @"getFunction('scrollFieldIntoView')(99999)");

  NSNumber* scrollY = ExecuteJavaScript(@"window.scrollY");
  EXPECT_EQ(0, [scrollY intValue]);
}

// Tests that scrollFieldIntoView does not scroll when the target is in a
// non-scrollable overflow: hidden container.
TEST_F(AutofillControllerJsTest, ScrollFieldIntoViewOverflowHidden) {
  web::test::LoadHtml(
      @"<html><body>"
       "<div style='height: 100px; overflow: hidden;'>"
       "  <div style='height: 3000px;'></div>"
       "  <input type='text' name='hidden_input' id='hidden_input' />"
       "</div>"
       "</body></html>",
      web_state());

  ExecuteJavaScript(@"__gCrWeb.getRegisteredApi('autofill')."
                    @"getFunction('extractForms')(false)");

  ExecuteJavaScript(@"__gCrWeb.getRegisteredApi('autofill')."
                    @"getFunction('scrollFieldIntoView')(1)");

  NSNumber* scrollY = ExecuteJavaScript(@"window.scrollY");
  EXPECT_EQ(0, [scrollY intValue]);
}

}  // namespace
