Bug 1379600 - Implement the feature of filling and previewing Credit Card fields. r=lchang,steveck draft
authorSean Lee <selee@mozilla.com>
Thu, 27 Jul 2017 03:53:19 +0800
changeset 651071 e1b4d7f8dd0885efcc7b83f97a38cbaf65adbfc7
parent 649622 7dddbd85047c6dc73ddbe1e423cd643a217845b3
child 651090 a889bcc60895080b9dcee95217eff6b3c695d735
push id75572
push userbmo:selee@mozilla.com
push dateWed, 23 Aug 2017 06:15:23 +0000
reviewerslchang, steveck
bugs1379600
milestone57.0a1
Bug 1379600 - Implement the feature of filling and previewing Credit Card fields. r=lchang,steveck MozReview-Commit-ID: ADklL3VFujI
browser/extensions/formautofill/FormAutofillContent.jsm
browser/extensions/formautofill/FormAutofillHandler.jsm
browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
browser/extensions/formautofill/test/mochitest/test_formautofill_preview_highlight.html
browser/extensions/formautofill/test/unit/test_autofillFormFields.js
browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js
--- a/browser/extensions/formautofill/FormAutofillContent.jsm
+++ b/browser/extensions/formautofill/FormAutofillContent.jsm
@@ -289,17 +289,17 @@ let ProfileAutocomplete = {
   _clearProfilePreview() {
     let focusedInput = formFillController.focusedInput || this._lastAutoCompleteFocusedInput;
     if (!focusedInput || !FormAutofillContent.getFormDetails(focusedInput)) {
       return;
     }
 
     let formHandler = FormAutofillContent.getFormHandler(focusedInput);
 
-    formHandler.clearPreviewedFormFields();
+    formHandler.clearPreviewedFormFields(focusedInput);
   },
 
   _previewSelectedProfile(selectedIndex) {
     let focusedInput = formFillController.focusedInput;
     if (!focusedInput || !FormAutofillContent.getFormDetails(focusedInput)) {
       // The observer notification is for a different process/frame.
       return;
     }
@@ -307,17 +307,17 @@ let ProfileAutocomplete = {
     if (!this._lastAutoCompleteResult ||
         this._lastAutoCompleteResult.getStyleAt(selectedIndex) != "autofill-profile") {
       return;
     }
 
     let profile = JSON.parse(this._lastAutoCompleteResult.getCommentAt(selectedIndex));
     let formHandler = FormAutofillContent.getFormHandler(focusedInput);
 
-    formHandler.previewFormFields(profile);
+    formHandler.previewFormFields(profile, focusedInput);
   },
 };
 
 /**
  * Handles content's interactions for the process.
  *
  * NOTE: Declares it by "var" to make it accessible in unit tests.
  */
--- a/browser/extensions/formautofill/FormAutofillHandler.jsm
+++ b/browser/extensions/formautofill/FormAutofillHandler.jsm
@@ -13,16 +13,18 @@ this.EXPORTED_SYMBOLS = ["FormAutofillHa
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 Cu.import("resource://formautofill/FormAutofillUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "FormAutofillHeuristics",
                                   "resource://formautofill/FormAutofillHeuristics.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MasterPassword",
+                                  "resource://formautofill/MasterPassword.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
  */
@@ -150,16 +152,29 @@ FormAutofillHandler.prototype = {
     return Array.of(...(this.address.fieldDetails),
                     ...(this.creditCard.fieldDetails));
   },
 
   getFieldDetailByName(fieldName) {
     return this.fieldDetails.find(detail => detail.fieldName == fieldName);
   },
 
+  getFieldDetailsByElement(element) {
+    let fieldDetail = this.fieldDetails.find(
+      detail => detail.elementWeakRef.get() == element
+    );
+    if (FormAutofillUtils.isAddressField(fieldDetail.fieldName)) {
+      return this.address.fieldDetails;
+    }
+    if (FormAutofillUtils.isCreditCardField(fieldDetail.fieldName)) {
+      return this.creditCard.fieldDetails;
+    }
+    return [];
+  },
+
   _cacheValue: {
     allFieldNames: null,
     oneLineStreetAddress: null,
     matchingSelectOption: null,
   },
 
   get allFieldNames() {
     if (!this._cacheValue.allFieldNames) {
@@ -250,85 +265,105 @@ FormAutofillHandler.prototype = {
 
   /**
    * 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 {Object} focusedInput
-   *        A focused input element which is skipped for filling.
+   *        A focused input element needed to determine the address or credit
+   *        card field.
    */
-  autofillFormFields(profile, focusedInput) {
+  async autofillFormFields(profile, focusedInput) {
+    let focusedDetail = this.fieldDetails.find(
+      detail => detail.elementWeakRef.get() == focusedInput
+    );
+    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
+      // card number. Otherwise, the number can be decrypted with the default
+      // password.
+      if (profile["cc-number-encrypted"]) {
+        try {
+          profile["cc-number"] = await MasterPassword.decrypt(profile["cc-number-encrypted"], true);
+        } catch (e) {
+          if (e.result == Cr.NS_ERROR_ABORT) {
+            log.warn("User canceled master password entry");
+            return;
+          }
+          throw e;
+        }
+      }
+      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);
 
-    this.address.filledRecordGUID = profile.guid;
-    for (let fieldDetail of this.address.fieldDetails) {
+    targetSet.filledRecordGUID = profile.guid;
+    for (let fieldDetail of targetSet.fieldDetails) {
       // Avoid filling field value in the following cases:
-      // 1. the focused input which is filled in FormFillController.
-      // 2. a non-empty input field
-      // 3. the invalid value set
-      // 4. value already chosen in select element
+      // 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;
       }
 
+      element.previewValue = "";
       let value = profile[fieldDetail.fieldName];
-      if (element instanceof Ci.nsIDOMHTMLInputElement && !element.value && value) {
-        if (element !== focusedInput) {
+
+      if (element instanceof Ci.nsIDOMHTMLInputElement && value) {
+        // For the focused input element, it will be filled with a valid value
+        // anyway.
+        // For the others, the fields should be only filled when their values
+        // are empty.
+        if (element == focusedInput ||
+            (element != focusedInput && !element.value)) {
           element.setUserInput(value);
+          this.changeFieldState(fieldDetail, "AUTO_FILLED");
+          continue;
         }
-        this.changeFieldState(fieldDetail, "AUTO_FILLED");
-      } else if (element instanceof Ci.nsIDOMHTMLSelectElement) {
+      }
+
+      if (element instanceof Ci.nsIDOMHTMLSelectElement) {
         let cache = this._cacheValue.matchingSelectOption.get(element) || {};
         let option = cache[value] && cache[value].get();
         if (!option) {
           continue;
         }
         // Do not change value or dispatch events if the option is already selected.
         // Use case for multiple select is not considered here.
         if (!option.selected) {
           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");
       }
-
-      // Unlike using setUserInput directly, FormFillController dispatches an
-      // asynchronous "DOMAutoComplete" event with an "input" event follows right
-      // after. So, we need to suppress the first "input" event fired off from
-      // focused input to make sure the latter change handler won't be affected
-      // by auto filling.
-      if (element === focusedInput) {
-        const suppressFirstInputHandler = e => {
-          if (e.isTrusted) {
-            e.stopPropagation();
-            element.removeEventListener("input", suppressFirstInputHandler);
-          }
-        };
-
-        element.addEventListener("input", suppressFirstInputHandler);
-      }
-      element.previewValue = "";
     }
 
     // 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 this.address.fieldDetails) {
+      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");
@@ -336,34 +371,43 @@ FormAutofillHandler.prototype = {
 
         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);
-        this.address.filledRecordGUID = null;
+        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 {Object} focusedInput
+   *        A focused input element for determining credit card or address fields.
    */
-  previewFormFields(profile) {
+  async previewFormFields(profile, focusedInput) {
     log.debug("preview profile in autofillFormFields:", profile);
 
-    for (let fieldDetail of this.address.fieldDetails) {
+    // Always show the decrypted credit card number when Master Password is
+    // disabled.
+    if (profile["cc-number-encrypted"] && !MasterPassword.isEnabled) {
+      profile["cc-number"] = await MasterPassword.decrypt(profile["cc-number-encrypted"], true);
+    }
+
+    let fieldDetails = this.getFieldDetailsByElement(focusedInput);
+    for (let fieldDetail of fieldDetails) {
       let element = fieldDetail.elementWeakRef.get();
       let value = profile[fieldDetail.fieldName] || "";
 
       // Skip the field that is null
       if (!element) {
         continue;
       }
 
@@ -385,21 +429,25 @@ FormAutofillHandler.prototype = {
       }
       element.previewValue = value;
       this.changeFieldState(fieldDetail, value ? "PREVIEW" : "NORMAL");
     }
   },
 
   /**
    * Clear preview text and background highlight of all fields.
+   *
+   * @param {Object} focusedInput
+   *        A focused input element for determining credit card or address fields.
    */
-  clearPreviewedFormFields() {
+  clearPreviewedFormFields(focusedInput) {
     log.debug("clear previewed fields in:", this.form);
 
-    for (let fieldDetail of this.address.fieldDetails) {
+    let fieldDetails = this.getFieldDetailsByElement(focusedInput);
+    for (let fieldDetail of fieldDetails) {
       let element = fieldDetail.elementWeakRef.get();
       if (!element) {
         log.warn(fieldDetail.fieldName, "is unreachable");
         continue;
       }
 
       element.previewValue = "";
 
--- a/browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
+++ b/browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
@@ -262,16 +262,21 @@ class AddressResult extends ProfileAutoC
       primary: "",
       secondary: "",
       categories: FormAutofillUtils.getCategoriesFromFieldNames(this._allFieldNames),
       focusedCategory: FormAutofillUtils.getCategoryFromFieldName(this._focusedFieldName),
     });
 
     return labels;
   }
+
+  getValueAt(index) {
+    this._checkIndexBounds(index);
+    return "";
+  }
 }
 
 class CreditCardResult extends ProfileAutoCompleteResult {
   constructor(...args) {
     super(...args);
   }
 
   _getSecondaryLabel(focusedFieldName, allFieldNames, profile) {
--- a/browser/extensions/formautofill/test/mochitest/test_formautofill_preview_highlight.html
+++ b/browser/extensions/formautofill/test/mochitest/test_formautofill_preview_highlight.html
@@ -44,17 +44,17 @@ function checkFieldPreview(elem, expecte
 }
 
 function checkFilledFieldHighlight(elem, expectedValue) {
   const computedStyle = window.getComputedStyle(elem);
   const isStyleApplied = computedStyle.getPropertyValue("filter") !== "none" &&
                          computedStyle.getPropertyValue("color") === defaultTextColor;
 
   is(SpecialPowers.wrap(elem).previewValue, "", `Checking #${elem.id} filled previewValue`);
-  is(expectedValue, isStyleApplied, `Checking #${elem.id} filled style`);
+  is(isStyleApplied, expectedValue, `Checking #${elem.id} filled style`);
 }
 
 function checkFormPreviewFields(previewingAddress) {
   const inputs = document.querySelectorAll("input");
 
   for (const input of inputs) {
     const previewValue = previewingAddress && previewingAddress[input.id] || "";
 
--- a/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
@@ -1,25 +1,27 @@
 /*
  * Test for form auto fill content helper fill all inputs function.
  */
 /* eslint-disable mozilla/no-arbitrary-setTimeout */
 
 "use strict";
 
 Cu.import("resource://formautofill/FormAutofillHandler.jsm");
+let {MasterPassword} = Cu.import("resource://formautofill/MasterPassword.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="tel"></form>`,
-    addressFieldDetails: [],
+    focusedInputId: "given-name",
     profileData: {},
+    expectedFillingForm: "address",
     expectedResult: {
       "street-addr": "",
       "city": "",
       "country": "",
       "email": "",
       "tel": "",
     },
   },
@@ -30,34 +32,27 @@ const TESTCASES = [
                <input id="street-addr" autocomplete="street-address">
                <input id="city" autocomplete="address-level2">
                <select id="country" autocomplete="country">
                  <option/>
                  <option value="US">United States</option>
                </select>
                <input id="email" autocomplete="email">
                <input id="tel" autocomplete="tel"></form>`,
-    addressFieldDetails: [
-      {"section": "", "addressType": "", "contactType": "", "fieldName": "given-name", "element": {}},
-      {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name", "element": {}},
-      {"section": "", "addressType": "", "contactType": "", "fieldName": "street-address", "element": {}},
-      {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2", "element": {}},
-      {"section": "", "addressType": "", "contactType": "", "fieldName": "country", "element": {}},
-      {"section": "", "addressType": "", "contactType": "", "fieldName": "email", "element": {}},
-      {"section": "", "addressType": "", "contactType": "", "fieldName": "tel", "element": {}},
-    ],
+    focusedInputId: "given-name",
     profileData: {
       "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",
     },
   },
@@ -68,33 +63,26 @@ const TESTCASES = [
                <input id="street-addr" autocomplete="shipping street-address">
                <input id="city" autocomplete="shipping address-level2">
                <select id="country" autocomplete="shipping country">
                  <option/>
                  <option value="US">United States</option>
                </select>
                <input id='email' autocomplete="shipping email">
                <input id="tel" autocomplete="shipping tel"></form>`,
-    addressFieldDetails: [
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name", "element": {}},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name", "element": {}},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address", "element": {}},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2", "element": {}},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "element": {}},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email", "element": {}},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel", "element": {}},
-    ],
+    focusedInputId: "given-name",
     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",
     },
   },
@@ -102,33 +90,26 @@ 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 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>`,
-    addressFieldDetails: [
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name", "element": {}},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name", "element": {}},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address", "element": {}},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2", "element": {}},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "element": {}},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email", "element": {}},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel", "element": {}},
-    ],
+    focusedInputId: "given-name",
     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": "",
     },
   },
@@ -136,319 +117,414 @@ const TESTCASES = [
     description: "Form with autocomplete properties but mismatched",
     document: `<form><input id="given-name" autocomplete="shipping given-name">
                <input id="family-name" autocomplete="shipping family-name">
                <input id="street-addr" autocomplete="billing street-address">
                <input id="city" autocomplete="billing address-level2">
                <input id="country" autocomplete="billing country">
                <input id='email' autocomplete="shipping email">
                <input id="tel" autocomplete="shipping tel"></form>`,
-    addressFieldDetails: [
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name", "element": {}},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name", "element": {}},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address", "element": {}},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2", "element": {}},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "element": {}},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email", "element": {}},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel", "element": {}},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "organization", "element": null},
-    ],
+    focusedInputId: "given-name",
     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",
     },
   },
   {
     description: "Form with autocomplete select elements and matching option values",
     document: `<form>
+               <input id="given-name" autocomplete="shipping given-name">
                <select id="country" autocomplete="shipping country">
                  <option value=""></option>
                  <option value="US">United States</option>
                </select>
                <select id="state" autocomplete="shipping address-level1">
                  <option value=""></option>
                  <option value="CA">California</option>
                  <option value="WA">Washington</option>
                </select>
                </form>`,
-    addressFieldDetails: [
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "element": {}},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level1", "element": {}},
-    ],
+    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>
+               <input id="given-name" autocomplete="shipping given-name">
                <select id="country" autocomplete="shipping country">
                  <option value=""></option>
                  <option value="US">United States</option>
                </select>
                <select id="state" autocomplete="shipping address-level1">
                  <option value=""></option>
                  <option value="CA">California</option>
                  <option value="WA">Washington</option>
                </select>
                </form>`,
-    addressFieldDetails: [
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "element": {}},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level1", "element": {}},
-    ],
+    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>
+               <input id="given-name" autocomplete="given-name">
+               <input id="family-name" autocomplete="family-name">
+               <input id="street-addr" autocomplete="street-address">
+               <input id="city" autocomplete="address-level2">
+               <select id="country" autocomplete="country">
+                 <option/>
+                 <option value="US">United States</option>
+               </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>`,
+    focusedInputId: "given-name",
+    profileData: {
+      "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": "",
+      "cc-exp-month": "",
+      "cc-exp-year": "",
+    },
+  },
+  {
+    description: "Fill credit card fields in a form with addr and CC fields.",
+    document: `<form>
+               <input id="given-name" autocomplete="given-name">
+               <input id="family-name" autocomplete="family-name">
+               <input id="street-addr" autocomplete="street-address">
+               <input id="city" autocomplete="address-level2">
+               <select id="country" autocomplete="country">
+                 <option/>
+                 <option value="US">United States</option>
+               </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>`,
+    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",
+      "cc-exp-month": "06",
+      "cc-exp-year": "25",
+    },
+  },
+
+
 ];
 
 const TESTCASES_INPUT_UNCHANGED = [
   {
     description: "Form with autocomplete select elements; with default and no matching options",
     document: `<form>
+               <input id="given-name" autocomplete="shipping given-name">
                <select id="country" autocomplete="shipping country">
                  <option value="US">United States</option>
                </select>
                <select id="state" autocomplete="shipping address-level1">
                  <option value=""></option>
                  <option value="CA">California</option>
                  <option value="WA">Washington</option>
                </select>
                </form>`,
-    addressFieldDetails: [
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "element": {}},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level1", "element": {}},
-    ],
+    focusedInputId: "given-name",
     profileData: {
       "guid": "123",
       "country": "US",
       "address-level1": "unknown state",
     },
+    expectedFillingForm: "address",
     expectedResult: {
       "country": "US",
       "state": "",
     },
   },
 ];
 
 const TESTCASES_FILL_SELECT = [
   // US States
   {
     description: "Form with US states select elements",
-    document: `<form><select id="state" autocomplete="shipping address-level1">
+    document: `<form>
+               <input id="given-name" autocomplete="shipping given-name">
+               <input id="family-name" autocomplete="shipping family-name">
+               <select id="state" autocomplete="shipping address-level1">
                  <option value=""></option>
                  <option value="CA">California</option>
                </select></form>`,
-    addressFieldDetails: [
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level1", "element": {}},
-    ],
+    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><select id="state" autocomplete="shipping address-level1">
+    document: `<form>
+               <input id="given-name" autocomplete="shipping given-name">
+               <input id="family-name" autocomplete="shipping family-name">
+               <select id="state" autocomplete="shipping address-level1">
                  <option value=""></option>
                  <option value="ca">ca</option>
                </select></form>`,
-    addressFieldDetails: [
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level1", "element": {}},
-    ],
+    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><select id="state" autocomplete="shipping address-level1">
+    document: `<form>
+               <input id="given-name" autocomplete="shipping given-name">
+               <input id="family-name" autocomplete="shipping family-name">
+               <select id="state" autocomplete="shipping address-level1">
                  <option value=""></option>
                  <option value="CA">CA</option>
                </select></form>`,
-    addressFieldDetails: [
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level1", "element": {}},
-    ],
+    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><select id="state" autocomplete="shipping address-level1">
+    document: `<form>
+               <input id="given-name" autocomplete="shipping given-name">
+               <input id="family-name" autocomplete="shipping family-name">
+               <select id="state" autocomplete="shipping address-level1">
                  <option value=""></option>
                  <option value="US-WA">WA-Washington</option>
                </select></form>`,
-    addressFieldDetails: [
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level1", "element": {}},
-    ],
+    focusedInputId: "given-name",
     profileData: {
       "guid": "123",
       "country": "US",
       "address-level1": "WA",
     },
+    expectedFillingForm: "address",
     expectedResult: {
       "state": "US-WA",
     },
   },
 
   // Country
   {
     description: "Form with country select elements",
-    document: `<form><select id="country" autocomplete="country">
+    document: `<form>
+               <input id="given-name" autocomplete="given-name">
+               <input id="family-name" autocomplete="family-name">
+               <select id="country" autocomplete="country">
                  <option value=""></option>
                  <option value="US">United States</option>
                </select></form>`,
-    addressFieldDetails: [
-      {"section": "", "addressType": "", "contactType": "", "fieldName": "country", "element": {}},
-    ],
+    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><select id="country" autocomplete="country">
+    document: `<form>
+               <input id="given-name" autocomplete="given-name">
+               <input id="family-name" autocomplete="family-name">
+               <select id="country" autocomplete="country">
                  <option value=""></option>
                  <option value="us">us</option>
                </select></form>`,
-    addressFieldDetails: [
-      {"section": "", "addressType": "", "contactType": "", "fieldName": "country", "element": {}},
-    ],
+    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><select id="country" autocomplete="country">
+    document: `<form>
+               <input id="given-name" autocomplete="given-name">
+               <input id="family-name" autocomplete="family-name">
+               <select id="country" autocomplete="country">
                  <option value=""></option>
                  <option value="XX">United States</option>
                </select></form>`,
-    addressFieldDetails: [
-      {"section": "", "addressType": "", "contactType": "", "fieldName": "country", "element": {}},
-    ],
+    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><select id="country" autocomplete="country">
+    document: `<form>
+               <input id="given-name" autocomplete="given-name">
+               <input id="family-name" autocomplete="family-name">
+               <select id="country" autocomplete="country">
                  <option value=""></option>
                  <option value="XX">America</option>
                </select></form>`,
-    addressFieldDetails: [
-      {"section": "", "addressType": "", "contactType": "", "fieldName": "country", "element": {}},
-    ],
+    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><select id="country" autocomplete="country">
+    document: `<form>
+               <input id="given-name" autocomplete="given-name">
+               <input id="family-name" autocomplete="family-name">
+               <select id="country" autocomplete="country">
                  <option value=""></option>
                  <option value="XX">Ship to America</option>
                </select></form>`,
-    addressFieldDetails: [
-      {"section": "", "addressType": "", "contactType": "", "fieldName": "country", "element": {}},
-    ],
+    focusedInputId: "given-name",
     profileData: {
       "guid": "123",
       "country": "US",
     },
+    expectedFillingForm: "address",
     expectedResult: {
       "country": "XX",
     },
   },
 ];
 
 function do_test(testcases, testFn) {
   for (let tc of testcases) {
     (function() {
       let testcase = tc;
       add_task(async function() {
         do_print("Starting testcase: " + testcase.description);
+        let ccNumber = testcase.profileData["cc-number"];
+        if (ccNumber) {
+          testcase.profileData["cc-number-encrypted"] = await MasterPassword.encrypt(ccNumber);
+          delete testcase.profileData["cc-number"];
+        }
 
         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 = [];
 
-        handler.fieldDetails = handler.address.fieldDetails = testcase.addressFieldDetails;
-        handler.address.fieldDetails.forEach((field, index) => {
-          let element = doc.querySelectorAll("input, select")[index];
-          field.elementWeakRef = Cu.getWeakReference(element);
+        handler.collectFormFields();
+        let handlerInfo = handler[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]);
-        handler.autofillFormFields(adaptedProfile);
-        Assert.equal(handler.address.filledRecordGUID, testcase.profileData.guid,
+        let focuedInput = doc.getElementById(testcase.focusedInputId);
+        await handler.autofillFormFields(adaptedProfile, focuedInput);
+        Assert.equal(handlerInfo.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_profileAutocompleteResult.js
+++ b/browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js
@@ -49,26 +49,26 @@ let addressTestCases = [{
   matchingProfiles,
   allFieldNames,
   searchString: "",
   fieldName: "organization",
   expected: {
     searchResult: Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
     defaultIndex: 0,
     items: [{
-      value: "Sesame Street",
+      value: "",
       style: "autofill-profile",
       comment: JSON.stringify(matchingProfiles[0]),
       label: JSON.stringify({
         primary: "Sesame Street",
         secondary: "123 Sesame Street.",
       }),
       image: "",
     }, {
-      value: "Mozilla",
+      value: "",
       style: "autofill-profile",
       comment: JSON.stringify(matchingProfiles[1]),
       label: JSON.stringify({
         primary: "Mozilla",
         secondary: "331 E. Evelyn Avenue",
       }),
       image: "",
     }],
@@ -79,35 +79,35 @@ let addressTestCases = [{
   matchingProfiles,
   allFieldNames,
   searchString: "",
   fieldName: "tel",
   expected: {
     searchResult: Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
     defaultIndex: 0,
     items: [{
-      value: "1-345-345-3456.",
+      value: "",
       style: "autofill-profile",
       comment: JSON.stringify(matchingProfiles[0]),
       label: JSON.stringify({
         primary: "1-345-345-3456.",
         secondary: "123 Sesame Street.",
       }),
       image: "",
     }, {
-      value: "1-650-903-0800",
+      value: "",
       style: "autofill-profile",
       comment: JSON.stringify(matchingProfiles[1]),
       label: JSON.stringify({
         primary: "1-650-903-0800",
         secondary: "331 E. Evelyn Avenue",
       }),
       image: "",
     }, {
-      value: "1-000-000-0000",
+      value: "",
       style: "autofill-profile",
       comment: JSON.stringify(matchingProfiles[2]),
       label: JSON.stringify({
         primary: "1-000-000-0000",
         secondary: "321, No Name St. 2nd line 3rd line",
       }),
       image: "",
     }],
@@ -118,35 +118,35 @@ let addressTestCases = [{
   matchingProfiles,
   allFieldNames,
   searchString: "",
   fieldName: "street-address",
   expected: {
     searchResult: Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
     defaultIndex: 0,
     items: [{
-      value: "123 Sesame Street.",
+      value: "",
       style: "autofill-profile",
       comment: JSON.stringify(matchingProfiles[0]),
       label: JSON.stringify({
         primary: "123 Sesame Street.",
         secondary: "Timothy Berners-Lee",
       }),
       image: "",
     }, {
-      value: "331 E. Evelyn Avenue",
+      value: "",
       style: "autofill-profile",
       comment: JSON.stringify(matchingProfiles[1]),
       label: JSON.stringify({
         primary: "331 E. Evelyn Avenue",
         secondary: "John Doe",
       }),
       image: "",
     }, {
-      value: "321, No Name St. 2nd line 3rd line",
+      value: "",
       style: "autofill-profile",
       comment: JSON.stringify(matchingProfiles[2]),
       label: JSON.stringify({
         primary: "321, No Name St. 2nd line 3rd line",
         secondary: "1-000-000-0000",
       }),
       image: "",
     }],
@@ -157,35 +157,35 @@ let addressTestCases = [{
   matchingProfiles,
   allFieldNames,
   searchString: "",
   fieldName: "address-line1",
   expected: {
     searchResult: Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
     defaultIndex: 0,
     items: [{
-      value: "123 Sesame Street.",
+      value: "",
       style: "autofill-profile",
       comment: JSON.stringify(matchingProfiles[0]),
       label: JSON.stringify({
         primary: "123 Sesame Street.",
         secondary: "Timothy Berners-Lee",
       }),
       image: "",
     }, {
-      value: "331 E. Evelyn Avenue",
+      value: "",
       style: "autofill-profile",
       comment: JSON.stringify(matchingProfiles[1]),
       label: JSON.stringify({
         primary: "331 E. Evelyn Avenue",
         secondary: "John Doe",
       }),
       image: "",
     }, {
-      value: "321, No Name St.",
+      value: "",
       style: "autofill-profile",
       comment: JSON.stringify(matchingProfiles[2]),
       label: JSON.stringify({
         primary: "321, No Name St.",
         secondary: "1-000-000-0000",
       }),
       image: "",
     }],