summaryrefslogtreecommitdiffstats
path: root/browser/extensions/formautofill
diff options
context:
space:
mode:
Diffstat (limited to 'browser/extensions/formautofill')
-rw-r--r--browser/extensions/formautofill/.eslintrc.js474
-rw-r--r--browser/extensions/formautofill/bootstrap.js12
-rw-r--r--browser/extensions/formautofill/content/FormAutofillContent.jsm134
-rw-r--r--browser/extensions/formautofill/content/FormAutofillParent.jsm173
-rw-r--r--browser/extensions/formautofill/content/ProfileStorage.jsm251
-rw-r--r--browser/extensions/formautofill/install.rdf.in32
-rw-r--r--browser/extensions/formautofill/jar.mn7
-rw-r--r--browser/extensions/formautofill/moz.build22
-rw-r--r--browser/extensions/formautofill/test/browser/.eslintrc.js7
-rw-r--r--browser/extensions/formautofill/test/browser/browser.ini3
-rw-r--r--browser/extensions/formautofill/test/browser/browser_check_installed.js14
-rw-r--r--browser/extensions/formautofill/test/unit/.eslintrc5
-rw-r--r--browser/extensions/formautofill/test/unit/head.js81
-rw-r--r--browser/extensions/formautofill/test/unit/test_autofillFormFields.js200
-rw-r--r--browser/extensions/formautofill/test/unit/test_collectFormFields.js122
-rw-r--r--browser/extensions/formautofill/test/unit/test_populateFieldValues.js106
-rw-r--r--browser/extensions/formautofill/test/unit/test_profileStorage.js222
-rw-r--r--browser/extensions/formautofill/test/unit/xpcshell.ini12
18 files changed, 1877 insertions, 0 deletions
diff --git a/browser/extensions/formautofill/.eslintrc.js b/browser/extensions/formautofill/.eslintrc.js
new file mode 100644
index 000000000..ec89029e5
--- /dev/null
+++ b/browser/extensions/formautofill/.eslintrc.js
@@ -0,0 +1,474 @@
+"use strict";
+
+module.exports = { // eslint-disable-line no-undef
+ "extends": "../../.eslintrc.js",
+
+ "globals": {
+ "Components": true,
+ "dump": true,
+ "TextDecoder": false,
+ "TextEncoder": false,
+ },
+
+ "rules": {
+ // Rules from the mozilla plugin
+ "mozilla/balanced-listeners": "error",
+ "mozilla/no-aArgs": "warn",
+ "mozilla/no-cpows-in-tests": "warn",
+ "mozilla/var-only-at-top-level": "warn",
+
+ "valid-jsdoc": ["error", {
+ "prefer": {
+ "return": "returns",
+ },
+ "preferType": {
+ "Boolean": "boolean",
+ "Number": "number",
+ "String": "string",
+ "bool": "boolean",
+ },
+ "requireParamDescription": false,
+ "requireReturn": false,
+ "requireReturnDescription": false,
+ }],
+
+ // Braces only needed for multi-line arrow function blocks
+ // "arrow-body-style": ["error", "as-needed"],
+
+ // Require spacing around =>
+ "arrow-spacing": "error",
+
+ // Always require spacing around a single line block
+ "block-spacing": "warn",
+
+ // Forbid spaces inside the square brackets of array literals.
+ "array-bracket-spacing": ["error", "never"],
+
+ // Forbid spaces inside the curly brackets of object literals.
+ "object-curly-spacing": ["error", "never"],
+
+ // No space padding in parentheses
+ "space-in-parens": ["error", "never"],
+
+ // Enforce one true brace style (opening brace on the same line) and avoid
+ // start and end braces on the same line.
+ "brace-style": ["error", "1tbs", {"allowSingleLine": true}],
+
+ // No space before always a space after a comma
+ "comma-spacing": ["error", {"before": false, "after": true}],
+
+ // Commas at the end of the line not the start
+ "comma-style": "error",
+
+ // Don't require spaces around computed properties
+ "computed-property-spacing": ["warn", "never"],
+
+ // Functions are not required to consistently return something or nothing
+ "consistent-return": "off",
+
+ // Require braces around blocks that start a new line
+ "curly": ["error", "all"],
+
+ // Always require a trailing EOL
+ "eol-last": "error",
+
+ // Require function* name()
+ "generator-star-spacing": ["error", {"before": false, "after": true}],
+
+ // Two space indent
+ "indent": ["error", 2, {"SwitchCase": 1}],
+
+ // Space after colon not before in property declarations
+ "key-spacing": ["error", {"beforeColon": false, "afterColon": true, "mode": "minimum"}],
+
+ // Require spaces before and after finally, catch, etc.
+ "keyword-spacing": "error",
+
+ // Unix linebreaks
+ "linebreak-style": ["error", "unix"],
+
+ // Always require parenthesis for new calls
+ "new-parens": "error",
+
+ // Use [] instead of Array()
+ "no-array-constructor": "error",
+
+ // No duplicate arguments in function declarations
+ "no-dupe-args": "error",
+
+ // No duplicate keys in object declarations
+ "no-dupe-keys": "error",
+
+ // No duplicate cases in switch statements
+ "no-duplicate-case": "error",
+
+ // If an if block ends with a return no need for an else block
+ // "no-else-return": "error",
+
+ // Disallow empty statements. This will report an error for:
+ // try { something(); } catch (e) {}
+ // but will not report it for:
+ // try { something(); } catch (e) { /* Silencing the error because ...*/ }
+ // which is a valid use case.
+ "no-empty": "error",
+
+ // No empty character classes in regex
+ "no-empty-character-class": "error",
+
+ // Disallow empty destructuring
+ "no-empty-pattern": "error",
+
+ // No assiging to exception variable
+ "no-ex-assign": "error",
+
+ // No using !! where casting to boolean is already happening
+ "no-extra-boolean-cast": "warn",
+
+ // No double semicolon
+ "no-extra-semi": "error",
+
+ // No overwriting defined functions
+ "no-func-assign": "error",
+
+ // No invalid regular expresions
+ "no-invalid-regexp": "error",
+
+ // No odd whitespace characters
+ "no-irregular-whitespace": "error",
+
+ // No single if block inside an else block
+ "no-lonely-if": "warn",
+
+ // No mixing spaces and tabs in indent
+ "no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
+
+ // Disallow use of multiple spaces (sometimes used to align const values,
+ // array or object items, etc.). It's hard to maintain and doesn't add that
+ // much benefit.
+ "no-multi-spaces": "warn",
+
+ // No reassigning native JS objects
+ "no-native-reassign": "error",
+
+ // No (!foo in bar)
+ "no-negated-in-lhs": "error",
+
+ // Nested ternary statements are confusing
+ "no-nested-ternary": "error",
+
+ // Use {} instead of new Object()
+ "no-new-object": "error",
+
+ // No Math() or JSON()
+ "no-obj-calls": "error",
+
+ // No octal literals
+ "no-octal": "error",
+
+ // No redeclaring variables
+ "no-redeclare": "error",
+
+ // No unnecessary comparisons
+ "no-self-compare": "error",
+
+ // No spaces between function name and parentheses
+ "no-spaced-func": "warn",
+
+ // No trailing whitespace
+ "no-trailing-spaces": "error",
+
+ // Error on newline where a semicolon is needed
+ "no-unexpected-multiline": "error",
+
+ // No unreachable statements
+ "no-unreachable": "error",
+
+ // No expressions where a statement is expected
+ "no-unused-expressions": "error",
+
+ // No declaring variables that are never used
+ "no-unused-vars": ["error", {"args": "none", "varsIgnorePattern": "^(Cc|Ci|Cr|Cu|EXPORTED_SYMBOLS)$"}],
+
+ // No using variables before defined
+ "no-use-before-define": "error",
+
+ // No using with
+ "no-with": "error",
+
+ // Always require semicolon at end of statement
+ "semi": ["error", "always"],
+
+ // Require space before blocks
+ "space-before-blocks": "error",
+
+ // Never use spaces before function parentheses
+ "space-before-function-paren": ["error", {"anonymous": "never", "named": "never"}],
+
+ // Require spaces around operators, except for a|"off".
+ "space-infix-ops": ["error", {"int32Hint": true}],
+
+ // ++ and -- should not need spacing
+ "space-unary-ops": ["warn", {"nonwords": false}],
+
+ // No comparisons to NaN
+ "use-isnan": "error",
+
+ // Only check typeof against valid results
+ "valid-typeof": "error",
+
+ // Disallow using variables outside the blocks they are defined (especially
+ // since only let and const are used, see "no-var").
+ "block-scoped-var": "error",
+
+ // Allow trailing commas for easy list extension. Having them does not
+ // impair readability, but also not required either.
+ "comma-dangle": ["error", "always-multiline"],
+
+ // Warn about cyclomatic complexity in functions.
+ "complexity": "warn",
+
+ // Don't warn for inconsistent naming when capturing this (not so important
+ // with auto-binding fat arrow functions).
+ // "consistent-this": ["error", "self"],
+
+ // Don't require a default case in switch statements. Avoid being forced to
+ // add a bogus default when you know all possible cases are handled.
+ "default-case": "off",
+
+ // Enforce dots on the next line with property name.
+ "dot-location": ["error", "property"],
+
+ // Encourage the use of dot notation whenever possible.
+ "dot-notation": "error",
+
+ // Allow using == instead of ===, in the interest of landing something since
+ // the devtools codebase is split on convention here.
+ "eqeqeq": "off",
+
+ // Don't require function expressions to have a name.
+ // This makes the code more verbose and hard to read. Our engine already
+ // does a fantastic job assigning a name to the function, which includes
+ // the enclosing function name, and worst case you have a line number that
+ // you can just look up.
+ "func-names": "off",
+
+ // Allow use of function declarations and expressions.
+ "func-style": "off",
+
+ // Don't enforce the maximum depth that blocks can be nested. The complexity
+ // rule is a better rule to check this.
+ "max-depth": "off",
+
+ // Maximum length of a line.
+ // Disabled because we exceed this in too many places.
+ "max-len": ["off", 80],
+
+ // Maximum depth callbacks can be nested.
+ "max-nested-callbacks": ["error", 4],
+
+ // Don't limit the number of parameters that can be used in a function.
+ "max-params": "off",
+
+ // Don't limit the maximum number of statement allowed in a function. We
+ // already have the complexity rule that's a better measurement.
+ "max-statements": "off",
+
+ // Don't require a capital letter for constructors, only check if all new
+ // operators are followed by a capital letter. Don't warn when capitalized
+ // functions are used without the new operator.
+ "new-cap": ["off", {"capIsNew": false}],
+
+ // Allow use of bitwise operators.
+ "no-bitwise": "off",
+
+ // Disallow use of arguments.caller or arguments.callee.
+ "no-caller": "error",
+
+ // Disallow the catch clause parameter name being the same as a variable in
+ // the outer scope, to avoid confusion.
+ "no-catch-shadow": "off",
+
+ // Disallow assignment in conditional expressions.
+ "no-cond-assign": "error",
+
+ // Disallow using the console API.
+ "no-console": "error",
+
+ // Allow using constant expressions in conditions like while (true)
+ "no-constant-condition": "off",
+
+ // Allow use of the continue statement.
+ "no-continue": "off",
+
+ // Disallow control characters in regular expressions.
+ "no-control-regex": "error",
+
+ // Disallow use of debugger.
+ "no-debugger": "error",
+
+ // Disallow deletion of variables (deleting properties is fine).
+ "no-delete-var": "error",
+
+ // Allow division operators explicitly at beginning of regular expression.
+ "no-div-regex": "off",
+
+ // Disallow use of eval(). We have other APIs to evaluate code in content.
+ "no-eval": "error",
+
+ // Disallow adding to native types
+ "no-extend-native": "error",
+
+ // Disallow unnecessary function binding.
+ "no-extra-bind": "error",
+
+ // Allow unnecessary parentheses, as they may make the code more readable.
+ "no-extra-parens": "off",
+
+ // Disallow fallthrough of case statements, except if there is a comment.
+ "no-fallthrough": "error",
+
+ // Allow the use of leading or trailing decimal points in numeric literals.
+ "no-floating-decimal": "off",
+
+ // Allow comments inline after code.
+ "no-inline-comments": "off",
+
+ // Disallow use of labels for anything other then loops and switches.
+ "no-labels": ["error", {"allowLoop": true}],
+
+ // Disallow use of multiline strings (use template strings instead).
+ "no-multi-str": "warn",
+
+ // Disallow multiple empty lines.
+ "no-multiple-empty-lines": ["warn", {"max": 2}],
+
+ // Allow reassignment of function parameters.
+ "no-param-reassign": "off",
+
+ // Allow string concatenation with __dirname and __filename (not a node env).
+ "no-path-concat": "off",
+
+ // Allow use of unary operators, ++ and --.
+ "no-plusplus": "off",
+
+ // Allow using process.env (not a node environment).
+ "no-process-env": "off",
+
+ // Allow using process.exit (not a node environment).
+ "no-process-exit": "off",
+
+ // Disallow usage of __proto__ property.
+ "no-proto": "error",
+
+ // Disallow multiple spaces in a regular expression literal.
+ "no-regex-spaces": "error",
+
+ // Allow reserved words being used as object literal keys.
+ "no-reserved-keys": "off",
+
+ // Don't restrict usage of specified node modules (not a node environment).
+ "no-restricted-modules": "off",
+
+ // Disallow use of assignment in return statement. It is preferable for a
+ // single line of code to have only one easily predictable effect.
+ "no-return-assign": "error",
+
+ // Don't warn about declaration of variables already declared in the outer scope.
+ "no-shadow": "off",
+
+ // Disallow shadowing of names such as arguments.
+ "no-shadow-restricted-names": "error",
+
+ // Allow use of synchronous methods (not a node environment).
+ "no-sync": "off",
+
+ // Allow the use of ternary operators.
+ "no-ternary": "off",
+
+ // Disallow throwing literals (eg. throw "error" instead of
+ // throw new Error("error")).
+ "no-throw-literal": "error",
+
+ // Disallow use of undeclared variables unless mentioned in a /* global */
+ // block. Note that globals from head.js are automatically imported in tests
+ // by the import-headjs-globals rule form the mozilla eslint plugin.
+ "no-undef": "error",
+
+ // Allow dangling underscores in identifiers (for privates).
+ "no-underscore-dangle": "off",
+
+ // Allow use of undefined variable.
+ "no-undefined": "off",
+
+ // Disallow the use of Boolean literals in conditional expressions.
+ "no-unneeded-ternary": "error",
+
+ // We use var-only-at-top-level instead of no-var as we allow top level
+ // vars.
+ "no-var": "off",
+
+ // Allow using TODO/FIXME comments.
+ "no-warning-comments": "off",
+
+ // Don't require method and property shorthand syntax for object literals.
+ // We use this in the code a lot, but not consistently, and this seems more
+ // like something to check at code review time.
+ "object-shorthand": "off",
+
+ // Allow more than one variable declaration per function.
+ "one-var": "off",
+
+ // Disallow padding within blocks.
+ "padded-blocks": ["warn", "never"],
+
+ // Don't require quotes around object literal property names.
+ "quote-props": "off",
+
+ // Double quotes should be used.
+ "quotes": ["warn", "double", {"avoidEscape": true, "allowTemplateLiterals": true}],
+
+ // Require use of the second argument for parseInt().
+ "radix": "error",
+
+ // Enforce spacing after semicolons.
+ "semi-spacing": ["error", {"before": false, "after": true}],
+
+ // Don't require to sort variables within the same declaration block.
+ // Anyway, one-var is disabled.
+ "sort-vars": "off",
+
+ // Require a space immediately following the // in a line comment.
+ "spaced-comment": ["error", "always"],
+
+ // Require "use strict" to be defined globally in the script.
+ "strict": ["error", "global"],
+
+ // Allow vars to be declared anywhere in the scope.
+ "vars-on-top": "off",
+
+ // Don't require immediate function invocation to be wrapped in parentheses.
+ "wrap-iife": "off",
+
+ // Don't require regex literals to be wrapped in parentheses (which
+ // supposedly prevent them from being mistaken for division operators).
+ "wrap-regex": "off",
+
+ // Disallow Yoda conditions (where literal value comes first).
+ "yoda": "error",
+
+ // disallow use of eval()-like methods
+ "no-implied-eval": "error",
+
+ // Disallow function or variable declarations in nested blocks
+ "no-inner-declarations": "error",
+
+ // Disallow usage of __iterator__ property
+ "no-iterator": "error",
+
+ // Disallow labels that share a name with a variable
+ "no-label-var": "error",
+
+ // Disallow creating new instances of String, Number, and Boolean
+ "no-new-wrappers": "error",
+ },
+};
diff --git a/browser/extensions/formautofill/bootstrap.js b/browser/extensions/formautofill/bootstrap.js
new file mode 100644
index 000000000..0b3f355bd
--- /dev/null
+++ b/browser/extensions/formautofill/bootstrap.js
@@ -0,0 +1,12 @@
+/* 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/. */
+
+"use strict";
+
+/* exported startup, shutdown, install, uninstall */
+
+function startup() {}
+function shutdown() {}
+function install() {}
+function uninstall() {}
diff --git a/browser/extensions/formautofill/content/FormAutofillContent.jsm b/browser/extensions/formautofill/content/FormAutofillContent.jsm
new file mode 100644
index 000000000..bde397580
--- /dev/null
+++ b/browser/extensions/formautofill/content/FormAutofillContent.jsm
@@ -0,0 +1,134 @@
+/* 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.
+ */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+/**
+ * Handles profile autofill for a DOM Form element.
+ * @param {HTMLFormElement} form Form that need to be auto filled
+ */
+function FormAutofillHandler(form) {
+ this.form = form;
+ this.fieldDetails = [];
+}
+
+FormAutofillHandler.prototype = {
+ /**
+ * DOM Form element to which this object is attached.
+ */
+ form: 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 backend. 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,
+
+ /**
+ * Returns information from the form about fields that can be autofilled, and
+ * populates the fieldDetails array on this object accordingly.
+ *
+ * @returns {Array<Object>} 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() {
+ let autofillData = [];
+
+ 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"].includes(info.fieldName)) {
+ 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;
+ }
+
+ let inputFormat = {
+ section: info.section,
+ addressType: info.addressType,
+ contactType: info.contactType,
+ fieldName: info.fieldName,
+ };
+ // Clone the inputFormat for caching the fields and elements together
+ let formatWithElement = Object.assign({}, inputFormat);
+
+ inputFormat.index = autofillData.length;
+ autofillData.push(inputFormat);
+
+ formatWithElement.element = element;
+ this.fieldDetails.push(formatWithElement);
+ }
+
+ return autofillData;
+ },
+
+ /**
+ * Processes form fields that can be autofilled, and populates them with the
+ * data provided by backend.
+ *
+ * @param {Array<Object>} autofillResult
+ * Data returned by the user interface.
+ * [{
+ * 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.
+ * index: Index to match the input in fieldDetails
+ * }],
+ * }
+ */
+ autofillFormFields(autofillResult) {
+ for (let field of autofillResult) {
+ // Get the field details, if it was processed by the user interface.
+ let fieldDetail = this.fieldDetails[field.index];
+
+ // Avoid the invalid value set
+ if (!fieldDetail || !field.value) {
+ continue;
+ }
+
+ let info = fieldDetail.element.getAutocompleteInfo();
+ if (field.section != info.section ||
+ field.addressType != info.addressType ||
+ field.contactType != info.contactType ||
+ field.fieldName != info.fieldName) {
+ Cu.reportError("Autocomplete tokens mismatched");
+ continue;
+ }
+
+ fieldDetail.element.setUserInput(field.value);
+ }
+ },
+};
+
+this.EXPORTED_SYMBOLS = ["FormAutofillHandler"];
diff --git a/browser/extensions/formautofill/content/FormAutofillParent.jsm b/browser/extensions/formautofill/content/FormAutofillParent.jsm
new file mode 100644
index 000000000..bdfe0f478
--- /dev/null
+++ b/browser/extensions/formautofill/content/FormAutofillParent.jsm
@@ -0,0 +1,173 @@
+/* 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 to access storage and communicate with content.
+ *
+ * A "fields" array is used to communicate with FormAutofillContent. Each item
+ * represents a single input field in the content page as well as its
+ * @autocomplete properties. The schema is as below. Please refer to
+ * FormAutofillContent.jsm for more details.
+ *
+ * [
+ * {
+ * section,
+ * addressType,
+ * contactType,
+ * fieldName,
+ * value,
+ * index
+ * },
+ * {
+ * // ...
+ * }
+ * ]
+ */
+
+/* exported FormAutofillParent */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ProfileStorage",
+ "resource://formautofill/ProfileStorage.jsm");
+
+const PROFILE_JSON_FILE_NAME = "autofill-profiles.json";
+
+let FormAutofillParent = {
+ _profileStore: null,
+
+ /**
+ * Initializes ProfileStorage and registers the message handler.
+ */
+ init: function() {
+ let storePath =
+ OS.Path.join(OS.Constants.Path.profileDir, PROFILE_JSON_FILE_NAME);
+
+ this._profileStore = new ProfileStorage(storePath);
+ this._profileStore.initialize();
+
+ let mm = Cc["@mozilla.org/globalmessagemanager;1"]
+ .getService(Ci.nsIMessageListenerManager);
+ mm.addMessageListener("FormAutofill:PopulateFieldValues", this);
+ },
+
+ /**
+ * Handles the message coming from FormAutofillContent.
+ *
+ * @param {string} message.name The name of the message.
+ * @param {object} message.data The data of the message.
+ * @param {nsIFrameMessageManager} message.target Caller's message manager.
+ */
+ receiveMessage: function({name, data, target}) {
+ switch (name) {
+ case "FormAutofill:PopulateFieldValues":
+ this._populateFieldValues(data, target);
+ break;
+ }
+ },
+
+ /**
+ * Returns the instance of ProfileStorage. To avoid syncing issues, anyone
+ * who needs to access the profile should request the instance by this instead
+ * of creating a new one.
+ *
+ * @returns {ProfileStorage}
+ */
+ getProfileStore: function() {
+ return this._profileStore;
+ },
+
+ /**
+ * Uninitializes FormAutofillParent. This is for testing only.
+ *
+ * @private
+ */
+ _uninit: function() {
+ if (this._profileStore) {
+ this._profileStore._saveImmediately();
+ this._profileStore = null;
+ }
+
+ let mm = Cc["@mozilla.org/globalmessagemanager;1"]
+ .getService(Ci.nsIMessageListenerManager);
+ mm.removeMessageListener("FormAutofill:PopulateFieldValues", this);
+ },
+
+ /**
+ * Populates the field values and notifies content to fill in. Exception will
+ * be thrown if there's no matching profile.
+ *
+ * @private
+ * @param {string} data.guid
+ * Indicates which profile to populate
+ * @param {Fields} data.fields
+ * The "fields" array collected from content.
+ * @param {nsIFrameMessageManager} target
+ * Content's message manager.
+ */
+ _populateFieldValues({guid, fields}, target) {
+ this._profileStore.notifyUsed(guid);
+ this._fillInFields(this._profileStore.get(guid), fields);
+ target.sendAsyncMessage("FormAutofill:fillForm", {fields});
+ },
+
+ /**
+ * Transforms a word with hyphen into camel case.
+ * (e.g. transforms "address-type" into "addressType".)
+ *
+ * @private
+ * @param {string} str The original string with hyphen.
+ * @returns {string} The camel-cased output string.
+ */
+ _camelCase(str) {
+ return str.toLowerCase().replace(/-([a-z])/g, s => s[1].toUpperCase());
+ },
+
+ /**
+ * Get the corresponding value from the specified profile according to a valid
+ * @autocomplete field name.
+ *
+ * Note that the field name doesn't need to match the property name defined in
+ * Profile object. This method can transform the raw data to fulfill it. (e.g.
+ * inputting "country-name" as "fieldName" will get a full name transformed
+ * from the country code that is recorded in "country" field.)
+ *
+ * @private
+ * @param {Profile} profile The specified profile.
+ * @param {string} fieldName A valid @autocomplete field name.
+ * @returns {string} The corresponding value. Returns "undefined" if there's
+ * no matching field.
+ */
+ _getDataByFieldName(profile, fieldName) {
+ let key = this._camelCase(fieldName);
+
+ // TODO: Transform the raw profile data to fulfill "fieldName" here.
+
+ return profile[key];
+ },
+
+ /**
+ * Fills in the "fields" array by the specified profile.
+ *
+ * @private
+ * @param {Profile} profile The specified profile to fill in.
+ * @param {Fields} fields The "fields" array collected from content.
+ */
+ _fillInFields(profile, fields) {
+ for (let field of fields) {
+ let value = this._getDataByFieldName(profile, field.fieldName);
+ if (value !== undefined) {
+ field.value = value;
+ }
+ }
+ },
+};
+
+this.EXPORTED_SYMBOLS = ["FormAutofillParent"];
diff --git a/browser/extensions/formautofill/content/ProfileStorage.jsm b/browser/extensions/formautofill/content/ProfileStorage.jsm
new file mode 100644
index 000000000..843177d4e
--- /dev/null
+++ b/browser/extensions/formautofill/content/ProfileStorage.jsm
@@ -0,0 +1,251 @@
+/* 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 an interface of the storage of Form Autofill.
+ *
+ * The data is stored in JSON format, without indentation, using UTF-8 encoding.
+ * With indentation applied, the file would look like this:
+ *
+ * {
+ * version: 1,
+ * profiles: [
+ * {
+ * guid, // 12 character...
+ *
+ * // profile
+ * organization, // Company
+ * streetAddress, // (Multiline)
+ * addressLevel2, // City/Town
+ * addressLevel1, // Province (Standardized code if possible)
+ * postalCode,
+ * country, // ISO 3166
+ * tel,
+ * email,
+ *
+ * // metadata
+ * timeCreated, // in ms
+ * timeLastUsed, // in ms
+ * timeLastModified, // in ms
+ * timesUsed
+ * },
+ * {
+ * // ...
+ * }
+ * ]
+ * }
+ */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "JSONFile",
+ "resource://gre/modules/JSONFile.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+const SCHEMA_VERSION = 1;
+
+// Name-related fields will be handled in follow-up bugs due to the complexity.
+const VALID_FIELDS = [
+ "organization",
+ "streetAddress",
+ "addressLevel2",
+ "addressLevel1",
+ "postalCode",
+ "country",
+ "tel",
+ "email",
+];
+
+function ProfileStorage(path) {
+ this._path = path;
+}
+
+ProfileStorage.prototype = {
+ /**
+ * Loads the profile data from file to memory.
+ *
+ * @returns {Promise}
+ * @resolves When the operation finished successfully.
+ * @rejects JavaScript exception.
+ */
+ initialize() {
+ this._store = new JSONFile({
+ path: this._path,
+ dataPostProcessor: this._dataPostProcessor.bind(this),
+ });
+ return this._store.load();
+ },
+
+ /**
+ * Adds a new profile.
+ *
+ * @param {Profile} profile
+ * The new profile for saving.
+ */
+ add(profile) {
+ this._store.ensureDataReady();
+
+ let profileToSave = this._normalizeProfile(profile);
+
+ profileToSave.guid = gUUIDGenerator.generateUUID().toString()
+ .replace(/[{}-]/g, "").substring(0, 12);
+
+ // Metadata
+ let now = Date.now();
+ profileToSave.timeCreated = now;
+ profileToSave.timeLastModified = now;
+ profileToSave.timeLastUsed = 0;
+ profileToSave.timesUsed = 0;
+
+ this._store.data.profiles.push(profileToSave);
+
+ this._store.saveSoon();
+ },
+
+ /**
+ * Update the specified profile.
+ *
+ * @param {string} guid
+ * Indicates which profile to update.
+ * @param {Profile} profile
+ * The new profile used to overwrite the old one.
+ */
+ update(guid, profile) {
+ this._store.ensureDataReady();
+
+ let profileFound = this._findByGUID(guid);
+ if (!profileFound) {
+ throw new Error("No matching profile.");
+ }
+
+ let profileToUpdate = this._normalizeProfile(profile);
+ for (let field of VALID_FIELDS) {
+ if (profileToUpdate[field] !== undefined) {
+ profileFound[field] = profileToUpdate[field];
+ } else {
+ delete profileFound[field];
+ }
+ }
+
+ profileFound.timeLastModified = Date.now();
+
+ this._store.saveSoon();
+ },
+
+ /**
+ * Notifies the stroage of the use of the specified profile, so we can update
+ * the metadata accordingly.
+ *
+ * @param {string} guid
+ * Indicates which profile to be notified.
+ */
+ notifyUsed(guid) {
+ this._store.ensureDataReady();
+
+ let profileFound = this._findByGUID(guid);
+ if (!profileFound) {
+ throw new Error("No matching profile.");
+ }
+
+ profileFound.timesUsed++;
+ profileFound.timeLastUsed = Date.now();
+
+ this._store.saveSoon();
+ },
+
+ /**
+ * Removes the specified profile. No error occurs if the profile isn't found.
+ *
+ * @param {string} guid
+ * Indicates which profile to remove.
+ */
+ remove(guid) {
+ this._store.ensureDataReady();
+
+ this._store.data.profiles =
+ this._store.data.profiles.filter(profile => profile.guid != guid);
+ this._store.saveSoon();
+ },
+
+ /**
+ * Returns the profile with the specified GUID.
+ *
+ * @param {string} guid
+ * Indicates which profile to retrieve.
+ * @returns {Profile}
+ * A clone of the profile.
+ */
+ get(guid) {
+ this._store.ensureDataReady();
+
+ let profileFound = this._findByGUID(guid);
+ if (!profileFound) {
+ throw new Error("No matching profile.");
+ }
+
+ // Profile is cloned to avoid accidental modifications from outside.
+ return this._clone(profileFound);
+ },
+
+ /**
+ * Returns all profiles.
+ *
+ * @returns {Array.<Profile>}
+ * An array containing clones of all profiles.
+ */
+ getAll() {
+ this._store.ensureDataReady();
+
+ // Profiles are cloned to avoid accidental modifications from outside.
+ return this._store.data.profiles.map(this._clone);
+ },
+
+ _clone(profile) {
+ return Object.assign({}, profile);
+ },
+
+ _findByGUID(guid) {
+ return this._store.data.profiles.find(profile => profile.guid == guid);
+ },
+
+ _normalizeProfile(profile) {
+ let result = {};
+ for (let key in profile) {
+ if (!VALID_FIELDS.includes(key)) {
+ throw new Error(`"${key}" is not a valid field.`);
+ }
+ if (typeof profile[key] !== "string" &&
+ typeof profile[key] !== "number") {
+ throw new Error(`"${key}" contains invalid data type.`);
+ }
+
+ result[key] = profile[key];
+ }
+ return result;
+ },
+
+ _dataPostProcessor(data) {
+ data.version = SCHEMA_VERSION;
+ if (!data.profiles) {
+ data.profiles = [];
+ }
+ return data;
+ },
+
+ // For test only.
+ _saveImmediately() {
+ return this._store._save();
+ },
+};
+
+this.EXPORTED_SYMBOLS = ["ProfileStorage"];
diff --git a/browser/extensions/formautofill/install.rdf.in b/browser/extensions/formautofill/install.rdf.in
new file mode 100644
index 000000000..5e34051ba
--- /dev/null
+++ b/browser/extensions/formautofill/install.rdf.in
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+#filter substitution
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>formautofill@mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:type>2</em:type>
+ <em:bootstrap>true</em:bootstrap>
+ <em:multiprocessCompatible>true</em:multiprocessCompatible>
+
+ <!-- Target Application this extension can install into,
+ with minimum and maximum supported versions. -->
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>@MOZ_APP_VERSION@</em:minVersion>
+ <em:maxVersion>@MOZ_APP_MAXVERSION@</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Form Autofill</em:name>
+ <em:description>Autofill forms with saved profiles</em:description>
+ </Description>
+</RDF>
diff --git a/browser/extensions/formautofill/jar.mn b/browser/extensions/formautofill/jar.mn
new file mode 100644
index 000000000..0cba721ef
--- /dev/null
+++ b/browser/extensions/formautofill/jar.mn
@@ -0,0 +1,7 @@
+# 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/.
+
+[features/formautofill@mozilla.org] chrome.jar:
+% resource formautofill %content/
+ content/ (content/*)
diff --git a/browser/extensions/formautofill/moz.build b/browser/extensions/formautofill/moz.build
new file mode 100644
index 000000000..92577db53
--- /dev/null
+++ b/browser/extensions/formautofill/moz.build
@@ -0,0 +1,22 @@
+# -*- 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/.
+
+DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
+DEFINES['MOZ_APP_MAXVERSION'] = CONFIG['MOZ_APP_MAXVERSION']
+
+FINAL_TARGET_FILES.features['formautofill@mozilla.org'] += [
+ 'bootstrap.js'
+]
+
+FINAL_TARGET_PP_FILES.features['formautofill@mozilla.org'] += [
+ 'install.rdf.in'
+]
+
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
+
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/browser/extensions/formautofill/test/browser/.eslintrc.js b/browser/extensions/formautofill/test/browser/.eslintrc.js
new file mode 100644
index 000000000..52a2004c9
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = { // eslint-disable-line no-undef
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js",
+ ],
+};
diff --git a/browser/extensions/formautofill/test/browser/browser.ini b/browser/extensions/formautofill/test/browser/browser.ini
new file mode 100644
index 000000000..500224636
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/browser.ini
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+[browser_check_installed.js]
diff --git a/browser/extensions/formautofill/test/browser/browser_check_installed.js b/browser/extensions/formautofill/test/browser/browser_check_installed.js
new file mode 100644
index 000000000..b018c0f71
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/browser_check_installed.js
@@ -0,0 +1,14 @@
+"use strict";
+
+add_task(function* test_enabled() {
+ let addon = yield new Promise(
+ resolve => AddonManager.getAddonByID("formautofill@mozilla.org", resolve)
+ );
+ isnot(addon, null, "Check addon exists");
+ is(addon.version, "1.0", "Check version");
+ is(addon.name, "Form Autofill", "Check name");
+ ok(addon.isCompatible, "Check application compatibility");
+ ok(!addon.appDisabled, "Check not app disabled");
+ ok(addon.isActive, "Check addon is active");
+ is(addon.type, "extension", "Check type is 'extension'");
+});
diff --git a/browser/extensions/formautofill/test/unit/.eslintrc b/browser/extensions/formautofill/test/unit/.eslintrc
new file mode 100644
index 000000000..8e33fb0c6
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/.eslintrc
@@ -0,0 +1,5 @@
+{
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ],
+}
diff --git a/browser/extensions/formautofill/test/unit/head.js b/browser/extensions/formautofill/test/unit/head.js
new file mode 100644
index 000000000..67e3bd60b
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/head.js
@@ -0,0 +1,81 @@
+/**
+ * Provides infrastructure for automated login components tests.
+ */
+
+ /* exported importAutofillModule, getTempFile */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://testing-common/MockDocument.jsm");
+
+// Redirect the path of the resouce in addon to the exact file path.
+let defineLazyModuleGetter = XPCOMUtils.defineLazyModuleGetter;
+XPCOMUtils.defineLazyModuleGetter = function() {
+ let result = /^resource\:\/\/formautofill\/(.+)$/.exec(arguments[2]);
+ if (result) {
+ arguments[2] = Services.io.newFileURI(do_get_file(result[1])).spec;
+ }
+ return defineLazyModuleGetter.apply(this, arguments);
+};
+
+// Load the module by Service newFileURI API for running extension's XPCShell test
+function importAutofillModule(module) {
+ return Cu.import(Services.io.newFileURI(do_get_file(module)).spec);
+}
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
+ "resource://gre/modules/DownloadPaths.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+// 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.
+let gFileCounter = 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 {string} leafName
+ * Suggested leaf name for the file to be created.
+ *
+ * @returns {nsIFile} pointing to 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.
+ */
+function getTempFile(leafName) {
+ // Prepend a serial number to the extension in the suggested leaf name.
+ let [base, ext] = DownloadPaths.splitBaseNameAndExtension(leafName);
+ let finalLeafName = base + "-" + gFileCounter + ext;
+ gFileCounter++;
+
+ // Get a file reference under the temporary directory for this test file.
+ let file = FileUtils.getFile("TmpD", [finalLeafName]);
+ do_check_false(file.exists());
+
+ do_register_cleanup(function() {
+ if (file.exists()) {
+ file.remove(false);
+ }
+ });
+
+ return file;
+}
+
+add_task(function* test_common_initialize() {
+ Services.prefs.setBoolPref("dom.forms.autocomplete.experimental", true);
+
+ // Clean up after every test.
+ do_register_cleanup(() => {
+ Services.prefs.setBoolPref("dom.forms.autocomplete.experimental", false);
+ });
+});
diff --git a/browser/extensions/formautofill/test/unit/test_autofillFormFields.js b/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
new file mode 100644
index 000000000..a842e84e8
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
@@ -0,0 +1,200 @@
+/*
+ * Test for form auto fill content helper fill all inputs function.
+ */
+
+"use strict";
+
+let {FormAutofillHandler} = importAutofillModule("FormAutofillContent.jsm");
+
+const TESTCASES = [
+ {
+ description: "Form without autocomplete property",
+ document: `<form><input id="given-name"><input id="family-name">
+ <input id="street-addr"><input id="city"><input id="country">
+ <input id='email'><input id="tel"></form>`,
+ fieldDetails: [],
+ profileData: [],
+ expectedResult: {
+ "given-name": "",
+ "family-name": "",
+ "street-addr": "",
+ "city": "",
+ "country": "",
+ "email": "",
+ "tel": "",
+ },
+ },
+ {
+ description: "Form with autocomplete properties and 1 token",
+ document: `<form><input id="given-name" autocomplete="given-name">
+ <input id="family-name" autocomplete="family-name">
+ <input id="street-addr" autocomplete="street-address">
+ <input id="city" autocomplete="address-level2">
+ <input id="country" autocomplete="country">
+ <input id="email" autocomplete="email">
+ <input id="tel" autocomplete="tel"></form>`,
+ fieldDetails: [
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "given-name", "element": {}},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name", "element": {}},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "street-address", "element": {}},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2", "element": {}},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "country", "element": {}},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "email", "element": {}},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "tel", "element": {}},
+ ],
+ profileData: [
+ {"section": "", "addressType": "", "fieldName": "given-name", "contactType": "", "index": 0, "value": "foo"},
+ {"section": "", "addressType": "", "fieldName": "family-name", "contactType": "", "index": 1, "value": "bar"},
+ {"section": "", "addressType": "", "fieldName": "street-address", "contactType": "", "index": 2, "value": "2 Harrison St"},
+ {"section": "", "addressType": "", "fieldName": "address-level2", "contactType": "", "index": 3, "value": "San Francisco"},
+ {"section": "", "addressType": "", "fieldName": "country", "contactType": "", "index": 4, "value": "US"},
+ {"section": "", "addressType": "", "fieldName": "email", "contactType": "", "index": 5, "value": "foo@mozilla.com"},
+ {"section": "", "addressType": "", "fieldName": "tel", "contactType": "", "index": 6, "value": "1234567"},
+ ],
+ expectedResult: {
+ "given-name": "foo",
+ "family-name": "bar",
+ "street-addr": "2 Harrison St",
+ "city": "San Francisco",
+ "country": "US",
+ "email": "foo@mozilla.com",
+ "tel": "1234567",
+ },
+ },
+ {
+ description: "Form with autocomplete properties and 2 tokens",
+ document: `<form><input id="given-name" autocomplete="shipping given-name">
+ <input id="family-name" autocomplete="shipping family-name">
+ <input id="street-addr" autocomplete="shipping street-address">
+ <input id="city" autocomplete="shipping address-level2">
+ <input id="country" autocomplete="shipping country">
+ <input id='email' autocomplete="shipping email">
+ <input id="tel" autocomplete="shipping tel"></form>`,
+ fieldDetails: [
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel", "element": {}},
+ ],
+ profileData: [
+ {"section": "", "addressType": "shipping", "fieldName": "given-name", "contactType": "", "index": 0, "value": "foo"},
+ {"section": "", "addressType": "shipping", "fieldName": "family-name", "contactType": "", "index": 1, "value": "bar"},
+ {"section": "", "addressType": "shipping", "fieldName": "street-address", "contactType": "", "index": 2, "value": "2 Harrison St"},
+ {"section": "", "addressType": "shipping", "fieldName": "address-level2", "contactType": "", "index": 3, "value": "San Francisco"},
+ {"section": "", "addressType": "shipping", "fieldName": "country", "contactType": "", "index": 4, "value": "US"},
+ {"section": "", "addressType": "shipping", "fieldName": "email", "contactType": "", "index": 5, "value": "foo@mozilla.com"},
+ {"section": "", "addressType": "shipping", "fieldName": "tel", "contactType": "", "index": 6, "value": "1234567"},
+ ],
+ expectedResult: {
+ "given-name": "foo",
+ "family-name": "bar",
+ "street-addr": "2 Harrison St",
+ "city": "San Francisco",
+ "country": "US",
+ "email": "foo@mozilla.com",
+ "tel": "1234567",
+ },
+ },
+ {
+ description: "Form with autocomplete properties and profile is partly matched",
+ document: `<form><input id="given-name" autocomplete="shipping given-name">
+ <input id="family-name" autocomplete="shipping family-name">
+ <input id="street-addr" autocomplete="shipping street-address">
+ <input id="city" autocomplete="shipping address-level2">
+ <input id="country" autocomplete="shipping country">
+ <input id='email' autocomplete="shipping email">
+ <input id="tel" autocomplete="shipping tel"></form>`,
+ fieldDetails: [
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel", "element": {}},
+ ],
+ profileData: [
+ {"section": "", "addressType": "shipping", "fieldName": "given-name", "contactType": "", "index": 0, "value": "foo"},
+ {"section": "", "addressType": "shipping", "fieldName": "family-name", "contactType": "", "index": 1, "value": "bar"},
+ {"section": "", "addressType": "shipping", "fieldName": "street-address", "contactType": "", "index": 2, "value": "2 Harrison St"},
+ {"section": "", "addressType": "shipping", "fieldName": "address-level2", "contactType": "", "index": 3, "value": "San Francisco"},
+ {"section": "", "addressType": "shipping", "fieldName": "country", "contactType": "", "index": 4, "value": "US"},
+ {"section": "", "addressType": "shipping", "fieldName": "email", "contactType": "", "index": 5},
+ {"section": "", "addressType": "shipping", "fieldName": "tel", "contactType": "", "index": 6},
+ ],
+ expectedResult: {
+ "given-name": "foo",
+ "family-name": "bar",
+ "street-addr": "2 Harrison St",
+ "city": "San Francisco",
+ "country": "US",
+ "email": "",
+ "tel": "",
+ },
+ },
+ {
+ description: "Form with autocomplete properties but mismatched",
+ document: `<form><input id="given-name" autocomplete="shipping given-name">
+ <input id="family-name" autocomplete="shipping family-name">
+ <input id="street-addr" autocomplete="billing street-address">
+ <input id="city" autocomplete="billing address-level2">
+ <input id="country" autocomplete="billing country">
+ <input id='email' autocomplete="shipping email">
+ <input id="tel" autocomplete="shipping tel"></form>`,
+ fieldDetails: [
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel", "element": {}},
+ ],
+ profileData: [
+ {"section": "", "addressType": "shipping", "fieldName": "given-name", "contactType": "", "index": 0, "value": "foo"},
+ {"section": "", "addressType": "shipping", "fieldName": "family-name", "contactType": "", "index": 1, "value": "bar"},
+ {"section": "", "addressType": "shipping", "fieldName": "street-address", "contactType": "", "index": 2, "value": "2 Harrison St"},
+ {"section": "", "addressType": "shipping", "fieldName": "address-level2", "contactType": "", "index": 3, "value": "San Francisco"},
+ {"section": "", "addressType": "shipping", "fieldName": "country", "contactType": "", "index": 4, "value": "US"},
+ {"section": "", "addressType": "shipping", "fieldName": "email", "contactType": "", "index": 5, "value": "foo@mozilla.com"},
+ {"section": "", "addressType": "shipping", "fieldName": "tel", "contactType": "", "index": 6, "value": "1234567"},
+ ],
+ expectedResult: {
+ "given-name": "foo",
+ "family-name": "bar",
+ "street-addr": "",
+ "city": "",
+ "country": "",
+ "email": "foo@mozilla.com",
+ "tel": "1234567",
+ },
+ },
+];
+
+for (let tc of TESTCASES) {
+ (function() {
+ let testcase = tc;
+ add_task(function* () {
+ do_print("Starting testcase: " + testcase.description);
+
+ let doc = MockDocument.createTestDocument("http://localhost:8080/test/",
+ testcase.document);
+ let form = doc.querySelector("form");
+ let handler = new FormAutofillHandler(form);
+
+ handler.fieldDetails = testcase.fieldDetails;
+ handler.fieldDetails.forEach((field, index) => {
+ field.element = doc.querySelectorAll("input")[index];
+ });
+
+ handler.autofillFormFields(testcase.profileData);
+ for (let id in testcase.expectedResult) {
+ Assert.equal(doc.getElementById(id).value, testcase.expectedResult[id],
+ "Check the " + id + " fields were filled with correct data");
+ }
+ });
+ })();
+}
diff --git a/browser/extensions/formautofill/test/unit/test_collectFormFields.js b/browser/extensions/formautofill/test/unit/test_collectFormFields.js
new file mode 100644
index 000000000..52549b746
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_collectFormFields.js
@@ -0,0 +1,122 @@
+/*
+ * Test for form auto fill content helper collectFormFields functions.
+ */
+
+"use strict";
+
+let {FormAutofillHandler} = importAutofillModule("FormAutofillContent.jsm");
+
+const TESTCASES = [
+ {
+ description: "Form without autocomplete property",
+ document: `<form><input id="given-name"><input id="family-name">
+ <input id="street-addr"><input id="city"><input id="country">
+ <input id='email'><input id="tel"></form>`,
+ returnedFormat: [],
+ fieldDetails: [],
+ },
+ {
+ description: "Form with autocomplete properties and 1 token",
+ document: `<form><input id="given-name" autocomplete="given-name">
+ <input id="family-name" autocomplete="family-name">
+ <input id="street-addr" autocomplete="street-address">
+ <input id="city" autocomplete="address-level2">
+ <input id="country" autocomplete="country">
+ <input id="email" autocomplete="email">
+ <input id="tel" autocomplete="tel"></form>`,
+ returnedFormat: [
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "given-name", "index": 0},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name", "index": 1},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "street-address", "index": 2},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2", "index": 3},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "country", "index": 4},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "email", "index": 5},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "tel", "index": 6},
+ ],
+ fieldDetails: [
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "given-name", "element": {}},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name", "element": {}},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "street-address", "element": {}},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2", "element": {}},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "country", "element": {}},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "email", "element": {}},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "tel", "element": {}},
+ ],
+ },
+ {
+ description: "Form with autocomplete properties and 2 tokens",
+ document: `<form><input id="given-name" autocomplete="shipping given-name">
+ <input id="family-name" autocomplete="shipping family-name">
+ <input id="street-addr" autocomplete="shipping street-address">
+ <input id="city" autocomplete="shipping address-level2">
+ <input id="country" autocomplete="shipping country">
+ <input id='email' autocomplete="shipping email">
+ <input id="tel" autocomplete="shipping tel"></form>`,
+ returnedFormat: [
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name", "index": 0},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name", "index": 1},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address", "index": 2},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2", "index": 3},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "index": 4},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email", "index": 5},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel", "index": 6},
+ ],
+ fieldDetails: [
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel", "element": {}},
+ ],
+ },
+ {
+ description: "Form with autocomplete properties and profile is partly matched",
+ document: `<form><input id="given-name" autocomplete="shipping given-name">
+ <input id="family-name" autocomplete="shipping family-name">
+ <input id="street-addr" autocomplete="shipping street-address">
+ <input id="city" autocomplete="shipping address-level2">
+ <input id="country" autocomplete="shipping country">
+ <input id='email' autocomplete="shipping email">
+ <input id="tel" autocomplete="shipping tel"></form>`,
+ returnedFormat: [
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name", "index": 0},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name", "index": 1},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address", "index": 2},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2", "index": 3},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "index": 4},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email", "index": 5},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel", "index": 6},
+ ],
+ fieldDetails: [
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email", "element": {}},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel", "element": {}},
+ ],
+ },
+];
+
+for (let tc of TESTCASES) {
+ (function() {
+ let testcase = tc;
+ add_task(function* () {
+ do_print("Starting testcase: " + testcase.description);
+
+ let doc = MockDocument.createTestDocument("http://localhost:8080/test/",
+ testcase.document);
+ let form = doc.querySelector("form");
+ let handler = new FormAutofillHandler(form);
+
+ Assert.deepEqual(handler.collectFormFields(), testcase.returnedFormat,
+ "Check the format of form autofill were returned correctly");
+
+ Assert.deepEqual(handler.fieldDetails, testcase.fieldDetails,
+ "Check the fieldDetails were set correctly");
+ });
+ })();
+}
diff --git a/browser/extensions/formautofill/test/unit/test_populateFieldValues.js b/browser/extensions/formautofill/test/unit/test_populateFieldValues.js
new file mode 100644
index 000000000..1215cbd16
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_populateFieldValues.js
@@ -0,0 +1,106 @@
+/*
+ * Test for populating field values in Form Autofill Parent.
+ */
+
+/* global FormAutofillParent */
+
+"use strict";
+
+importAutofillModule("FormAutofillParent.jsm");
+
+do_get_profile();
+
+const TEST_FIELDS = [
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "organization"},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address"},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2"},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level1"},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "postal-code"},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country"},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel"},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email"},
+];
+
+const TEST_GUID = "test-guid";
+
+const TEST_PROFILE = {
+ guid: TEST_GUID,
+ organization: "World Wide Web Consortium",
+ streetAddress: "32 Vassar Street\nMIT Room 32-G524",
+ addressLevel2: "Cambridge",
+ addressLevel1: "MA",
+ postalCode: "02139",
+ country: "US",
+ tel: "+1 617 253 5702",
+ email: "timbl@w3.org",
+};
+
+function camelCase(str) {
+ return str.toLowerCase().replace(/-([a-z])/g, s => s[1].toUpperCase());
+}
+
+add_task(function* test_populateFieldValues() {
+ FormAutofillParent.init();
+
+ let store = FormAutofillParent.getProfileStore();
+ do_check_neq(store, null);
+
+ store.get = function(guid) {
+ do_check_eq(guid, TEST_GUID);
+ return store._clone(TEST_PROFILE);
+ };
+
+ let notifyUsedCalledCount = 0;
+ store.notifyUsed = function(guid) {
+ do_check_eq(guid, TEST_GUID);
+ notifyUsedCalledCount++;
+ };
+
+ yield new Promise((resolve) => {
+ FormAutofillParent.receiveMessage({
+ name: "FormAutofill:PopulateFieldValues",
+ data: {
+ guid: TEST_GUID,
+ fields: TEST_FIELDS,
+ },
+ target: {
+ sendAsyncMessage: function(name, data) {
+ do_check_eq(name, "FormAutofill:fillForm");
+
+ let fields = data.fields;
+ do_check_eq(fields.length, TEST_FIELDS.length);
+
+ for (let i = 0; i < fields.length; i++) {
+ do_check_eq(fields[i].fieldName, TEST_FIELDS[i].fieldName);
+ do_check_eq(fields[i].value,
+ TEST_PROFILE[camelCase(fields[i].fieldName)]);
+ }
+
+ resolve();
+ },
+ },
+ });
+ });
+
+ do_check_eq(notifyUsedCalledCount, 1);
+
+ FormAutofillParent._uninit();
+ do_check_null(FormAutofillParent.getProfileStore());
+});
+
+add_task(function* test_populateFieldValues_with_invalid_guid() {
+ FormAutofillParent.init();
+
+ Assert.throws(() => {
+ FormAutofillParent.receiveMessage({
+ name: "FormAutofill:PopulateFieldValues",
+ data: {
+ guid: "invalid-guid",
+ fields: TEST_FIELDS,
+ },
+ target: {},
+ });
+ }, /No matching profile\./);
+
+ FormAutofillParent._uninit();
+});
diff --git a/browser/extensions/formautofill/test/unit/test_profileStorage.js b/browser/extensions/formautofill/test/unit/test_profileStorage.js
new file mode 100644
index 000000000..018adedb8
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_profileStorage.js
@@ -0,0 +1,222 @@
+/**
+ * Tests ProfileStorage object.
+ */
+
+/* global ProfileStorage */
+
+"use strict";
+
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import(Services.io.newFileURI(do_get_file("ProfileStorage.jsm")).spec);
+
+const TEST_STORE_FILE_NAME = "test-profile.json";
+
+const TEST_PROFILE_1 = {
+ organization: "World Wide Web Consortium",
+ streetAddress: "32 Vassar Street\nMIT Room 32-G524",
+ addressLevel2: "Cambridge",
+ addressLevel1: "MA",
+ postalCode: "02139",
+ country: "US",
+ tel: "+1 617 253 5702",
+ email: "timbl@w3.org",
+};
+
+const TEST_PROFILE_2 = {
+ streetAddress: "Some Address",
+ country: "US",
+};
+
+const TEST_PROFILE_3 = {
+ streetAddress: "Other Address",
+ postalCode: "12345",
+};
+
+const TEST_PROFILE_WITH_INVALID_FIELD = {
+ streetAddress: "Another Address",
+ invalidField: "INVALID",
+};
+
+let prepareTestProfiles = Task.async(function* (path) {
+ let profileStorage = new ProfileStorage(path);
+ yield profileStorage.initialize();
+
+ profileStorage.add(TEST_PROFILE_1);
+ profileStorage.add(TEST_PROFILE_2);
+ yield profileStorage._saveImmediately();
+});
+
+let do_check_profile_matches = (profileWithMeta, profile) => {
+ for (let key in profile) {
+ do_check_eq(profileWithMeta[key], profile[key]);
+ }
+};
+
+add_task(function* test_initialize() {
+ let path = getTempFile(TEST_STORE_FILE_NAME).path;
+ let profileStorage = new ProfileStorage(path);
+ yield profileStorage.initialize();
+
+ do_check_eq(profileStorage._store.data.version, 1);
+ do_check_eq(profileStorage._store.data.profiles.length, 0);
+
+ let data = profileStorage._store.data;
+
+ yield profileStorage._saveImmediately();
+
+ profileStorage = new ProfileStorage(path);
+ yield profileStorage.initialize();
+
+ Assert.deepEqual(profileStorage._store.data, data);
+});
+
+add_task(function* test_getAll() {
+ let path = getTempFile(TEST_STORE_FILE_NAME).path;
+ yield prepareTestProfiles(path);
+
+ let profileStorage = new ProfileStorage(path);
+ yield profileStorage.initialize();
+
+ let profiles = profileStorage.getAll();
+
+ do_check_eq(profiles.length, 2);
+ do_check_profile_matches(profiles[0], TEST_PROFILE_1);
+ do_check_profile_matches(profiles[1], TEST_PROFILE_2);
+
+ // Modifying output shouldn't affect the storage.
+ profiles[0].organization = "test";
+ do_check_profile_matches(profileStorage.getAll()[0], TEST_PROFILE_1);
+});
+
+add_task(function* test_get() {
+ let path = getTempFile(TEST_STORE_FILE_NAME).path;
+ yield prepareTestProfiles(path);
+
+ let profileStorage = new ProfileStorage(path);
+ yield profileStorage.initialize();
+
+ let profiles = profileStorage.getAll();
+ let guid = profiles[0].guid;
+
+ let profile = profileStorage.get(guid);
+ do_check_profile_matches(profile, TEST_PROFILE_1);
+
+ // Modifying output shouldn't affect the storage.
+ profile.organization = "test";
+ do_check_profile_matches(profileStorage.get(guid), TEST_PROFILE_1);
+
+ Assert.throws(() => profileStorage.get("INVALID_GUID"),
+ /No matching profile\./);
+});
+
+add_task(function* test_add() {
+ let path = getTempFile(TEST_STORE_FILE_NAME).path;
+ yield prepareTestProfiles(path);
+
+ let profileStorage = new ProfileStorage(path);
+ yield profileStorage.initialize();
+
+ let profiles = profileStorage.getAll();
+
+ do_check_eq(profiles.length, 2);
+
+ do_check_profile_matches(profiles[0], TEST_PROFILE_1);
+ do_check_profile_matches(profiles[1], TEST_PROFILE_2);
+
+ do_check_neq(profiles[0].guid, undefined);
+ do_check_neq(profiles[0].timeCreated, undefined);
+ do_check_eq(profiles[0].timeLastModified, profiles[0].timeCreated);
+ do_check_eq(profiles[0].timeLastUsed, 0);
+ do_check_eq(profiles[0].timesUsed, 0);
+
+ Assert.throws(() => profileStorage.add(TEST_PROFILE_WITH_INVALID_FIELD),
+ /"invalidField" is not a valid field\./);
+});
+
+add_task(function* test_update() {
+ let path = getTempFile(TEST_STORE_FILE_NAME).path;
+ yield prepareTestProfiles(path);
+
+ let profileStorage = new ProfileStorage(path);
+ yield profileStorage.initialize();
+
+ let profiles = profileStorage.getAll();
+ let guid = profiles[1].guid;
+ let timeLastModified = profiles[1].timeLastModified;
+
+ do_check_neq(profiles[1].country, undefined);
+
+ profileStorage.update(guid, TEST_PROFILE_3);
+ yield profileStorage._saveImmediately();
+
+ profileStorage = new ProfileStorage(path);
+ yield profileStorage.initialize();
+
+ let profile = profileStorage.get(guid);
+
+ do_check_eq(profile.country, undefined);
+ do_check_neq(profile.timeLastModified, timeLastModified);
+ do_check_profile_matches(profile, TEST_PROFILE_3);
+
+ Assert.throws(
+ () => profileStorage.update("INVALID_GUID", TEST_PROFILE_3),
+ /No matching profile\./
+ );
+
+ Assert.throws(
+ () => profileStorage.update(guid, TEST_PROFILE_WITH_INVALID_FIELD),
+ /"invalidField" is not a valid field\./
+ );
+});
+
+add_task(function* test_notifyUsed() {
+ let path = getTempFile(TEST_STORE_FILE_NAME).path;
+ yield prepareTestProfiles(path);
+
+ let profileStorage = new ProfileStorage(path);
+ yield profileStorage.initialize();
+
+ let profiles = profileStorage.getAll();
+ let guid = profiles[1].guid;
+ let timeLastUsed = profiles[1].timeLastUsed;
+ let timesUsed = profiles[1].timesUsed;
+
+ profileStorage.notifyUsed(guid);
+ yield profileStorage._saveImmediately();
+
+ profileStorage = new ProfileStorage(path);
+ yield profileStorage.initialize();
+
+ let profile = profileStorage.get(guid);
+
+ do_check_eq(profile.timesUsed, timesUsed + 1);
+ do_check_neq(profile.timeLastUsed, timeLastUsed);
+
+ Assert.throws(() => profileStorage.notifyUsed("INVALID_GUID"),
+ /No matching profile\./);
+});
+
+add_task(function* test_remove() {
+ let path = getTempFile(TEST_STORE_FILE_NAME).path;
+ yield prepareTestProfiles(path);
+
+ let profileStorage = new ProfileStorage(path);
+ yield profileStorage.initialize();
+
+ let profiles = profileStorage.getAll();
+ let guid = profiles[1].guid;
+
+ do_check_eq(profiles.length, 2);
+
+ profileStorage.remove(guid);
+ yield profileStorage._saveImmediately();
+
+ profileStorage = new ProfileStorage(path);
+ yield profileStorage.initialize();
+
+ profiles = profileStorage.getAll();
+
+ do_check_eq(profiles.length, 1);
+
+ Assert.throws(() => profileStorage.get(guid), /No matching profile\./);
+});
diff --git a/browser/extensions/formautofill/test/unit/xpcshell.ini b/browser/extensions/formautofill/test/unit/xpcshell.ini
new file mode 100644
index 000000000..2c5763681
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/xpcshell.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+head = head.js
+tail =
+support-files =
+ ../../content/FormAutofillContent.jsm
+ ../../content/FormAutofillParent.jsm
+ ../../content/ProfileStorage.jsm
+
+[test_autofillFormFields.js]
+[test_collectFormFields.js]
+[test_populateFieldValues.js]
+[test_profileStorage.js]