Bug 990219 - Part 1: Add earlyformsubmit observer and dispatch saveProfile message. r?MattN draft
authorsteveck-chung <schung@mozilla.com>
Tue, 14 Mar 2017 12:12:41 +0800
changeset 584250 044a5bd342d438d7bab20ebe5a8a6e8a5868bff6
parent 582257 9851fcb0bf4d855c36729d7de19f0fa5c9f69776
child 584251 4620ba8c8f12ca937252204349099307be75b91e
push id60664
push userbmo:schung@mozilla.com
push dateThu, 25 May 2017 03:36:27 +0000
reviewersMattN
bugs990219
milestone55.0a1
Bug 990219 - Part 1: Add earlyformsubmit observer and dispatch saveProfile message. r?MattN MozReview-Commit-ID: 7cnFelRfb6f
browser/extensions/formautofill/FormAutofillContent.jsm
browser/extensions/formautofill/FormAutofillHandler.jsm
browser/extensions/formautofill/FormAutofillParent.jsm
browser/extensions/formautofill/test/unit/test_onFormSubmitted.js
--- a/browser/extensions/formautofill/FormAutofillContent.jsm
+++ b/browser/extensions/formautofill/FormAutofillContent.jsm
@@ -9,16 +9,17 @@
 /* eslint-disable no-use-before-define */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["FormAutofillContent"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr, manager: Cm} = Components;
 
+Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://formautofill/FormAutofillUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ProfileAutoCompleteResult",
                                   "resource://formautofill/ProfileAutoCompleteResult.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormAutofillHandler",
                                   "resource://formautofill/FormAutofillHandler.jsm");
@@ -308,30 +309,63 @@ var FormAutofillContent = {
     if (Services.cpmm.initialProcessData.autofillEnabled) {
       ProfileAutocomplete.ensureRegistered();
     }
 
     this.savedFieldNames =
       Services.cpmm.initialProcessData.autofillSavedFieldNames || new Set();
   },
 
