Bug 1020607 - Populate pending elements with values given by requestAutocomplete UI. r=MattN
authorBrian Nicholson <bnicholson@mozilla.com>
Wed, 25 Jun 2014 17:22:00 +0100
changeset 193651 c517a17ebe6bba7ac80b87228055a0a7df066c5a
parent 193650 4b08f0ea3bb68b3763d39078b50aaf78a7942e97
child 193796 827f88a7eb65f8557c7f05748139bb69691f51fa
push id7718
push userpaolo.mozmail@amadzone.org
push dateMon, 14 Jul 2014 12:52:10 +0000
treeherderfx-team@c517a17ebe6b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1020607
milestone33.0a1
Bug 1020607 - Populate pending elements with values given by requestAutocomplete UI. r=MattN
toolkit/components/formautofill/FormAutofillContentService.js
toolkit/components/formautofill/content/RequestAutocompleteUI.jsm
toolkit/components/formautofill/content/requestAutocomplete.js
toolkit/components/formautofill/test/browser/browser_ui_requestAutocomplete.js
toolkit/components/formautofill/test/head_common.js
--- a/toolkit/components/formautofill/FormAutofillContentService.js
+++ b/toolkit/components/formautofill/FormAutofillContentService.js
@@ -22,81 +22,136 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/Task.jsm");
 
 /**
  * Handles requestAutocomplete for a DOM Form element.
  */
 function FormHandler(aForm, aWindow) {
   this.form = aForm;
   this.window = aWindow;
+
+  this.fieldDetails = [];
 }
 
 FormHandler.prototype = {
   /**
    * DOM Form element to which this object is attached.
    */
   form: null,
 
   /**
    * nsIDOMWindow to which this object is attached.
    */
   window: null,
 
   /**
+   * Array of collected data about relevant form fields.  Each item is an object
+   * storing the identifying details of the field and a reference to the
+   * originally associated element from the form.
+   *
+   * The "section", "addressType", "contactType", and "fieldName" values are
+   * used to identify the exact field when the serializable data is received
+   * from the requestAutocomplete user interface.  There cannot be multiple
+   * fields which have the same exact combination of these values.
+   *
+   * A direct reference to the associated element cannot be sent to the user
+   * interface because processing may be done in the parent process.
+   */
+  fieldDetails: null,
+
+  /**
    * Handles requestAutocomplete and generates the DOM events when finished.
    */
   handleRequestAutocomplete: Task.async(function* () {
     // Start processing the request asynchronously.  At the end, the "reason"
     // variable will contain the outcome of the operation, where an empty
     // string indicates that an unexpected exception occurred.
     let reason = "";
     try {
-      let data = this.collectFormElements();
-
-      let ui = yield FormAutofill.integration.createRequestAutocompleteUI(data);
-      let result = yield ui.show();
-
-      // At present, we only have cancellation and success cases, since we
-      // don't do any validation or precondition check.
-      reason = result.canceled ? "cancel" : "success";
+      reason = yield this.promiseRequestAutocomplete();
     } catch (ex) {
       Cu.reportError(ex);
     }
 
     // The type of event depends on whether this is a success condition.
     let event = (reason == "success")
                 ? new this.window.Event("autocomplete", { bubbles: true })
                 : new this.window.AutocompleteErrorEvent("autocompleteerror",
                                                          { bubbles: true,
                                                            reason: reason });
     yield this.waitForTick();
     this.form.dispatchEvent(event);
   }),
 
   /**
-   * Collects information from the form about fields that can be autofilled, and
-   * returns an object that can be used to build RequestAutocompleteUI.
+   * Handles requestAutocomplete and returns the outcome when finished.
+   *
+   * @return {Promise}
+   * @resolves The "reason" value indicating the outcome of the
+   *           requestAutocomplete operation, including "success" if the
+   *           operation completed successfully.
    */
-  collectFormElements: function () {
+  promiseRequestAutocomplete: Task.async(function* () {
+    let data = this.collectFormFields();
+    if (!data) {
+      return "disabled";
+    }
+
+    let ui = yield FormAutofill.integration.createRequestAutocompleteUI(data);
+    let result = yield ui.show();
+    if (result.canceled) {
+      return "cancel";
+    }
+
+    this.autofillFormFields(result);
+
+    return "success";
+  }),
+
+  /**
+   * Returns information from the form about fields that can be autofilled, and
+   * populates the fieldDetails array on this object accordingly.
+   *
+   * @returns Serializable data structure that can be sent to the user
+   *          interface, or null if the operation failed because the constraints
+   *          on the allowed fields were not honored.
+   */
+  collectFormFields: function () {
     let autofillData = {
       sections: [],
     };
 
     for (let element of this.form.elements) {
       // Query the interface and exclude elements that cannot be autocompleted.
       if (!(element instanceof Ci.nsIDOMHTMLInputElement)) {
         continue;
       }
 
       // Exclude elements to which no autocomplete field has been assigned.
       let info = element.getAutocompleteInfo();
       if (!info.fieldName || ["on", "off"].indexOf(info.fieldName) != -1) {
         continue;
       }
 
+      // Store the association between the field metadata and the element.
+      if (this.fieldDetails.some(f => f.section == info.section &&
+                                      f.addressType == info.addressType &&
+                                      f.contactType == info.contactType &&
+                                      f.fieldName == info.fieldName)) {
+        // A field with the same identifier already exists.
+        return null;
+      }
+      this.fieldDetails.push({
+        section: info.section,
+        addressType: info.addressType,
+        contactType: info.contactType,
+        fieldName: info.fieldName,
+        element: element,
+      });
+
       // The first level is the custom section.
       let section = autofillData.sections
                                 .find(s => s.name == info.section);
       if (!section) {
         section = {
           name: info.section,
           addressSections: [],
         };
@@ -121,16 +176,48 @@ FormHandler.prototype = {
       };
       addressSection.fields.push(field);
     }
 
     return autofillData;
   },
 
   /**
+   * Processes form fields that can be autofilled, and populates them with the
+   * data provided by RequestAutocompleteUI.
+   *
+   * @param aAutofillResult
+   *        Data returned by the user interface.
+   *        {
+   *          fields: [
+   *            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.
+   *          ],
+   *        }
+   */
+  autofillFormFields: function (aAutofillResult) {
+    for (let field of aAutofillResult.fields) {
+      // Get the field details, if it was processed by the user interface.
+      let fieldDetail = this.fieldDetails
+                            .find(f => f.section == field.section &&
+                                       f.addressType == field.addressType &&
+                                       f.contactType == field.contactType &&
+                                       f.fieldName == field.fieldName);
+      if (!fieldDetail) {
+        continue;
+      }
+
+      fieldDetail.element.value = field.value;
+    }
+  },
+
+  /**
    * Waits for one tick of the event loop before resolving the returned promise.
    */
   waitForTick: function () {
     return new Promise(function (resolve) {
       Services.tm.currentThread.dispatch(resolve, Ci.nsIThread.DISPATCH_NORMAL);
     });
   },
 };
--- a/toolkit/components/formautofill/content/RequestAutocompleteUI.jsm
+++ b/toolkit/components/formautofill/content/RequestAutocompleteUI.jsm
@@ -21,29 +21,33 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
 /**
  * Handles the requestAutocomplete user interface.
  */
 this.RequestAutocompleteUI = function (aAutofillData) {
-  Services.console.logStringMessage("rAc UI request: " +
-                                    JSON.stringify(aAutofillData));
+  this._autofillData = aAutofillData;
 }
 
 this.RequestAutocompleteUI.prototype = {
+  _autofillData: null,
+
   show: Task.async(function* () {
     // Create a new promise and store the function that will resolve it.  This
     // will be called by the UI once the selection has been made.
     let resolveFn;
     let uiPromise = new Promise(resolve => resolveFn = resolve);
 
     // Wrap the callback function so that it survives XPCOM.
-    let args = { resolveFn: resolveFn };
+    let args = {
+      resolveFn: resolveFn,
+      autofillData: this._autofillData,
+    };
     args.wrappedJSObject = args;
 
     // Open the window providing the function to call when it closes.
     Services.ww.openWindow(null,
                            "chrome://formautofill/content/requestAutocomplete.xhtml",
                            "Toolkit:RequestAutocomplete",
                            "chrome,dialog=no,resizable",
                            args);
--- a/toolkit/components/formautofill/content/requestAutocomplete.js
+++ b/toolkit/components/formautofill/content/requestAutocomplete.js
@@ -15,30 +15,71 @@ Cu.import("resource://gre/modules/Servic
 
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
 const RequestAutocompleteDialog = {
   resolveFn: null,
+  autofillData: null,
 
   onLoad: function () {
     Task.spawn(function* () {
-      this.resolveFn = window.arguments[0].wrappedJSObject.resolveFn;
+      let args = window.arguments[0].wrappedJSObject;
+      this.resolveFn = args.resolveFn;
+      this.autofillData = args.autofillData;
 
       window.sizeToContent();
 
       Services.obs.notifyObservers(window,
                                    "formautofill-window-initialized", "");
     }.bind(this)).catch(Cu.reportError);
   },
 
   onAccept: function () {
+    // TODO: Replace with autofill storage module (bug 1018304).
+    const dummyDB = {
+      "": {
+        "name": "Mozzy La",
+        "street-address": "331 E Evelyn Ave",
+        "address-level2": "Mountain View",
+        "address-level1": "CA",
+        "country": "US",
+        "postal-code": "94041",
+        "email": "email@example.org",
+      }
+    };
+
+    let result = { fields: [] };
+    for (let section of this.autofillData.sections) {
+      for (let addressSection of section.addressSections) {
+        let addressType = addressSection.addressType;
+        if (!(addressType in dummyDB)) {
+          continue;
+        }
+
+        for (let field of addressSection.fields) {
+          let fieldName = field.fieldName;
+          if (!(fieldName in dummyDB[addressType])) {
+            continue;
+          }
+
+          result.fields.push({
+            section: section.name,
+            addressType: addressType,
+            contactType: field.contactType,
+            fieldName: field.fieldName,
+            value: dummyDB[addressType][fieldName],
+          });
+        }
+      }
+    }
+
     window.close();
-    this.resolveFn({ email: "email@example.org" });
+    this.resolveFn(result);
   },
 
   onCancel: function () {
     window.close();
     this.resolveFn({ canceled: true });
   },
 };
--- a/toolkit/components/formautofill/test/browser/browser_ui_requestAutocomplete.js
+++ b/toolkit/components/formautofill/test/browser/browser_ui_requestAutocomplete.js
@@ -8,34 +8,39 @@
 "use strict";
 
 /**
  * Open the requestAutocomplete UI and test that selecting a profile results in
  * the correct data being sent back to the opener.
  */
 add_task(function* test_select_profile() {
   // Request an e-mail address.
-  let data = { "": { "": { "email": null } } };
-  let { uiWindow, promiseResult } = yield FormAutofillTest.showUI(data);
+  let { uiWindow, promiseResult } = yield FormAutofillTest.showUI(
+                                          TestData.requestEmailOnly);
 
   // Accept the dialog.
   let acceptButton = uiWindow.document.getElementById("accept");
   EventUtils.synthesizeMouseAtCenter(acceptButton, {}, uiWindow);
 
   let result = yield promiseResult;
-  Assert.equal(result.email, "email@example.org");
+  Assert.equal(result.fields.length, 1);
+  Assert.equal(result.fields[0].section, "");
+  Assert.equal(result.fields[0].addressType, "");
+  Assert.equal(result.fields[0].contactType, "");
+  Assert.equal(result.fields[0].fieldName, "email");
+  Assert.equal(result.fields[0].value, "email@example.org");
 });
 
 /**
  * Open the requestAutocomplete UI and cancel the dialog.
  */
 add_task(function* test_cancel() {
   // Request an e-mail address.
-  let data = { "": { "": { "email": null } } };
-  let { uiWindow, promiseResult } = yield FormAutofillTest.showUI(data);
+  let { uiWindow, promiseResult } = yield FormAutofillTest.showUI(
+                                          TestData.requestEmailOnly);
 
   // Cancel the dialog.
   let acceptButton = uiWindow.document.getElementById("cancel");
   EventUtils.synthesizeMouseAtCenter(acceptButton, {}, uiWindow);
 
   let result = yield promiseResult;
   Assert.ok(result.canceled);
 });
--- a/toolkit/components/formautofill/test/head_common.js
+++ b/toolkit/components/formautofill/test/head_common.js
@@ -188,27 +188,48 @@ let FormAutofillTest = {
    *           }
    */
   showUI: Task.async(function* (aFormAutofillData) {
     Output.print("Opening UI with data: " + JSON.stringify(aFormAutofillData));
 
     // Wait for the initialization event before opening the window.
     let promiseUIWindow =
         TestUtils.waitForNotification("formautofill-window-initialized");
-    let ui = yield FormAutofill.integration.createRequestAutocompleteUI({});
+    let ui = yield FormAutofill.integration.createRequestAutocompleteUI(
+                                                         aFormAutofillData);
     let promiseResult = ui.show();
 
     // The window is the subject of the observer notification.
     return {
       uiWindow: (yield promiseUIWindow)[0],
       promiseResult: promiseResult,
     };
   }),
 };
 
+let TestData = {
+  /**
+   * Autofill UI request for the e-mail field only.
+   */
+  get requestEmailOnly() {
+    return {
+      sections: [{
+        name: "",
+        addressSections: [{
+          addressType: "",
+          fields: [{
+            fieldName: "email",
+            contactType: "",
+          }],
+        }],
+      }],
+    };
+  },
+};
+
 /* --- Initialization and termination functions common to all tests --- */
 
 add_task(function* test_common_initialize() {
   // We must manually enable the feature while testing.
   Services.prefs.setBoolPref("dom.forms.requestAutocomplete", true);
   add_termination_task(function* () {
     Services.prefs.clearUserPref("dom.forms.requestAutocomplete");
   });