Bug 1339721 - Use FormAutofillHandler.autofillFormFields to fill in fields.; r?MattN draft
authorSean Lee <selee@mozilla.com>
Wed, 15 Feb 2017 16:27:45 +0800
changeset 490650 8d719c7ee3ec0fa0495c5c501c987876c86cfe04
parent 489583 f36062d04d165f6f6e781cf0633ffcbbebe6c273
child 547333 754b428858eedea4c393b7b0384800d9a7aac19b
push id47179
push userbmo:selee@mozilla.com
push dateWed, 01 Mar 2017 02:25:24 +0000
reviewersMattN
bugs1339721
milestone54.0a1
Bug 1339721 - Use FormAutofillHandler.autofillFormFields to fill in fields.; r?MattN MozReview-Commit-ID: KgphpgQ4FNp
browser/extensions/formautofill/FormAutofillContent.jsm
browser/extensions/formautofill/FormAutofillHandler.jsm
browser/extensions/formautofill/test/unit/test_autofillFormFields.js
--- a/browser/extensions/formautofill/FormAutofillContent.jsm
+++ b/browser/extensions/formautofill/FormAutofillContent.jsm
@@ -228,30 +228,19 @@ let ProfileAutocomplete = {
 
     if (selectedIndex == -1 ||
         !this._lastAutoCompleteResult ||
         this._lastAutoCompleteResult.getStyleAt(selectedIndex) != "autofill-profile") {
       return;
     }
 
     let profile = JSON.parse(this._lastAutoCompleteResult.getCommentAt(selectedIndex));
+    let formHandler = FormAutofillContent.getFormHandler(focusedInput);
 
-    // TODO: FormAutofillHandler.autofillFormFields will be used for filling
-    // fields logic eventually.
-    for (let inputInfo of formDetails) {
-      // Skip filling the value of focused input which is filled in
-      // FormFillController.
-      if (inputInfo.element === focusedInput) {
-        continue;
-      }
-      let value = profile[inputInfo.fieldName];
-      if (value) {
-        inputInfo.element.setUserInput(value);
-      }
-    }
+    formHandler.autofillFormFields(profile, focusedInput);
   },
 };
 
 /**
  * Handles content's interactions for the process.
  *
  * NOTE: Declares it by "var" to make it accessible in unit tests.
  */