-  _onFormSubmit(handler) {
-    // TODO: Handle form submit event for profile saving(bug 990219) and metrics(bug 1341569).
+  /**
+   * Send the profile to parent for doorhanger and storage saving/updating.
+   *
+   * @param {Object} profile Submitted form's address/creditcard guid and record.
+   */
+  _onFormSubmit(profile) {
+    Services.cpmm.sendAsyncMessage("FormAutofill:OnFormSubmit", profile);
   },
 
-  notify(formElement) {
-    this.log.debug("notified for form early submission");
+  /**
+   * Handle earlyformsubmit event and early return when:
+   * 1. In private browsing mode.
+   * 2. Could not map any autofill handler by form element.
+   * 3. Number of filled fields is less than autofill threshold
+   *
+   * @param {HTMLElement} formElement Root element which receives earlyformsubmit event.
+   * @param {Object} domWin Content window
+   * @returns {boolean} Should always return true so form submission isn't canceled.
+   */
+  notify(formElement, domWin) {
+    this.log.debug("Notifying form early submission");
+
+    if (domWin && PrivateBrowsingUtils.isContentWindowPrivate(domWin)) {
+      this.log.debug("Ignoring submission in a private window");
+      return true;
+    }
 
     let handler = this._formsDetails.get(formElement);
     if (!handler) {
       this.log.debug("Form element could not map to an existing handler");
-    } else {
-      this._onFormSubmit(handler);
+      return true;
+    }
+
+    let pendingAddress = handler.createProfile();
+    if (Object.keys(pendingAddress).length < AUTOFILL_FIELDS_THRESHOLD) {
+      this.log.debug(`Not saving since there are only ${Object.keys(pendingAddress).length} usable fields`);
+      return true;
     }
 
+    this._onFormSubmit({
+      address: {
+        guid: handler.filledProfileGUID,
+        record: pendingAddress,
+      },
+      // creditCard: {}
+    });
+
     return true;
   },
 
   receiveMessage({name, data}) {
     switch (name) {
       case "FormAutofill:enabledStatus": {
         if (data) {
           ProfileAutocomplete.ensureRegistered();
--- a/browser/extensions/formautofill/FormAutofillHandler.jsm
+++ b/browser/extensions/formautofill/FormAutofillHandler.jsm
@@ -128,9 +128,32 @@ FormAutofillHandler.prototype = {
       // We keep the highlight of all fields if this form has
       // already been auto-filled with a profile.
       if (this.filledProfileGUID == null) {
         // TODO: Remove highlight style
       }
     }
     */
   },
+
+  /**
+   * Return the profile that is converted from fieldDetails and only non-empty fields
+   * are included.
+   *
+   * @returns {Object} The new profile that convert from details with trimmed result.
+   */
+  createProfile() {
+    let profile = {};
+
+    this.fieldDetails.forEach(detail => {
+      let element = detail.elementWeakRef.get();
+      // Remove the unnecessary spaces
+      let value = element && element.value.trim();
+      if (!value) {
+        return;
+      }
+
+      profile[detail.fieldName] = value;
+    });
+
+    return profile;
+  },
 };
--- a/browser/extensions/formautofill/FormAutofillParent.jsm
+++ b/browser/extensions/formautofill/FormAutofillParent.jsm
@@ -67,16 +67,17 @@ FormAutofillParent.prototype = {
   async init() {
     log.debug("init");
     await profileStorage.initialize();
 
     Services.obs.addObserver(this, "advanced-pane-loaded");
     Services.ppmm.addMessageListener("FormAutofill:GetAddresses", this);
     Services.ppmm.addMessageListener("FormAutofill:SaveAddress", this);
     Services.ppmm.addMessageListener("FormAutofill:RemoveAddresses", this);
+    Services.ppmm.addMessageListener("FormAutofill:OnFormSubmit", this);
 
     // Observing the pref and storage changes
     Services.prefs.addObserver(ENABLED_PREF, this);
     Services.obs.addObserver(this, "formautofill-storage-changed");
 
     // Force to trigger the onStatusChanged function for setting listeners properly
     // while initizlization
     this._setStatus(this._getStatus());
@@ -186,16 +187,19 @@ FormAutofillParent.prototype = {
           profileStorage.addresses.add(data.address);
         }
         break;
       }
       case "FormAutofill:RemoveAddresses": {
         data.guids.forEach(guid => profileStorage.addresses.remove(guid));
         break;
       }
+      case "FormAutofill:OnFormSubmit": {
+        this._onFormSubmit(data, target);
+      }
     }
   },
 
   /**
    * Uninitializes FormAutofillParent. This is for testing only.
    *
    * @private
    */
@@ -252,9 +256,22 @@ FormAutofillParent.prototype = {
     // Remove the internal guid and metadata fields.
     profileStorage.INTERNAL_FIELDS.forEach((fieldName) => {
       Services.ppmm.initialProcessData.autofillSavedFieldNames.delete(fieldName);
     });
 
     Services.ppmm.broadcastAsyncMessage("FormAutofill:savedFieldNames",
                                         Services.ppmm.initialProcessData.autofillSavedFieldNames);
   },
+
+  _onFormSubmit(data, target) {
+    let {address} = data;
+
+    if (address.guid) {
+      // TODO: Show update doorhanger(bug 1303513) and set probe(bug 990200)
+      // if (!profileStorage.addresses.mergeIfPossible(address.guid, address.record)) {
+      // }
+    } else {
+      // TODO: Add first time use probe(bug 990199) and doorhanger(bug 1303510)
+      profileStorage.addresses.add(address.record);
+    }
+  },
 };
--- a/browser/extensions/formautofill/test/unit/test_onFormSubmitted.js
+++ b/browser/extensions/formautofill/test/unit/test_onFormSubmitted.js
@@ -7,24 +7,118 @@ const MOCK_DOC = MockDocument.createTest
                       <input id="street-addr" autocomplete="street-address">
                       <input id="city" autocomplete="address-level2">
                       <input id="country" autocomplete="country">
                       <input id="email" autocomplete="email">
                       <input id="tel" autocomplete="tel">
                       <input id="submit" type="submit">
                     </form>`);
 
-add_task(function* () {
-  do_print("Starting testcase: Make sure content handle earlyformsubmit correctly");
+const TESTCASES = [
+  {
+    description: "Should not trigger saving if the number of fields is less than 3",
+    formValue: {
+      "street-addr": "331 E. Evelyn Avenue",
+      "tel": "1-650-903-0800",
+    },
+    expectedResult: {
+      formSubmission: false,
+    },
+  },
+  {
+    description: "Trigger profile saving",
+    formValue: {
+      "street-addr": "331 E. Evelyn Avenue",
+      "country": "USA",
+      "tel": "1-650-903-0800",
+    },
+    expectedResult: {
+      formSubmission: true,
+      profile: {
+        address: {
+          guid: null,
+          record: {
+            "street-address": "331 E. Evelyn Avenue",
+            "country": "USA",
+            "tel": "1-650-903-0800",
+          },
+        },
+      },
+    },
+  },
+  {
+    description: "Profile saved with trimmed string",
+    formValue: {
+      "street-addr": "331 E. Evelyn Avenue  ",
+      "country": "USA",
+      "tel": "  1-650-903-0800",
+    },
+    expectedResult: {
+      formSubmission: true,
+      profile: {
+        address: {
+          guid: null,
+          record: {
+            "street-address": "331 E. Evelyn Avenue",
+            "country": "USA",
+            "tel": "1-650-903-0800",
+          },
+        },
+      },
+    },
+  },
+  {
+    description: "Eliminate the field that is empty after trimmed",
+    formValue: {
+      "street-addr": "331 E. Evelyn Avenue",
+      "country": "USA",
+      "email": "  ",
+      "tel": "1-650-903-0800",
+    },
+    expectedResult: {
+      formSubmission: true,
+      profile: {
+        address: {
+          guid: null,
+          record: {
+            "street-address": "331 E. Evelyn Avenue",
+            "country": "USA",
+            "tel": "1-650-903-0800",
+          },
+        },
+      },
+    },
+  },
+];
 
-  let form = MOCK_DOC.getElementById("form1");
-  FormAutofillContent.identifyAutofillFields(MOCK_DOC);
+add_task(async function handle_earlyformsubmit_event() {
+  do_print("Starting testcase: Test an invalid form element");
+  let fakeForm = MOCK_DOC.createElement("form");
   sinon.spy(FormAutofillContent, "_onFormSubmit");
 
-  do_check_eq(FormAutofillContent.notify(form), true);
-  do_check_eq(FormAutofillContent._onFormSubmit.called, true);
-
-  let fakeForm = MOCK_DOC.createElement("form");
-  FormAutofillContent._onFormSubmit.reset();
-
   do_check_eq(FormAutofillContent.notify(fakeForm), true);
   do_check_eq(FormAutofillContent._onFormSubmit.called, false);
+  FormAutofillContent._onFormSubmit.restore();
 });
+
+TESTCASES.forEach(testcase => {
+  add_task(async function check_profile_saving_is_called_correctly() {
+    do_print("Starting testcase: " + testcase.description);
+
+    let form = MOCK_DOC.getElementById("form1");
+    for (let key in testcase.formValue) {
+      let input = MOCK_DOC.getElementById(key);
+      input.value = testcase.formValue[key];
+    }
+    sinon.spy(FormAutofillContent, "_onFormSubmit");
+
+    FormAutofillContent.identifyAutofillFields(MOCK_DOC);
+    FormAutofillContent.notify(form);
+
+    do_check_eq(FormAutofillContent._onFormSubmit.called,
+                testcase.expectedResult.formSubmission);
+    if (FormAutofillContent._onFormSubmit.called) {
+      Assert.deepEqual(FormAutofillContent._onFormSubmit.args[0][0],
+                       testcase.expectedResult.profile);
+    }
+    FormAutofillContent._onFormSubmit.restore();
+  });
+});