Bug 1364823 - Populate select elements with form autofill profile data. r?lchang draft
authorScott Wu <scottcwwu@gmail.com>
Tue, 16 May 2017 16:53:01 +0800
changeset 588738 69c524fc070234c7e009b1f0d77d6c3c18b0558e
parent 588052 aeb3d0ca558f034cbef1c5a68bd07dd738611494
child 589475 d5e2390406ff04cb5a900bf7907e378418e1e188
child 589536 9187692fa15555974952d43586bf656fc7bafac3
push id62135
push userbmo:scwwu@mozilla.com
push dateSun, 04 Jun 2017 09:00:42 +0000
reviewerslchang
bugs1364823
milestone55.0a1
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);
+  });
+});