diff options
Diffstat (limited to 'toolkit/components/formautofill')
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] |