Bug 1020607 - Populate pending elements with values given by requestAutocomplete UI. r=MattN
--- 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");
});