@@ -291,28 +280,41 @@ var FormAutofillContent = {
       if (element == detail.element) {
         return detail;
       }
     }
     return null;
   },
 
   /**
+   * Get the form's handler from cache which is created after page identified.
+   *
+   * @param {HTMLInputElement} element Focused input which triggered profile searching
+   * @returns {Array<Object>|null}
+   *          Return target form's handler from content cache
+   *          (or return null if the information is not found in the cache).
+   *
+   */
+  getFormHandler(element) {
+    let rootElement = FormLikeFactory.findRootForField(element);
+    return this._formsDetails.get(rootElement);
+  },
+
+  /**
    * Get the form's information from cache which is created after page identified.
    *
    * @param {HTMLInputElement} element Focused input which triggered profile searching
    * @returns {Array<Object>|null}
-   *          Return target form's information that cloned from content cache
+   *          Return target form's information from content cache
    *          (or return null if the information is not found in the cache).
    *
    */
   getFormDetails(element) {
-    let rootElement = FormLikeFactory.findRootForField(element);
-    let formDetails = this._formsDetails.get(rootElement);
-    return formDetails ? formDetails.fieldDetails : null;
+    let formHandler = this.getFormHandler(element);
+    return formHandler ? formHandler.fieldDetails : null;
   },
 
   getAllFieldNames(element) {
     let formDetails = this.getFormDetails(element);
     return formDetails.map(record => record.fieldName);
   },
 
   identifyAutofillFields(doc) {
--- a/browser/extensions/formautofill/FormAutofillHandler.jsm
+++ b/browser/extensions/formautofill/FormAutofillHandler.jsm
@@ -98,50 +98,35 @@ FormAutofillHandler.prototype = {
 
     log.debug("Collected details on", autofillData.length, "fields");
 
     return autofillData;
   },
 
   /**
    * Processes form fields that can be autofilled, and populates them with the
-   * data provided by backend.
+   * profile provided by backend.
    *
-   * @param {Array<Object>} autofillResult
-   *        Data returned by the user interface.
-   *        [{
-   *          section: Value originally provided to the user interface.
-   *          addressType: Value originally provided to the user interface.
-   *          contactType: Value originally provided to the user interface.
-   *          fieldName: Value originally provided to the user interface.
-   *          value: String with which the field should be updated.
-   *          index: Index to match the input in fieldDetails
-   *        }],
-   *        }
+   * @param {Object} profile
+   *        A profile to be filled in.
+   * @param {Object} focusedInput
+   *        A focused input element which is skipped for filling.
    */
-  autofillFormFields(autofillResult) {
-    log.debug("autofillFormFields:", autofillResult);
-    for (let field of autofillResult) {
-      // TODO: Skip filling the value of focused input which is filled in
-      // FormFillController.
+  autofillFormFields(profile, focusedInput) {
+    log.debug("profile in autofillFormFields:", profile);
+    for (let fieldDetail of this.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
 
-      // Get the field details, if it was processed by the user interface.
-      let fieldDetail = this.fieldDetails[field.index];
-
-      // Avoid the invalid value set
-      if (!fieldDetail || !field.value) {
+      if (fieldDetail.element === focusedInput ||
+          fieldDetail.element.value) {
         continue;
       }
 
-      let info = FormAutofillHeuristics.getInfo(fieldDetail.element);
-      if (!info ||
-          field.section != info.section ||
-          field.addressType != info.addressType ||
-          field.contactType != info.contactType ||
-          field.fieldName != info.fieldName) {
-        Cu.reportError("Autocomplete tokens mismatched");
-        continue;
+      let value = profile[fieldDetail.fieldName];
+      if (value) {
+        fieldDetail.element.setUserInput(value);
       }
-
-      fieldDetail.element.setUserInput(field.value);
     }
   },
 };
--- a/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
@@ -8,17 +8,17 @@ Cu.import("resource://formautofill/FormA
 
 const TESTCASES = [
   {
     description: "Form without autocomplete property",
     document: `<form><input id="given-name"><input id="family-name">
                <input id="street-addr"><input id="city"><input id="country">
                <input id='email'><input id="tel"></form>`,
     fieldDetails: [],
-    profileData: [],
+    profileData: {},
     expectedResult: {
       "street-addr": "",
       "city": "",
       "country": "",
       "email": "",
       "tel": "",
     },
   },
@@ -35,25 +35,23 @@ const TESTCASES = [
       {"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": {}},
     ],
-    profileData: [
-      {"section": "", "addressType": "", "fieldName": "given-name", "contactType": "", "index": 0, "value": "foo"},
-      {"section": "", "addressType": "", "fieldName": "family-name", "contactType": "", "index": 1, "value": "bar"},
-      {"section": "", "addressType": "", "fieldName": "street-address", "contactType": "", "index": 2, "value": "2 Harrison St"},
-      {"section": "", "addressType": "", "fieldName": "address-level2", "contactType": "", "index": 3, "value": "San Francisco"},
-      {"section": "", "addressType": "", "fieldName": "country", "contactType": "", "index": 4, "value": "US"},
-      {"section": "", "addressType": "", "fieldName": "email", "contactType": "", "index": 5, "value": "foo@mozilla.com"},
-      {"section": "", "addressType": "", "fieldName": "tel", "contactType": "", "index": 6, "value": "1234567"},
-    ],
+    profileData: {
+      "street-address": "2 Harrison St",
+      "address-level2": "San Francisco",
+      "country": "US",
+      "email": "foo@mozilla.com",
+      "tel": "1234567",
+    },
     expectedResult: {
       "street-addr": "2 Harrison St",
       "city": "San Francisco",
       "country": "US",
       "email": "foo@mozilla.com",
       "tel": "1234567",
     },
   },
@@ -70,25 +68,23 @@ const TESTCASES = [
       {"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": {}},
     ],
-    profileData: [
-      {"section": "", "addressType": "shipping", "fieldName": "given-name", "contactType": "", "index": 0, "value": "foo"},
-      {"section": "", "addressType": "shipping", "fieldName": "family-name", "contactType": "", "index": 1, "value": "bar"},
-      {"section": "", "addressType": "shipping", "fieldName": "street-address", "contactType": "", "index": 2, "value": "2 Harrison St"},
-      {"section": "", "addressType": "shipping", "fieldName": "address-level2", "contactType": "", "index": 3, "value": "San Francisco"},
-      {"section": "", "addressType": "shipping", "fieldName": "country", "contactType": "", "index": 4, "value": "US"},
-      {"section": "", "addressType": "shipping", "fieldName": "email", "contactType": "", "index": 5, "value": "foo@mozilla.com"},
-      {"section": "", "addressType": "shipping", "fieldName": "tel", "contactType": "", "index": 6, "value": "1234567"},
-    ],
+    profileData: {
+      "street-address": "2 Harrison St",
+      "address-level2": "San Francisco",
+      "country": "US",
+      "email": "foo@mozilla.com",
+      "tel": "1234567",
+    },
     expectedResult: {
       "street-addr": "2 Harrison St",
       "city": "San Francisco",
       "country": "US",
       "email": "foo@mozilla.com",
       "tel": "1234567",
     },
   },
@@ -105,25 +101,23 @@ const TESTCASES = [
       {"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": {}},
     ],
-    profileData: [
-      {"section": "", "addressType": "shipping", "fieldName": "given-name", "contactType": "", "index": 0, "value": "foo"},
-      {"section": "", "addressType": "shipping", "fieldName": "family-name", "contactType": "", "index": 1, "value": "bar"},
-      {"section": "", "addressType": "shipping", "fieldName": "street-address", "contactType": "", "index": 2, "value": "2 Harrison St"},
-      {"section": "", "addressType": "shipping", "fieldName": "address-level2", "contactType": "", "index": 3, "value": "San Francisco"},
-      {"section": "", "addressType": "shipping", "fieldName": "country", "contactType": "", "index": 4, "value": "US"},
-      {"section": "", "addressType": "shipping", "fieldName": "email", "contactType": "", "index": 5},
-      {"section": "", "addressType": "shipping", "fieldName": "tel", "contactType": "", "index": 6},
-    ],
+    profileData: {
+      "street-address": "2 Harrison St",
+      "address-level2": "San Francisco",
+      "country": "US",
+      "email": "",
+      "tel": "",
+    },
     expectedResult: {
       "street-addr": "2 Harrison St",
       "city": "San Francisco",
       "country": "US",
       "email": "",
       "tel": "",
     },
   },
@@ -140,25 +134,23 @@ const TESTCASES = [
       {"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": {}},
     ],
-    profileData: [
-      {"section": "", "addressType": "shipping", "fieldName": "given-name", "contactType": "", "index": 0, "value": "foo"},
-      {"section": "", "addressType": "shipping", "fieldName": "family-name", "contactType": "", "index": 1, "value": "bar"},
-      {"section": "", "addressType": "shipping", "fieldName": "street-address", "contactType": "", "index": 2, "value": "2 Harrison St"},
-      {"section": "", "addressType": "shipping", "fieldName": "address-level2", "contactType": "", "index": 3, "value": "San Francisco"},
-      {"section": "", "addressType": "shipping", "fieldName": "country", "contactType": "", "index": 4, "value": "US"},
-      {"section": "", "addressType": "shipping", "fieldName": "email", "contactType": "", "index": 5, "value": "foo@mozilla.com"},
-      {"section": "", "addressType": "shipping", "fieldName": "tel", "contactType": "", "index": 6, "value": "1234567"},
-    ],
+    profileData: {
+      "street-address": "",
+      "address-level2": "",
+      "country": "",
+      "email": "foo@mozilla.com",
+      "tel": "1234567",
+    },
     expectedResult: {
       "street-addr": "",
       "city": "",
       "country": "",
       "email": "foo@mozilla.com",
       "tel": "1234567",
     },
   },
@@ -169,22 +161,34 @@ for (let tc of TESTCASES) {
     let testcase = tc;
     add_task(function* () {
       do_print("Starting testcase: " + testcase.description);
 
       let doc = MockDocument.createTestDocument("http://localhost:8080/test/",
                                                 testcase.document);
       let form = doc.querySelector("form");
       let handler = new FormAutofillHandler(form);
+      let onChangePromises = [];
 
       handler.fieldDetails = testcase.fieldDetails;
       handler.fieldDetails.forEach((field, index) => {
-        field.element = doc.querySelectorAll("input")[index];
+        let element = doc.querySelectorAll("input")[index];
+        field.element = element;
+        if (!testcase.profileData[field.fieldName]) {
+          // Avoid waiting for `change` event of a input with a blank value to
+          // be filled.
+          return;
+        }
+        onChangePromises.push(new Promise(resolve => {
+          element.addEventListener("change", () => {
+            let id = element.id;
+            Assert.equal(element.value, testcase.expectedResult[id],
+                        "Check the " + id + " fields were filled with correct data");
+            resolve();
+          }, {once: true});
+        }));
       });
 
       handler.autofillFormFields(testcase.profileData);
-      for (let id in testcase.expectedResult) {
-        Assert.equal(doc.getElementById(id).value, testcase.expectedResult[id],
-                    "Check the " + id + " fields were filled with correct data");
-      }
+      yield Promise.all(onChangePromises);
     });
   })();
 }