summaryrefslogtreecommitdiffstats
path: root/toolkit/components/formautofill
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/formautofill')
-rw-r--r--toolkit/components/formautofill/FormAutofill.jsm85
-rw-r--r--toolkit/components/formautofill/FormAutofillContentService.js272
-rw-r--r--toolkit/components/formautofill/FormAutofillIntegration.jsm62
-rw-r--r--toolkit/components/formautofill/FormAutofillStartup.js64
-rw-r--r--toolkit/components/formautofill/content/RequestAutocompleteUI.jsm58
-rw-r--r--toolkit/components/formautofill/content/requestAutocomplete.js85
-rw-r--r--toolkit/components/formautofill/content/requestAutocomplete.xhtml31
-rw-r--r--toolkit/components/formautofill/formautofill.manifest7
-rw-r--r--toolkit/components/formautofill/jar.mn8
-rw-r--r--toolkit/components/formautofill/moz.build46
-rw-r--r--toolkit/components/formautofill/nsIFormAutofillContentService.idl46
-rw-r--r--toolkit/components/formautofill/test/browser/.eslintrc.js7
-rw-r--r--toolkit/components/formautofill/test/browser/browser.ini10
-rw-r--r--toolkit/components/formautofill/test/browser/browser_infrastructure.js48
-rw-r--r--toolkit/components/formautofill/test/browser/browser_ui_requestAutocomplete.js48
-rw-r--r--toolkit/components/formautofill/test/browser/head.js18
-rw-r--r--toolkit/components/formautofill/test/browser/loader.js38
-rw-r--r--toolkit/components/formautofill/test/chrome/.eslintrc.js7
-rw-r--r--toolkit/components/formautofill/test/chrome/chrome.ini17
-rw-r--r--toolkit/components/formautofill/test/chrome/head.js15
-rw-r--r--toolkit/components/formautofill/test/chrome/loader.js116
-rw-r--r--toolkit/components/formautofill/test/chrome/loader_parent.js77
-rw-r--r--toolkit/components/formautofill/test/chrome/test_infrastructure.html8
-rw-r--r--toolkit/components/formautofill/test/chrome/test_infrastructure.js61
-rw-r--r--toolkit/components/formautofill/test/chrome/test_requestAutocomplete_cancel.html9
-rw-r--r--toolkit/components/formautofill/test/chrome/test_requestAutocomplete_cancel.js26
-rw-r--r--toolkit/components/formautofill/test/head_common.js245
-rw-r--r--toolkit/components/formautofill/test/loader_common.js120
-rw-r--r--toolkit/components/formautofill/test/xpcshell/.eslintrc.js7
-rw-r--r--toolkit/components/formautofill/test/xpcshell/head.js23
-rw-r--r--toolkit/components/formautofill/test/xpcshell/loader.js46
-rw-r--r--toolkit/components/formautofill/test/xpcshell/test_infrastructure.js48
-rw-r--r--toolkit/components/formautofill/test/xpcshell/test_integration.js72
-rw-r--r--toolkit/components/formautofill/test/xpcshell/xpcshell.ini12
34 files changed, 1842 insertions, 0 deletions
diff --git a/toolkit/components/formautofill/FormAutofill.jsm b/toolkit/components/formautofill/FormAutofill.jsm
new file mode 100644
index 000000000..aae3a956c
--- /dev/null
+++ b/toolkit/components/formautofill/FormAutofill.jsm
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Main module handling references to objects living in the main process.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "FormAutofill",
+];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Integration.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+/**
+ * Main module handling references to objects living in the main process.
+ */
+this.FormAutofill = {
+ /**
+ * Registers new overrides for the FormAutofillIntegration methods. Example:
+ *
+ * FormAutofill.registerIntegration(base => ({
+ * createRequestAutocompleteUI: Task.async(function* () {
+ * yield base.createRequestAutocompleteUI.apply(this, arguments);
+ * }),
+ * }));
+ *
+ * @param aIntegrationFn
+ * Function returning an object defining the methods that should be
+ * overridden. Its only parameter is an object that contains the base
+ * implementation of all the available methods.
+ *
+ * @note The integration function is called every time the list of registered
+ * integration functions changes. Thus, it should not have any side
+ * effects or do any other initialization.
+ */
+ registerIntegration(aIntegrationFn) {
+ Integration.formAutofill.register(aIntegrationFn);
+ },
+
+ /**
+ * Removes a previously registered FormAutofillIntegration override.
+ *
+ * Overrides don't usually need to be unregistered, unless they are added by a
+ * restartless add-on, in which case they should be unregistered when the
+ * add-on is disabled or uninstalled.
+ *
+ * @param aIntegrationFn
+ * This must be the same function object passed to registerIntegration.
+ */
+ unregisterIntegration(aIntegrationFn) {
+ Integration.formAutofill.unregister(aIntegrationFn);
+ },
+
+ /**
+ * Processes a requestAutocomplete message asynchronously.
+ *
+ * @param aData
+ * Provided to FormAutofillIntegration.createRequestAutocompleteUI.
+ *
+ * @return {Promise}
+ * @resolves Structured data received from the requestAutocomplete UI.
+ */
+ processRequestAutocomplete: Task.async(function* (aData) {
+ let ui = yield FormAutofill.integration.createRequestAutocompleteUI(aData);
+ return yield ui.show();
+ }),
+};
+
+/**
+ * Dynamically generated object implementing the FormAutofillIntegration
+ * methods. Platform-specific code and add-ons can override methods of this
+ * object using the registerIntegration method.
+ */
+Integration.formAutofill.defineModuleGetter(
+ this.FormAutofill,
+ "integration",
+ "resource://gre/modules/FormAutofillIntegration.jsm",
+ "FormAutofillIntegration"
+);
diff --git a/toolkit/components/formautofill/FormAutofillContentService.js b/toolkit/components/formautofill/FormAutofillContentService.js
new file mode 100644
index 000000000..ee8e978ad
--- /dev/null
+++ b/toolkit/components/formautofill/FormAutofillContentService.js
@@ -0,0 +1,272 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Implements a service used by DOM content to request Form Autofill, in
+ * particular when the requestAutocomplete method of Form objects is invoked.
+ *
+ * See the nsIFormAutofillContentService documentation for details.
+ */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FormAutofill",
+ "resource://gre/modules/FormAutofill.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "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 {
+ 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);
+ }),
+
+ /**
+ * 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.
+ */
+ promiseRequestAutocomplete: Task.async(function* () {
+ let data = this.collectFormFields();
+ if (!data) {
+ return "disabled";
+ }
+
+ // Access the frame message manager of the window starting the request.
+ let rootDocShell = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .sameTypeRootTreeItem
+ .QueryInterface(Ci.nsIDocShell);
+ let frameMM = rootDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+
+ // We need to set up a temporary message listener for our result before we
+ // send the request to the parent process. At present, there is no check
+ // for reentrancy (bug 1020459), thus it is possible that we'll receive a
+ // message for a different request, but this will not be normally allowed.
+ let promiseRequestAutocompleteResult = new Promise((resolve, reject) => {
+ frameMM.addMessageListener("FormAutofill:RequestAutocompleteResult",
+ function onResult(aMessage) {
+ frameMM.removeMessageListener(
+ "FormAutofill:RequestAutocompleteResult", onResult);
+ // Exceptions in the parent process are serialized and propagated in
+ // the response message that we received.
+ if ("exception" in aMessage.data) {
+ reject(aMessage.data.exception);
+ } else {
+ resolve(aMessage.data);
+ }
+ });
+ });
+
+ // Send the message to the parent process, and wait for the result. This
+ // will throw an exception if one occurred in the parent process.
+ frameMM.sendAsyncMessage("FormAutofill:RequestAutocomplete", data);
+ let result = yield promiseRequestAutocompleteResult;
+ 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: [],
+ };
+ autofillData.sections.push(section);
+ }
+
+ // The second level is the address section.
+ let addressSection = section.addressSections
+ .find(s => s.addressType == info.addressType);
+ if (!addressSection) {
+ addressSection = {
+ addressType: info.addressType,
+ fields: [],
+ };
+ section.addressSections.push(addressSection);
+ }
+
+ // The third level contains all the fields within the section.
+ let field = {
+ fieldName: info.fieldName,
+ contactType: info.contactType,
+ };
+ 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);
+ });
+ },
+};
+
+/**
+ * Implements a service used by DOM content to request Form Autofill, in
+ * particular when the requestAutocomplete method of Form objects is invoked.
+ */
+function FormAutofillContentService() {
+}
+
+FormAutofillContentService.prototype = {
+ classID: Components.ID("{ed9c2c3c-3f86-4ae5-8e31-10f71b0f19e6}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormAutofillContentService]),
+
+ // nsIFormAutofillContentService
+ requestAutocomplete: function (aForm, aWindow) {
+ new FormHandler(aForm, aWindow).handleRequestAutocomplete()
+ .catch(Cu.reportError);
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FormAutofillContentService]);
diff --git a/toolkit/components/formautofill/FormAutofillIntegration.jsm b/toolkit/components/formautofill/FormAutofillIntegration.jsm
new file mode 100644
index 000000000..4b838a6ab
--- /dev/null
+++ b/toolkit/components/formautofill/FormAutofillIntegration.jsm
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * This module defines the default implementation of platform-specific functions
+ * that can be overridden by the host application and by add-ons.
+ *
+ * This module should not be imported directly, but the "integration" getter of
+ * the FormAutofill module should be used to get a reference to the currently
+ * defined implementations of the methods.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "FormAutofillIntegration",
+];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RequestAutocompleteUI",
+ "resource://gre/modules/RequestAutocompleteUI.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+/**
+ * This module defines the default implementation of platform-specific functions
+ * that can be overridden by the host application and by add-ons.
+ */
+this.FormAutofillIntegration = {
+ /**
+ * Creates a new RequestAutocompleteUI object.
+ *
+ * @param aAutofillData
+ * Provides the initial data required to display the user interface.
+ * {
+ * sections: [{
+ * name: User-specified section name, or empty string.
+ * addressSections: [{
+ * addressType: "shipping", "billing", or empty string.
+ * fields: [{
+ * fieldName: Type of information requested, like "email".
+ * contactType: For example "work", "home", or empty string.
+ * }],
+ * }],
+ * }],
+ * }
+ *
+ * @return {Promise}
+ * @resolves The newly created RequestAutocompleteUI object.
+ * @rejects JavaScript exception.
+ */
+ createRequestAutocompleteUI: Task.async(function* (aAutofillData) {
+ return new RequestAutocompleteUI(aAutofillData);
+ }),
+};
diff --git a/toolkit/components/formautofill/FormAutofillStartup.js b/toolkit/components/formautofill/FormAutofillStartup.js
new file mode 100644
index 000000000..92887f872
--- /dev/null
+++ b/toolkit/components/formautofill/FormAutofillStartup.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Handles startup in the parent process.
+ */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FormAutofill",
+ "resource://gre/modules/FormAutofill.jsm");
+
+/**
+ * Handles startup in the parent process.
+ */
+function FormAutofillStartup() {
+}
+
+FormAutofillStartup.prototype = {
+ classID: Components.ID("{51c95b3d-7431-467b-8d50-383f158ce9e5}"),
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIFrameMessageListener,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference,
+ ]),
+
+ // nsIObserver
+ observe: function (aSubject, aTopic, aData) {
+ // This method is called by the "profile-after-change" category on startup,
+ // which is called before any web page loads. At this time, we need to
+ // register a global message listener in the parent process preemptively,
+ // because we can receive requests from child processes at any time. For
+ // performance reasons, we use this object as a message listener, so that we
+ // don't have to load the FormAutoFill module at startup.
+ let globalMM = Cc["@mozilla.org/globalmessagemanager;1"]
+ .getService(Ci.nsIMessageListenerManager);
+ globalMM.addMessageListener("FormAutofill:RequestAutocomplete", this);
+ },
+
+ // nsIFrameMessageListener
+ receiveMessage: function (aMessage) {
+ // Process the "FormAutofill:RequestAutocomplete" message. Any exception
+ // raised in the parent process is caught and serialized into the reply
+ // message that is sent to the requesting child process.
+ FormAutofill.processRequestAutocomplete(aMessage.data)
+ .catch(ex => { return { exception: ex } })
+ .then(result => {
+ // The browser message manager in the parent will send the reply to the
+ // associated frame message manager in the child.
+ let browserMM = aMessage.target.messageManager;
+ browserMM.sendAsyncMessage("FormAutofill:RequestAutocompleteResult",
+ result);
+ })
+ .catch(Cu.reportError);
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FormAutofillStartup]);
diff --git a/toolkit/components/formautofill/content/RequestAutocompleteUI.jsm b/toolkit/components/formautofill/content/RequestAutocompleteUI.jsm
new file mode 100644
index 000000000..74c4834ba
--- /dev/null
+++ b/toolkit/components/formautofill/content/RequestAutocompleteUI.jsm
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Handles the requestAutocomplete user interface.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "RequestAutocompleteUI",
+];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+/**
+ * Handles the requestAutocomplete user interface.
+ */
+this.RequestAutocompleteUI = function (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,
+ 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);
+
+ // Wait for the window to be closed and the operation confirmed.
+ return yield uiPromise;
+ }),
+};
diff --git a/toolkit/components/formautofill/content/requestAutocomplete.js b/toolkit/components/formautofill/content/requestAutocomplete.js
new file mode 100644
index 000000000..47d500964
--- /dev/null
+++ b/toolkit/components/formautofill/content/requestAutocomplete.js
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Implementation of "requestAutocomplete.xhtml".
+ */
+
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+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* () {
+ 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(result);
+ },
+
+ onCancel: function () {
+ window.close();
+ this.resolveFn({ canceled: true });
+ },
+};
diff --git a/toolkit/components/formautofill/content/requestAutocomplete.xhtml b/toolkit/components/formautofill/content/requestAutocomplete.xhtml
new file mode 100644
index 000000000..269e55bd6
--- /dev/null
+++ b/toolkit/components/formautofill/content/requestAutocomplete.xhtml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % requestAutocompleteDTD SYSTEM "chrome://formautofill/locale/requestAutocomplete.dtd">
+ %requestAutocompleteDTD;
+ <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
+ %globalDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>requestAutocomplete demo window</title>
+ <link rel="stylesheet"
+ href="chrome://mozapps/skin/formautofill/requestAutocomplete.css" />
+ <script type="application/javascript;version=1.7"
+ src="chrome://formautofill/content/requestAutocomplete.js" />
+ </head>
+ <body dir="&locale.dir;" onload="RequestAutocompleteDialog.onLoad();">
+ <h1>requestAutocomplete</h1>
+ <p>This is a demo window.</p>
+ <input id="accept" type="button" value="(OK)"
+ onclick="RequestAutocompleteDialog.onAccept();" />
+ <input id="cancel" type="button" value="(Cancel)"
+ onclick="RequestAutocompleteDialog.onCancel();" />
+ </body>
+</html>
diff --git a/toolkit/components/formautofill/formautofill.manifest b/toolkit/components/formautofill/formautofill.manifest
new file mode 100644
index 000000000..880972edc
--- /dev/null
+++ b/toolkit/components/formautofill/formautofill.manifest
@@ -0,0 +1,7 @@
+component {ed9c2c3c-3f86-4ae5-8e31-10f71b0f19e6} FormAutofillContentService.js
+contract @mozilla.org/formautofill/content-service;1 {ed9c2c3c-3f86-4ae5-8e31-10f71b0f19e6}
+component {51c95b3d-7431-467b-8d50-383f158ce9e5} FormAutofillStartup.js
+contract @mozilla.org/formautofill/startup;1 {51c95b3d-7431-467b-8d50-383f158ce9e5}
+#ifdef NIGHTLY_BUILD
+category profile-after-change FormAutofillStartup @mozilla.org/formautofill/startup;1
+#endif
diff --git a/toolkit/components/formautofill/jar.mn b/toolkit/components/formautofill/jar.mn
new file mode 100644
index 000000000..ebe869b58
--- /dev/null
+++ b/toolkit/components/formautofill/jar.mn
@@ -0,0 +1,8 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+toolkit.jar:
+% content formautofill %content/formautofill/
+ content/formautofill/requestAutocomplete.js (content/requestAutocomplete.js)
+ content/formautofill/requestAutocomplete.xhtml (content/requestAutocomplete.xhtml)
diff --git a/toolkit/components/formautofill/moz.build b/toolkit/components/formautofill/moz.build
new file mode 100644
index 000000000..2c2179f81
--- /dev/null
+++ b/toolkit/components/formautofill/moz.build
@@ -0,0 +1,46 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+if CONFIG['NIGHTLY_BUILD']:
+ BROWSER_CHROME_MANIFESTS += [
+ 'test/browser/browser.ini',
+ ]
+
+ MOCHITEST_CHROME_MANIFESTS += [
+ 'test/chrome/chrome.ini',
+ ]
+
+XPCSHELL_TESTS_MANIFESTS += [
+ 'test/xpcshell/xpcshell.ini',
+]
+
+XPIDL_SOURCES += [
+ 'nsIFormAutofillContentService.idl',
+]
+
+XPIDL_MODULE = 'toolkit_formautofill'
+
+EXTRA_COMPONENTS += [
+ 'FormAutofillContentService.js',
+ 'FormAutofillStartup.js',
+]
+
+EXTRA_PP_COMPONENTS += [
+ 'formautofill.manifest',
+]
+
+EXTRA_JS_MODULES += [
+ 'content/RequestAutocompleteUI.jsm',
+ 'FormAutofill.jsm',
+ 'FormAutofillIntegration.jsm',
+]
+
+JAR_MANIFESTS += [
+ 'jar.mn',
+]
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Form Manager')
diff --git a/toolkit/components/formautofill/nsIFormAutofillContentService.idl b/toolkit/components/formautofill/nsIFormAutofillContentService.idl
new file mode 100644
index 000000000..300645e74
--- /dev/null
+++ b/toolkit/components/formautofill/nsIFormAutofillContentService.idl
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIDOMHTMLFormElement;
+interface nsIDOMWindow;
+
+/**
+ * Defines a service used by DOM content to request Form Autofill, in particular
+ * when the requestAutocomplete method of Form objects is invoked.
+ *
+ * This service lives in the process that hosts the requesting DOM content.
+ * This means that, in a multi-process (e10s) environment, there can be an
+ * instance of the service for each content process, in addition to an instance
+ * for the chrome process.
+ *
+ * @remarks The service implementation uses a child-side message manager to
+ * communicate with a parent-side message manager living in the chrome
+ * process, where most of the processing is located.
+ */
+[scriptable, uuid(1db29340-99df-4845-9102-0c5d281b2fe8)]
+interface nsIFormAutofillContentService : nsISupports
+{
+ /**
+ * Invoked by the requestAutocomplete method of the DOM Form object.
+ *
+ * The application is expected to display a user interface asking for the
+ * details that are relevant to the form being filled in. The application
+ * should use the "autocomplete" attributes on the input elements as hints
+ * about which type of information is being requested.
+ *
+ * The processing will result in either an "autocomplete" simple DOM Event or
+ * an AutocompleteErrorEvent being fired on the form.
+ *
+ * @param aForm
+ * The form on which the requestAutocomplete method was invoked.
+ * @param aWindow
+ * The window where the form is located. This must be specified even
+ * for elements that are not in a document, and is used to generate the
+ * DOM events resulting from the operation.
+ */
+ void requestAutocomplete(in nsIDOMHTMLFormElement aForm,
+ in nsIDOMWindow aWindow);
+};
diff --git a/toolkit/components/formautofill/test/browser/.eslintrc.js b/toolkit/components/formautofill/test/browser/.eslintrc.js
new file mode 100644
index 000000000..7c8021192
--- /dev/null
+++ b/toolkit/components/formautofill/test/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/formautofill/test/browser/browser.ini b/toolkit/components/formautofill/test/browser/browser.ini
new file mode 100644
index 000000000..dff9c3381
--- /dev/null
+++ b/toolkit/components/formautofill/test/browser/browser.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+# The following files starting with ".." are installed in the current folder.
+support-files =
+ ../head_common.js
+ ../loader_common.js
+ head.js
+ loader.js
+
+[browser_infrastructure.js]
+[browser_ui_requestAutocomplete.js]
diff --git a/toolkit/components/formautofill/test/browser/browser_infrastructure.js b/toolkit/components/formautofill/test/browser/browser_infrastructure.js
new file mode 100644
index 000000000..af27cfdb5
--- /dev/null
+++ b/toolkit/components/formautofill/test/browser/browser_infrastructure.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests the local testing infrastructure.
+ */
+
+"use strict";
+
+/**
+ * Tests the truth assertion function.
+ */
+add_task(function* test_assert_truth() {
+ Assert.ok(1 != 2);
+});
+
+/**
+ * Tests the equality assertion function.
+ */
+add_task(function* test_assert_equality() {
+ Assert.equal(1 + 1, 2);
+});
+
+/**
+ * Uses some of the utility functions provided by the framework.
+ */
+add_task(function* test_utility_functions() {
+ // The "print" function is useful to log information that is not known before.
+ let randomString = "R" + Math.floor(Math.random() * 10);
+ Output.print("The random contents will be '" + randomString + "'.");
+
+ // Create the text file with the random contents.
+ let path = yield TestUtils.getTempFile("test-infrastructure.txt");
+ yield OS.File.writeAtomic(path, new TextEncoder().encode(randomString));
+
+ // Test a few utility functions.
+ yield TestUtils.waitForTick();
+ yield TestUtils.waitMs(50);
+
+ let promiseMyNotification = TestUtils.waitForNotification("my-topic");
+ Services.obs.notifyObservers(null, "my-topic", "");
+ yield promiseMyNotification;
+
+ // Check the file size. The file will be deleted automatically later.
+ Assert.equal((yield OS.File.stat(path)).size, randomString.length);
+});
+
+add_task(terminationTaskFn);
diff --git a/toolkit/components/formautofill/test/browser/browser_ui_requestAutocomplete.js b/toolkit/components/formautofill/test/browser/browser_ui_requestAutocomplete.js
new file mode 100644
index 000000000..2a7b58f12
--- /dev/null
+++ b/toolkit/components/formautofill/test/browser/browser_ui_requestAutocomplete.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests the requestAutocomplete user interface.
+ */
+
+"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 { 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.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 { 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);
+});
+
+add_task(terminationTaskFn);
diff --git a/toolkit/components/formautofill/test/browser/head.js b/toolkit/components/formautofill/test/browser/head.js
new file mode 100644
index 000000000..882f3fd5e
--- /dev/null
+++ b/toolkit/components/formautofill/test/browser/head.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Initialization specific to Form Autofill mochitest-browser tests.
+ */
+
+"use strict";
+
+// We cannot start initialization from "loader.js" like we do in the xpcshell
+// and mochitest-chrome frameworks, thus we load the script here.
+Services.scriptloader.loadSubScript(getRootDirectory(gTestPath) + "loader.js",
+ this);
+
+// The testing framework is fully initialized at this point, you can add
+// mochitest-browser specific test initialization here. If you need shared
+// functions or initialization that are not specific to mochitest-browser,
+// consider adding them to "head_common.js" in the parent folder instead.
diff --git a/toolkit/components/formautofill/test/browser/loader.js b/toolkit/components/formautofill/test/browser/loader.js
new file mode 100644
index 000000000..bfd5b9ee0
--- /dev/null
+++ b/toolkit/components/formautofill/test/browser/loader.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Infrastructure for the mochitest-browser tests located in this folder.
+ *
+ * See "loader_common.js" in the parent folder for a general overview.
+ *
+ * Unless you are adding new features to the framework, you shouldn't have to
+ * modify this file. Use "head_common.js" or "head.js" for shared code.
+ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(getRootDirectory(gTestPath) +
+ "loader_common.js", this);
+
+// Define output functions so they look the same across all frameworks.
+var Output = {
+ print: info,
+};
+
+// Define assertion functions so they look the same across all frameworks.
+var Assert = {
+ ok: _mochitestAssert.ok,
+ equal: _mochitestAssert.equal,
+};
+
+// Define task registration functions, see description in "loader_common.js".
+var add_task_in_parent_process = add_task;
+var add_task_in_child_process = function () {};
+var add_task_in_both_processes = add_task;
+
+Services.scriptloader.loadSubScript(getRootDirectory(gTestPath) +
+ "head_common.js", this);
+
+// Reminder: unless you are adding new features to the framework, you shouldn't
+// have to modify this file. Use "head_common.js" or "head.js" for shared code.
diff --git a/toolkit/components/formautofill/test/chrome/.eslintrc.js b/toolkit/components/formautofill/test/chrome/.eslintrc.js
new file mode 100644
index 000000000..8c0f4f574
--- /dev/null
+++ b/toolkit/components/formautofill/test/chrome/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/chrome.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/formautofill/test/chrome/chrome.ini b/toolkit/components/formautofill/test/chrome/chrome.ini
new file mode 100644
index 000000000..67b7869af
--- /dev/null
+++ b/toolkit/components/formautofill/test/chrome/chrome.ini
@@ -0,0 +1,17 @@
+[DEFAULT]
+# The following files starting with ".." are installed in the current folder.
+support-files =
+ ../head_common.js
+ ../loader_common.js
+ head.js
+ test_infrastructure.js
+ test_requestAutocomplete_cancel.js
+ loader_parent.js
+ loader.js
+
+# For each test defined below, the associated JavaScript file must be declared
+# in the list above. This is required because a "support-files" declaration on
+# the individual test would override the global list instead of adding entries.
+
+[test_infrastructure.html]
+[test_requestAutocomplete_cancel.html]
diff --git a/toolkit/components/formautofill/test/chrome/head.js b/toolkit/components/formautofill/test/chrome/head.js
new file mode 100644
index 000000000..4110d5e7c
--- /dev/null
+++ b/toolkit/components/formautofill/test/chrome/head.js
@@ -0,0 +1,15 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Initialization specific to Form Autofill mochitest-chrome tests.
+ *
+ * This file is loaded by "loader.js".
+ */
+
+"use strict";
+
+// The testing framework is fully initialized at this point, you can add
+// mochitest-chrome specific test initialization here. If you need shared
+// functions or initialization that are not specific to mochitest-chrome,
+// consider adding them to "head_common.js" in the parent folder instead.
diff --git a/toolkit/components/formautofill/test/chrome/loader.js b/toolkit/components/formautofill/test/chrome/loader.js
new file mode 100644
index 000000000..25b0e6ea3
--- /dev/null
+++ b/toolkit/components/formautofill/test/chrome/loader.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Infrastructure for the mochitest-chrome tests located in this folder.
+ *
+ * See "loader_common.js" in the parent folder for a general overview.
+ *
+ * Unless you are adding new features to the framework, you shouldn't have to
+ * modify this file. Use "head_common.js" or "head.js" for shared code.
+ */
+
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/SimpleTest.js", this);
+
+var sharedUrl = SimpleTest.getTestFileURL("loader_common.js");
+Services.scriptloader.loadSubScript(sharedUrl, this);
+
+var parentScript = SpecialPowers.loadChromeScript(
+ SimpleTest.getTestFileURL("loader_parent.js"));
+
+// Replace the extension of the loaded HTML file with ".js"
+var testUrl = location.href.replace(/\.\w+$/, ".js");
+
+// Start loading the test script in the parent process.
+var promiseParentInitFinished = new Promise(function (resolve) {
+ parentScript.addMessageListener("finish_load_in_parent", resolve);
+});
+parentScript.sendAsyncMessage("start_load_in_parent", { testUrl: testUrl });
+
+// Define output functions so they look the same across all frameworks.
+var Output = {
+ print: info,
+};
+
+// Define assertion functions so they look the same across all frameworks.
+var Assert = {
+ ok: _mochitestAssert.ok,
+ equal: _mochitestAssert.equal,
+};
+
+var executeSoon = SimpleTest.executeSoon;
+
+var gTestTasks = [];
+
+// Define task registration functions, see description in "loader_common.js".
+function add_task(taskFn) {
+ gTestTasks.push([taskFn, "content", taskFn.name]);
+}
+function add_task_in_parent_process(taskFn, taskIdOverride) {
+ let taskId = taskIdOverride || getTaskId(Components.stack.caller);
+ gTestTasks.push([taskFn, "parent", taskId]);
+}
+function add_task_in_both_processes(taskFn) {
+ // We need to define a task ID based on our direct caller.
+ add_task_in_parent_process(taskFn, getTaskId(Components.stack.caller));
+ add_task(taskFn);
+}
+var add_task_in_child_process = add_task;
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+
+ Task.spawn(function* () {
+ try {
+ for (let [taskFn, taskType, taskId] of gTestTasks) {
+ if (taskType == "content") {
+ // This is a normal task executed in the current process.
+ info("Running " + taskFn.name);
+ yield Task.spawn(taskFn);
+ } else {
+ // This is a task executed in the parent process.
+ info("Running task in parent process: " + taskFn.name);
+ let promiseFinished = new Promise(function (resolve) {
+ parentScript.addMessageListener("finish_task_" + taskId, resolve);
+ });
+ parentScript.sendAsyncMessage("start_task_" + taskId);
+ yield promiseFinished;
+ info("Finished task in parent process: " + taskFn.name);
+ }
+ }
+ } catch (ex) {
+ ok(false, ex);
+ }
+
+ SimpleTest.finish();
+ });
+});
+
+// Wait for the test script to be loaded in the parent process. This means that
+// test tasks are registered and ready, but have not been executed yet.
+add_task(function* wait_loading_in_parent_process() {
+ yield promiseParentInitFinished;
+});
+
+var headUrl = SimpleTest.getTestFileURL("head_common.js");
+Services.scriptloader.loadSubScript(headUrl, this);
+
+Output.print("Loading test file: " + testUrl);
+Services.scriptloader.loadSubScript(testUrl, this);
+
+// Register the execution of termination tasks after all other tasks.
+add_task(terminationTaskFn);
+add_task_in_parent_process(terminationTaskFn, terminationTaskFn.name);
+
+SimpleTest.waitForExplicitFinish();
+
+// Reminder: unless you are adding new features to the framework, you shouldn't
+// have to modify this file. Use "head_common.js" or "head.js" for shared code.
diff --git a/toolkit/components/formautofill/test/chrome/loader_parent.js b/toolkit/components/formautofill/test/chrome/loader_parent.js
new file mode 100644
index 000000000..bf823218e
--- /dev/null
+++ b/toolkit/components/formautofill/test/chrome/loader_parent.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Infrastructure for the mochitest-chrome tests located in this folder, always
+ * executed in the parent process.
+ *
+ * See "loader_common.js" in the parent folder for a general overview.
+ *
+ * Unless you are adding new features to the framework, you shouldn't have to
+ * modify this file. Use "head_common.js" or "head.js" for shared code.
+ */
+
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+
+var sharedUrl = "chrome://mochitests/content/chrome/" +
+ "toolkit/components/formautofill/test/chrome/loader_common.js";
+Services.scriptloader.loadSubScript(sharedUrl, this);
+
+// Define output functions so they look the same across all frameworks. Since
+// we don't have an output function available here, we report as TEST-PASS.
+var Output = {
+ print: message => assert.ok(true, message),
+};
+
+// Define assertion functions so they look the same across all frameworks.
+var Assert = {
+ ok: assert.ok,
+ equal: assert.equal,
+};
+
+// Define task registration functions, see description in "loader_common.js".
+function add_task_in_parent_process(taskFn, taskIdOverride) {
+ let taskId = taskIdOverride || getTaskId(Components.stack.caller);
+ Output.print("Registering in the parent process: " + taskId);
+ addMessageListener("start_task_" + taskId, function () {
+ Task.spawn(function* () {
+ try {
+ Output.print("Running in the parent process " + taskId);
+ yield Task.spawn(taskFn);
+ } catch (ex) {
+ assert.ok(false, ex);
+ }
+
+ sendAsyncMessage("finish_task_" + taskId, {});
+ });
+ });
+}
+var add_task = function () {};
+var add_task_in_child_process = function () {};
+var add_task_in_both_processes = add_task_in_parent_process;
+
+// We need to wait for the child process to send us the path of the test file
+// to load before we can actually start loading it.
+var context = this;
+addMessageListener("start_load_in_parent", function (message) {
+ Output.print("Starting loading infrastructure in parent process.");
+ let headUrl = "chrome://mochitests/content/chrome/" +
+ "toolkit/components/formautofill/test/chrome/head_common.js";
+ Services.scriptloader.loadSubScript(headUrl, context);
+
+ Services.scriptloader.loadSubScript(message.testUrl, context);
+
+ // Register the execution of termination tasks after all other tasks.
+ add_task_in_parent_process(terminationTaskFn, terminationTaskFn.name);
+
+ Output.print("Finished loading infrastructure in parent process.");
+ sendAsyncMessage("finish_load_in_parent", {});
+});
+
+// Reminder: unless you are adding new features to the framework, you shouldn't
+// have to modify this file. Use "head_common.js" or "head.js" for shared code.
diff --git a/toolkit/components/formautofill/test/chrome/test_infrastructure.html b/toolkit/components/formautofill/test/chrome/test_infrastructure.html
new file mode 100644
index 000000000..54f417f77
--- /dev/null
+++ b/toolkit/components/formautofill/test/chrome/test_infrastructure.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<script type="application/javascript;version=1.7" src="loader.js"></script>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<p id="paragraph">Paragraph contents.</p>
+
+</body></html>
diff --git a/toolkit/components/formautofill/test/chrome/test_infrastructure.js b/toolkit/components/formautofill/test/chrome/test_infrastructure.js
new file mode 100644
index 000000000..c3b0b43ff
--- /dev/null
+++ b/toolkit/components/formautofill/test/chrome/test_infrastructure.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests the local testing infrastructure.
+ */
+
+"use strict";
+
+/**
+ * Tests the truth assertion function.
+ */
+add_task(function* test_assert_truth() {
+ Assert.ok(1 != 2);
+});
+
+/**
+ * Tests the equality assertion function.
+ */
+add_task(function* test_assert_equality() {
+ Assert.equal(1 + 1, 2);
+});
+
+/**
+ * Uses some of the utility functions provided by the framework.
+ */
+add_task(function* test_utility_functions() {
+ // The "print" function is useful to log information that is not known before.
+ let randomString = "R" + Math.floor(Math.random() * 10);
+ Output.print("The random contents will be '" + randomString + "'.");
+
+ // Create the text file with the random contents.
+ let path = yield TestUtils.getTempFile("test-infrastructure.txt");
+ yield OS.File.writeAtomic(path, new TextEncoder().encode(randomString));
+
+ // Test a few utility functions.
+ yield TestUtils.waitForTick();
+ yield TestUtils.waitMs(50);
+
+ let promiseMyNotification = TestUtils.waitForNotification("my-topic");
+ Services.obs.notifyObservers(null, "my-topic", "");
+ yield promiseMyNotification;
+
+ // Check the file size. The file will be deleted automatically later.
+ Assert.equal((yield OS.File.stat(path)).size, randomString.length);
+});
+
+/**
+ * This type of test has access to the content declared above.
+ */
+add_task(function* test_content() {
+ Assert.equal($("paragraph").innerHTML, "Paragraph contents.");
+
+ let promiseMyEvent = TestUtils.waitForEvent($("paragraph"), "MyEvent");
+
+ let event = document.createEvent("CustomEvent");
+ event.initCustomEvent("MyEvent", true, false, {});
+ $("paragraph").dispatchEvent(event);
+
+ yield promiseMyEvent;
+});
diff --git a/toolkit/components/formautofill/test/chrome/test_requestAutocomplete_cancel.html b/toolkit/components/formautofill/test/chrome/test_requestAutocomplete_cancel.html
new file mode 100644
index 000000000..8ae7ffd4b
--- /dev/null
+++ b/toolkit/components/formautofill/test/chrome/test_requestAutocomplete_cancel.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<script type="application/javascript;version=1.7" src="loader.js"></script>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<form id="form">
+</form>
+
+</body></html>
diff --git a/toolkit/components/formautofill/test/chrome/test_requestAutocomplete_cancel.js b/toolkit/components/formautofill/test/chrome/test_requestAutocomplete_cancel.js
new file mode 100644
index 000000000..1ee12bd9a
--- /dev/null
+++ b/toolkit/components/formautofill/test/chrome/test_requestAutocomplete_cancel.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests the response sent when requestAutocomplete is canceled by the user.
+ */
+
+"use strict";
+
+/**
+ * The requestAutocomplete UI will not be displayed during these tests.
+ */
+add_task_in_parent_process(function* test_cancel_init() {
+ FormAutofillTest.requestAutocompleteResponse = { canceled: true };
+});
+
+/**
+ * Tests the case where the feature is canceled.
+ */
+add_task(function* test_cancel() {
+ let promise = TestUtils.waitForEvent($("form"), "autocompleteerror");
+ $("form").requestAutocomplete();
+ let errorEvent = yield promise;
+
+ Assert.equal(errorEvent.reason, "cancel");
+});
diff --git a/toolkit/components/formautofill/test/head_common.js b/toolkit/components/formautofill/test/head_common.js
new file mode 100644
index 000000000..82b87e4a6
--- /dev/null
+++ b/toolkit/components/formautofill/test/head_common.js
@@ -0,0 +1,245 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Initialization of Form Autofill tests shared between all frameworks.
+ *
+ * A copy of this file is installed in each of the framework subfolders, this
+ * means it becomes a sibling of the test files in the final layout. This is
+ * determined by how manifest "support-files" installation works.
+ */
+
+"use strict";
+
+// The requestAutocomplete framework is available at this point, you can add
+// mochitest-chrome specific test initialization here. If you need shared
+// functions or initialization that are not specific to mochitest-chrome,
+// consider adding them to "head_common.js" in the parent folder instead.
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
+ "resource://gre/modules/DownloadPaths.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FormAutofill",
+ "resource://gre/modules/FormAutofill.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+
+/* --- Global helpers --- */
+
+// Some of these functions are already implemented in other parts of the source
+// tree, see bug 946708 about sharing more code.
+
+var TestUtils = {
+ /**
+ * Waits for at least one tick of the event loop. This means that all pending
+ * events at the time of this call will have been processed. Other events may
+ * be processed before the returned promise is resolved.
+ *
+ * @return {Promise}
+ * @resolves When pending events have been processed.
+ * @rejects Never.
+ */
+ waitForTick: function () {
+ return new Promise(resolve => executeSoon(resolve));
+ },
+
+ /**
+ * Waits for the specified timeout.
+ *
+ * @param aTimeMs
+ * Minimum time to wait from the moment of this call, in milliseconds.
+ * The actual wait may be longer, due to system timer resolution and
+ * pending events being processed before the promise is resolved.
+ *
+ * @return {Promise}
+ * @resolves When the specified time has passed.
+ * @rejects Never.
+ */
+ waitMs: function (aTimeMs) {
+ return new Promise(resolve => setTimeout(resolve, aTimeMs));
+ },
+
+ /**
+ * Allows waiting for an observer notification once.
+ *
+ * @param aTopic
+ * Notification topic to observe.
+ *
+ * @return {Promise}
+ * @resolves The array [aSubject, aData] from the observed notification.
+ * @rejects Never.
+ */
+ waitForNotification: function (aTopic) {
+ Output.print("Waiting for notification: '" + aTopic + "'.");
+
+ return new Promise(resolve => Services.obs.addObserver(
+ function observe(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(observe, aTopic);
+ resolve([aSubject, aData]);
+ }, aTopic, false));
+ },
+
+ /**
+ * Waits for a DOM event on the specified target.
+ *
+ * @param aTarget
+ * The DOM EventTarget on which addEventListener should be called.
+ * @param aEventName
+ * String with the name of the event.
+ * @param aUseCapture
+ * This parameter is passed to the addEventListener call.
+ *
+ * @return {Promise}
+ * @resolves The arguments from the observed event.
+ * @rejects Never.
+ */
+ waitForEvent: function (aTarget, aEventName, aUseCapture = false) {
+ Output.print("Waiting for event: '" + aEventName + "' on " + aTarget + ".");
+
+ return new Promise(resolve => aTarget.addEventListener(aEventName,
+ function onEvent(...aArgs) {
+ aTarget.removeEventListener(aEventName, onEvent, aUseCapture);
+ resolve(...aArgs);
+ }, aUseCapture));
+ },
+
+ // While the previous test file should have deleted all the temporary files it
+ // used, on Windows these might still be pending deletion on the physical file
+ // system. Thus, start from a new base number every time, to make a collision
+ // with a file that is still pending deletion highly unlikely.
+ _fileCounter: Math.floor(Math.random() * 1000000),
+
+ /**
+ * Returns a reference to a temporary file, that is guaranteed not to exist,
+ * and to have never been created before.
+ *
+ * @param aLeafName
+ * Suggested leaf name for the file to be created.
+ *
+ * @return {Promise}
+ * @resolves Path of a non-existent file in a temporary directory.
+ *
+ * @note It is not enough to delete the file if it exists, or to delete the
+ * file after calling nsIFile.createUnique, because on Windows the
+ * delete operation in the file system may still be pending, preventing
+ * a new file with the same name to be created.
+ */
+ getTempFile: Task.async(function* (aLeafName) {
+ // Prepend a serial number to the extension in the suggested leaf name.
+ let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
+ let leafName = base + "-" + this._fileCounter + ext;
+ this._fileCounter++;
+
+ // Get a file reference under the temporary directory for this test file.
+ let path = OS.Path.join(OS.Constants.Path.tmpDir, leafName);
+ Assert.ok(!(yield OS.File.exists(path)));
+
+ // Ensure the file is deleted whe the test terminates.
+ add_termination_task(function* () {
+ if (yield OS.File.exists(path)) {
+ yield OS.File.remove(path);
+ }
+ });
+
+ return path;
+ }),
+};
+
+/* --- Local helpers --- */
+
+var FormAutofillTest = {
+ /**
+ * Stores the response that the next call to the mock requestAutocomplete UI
+ * will return to the requester, or null to enable displaying the default UI.
+ */
+ requestAutocompleteResponse: null,
+
+ /**
+ * Displays the requestAutocomplete user interface using the specified data.
+ *
+ * @param aFormAutofillData
+ * Serializable object containing the set of requested fields.
+ *
+ * @return {Promise}
+ * @resolves An object with the following properties:
+ * {
+ * uiWindow: Reference to the initialized window.
+ * promiseResult: Promise resolved by the UI when it closes.
+ * }
+ */
+ 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(
+ aFormAutofillData);
+ let promiseResult = ui.show();
+
+ // The window is the subject of the observer notification.
+ return {
+ uiWindow: (yield promiseUIWindow)[0],
+ promiseResult: promiseResult,
+ };
+ }),
+};
+
+var 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_in_parent_process(function* () {
+ // If required, we return a mock response instead of displaying the UI.
+ let mockIntegrationFn = base => ({
+ createRequestAutocompleteUI: Task.async(function* () {
+ // Call the base method to display the UI if override is not requested.
+ if (FormAutofillTest.requestAutocompleteResponse === null) {
+ return yield base.createRequestAutocompleteUI.apply(this, arguments);
+ }
+
+ // Return a mock RequestAutocompleteUI object.
+ return {
+ show: Task.async(function* () {
+ let response = FormAutofillTest.requestAutocompleteResponse;
+ Output.print("Mock UI response: " + JSON.stringify(response));
+ return response;
+ }),
+ };
+ }),
+ });
+
+ FormAutofill.registerIntegration(mockIntegrationFn);
+ add_termination_task(function* () {
+ FormAutofill.unregisterIntegration(mockIntegrationFn);
+ });
+});
+
+add_task_in_both_processes(function* () {
+ // 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");
+ });
+});
diff --git a/toolkit/components/formautofill/test/loader_common.js b/toolkit/components/formautofill/test/loader_common.js
new file mode 100644
index 000000000..340586b65
--- /dev/null
+++ b/toolkit/components/formautofill/test/loader_common.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Infrastructure common to the test frameworks located in subfolders.
+ *
+ * A copy of this file is installed in each of the framework subfolders, this
+ * means it becomes a sibling of the test files in the final layout. This is
+ * determined by how manifest "support-files" installation works.
+ *
+ * Unless you are adding new features to the framework, you shouldn't have to
+ * modify this file. Use "head_common.js" or the "head.js" file of each
+ * framework for shared code.
+ */
+
+"use strict";
+
+/*
+ * --------------------
+ * FRAMEWORK OVERVIEW
+ * --------------------
+ *
+ * This framework is designed in such a way that test can be written in similar
+ * ways in the xpcshell, mochitest-chrome, and mochitest-browser frameworks,
+ * both when tests are running in the parent process or in a content process.
+ *
+ * There are some basic self-documenting assertion and output functions:
+ *
+ * Assert.ok(actualValue);
+ * Assert.is(actualValue, expectedValue);
+ * Output.print(string);
+ *
+ * Test cases and initialization functions are declared in shared head files
+ * ("head_common.js" and "head.js") as well as individual test files. When
+ * tests run in an Elecrolysis (e10s) environment, they are executed in both
+ * processes at first. Normally, at this point only the registration of test
+ * cases happen. When everything has finished loading, tests are started and
+ * appropriately synchronized between processes.
+ *
+ * Tests can be declared using the add_task syntax:
+ *
+ * add_task(function* test_something () { ... });
+ * This adds a test either in the parent process or child process:
+ * - Parent: xpcshell, mochitest-chrome --disable-e10s, mochitest-browser
+ * - Child: mochitest-chrome with e10s
+ * In the future, these might run in the child process for "xpcshell".
+ *
+ * add_task_in_parent_process(function* test_something () { ... });
+ * This test runs in the parent process, but the child process will wait for
+ * its completion before continuing with the next task. This wait currently
+ * happens only in mochitest-chrome with e10s, in other frameworks that run
+ * only in the parent process this is the same as a normal add_task.
+ *
+ * add_task_in_child_process(function* test_something () { ... });
+ * This test runs only in the child process. This means that the test is not
+ * run unless this is an e10s test, currently mochitest-chrome with e10s.
+ *
+ * add_task_in_both_processes(function* test_something () { ... });
+ * Useful for initialization that must be done both in the parent and the
+ * child, like setting preferences.
+ *
+ * add_termination_task(function* () { ... });
+ * Registers a new asynchronous termination task. This is executed after all
+ * test cases in the file finished, and always in the same process where the
+ * termination task is registered.
+ */
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+var gTerminationTasks = [];
+
+/**
+ * None of the testing frameworks support asynchronous termination functions, so
+ * this task must be registered later, after the other "add_task" calls.
+ *
+ * Even xpcshell doesn't support calling "add_task" in the "tail.js" file,
+ * because it registers the task but does not wait for its termination,
+ * potentially leading to intermittent failures in subsequent tests.
+ */
+function* terminationTaskFn() {
+ for (let taskFn of gTerminationTasks) {
+ try {
+ yield Task.spawn(taskFn);
+ } catch (ex) {
+ Output.print(ex);
+ Assert.ok(false);
+ }
+ }
+}
+
+function add_termination_task(taskFn) {
+ gTerminationTasks.push(taskFn);
+}
+
+/**
+ * Returns a unique identifier used for synchronizing the given test task
+ * between the parent and child processes.
+ */
+function getTaskId(stackFrame) {
+ return stackFrame.filename + ":" + stackFrame.lineNumber;
+}
+
+// This is a shared helper for mochitest-chrome and mochitest-browser.
+var _mochitestAssert = {
+ ok: function (actual) {
+ let stack = Components.stack.caller;
+ ok(actual, "[" + stack.name + " : " + stack.lineNumber + "] " + actual +
+ " == true");
+ },
+ equal: function (actual, expected) {
+ let stack = Components.stack.caller;
+ is(actual, expected, "[" + stack.name + " : " + stack.lineNumber + "] " +
+ actual + " == " + expected);
+ },
+};
+
+// Reminder: unless you are adding new features to the framework, you shouldn't
+// have to modify this file. Use "head_common.js" or "head.js" for shared code.
diff --git a/toolkit/components/formautofill/test/xpcshell/.eslintrc.js b/toolkit/components/formautofill/test/xpcshell/.eslintrc.js
new file mode 100644
index 000000000..d35787cd2
--- /dev/null
+++ b/toolkit/components/formautofill/test/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/formautofill/test/xpcshell/head.js b/toolkit/components/formautofill/test/xpcshell/head.js
new file mode 100644
index 000000000..1cee023f2
--- /dev/null
+++ b/toolkit/components/formautofill/test/xpcshell/head.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Initialization specific to Form Autofill xpcshell tests.
+ *
+ * This file is loaded by "loader.js".
+ */
+
+"use strict";
+
+// The testing framework is fully initialized at this point, you can add
+// xpcshell specific test initialization here. If you need shared functions or
+// initialization that are not specific to xpcshell, consider adding them to
+// "head_common.js" in the parent folder instead.
+
+add_task_in_parent_process(function* test_xpcshell_initialize_profile() {
+ // We need to send the profile-after-change notification manually to the
+ // startup component to ensure it has been initialized.
+ Cc["@mozilla.org/formautofill/startup;1"]
+ .getService(Ci.nsIObserver)
+ .observe(null, "profile-after-change", "");
+});
diff --git a/toolkit/components/formautofill/test/xpcshell/loader.js b/toolkit/components/formautofill/test/xpcshell/loader.js
new file mode 100644
index 000000000..449989c8a
--- /dev/null
+++ b/toolkit/components/formautofill/test/xpcshell/loader.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Infrastructure for the xpcshell tests located in this folder.
+ *
+ * See "loader_common.js" in the parent folder for a general overview.
+ *
+ * Unless you are adding new features to the framework, you shouldn't have to
+ * modify this file. Use "head_common.js" or "head.js" for shared code.
+ */
+
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+
+Services.scriptloader.loadSubScript(
+ Services.io.newFileURI(do_get_file("loader_common.js")).spec, this);
+
+// Define output functions so they look the same across all frameworks.
+var Output = {
+ print: do_print,
+};
+
+var executeSoon = do_execute_soon;
+var setTimeout = (fn, delay) => do_timeout(delay, fn);
+
+// Define task registration functions, see description in "loader_common.js".
+var add_task_in_parent_process = add_task;
+var add_task_in_child_process = function () {};
+var add_task_in_both_processes = add_task;
+
+Services.scriptloader.loadSubScript(
+ Services.io.newFileURI(do_get_file("head_common.js")).spec, this);
+
+// Tests are always run asynchronously and with the profile loaded.
+function run_test() {
+ do_get_profile();
+ run_next_test();
+}
+
+// Reminder: unless you are adding new features to the framework, you shouldn't
+// have to modify this file. Use "head_common.js" or "head.js" for shared code.
diff --git a/toolkit/components/formautofill/test/xpcshell/test_infrastructure.js b/toolkit/components/formautofill/test/xpcshell/test_infrastructure.js
new file mode 100644
index 000000000..af27cfdb5
--- /dev/null
+++ b/toolkit/components/formautofill/test/xpcshell/test_infrastructure.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests the local testing infrastructure.
+ */
+
+"use strict";
+
+/**
+ * Tests the truth assertion function.
+ */
+add_task(function* test_assert_truth() {
+ Assert.ok(1 != 2);
+});
+
+/**
+ * Tests the equality assertion function.
+ */
+add_task(function* test_assert_equality() {
+ Assert.equal(1 + 1, 2);
+});
+
+/**
+ * Uses some of the utility functions provided by the framework.
+ */
+add_task(function* test_utility_functions() {
+ // The "print" function is useful to log information that is not known before.
+ let randomString = "R" + Math.floor(Math.random() * 10);
+ Output.print("The random contents will be '" + randomString + "'.");
+
+ // Create the text file with the random contents.
+ let path = yield TestUtils.getTempFile("test-infrastructure.txt");
+ yield OS.File.writeAtomic(path, new TextEncoder().encode(randomString));
+
+ // Test a few utility functions.
+ yield TestUtils.waitForTick();
+ yield TestUtils.waitMs(50);
+
+ let promiseMyNotification = TestUtils.waitForNotification("my-topic");
+ Services.obs.notifyObservers(null, "my-topic", "");
+ yield promiseMyNotification;
+
+ // Check the file size. The file will be deleted automatically later.
+ Assert.equal((yield OS.File.stat(path)).size, randomString.length);
+});
+
+add_task(terminationTaskFn);
diff --git a/toolkit/components/formautofill/test/xpcshell/test_integration.js b/toolkit/components/formautofill/test/xpcshell/test_integration.js
new file mode 100644
index 000000000..7707f3880
--- /dev/null
+++ b/toolkit/components/formautofill/test/xpcshell/test_integration.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests overriding the FormAutofillIntegration module functions.
+ */
+
+"use strict";
+
+/**
+ * The requestAutocomplete UI will not be displayed during these tests.
+ */
+add_task_in_parent_process(function* test_initialize() {
+ FormAutofillTest.requestAutocompleteResponse = { canceled: true };
+});
+
+/**
+ * Registers and unregisters an integration override function.
+ */
+add_task(function* test_integration_override() {
+ let overrideCalled = false;
+
+ let newIntegrationFn = base => ({
+ createRequestAutocompleteUI: Task.async(function* () {
+ overrideCalled = true;
+ return yield base.createRequestAutocompleteUI.apply(this, arguments);
+ }),
+ });
+
+ FormAutofill.registerIntegration(newIntegrationFn);
+ try {
+ let ui = yield FormAutofill.integration.createRequestAutocompleteUI({});
+ let result = yield ui.show();
+ Assert.ok(result.canceled);
+ } finally {
+ FormAutofill.unregisterIntegration(newIntegrationFn);
+ }
+
+ Assert.ok(overrideCalled);
+});
+
+/**
+ * Registers an integration override function that throws an exception, and
+ * ensures that this does not block other functions from being registered.
+ */
+add_task(function* test_integration_override_error() {
+ let overrideCalled = false;
+
+ let errorIntegrationFn = base => { throw "Expected error." };
+
+ let newIntegrationFn = base => ({
+ createRequestAutocompleteUI: Task.async(function* () {
+ overrideCalled = true;
+ return yield base.createRequestAutocompleteUI.apply(this, arguments);
+ }),
+ });
+
+ FormAutofill.registerIntegration(errorIntegrationFn);
+ FormAutofill.registerIntegration(newIntegrationFn);
+ try {
+ let ui = yield FormAutofill.integration.createRequestAutocompleteUI({});
+ let result = yield ui.show();
+ Assert.ok(result.canceled);
+ } finally {
+ FormAutofill.unregisterIntegration(errorIntegrationFn);
+ FormAutofill.unregisterIntegration(newIntegrationFn);
+ }
+
+ Assert.ok(overrideCalled);
+});
+
+add_task(terminationTaskFn);
diff --git a/toolkit/components/formautofill/test/xpcshell/xpcshell.ini b/toolkit/components/formautofill/test/xpcshell/xpcshell.ini
new file mode 100644
index 000000000..711c03399
--- /dev/null
+++ b/toolkit/components/formautofill/test/xpcshell/xpcshell.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+head = loader.js head.js
+tail =
+skip-if = toolkit == 'android'
+# The following files starting with ".." are installed in the current folder.
+# However, they cannot be referenced directly in the "head" directive above.
+support-files =
+ ../head_common.js
+ ../loader_common.js
+
+[test_infrastructure.js]
+[test_integration.js]