Bug 1417834 - Part 2: Implement FormAutofillCreditCardSection and FormAutofillAddressSection inhertied from FormAutofillSection. r=lchang,ralin draft
authorSean Lee <selee@mozilla.com>
Sat, 16 Dec 2017 13:20:48 -0600
changeset 717512 c4dc41200b8d4cbf8aea20c626541ebda76fe6a6
parent 717511 774fd6c4bf790c46d481472f81146262155f6508
child 745286 37de47b41fa22a48fb2abf5b4a219870b0306a2b
push id94711
push userbmo:selee@mozilla.com
push dateTue, 09 Jan 2018 05:51:03 +0000
reviewerslchang, ralin
bugs1417834
milestone59.0a1
Bug 1417834 - Part 2: Implement FormAutofillCreditCardSection and FormAutofillAddressSection inhertied from FormAutofillSection. r=lchang,ralin MozReview-Commit-ID: 4i4suqQhUA1
browser/extensions/formautofill/FormAutofillContent.jsm
browser/extensions/formautofill/FormAutofillHandler.jsm
browser/extensions/formautofill/test/unit/test_autofillFormFields.js
browser/extensions/formautofill/test/unit/test_collectFormFields.js
--- a/browser/extensions/formautofill/FormAutofillContent.jsm
+++ b/browser/extensions/formautofill/FormAutofillContent.jsm
@@ -98,17 +98,17 @@ AutofillProfileAutoCompleteSearch.protot
     let {activeInput, activeSection, activeFieldDetail, savedFieldNames} = FormAutofillContent;
     this.forceStop = false;
 
     this.log.debug("startSearch: for", searchString, "with input", activeInput);
 
     let isAddressField = FormAutofillUtils.isAddressField(activeFieldDetail.fieldName);
     let isInputAutofilled = activeFieldDetail.state == FIELD_STATES.AUTO_FILLED;
     let allFieldNames = activeSection.allFieldNames;
-    let filledRecordGUID = activeSection.getFilledRecordGUID();
+    let filledRecordGUID = activeSection.filledRecordGUID;
     let searchPermitted = isAddressField ?
                           FormAutofillUtils.isAutofillAddressesEnabled :
                           FormAutofillUtils.isAutofillCreditCardsEnabled;
     let AutocompleteResult = isAddressField ? AddressResult : CreditCardResult;
 
     ProfileAutocomplete.lastProfileAutoCompleteFocusedInput = activeInput;
     // Fallback to form-history if ...
     //   - specified autofill feature is pref off.
--- a/browser/extensions/formautofill/FormAutofillHandler.jsm
+++ b/browser/extensions/formautofill/FormAutofillHandler.jsm
@@ -26,246 +26,157 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 this.log = null;
 FormAutofillUtils.defineLazyLogGetter(this, this.EXPORTED_SYMBOLS[0]);
 
 const {FIELD_STATES} = FormAutofillUtils;
 
 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,
-    };
+    this.fieldDetails = fieldDetails;
+    this.filledRecordGUID = null;
+    this.winUtils = winUtils;
 
     /**
      * Enum for form autofill MANUALLY_MANAGED_STATES values
      */
     this._FIELD_STATE_ENUM = {
       // not themed
       [FIELD_STATES.NORMAL]: null,
       // highlighted
       [FIELD_STATES.AUTO_FILLED]: "-moz-autofill",
       // highlighted && grey color text
       [FIELD_STATES.PREVIEW]: "-moz-autofill-preview",
     };
 
-    this.winUtils = winUtils;
-
-    this.address.fieldDetails = fieldDetails.filter(
-      detail => FormAutofillUtils.isAddressField(detail.fieldName)
-    );
-    if (this.address.fieldDetails.length < FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD) {
-      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 section.");
-      this.creditCard.fieldDetails = [];
+    if (!this.isValidSection()) {
+      this.fieldDetails = [];
+      log.debug(`Ignoring ${this.constructor.name} related fields since it is an invalid section`);
     }
 
     this._cacheValue = {
       allFieldNames: null,
-      oneLineStreetAddress: null,
       matchingSelectOption: null,
     };
+  }
 
-    this._validDetails = Array.of(...(this.address.fieldDetails),
-                                  ...(this.creditCard.fieldDetails));
-    log.debug(this._validDetails.length, "valid fields in the section is collected.");
+  /*
+   * Examine the section is a valid section or not based on its fieldDetails or
+   * other information. This method must be overrided.
+   *
+   * @returns {boolean} True for a valid section, otherwise false
+   *
+   */
+  isValidSection() {
+    throw new TypeError("isValidSection method must be overrided");
+  }
+
+  /*
+   * Examine the section is an enabled section type or not based on its
+   * preferences. This method must be overrided.
+   *
+   * @returns {boolean} True for an enabled section type, otherwise false
+   *
+   */
+  isEnabled() {
+    throw new TypeError("isEnabled method must be overrided");
+  }
+
+  /*
+   * Examine the section is createable for storing the profile. This method
+   * must be overrided.
+   *
+   * @param {Object} record The record for examining createable
+   * @returns {boolean} True for the record is createable, otherwise false
+   *
+   */
+  isRecordCreatable(record) {
+    throw new TypeError("isRecordCreatable method must be overrided");
   }
 
