Bug 1417803 - Part 1: Use activeSection to record the current focused field or section. r=lchang,ralin draft
authorSean Lee <selee@mozilla.com>
Tue, 05 Dec 2017 14:11:36 +0800
changeset 712912 825ece9ece805af5ea78715cc2838c0a23e488e1
parent 712870 7c4579e705c4a3a3610183fe6f44affff3ad57ef
child 712913 64be11aca32ce8b0b9b5097eecc4a9b412bc03be
push id93484
push userbmo:selee@mozilla.com
push dateTue, 19 Dec 2017 03:33:17 +0000
reviewerslchang, ralin
bugs1417803
milestone59.0a1
Bug 1417803 - Part 1: Use activeSection to record the current focused field or section. r=lchang,ralin MozReview-Commit-ID: 4mhmTcJOOz2
browser/extensions/formautofill/FormAutofillContent.jsm
browser/extensions/formautofill/FormAutofillHandler.jsm
browser/extensions/formautofill/test/unit/test_autofillFormFields.js
browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js
--- a/browser/extensions/formautofill/FormAutofillContent.jsm
+++ b/browser/extensions/formautofill/FormAutofillContent.jsm
@@ -101,18 +101,19 @@ AutofillProfileAutoCompleteSearch.protot
 
     let savedFieldNames = FormAutofillContent.savedFieldNames;
 
     let focusedInput = formFillController.focusedInput;
     let info = FormAutofillContent.getInputDetails(focusedInput);
     let isAddressField = FormAutofillUtils.isAddressField(info.fieldName);
     let isInputAutofilled = info.state == FIELD_STATES.AUTO_FILLED;
     let handler = FormAutofillContent.getFormHandler(focusedInput);
-    let allFieldNames = handler.getAllFieldNames(focusedInput);
-    let filledRecordGUID = handler.getFilledRecordGUID(focusedInput);
+    let activeSection = handler.activeSection;
+    let allFieldNames = activeSection.allFieldNames;
+    let filledRecordGUID = activeSection.getFilledRecordGUID();
     let searchPermitted = isAddressField ?
                           FormAutofillUtils.isAutofillAddressesEnabled :
                           FormAutofillUtils.isAutofillCreditCardsEnabled;
     let AutocompleteResult = isAddressField ? AddressResult : CreditCardResult;
 
     ProfileAutocomplete.lastProfileAutoCompleteFocusedInput = focusedInput;
     // Fallback to form-history if ...
     //   - specified autofill feature is pref off.
