Bug 1370429 - Part 2: Implement address-lines parser and refactor getInfo function. r=MattN
authorSean Lee <selee@mozilla.com>
Wed, 19 Jul 2017 10:15:34 +0800
changeset 420368 5b3ea49d9f756214e77b363e4970c047e3e01db0
parent 420367 fc7970f8303c61999b153a952a834167db517655
child 420369 bd882520e8a4628f4116d8e74473f73a5569ad3b
push id7566
push usermtabara@mozilla.com
push dateWed, 02 Aug 2017 08:25:16 +0000
treeherdermozilla-beta@86913f512c3c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1370429
milestone56.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1370429 - Part 2: Implement address-lines parser and refactor getInfo function. r=MattN MozReview-Commit-ID: 5gseB36n1M0
browser/extensions/formautofill/FormAutofillHeuristics.jsm
browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js
browser/extensions/formautofill/test/unit/test_getInfo.js
--- a/browser/extensions/formautofill/FormAutofillHeuristics.jsm
+++ b/browser/extensions/formautofill/FormAutofillHeuristics.jsm
@@ -18,36 +18,91 @@ Cu.import("resource://formautofill/FormA
 
 this.log = null;
 FormAutofillUtils.defineLazyLogGetter(this, this.EXPORTED_SYMBOLS[0]);
 
 const PREF_HEURISTICS_ENABLED = "extensions.formautofill.heuristics.enabled";
 
 /**
  * A scanner for traversing all elements in a form and retrieving the field
- * detail with FormAutofillHeuristics.getInfo function.
+ * detail with FormAutofillHeuristics.getInfo function. It also provides a
+ * cursor (parsingIndex) to indicate which element is waiting for parsing.
  */
 class FieldScanner {
   /**
    * Create a FieldScanner based on form elements with the existing
    * fieldDetails.
    *
    * @param {Array.DOMElement} elements
    *        The elements from a form for each parser.
    */
   constructor(elements) {
     this._elementsWeakRef = Cu.getWeakReference(elements);
     this.fieldDetails = [];
+    this._parsingIndex = 0;
   }
 
   get _elements() {
     return this._elementsWeakRef.get();
   }
 
   /**
+   * This cursor means the index of the element which is waiting for parsing.
+   *
+   * @returns {number}
+   *          The index of the element which is waiting for parsing.
+   */
+  get parsingIndex() {
+    return this._parsingIndex;
+  }
+
+  /**
+   * Move the parsingIndex to the next elements. Any elements behind this index
+   * means the parsing tasks are finished.
+   *
+   * @param {number} index
+   *        The latest index of elements waiting for parsing.
+   */
+  set parsingIndex(index) {
+    if (index > this.fieldDetails.length) {
+      throw new Error("The parsing index is out of range.");
+    }
+    this._parsingIndex = index;
+  }
+
+  /**
+   * Retrieve the field detail by the index. If the field detail is not ready,
+   * the elements will be traversed until matching the index.
+   *
+   * @param {number} index
+   *        The index of the element that you want to retrieve.
+   * @returns {Object}
+   *          The field detail at the specific index.
+   */
+  getFieldDetailByIndex(index) {
+    if (index >= this._elements.length) {
+      throw new Error(`The index ${index} is out of range.(${this._elements.length})`);
+    }
+
+    if (index < this.fieldDetails.length) {
+      return this.fieldDetails[index];
+    }
+
+    for (let i = this.fieldDetails.length; i < (index + 1); i++) {
+      this.pushDetail();
+    }
+
+    return this.fieldDetails[index];
+  }
+
+  get parsingFinished() {
+    return this.parsingIndex >= this._elements.length;
+  }
+
+  /**
    * This function will prepare an autocomplete info object with getInfo
    * function and push the detail to fieldDetails property. Any duplicated
    * detail will be marked as _duplicated = true for the parser.
    *
    * Any element without the related detail will be used for adding the detail
    * to the end of field details.
    */
   pushDetail() {
@@ -73,16 +128,38 @@ class FieldScanner {
       // A field with the same identifier already exists.
       log.debug("Not collecting a field matching another with the same info:", info);
       fieldInfo._duplicated = true;
     }
 
     this.fieldDetails.push(fieldInfo);
   }
 
+  /**
+   * When a field detail should be changed its fieldName after parsing, use
+   * this function to update the fieldName which is at a specific index.
+   *
+   * @param {number} index
+   *        The index indicates a field detail to be updated.
+   * @param {string} fieldName
+   *        The new fieldName
+   */
+  updateFieldName(index, fieldName) {
+    if (index >= this.fieldDetails.length) {
+      throw new Error("Try to update the non-existing field detail.");
+    }
+    this.fieldDetails[index].fieldName = fieldName;
+
+    delete this.fieldDetails[index]._duplicated;
+    let indexSame = this.findSameField(this.fieldDetails[index]);
+    if (indexSame != index && indexSame != -1) {
+      this.fieldDetails[index]._duplicated = true;
+    }
+  }
+
   findSameField(info) {
     return this.fieldDetails.findIndex(f => f.section == info.section &&
                                        f.addressType == info.addressType &&
                                        f.contactType == info.contactType &&
                                        f.fieldName == info.fieldName);
   }
 
   /**
@@ -120,81 +197,102 @@ this.FormAutofillHeuristics = {
       "country",
     ],
     TEL: ["tel"],
     EMAIL: ["email"],
   },
 
   RULES: null,
 
+  /**
+   * Try to find the correct address-line[1-3] sequence and correct their field
+   * names.
+   *
+   * @param {FieldScanner} fieldScanner
+   *        The current parsing status for all elements
+   * @returns {boolean}
+   *          Return true if there is any field can be recognized in the parser,
+   *          otherwise false.
+   */
+  _parseAddressFields(fieldScanner) {
+    let parsedFields = false;
+    let addressLines = ["address-line1", "address-line2", "address-line3"];
+    for (let i = 0; !fieldScanner.parsingFinished && i < addressLines.length; i++) {
+      let detail = fieldScanner.getFieldDetailByIndex(fieldScanner.parsingIndex);
+      if (!detail || !addressLines.includes(detail.fieldName)) {
+        // When the field is not related to any address-line[1-3] fields, it
+        // means the parsing process can be terminated.
+        break;
+      }
+      fieldScanner.updateFieldName(fieldScanner.parsingIndex, addressLines[i]);
+      fieldScanner.parsingIndex++;
+      parsedFields = true;
+    }
+
+    return parsedFields;
+  },
+
   getFormInfo(form) {
-    if (form.autocomplete == "off") {
+    if (form.autocomplete == "off" || form.elements.length <= 0) {
       return [];
     }
+
     let fieldScanner = new FieldScanner(form.elements);
-    for (let i = 0; i < fieldScanner.elements.length; i++) {
-      let element = fieldScanner.elements[i];
-      let info = this.getInfo(element, fieldScanner.fieldDetails);
-      fieldScanner.pushDetail(i, info);
+    while (!fieldScanner.parsingFinished) {
+      let parsedAddressFields = this._parseAddressFields(fieldScanner);
+
+      // If there is no any field parsed, the parsing cursor can be moved
+      // forward to the next one.
+      if (!parsedAddressFields) {
+        fieldScanner.parsingIndex++;
+      }
     }
     return fieldScanner.trimmedFieldDetail;
   },
 
   /**
    * Get the autocomplete info (e.g. fieldName) determined by the regexp
-   * (this.RULES) matching to a feature string. The result is based on the
-   * existing field names to prevent duplicating predictions
-   * (e.g. address-line[1-3).
+   * (this.RULES) matching to a feature string.
    *
    * @param {string} string a feature string to be determined.
-   * @param {Array<string>} existingFieldNames the array of exising field names
-   *                        in a form.
    * @returns {Object}
    *          Provide the predicting result including the field name.
    *
    */
-  _matchStringToFieldName(string, existingFieldNames) {
+  _matchStringToFieldName(string) {
     let result = {
       fieldName: "",
       section: "",
       addressType: "",
       contactType: "",
     };
     if (this.RULES.email.test(string)) {
       result.fieldName = "email";
       return result;
     }
     if (this.RULES.tel.test(string)) {
       result.fieldName = "tel";
       return result;
     }
     for (let fieldName of this.FIELD_GROUPS.ADDRESS) {
       if (this.RULES[fieldName].test(string)) {
-        // If "address-line1" or "address-line2" exist already, the string
-        // could be satisfied with "address-line2" or "address-line3".
-        if ((fieldName == "address-line1" &&
-            existingFieldNames.includes("address-line1")) ||
-            (fieldName == "address-line2" &&
-            existingFieldNames.includes("address-line2"))) {
-          continue;
-        }
         result.fieldName = fieldName;
         return result;
       }
     }
     for (let fieldName of this.FIELD_GROUPS.NAME) {
       if (this.RULES[fieldName].test(string)) {
         result.fieldName = fieldName;
         return result;
       }
     }
     return null;
   },
 
-  getInfo(element, fieldDetails) {
+  getInfo(element) {
     if (!FormAutofillUtils.isFieldEligibleForAutofill(element)) {
       return null;
     }
 
     let info = element.getAutocompleteInfo();
     // An input[autocomplete="on"] will not be early return here since it stll
     // needs to find the field name.
     if (info && info.fieldName && info.fieldName != "on") {
@@ -213,35 +311,31 @@ this.FormAutofillHeuristics = {
       return {
         fieldName: "email",
         section: "",
         addressType: "",
         contactType: "",
       };
     }
 
-    let existingFieldNames = fieldDetails ? fieldDetails.map(i => i.fieldName) : "";
-
     for (let elementString of [element.id, element.name]) {
-      let fieldNameResult = this._matchStringToFieldName(elementString,
-                                                         existingFieldNames);
+      let fieldNameResult = this._matchStringToFieldName(elementString);
       if (fieldNameResult) {
         return fieldNameResult;
       }
     }
     let labels = FormAutofillUtils.findLabelElements(element);
     if (!labels || labels.length == 0) {
       log.debug("No label found for", element);
       return null;
     }
     for (let label of labels) {
       let strings = FormAutofillUtils.extractLabelStrings(label);
       for (let string of strings) {
-        let fieldNameResult = this._matchStringToFieldName(string,
-                                                           existingFieldNames);
+        let fieldNameResult = this._matchStringToFieldName(string);
         if (fieldNameResult) {
           return fieldNameResult;
         }
       }
     }
 
     return null;
   },
--- a/browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js
+++ b/browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js
@@ -88,18 +88,18 @@ const TESTCASES = [
                <input id="line1" autocomplete="address-line1">
                <input id="line3" autocomplete="address-line3">
                </form>`,
     profileData: [Object.assign({}, DEFAULT_PROFILE)],
     expectedResult: [{
       "guid": "123",
       "street-address": "2 Harrison St line2 line3",
       "-moz-street-address-one-line": "2 Harrison St line2 line3",
-      "address-line1": "2 Harrison St line2",
-      "address-line2": "line2",
+      "address-line1": "2 Harrison St",
+      "address-line2": "line2 line3",
       "address-line3": "line3",
     }],
   },
 ];
 
 for (let testcase of TESTCASES) {
   add_task(async function() {
     do_print("Starting testcase: " + testcase.description);
--- a/browser/extensions/formautofill/test/unit/test_getInfo.js
+++ b/browser/extensions/formautofill/test/unit/test_getInfo.js
@@ -91,39 +91,22 @@ const TESTCASES = [
     expectedReturnValue: {
       fieldName: "address-level1",
       section: "",
       addressType: "",
       contactType: "",
     },
   },
   {
-    description: "2 address line inputs",
+    description: "address line input",
     document: `<label for="targetElement">street</label>
                <input id="targetElement" type="text">`,
     elementId: "targetElement",
-    addressFieldDetails: [{fieldName: "address-line1"}],
     expectedReturnValue: {
-      fieldName: "address-line2",
-      section: "",
-      addressType: "",
-      contactType: "",
-    },
-  },
-  {
-    description: "3 address line inputs",
-    document: `<label for="targetElement">street</label>
-               <input id="targetElement" type="text">`,
-    elementId: "targetElement",
-    addressFieldDetails: [
-      {fieldName: "address-line1"},
-      {fieldName: "address-line2"},
-    ],
-    expectedReturnValue: {
-      fieldName: "address-line3",
+      fieldName: "address-line1",
       section: "",
       addressType: "",
       contactType: "",
     },
   },
   {
     description: "CJK character - Traditional Chinese",
     document: `<label> 郵遞區號
@@ -213,13 +196,13 @@ const TESTCASES = [
 TESTCASES.forEach(testcase => {
   add_task(async function() {
     do_print("Starting testcase: " + testcase.description);
 
     let doc = MockDocument.createTestDocument(
       "http://localhost:8080/test/", testcase.document);
 
     let element = doc.getElementById(testcase.elementId);
-    let value = FormAutofillHeuristics.getInfo(element, testcase.addressFieldDetails);
+    let value = FormAutofillHeuristics.getInfo(element);
 
     Assert.deepEqual(value, testcase.expectedReturnValue);
   });
 });