Bug 1339731 - Refactor FormAutofillHandler to support multiple section machanism. r=lchang,ralin draft
authorSean Lee <selee@mozilla.com>
Mon, 20 Nov 2017 08:21:30 +0800
changeset 700394 aa30e44c554ccb6076422244da0fcb69134c660f
parent 700200 3a0aac55195fd50ca4b1b41be450bfbaaafc191c
child 700395 0c91687cd45d66ec12a68d5b48c0c7bdbea36048
push id89801
push userbmo:selee@mozilla.com
push dateMon, 20 Nov 2017 03:18:51 +0000
reviewerslchang, ralin
bugs1339731
milestone59.0a1
Bug 1339731 - Refactor FormAutofillHandler to support multiple section machanism. r=lchang,ralin MozReview-Commit-ID: D9g5fKTeTaL
browser/app/profile/firefox.js
browser/extensions/formautofill/FormAutofillContent.jsm
browser/extensions/formautofill/FormAutofillHandler.jsm
browser/extensions/formautofill/FormAutofillHeuristics.jsm
browser/extensions/formautofill/FormAutofillParent.jsm
browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
browser/extensions/formautofill/test/unit/head.js
browser/extensions/formautofill/test/unit/test_autofillFormFields.js
browser/extensions/formautofill/test/unit/test_collectFormFields.js
browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1704,16 +1704,17 @@ pref("extensions.formautofill.creditCard
 // 0: none
 // 1: submitted a manually-filled credit card form (but didn't see the doorhanger
 //    because of a duplicate profile in the storage)
 // 2: saw the doorhanger
 // 3: submitted an autofill'ed credit card form
 pref("extensions.formautofill.creditCards.used", 0);
 pref("extensions.formautofill.firstTimeUse", true);
 pref("extensions.formautofill.heuristics.enabled", true);
+pref("extensions.formautofill.section.enabled", true);
 pref("extensions.formautofill.loglevel", "Warn");
 
 // Whether or not to restore a session with lazy-browser tabs.
 pref("browser.sessionstore.restore_tabs_lazily", true);
 
 pref("browser.suppress_first_window_animation", true);
 
 // Preferences for Photon onboarding system extension
--- a/browser/extensions/formautofill/FormAutofillContent.jsm
+++ b/browser/extensions/formautofill/FormAutofillContent.jsm
@@ -97,18 +97,19 @@ AutofillProfileAutoCompleteSearch.protot
     this.forceStop = false;
 
     let savedFieldNames = FormAutofillContent.savedFieldNames;
 
     let focusedInput = formFillController.focusedInput;
     let info = FormAutofillContent.getInputDetails(focusedInput);
     let isAddressField = FormAutofillUtils.isAddressField(info.fieldName);
     let handler = FormAutofillContent.getFormHandler(focusedInput);
-    let allFieldNames = handler.allFieldNames;
-    let filledRecordGUID = isAddressField ? handler.address.filledRecordGUID : handler.creditCard.filledRecordGUID;
+    let section = handler.getSectionByElement(focusedInput);
+    let allFieldNames = section.allFieldNames;
+    let filledRecordGUID = isAddressField ? section.address.filledRecordGUID : section.creditCard.filledRecordGUID;
     let searchPermitted = isAddressField ?
                           FormAutofillUtils.isAutofillAddressesEnabled :
                           FormAutofillUtils.isAutofillCreditCardsEnabled;
 
     ProfileAutocomplete.lastProfileAutoCompleteFocusedInput = focusedInput;
     // Fallback to form-history if ...
     //   - specified autofill feature is pref off.
     //   - no profile can fill the currently-focused input.
@@ -144,17 +145,17 @@ AutofillProfileAutoCompleteSearch.protot
 
     this._getRecords(data).then((records) => {
       if (this.forceStop) {
         return;
       }
       // Sort addresses by timeLastUsed for showing the lastest used address at top.
       records.sort((a, b) => b.timeLastUsed - a.timeLastUsed);
 
-      let adaptedRecords = handler.getAdaptedProfiles(records);
+      let adaptedRecords = handler.getAdaptedProfiles(records, focusedInput);
       let result = null;
       if (isAddressField) {
         result = new AddressResult(searchString,
                                    info.fieldName,
                                    allFieldNames,
                                    adaptedRecords,
                                    {});
       } else {
@@ -476,17 +477,17 @@ var FormAutofillContent = {
    */
   getFormDetails(element) {
     let formHandler = this.getFormHandler(element);
     return formHandler ? formHandler.fieldDetails : null;
   },
 
   getAllFieldNames(element) {
     let formHandler = this.getFormHandler(element);
-    return formHandler ? formHandler.allFieldNames : null;
+    return formHandler ? formHandler.getAllFieldNames(element) : null;
   },
 
   identifyAutofillFields(element) {
     this.log.debug("identifyAutofillFields:", "" + element.ownerDocument.location);
 
     if (!this.savedFieldNames) {
       this.log.debug("identifyAutofillFields: savedFieldNames are not known yet");
       Services.cpmm.sendAsyncMessage("FormAutofill:InitStorage");
--- a/browser/extensions/formautofill/FormAutofillHandler.jsm
+++ b/browser/extensions/formautofill/FormAutofillHandler.jsm
@@ -1,16 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /*
  * Defines a handler object to represent forms that autofill can handle.
  */
 
+/* exported FormAutofillHandler */
+
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["FormAutofillHandler"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@@ -20,212 +22,91 @@ Cu.import("resource://formautofill/FormA
 XPCOMUtils.defineLazyModuleGetter(this, "FormAutofillHeuristics",
                                   "resource://formautofill/FormAutofillHeuristics.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormLikeFactory",
                                   "resource://gre/modules/FormLikeFactory.jsm");
 
 this.log = null;
 FormAutofillUtils.defineLazyLogGetter(this, this.EXPORTED_SYMBOLS[0]);
 
-/**
- * Handles profile autofill for a DOM Form element.
- * @param {FormLike} form Form that need to be auto filled
- */
-function FormAutofillHandler(form) {
-  this._updateForm(form);
-  this.winUtils = this.form.rootElement.ownerGlobal.QueryInterface(Ci.nsIInterfaceRequestor)
-    .getInterface(Ci.nsIDOMWindowUtils);
-
-  this.address = {
-    /**
-     * Similar to the `fieldDetails` above but contains address fields only.
-     */
-    fieldDetails: [],
-    /**
-     * String of the filled address' guid.
-     */
-    filledRecordGUID: null,
-  };
-
-  this.creditCard = {
-    /**
-     * Similar to the `fieldDetails` above but contains credit card fields only.
-     */
-    fieldDetails: [],
-    /**
-     * String of the filled creditCard's guid.
-     */
-    filledRecordGUID: null,
-  };
-
-  this._cacheValue = {
-    allFieldNames: null,
-    oneLineStreetAddress: null,
-    matchingSelectOption: null,
-  };
-}
-
-FormAutofillHandler.prototype = {
-  /**
-   * DOM Form element to which this object is attached.
-   */
-  form: null,
-
-  /**
-   * Array of collected data about relevant form fields.  Each item is an object
-   * storing the identifying details of the field and a reference to the
-   * originally associated element from the form.
-   *
-   * The "section", "addressType", "contactType", and "fieldName" values are
-   * used to identify the exact field when the serializable data is received
-   * from the backend.  There cannot be multiple fields which have
-   * the same exact combination of these values.
-   *
-   * A direct reference to the associated element cannot be sent to the user
-   * interface because processing may be done in the parent process.
-   */
-  fieldDetails: null,
-
-  /**
-   * Subcategory of handler that contains address related data.
-   */
-  address: null,
-
-  /**
-   * Subcategory of handler that contains credit card related data.
-   */
-  creditCard: null,
-
-  /**
-   * A WindowUtils reference of which Window the form belongs
-   */
-  winUtils: null,
-
-  /**
-   * Enum for form autofill MANUALLY_MANAGED_STATES values
-   */
-  fieldStateEnum: {
-    // not themed
-    NORMAL: null,
-    // highlighted
-    AUTO_FILLED: "-moz-autofill",
-    // highlighted && grey color text
-    PREVIEW: "-moz-autofill-preview",
-  },
-
-  /**
-   * Time in milliseconds since epoch when a user started filling in the form.
-   */
-  timeStartedFillingMS: null,
-
-  /**
-  * Check the form is necessary to be updated. This function should be able to
-  * detect any changes including all control elements in the form.
-  * @param {HTMLElement} element The element supposed to be in the form.
-  * @returns {boolean} FormAutofillHandler.form is updated or not.
-  */
-  updateFormIfNeeded(element) {
-    // When the following condition happens, FormAutofillHandler.form should be
-    // updated:
-    // * The count of form controls is changed.
-    // * When the element can not be found in the current form.
-    //
-    // However, we should improve the function to detect the element changes.
-    // e.g. a tel field is changed from type="hidden" to type="tel".
-
-    let _formLike;
-    let getFormLike = () => {
-      if (!_formLike) {
-        _formLike = FormLikeFactory.createFromField(element);
-      }
-      return _formLike;
+class FormAutofillSection {
+  constructor(fieldDetails, winUtils) {
+    this.address = {
+      /**
+       * Similar to the `_validDetails` but contains address fields only.
+       */
+      fieldDetails: [],
+      /**
+       * String of the filled address' guid.
+       */
+      filledRecordGUID: null,
+    };
+    this.creditCard = {
+      /**
+       * Similar to the `_validDetails` but contains credit card fields only.
+       */
+      fieldDetails: [],
+      /**
+       * String of the filled creditCard's' guid.
+       */
+      filledRecordGUID: null,
     };
 
-    let currentForm = element.form;
-    if (!currentForm) {
-      currentForm = getFormLike();
-    }
-
-    if (currentForm.elements.length != this.form.elements.length) {
-      log.debug("The count of form elements is changed.");
-      this._updateForm(getFormLike());
-      return true;
-    }
-
-    if (this.form.elements.indexOf(element) === -1) {
-      log.debug("The element can not be found in the current form.");
-      this._updateForm(getFormLike());
-      return true;
-    }
-
-    return false;
-  },
+    /**
+     * Enum for form autofill MANUALLY_MANAGED_STATES values
+     */
+    this._FIELD_STATE_ENUM = {
+      // not themed
+      NORMAL: null,
+      // highlighted
+      AUTO_FILLED: "-moz-autofill",
+      // highlighted && grey color text
+      PREVIEW: "-moz-autofill-preview",
+    };
 
-  /**
-  * Update the form with a new FormLike, and the related fields should be
-  * updated or clear to ensure the data consistency.
-  * @param {FormLike} form a new FormLike to replace the original one.
-  */
-  _updateForm(form) {
-    this.form = form;
-    this.fieldDetails = [];
+    this.winUtils = winUtils;
 
-    if (this.address) {
-      this.address.fieldDetails = [];
-    }
-    if (this.creditCard) {
-      this.creditCard.fieldDetails = [];
-    }
-  },
-
-  /**
-   * Set fieldDetails from the form about fields that can be autofilled.
-   *
-   * @param {boolean} allowDuplicates
-   *        true to remain any duplicated field details otherwise to remove the
-   *        duplicated ones.
-   * @returns {Array} The valid address and credit card details.
-   */
-  collectFormFields(allowDuplicates = false) {
-    this._cacheValue.allFieldNames = null;
-    let fieldDetails = FormAutofillHeuristics.getFormInfo(this.form, allowDuplicates);
-    this.fieldDetails = fieldDetails ? fieldDetails : [];
-    log.debug("Collected details on", this.fieldDetails.length, "fields");
-
-    this.address.fieldDetails = this.fieldDetails.filter(
+    this.address.fieldDetails = fieldDetails.filter(
       detail => FormAutofillUtils.isAddressField(detail.fieldName)
     );
-    this.creditCard.fieldDetails = this.fieldDetails.filter(
-      detail => FormAutofillUtils.isCreditCardField(detail.fieldName)
-    );
-
     if (this.address.fieldDetails.length < FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD) {
-      log.debug("Ignoring address related fields since it has only",
+      log.debug("Ignoring address related fields since the section has only",
                 this.address.fieldDetails.length,
                 "field(s)");
       this.address.fieldDetails = [];
     }
 
+    this.creditCard.fieldDetails = fieldDetails.filter(
+      detail => FormAutofillUtils.isCreditCardField(detail.fieldName)
+    );
     if (!this._isValidCreditCardForm(this.creditCard.fieldDetails)) {
-      log.debug("Invalid credit card form");
+      log.debug("Invalid credit card section.");
       this.creditCard.fieldDetails = [];
     }
 
-    let validDetails = Array.of(...(this.address.fieldDetails),
-                                ...(this.creditCard.fieldDetails));
-    for (let detail of validDetails) {
-      let input = detail.elementWeakRef.get();
-      if (!input) {
-        continue;
-      }
-      input.addEventListener("input", this);
-    }
+    this._cacheValue = {
+      allFieldNames: null,
+      oneLineStreetAddress: null,
+      matchingSelectOption: null,
+    };
 
-    return validDetails;
-  },
+    this._validDetails = Array.of(...(this.address.fieldDetails),
+                                  ...(this.creditCard.fieldDetails));
+    log.debug(this._validDetails.length, "valid fields in the section is collected.");
+  }
+
+  get validDetails() {
+    return this._validDetails;
+  }
+
+  getFieldDetailByElement(element) {
+    return this._validDetails.find(
+      detail => detail.elementWeakRef.get() == element
+    );
+  }
 
   _isValidCreditCardForm(fieldDetails) {
     let ccNumberReason = "";
     let hasCCNumber = false;
     let hasExpiryDate = false;
 
     for (let detail of fieldDetails) {
       switch (detail.fieldName) {
@@ -237,58 +118,52 @@ FormAutofillHandler.prototype = {
         case "cc-exp-month":
         case "cc-exp-year":
           hasExpiryDate = true;
           break;
       }
     }
 
     return hasCCNumber && (ccNumberReason == "autocomplete" || hasExpiryDate);
-  },
+  }
+
+  get allFieldNames() {
+    if (!this._cacheValue.allFieldNames) {
+      this._cacheValue.allFieldNames = this._validDetails.map(record => record.fieldName);
+    }
+    return this._cacheValue.allFieldNames;
+  }
 
   getFieldDetailByName(fieldName) {
-    return this.fieldDetails.find(detail => detail.fieldName == fieldName);
-  },
-
-  getFieldDetailByElement(element) {
-    return this.fieldDetails.find(
-      detail => detail.elementWeakRef.get() == element
-    );
-  },
+    return this._validDetails.find(detail => detail.fieldName == fieldName);
+  }
 
   getFieldDetailsByElement(element) {
     let fieldDetail = this.getFieldDetailByElement(element);
     if (!fieldDetail) {
       return [];
     }
     if (FormAutofillUtils.isAddressField(fieldDetail.fieldName)) {
       return this.address.fieldDetails;
     }
     if (FormAutofillUtils.isCreditCardField(fieldDetail.fieldName)) {
       return this.creditCard.fieldDetails;
     }
     return [];
-  },
-
-  get allFieldNames() {
-    if (!this._cacheValue.allFieldNames) {
-      this._cacheValue.allFieldNames = this.fieldDetails.map(record => record.fieldName);
-    }
-    return this._cacheValue.allFieldNames;
-  },
+  }
 
   _getOneLineStreetAddress(address) {
     if (!this._cacheValue.oneLineStreetAddress) {
       this._cacheValue.oneLineStreetAddress = {};
     }
     if (!this._cacheValue.oneLineStreetAddress[address]) {
       this._cacheValue.oneLineStreetAddress[address] = FormAutofillUtils.toOneLineAddress(address);
     }
     return this._cacheValue.oneLineStreetAddress[address];
-  },
+  }
 
   _addressTransformer(profile) {
     if (profile["street-address"]) {
       // "-moz-street-address-one-line" is used by the labels in
       // ProfileAutoCompleteResult.
       profile["-moz-street-address-one-line"] = this._getOneLineStreetAddress(profile["street-address"]);
       let streetAddressDetail = this.getFieldDetailByName("street-address");
       if (streetAddressDetail &&
@@ -302,17 +177,17 @@ FormAutofillHandler.prototype = {
         if (this.getFieldDetailByName(f)) {
           if (waitForConcat.length > 1) {
             profile[f] = FormAutofillUtils.toOneLineAddress(waitForConcat);
           }
           waitForConcat = [];
         }
       }
     }
-  },
+  }
 
   /**
    * Replace tel with tel-national if tel violates the input element's
    * restriction.
    * @param {Object} profile
    *        A profile to be converted.
    */
   _telTransformer(profile) {
@@ -356,17 +231,17 @@ FormAutofillHandler.prototype = {
       if (testPattern(profile["tel-national"])) {
         profile.tel = profile["tel-national"];
       }
     } else if (element.maxLength) {
       if (profile["tel-national"].length <= element.maxLength) {
         profile.tel = profile["tel-national"];
       }
     }
-  },
+  }
 
   _matchSelectOptions(profile) {
     if (!this._cacheValue.matchingSelectOption) {
       this._cacheValue.matchingSelectOption = new WeakMap();
     }
 
     for (let fieldName in profile) {
       let fieldDetail = this.getFieldDetailByName(fieldName);
@@ -394,17 +269,17 @@ FormAutofillHandler.prototype = {
           delete cache[value];
           this._cacheValue.matchingSelectOption.set(element, cache);
         }
         // Delete the field so the phishing hint won't treat it as a "also fill"
         // field.
         delete profile[fieldName];
       }
     }
-  },
+  }
 
   _creditCardExpDateTransformer(profile) {
     if (!profile["cc-exp"]) {
       return;
     }
 
     let detail = this.getFieldDetailByName("cc-exp");
     if (!detail) {
@@ -430,39 +305,39 @@ FormAutofillHandler.prototype = {
     }
 
     result = /(?:[^y]|\b)(y{2,4})\s*([-/\\]*)\s*(m{1,2})(?!m)/i.exec(placeholder);
     if (result) {
       profile["cc-exp"] = String(ccExpYear).substr(-1 * result[1].length) +
                           result[2] +
                           String(ccExpMonth).padStart(result[3].length, "0");
     }
-  },
+  }
 
   getAdaptedProfiles(originalProfiles) {
     for (let profile of originalProfiles) {
       this._addressTransformer(profile);
       this._telTransformer(profile);
       this._matchSelectOptions(profile);
       this._creditCardExpDateTransformer(profile);
     }
     return originalProfiles;
-  },
+  }
 
   /**
    * Processes form fields that can be autofilled, and populates them with the
    * profile provided by backend.
    *
    * @param {Object} profile
    *        A profile to be filled in.
    * @param {HTMLElement} focusedInput
    *        A focused input element needed to determine the address or credit
    *        card field.
    */
-  async autofillFormFields(profile, focusedInput) {
+  async autofillFields(profile, focusedInput) {
     let focusedDetail = this.getFieldDetailByElement(focusedInput);
     if (!focusedDetail) {
       throw new Error("No fieldDetail for the focused input.");
     }
     let targetSet;
     if (FormAutofillUtils.isCreditCardField(focusedDetail.fieldName)) {
       // When Master Password is enabled by users, the decryption process
       // should prompt Master Password dialog to get the decrypted credit
@@ -480,17 +355,17 @@ FormAutofillHandler.prototype = {
       }
       targetSet = this.creditCard;
     } else if (FormAutofillUtils.isAddressField(focusedDetail.fieldName)) {
       targetSet = this.address;
     } else {
       throw new Error("Unknown form fields");
     }
 
-    log.debug("profile in autofillFormFields:", profile);
+    log.debug("profile in autofillFields:", profile);
 
     targetSet.filledRecordGUID = profile.guid;
     for (let fieldDetail of targetSet.fieldDetails) {
       // Avoid filling field value in the following cases:
       // 1. a non-empty input field for an unfocused input
       // 2. the invalid value set
       // 3. value already chosen in select element
 
@@ -527,62 +402,28 @@ FormAutofillHandler.prototype = {
           option.selected = true;
           element.dispatchEvent(new element.ownerGlobal.UIEvent("input", {bubbles: true}));
           element.dispatchEvent(new element.ownerGlobal.Event("change", {bubbles: true}));
         }
         // Autofill highlight appears regardless if value is changed or not
         this.changeFieldState(fieldDetail, "AUTO_FILLED");
       }
     }
-
-    // Handle the highlight style resetting caused by user's correction afterward.
-    log.debug("register change handler for filled form:", this.form);
-    const onChangeHandler = e => {
-      let hasFilledFields;
-
-      if (!e.isTrusted) {
-        return;
-      }
-
-      for (let fieldDetail of targetSet.fieldDetails) {
-        let element = fieldDetail.elementWeakRef.get();
-
-        if (!element) {
-          return;
-        }
-
-        if (e.target == element || (e.target == element.form && e.type == "reset")) {
-          this.changeFieldState(fieldDetail, "NORMAL");
-        }
-
-        hasFilledFields |= (fieldDetail.state == "AUTO_FILLED");
-      }
-
-      // Unregister listeners and clear guid once no field is in AUTO_FILLED state.
-      if (!hasFilledFields) {
-        this.form.rootElement.removeEventListener("input", onChangeHandler);
-        this.form.rootElement.removeEventListener("reset", onChangeHandler);
-        targetSet.filledRecordGUID = null;
-      }
-    };
-
-    this.form.rootElement.addEventListener("input", onChangeHandler);
-    this.form.rootElement.addEventListener("reset", onChangeHandler);
-  },
+  }
 
   /**
    * Populates result to the preview layers with given profile.
    *
    * @param {Object} profile
    *        A profile to be previewed with
    * @param {HTMLElement} focusedInput
    *        A focused input element for determining credit card or address fields.
    */
   previewFormFields(profile, focusedInput) {
-    log.debug("preview profile in autofillFormFields:", profile);
+    log.debug("preview profile: ", profile);
 
     // Always show the decrypted credit card number when Master Password is
     // disabled.
     if (profile["cc-number-decrypted"]) {
       profile["cc-number"] = profile["cc-number-decrypted"];
     }
 
     let fieldDetails = this.getFieldDetailsByElement(focusedInput);
@@ -609,17 +450,17 @@ FormAutofillHandler.prototype = {
         }
       } else if (element.value) {
         // Skip the field if it already has text entered.
         continue;
       }
       element.previewValue = value;
       this.changeFieldState(fieldDetail, value ? "PREVIEW" : "NORMAL");
     }
-  },
+  }
 
   /**
    * Clear preview text and background highlight of all fields.
    *
    * @param {HTMLElement} focusedInput
    *        A focused input element for determining credit card or address fields.
    */
   clearPreviewedFormFields(focusedInput) {
@@ -638,74 +479,101 @@ FormAutofillHandler.prototype = {
       // We keep the state if this field has
       // already been auto-filled.
       if (fieldDetail.state === "AUTO_FILLED") {
         continue;
       }
 
       this.changeFieldState(fieldDetail, "NORMAL");
     }
-  },
+  }
 
   /**
    * Change the state of a field to correspond with different presentations.
    *
    * @param {Object} fieldDetail
    *        A fieldDetail of which its element is about to update the state.
    * @param {string} nextState
    *        Used to determine the next state
    */
   changeFieldState(fieldDetail, nextState) {
     let element = fieldDetail.elementWeakRef.get();
 
     if (!element) {
       log.warn(fieldDetail.fieldName, "is unreachable while changing state");
       return;
     }
-    if (!(nextState in this.fieldStateEnum)) {
+    if (!(nextState in this._FIELD_STATE_ENUM)) {
       log.warn(fieldDetail.fieldName, "is trying to change to an invalid state");
       return;
     }
 
-    for (let [state, mmStateValue] of Object.entries(this.fieldStateEnum)) {
+    for (let [state, mmStateValue] of Object.entries(this._FIELD_STATE_ENUM)) {
       // The NORMAL state is simply the absence of other manually
       // managed states so we never need to add or remove it.
       if (!mmStateValue) {
         continue;
       }
 
       if (state == nextState) {
         this.winUtils.addManuallyManagedState(element, mmStateValue);
       } else {
         this.winUtils.removeManuallyManagedState(element, mmStateValue);
       }
     }
 
     fieldDetail.state = nextState;
-  },
+  }
+
+  clearFieldState(focusedInput) {
+    let fieldDetail = this.getFieldDetailByElement(focusedInput);
+    this.changeFieldState(fieldDetail, "NORMAL");
+    let targetSet;
+    if (FormAutofillUtils.isAddressField(focusedInput)) {
+      targetSet = this.address;
+    } else if (FormAutofillUtils.isCreditCardField(focusedInput)) {
+      targetSet = this.creditCard;
+    }
+
+    if (!targetSet.fieldDetails.some(detail => detail.state == "AUTO_FILLED")) {
+      targetSet.filledRecordGUID = null;
+    }
+  }
+
+  resetFieldStates() {
+    for (let fieldDetail of this._validDetails) {
+      this.changeFieldState(fieldDetail, "NORMAL");
+    }
+    this.address.filledRecordGUID = null;
+    this.creditCard.filledRecordGUID = null;
+  }
+
+  isFilled() {
+    return !!(this.address.filledRecordGUID || this.creditCard.filledRecordGUID);
+  }
 
   _isAddressRecordCreatable(record) {
     let hasName = 0;
     let length = 0;
     for (let key of Object.keys(record)) {
       if (!record[key]) {
         continue;
       }
       if (FormAutofillUtils.getCategoryFromFieldName(key) == "name") {
         hasName = 1;
         continue;
       }
       length++;
     }
     return (length + hasName) >= FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD;
-  },
+  }
 
   _isCreditCardRecordCreatable(record) {
     return record["cc-number"] && FormAutofillUtils.isCCNumber(record["cc-number"]);
-  },
+  }
 
   /**
    * Return the records that is converted from address/creditCard fieldDetails and
    * only valid form records are included.
    *
    * @returns {Object}
    *          Consists of two record objects: address, creditCard. Each one can
    *          be omitted if there's no valid fields. A record object consists of
@@ -789,17 +657,17 @@ FormAutofillHandler.prototype = {
 
     // If both address and credit card exists, skip this metrics because it not a
     // general case and each specific histogram might contains insufficient data set.
     if (data.address && data.creditCard) {
       this.timeStartedFillingMS = null;
     }
 
     return data;
-  },
+  }
 
   _normalizeAddress(address) {
     if (!address) {
       return;
     }
 
     // Normalize Country
     if (address.record.country) {
@@ -832,28 +700,242 @@ FormAutofillHandler.prototype = {
         // number part isn't between 5 and 15.
         // (The maximum length of a valid number in E.164 format is 15 digits
         //  according to https://en.wikipedia.org/wiki/E.164 )
         if (!/^(\+?)[\da-zA-Z]{5,15}$/.test(strippedNumber)) {
           address.record.tel = "";
         }
       }
     }
-  },
+  }
 
   async _decrypt(cipherText, reauth) {
     return new Promise((resolve) => {
       Services.cpmm.addMessageListener("FormAutofill:DecryptedString", function getResult(result) {
         Services.cpmm.removeMessageListener("FormAutofill:DecryptedString", getResult);
         resolve(result.data);
       });
 
       Services.cpmm.sendAsyncMessage("FormAutofill:GetDecryptedString", {cipherText, reauth});
     });
-  },
+  }
+}
+
+/**
+ * Handles profile autofill for a DOM Form element.
+ */
+class FormAutofillHandler {
+  /**
+   * Initialize the form from `FormLike` object to handle the section or form
+   * operations.
+   * @param {FormLike} form Form that need to be auto filled
+   */
+  constructor(form) {
+    this._updateForm(form);
+
+    /**
+     * A WindowUtils reference of which Window the form belongs
+     */
+    this.winUtils = this.form.rootElement.ownerGlobal.QueryInterface(Ci.nsIInterfaceRequestor)
+      .getInterface(Ci.nsIDOMWindowUtils);
+
+    /**
+     * Time in milliseconds since epoch when a user started filling in the form.
+     */
+    this.timeStartedFillingMS = null;
+  }
+
+  /**
+   * Check the form is necessary to be updated. This function should be able to
+   * detect any changes including all control elements in the form.
+   * @param {HTMLElement} element The element supposed to be in the form.
+   * @returns {boolean} FormAutofillHandler.form is updated or not.
+   */
+  updateFormIfNeeded(element) {
+    // When the following condition happens, FormAutofillHandler.form should be
+    // updated:
+    // * The count of form controls is changed.
+    // * When the element can not be found in the current form.
+    //
+    // However, we should improve the function to detect the element changes.
+    // e.g. a tel field is changed from type="hidden" to type="tel".
+
+    let _formLike;
+    let getFormLike = () => {
+      if (!_formLike) {
+        _formLike = FormLikeFactory.createFromField(element);
+      }
+      return _formLike;
+    };
+
+    let currentForm = element.form;
+    if (!currentForm) {
+      currentForm = getFormLike();
+    }
+
+    if (currentForm.elements.length != this.form.elements.length) {
+      log.debug("The count of form elements is changed.");
+      this._updateForm(getFormLike());
+      return true;
+    }
+
+    if (this.form.elements.indexOf(element) === -1) {
+      log.debug("The element can not be found in the current form.");
+      this._updateForm(getFormLike());
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Update the form with a new FormLike, and the related fields should be
+   * updated or clear to ensure the data consistency.
+   * @param {FormLike} form a new FormLike to replace the original one.
+   */
+  _updateForm(form) {
+    /**
+     * DOM Form element to which this object is attached.
+     */
+    this.form = form;
+
+    /**
+     * Array of collected data about relevant form fields.  Each item is an object
+     * storing the identifying details of the field and a reference to the
+     * originally associated element from the form.
+     *
+     * The "section", "addressType", "contactType", and "fieldName" values are
+     * used to identify the exact field when the serializable data is received
+     * from the backend.  There cannot be multiple fields which have
+     * the same exact combination of these values.
+     *
+     * A direct reference to the associated element cannot be sent to the user
+     * interface because processing may be done in the parent process.
+     */
+    this.fieldDetails = null;
+
+    this.sections = [];
+  }
+
+  /**
+   * Set fieldDetails from the form about fields that can be autofilled.
+   *
+   * @param {boolean} allowDuplicates
+   *        true to remain any duplicated field details otherwise to remove the
+   *        duplicated ones.
+   * @returns {Array} The valid address and credit card details.
+   */
+  collectFormFields(allowDuplicates = false) {
+    let sections = FormAutofillHeuristics.getFormInfo(this.form, allowDuplicates);
+    let allValidDetails = [];
+    for (let fieldDetails of sections) {
+      let section = new FormAutofillSection(fieldDetails, this.winUtils);
+      this.sections.push(section);
+      allValidDetails.push(...section.validDetails);
+    }
+
+    for (let detail of allValidDetails) {
+      let input = detail.elementWeakRef.get();
+      if (!input) {
+        continue;
+      }
+      input.addEventListener("input", this);
+    }
+
+    this.fieldDetails = allValidDetails;
+    return allValidDetails;
+  }
+
+  getFieldDetailByElement(element) {
+    return this.fieldDetails.find(
+      detail => detail.elementWeakRef.get() == element
+    );
+  }
+
+  getSectionByElement(element) {
+    return this.sections.find(
+      section => section.getFieldDetailByElement(element)
+    );
+  }
+
+  getFieldDetailsByElement(element) {
+    let fieldDetail = this.getFieldDetailByElement(element);
+    if (!fieldDetail) {
+      return [];
+    }
+    return this.getSectionByElement(element).getFieldDetailsByElement(element);
+  }
+
+  getAllFieldNames(focusedInput) {
+    let section = this.getSectionByElement(focusedInput);
+    return section.allFieldNames;
+  }
+
+  previewFormFields(profile, focusedInput) {
+    let section = this.getSectionByElement(focusedInput);
+    section.previewFormFields(profile, focusedInput);
+  }
+
+  clearPreviewedFormFields(focusedInput) {
+    let section = this.getSectionByElement(focusedInput);
+    section.clearPreviewedFormFields(focusedInput);
+  }
+
+  getAdaptedProfiles(originalProfiles, focusedInput) {
+    let section = this.getSectionByElement(focusedInput);
+    section.getAdaptedProfiles(originalProfiles);
+    return originalProfiles;
+  }
+
+  hasFilledSection() {
+    return this.sections.some(section => section.isFilled());
+  }
+
+  /**
+   * Processes form fields that can be autofilled, and populates them with the
+   * profile provided by backend.
+   *
+   * @param {Object} profile
+   *        A profile to be filled in.
+   * @param {HTMLElement} focusedInput
+   *        A focused input element needed to determine the address or credit
+   *        card field.
+   */
+  async autofillFormFields(profile, focusedInput) {
+    let noFilledSections = !this.hasFilledSection();
+    await this.getSectionByElement(focusedInput).autofillFields(profile, focusedInput);
+
+    // Handle the highlight style resetting caused by user's correction afterward.
+    log.debug("register change handler for filled form:", this.form);
+    const onChangeHandler = e => {
+      if (!e.isTrusted) {
+        return;
+      }
+
+      if (e.type == "input") {
+        let section = this.getSectionByElement(e.target);
+        section.clearFieldState(e.target);
+      } else if (e.type == "reset") {
+        for (let section of this.sections) {
+          section.resetFieldStates();
+        }
+      }
+
+      // Unregister listeners once no field is in AUTO_FILLED state.
+      if (!this.hasFilledSection()) {
+        this.form.rootElement.removeEventListener("input", onChangeHandler);
+        this.form.rootElement.removeEventListener("reset", onChangeHandler);
+      }
+    };
+
+    if (noFilledSections) {
+      this.form.rootElement.addEventListener("input", onChangeHandler);
+      this.form.rootElement.addEventListener("reset", onChangeHandler);
+    }
+  }
 
   handleEvent(event) {
     switch (event.type) {
       case "input":
         if (!event.isTrusted) {
           return;
         }
 
@@ -862,10 +944,20 @@ FormAutofillHandler.prototype = {
           if (!input) {
             continue;
           }
           input.removeEventListener("input", this);
         }
         this.timeStartedFillingMS = Date.now();
         break;
     }
-  },
-};
+  }
+
+  createRecords() {
+    // TODO [Bug 1415073] `FormAutofillHandler.createRecords` should traverse
+    // all sections and aggregate the records into one result.
+    if (this.sections.length > 0) {
+      return this.sections[0].createRecords();
+    }
+    return null;
+  }
+}
+
--- a/browser/extensions/formautofill/FormAutofillHeuristics.jsm
+++ b/browser/extensions/formautofill/FormAutofillHeuristics.jsm
@@ -15,16 +15,17 @@ const {classes: Cc, interfaces: Ci, util
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://formautofill/FormAutofillUtils.jsm");
 
 this.log = null;
 FormAutofillUtils.defineLazyLogGetter(this, this.EXPORTED_SYMBOLS[0]);
 
 const PREF_HEURISTICS_ENABLED = "extensions.formautofill.heuristics.enabled";
+const PREF_SECTION_ENABLED = "extensions.formautofill.section.enabled";
 
 /**
  * A scanner for traversing all elements in a form and retrieving the field
  * detail with FormAutofillHeuristics.getInfo function. It also provides a
  * cursor (parsingIndex) to indicate which element is waiting for parsing.
  */
 class FieldScanner {
   /**
@@ -539,30 +540,31 @@ this.FormAutofillHeuristics = {
     // that preceding the actual expiration fields.
     fieldScanner.updateFieldName(fieldScanner.parsingIndex, "cc-exp");
     fieldScanner.parsingIndex++;
 
     return true;
   },
 
   /**
-   * This function should provide all field details of a form. The details
-   * contain the autocomplete info (e.g. fieldName, section, etc).
+   * This function should provide all field details of a form which are placed
+   * in the belonging section. The details contain the autocomplete info
+   * (e.g. fieldName, section, etc).
    *
    * `allowDuplicates` is used for the xpcshell-test purpose currently because
    * the heuristics should be verified that some duplicated elements still can
    * be predicted correctly.
    *
    * @param {HTMLFormElement} form
    *        the elements in this form to be predicted the field info.
    * @param {boolean} allowDuplicates
    *        true to remain any duplicated field details otherwise to remove the
    *        duplicated ones.
-   * @returns {Array<Object>}
-   *        all field details in the form.
+   * @returns {Array<Array<Object>>}
+   *        all sections within its field details in the form.
    */
   getFormInfo(form, allowDuplicates = false) {
     const eligibleFields = Array.from(form.elements)
       .filter(elem => FormAutofillUtils.isFieldEligibleForAutofill(elem));
 
     if (eligibleFields.length <= 0) {
       return [];
     }
@@ -577,21 +579,29 @@ this.FormAutofillHeuristics = {
       // forward to the next one.
       if (!parsedPhoneFields && !parsedAddressFields && !parsedExpirationDateFields) {
         fieldScanner.parsingIndex++;
       }
     }
 
     LabelUtils.clearLabelMap();
 
-    if (allowDuplicates) {
-      return fieldScanner.fieldDetails;
+    if (!this._sectionEnabled) {
+      // When the section feature is disabled, `getFormInfo` should provide a
+      // single section result.
+      return [allowDuplicates ? fieldScanner.fieldDetails : fieldScanner.trimmedFieldDetail];
     }
 
-    return fieldScanner.trimmedFieldDetail;
+    return this._groupingFields(fieldScanner, allowDuplicates);
+  },
+
+  _groupingFields(fieldScanner, allowDuplicates) {
+    // TODO [Bug 1415077] This function should be able to handle the section
+    // part of autocomplete attr.
+    return [allowDuplicates ? fieldScanner.fieldDetails : fieldScanner.trimmedFieldDetail];
   },
 
   _regExpTableHashValue(...signBits) {
     return signBits.reduce((p, c, i) => p | !!c << i, 0);
   },
 
   _setRegExpListCache(regexps, b0, b1, b2) {
     if (!this._regexpList) {
@@ -890,8 +900,16 @@ XPCOMUtils.defineLazyGetter(this.FormAut
 XPCOMUtils.defineLazyGetter(this.FormAutofillHeuristics, "_prefEnabled", () => {
   return Services.prefs.getBoolPref(PREF_HEURISTICS_ENABLED);
 });
 
 Services.prefs.addObserver(PREF_HEURISTICS_ENABLED, () => {
   this.FormAutofillHeuristics._prefEnabled = Services.prefs.getBoolPref(PREF_HEURISTICS_ENABLED);
 });
 
+XPCOMUtils.defineLazyGetter(this.FormAutofillHeuristics, "_sectionEnabled", () => {
+  return Services.prefs.getBoolPref(PREF_SECTION_ENABLED);
+});
+
+Services.prefs.addObserver(PREF_SECTION_ENABLED, () => {
+  this.FormAutofillHeuristics._sectionEnabled = Services.prefs.getBoolPref(PREF_SECTION_ENABLED);
+});
+
--- a/browser/extensions/formautofill/FormAutofillParent.jsm
+++ b/browser/extensions/formautofill/FormAutofillParent.jsm
@@ -20,18 +20,16 @@
  *     index
  *   },
  *   {
  *     // ...
  *   }
  * ]
  */
 
-/* exported FormAutofillParent */
-
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["FormAutofillParent"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
--- a/browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
+++ b/browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
@@ -1,15 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+/* exported AddressResult, CreditCardResult */
+
 "use strict";
 
-this.EXPORTED_SYMBOLS = ["AddressResult", "CreditCardResult"]; /* exported AddressResult, CreditCardResult */
+this.EXPORTED_SYMBOLS = ["AddressResult", "CreditCardResult"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://formautofill/FormAutofillUtils.jsm");
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "insecureWarningEnabled", "security.insecure_field_warning.contextual.enabled");
--- a/browser/extensions/formautofill/test/unit/head.js
+++ b/browser/extensions/formautofill/test/unit/head.js
@@ -95,17 +95,23 @@ function runHeuristicsTest(patterns, fix
         if (!forms.some(form => form.rootElement === formLike.rootElement)) {
           forms.push(formLike);
         }
       }
 
       Assert.equal(forms.length, testPattern.expectedResult.length, "Expected form count.");
 
       forms.forEach((form, formIndex) => {
-        let formInfo = FormAutofillHeuristics.getFormInfo(form);
+        let sections = FormAutofillHeuristics.getFormInfo(form);
+        if (testPattern.expectedResult[formIndex].length == 0) {
+          return;
+        }
+        // TODO [Bug 1415077] the test should be able to support traversing all
+        // sections.
+        let formInfo = sections[0];
         do_print("FieldName Prediction Results: " + formInfo.map(i => i.fieldName));
         do_print("FieldName Expected Results:   " + testPattern.expectedResult[formIndex].map(i => i.fieldName));
         Assert.equal(formInfo.length, testPattern.expectedResult[formIndex].length, "Expected field count.");
         formInfo.forEach((field, fieldIndex) => {
           let expectedField = testPattern.expectedResult[formIndex][fieldIndex];
           delete field._reason;
           expectedField.elementWeakRef = field.elementWeakRef;
           Assert.deepEqual(field, expectedField);
@@ -162,18 +168,20 @@ function objectMatches(object, fields) {
   }
   return ObjectUtils.deepEqual(actual, fields);
 }
 
 add_task(async function head_initialize() {
   Services.prefs.setStringPref("extensions.formautofill.available", "on");
   Services.prefs.setBoolPref("extensions.formautofill.creditCards.available", true);
   Services.prefs.setBoolPref("extensions.formautofill.heuristics.enabled", true);
+  Services.prefs.setBoolPref("extensions.formautofill.section.enabled", false);
   Services.prefs.setBoolPref("dom.forms.autocomplete.formautofill", true);
 
   // Clean up after every test.
   do_register_cleanup(function head_cleanup() {
     Services.prefs.clearUserPref("extensions.formautofill.available");
     Services.prefs.clearUserPref("extensions.formautofill.creditCards.available");
     Services.prefs.clearUserPref("extensions.formautofill.heuristics.enabled");
+    Services.prefs.clearUserPref("extensions.formautofill.section.enabled");
     Services.prefs.clearUserPref("dom.forms.autocomplete.formautofill");
   });
 });
--- a/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
@@ -499,44 +499,51 @@ function do_test(testcases, testFn) {
 
         let doc = MockDocument.createTestDocument("http://localhost:8080/test/",
                                                   testcase.document);
         let form = doc.querySelector("form");
         let formLike = FormLikeFactory.createFromForm(form);
         let handler = new FormAutofillHandler(formLike);
         let promises = [];
         // Replace the interal decrypt method with MasterPassword API
-        handler._decrypt = async (cipherText, reauth) => {
+        let decryptHelper = async (cipherText, reauth) => {
           let string;
           try {
             string = await MasterPassword.decrypt(cipherText, reauth);
           } catch (e) {
             if (e.result != Cr.NS_ERROR_ABORT) {
               throw e;
             }
             do_print("User canceled master password entry");
           }
           return string;
         };
 
         handler.collectFormFields();
-        let handlerInfo = handler[testcase.expectedFillingForm];
+        for (let section of handler.sections) {
+          section._decrypt = decryptHelper;
+        }
+
+        // TODO [Bug 1415077] We can assume all test cases with only one section
+        // should be filled. Eventually, the test needs to verify the filling
+        // feature in a multiple section case.
+        let handlerInfo = handler.sections[0][testcase.expectedFillingForm];
         handlerInfo.fieldDetails.forEach(field => {
           let element = field.elementWeakRef.get();
           if (!testcase.profileData[field.fieldName]) {
             // Avoid waiting for `change` event of a input with a blank value to
             // be filled.
             return;
           }
           promises.push(...testFn(testcase, element));
         });
 
-        let [adaptedProfile] = handler.getAdaptedProfiles([testcase.profileData]);
-        let focuedInput = doc.getElementById(testcase.focusedInputId);
-        await handler.autofillFormFields(adaptedProfile, focuedInput);
+        let focusedInput = doc.getElementById(testcase.focusedInputId);
+        let [adaptedProfile] = handler.getAdaptedProfiles([testcase.profileData], focusedInput);
+        await handler.autofillFormFields(adaptedProfile, focusedInput);
         Assert.equal(handlerInfo.filledRecordGUID, testcase.profileData.guid,
                      "Check if filledRecordGUID is set correctly");
         await Promise.all(promises);
       });
     })();
   }
 }
 
--- a/browser/extensions/formautofill/test/unit/test_collectFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_collectFormFields.js
@@ -435,14 +435,17 @@ for (let tc of TESTCASES) {
         testcase.addressFieldDetails,
         testcase.creditCardFieldDetails,
         testcase.validFieldDetails,
       ].forEach(details => setElementWeakRef(details));
 
       let handler = new FormAutofillHandler(formLike);
       let validFieldDetails = handler.collectFormFields(testcase.allowDuplicates);
 
-      verifyDetails(handler.address.fieldDetails, testcase.addressFieldDetails);
-      verifyDetails(handler.creditCard.fieldDetails, testcase.creditCardFieldDetails);
+      // TODO [Bug 1415077] We can assume all test cases with only one section
+      // should be filled. Eventually, the test needs to verify the filling
+      // feature in a multiple section case.
+      verifyDetails(handler.sections[0].address.fieldDetails, testcase.addressFieldDetails);
+      verifyDetails(handler.sections[0].creditCard.fieldDetails, testcase.creditCardFieldDetails);
       verifyDetails(validFieldDetails, testcase.validFieldDetails);
     });
   })();
 }