@@ -157,17 +158,17 @@ AutofillProfileAutoCompleteSearch.protot
 
     this._getRecords(data).then((records) => {
       if (this.forceStop) {
         return;
       }
       // Sort addresses by timeLastUsed for showing the lastest used address at top.
       records.sort((a, b) => b.timeLastUsed - a.timeLastUsed);
 
-      let adaptedRecords = handler.getAdaptedProfiles(records, focusedInput);
+      let adaptedRecords = activeSection.getAdaptedProfiles(records);
       let result = null;
       let isSecure = InsecurePasswordUtils.isFormSecure(handler.form);
 
       result = new AutocompleteResult(searchString,
                                       info.fieldName,
                                       allFieldNames,
                                       adaptedRecords,
                                       {isSecure, isInputAutofilled});
@@ -294,30 +295,30 @@ let ProfileAutocomplete = {
         this.lastProfileAutoCompleteResult.getStyleAt(selectedIndex) != "autofill-profile") {
       return;
     }
 
     let profile = JSON.parse(this.lastProfileAutoCompleteResult.getCommentAt(selectedIndex));
     let {fieldName} = FormAutofillContent.getInputDetails(focusedInput);
     let formHandler = FormAutofillContent.getFormHandler(focusedInput);
 
-    formHandler.autofillFormFields(profile, focusedInput).then(() => {
+    formHandler.autofillFormFields(profile).then(() => {
       autocompleteController.searchString = profile[fieldName];
     });
   },
 
   _clearProfilePreview() {
     let focusedInput = formFillController.focusedInput || this.lastProfileAutoCompleteFocusedInput;
     if (!focusedInput || !FormAutofillContent.getFormDetails(focusedInput)) {
       return;
     }
 
     let formHandler = FormAutofillContent.getFormHandler(focusedInput);
 
-    formHandler.clearPreviewedFormFields(focusedInput);
+    formHandler.activeSection.clearPreviewedFormFields();
   },
 
   _previewSelectedProfile(selectedIndex) {
     let focusedInput = formFillController.focusedInput;
     if (!focusedInput || !FormAutofillContent.getFormDetails(focusedInput)) {
       // The observer notification is for a different process/frame.
       return;
     }
@@ -325,17 +326,17 @@ let ProfileAutocomplete = {
     if (!this.lastProfileAutoCompleteResult ||
         this.lastProfileAutoCompleteResult.getStyleAt(selectedIndex) != "autofill-profile") {
       return;
     }
 
     let profile = JSON.parse(this.lastProfileAutoCompleteResult.getCommentAt(selectedIndex));
     let formHandler = FormAutofillContent.getFormHandler(focusedInput);
 
-    formHandler.previewFormFields(profile, focusedInput);
+    formHandler.activeSection.previewFormFields(profile);
   },
 };
 
 /**
  * Handles content's interactions for the process.
  *
  * NOTE: Declares it by "var" to make it accessible in unit tests.
  */
@@ -484,54 +485,56 @@ var FormAutofillContent = {
    */
   getFormDetails(element) {
     let formHandler = this.getFormHandler(element);
     return formHandler ? formHandler.fieldDetails : null;
   },
 
   getAllFieldNames(element) {
     let formHandler = this.getFormHandler(element);
-    return formHandler ? formHandler.getAllFieldNames(element) : null;
+    return formHandler ? formHandler.activeSection.allFieldNames : null;
   },
 
   identifyAutofillFields(element) {
     this.log.debug("identifyAutofillFields:", "" + element.ownerDocument.location);
 
     if (!this.savedFieldNames) {
       this.log.debug("identifyAutofillFields: savedFieldNames are not known yet");
       Services.cpmm.sendAsyncMessage("FormAutofill:InitStorage");
     }
 
     let formHandler = this.getFormHandler(element);
     if (!formHandler) {
       let formLike = FormLikeFactory.createFromField(element);
       formHandler = new FormAutofillHandler(formLike);
     } else if (!formHandler.updateFormIfNeeded(element)) {
+      formHandler.focusedInput = element;
       this.log.debug("No control is removed or inserted since last collection.");
       return;
     }
 
     let validDetails = formHandler.collectFormFields();
 
     this._formsDetails.set(formHandler.form.rootElement, formHandler);
     this.log.debug("Adding form handler to _formsDetails:", formHandler);
 
     validDetails.forEach(detail =>
       this._markAsAutofillField(detail.elementWeakRef.get())
     );
+    formHandler.focusedInput = element;
   },
 
   clearForm() {
     let focusedInput = formFillController.focusedInput || ProfileAutocomplete._lastAutoCompleteFocusedInput;
     if (!focusedInput) {
       return;
     }
 
     let formHandler = this.getFormHandler(focusedInput);
-    formHandler.clearPopulatedForm(focusedInput);
+    formHandler.activeSection.clearPopulatedForm();
     autocompleteController.searchString = "";
   },
 
   previewProfile(doc) {
     let docWin = doc.ownerGlobal;
     let selectedIndex = ProfileAutocomplete._getSelectedIndex(docWin);
     let lastAutoCompleteResult = ProfileAutocomplete.lastProfileAutoCompleteResult;
     let focusedInput = formFillController.focusedInput;
--- a/browser/extensions/formautofill/FormAutofillHandler.jsm
+++ b/browser/extensions/formautofill/FormAutofillHandler.jsm
@@ -94,16 +94,20 @@ class FormAutofillSection {
                                   ...(this.creditCard.fieldDetails));
     log.debug(this._validDetails.length, "valid fields in the section is collected.");
   }
 
   get validDetails() {
     return this._validDetails;
   }
 
+  set focusedInput(element) {
+    this._focusedDetail = this.getFieldDetailByElement(element);
+  }
+
   getFieldDetailByElement(element) {
     return this._validDetails.find(
       detail => detail.elementWeakRef.get() == element
     );
   }
 
   _isValidCreditCardForm(fieldDetails) {
     let ccNumberReason = "";
@@ -133,37 +137,37 @@ class FormAutofillSection {
     }
     return this._cacheValue.allFieldNames;
   }
 
   getFieldDetailByName(fieldName) {
     return this._validDetails.find(detail => detail.fieldName == fieldName);
   }
 
-  _getTargetSet(element) {
-    let fieldDetail = this.getFieldDetailByElement(element);
+  _getTargetSet() {
+    let fieldDetail = this._focusedDetail;
     if (!fieldDetail) {
       return null;
     }
     if (FormAutofillUtils.isAddressField(fieldDetail.fieldName)) {
       return this.address;
     }
     if (FormAutofillUtils.isCreditCardField(fieldDetail.fieldName)) {
       return this.creditCard;
     }
     return null;
   }
 
-  getFieldDetailsByElement(element) {
-    let targetSet = this._getTargetSet(element);
+  _getFieldDetails() {
+    let targetSet = this._getTargetSet();
     return targetSet ? targetSet.fieldDetails : [];
   }
 
-  getFilledRecordGUID(element) {
-    let targetSet = this._getTargetSet(element);
+  getFilledRecordGUID() {
+    let targetSet = this._getTargetSet();
     return targetSet ? targetSet.filledRecordGUID : null;
   }
 
   _getOneLineStreetAddress(address) {
     if (!this._cacheValue.oneLineStreetAddress) {
       this._cacheValue.oneLineStreetAddress = {};
     }
     if (!this._cacheValue.oneLineStreetAddress[address]) {
@@ -361,26 +365,23 @@ class FormAutofillSection {
   }
 
   /**
    * 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 {HTMLElement} focusedInput
-   *        A focused input element needed to determine the address or credit
-   *        card field.
    */
-  async autofillFields(profile, focusedInput) {
-    let focusedDetail = this.getFieldDetailByElement(focusedInput);
+  async autofillFields(profile) {
+    let focusedDetail = this._focusedDetail;
     if (!focusedDetail) {
       throw new Error("No fieldDetail for the focused input.");
     }
-    let targetSet = this._getTargetSet(focusedInput);
+    let targetSet = this._getTargetSet();
     if (FormAutofillUtils.isCreditCardField(focusedDetail.fieldName)) {
       // When Master Password is enabled by users, the decryption process
       // should prompt Master Password dialog to get the decrypted credit
       // card number. Otherwise, the number can be decrypted with the default
       // password.
       if (profile["cc-number-encrypted"]) {
         let decrypted = await this._decrypt(profile["cc-number-encrypted"], true);
 
@@ -410,16 +411,17 @@ class FormAutofillSection {
       element.previewValue = "";
       let value = profile[fieldDetail.fieldName];
 
       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.
+        let focusedInput = focusedDetail.elementWeakRef.get();
         if (element == focusedInput ||
             (element != focusedInput && !element.value)) {
           element.setUserInput(value);
           this.changeFieldState(fieldDetail, FIELD_STATES.AUTO_FILLED);
         }
       } else if (ChromeUtils.getClassName(element) === "HTMLSelectElement") {
         let cache = this._cacheValue.matchingSelectOption.get(element) || {};
         let option = cache[value] && cache[value].get();
@@ -442,29 +444,27 @@ class FormAutofillSection {
     }
   }
 
   /**
    * Populates result to the preview layers with given profile.
    *
    * @param {Object} profile
    *        A profile to be previewed with
-   * @param {HTMLElement} focusedInput
-   *        A focused input element for determining credit card or address fields.
    */
-  previewFormFields(profile, focusedInput) {
+  previewFormFields(profile) {
     log.debug("preview profile: ", profile);
 
     // Always show the decrypted credit card number when Master Password is
     // disabled.
     if (profile["cc-number-decrypted"]) {
       profile["cc-number"] = profile["cc-number-decrypted"];
     }
 
-    let fieldDetails = this.getFieldDetailsByElement(focusedInput);
+    let fieldDetails = this._getFieldDetails();
     for (let fieldDetail of fieldDetails) {
       let element = fieldDetail.elementWeakRef.get();
       let value = profile[fieldDetail.fieldName] || "";
 
       // Skip the field that is null
       if (!element) {
         continue;
       }
@@ -487,24 +487,21 @@ class FormAutofillSection {
       }
       element.previewValue = value;
       this.changeFieldState(fieldDetail, value ? FIELD_STATES.PREVIEW : FIELD_STATES.NORMAL);
     }
   }
 
   /**
    * Clear preview text and background highlight of all fields.
-   *
-   * @param {HTMLElement} focusedInput
-   *        A focused input element for determining credit card or address fields.
    */
-  clearPreviewedFormFields(focusedInput) {
+  clearPreviewedFormFields() {
     log.debug("clear previewed fields in:", this.form);
 
-    let fieldDetails = this.getFieldDetailsByElement(focusedInput);
+    let fieldDetails = this._getFieldDetails();
     for (let fieldDetail of fieldDetails) {
       let element = fieldDetail.elementWeakRef.get();
       if (!element) {
         log.warn(fieldDetail.fieldName, "is unreachable");
         continue;
       }
 
       element.previewValue = "";
@@ -516,22 +513,19 @@ class FormAutofillSection {
       }
 
       this.changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
     }
   }
 
   /**
    * Clear value and highlight style of all filled fields.
-   *
-   * @param {Object} focusedInput
-   *        A focused input element for determining credit card or address fields.
    */
-  clearPopulatedForm(focusedInput) {
-    let fieldDetails = this.getFieldDetailsByElement(focusedInput);
+  clearPopulatedForm() {
+    let fieldDetails = this._getFieldDetails();
     for (let fieldDetail of fieldDetails) {
       let element = fieldDetail.elementWeakRef.get();
       if (!element) {
         log.warn(fieldDetail.fieldName, "is unreachable");
         continue;
       }
 
       // Only reset value for input element.
@@ -802,16 +796,36 @@ class FormAutofillHandler {
       .getInterface(Ci.nsIDOMWindowUtils);
 
     /**
      * Time in milliseconds since epoch when a user started filling in the form.
      */
     this.timeStartedFillingMS = null;
   }
 
+  set focusedInput(element) {
+    let section = this._sectionCache.get(element);
+    if (!section) {
+      section = this.sections.find(
+        s => s.getFieldDetailByElement(element)
+      );
+      this._sectionCache.set(element, section);
+    }
+
+    this._focusedSection = section;
+
+    if (section) {
+      section.focusedInput = element;
+    }
+  }
+
+  get activeSection() {
+    return this._focusedSection;
+  }
+
   /**
    * Check the form is necessary to be updated. This function should be able to
    * detect any changes including all control elements in the form.
    * @param {HTMLElement} element The element supposed to be in the form.
    * @returns {boolean} FormAutofillHandler.form is updated or not.
    */
   updateFormIfNeeded(element) {
     // When the following condition happens, FormAutofillHandler.form should be
@@ -904,75 +918,30 @@ class FormAutofillHandler {
       }
       input.addEventListener("input", this, {mozSystemGroup: true});
     }
 
     this.fieldDetails = allValidDetails;
     return allValidDetails;
   }
 
-  getSectionByElement(element) {
-    let section = this._sectionCache.get(element);
-    if (!section) {
-      section = this.sections.find(
-        s => s.getFieldDetailByElement(element)
-      );
-      this._sectionCache.set(element, section);
-    }
-    return section;
-  }
-
-  getAllFieldNames(focusedInput) {
-    let section = this.getSectionByElement(focusedInput);
-    return section.allFieldNames;
-  }
-
-  previewFormFields(profile, focusedInput) {
-    let section = this.getSectionByElement(focusedInput);
-    section.previewFormFields(profile, focusedInput);
-  }
-
-  clearPreviewedFormFields(focusedInput) {
-    let section = this.getSectionByElement(focusedInput);
-    section.clearPreviewedFormFields(focusedInput);
-  }
-
-  clearPopulatedForm(focusedInput) {
-    let section = this.getSectionByElement(focusedInput);
-    section.clearPopulatedForm(focusedInput);
-  }
-
-  getFilledRecordGUID(focusedInput) {
-    let section = this.getSectionByElement(focusedInput);
-    return section.getFilledRecordGUID(focusedInput);
-  }
-
-  getAdaptedProfiles(originalProfiles, focusedInput) {
-    let section = this.getSectionByElement(focusedInput);
-    section.getAdaptedProfiles(originalProfiles);
-    return originalProfiles;
-  }
-
   hasFilledSection() {
     return this.sections.some(section => section.isFilled());
   }
 
   /**
    * 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 {HTMLElement} focusedInput
-   *        A focused input element needed to determine the address or credit
-   *        card field.
    */
-  async autofillFormFields(profile, focusedInput) {
+  async autofillFormFields(profile) {
     let noFilledSectionsPreviously = !this.hasFilledSection();
-    await this.getSectionByElement(focusedInput).autofillFields(profile, focusedInput);
+    await this.activeSection.autofillFields(profile);
 
     const onChangeHandler = e => {
       if (!e.isTrusted) {
         return;
       }
       if (e.type == "reset") {
         for (let section of this.sections) {
           section.resetFieldStates();
--- a/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
@@ -532,17 +532,18 @@ function do_test(testcases, testFn) {
             // Avoid waiting for `change` event of a input with a blank value to
             // be filled.
             return;
           }
           promises.push(...testFn(testcase, element));
         });
 
         let focusedInput = doc.getElementById(testcase.focusedInputId);
-        let [adaptedProfile] = handler.getAdaptedProfiles([testcase.profileData], focusedInput);
+        handler.focusedInput = focusedInput;
+        let [adaptedProfile] = handler.activeSection.getAdaptedProfiles([testcase.profileData]);
         await handler.autofillFormFields(adaptedProfile, focusedInput);
         Assert.equal(handlerInfo.filledRecordGUID, testcase.profileData.guid,
                      "Check if filledRecordGUID is set correctly");
         await Promise.all(promises);
       });
     })();
   }
 }
--- a/browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js
+++ b/browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js
@@ -929,30 +929,29 @@ for (let testcase of TESTCASES) {
 
     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);
 
     handler.collectFormFields();
-    let focusedInput = form.elements[0];
-    let adaptedRecords = handler.getAdaptedProfiles(testcase.profileData, focusedInput);
+    handler.focusedInput = form.elements[0];
+    let adaptedRecords = handler.activeSection.getAdaptedProfiles(testcase.profileData);
     Assert.deepEqual(adaptedRecords, testcase.expectedResult);
 
     if (testcase.expectedOptionElements) {
       testcase.expectedOptionElements.forEach((expectedOptionElement, i) => {
         for (let field in expectedOptionElement) {
           let select = form.querySelector(`[autocomplete=${field}]`);
           let expectedOption = doc.getElementById(expectedOptionElement[field]);
           Assert.notEqual(expectedOption, null);
 
           let value = testcase.profileData[i][field];
-          let section = handler.getSectionByElement(select);
-          let cache = section._cacheValue.matchingSelectOption.get(select);
+          let cache = handler.activeSection._cacheValue.matchingSelectOption.get(select);
           let targetOption = cache[value] && cache[value].get();
           Assert.notEqual(targetOption, null);
 
           Assert.equal(targetOption, expectedOption);
         }
       });
     }
   });