-  get validDetails() {
-    return this._validDetails;
+  /**
+   * Override this method if the profile is needed to apply some transformers.
+   *
+   * @param {Object} profile
+   *        A profile should be converted based on the specific requirement.
+   */
+  applyTransformers(profile) {}
+
+  /**
+   * Override this method if the profile is needed to be customized for
+   * previewing values.
+   *
+   * @param {Object} profile
+   *        A profile for pre-processing before previewing values.
+   */
+  preparePreviewProfile(profile) {}
+
+  /**
+   * Override this method if the profile is needed to be customized for filling
+   * values.
+   *
+   * @param {Object} profile
+   *        A profile for pre-processing before filling values.
+   */
+  async prepareFillingProfile(profile) {}
+
+  /*
+   * Override this methid if any data for `createRecord` is needed to be
+   * normailized before submitting the record.
+   *
+   * @param {Object} profile
+   *        A record for normalization.
+   */
+  normalizeCreatingRecord(data) {}
+
+  /*
+   * Override this method if there is any field value needs to compute for a
+   * specific case. Return the original value in the default case.
+   * @param {String} value
+   *        The original field value.
+   * @param {Object} fieldDetail
+   *        A fieldDetail of the related element.
+   * @param {HTMLElement} element
+   *        A element for checking converting value.
+   *
+   * @returns {String}
+   *          A string of the converted value.
+   */
+  computeFillingValue(value, fieldName, element) {
+    return value;
   }
 
   set focusedInput(element) {
     this._focusedDetail = this.getFieldDetailByElement(element);
   }
 
   getFieldDetailByElement(element) {
-    return this._validDetails.find(
+    return this.fieldDetails.find(
       detail => detail.elementWeakRef.get() == element
     );
   }
 
-  _isValidCreditCardForm(fieldDetails) {
-    let ccNumberReason = "";
-    let hasCCNumber = false;
-    let hasExpiryDate = false;
-
-    for (let detail of fieldDetails) {
-      switch (detail.fieldName) {
-        case "cc-number":
-          hasCCNumber = true;
-          ccNumberReason = detail._reason;
-          break;
-        case "cc-exp":
-        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);
+      this._cacheValue.allFieldNames = this.fieldDetails.map(record => record.fieldName);
     }
     return this._cacheValue.allFieldNames;
   }
 
-  _getFieldDetailByName(fieldName) {
-    return this._validDetails.find(detail => detail.fieldName == fieldName);
-  }
-
-  _getTargetSet() {
-    let fieldDetail = this._focusedDetail;
-    if (!fieldDetail) {
-      return null;
-    }
-    if (FormAutofillUtils.isAddressField(fieldDetail.fieldName)) {
-      return this.address;
-    }
-    if (FormAutofillUtils.isCreditCardField(fieldDetail.fieldName)) {
-      return this.creditCard;
-    }
-    return null;
-  }
-
-  _getFieldDetails() {
-    let targetSet = this._getTargetSet();
-    return targetSet ? targetSet.fieldDetails : [];
-  }
-
-  getFilledRecordGUID() {
-    let targetSet = this._getTargetSet();
-    return targetSet ? targetSet.filledRecordGUID : null;
-  }
-
-  _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];
+  getFieldDetailByName(fieldName) {
+    return this.fieldDetails.find(detail => detail.fieldName == fieldName);
   }
 
-  _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 &&
-          (streetAddressDetail.elementWeakRef.get() instanceof Ci.nsIDOMHTMLInputElement)) {
-        profile["street-address"] = profile["-moz-street-address-one-line"];
-      }
-
-      let waitForConcat = [];
-      for (let f of ["address-line3", "address-line2", "address-line1"]) {
-        waitForConcat.unshift(profile[f]);
-        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) {
-    if (!profile.tel || !profile["tel-national"]) {
-      return;
-    }
-
-    let detail = this._getFieldDetailByName("tel");
-    if (!detail) {
-      return;
-    }
-
-    let element = detail.elementWeakRef.get();
-    let _pattern;
-    let testPattern = str => {
-      if (!_pattern) {
-        // The pattern has to match the entire value.
-        _pattern = new RegExp("^(?:" + element.pattern + ")$", "u");
-      }
-      return _pattern.test(str);
-    };
-    if (element.pattern) {
-      if (testPattern(profile.tel)) {
-        return;
-      }
-    } else if (element.maxLength) {
-      if (detail._reason == "autocomplete" && profile.tel.length <= element.maxLength) {
-        return;
-      }
-    }
-
-    if (detail._reason != "autocomplete") {
-      // Since we only target people living in US and using en-US websites in
-      // MVP, it makes more sense to fill `tel-national` instead of `tel`
-      // if the field is identified by heuristics and no other clues to
-      // determine which one is better.
-      // TODO: [Bug 1407545] This should be improved once more countries are
-      // supported.
-      profile.tel = profile["tel-national"];
-    } else if (element.pattern) {
-      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) {
+  matchSelectOptions(profile) {
     if (!this._cacheValue.matchingSelectOption) {
       this._cacheValue.matchingSelectOption = new WeakMap();
     }
 
     for (let fieldName in profile) {
-      let fieldDetail = this._getFieldDetailByName(fieldName);
+      let fieldDetail = this.getFieldDetailByName(fieldName);
       if (!fieldDetail) {
         continue;
       }
 
       let element = fieldDetail.elementWeakRef.get();
       if (ChromeUtils.getClassName(element) !== "HTMLSelectElement") {
         continue;
       }
@@ -287,55 +198,19 @@ class FormAutofillSection {
         }
         // 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) {
-      return;
-    }
-
-    let element = detail.elementWeakRef.get();
-    if (element.tagName != "INPUT" || !element.placeholder) {
-      return;
-    }
-
-    let result,
-      ccExpMonth = profile["cc-exp-month"],
-      ccExpYear = profile["cc-exp-year"],
-      placeholder = element.placeholder;
-
-    result = /(?:[^m]|\b)(m{1,2})\s*([-/\\]*)\s*(y{2,4})(?!y)/i.exec(placeholder);
-    if (result) {
-      profile["cc-exp"] = String(ccExpMonth).padStart(result[1].length, "0") +
-                          result[2] +
-                          String(ccExpYear).substr(-1 * result[3].length);
-      return;
-    }
-
-    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");
-    }
-  }
-
-  _adaptFieldMaxLength(profile) {
+  adaptFieldMaxLength(profile) {
     for (let key in profile) {
-      let detail = this._getFieldDetailByName(key);
+      let detail = this.getFieldDetailByName(key);
       if (!detail) {
         continue;
       }
 
       let element = detail.elementWeakRef.get();
       if (!element) {
         continue;
       }
@@ -350,59 +225,39 @@ class FormAutofillSection {
       } else {
         delete profile[key];
       }
     }
   }
 
   getAdaptedProfiles(originalProfiles) {
     for (let profile of originalProfiles) {
-      this._addressTransformer(profile);
-      this._telTransformer(profile);
-      this._matchSelectOptions(profile);
-      this._creditCardExpDateTransformer(profile);
-      this._adaptFieldMaxLength(profile);
+      this.applyTransformers(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.
    */
   async autofillFields(profile) {
     let focusedDetail = this._focusedDetail;
     if (!focusedDetail) {
       throw new Error("No fieldDetail for the focused input.");
     }
-    let targetSet = this._getTargetSet();
-    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
-      // card number. Otherwise, the number can be decrypted with the default
-      // password.
-      if (profile["cc-number-encrypted"]) {
-        let decrypted = await this._decrypt(profile["cc-number-encrypted"], true);
 
-        if (!decrypted) {
-          // Early return if the decrypted is empty or undefined
-          return;
-        }
-
-        profile["cc-number"] = decrypted;
-      }
-    }
-
+    await this.prepareFillingProfile(profile);
     log.debug("profile in autofillFields:", profile);
 
-    targetSet.filledRecordGUID = profile.guid;
-    for (let fieldDetail of targetSet.fieldDetails) {
+    this.filledRecordGUID = profile.guid;
+    for (let fieldDetail of this.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
 
       let element = fieldDetail.elementWeakRef.get();
       if (!element) {
         continue;
@@ -445,24 +300,19 @@ class FormAutofillSection {
    * Populates result to the preview layers with given profile.
    *
    * @param {Object} profile
    *        A profile to be previewed with
    */
   previewFormFields(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"];
-    }
+    this.preparePreviewProfile(profile);
 
-    let fieldDetails = this._getFieldDetails();
-    for (let fieldDetail of fieldDetails) {
+    for (let fieldDetail of this.fieldDetails) {
       let element = fieldDetail.elementWeakRef.get();
       let value = profile[fieldDetail.fieldName] || "";
 
       // Skip the field that is null
       if (!element) {
         continue;
       }
 
@@ -488,18 +338,17 @@ class FormAutofillSection {
   }
 
   /**
    * Clear preview text and background highlight of all fields.
    */
   clearPreviewedFormFields() {
     log.debug("clear previewed fields in:", this.form);
 
-    let fieldDetails = this._getFieldDetails();
-    for (let fieldDetail of fieldDetails) {
+    for (let fieldDetail of this.fieldDetails) {
       let element = fieldDetail.elementWeakRef.get();
       if (!element) {
         log.warn(fieldDetail.fieldName, "is unreachable");
         continue;
       }
 
       element.previewValue = "";
 
@@ -512,18 +361,17 @@ class FormAutofillSection {
       this._changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
     }
   }
 
   /**
    * Clear value and highlight style of all filled fields.
    */
   clearPopulatedForm() {
-    let fieldDetails = this._getFieldDetails();
-    for (let fieldDetail of fieldDetails) {
+    for (let fieldDetail of this.fieldDetails) {
       let element = fieldDetail.elementWeakRef.get();
       if (!element) {
         log.warn(fieldDetail.fieldName, "is unreachable");
         continue;
       }
 
       // Only reset value for input element.
       if (fieldDetail.state == FIELD_STATES.AUTO_FILLED &&
@@ -582,151 +430,276 @@ class FormAutofillSection {
         break;
       }
     }
 
     fieldDetail.state = nextState;
   }
 
   resetFieldStates() {
-    for (let fieldDetail of this._validDetails) {
+    for (let fieldDetail of this.fieldDetails) {
       const element = fieldDetail.elementWeakRef.get();
       element.removeEventListener("input", this, {mozSystemGroup: true});
       this._changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
     }
-    this.address.filledRecordGUID = null;
-    this.creditCard.filledRecordGUID = null;
+    this.filledRecordGUID = null;
   }
 
   isFilled() {
-    return !!(this.address.filledRecordGUID || this.creditCard.filledRecordGUID);
+    return !!this.filledRecordGUID;
+  }
+
+  /**
+   * Return the record that is converted from `fieldDetails` and only valid
+   * form record is included.
+   *
+   * @returns {Object|null}
+   *          A record object consists of three properties:
+   *            - guid: The id of the previously-filled profile or null if omitted.
+   *            - record: A valid record converted from details with trimmed result.
+   *            - untouchedFields: Fields that aren't touched after autofilling.
+   *          Return `null` for any uncreatable or invalid record.
+   */
+  createRecord() {
+    let details = this.fieldDetails;
+    if (!this.isEnabled() || !details || details.length == 0) {
+      return null;
+    }
+
+    let data = {
+      guid: this.filledRecordGUID,
+      record: {},
+      untouchedFields: [],
+    };
+
+    details.forEach(detail => {
+      let element = detail.elementWeakRef.get();
+      // Remove the unnecessary spaces
+      let value = element && element.value.trim();
+      value = this.computeFillingValue(value, detail, element);
+
+      if (!value || value.length > FormAutofillUtils.MAX_FIELD_VALUE_LENGTH) {
+        // Keep the property and preserve more information for updating
+        data.record[detail.fieldName] = "";
+        return;
+      }
+
+      data.record[detail.fieldName] = value;
+
+      if (detail.state == FIELD_STATES.AUTO_FILLED) {
+        data.untouchedFields.push(detail.fieldName);
+      }
+    });
+
+    this.normalizeCreatingRecord(data);
+
+    if (!this.isRecordCreatable(data.record)) {
+      return null;
+    }
+
+    return data;
   }
 
-  _isAddressRecordCreatable(record) {
+  handleEvent(event) {
+    switch (event.type) {
+      case "input": {
+        if (!event.isTrusted) {
+          return;
+        }
+        const target = event.target;
+        const targetFieldDetail = this.getFieldDetailByElement(target);
+
+        this._changeFieldState(targetFieldDetail, FIELD_STATES.NORMAL);
+
+        let isAutofilled = false;
+        let dimFieldDetails = [];
+        for (const fieldDetail of this.fieldDetails) {
+          const element = fieldDetail.elementWeakRef.get();
+
+          if (ChromeUtils.getClassName(element) === "HTMLSelectElement") {
+            // Dim fields are those we don't attempt to revert their value
+            // when clear the target set, such as <select>.
+            dimFieldDetails.push(fieldDetail);
+          } else {
+            isAutofilled |= fieldDetail.state == FIELD_STATES.AUTO_FILLED;
+          }
+        }
+        if (!isAutofilled) {
+          // Restore the dim fields to initial state as well once we knew
+          // that user had intention to clear the filled form manually.
+          for (const fieldDetail of dimFieldDetails) {
+            this._changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
+          }
+          this.filledRecordGUID = null;
+        }
+        break;
+      }
+    }
+  }
+}
+
+class FormAutofillAddressSection extends FormAutofillSection {
+  constructor(fieldDetails, winUtils) {
+    super(fieldDetails, winUtils);
+
+    this._cacheValue.oneLineStreetAddress = null;
+  }
+
+  isValidSection() {
+    return this.fieldDetails.length >= FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD;
+  }
+
+  isEnabled() {
+    return FormAutofillUtils.isAutofillAddressesEnabled;
+  }
+
+  isRecordCreatable(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"]);
+  _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 &&
+          (streetAddressDetail.elementWeakRef.get() instanceof Ci.nsIDOMHTMLInputElement)) {
+        profile["street-address"] = profile["-moz-street-address-one-line"];
+      }
+
+      let waitForConcat = [];
+      for (let f of ["address-line3", "address-line2", "address-line1"]) {
+        waitForConcat.unshift(profile[f]);
+        if (this.getFieldDetailByName(f)) {
+          if (waitForConcat.length > 1) {
+            profile[f] = FormAutofillUtils.toOneLineAddress(waitForConcat);
+          }
+          waitForConcat = [];
+        }
+      }
+    }
   }
 
   /**
-   * 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
-   *          three properties:
-   *            - guid: The id of the previously-filled profile or null if omitted.
-   *            - record: A valid record converted from details with trimmed result.
-   *            - untouchedFields: Fields that aren't touched after autofilling.
+   * Replace tel with tel-national if tel violates the input element's
+   * restriction.
+   * @param {Object} profile
+   *        A profile to be converted.
    */
-  createRecords() {
-    let data = {};
-    let target = [];
+  telTransformer(profile) {
+    if (!profile.tel || !profile["tel-national"]) {
+      return;
+    }
 
-    if (FormAutofillUtils.isAutofillAddressesEnabled) {
-      target.push("address");
-    }
-    if (FormAutofillUtils.isAutofillCreditCardsEnabled) {
-      target.push("creditCard");
+    let detail = this.getFieldDetailByName("tel");
+    if (!detail) {
+      return;
     }
 
-    target.forEach(type => {
-      let details = this[type].fieldDetails;
-      if (!details || details.length == 0) {
+    let element = detail.elementWeakRef.get();
+    let _pattern;
+    let testPattern = str => {
+      if (!_pattern) {
+        // The pattern has to match the entire value.
+        _pattern = new RegExp("^(?:" + element.pattern + ")$", "u");
+      }
+      return _pattern.test(str);
+    };
+    if (element.pattern) {
+      if (testPattern(profile.tel)) {
+        return;
+      }
+    } else if (element.maxLength) {
+      if (detail._reason == "autocomplete" && profile.tel.length <= element.maxLength) {
         return;
       }
-
-      data[type] = {
-        guid: this[type].filledRecordGUID,
-        record: {},
-        untouchedFields: [],
-      };
-
-      details.forEach(detail => {
-        let element = detail.elementWeakRef.get();
-        // Remove the unnecessary spaces
-        let value = element && element.value.trim();
-
-        // Try to abbreviate the value of select element.
-        if (type == "address" &&
-            detail.fieldName == "address-level1" &&
-            ChromeUtils.getClassName(element) === "HTMLSelectElement") {
-          // Don't save the record when the option value is empty *OR* there
-          // are multiple options being selected. The empty option is usually
-          // assumed to be default along with a meaningless text to users.
-          if (!value || element.selectedOptions.length != 1) {
-            // Keep the property and preserve more information for address updating
-            data[type].record[detail.fieldName] = "";
-            return;
-          }
-
-          let text = element.selectedOptions[0].text.trim();
-          value = FormAutofillUtils.getAbbreviatedSubregionName([value, text]) || text;
-        }
-
-        if (!value || value.length > FormAutofillUtils.MAX_FIELD_VALUE_LENGTH) {
-          // Keep the property and preserve more information for updating
-          data[type].record[detail.fieldName] = "";
-          return;
-        }
-
-        data[type].record[detail.fieldName] = value;
-
-        if (detail.state == FIELD_STATES.AUTO_FILLED) {
-          data[type].untouchedFields.push(detail.fieldName);
-        }
-      });
-    });
-
-    this._normalizeAddress(data.address);
-
-    if (data.address && !this._isAddressRecordCreatable(data.address.record)) {
-      log.debug("No address record saving since there are only",
-                Object.keys(data.address.record).length,
-                "usable fields");
-      delete data.address;
     }
 
-    if (data.creditCard && !this._isCreditCardRecordCreatable(data.creditCard.record)) {
-      log.debug("No credit card record saving since card number is invalid");
-      delete data.creditCard;
+    if (detail._reason != "autocomplete") {
+      // Since we only target people living in US and using en-US websites in
+      // MVP, it makes more sense to fill `tel-national` instead of `tel`
+      // if the field is identified by heuristics and no other clues to
+      // determine which one is better.
+      // TODO: [Bug 1407545] This should be improved once more countries are
+      // supported.
+      profile.tel = profile["tel-national"];
+    } else if (element.pattern) {
+      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"];
+      }
     }
-
-    // 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) {
+  /*
+   * Apply all address related transformers.
+   *
+   * @param {Object} profile
+   *        A profile for adjusting address related value.
+   * @override
+   */
+  applyTransformers(profile) {
+    this.addressTransformer(profile);
+    this.telTransformer(profile);
+    this.matchSelectOptions(profile);
+    this.adaptFieldMaxLength(profile);
+  }
+
+  computeFillingValue(value, fieldDetail, element) {
+    // Try to abbreviate the value of select element.
+    if (fieldDetail.fieldName == "address-level1" &&
+      ChromeUtils.getClassName(element) === "HTMLSelectElement") {
+      // Don't save the record when the option value is empty *OR* there
+      // are multiple options being selected. The empty option is usually
+      // assumed to be default along with a meaningless text to users.
+      if (!value || element.selectedOptions.length != 1) {
+        // Keep the property and preserve more information for address updating
+        value = "";
+      } else {
+        let text = element.selectedOptions[0].text.trim();
+        value = FormAutofillUtils.getAbbreviatedSubregionName([value, text]) || text;
+      }
+    }
+    return value;
+  }
+
+  normalizeCreatingRecord(address) {
     if (!address) {
       return;
     }
 
     // Normalize Country
     if (address.record.country) {
-      let detail = this._getFieldDetailByName("country");
+      let detail = this.getFieldDetailByName("country");
       // Try identifying country field aggressively if it doesn't come from
       // @autocomplete.
       if (detail._reason != "autocomplete") {
         let countryCode = FormAutofillUtils.identifyCountryCode(address.record.country);
         if (countryCode) {
           address.record.country = countryCode;
         }
       }
@@ -751,63 +724,149 @@ class FormAutofillSection {
         // (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 = "";
         }
       }
     }
   }
+}
+
+class FormAutofillCreditCardSection extends FormAutofillSection {
+  constructor(fieldDetails, winUtils) {
+    super(fieldDetails, winUtils);
+  }
+
+  isValidSection() {
+    let ccNumberReason = "";
+    let hasCCNumber = false;
+    let hasExpiryDate = false;
+
+    for (let detail of this.fieldDetails) {
+      switch (detail.fieldName) {
+        case "cc-number":
+          hasCCNumber = true;
+          ccNumberReason = detail._reason;
+          break;
+        case "cc-exp":
+        case "cc-exp-month":
+        case "cc-exp-year":
+          hasExpiryDate = true;
+          break;
+      }
+    }
+
+    return hasCCNumber && (ccNumberReason == "autocomplete" || hasExpiryDate);
+  }
+
+  isEnabled() {
+    return FormAutofillUtils.isAutofillCreditCardsEnabled;
+  }
+
+  isRecordCreatable(record) {
+    return record["cc-number"] && FormAutofillUtils.isCCNumber(record["cc-number"]);
+  }
+
+  creditCardExpDateTransformer(profile) {
+    if (!profile["cc-exp"]) {
+      return;
+    }
+
+    let detail = this.getFieldDetailByName("cc-exp");
+    if (!detail) {
+      return;
+    }
+
+    let element = detail.elementWeakRef.get();
+    if (element.tagName != "INPUT" || !element.placeholder) {
+      return;
+    }
+
+    let result,
+      ccExpMonth = profile["cc-exp-month"],
+      ccExpYear = profile["cc-exp-year"],
+      placeholder = element.placeholder;
+
+    result = /(?:[^m]|\b)(m{1,2})\s*([-/\\]*)\s*(y{2,4})(?!y)/i.exec(placeholder);
+    if (result) {
+      profile["cc-exp"] = String(ccExpMonth).padStart(result[1].length, "0") +
+                          result[2] +
+                          String(ccExpYear).substr(-1 * result[3].length);
+      return;
+    }
+
+    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");
+    }
+  }
 
   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});
     });
   }
 
-  handleEvent(event) {
-    switch (event.type) {
-      case "input": {
-        if (!event.isTrusted) {
-          return;
-        }
-        const target = event.target;
-        const targetFieldDetail = this.getFieldDetailByElement(target);
-        const targetSet = this._getTargetSet(target);
-
-        this._changeFieldState(targetFieldDetail, FIELD_STATES.NORMAL);
-
-        let isAutofilled = false;
-        let dimFieldDetails = [];
-        for (const fieldDetail of targetSet.fieldDetails) {
-          const element = fieldDetail.elementWeakRef.get();
+  /*
+   * Apply all credit card related transformers.
+   *
+   * @param {Object} profile
+   *        A profile for adjusting credit card related value.
+   * @override
+   */
+  applyTransformers(profile) {
+    this.matchSelectOptions(profile);
+    this.creditCardExpDateTransformer(profile);
+    this.adaptFieldMaxLength(profile);
+  }
 
-          if (ChromeUtils.getClassName(element) === "HTMLSelectElement") {
-            // Dim fields are those we don't attempt to revert their value
-            // when clear the target set, such as <select>.
-            dimFieldDetails.push(fieldDetail);
-          } else {
-            isAutofilled |= fieldDetail.state == FIELD_STATES.AUTO_FILLED;
-          }
-        }
-        if (!isAutofilled) {
-          // Restore the dim fields to initial state as well once we knew
-          // that user had intention to clear the filled form manually.
-          for (const fieldDetail of dimFieldDetails) {
-            this._changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
-          }
-          targetSet.filledRecordGUID = null;
-        }
-        break;
+  /**
+   * Customize for previewing prorifle.
+   *
+   * @param {Object} profile
+   *        A profile for pre-processing before previewing values.
+   * @override
+   */
+  preparePreviewProfile(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"];
+    }
+  }
+
+  /**
+   * Customize for filling prorifle.
+   *
+   * @param {Object} profile
+   *        A profile for pre-processing before filling values.
+   * @override
+   */
+  async prepareFillingProfile(profile) {
+    // When Master Password is enabled by users, the decryption process
+    // should prompt Master Password dialog to get the decrypted credit
+    // card number. Otherwise, the number can be decrypted with the default
+    // password.
+    if (profile["cc-number-encrypted"]) {
+      let decrypted = await this._decrypt(profile["cc-number-encrypted"], true);
+
+      if (!decrypted) {
+        // Early return if the decrypted is empty or undefined
+        return;
       }
+
+      profile["cc-number"] = decrypted;
     }
   }
 }
 
 /**
  * Handles profile autofill for a DOM Form element.
  */
 class FormAutofillHandler {
@@ -930,20 +989,27 @@ class FormAutofillHandler {
    * @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);
+    for (let {fieldDetails, type} of sections) {
+      let section;
+      if (type == FormAutofillUtils.SECTION_TYPES.ADDRESS) {
+        section = new FormAutofillAddressSection(fieldDetails, this.winUtils);
+      } else if (type == FormAutofillUtils.SECTION_TYPES.CREDIT_CARD) {
+        section = new FormAutofillCreditCardSection(fieldDetails, this.winUtils);
+      } else {
+        throw new Error("Unknown field type.");
+      }
       this.sections.push(section);
-      allValidDetails.push(...section.validDetails);
+      allValidDetails.push(...section.fieldDetails);
     }
 
     for (let detail of allValidDetails) {
       let input = detail.elementWeakRef.get();
       if (!input) {
         continue;
       }
       input.addEventListener("input", this, {mozSystemGroup: true});
@@ -1021,17 +1087,24 @@ class FormAutofillHandler {
    */
   createRecords() {
     const records = {
       address: [],
       creditCard: [],
     };
 
     for (const section of this.sections) {
-      const secRecords = section.createRecords();
-      for (const [type, record] of Object.entries(secRecords)) {
-        records[type].push(record);
+      const secRecord = section.createRecord();
+      if (!secRecord) {
+        continue;
+      }
+      if (section instanceof FormAutofillAddressSection) {
+        records.address.push(secRecord);
+      } else if (section instanceof FormAutofillCreditCardSection) {
+        records.creditCard.push(secRecord);
+      } else {
+        throw new Error("Unknown section type");
       }
     }
     log.debug("Create records:", records);
     return records;
   }
 }
--- a/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
@@ -11,17 +11,16 @@ let {MasterPassword} = Cu.import("resour
 const TESTCASES = [
   {
     description: "Form without autocomplete property",
     document: `<form><input id="given-name"><input id="family-name">
                <input id="street-addr"><input id="city"><select id="country"></select>
                <input id='email'><input id="tel"></form>`,
     focusedInputId: "given-name",
     profileData: {},
-    expectedFillingForm: "address",
     expectedResult: {
       "street-addr": "",
       "city": "",
       "country": "",
       "email": "",
       "tel": "",
     },
   },
@@ -42,17 +41,16 @@ const TESTCASES = [
       "guid": "123",
       "street-address": "2 Harrison St line2",
       "-moz-street-address-one-line": "2 Harrison St line2",
       "address-level2": "San Francisco",
       "country": "US",
       "email": "foo@mozilla.com",
       "tel": "1234567",
     },
-    expectedFillingForm: "address",
     expectedResult: {
       "street-addr": "2 Harrison St line2",
       "city": "San Francisco",
       "country": "US",
       "email": "foo@mozilla.com",
       "tel": "1234567",
     },
   },
@@ -72,17 +70,16 @@ const TESTCASES = [
     profileData: {
       "guid": "123",
       "street-address": "2 Harrison St",
       "address-level2": "San Francisco",
       "country": "US",
       "email": "foo@mozilla.com",
       "tel": "1234567",
     },
-    expectedFillingForm: "address",
     expectedResult: {
       "street-addr": "2 Harrison St",
       "city": "San Francisco",
       "country": "US",
       "email": "foo@mozilla.com",
       "tel": "1234567",
     },
   },
@@ -99,17 +96,16 @@ const TESTCASES = [
     profileData: {
       "guid": "123",
       "street-address": "2 Harrison St",
       "address-level2": "San Francisco",
       "country": "US",
       "email": "",
       "tel": "",
     },
-    expectedFillingForm: "address",
     expectedResult: {
       "street-addr": "2 Harrison St",
       "city": "San Francisco",
       "country": "US",
       "email": "",
       "tel": "",
     },
   },
@@ -126,17 +122,16 @@ const TESTCASES = [
     profileData: {
       "guid": "123",
       "street-address": "",
       "address-level2": "",
       "country": "",
       "email": "foo@mozilla.com",
       "tel": "1234567",
     },
-    expectedFillingForm: "address",
     expectedResult: {
       "street-addr": "",
       "city": "",
       "country": "",
       "email": "foo@mozilla.com",
       "tel": "1234567",
     },
   },
@@ -155,17 +150,16 @@ const TESTCASES = [
                </select>
                </form>`,
     focusedInputId: "given-name",
     profileData: {
       "guid": "123",
       "country": "US",
       "address-level1": "CA",
     },
-    expectedFillingForm: "address",
     expectedResult: {
       "country": "US",
       "state": "CA",
     },
   },
   {
     description: "Form with autocomplete select elements and matching option texts",
     document: `<form>
@@ -181,17 +175,16 @@ const TESTCASES = [
                </select>
                </form>`,
     focusedInputId: "given-name",
     profileData: {
       "guid": "123",
       "country": "United States",
       "address-level1": "California",
     },
-    expectedFillingForm: "address",
     expectedResult: {
       "country": "US",
       "state": "CA",
     },
   },
   {
     description: "Fill address fields in a form with addr and CC fields.",
     document: `<form>
@@ -215,17 +208,16 @@ const TESTCASES = [
       "guid": "123",
       "street-address": "2 Harrison St line2",
       "-moz-street-address-one-line": "2 Harrison St line2",
       "address-level2": "San Francisco",
       "country": "US",
       "email": "foo@mozilla.com",
       "tel": "1234567",
     },
-    expectedFillingForm: "address",
     expectedResult: {
       "street-addr": "2 Harrison St line2",
       "city": "San Francisco",
       "country": "US",
       "email": "foo@mozilla.com",
       "tel": "1234567",
       "cc-number": "",
       "cc-name": "",
@@ -254,17 +246,16 @@ const TESTCASES = [
     focusedInputId: "cc-number",
     profileData: {
       "guid": "123",
       "cc-number": "1234000056780000",
       "cc-name": "test name",
       "cc-exp-month": "06",
       "cc-exp-year": "25",
     },
-    expectedFillingForm: "creditCard",
     expectedResult: {
       "street-addr": "",
       "city": "",
       "country": "",
       "email": "",
       "tel": "",
       "cc-number": "1234000056780000",
       "cc-name": "test name",
@@ -291,17 +282,16 @@ const TESTCASES_INPUT_UNCHANGED = [
                </select>
                </form>`,
     focusedInputId: "given-name",
     profileData: {
       "guid": "123",
       "country": "US",
       "address-level1": "unknown state",
     },
-    expectedFillingForm: "address",
     expectedResult: {
       "country": "US",
       "state": "",
     },
   },
 ];
 
 const TESTCASES_FILL_SELECT = [
@@ -316,17 +306,16 @@ const TESTCASES_FILL_SELECT = [
                  <option value="CA">California</option>
                </select></form>`,
     focusedInputId: "given-name",
     profileData: {
       "guid": "123",
       "country": "US",
       "address-level1": "CA",
     },
-    expectedFillingForm: "address",
     expectedResult: {
       "state": "CA",
     },
   },
   {
     description: "Form with US states select elements; with lower case state key",
     document: `<form>
                <input id="given-name" autocomplete="shipping given-name">
@@ -336,17 +325,16 @@ const TESTCASES_FILL_SELECT = [
                  <option value="ca">ca</option>
                </select></form>`,
     focusedInputId: "given-name",
     profileData: {
       "guid": "123",
       "country": "US",
       "address-level1": "CA",
     },
-    expectedFillingForm: "address",
     expectedResult: {
       "state": "ca",
     },
   },
   {
     description: "Form with US states select elements; with state name and extra spaces",
     document: `<form>
                <input id="given-name" autocomplete="shipping given-name">
@@ -356,17 +344,16 @@ const TESTCASES_FILL_SELECT = [
                  <option value="CA">CA</option>
                </select></form>`,
     focusedInputId: "given-name",
     profileData: {
       "guid": "123",
       "country": "US",
       "address-level1": " California ",
     },
-    expectedFillingForm: "address",
     expectedResult: {
       "state": "CA",
     },
   },
   {
     description: "Form with US states select elements; with partial state key match",
     document: `<form>
                <input id="given-name" autocomplete="shipping given-name">
@@ -376,17 +363,16 @@ const TESTCASES_FILL_SELECT = [
                  <option value="US-WA">WA-Washington</option>
                </select></form>`,
     focusedInputId: "given-name",
     profileData: {
       "guid": "123",
       "country": "US",
       "address-level1": "WA",
     },
-    expectedFillingForm: "address",
     expectedResult: {
       "state": "US-WA",
     },
   },
 
   // Country
   {
     description: "Form with country select elements",
@@ -397,17 +383,16 @@ const TESTCASES_FILL_SELECT = [
                  <option value=""></option>
                  <option value="US">United States</option>
                </select></form>`,
     focusedInputId: "given-name",
     profileData: {
       "guid": "123",
       "country": "US",
     },
-    expectedFillingForm: "address",
     expectedResult: {
       "country": "US",
     },
   },
   {
     description: "Form with country select elements; with lower case key",
     document: `<form>
                <input id="given-name" autocomplete="given-name">
@@ -416,17 +401,16 @@ const TESTCASES_FILL_SELECT = [
                  <option value=""></option>
                  <option value="us">us</option>
                </select></form>`,
     focusedInputId: "given-name",
     profileData: {
       "guid": "123",
       "country": "US",
     },
-    expectedFillingForm: "address",
     expectedResult: {
       "country": "us",
     },
   },
   {
     description: "Form with country select elements; with alternative name 1",
     document: `<form>
                <input id="given-name" autocomplete="given-name">
@@ -435,17 +419,16 @@ const TESTCASES_FILL_SELECT = [
                  <option value=""></option>
                  <option value="XX">United States</option>
                </select></form>`,
     focusedInputId: "given-name",
     profileData: {
       "guid": "123",
       "country": "US",
     },
-    expectedFillingForm: "address",
     expectedResult: {
       "country": "XX",
     },
   },
   {
     description: "Form with country select elements; with alternative name 2",
     document: `<form>
                <input id="given-name" autocomplete="given-name">
@@ -454,17 +437,16 @@ const TESTCASES_FILL_SELECT = [
                  <option value=""></option>
                  <option value="XX">America</option>
                </select></form>`,
     focusedInputId: "given-name",
     profileData: {
       "guid": "123",
       "country": "US",
     },
-    expectedFillingForm: "address",
     expectedResult: {
       "country": "XX",
     },
   },
   {
     description: "Form with country select elements; with partial matching value",
     document: `<form>
                <input id="given-name" autocomplete="given-name">
@@ -473,17 +455,16 @@ const TESTCASES_FILL_SELECT = [
                  <option value=""></option>
                  <option value="XX">Ship to America</option>
                </select></form>`,
     focusedInputId: "given-name",
     profileData: {
       "guid": "123",
       "country": "US",
     },
-    expectedFillingForm: "address",
     expectedResult: {
       "country": "XX",
     },
   },
 ];
 
 function do_test(testcases, testFn) {
   for (let tc of testcases) {
@@ -513,39 +494,37 @@ function do_test(testcases, testFn) {
               throw e;
             }
             info("User canceled master password entry");
           }
           return string;
         };
 
         handler.collectFormFields();
+
+        let focusedInput = doc.getElementById(testcase.focusedInputId);
+        handler.focusedInput = focusedInput;
+
         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 => {
+        handler.activeSection.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 focusedInput = doc.getElementById(testcase.focusedInputId);
-        handler.focusedInput = focusedInput;
         let [adaptedProfile] = handler.activeSection.getAdaptedProfiles([testcase.profileData]);
         await handler.autofillFormFields(adaptedProfile, focusedInput);
-        Assert.equal(handlerInfo.filledRecordGUID, testcase.profileData.guid,
+        Assert.equal(handler.activeSection.filledRecordGUID, testcase.profileData.guid,
                      "Check if filledRecordGUID is set correctly");
         await Promise.all(promises);
       });
     })();
   }
 }
 
 do_test(TESTCASES, (testcase, element) => {
--- a/browser/extensions/formautofill/test/unit/test_collectFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_collectFormFields.js
@@ -7,28 +7,27 @@
 Cu.import("resource://formautofill/FormAutofillHandler.jsm");
 
 const TESTCASES = [
   {
     description: "Form without autocomplete property",
     document: `<form><input id="given-name"><input id="family-name">
                <input id="street-addr"><input id="city"><select id="country"></select>
                <input id='email'><input id="phone"></form>`,
-    sections: [{
-      addressFieldDetails: [
+    sections: [
+      [
         {"section": "", "addressType": "", "contactType": "", "fieldName": "given-name"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line1"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "country"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "email"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "tel"},
       ],
-      creditCardFieldDetails: [],
-    }],
+    ],
     validFieldDetails: [
       {"section": "", "addressType": "", "contactType": "", "fieldName": "given-name"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line1"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "country"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "email"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "tel"},
@@ -45,33 +44,32 @@ const TESTCASES = [
                <select id="country" autocomplete="country"></select>
                <input id="email" autocomplete="email">
                <input id="tel" autocomplete="tel">
                <input id="cc-number" autocomplete="cc-number">
                <input id="cc-name" autocomplete="cc-name">
                <input id="cc-exp-month" autocomplete="cc-exp-month">
                <input id="cc-exp-year" autocomplete="cc-exp-year">
                </form>`,
-    sections: [{
-      addressFieldDetails: [
+    sections: [
+      [
         {"section": "", "addressType": "", "contactType": "", "fieldName": "given-name"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "street-address"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "country"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "email"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "tel"},
-      ],
-      creditCardFieldDetails: [
+      ], [
         {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-number"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-name"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-exp-month"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-exp-year"},
       ],
-    }],
+    ],
     validFieldDetails: [
       {"section": "", "addressType": "", "contactType": "", "fieldName": "given-name"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "street-address"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "country"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "email"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "tel"},
@@ -85,28 +83,27 @@ const TESTCASES = [
     description: "An address form with autocomplete properties and 2 tokens",
     document: `<form><input id="given-name" autocomplete="shipping given-name">
                <input id="family-name" autocomplete="shipping family-name">
                <input id="street-addr" autocomplete="shipping street-address">
                <input id="city" autocomplete="shipping address-level2">
                <input id="country" autocomplete="shipping country">
                <input id='email' autocomplete="shipping email">
                <input id="tel" autocomplete="shipping tel"></form>`,
-    sections: [{
-      addressFieldDetails: [
+    sections: [
+      [
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name"},
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name"},
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address"},
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2"},
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country"},
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email"},
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel"},
       ],
-      creditCardFieldDetails: [],
-    }],
+    ],
     validFieldDetails: [
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel"},
@@ -116,28 +113,27 @@ const TESTCASES = [
     description: "Form with autocomplete properties and profile is partly matched",
     document: `<form><input id="given-name" autocomplete="shipping given-name">
                <input id="family-name" autocomplete="shipping family-name">
                <input id="street-addr" autocomplete="shipping street-address">
                <input autocomplete="shipping address-level2">
                <select autocomplete="shipping country"></select>
                <input id='email' autocomplete="shipping email">
                <input id="tel" autocomplete="shipping tel"></form>`,
-    sections: [{
-      addressFieldDetails: [
+    sections: [
+      [
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name"},
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name"},
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address"},
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2"},
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country"},
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email"},
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel"},
       ],
-      creditCardFieldDetails: [],
-    }],
+    ],
     validFieldDetails: [
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel"},
@@ -146,122 +142,109 @@ const TESTCASES = [
   {
     description: "It's a valid address and credit card form.",
     document: `<form>
                <input id="given-name" autocomplete="shipping given-name">
                <input id="family-name" autocomplete="shipping family-name">
                <input id="street-addr" autocomplete="shipping street-address">
                <input id="cc-number" autocomplete="shipping cc-number">
                </form>`,
-    sections: [{
-      addressFieldDetails: [
+    sections: [
+      [
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name"},
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name"},
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address"},
-      ],
-      creditCardFieldDetails: [
+      ], [
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "cc-number"},
       ],
-    }],
+    ],
     validFieldDetails: [
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "cc-number"},
     ],
   },
   {
     description: "An invalid address form due to less than 3 fields.",
     document: `<form>
                <input id="given-name" autocomplete="shipping given-name">
                <input autocomplete="shipping address-level2">
                </form>`,
-    sections: [{
-      addressFieldDetails: [],
-      creditCardFieldDetails: [],
-    }],
+    sections: [[]],
     validFieldDetails: [],
   },
   {
     description: "An invalid credit card form due to omitted cc-number.",
     document: `<form>
                <input id="cc-name" autocomplete="cc-name">
                <input id="cc-exp-month" autocomplete="cc-exp-month">
                <input id="cc-exp-year" autocomplete="cc-exp-year">
                </form>`,
-    sections: [{
-      addressFieldDetails: [],
-      creditCardFieldDetails: [],
-    }],
+    sections: [[]],
     validFieldDetails: [],
   },
   {
     description: "An invalid credit card form due to non-autocomplete-attr cc-number and omitted cc-exp-*.",
     document: `<form>
                <input id="cc-name" autocomplete="cc-name">
                <input id="cc-number" name="card-number">
                </form>`,
-    sections: [{
-      addressFieldDetails: [],
-      creditCardFieldDetails: [],
-    }],
+    sections: [[]],
     validFieldDetails: [],
   },
   {
     description: "A valid credit card form with autocomplete-attr cc-number only.",
     document: `<form>
                <input id="cc-number" autocomplete="cc-number">
                </form>`,
-    sections: [{
-      addressFieldDetails: [],
-      creditCardFieldDetails: [
+    sections: [
+      [
         {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-number"},
       ],
-    }],
+    ],
     validFieldDetails: [
       {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-number"},
     ],
   },
   {
     description: "A valid credit card form with non-autocomplete-attr cc-number and cc-exp.",
     document: `<form>
                <input id="cc-number" name="card-number">
                <input id="cc-exp" autocomplete="cc-exp">
                </form>`,
-    sections: [{
-      addressFieldDetails: [],
-      creditCardFieldDetails: [
+    sections: [
+      [
         {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-number"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-exp"},
       ],
-    }],
+    ],
     validFieldDetails: [
       {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-number"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-exp"},
     ],
     ids: [
       "cc-number",
       "cc-exp",
     ],
   },
   {
     description: "A valid credit card form with non-autocomplete-attr cc-number and cc-exp-month/cc-exp-year.",
     document: `<form>
                <input id="cc-number" name="card-number">
                <input id="cc-exp-month" autocomplete="cc-exp-month">
                <input id="cc-exp-year" autocomplete="cc-exp-year">
                </form>`,
-    sections: [{
-      addressFieldDetails: [],
-      creditCardFieldDetails: [
+    sections: [
+      [
         {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-number"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-exp-month"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-exp-year"},
       ],
-    }],
+    ],
     validFieldDetails: [
       {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-number"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-exp-month"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-exp-year"},
     ],
     ids: [
       "cc-number",
       "cc-exp-month",
@@ -281,44 +264,37 @@ const TESTCASES = [
                  <input id="billingSuffix" name="phone" maxlength="4">
 
                  <input id="otherCC" name="phone" maxlength="3">
                  <input id="otherAC" name="phone" maxlength="3">
                  <input id="otherPrefix" name="phone" maxlength="3">
                  <input id="otherSuffix" name="phone" maxlength="4">
                </form>`,
     allowDuplicates: true,
-    sections: [{
-      addressFieldDetails: [
+    sections: [
+      [
         {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-area-code"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-local-prefix"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-local-suffix"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-extension"},
-      ],
-      creditCardFieldDetails: [],
-    }, {
-      addressFieldDetails: [
+      ], [
         {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-area-code"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-local-prefix"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-local-suffix"},
 
         // TODO Bug 1421181 - "tel-country-code" field should belong to the next
         // section. There should be a way to group the related fields during the
         // parsing stage.
         {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-country-code"},
-      ],
-      creditCardFieldDetails: [],
-    }, {
-      addressFieldDetails: [
+      ], [
         {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-area-code"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-local-prefix"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-local-suffix"},
       ],
-      creditCardFieldDetails: [],
-    }],
+    ],
     validFieldDetails: [
       {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-area-code"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-local-prefix"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-local-suffix"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-extension"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-area-code"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-local-prefix"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-local-suffix"},
@@ -340,26 +316,25 @@ const TESTCASES = [
                  <input id="i2" autocomplete="family-name">
                  <input id="i3" autocomplete="street-address">
                  <input id="i4" autocomplete="email">
 
                  <input id="homePhone" maxlength="10">
                  <input id="mobilePhone" maxlength="10">
                  <input id="officePhone" maxlength="10">
                </form>`,
-    sections: [{
-      addressFieldDetails: [
+    sections: [
+      [
         {"section": "", "addressType": "", "contactType": "", "fieldName": "given-name"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "street-address"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "email"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "tel"},
       ],
-      creditCardFieldDetails: [],
-    }],
+    ],
     validFieldDetails: [
       {"section": "", "addressType": "", "contactType": "", "fieldName": "given-name"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "street-address"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "email"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "tel"},
     ],
     ids: ["i1", "i2", "i3", "i4", "homePhone"],
@@ -371,33 +346,32 @@ const TESTCASES = [
                  <input id="i2" autocomplete="shipping family-name">
                  <input id="i3" autocomplete="shipping street-address">
                  <input id="i4" autocomplete="shipping email">
                  <input id="singlePhone" autocomplete="shipping tel">
                  <input id="shippingAreaCode" autocomplete="shipping tel-area-code">
                  <input id="shippingPrefix" autocomplete="shipping tel-local-prefix">
                  <input id="shippingSuffix" autocomplete="shipping tel-local-suffix">
                </form>`,
-    sections: [{
-      addressFieldDetails: [
+    sections: [
+      [
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name"},
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name"},
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address"},
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email"},
 
         // NOTES: Ideally, there is only one full telephone field(s) in a form for
         // this case. We can see if there is any better solution later.
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel"},
 
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel-area-code"},
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel-local-prefix"},
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel-local-suffix"},
       ],
-      creditCardFieldDetails: [],
-    }],
+    ],
     validFieldDetails: [
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel-area-code"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel-local-prefix"},
@@ -410,24 +384,23 @@ const TESTCASES = [
     description: "Always adopt the info from autocomplete attribute.",
     document: `<form>
                  <input id="given-name" autocomplete="shipping given-name">
                  <input id="family-name" autocomplete="shipping family-name">
                  <input id="dummyAreaCode" autocomplete="shipping tel" maxlength="3">
                  <input id="dummyPrefix" autocomplete="shipping tel" maxlength="3">
                  <input id="dummySuffix" autocomplete="shipping tel" maxlength="4">
                </form>`,
-    sections: [{
-      addressFieldDetails: [
+    sections: [
+      [
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name"},
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name"},
         {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel"},
       ],
-      creditCardFieldDetails: [],
-    }],
+    ],
     validFieldDetails: [
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel"},
     ],
     ids: ["given-name", "family-name", "dummyAreaCode"],
   },
 ];
@@ -459,36 +432,35 @@ for (let tc of TESTCASES) {
         });
       }
 
       function verifyDetails(handlerDetails, testCaseDetails) {
         if (handlerDetails === null) {
           Assert.equal(handlerDetails, testCaseDetails);
           return;
         }
-        Assert.equal(handlerDetails.length, testCaseDetails.length);
+        Assert.equal(handlerDetails.length, testCaseDetails.length, "field count");
         handlerDetails.forEach((detail, index) => {
           Assert.equal(detail.fieldName, testCaseDetails[index].fieldName, "fieldName");
           Assert.equal(detail.section, testCaseDetails[index].section, "section");
           Assert.equal(detail.addressType, testCaseDetails[index].addressType, "addressType");
           Assert.equal(detail.contactType, testCaseDetails[index].contactType, "contactType");
           Assert.equal(detail.elementWeakRef.get(), testCaseDetails[index].elementWeakRef.get(), "DOM reference");
         });
       }
       setElementWeakRef(testcase.sections.reduce((fieldDetails, section) => {
-        fieldDetails.push(...section.addressFieldDetails, ...section.creditCardFieldDetails);
+        fieldDetails.push(...section);
         return fieldDetails;
       }, []));
       setElementWeakRef(testcase.validFieldDetails);
 
       let handler = new FormAutofillHandler(formLike);
       let validFieldDetails = handler.collectFormFields(testcase.allowDuplicates);
 
-      Assert.equal(handler.sections.length, testcase.sections.length);
+      Assert.equal(handler.sections.length, testcase.sections.length, "section count");
       for (let i = 0; i < handler.sections.length; i++) {
         let section = handler.sections[i];
-        verifyDetails(section.address.fieldDetails, testcase.sections[i].addressFieldDetails);
-        verifyDetails(section.creditCard.fieldDetails, testcase.sections[i].creditCardFieldDetails);
+        verifyDetails(section.fieldDetails, testcase.sections[i]);
       }
       verifyDetails(validFieldDetails, testcase.validFieldDetails);
     });
   })();
 }