Bug 1364823 - Populate select elements with form autofill profile data. r=lchang
authorScott Wu <scottcwwu@gmail.com>
Tue, 16 May 2017 16:53:01 +0800
changeset 412927 96ed52d7654721ef4e4b9f4dbdb1873d607ac9de
parent 412926 3a10169452a57e6b6da140ce84aafa4d88629936
child 412928 0e956bdfab8b666a4a6ec3d05b194c8528ac527e
push id1490
push usermtabara@mozilla.com
push dateMon, 31 Jul 2017 14:08:16 +0000
treeherdermozilla-release@70e32e6bf15e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslchang
bugs1364823
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1364823 - Populate select elements with form autofill profile data. r=lchang MozReview-Commit-ID: 21K5mC2tYQn
browser/extensions/formautofill/FormAutofillHandler.jsm
browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html
browser/extensions/formautofill/test/unit/test_autofillFormFields.js
--- a/browser/extensions/formautofill/FormAutofillHandler.jsm
+++ b/browser/extensions/formautofill/FormAutofillHandler.jsm
@@ -79,26 +79,45 @@ FormAutofillHandler.prototype = {
     log.debug("profile in autofillFormFields:", profile);
 
     this.filledProfileGUID = profile.guid;
     for (let fieldDetail of this.fieldDetails) {
       // Avoid filling field value in the following cases:
       // 1. the focused input which is filled in FormFillController.
       // 2. a non-empty input field
       // 3. the invalid value set
+      // 4. value already chosen in select element
 
       let element = fieldDetail.elementWeakRef.get();
-      if (!element || element === focusedInput || element.value) {
+      if (!element || element === focusedInput) {
         continue;
       }
 
       let value = profile[fieldDetail.fieldName];
-      // TODO: Bug 1364823 is implemeting the value filling of select element.
       if (element instanceof Ci.nsIDOMHTMLInputElement && value) {
+        if (element.value) {
+          continue;
+        }
         element.setUserInput(value);
+      } else if (element instanceof Ci.nsIDOMHTMLSelectElement) {
+        for (let option of element.options) {
+          if (value === option.textContent || value === option.value) {
+            // Do not change value if the option is already selected.
+            // Use case for multiple select is not considered here.
+            if (option.selected) {
+              break;
+            }
+            // TODO: Using dispatchEvent does not 100% simulate select change.
+            //       Should investigate further in Bug 1365895.
+            option.selected = true;
+            element.dispatchEvent(new Event("input", {"bubbles": true}));
+            element.dispatchEvent(new Event("change", {"bubbles": true}));
+            break;
+          }
+        }
       }
     }
   },
 
   /**
    * Populates result to the preview layers with given profile.
    *
    * @param {Object} profile
--- a/browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html
+++ b/browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html
@@ -19,20 +19,22 @@ Form autofill test: simple form address 
 
 "use strict";
 
 let expectingPopup = null;
 let MOCK_STORAGE = [{
   organization: "Sesame Street",
   "street-address": "123 Sesame Street.",
   tel: "1-345-345-3456",
+  country: "US",
 }, {
   organization: "Mozilla",
   "street-address": "331 E. Evelyn Avenue",
   tel: "1-650-903-0800",
+  country: "US",
 }];
 
 function expectPopup() {
   info("expecting a popup");
   return new Promise(resolve => {
     expectingPopup = resolve;
   });
 }
@@ -80,17 +82,17 @@ function checkFormFilled(address) {
 async function setupAddressStorage() {
   await addAddress(MOCK_STORAGE[0]);
   await addAddress(MOCK_STORAGE[1]);
 }
 
 async function setupFormHistory() {
   await updateFormHistory([
     {op: "add", fieldname: "tel", value: "1-234-567-890"},
-    {op: "add", fieldname: "country", value: "US"},
+    {op: "add", fieldname: "email", value: "foo@mozilla.com"},
   ]);
 }
 
 // Form with history only.
 add_task(async function history_only_menu_checking() {
   await setupFormHistory();
 
   setInput("#tel", "");
@@ -122,20 +124,20 @@ add_task(async function check_menu_when_
   await expectPopup();
   checkMenuEntries(MOCK_STORAGE.map(address =>
     JSON.stringify({primary: address.tel, secondary: address["street-address"]})
   ));
 });
 
 // Display history search result if no matched data in addresses.
 add_task(async function check_fallback_for_mismatched_field() {
-  setInput("#country", "");
+  setInput("#email", "");
   doKey("down");
   await expectPopup();
-  checkMenuEntries(["US"]);
+  checkMenuEntries(["foo@mozilla.com"]);
 });
 
 // Autofill the address from dropdown menu.
 add_task(async function check_fields_after_form_autofill() {
   setInput("#organization", "Moz");
   doKey("down");
   await expectPopup();
   checkMenuEntries(MOCK_STORAGE.map(address =>
@@ -161,16 +163,20 @@ registerPopupShownListener(popupShownLis
 
 <div id="content">
 
   <form id="form1">
     <p>This is a basic form.</p>
     <p><label>organization: <input id="organization" name="organization" autocomplete="organization" type="text"></label></p>
     <p><label>streetAddress: <input id="street-address" name="street-address" autocomplete="street-address" type="text"></label></p>
     <p><label>tel: <input id="tel" name="tel" autocomplete="tel" type="text"></label></p>
-    <p><label>country: <input id="country" name="country" autocomplete="country" type="text"></label></p>
+    <p><label>email: <input id="email" name="email" autocomplete="email" type="text"></label></p>
+    <p><label>country: <select id="country" name="country" autocomplete="country">
+      <option/>
+      <option value="US">United States</option>
+    </label></p>
   </form>
 
 </div>
 
 <pre id="test"></pre>
 </body>
 </html>
--- a/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
@@ -23,17 +23,20 @@ const TESTCASES = [
     },
   },
   {
     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">
-               <select id="country" autocomplete="country"></select>
+               <select id="country" autocomplete="country">
+                 <option/>
+                 <option value="US">United States</option>
+               </select>
                <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": {}},
@@ -57,17 +60,20 @@ const TESTCASES = [
     },
   },
   {
     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">
-               <select id="country" autocomplete="shipping country"></select>
+               <select id="country" autocomplete="shipping country">
+                 <option/>
+                 <option value="US">United States</option>
+               </select>
                <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": {}},
@@ -154,54 +160,158 @@ const TESTCASES = [
     expectedResult: {
       "street-addr": "",
       "city": "",
       "country": "",
       "email": "foo@mozilla.com",
       "tel": "1234567",
     },
   },
+  {
+    description: "Form with autocomplete select elements and matching option values",
+    document: `<form>
+               <select id="country" autocomplete="shipping country">
+                 <option value=""></option>
+                 <option value="US">United States</option>
+               </select>
+               <select id="state" autocomplete="shipping address-level1">
+                 <option value=""></option>
+                 <option value="CA">California</option>
+                 <option value="WA">Washington</option>
+               </select>
+               </form>`,
+    fieldDetails: [
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "element": {}},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level1", "element": {}},
+    ],
+    profileData: {
+      "guid": "123",
+      "country": "US",
+      "address-level1": "CA",
+    },
+    expectedResult: {
+      "country": "US",
+      "state": "CA",
+    },
+  },
+  {
+    description: "Form with autocomplete select elements and matching option texts",
+    document: `<form>
+               <select id="country" autocomplete="shipping country">
+                 <option value=""></option>
+                 <option value="US">United States</option>
+               </select>
+               <select id="state" autocomplete="shipping address-level1">
+                 <option value=""></option>
+                 <option value="CA">California</option>
+                 <option value="WA">Washington</option>
+               </select>
+               </form>`,
+    fieldDetails: [
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "element": {}},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level1", "element": {}},
+    ],
+    profileData: {
+      "guid": "123",
+      "country": "United States",
+      "address-level1": "California",
+    },
+    expectedResult: {
+      "country": "US",
+      "state": "CA",
+    },
+  },
 ];
 
-for (let tc of TESTCASES) {
-  (function() {
-    let testcase = tc;
-    add_task(async function() {
-      do_print("Starting testcase: " + testcase.description);
+const TESTCASES_INPUT_UNCHANGED = [
+  {
+    description: "Form with autocomplete select elements; with default and no matching options",
+    document: `<form>
+               <select id="country" autocomplete="shipping country">
+                 <option value="US">United States</option>
+               </select>
+               <select id="state" autocomplete="shipping address-level1">
+                 <option value=""></option>
+                 <option value="CA">California</option>
+                 <option value="WA">Washington</option>
+               </select>
+               </form>`,
+    fieldDetails: [
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "element": {}},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level1", "element": {}},
+    ],
+    profileData: {
+      "guid": "123",
+      "country": "US",
+      "address-level1": "unknown state",
+    },
+    expectedResult: {
+      "country": "US",
+      "state": "",
+    },
+  },
+];
 
-      let doc = MockDocument.createTestDocument("http://localhost:8080/test/",
-                                                testcase.document);
-      let form = doc.querySelector("form");
-      let handler = new FormAutofillHandler(form);
-      let onChangePromises = [];
+function do_test(testcases, testFn) {
+  for (let tc of testcases) {
+    (function() {
+      let testcase = tc;
+      add_task(async 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);
+        let promises = [];
 
-      handler.fieldDetails = testcase.fieldDetails;
-      handler.fieldDetails.forEach((field, index) => {
-        let element = doc.querySelectorAll("input, select")[index];
-        field.elementWeakRef = Cu.getWeakReference(element);
-        if (element instanceof Ci.nsIDOMHTMLSelectElement) {
-          // TODO: Bug 1364823 should remove the condition and handle filling
-          // value in <select>
-          return;
-        }
-        if (!testcase.profileData[field.fieldName]) {
-          // Avoid waiting for `change` event of a input with a blank value to
-          // be filled.
-          return;
-        }
-        onChangePromises.push(new Promise(resolve => {
-          element.addEventListener("change", () => {
-            let id = element.id;
-            Assert.equal(element.value, testcase.expectedResult[id],
-                        "Check the " + id + " fields were filled with correct data");
-            resolve();
-          }, {once: true});
-        }));
+        handler.fieldDetails = testcase.fieldDetails;
+        handler.fieldDetails.forEach((field, index) => {
+          let element = doc.querySelectorAll("input, select")[index];
+          field.elementWeakRef = Cu.getWeakReference(element);
+          if (!testcase.profileData[field.fieldName]) {
+            // Avoid waiting for `change` event of a input with a blank value to
+            // be filled.
+            return;
+          }
+          promises.push(testFn(testcase, element));
+        });
+
+        handler.autofillFormFields(testcase.profileData);
+        Assert.equal(handler.filledProfileGUID, testcase.profileData.guid,
+                     "Check if filledProfileGUID is set correctly");
+        await Promise.all(promises);
       });
+    })();
+  }
+}
 
-      handler.autofillFormFields(testcase.profileData);
+do_test(TESTCASES, (testcase, element) => {
+  return new Promise(resolve => {
+    element.addEventListener("change", () => {
+      let id = element.id;
+      Assert.equal(element.value, testcase.expectedResult[id],
+                  "Check the " + id + " field was filled with correct data");
+      resolve();
+    }, {once: true});
+  });
+});
 
-      Assert.equal(handler.filledProfileGUID, testcase.profileData.guid,
-                   "Check if filledProfileGUID is set correctly");
-      await Promise.all(onChangePromises);
-    });
-  })();
-}
+do_test(TESTCASES_INPUT_UNCHANGED, (testcase, element) => {
+  return new Promise((resolve, reject) => {
+    // Make sure no change or input event is fired when no change occurs.
+    let cleaner;
+    let timer = setTimeout(() => {
+      let id = element.id;
+      element.removeEventListener("change", cleaner);
+      element.removeEventListener("input", cleaner);
+      Assert.equal(element.value, testcase.expectedResult[id],
+                  "Check no value is changed on the " + id + " field");
+      resolve();
+    }, 1000);
+    cleaner = event => {
+      clearTimeout(timer);
+      reject(`${event.type} event should not fire`);
+    };
+    element.addEventListener("change", cleaner);
+    element.addEventListener("input", cleaner);
+  });
+});