Bug 1300993 - Connect satchel autocomplete to form autofill results instead of form history when appropriate. r=MattN draft
authorLuke Chang <lchang@mozilla.com>
Thu, 22 Dec 2016 17:53:17 +0800
changeset 456279 0d70823086782fecedc163fae72c0b12c93db04e
parent 454127 d7b6af32811bddcec10a47d24bd455a1ec1836fc
child 541177 7d202b74c2b64878053390718153f6dd6464bb4d
push id40444
push userbmo:lchang@mozilla.com
push dateThu, 05 Jan 2017 09:25:54 +0000
reviewersMattN
bugs1300993
milestone53.0a1
Bug 1300993 - Connect satchel autocomplete to form autofill results instead of form history when appropriate. r=MattN MozReview-Commit-ID: DcpC1oJCGwN
browser/extensions/formautofill/FormAutofillParent.jsm
browser/extensions/formautofill/content/FormAutofillContent.js
browser/extensions/formautofill/test/unit/head.js
browser/extensions/formautofill/test/unit/test_autofillFormFields.js
browser/extensions/formautofill/test/unit/test_collectFormFields.js
browser/extensions/formautofill/test/unit/test_markAsAutofillField.js
browser/extensions/formautofill/test/unit/xpcshell.ini
--- a/browser/extensions/formautofill/FormAutofillParent.jsm
+++ b/browser/extensions/formautofill/FormAutofillParent.jsm
@@ -3,17 +3,17 @@
  * 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.
+ * FormAutofillContent.js for more details.
  *
  * [
  *   {
  *     section,
  *     addressType,
  *     contactType,
  *     fieldName,
  *     value,
--- a/browser/extensions/formautofill/content/FormAutofillContent.js
+++ b/browser/extensions/formautofill/content/FormAutofillContent.js
@@ -6,17 +6,55 @@
  * Form Autofill frame script.
  */
 
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr, manager: Cm} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-let {FormAutoCompleteResult} = Cu.import("resource://gre/modules/nsFormAutoCompleteResult.jsm", {});
+Cu.import("resource://gre/modules/nsFormAutoCompleteResult.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FormLikeFactory",
+                                  "resource://gre/modules/FormLikeFactory.jsm");
+
+const formFillController = Cc["@mozilla.org/satchel/form-fill-controller;1"]
+                             .getService(Ci.nsIFormFillController);
+
+const AUTOFILL_FIELDS_THRESHOLD = 3;
+
+/**
+ * Returns the autocomplete information of fields according to heuristics.
+ */
+let FormAutofillHeuristics = {
+  VALID_FIELDS: [
+    "organization",
+    "street-address",
+    "address-level2",
+    "address-level1",
+    "postal-code",
+    "country",
+    "tel",
+    "email",
+  ],
+
+  getInfo(element) {
+    if (!(element instanceof Ci.nsIDOMHTMLInputElement)) {
+      return null;
+    }
+
+    let info = element.getAutocompleteInfo();
+    if (!info || !info.fieldName ||
+        !this.VALID_FIELDS.includes(info.fieldName)) {
+      return null;
+    }
+
+    return info;
+  },
+};
 
 /**
  * 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 = [];
@@ -50,24 +88,19 @@ FormAutofillHandler.prototype = {
    * @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)) {
+      let info = FormAutofillHeuristics.getInfo(element);
+      if (!info) {
         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)) {
@@ -115,18 +148,19 @@ FormAutofillHandler.prototype = {
       // 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 ||
+      let info = FormAutofillHeuristics.getInfo(fieldDetail.element);
+      if (!info ||
+          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);
@@ -208,19 +242,16 @@ AutofillProfileAutoCompleteSearch.protot
    * Stops an asynchronous search that is in progress
    */
   stopSearch() {
   },
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AutofillProfileAutoCompleteSearch]);
 
-// TODO: Remove this lint option once we apply ProfileAutocomplete while
-//       content script initialization.
-/* eslint no-unused-vars: [2, {"vars": "local"}] */
 let ProfileAutocomplete = {
   _registered: false,
   _factory: null,
 
   ensureRegistered() {
     if (this._registered) {
       return;
     }
@@ -235,8 +266,68 @@ let ProfileAutocomplete = {
       return;
     }
 
     this._factory.unregister();
     this._factory = null;
     this._registered = false;
   },
 };
+
+/**
+ * Handles content's interactions.
+ *
+ * NOTE: Declares it by "var" to make it accessible in unit tests.
+ */
+var FormAutofillContent = {
+  init() {
+    ProfileAutocomplete.ensureRegistered();
+
+    addEventListener("DOMContentLoaded", this);
+  },
+
+  handleEvent(evt) {
+    if (!evt.isTrusted) {
+      return;
+    }
+
+    switch (evt.type) {
+      case "DOMContentLoaded":
+        let doc = evt.target;
+        if (!(doc instanceof Ci.nsIDOMHTMLDocument)) {
+          return;
+        }
+        this._identifyAutofillFields(doc);
+        break;
+    }
+  },
+
+  _identifyAutofillFields(doc) {
+    let forms = [];
+
+    // Collects root forms from inputs.
+    for (let field of doc.getElementsByTagName("input")) {
+      let formLike = FormLikeFactory.createFromField(field);
+      if (!forms.some(form => form.rootElement === formLike.rootElement)) {
+        forms.push(formLike);
+      }
+    }
+
+    // Collects the fields that can be autofilled from each form and marks them
+    // as autofill fields if the amount is above the threshold.
+    forms.forEach(form => {
+      let formHandler = new FormAutofillHandler(form);
+      formHandler.collectFormFields();
+      if (formHandler.fieldDetails.length < AUTOFILL_FIELDS_THRESHOLD) {
+        return;
+      }
+
+      formHandler.fieldDetails.forEach(
+        detail => this._markAsAutofillField(detail.element));
+    });
+  },
+
+  _markAsAutofillField(field) {
+    formFillController.markAsAutofillField(field);
+  },
+};
+
+FormAutofillContent.init();
--- a/browser/extensions/formautofill/test/unit/head.js
+++ b/browser/extensions/formautofill/test/unit/head.js
@@ -1,13 +1,13 @@
 /**
  * Provides infrastructure for automated formautofill components tests.
  */
 
