Bug 1416664 - Identify the sections for the fields without the section part of autocomplete attr. r=lchang,ralin draft
authorSean Lee <selee@mozilla.com>
Wed, 22 Nov 2017 15:57:33 +0800
changeset 706059 e203198cd62ee497c6d94f49340be45ae4760b79
parent 706055 0c01145948e014320458c8c5b7a8943f0410ad2d
child 706060 08b4a04bfa04b4be9782f79ebf2e34ce1c276199
push id91679
push userbmo:selee@mozilla.com
push dateFri, 01 Dec 2017 03:52:21 +0000
reviewerslchang, ralin
bugs1416664
milestone59.0a1
Bug 1416664 - Identify the sections for the fields without the section part of autocomplete attr. r=lchang,ralin MozReview-Commit-ID: 7La8Bn0TF1y
browser/extensions/formautofill/FormAutofillHeuristics.jsm
browser/extensions/formautofill/test/fixtures/multiple_section.html
browser/extensions/formautofill/test/unit/heuristics/test_multiple_section.js
--- a/browser/extensions/formautofill/FormAutofillHeuristics.jsm
+++ b/browser/extensions/formautofill/FormAutofillHeuristics.jsm
@@ -16,35 +16,38 @@ Cu.import("resource://gre/modules/Servic
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://formautofill/FormAutofillUtils.jsm");
 
 this.log = null;
 FormAutofillUtils.defineLazyLogGetter(this, this.EXPORTED_SYMBOLS[0]);
 
 const PREF_HEURISTICS_ENABLED = "extensions.formautofill.heuristics.enabled";
 const PREF_SECTION_ENABLED = "extensions.formautofill.section.enabled";
+const DEFAULT_SECTION_NAME = "-moz-section-default";
 
 /**
  * A scanner for traversing all elements in a form and retrieving the field
  * detail with FormAutofillHeuristics.getInfo function. It also provides a
  * cursor (parsingIndex) to indicate which element is waiting for parsing.
  */
 class FieldScanner {
   /**
    * 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) {
+  constructor(elements, {allowDuplicates = false, sectionEnabled = true}) {
     this._elementsWeakRef = Cu.getWeakReference(elements);
     this.fieldDetails = [];
     this._parsingIndex = 0;
     this._sections = [];
+    this._allowDuplicates = allowDuplicates;
+    this._sectionEnabled = sectionEnabled;
   }
 
   get _elements() {
     return this._elementsWeakRef.get();
   }
 
   /**
    * This cursor means the index of the element which is waiting for parsing.
@@ -107,32 +110,73 @@ class FieldScanner {
       }
     }
     this._sections.push({
       name,
       fieldDetails: [fieldDetail],
     });
   }
 
-  getSectionFieldDetails(allowDuplicates) {
-    // TODO: [Bug 1416664] If there is only one section which is not defined by
-    // `autocomplete` attribute, the sections should be classified by the
-    // heuristics.
-    return this._sections.map(section => {
-      if (allowDuplicates) {
-        return section.fieldDetails;
+  _classifySections() {
+    let fieldDetails = this._sections[0].fieldDetails;
+    this._sections = [];
+    let seenTypes = new Set();
+    let previousType;
+    let sectionCount = 0;
+
+    for (let fieldDetail of fieldDetails) {
+      if (!fieldDetail.fieldName) {
+        continue;
+      }
+      if (seenTypes.has(fieldDetail.fieldName) &&
+          previousType != fieldDetail.fieldName) {
+        seenTypes.clear();
+        sectionCount++;
       }
-      return this._trimFieldDetails(section.fieldDetails);
-    });
+      previousType = fieldDetail.fieldName;
+      seenTypes.add(fieldDetail.fieldName);
+      delete fieldDetail._duplicated;
+      this._pushToSection(DEFAULT_SECTION_NAME + "-" + sectionCount, fieldDetail);
+    }
+  }
+
+  /**
+   * The result is an array contains the sections with its belonging field
+   * details. If `this._sections` contains one section only with the default
+   * section name (DEFAULT_SECTION_NAME), `this._classifySections` should be
+   * able to identify all sections in the heuristic way.
+   *
+   * @returns {Array<Object>}
+   *          The array with the sections, and the belonging fieldDetails are in
+   *          each section.
+   */
+  getSectionFieldDetails() {
+    // When the section feature is disabled, `getSectionFieldDetails` should
+    // provide a single section result.
+    if (!this._sectionEnabled) {
+      return [this._getFinalDetails(this.fieldDetails)];
+    }
+    if (this._sections.length == 0) {
+      return [];
+    }
+    if (this._sections.length == 1 && this._sections[0].name == DEFAULT_SECTION_NAME) {
+      this._classifySections();
+    }
+
+    return this._sections.map(section =>
+      this._getFinalDetails(section.fieldDetails)
+    );
   }
 
   /**
    * 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 field will be pushed into `this._sections` based on the section name
+   * in `autocomplete` attribute.
    *
    * Any element without the related detail will be used for adding the detail
    * to the end of field details.
    */
   pushDetail() {
     let elementIndex = this.fieldDetails.length;
     if (elementIndex >= this._elements.length) {
       throw new Error("Try to push the non-existing element info.");
@@ -168,17 +212,17 @@ class FieldScanner {
   _getSectionName(info) {
     let names = [];
     if (info.section) {
       names.push(info.section);
     }
     if (info.addressType) {
       names.push(info.addressType);
     }
-    return names.length ? names.join(" ") : "-moz-section-default";
+    return names.length ? names.join(" ") : DEFAULT_SECTION_NAME;
   }
 
   /**
    * 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.
@@ -200,32 +244,34 @@ class FieldScanner {
 
   findSameField(info) {
     return this.fieldDetails.findIndex(f => f.section == info.section &&
                                        f.addressType == info.addressType &&
                                        f.fieldName == info.fieldName);
   }
 
   /**
-   * Provide the field details without invalid field name and duplicated fields.
+   * Provide the final field details without invalid field name, and the
+   * duplicated fields will be removed as well. For the debugging purpose,
+   * the final `fieldDetails` will include the duplicated fields if
+   * `_allowDuplicates` is true.
    *
    * @param   {Array<Object>} fieldDetails
    *          The field details for trimming.
    * @returns {Array<Object>}
    *          The array with the field details without invalid field name and
    *          duplicated fields.
    */
-  _trimFieldDetails(fieldDetails) {
+  _getFinalDetails(fieldDetails) {
+    if (this._allowDuplicates) {
+      return fieldDetails.filter(f => f.fieldName);
+    }
     return fieldDetails.filter(f => f.fieldName && !f._duplicated);
   }
 
-  getFieldDetails(allowDuplicates) {
-    return allowDuplicates ? this.fieldDetails : this._trimFieldDetails(this.fieldDetails);
-  }
-
   elementExisting(index) {
     return index < this._elements.length;
   }
 }
 
 this.LabelUtils = {
   // The tag name list is from Chromium except for "STYLE":
   // eslint-disable-next-line max-len
@@ -646,38 +692,33 @@ this.FormAutofillHeuristics = {
   getFormInfo(form, allowDuplicates = false) {
     const eligibleFields = Array.from(form.elements)
       .filter(elem => FormAutofillUtils.isFieldEligibleForAutofill(elem));
 
     if (eligibleFields.length <= 0) {
       return [];
     }
 
-    let fieldScanner = new FieldScanner(eligibleFields);
+    let fieldScanner = new FieldScanner(eligibleFields,
+      {allowDuplicates, sectionEnabled: this._sectionEnabled});
     while (!fieldScanner.parsingFinished) {
       let parsedPhoneFields = this._parsePhoneFields(fieldScanner);
       let parsedAddressFields = this._parseAddressFields(fieldScanner);
       let parsedExpirationDateFields = this._parseCreditCardExpirationDateFields(fieldScanner);
 
       // If there is no any field parsed, the parsing cursor can be moved
       // forward to the next one.
       if (!parsedPhoneFields && !parsedAddressFields && !parsedExpirationDateFields) {
         fieldScanner.parsingIndex++;
       }
     }
 
     LabelUtils.clearLabelMap();
 
-    if (!this._sectionEnabled) {
-      // When the section feature is disabled, `getFormInfo` should provide a
-      // single section result.
-      return [fieldScanner.getFieldDetails(allowDuplicates)];
-    }
-
-    return fieldScanner.getSectionFieldDetails(allowDuplicates);
+    return fieldScanner.getSectionFieldDetails();
   },
 
   _regExpTableHashValue(...signBits) {
     return signBits.reduce((p, c, i) => p | !!c << i, 0);
   },
 
   _setRegExpListCache(regexps, b0, b1, b2) {
     if (!this._regexpList) {
--- a/browser/extensions/formautofill/test/fixtures/multiple_section.html
+++ b/browser/extensions/formautofill/test/fixtures/multiple_section.html
@@ -6,42 +6,79 @@
 </head>
 <body>
   <h1>Form Autofill Demo Page</h1>
   <form>
     <label>Name: <input id="name" autocomplete="name"></label><br/>
     <label>Organization: <input id="organization" autocomplete="organization"></label><br/>
 
     <br/>
-    <label>Street Address: <input id="street-address" autocomplete="shipping street-address"></label><br/>
-    <label>Address Level 2: <input id="address-level2" autocomplete="shipping address-level2"></label><br/>
-    <label>Address Level 1: <input id="address-level1" autocomplete="shipping address-level1"></label><br/>
-    <label>Postal Code: <input id="postal-code" autocomplete="shipping postal-code"></label><br/>
-    <label>Country: <input id="country" autocomplete="shipping country"></label><br/>
+    <label>Street Address: <input id="street-address-a" autocomplete="shipping street-address"></label><br/>
+    <label>Address Level 2: <input id="address-level2-a" autocomplete="shipping address-level2"></label><br/>
+    <label>Address Level 1: <input id="address-level1-a" autocomplete="shipping address-level1"></label><br/>
+    <label>Postal Code: <input id="postal-code-a" autocomplete="shipping postal-code"></label><br/>
+    <label>Country: <input id="country-a" autocomplete="shipping country"></label><br/>
+
+    <br/>
+    <label>Street Address: <input id="street-address-b" autocomplete="billing street-address"></label><br/>
+    <label>Address Level 2: <input id="address-level2-b" autocomplete="billing address-level2"></label><br/>
+    <label>Address Level 1: <input id="address-level1-b" autocomplete="billing address-level1"></label><br/>
+    <label>Postal Code: <input id="postal-code-b" autocomplete="billing postal-code"></label><br/>
+    <label>Country: <input id="country-b" autocomplete="billing country"></label><br/>
+
+    <br/>
+    <label>Street Address: <input id="street-address-c" autocomplete="section-my street-address"></label><br/>
+    <label>Address Level 2: <input id="address-level2-c" autocomplete="section-my address-level2"></label><br/>
+    <label>Address Level 1: <input id="address-level1-c" autocomplete="section-my address-level1"></label><br/>
+    <label>Postal Code: <input id="postal-code-c" autocomplete="section-my postal-code"></label><br/>
+    <label>Country: <input id="country-c" autocomplete="section-my country"></label><br/>
 
     <br/>
-    <label>Street Address: <input id="street-address" autocomplete="billing street-address"></label><br/>
-    <label>Address Level 2: <input id="address-level2" autocomplete="billing address-level2"></label><br/>
-    <label>Address Level 1: <input id="address-level1" autocomplete="billing address-level1"></label><br/>
-    <label>Postal Code: <input id="postal-code" autocomplete="billing postal-code"></label><br/>
-    <label>Country: <input id="country" autocomplete="billing country"></label><br/>
+    <label>Telephone: <input id="tel-a" autocomplete="work tel"></label><br/>
+    <label>Email: <input id="email-a" autocomplete="work email"></label><br/>
+    <br/>
+    <label>Telephone: <input id="tel-b" autocomplete="home tel"></label><br/>
+    <label>Email: <input id="email-b" autocomplete="home email"></label><br/>
+    <p>
+      <input type="submit" value="Submit">
+      <button type="reset">Reset</button>
+    </p>
+  </form>
+
+  <form>
+    <label>Name: <input autocomplete="name"></label><br/>
+    <label>Organization: <input autocomplete="organization"></label><br/>
 
     <br/>
-    <label>Street Address: <input id="street-address" autocomplete="section-my street-address"></label><br/>
-    <label>Address Level 2: <input id="address-level2" autocomplete="section-my address-level2"></label><br/>
-    <label>Address Level 1: <input id="address-level1" autocomplete="section-my address-level1"></label><br/>
-    <label>Postal Code: <input id="postal-code" autocomplete="section-my postal-code"></label><br/>
-    <label>Country: <input id="country" autocomplete="section-my country"></label><br/>
+    <label>Street Address: <input autocomplete="street-address"></label><br/>
+    <label>Address Level 2: <input autocomplete="address-level2"></label><br/>
+    <label>Address Level 1: <input autocomplete="address-level1"></label><br/>
+    <label>Postal Code: <input autocomplete="postal-code"></label><br/>
+    <label>Country: <input autocomplete="country"></label><br/>
 
     <br/>
-    <label>Telephone: <input id="tel" autocomplete="work tel"></label><br/>
-    <label>Email: <input id="email" autocomplete="work email"></label><br/>
+    <label>Street Address: <input autocomplete="street-address"></label><br/>
+    <label>Address Level 2: <input autocomplete="address-level2"></label><br/>
+    <label>Address Level 1: <input autocomplete="address-level1"></label><br/>
+    <label>Postal Code: <input autocomplete="postal-code"></label><br/>
+    <label>Country: <input autocomplete="country"></label><br/>
+
     <br/>
-    <label>Telephone: <input id="tel" autocomplete="home tel"></label><br/>
-    <label>Email: <input id="email" autocomplete="home email"></label><br/>
+    <label>Street Address: <input autocomplete="street-address"></label><br/>
+    <label>Address Level 2: <input autocomplete="address-level2"></label><br/>
+    <label>Address Level 1: <input autocomplete="address-level1"></label><br/>
+    <label>Postal Code: <input autocomplete="postal-code"></label><br/>
+    <label>Country: <input autocomplete="country"></label><br/>
+
+    <br/>
+    <label>Telephone: <input autocomplete="work tel"></label><br/>
+    <label>Email: <input autocomplete="work email"></label><br/>
+    <br/>
+    <label>Telephone: <input autocomplete="home tel"></label><br/>
+    <label>Email: <input autocomplete="home email"></label><br/>
     <p>
       <input type="submit" value="Submit">
       <button type="reset">Reset</button>
     </p>
   </form>
 
 </body>
 </html>
--- a/browser/extensions/formautofill/test/unit/heuristics/test_multiple_section.js
+++ b/browser/extensions/formautofill/test/unit/heuristics/test_multiple_section.js
@@ -36,12 +36,43 @@ runHeuristicsTest([
         [
           {"section": "section-my", "addressType": "", "contactType": "", "fieldName": "street-address"},
           {"section": "section-my", "addressType": "", "contactType": "", "fieldName": "address-level2"},
           {"section": "section-my", "addressType": "", "contactType": "", "fieldName": "address-level1"},
           {"section": "section-my", "addressType": "", "contactType": "", "fieldName": "postal-code"},
           {"section": "section-my", "addressType": "", "contactType": "", "fieldName": "country"},
         ],
       ],
+      [
+        [
+          {"section": "", "addressType": "", "contactType": "", "fieldName": "name"},
+          {"section": "", "addressType": "", "contactType": "", "fieldName": "organization"},
+          {"section": "", "addressType": "", "contactType": "", "fieldName": "street-address"},
+          {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2"},
+          {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level1"},
+          {"section": "", "addressType": "", "contactType": "", "fieldName": "postal-code"},
+          {"section": "", "addressType": "", "contactType": "", "fieldName": "country"},
+        ],
+        [
+          {"section": "", "addressType": "", "contactType": "", "fieldName": "street-address"},
+          {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2"},
+          {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level1"},
+          {"section": "", "addressType": "", "contactType": "", "fieldName": "postal-code"},
+          {"section": "", "addressType": "", "contactType": "", "fieldName": "country"},
+        ],
+        [
+          {"section": "", "addressType": "", "contactType": "", "fieldName": "street-address"},
+          {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2"},
+          {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level1"},
+          {"section": "", "addressType": "", "contactType": "", "fieldName": "postal-code"},
+          {"section": "", "addressType": "", "contactType": "", "fieldName": "country"},
+          {"section": "", "addressType": "", "contactType": "work", "fieldName": "tel"},
+          {"section": "", "addressType": "", "contactType": "work", "fieldName": "email"},
+        ],
+        [
+          {"section": "", "addressType": "", "contactType": "home", "fieldName": "tel"},
+          {"section": "", "addressType": "", "contactType": "home", "fieldName": "email"},
+        ],
+      ],
     ],
   },
 ], "../../fixtures/");