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 215713 c517a17ebe6bba7ac80b87228055a0a7df066c5a
parent 215712 4b08f0ea3bb68b3763d39078b50aaf78a7942e97
child 215714 827f88a7eb65f8557c7f05748139bb69691f51fa
push id515
push userraliiev@mozilla.com
push dateMon, 06 Oct 2014 12:51:51 +0000
treeherdermozilla-release@267c7a481bef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1020607
milestone33.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 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");
   });