-/* exported getTempFile */
+/* exported loadFormAutofillContent, 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://gre/modules/NetUtil.jsm");
@@ -33,17 +33,19 @@ Components.manager.addBootstrappedManife
 
 // 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);
 
 function loadFormAutofillContent() {
-  let facGlobal = {};
+  let facGlobal = {
+    addEventListener: function() {},
+  };
   let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
                .getService(Ci.mozIJSSubScriptLoader);
   loader.loadSubScriptWithOptions("chrome://formautofill/content/FormAutofillContent.js", {
     target: facGlobal,
   });
 
   return facGlobal;
 }
@@ -79,16 +81,15 @@ function getTempFile(leafName) {
   });
 
   return file;
 }
 
 add_task(function* test_common_initialize() {
   Services.prefs.setBoolPref("browser.formautofill.enabled", true);
   Services.prefs.setBoolPref("dom.forms.autocomplete.experimental", true);
-  loadFormAutofillContent();
 
   // Clean up after every test.
   do_register_cleanup(() => {
     Services.prefs.clearUserPref("browser.formautofill.enabled");
     Services.prefs.clearUserPref("dom.forms.autocomplete.experimental");
   });
 });
--- a/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
@@ -10,18 +10,16 @@ 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": "",
     },
   },
   {
@@ -47,18 +45,16 @@ const TESTCASES = [
       {"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",
     },
   },
   {
@@ -84,18 +80,16 @@ const TESTCASES = [
       {"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",
     },
   },
   {
@@ -121,18 +115,16 @@ const TESTCASES = [
       {"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": "",
     },
   },
   {
@@ -158,18 +150,16 @@ const TESTCASES = [
       {"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",
     },
   },
 ];
--- a/browser/extensions/formautofill/test/unit/test_collectFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_collectFormFields.js
@@ -20,27 +20,23 @@ const TESTCASES = [
     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},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "street-address", "index": 0},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2", "index": 1},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "country", "index": 2},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "email", "index": 3},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "tel", "index": 4},
     ],
     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": {}},
     ],
   },
   {
@@ -48,27 +44,23 @@ const TESTCASES = [
     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},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address", "index": 0},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2", "index": 1},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "index": 2},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email", "index": 3},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel", "index": 4},
     ],
     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": {}},
     ],
   },
   {
@@ -76,27 +68,23 @@ const TESTCASES = [
     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},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address", "index": 0},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2", "index": 1},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "index": 2},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email", "index": 3},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel", "index": 4},
     ],
     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": {}},
     ],
   },
 ];
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_markAsAutofillField.js
@@ -0,0 +1,72 @@
+"use strict";
+
+let {FormAutofillContent} = loadFormAutofillContent();
+
+const TESTCASES = [
+  {
+    description: "Form containing 5 fields with autocomplete attribute.",
+    document: `<form>
+                 <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">
+                 <input id="without-autocomplete-1">
+                 <input id="without-autocomplete-2">
+               </form>`,
+    expectedResult: [
+      "street-addr",
+      "city",
+      "country",
+      "email",
+      "tel",
+    ],
+  },
+  {
+    description: "Form containing only 2 fields with autocomplete attribute.",
+    document: `<form>
+                 <input id="street-addr" autocomplete="street-address">
+                 <input id="city" autocomplete="address-level2">
+                 <input id="without-autocomplete-1">
+                 <input id="without-autocomplete-2">
+               </form>`,
+    expectedResult: [],
+  },
+  {
+    description: "Fields without form element.",
+    document: `<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">
+               <input id="without-autocomplete-1">
+               <input id="without-autocomplete-2">`,
+    expectedResult: [
+      "street-addr",
+      "city",
+      "country",
+      "email",
+      "tel",
+    ],
+  },
+];
+
+let markedFieldId = [];
+FormAutofillContent._markAsAutofillField = function(field) {
+  markedFieldId.push(field.id);
+};
+
+TESTCASES.forEach(testcase => {
+  add_task(function* () {
+    do_print("Starting testcase: " + testcase.description);
+
+    markedFieldId = [];
+
+    let doc = MockDocument.createTestDocument(
+      "http://localhost:8080/test/", testcase.document);
+    FormAutofillContent._identifyAutofillFields(doc);
+
+    Assert.deepEqual(markedFieldId, testcase.expectedResult,
+      "Check the fields were marked correctly.");
+  });
+});
--- a/browser/extensions/formautofill/test/unit/xpcshell.ini
+++ b/browser/extensions/formautofill/test/unit/xpcshell.ini
@@ -3,8 +3,9 @@ firefox-appdir = browser
 head = head.js
 tail =
 support-files =
 
 [test_autofillFormFields.js]
 [test_collectFormFields.js]
 [test_populateFieldValues.js]
 [test_profileStorage.js]
+[test_markAsAutofillField.js]