--- a/browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js
+++ b/browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js
@@ -24,16 +24,18 @@ const DEFAULT_CREDITCARD_RECORD = {
   "cc-exp-year": 2025,
   "cc-exp": "2025-01",
 };
 
 const TESTCASES = [
   {
     description: "Address form with street-address",
     document: `<form>
+               <input autocomplete="given-name">
+               <input autocomplete="family-name">
                <input id="street-addr" autocomplete="street-address">
                </form>`,
     profileData: [Object.assign({}, DEFAULT_ADDRESS_RECORD)],
     expectedResult: [{
       "guid": "123",
       "street-address": "2 Harrison St line2 line3",
       "-moz-street-address-one-line": "2 Harrison St line2 line3",
       "address-line1": "2 Harrison St",
@@ -65,16 +67,17 @@ const TESTCASES = [
       "country": "US",
       "tel": "+19876543210",
       "tel-national": "9876543210",
     }],
   },
   {
     description: "Address form with street-address, address-line1",
     document: `<form>
+               <input autocomplete="given-name">
                <input id="street-addr" autocomplete="street-address">
                <input id="line1" autocomplete="address-line1">
                </form>`,
     profileData: [Object.assign({}, DEFAULT_ADDRESS_RECORD)],
     expectedResult: [{
       "guid": "123",
       "street-address": "2 Harrison St line2 line3",
       "-moz-street-address-one-line": "2 Harrison St line2 line3",
@@ -127,16 +130,17 @@ const TESTCASES = [
       "country": "US",
       "tel": "+19876543210",
       "tel-national": "9876543210",
     }],
   },
   {
     description: "Address form with exact matching options in select",
     document: `<form>
+               <input autocomplete="given-name">
                <select autocomplete="address-level1">
                  <option id="option-address-level1-XX" value="XX">Dummy</option>
                  <option id="option-address-level1-CA" value="CA">California</option>
                </select>
                <select autocomplete="country">
                  <option id="option-country-XX" value="XX">Dummy</option>
                  <option id="option-country-US" value="US">United States</option>
                </select>
@@ -157,16 +161,17 @@ const TESTCASES = [
     expectedOptionElements: [{
       "address-level1": "option-address-level1-CA",
       "country": "option-country-US",
     }],
   },
   {
     description: "Address form with inexact matching options in select",
     document: `<form>
+               <input autocomplete="given-name">
                <select autocomplete="address-level1">
                  <option id="option-address-level1-XX" value="XX">Dummy</option>
                  <option id="option-address-level1-OO" value="OO">California</option>
                </select>
                <select autocomplete="country">
                  <option id="option-country-XX" value="XX">Dummy</option>
                  <option id="option-country-OO" value="OO">United States</option>
                </select>
@@ -187,16 +192,17 @@ const TESTCASES = [
     expectedOptionElements: [{
       "address-level1": "option-address-level1-OO",
       "country": "option-country-OO",
     }],
   },
   {
     description: "Address form with value-omitted options in select",
     document: `<form>
+               <input autocomplete="given-name">
                <select autocomplete="address-level1">
                  <option id="option-address-level1-1" value="">Dummy</option>
                  <option id="option-address-level1-2" value="">California</option>
                </select>
                <select autocomplete="country">
                  <option id="option-country-1" value="">Dummy</option>
                  <option id="option-country-2" value="">United States</option>
                </select>
@@ -217,16 +223,17 @@ const TESTCASES = [
     expectedOptionElements: [{
       "address-level1": "option-address-level1-2",
       "country": "option-country-2",
     }],
   },
   {
     description: "Address form with options with the same value in select ",
     document: `<form>
+               <input autocomplete="given-name">
                <select autocomplete="address-level1">
                  <option id="option-address-level1-same1" value="same">Dummy</option>
                  <option id="option-address-level1-same2" value="same">California</option>
                </select>
                <select autocomplete="country">
                  <option id="option-country-same1" value="sametoo">Dummy</option>
                  <option id="option-country-same2" value="sametoo">United States</option>
                </select>
@@ -247,16 +254,17 @@ const TESTCASES = [
     expectedOptionElements: [{
       "address-level1": "option-address-level1-same2",
       "country": "option-country-same2",
     }],
   },
   {
     description: "Address form without matching options in select for address-level1 and country",
     document: `<form>
+               <input autocomplete="given-name">
                <select autocomplete="address-level1">
                  <option id="option-address-level1-dummy1" value="">Dummy</option>
                  <option id="option-address-level1-dummy2" value="">Dummy 2</option>
                </select>
                <select autocomplete="country">
                  <option id="option-country-dummy1" value="">Dummy</option>
                  <option id="option-country-dummy2" value="">Dummy 2</option>
                </select>
@@ -460,16 +468,17 @@ const TESTCASES = [
       "country": "US",
       "tel": "9876543210",
       "tel-national": "9876543210",
     }],
   },
   {
     description: "Credit Card form with matching options of cc-exp-year and cc-exp-month",
     document: `<form>
+               <input autocomplete="cc-number">
                <select autocomplete="cc-exp-month">
                  <option id="option-cc-exp-month-01" value="1">01</option>
                  <option id="option-cc-exp-month-02" value="2">02</option>
                  <option id="option-cc-exp-month-03" value="3">03</option>
                  <option id="option-cc-exp-month-04" value="4">04</option>
                  <option id="option-cc-exp-month-05" value="5">05</option>
                  <option id="option-cc-exp-month-06" value="6">06</option>
                  <option id="option-cc-exp-month-07" value="7">07</option>
@@ -499,16 +508,17 @@ const TESTCASES = [
     expectedOptionElements: [{
       "cc-exp-month": "option-cc-exp-month-01",
       "cc-exp-year": "option-cc-exp-year-25",
     }],
   },
   {
     description: "Credit Card form with matching options which contain labels",
     document: `<form>
+               <input autocomplete="cc-number">
                <select autocomplete="cc-exp-month">
                  <option value="" selected="selected">Month</option>
                  <option label="01 - January" id="option-cc-exp-month-01" value="object:17">dummy</option>
                  <option label="02 - February" id="option-cc-exp-month-02" value="object:18">dummy</option>
                  <option label="03 - March" id="option-cc-exp-month-03" value="object:19">dummy</option>
                  <option label="04 - April" id="option-cc-exp-month-04" value="object:20">dummy</option>
                  <option label="05 - May" id="option-cc-exp-month-05" value="object:21">dummy</option>
                  <option label="06 - June" id="option-cc-exp-month-06" value="object:22">dummy</option>
@@ -546,179 +556,210 @@ const TESTCASES = [
     expectedResult: [DEFAULT_CREDITCARD_RECORD],
     expectedOptionElements: [{
       "cc-exp-month": "option-cc-exp-month-01",
       "cc-exp-year": "option-cc-exp-year-25",
     }],
   },
   {
     description: "Compound cc-exp: {MON1}/{YEAR2}",
-    document: `<form><select autocomplete="cc-exp">
+    document: `<form>
+               <input autocomplete="cc-number">
+               <select autocomplete="cc-exp">
                  <option value="3/17">3/17</option>
                  <option value="1/25" id="selected-cc-exp">1/25</option>
                </select></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [DEFAULT_CREDITCARD_RECORD],
     expectedOptionElements: [{"cc-exp": "selected-cc-exp"}],
   },
   {
     description: "Compound cc-exp: {MON1}/{YEAR4}",
-    document: `<form><select autocomplete="cc-exp">
+    document: `<form>
+               <input autocomplete="cc-number">
+               <select autocomplete="cc-exp">
                  <option value="3/2017">3/2017</option>
                  <option value="1/2025" id="selected-cc-exp">1/2025</option>
                </select></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [DEFAULT_CREDITCARD_RECORD],
     expectedOptionElements: [{"cc-exp": "selected-cc-exp"}],
   },
   {
     description: "Compound cc-exp: {MON2}/{YEAR2}",
-    document: `<form><select autocomplete="cc-exp">
+    document: `<form>
+               <input autocomplete="cc-number">
+               <select autocomplete="cc-exp">
                  <option value="03/17">03/17</option>
                  <option value="01/25" id="selected-cc-exp">01/25</option>
                </select></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [DEFAULT_CREDITCARD_RECORD],
     expectedOptionElements: [{"cc-exp": "selected-cc-exp"}],
   },
   {
     description: "Compound cc-exp: {MON2}/{YEAR4}",
-    document: `<form><select autocomplete="cc-exp">
+    document: `<form>
+               <input autocomplete="cc-number">
+               <select autocomplete="cc-exp">
                  <option value="03/2017">03/2017</option>
                  <option value="01/2025" id="selected-cc-exp">01/2025</option>
                </select></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [DEFAULT_CREDITCARD_RECORD],
     expectedOptionElements: [{"cc-exp": "selected-cc-exp"}],
   },
   {
     description: "Compound cc-exp: {MON1}-{YEAR2}",
-    document: `<form><select autocomplete="cc-exp">
+    document: `<form>
+               <input autocomplete="cc-number">
+               <select autocomplete="cc-exp">
                  <option value="3-17">3-17</option>
                  <option value="1-25" id="selected-cc-exp">1-25</option>
                </select></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [DEFAULT_CREDITCARD_RECORD],
     expectedOptionElements: [{"cc-exp": "selected-cc-exp"}],
   },
   {
     description: "Compound cc-exp: {MON1}-{YEAR4}",
-    document: `<form><select autocomplete="cc-exp">
+    document: `<form>
+               <input autocomplete="cc-number">
+               <select autocomplete="cc-exp">
                  <option value="3-2017">3-2017</option>
                  <option value="1-2025" id="selected-cc-exp">1-2025</option>
                </select></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [DEFAULT_CREDITCARD_RECORD],
     expectedOptionElements: [{"cc-exp": "selected-cc-exp"}],
   },
   {
     description: "Compound cc-exp: {MON2}-{YEAR2}",
-    document: `<form><select autocomplete="cc-exp">
+    document: `<form>
+               <input autocomplete="cc-number">
+               <select autocomplete="cc-exp">
                  <option value="03-17">03-17</option>
                  <option value="01-25" id="selected-cc-exp">01-25</option>
                </select></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [DEFAULT_CREDITCARD_RECORD],
     expectedOptionElements: [{"cc-exp": "selected-cc-exp"}],
   },
   {
     description: "Compound cc-exp: {MON2}-{YEAR4}",
-    document: `<form><select autocomplete="cc-exp">
+    document: `<form>
+               <input autocomplete="cc-number">
+               <select autocomplete="cc-exp">
                  <option value="03-2017">03-2017</option>
                  <option value="01-2025" id="selected-cc-exp">01-2025</option>
                </select></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [DEFAULT_CREDITCARD_RECORD],
     expectedOptionElements: [{"cc-exp": "selected-cc-exp"}],
   },
   {
     description: "Compound cc-exp: {YEAR2}-{MON2}",
-    document: `<form><select autocomplete="cc-exp">
+    document: `<form>
+               <input autocomplete="cc-number">
+               <select autocomplete="cc-exp">
                  <option value="17-03">17-03</option>
                  <option value="25-01" id="selected-cc-exp">25-01</option>
                </select></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [DEFAULT_CREDITCARD_RECORD],
     expectedOptionElements: [{"cc-exp": "selected-cc-exp"}],
   },
   {
     description: "Compound cc-exp: {YEAR4}-{MON2}",
-    document: `<form><select autocomplete="cc-exp">
+    document: `<form>
+               <input autocomplete="cc-number">
+               <select autocomplete="cc-exp">
                  <option value="2017-03">2017-03</option>
                  <option value="2025-01" id="selected-cc-exp">2025-01</option>
                </select></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [DEFAULT_CREDITCARD_RECORD],
     expectedOptionElements: [{"cc-exp": "selected-cc-exp"}],
   },
   {
     description: "Compound cc-exp: {YEAR4}/{MON2}",
-    document: `<form><select autocomplete="cc-exp">
+    document: `<form>
+               <input autocomplete="cc-number">
+               <select autocomplete="cc-exp">
                  <option value="2017/3">2017/3</option>
                  <option value="2025/1" id="selected-cc-exp">2025/1</option>
                </select></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [DEFAULT_CREDITCARD_RECORD],
     expectedOptionElements: [{"cc-exp": "selected-cc-exp"}],
   },
   {
     description: "Compound cc-exp: {MON2}{YEAR2}",
-    document: `<form><select autocomplete="cc-exp">
+    document: `<form>
+               <input autocomplete="cc-number">
+               <select autocomplete="cc-exp">
                  <option value="0317">0317</option>
                  <option value="0125" id="selected-cc-exp">0125</option>
                </select></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [DEFAULT_CREDITCARD_RECORD],
     expectedOptionElements: [{"cc-exp": "selected-cc-exp"}],
   },
   {
     description: "Compound cc-exp: {YEAR2}{MON2}",
-    document: `<form><select autocomplete="cc-exp">
+    document: `<form>
+               <input autocomplete="cc-number">
+               <select autocomplete="cc-exp">
                  <option value="1703">1703</option>
                  <option value="2501" id="selected-cc-exp">2501</option>
                </select></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [DEFAULT_CREDITCARD_RECORD],
     expectedOptionElements: [{"cc-exp": "selected-cc-exp"}],
   },
   {
     description: "Fill a cc-exp without cc-exp-month value in the profile",
-    document: `<form><select autocomplete="cc-exp">
+    document: `<form>
+               <input autocomplete="cc-number">
+               <select autocomplete="cc-exp">
                  <option value="03/17">03/17</option>
                  <option value="01/25">01/25</option>
                </select></form>`,
     profileData: [Object.assign({}, {
       "guid": "123",
       "cc-exp-year": 2025,
     })],
     expectedResult: [{
       "guid": "123",
       "cc-exp-year": 2025,
     }],
     expectedOptionElements: [],
   },
   {
     description: "Fill a cc-exp without cc-exp-year value in the profile",
-    document: `<form><select autocomplete="cc-exp">
+    document: `<form>
+               <input autocomplete="cc-number">
+               <select autocomplete="cc-exp">
                  <option value="03/17">03/17</option>
                  <option value="01/25">01/25</option>
                </select></form>`,
     profileData: [Object.assign({}, {
       "guid": "123",
       "cc-exp-month": 1,
     })],
     expectedResult: [{
       "guid": "123",
       "cc-exp-month": 1,
     }],
     expectedOptionElements: [],
   },
   {
     description: "Fill a cc-exp* without cc-exp-month value in the profile",
     document: `<form>
+               <input autocomplete="cc-number">
                <select autocomplete="cc-exp-month">
                  <option value="03">03</option>
                  <option value="01">01</option>
                </select>
                <select autocomplete="cc-exp-year">
                  <option value="17">2017</option>
                  <option value="25">2025</option>
                </select>
@@ -731,16 +772,17 @@ const TESTCASES = [
       "guid": "123",
       "cc-exp-year": 2025,
     }],
     expectedOptionElements: [],
   },
   {
     description: "Fill a cc-exp* without cc-exp-year value in the profile",
     document: `<form>
+               <input autocomplete="cc-number">
                <select autocomplete="cc-exp-month">
                  <option value="03">03</option>
                  <option value="01">01</option>
                </select>
                <select autocomplete="cc-exp-year">
                  <option value="17">2017</option>
                  <option value="25">2025</option>
                </select>
@@ -752,113 +794,125 @@ const TESTCASES = [
     expectedResult: [{
       "guid": "123",
       "cc-exp-month": 1,
     }],
     expectedOptionElements: [],
   },
   {
     description: "Use placeholder to adjust cc-exp format [mm/yy].",
-    document: `<form><input placeholder="mm/yy" autocomplete="cc-exp"></form>`,
+    document: `<form><input autocomplete="cc-number">
+               <input placeholder="mm/yy" autocomplete="cc-exp"></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
       "cc-exp": "01/25",
     })],
   },
   {
     description: "Use placeholder to adjust cc-exp format [mm / yy].",
-    document: `<form><input placeholder="mm / yy" autocomplete="cc-exp"></form>`,
+    document: `<form><input autocomplete="cc-number">
+               <input placeholder="mm / yy" autocomplete="cc-exp"></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
       "cc-exp": "01/25",
     })],
   },
   {
     description: "Use placeholder to adjust cc-exp format [MM / YY].",
-    document: `<form><input placeholder="MM / YY" autocomplete="cc-exp"></form>`,
+    document: `<form><input autocomplete="cc-number">
+               <input placeholder="MM / YY" autocomplete="cc-exp"></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
       "cc-exp": "01/25",
     })],
   },
   {
     description: "Use placeholder to adjust cc-exp format [mm / yyyy].",
-    document: `<form><input placeholder="mm / yyyy" autocomplete="cc-exp"></form>`,
+    document: `<form><input autocomplete="cc-number">
+               <input placeholder="mm / yyyy" autocomplete="cc-exp"></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
       "cc-exp": "01/2025",
     })],
   },
   {
     description: "Use placeholder to adjust cc-exp format [mm - yyyy].",
-    document: `<form><input placeholder="mm - yyyy" autocomplete="cc-exp"></form>`,
+    document: `<form><input autocomplete="cc-number">
+               <input placeholder="mm - yyyy" autocomplete="cc-exp"></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
       "cc-exp": "01-2025",
     })],
   },
   {
     description: "Use placeholder to adjust cc-exp format [yyyy-mm].",
-    document: `<form><input placeholder="yyyy-mm" autocomplete="cc-exp"></form>`,
+    document: `<form><input autocomplete="cc-number">
+               <input placeholder="yyyy-mm" autocomplete="cc-exp"></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
       "cc-exp": "2025-01",
     })],
   },
   {
     description: "Use placeholder to adjust cc-exp format [yyy-mm].",
-    document: `<form><input placeholder="yyy-mm" autocomplete="cc-exp"></form>`,
+    document: `<form><input autocomplete="cc-number">
+               <input placeholder="yyy-mm" autocomplete="cc-exp"></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
       "cc-exp": "025-01",
     })],
   },
   {
     description: "Use placeholder to adjust cc-exp format [mmm yyyy].",
-    document: `<form><input placeholder="mmm yyyy" autocomplete="cc-exp"></form>`,
+    document: `<form><input autocomplete="cc-number">
+               <input placeholder="mmm yyyy" autocomplete="cc-exp"></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
   },
   {
     description: "Use placeholder to adjust cc-exp format [mm foo yyyy].",
-    document: `<form><input placeholder="mm foo yyyy" autocomplete="cc-exp"></form>`,
+    document: `<form><input autocomplete="cc-number">
+               <input placeholder="mm foo yyyy" autocomplete="cc-exp"></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
   },
   {
     description: "Use placeholder to adjust cc-exp format [mm - - yyyy].",
-    document: `<form><input placeholder="mm - - yyyy" autocomplete="cc-exp"></form>`,
+    document: `<form><input autocomplete="cc-number">
+               <input placeholder="mm - - yyyy" autocomplete="cc-exp"></form>`,
     profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
     expectedResult: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
   },
 ];
 
 for (let testcase of TESTCASES) {
   add_task(async function() {
     do_print("Starting testcase: " + testcase.description);
 
     let doc = MockDocument.createTestDocument("http://localhost:8080/test/",
                                               testcase.document);
     let form = doc.querySelector("form");
     let formLike = FormLikeFactory.createFromForm(form);
     let handler = new FormAutofillHandler(formLike);
 
     handler.collectFormFields();
-    let adaptedRecords = handler.getAdaptedProfiles(testcase.profileData);
+    let focusedInput = form.elements[0];
+    let adaptedRecords = handler.getAdaptedProfiles(testcase.profileData, focusedInput);
     Assert.deepEqual(adaptedRecords, testcase.expectedResult);
 
     if (testcase.expectedOptionElements) {
       testcase.expectedOptionElements.forEach((expectedOptionElement, i) => {
         for (let field in expectedOptionElement) {
           let select = form.querySelector(`[autocomplete=${field}]`);
           let expectedOption = doc.getElementById(expectedOptionElement[field]);
           Assert.notEqual(expectedOption, null);
 
           let value = testcase.profileData[i][field];
-          let cache = handler._cacheValue.matchingSelectOption.get(select);
+          let section = handler.getSectionByElement(select);
+          let cache = section._cacheValue.matchingSelectOption.get(select);
           let targetOption = cache[value] && cache[value].get();
           Assert.notEqual(targetOption, null);
 
           Assert.equal(targetOption, expectedOption);
         }
       });
     }
   });