Bug 1388152 - Save abbreviated state name of address-level1 select element rather than its value in form autofill submission. r=lchang, MattN draft
authorRay Lin <ralin@mozilla.com>
Mon, 14 Aug 2017 18:08:44 +0800
changeset 648324 23c8b2421f6725779812d6e0869eede3b1398587
parent 647817 63ca686c3f1e870649b6d9c559973d100573aec2
child 726787 c28db337dfd6162d644b9f632e32d458079fd533
push id74713
push userbmo:ralin@mozilla.com
push dateThu, 17 Aug 2017 15:58:34 +0000
reviewerslchang, MattN
bugs1388152
milestone57.0a1
Bug 1388152 - Save abbreviated state name of address-level1 select element rather than its value in form autofill submission. r=lchang, MattN MozReview-Commit-ID: IjZKzEdLlq5
browser/extensions/formautofill/FormAutofillHandler.jsm
browser/extensions/formautofill/FormAutofillUtils.jsm
browser/extensions/formautofill/test/unit/test_onFormSubmitted.js
--- a/browser/extensions/formautofill/FormAutofillHandler.jsm
+++ b/browser/extensions/formautofill/FormAutofillHandler.jsm
@@ -476,16 +476,32 @@ FormAutofillHandler.prototype = {
         record: {},
         untouchedFields: [],
       };
 
       details.forEach(detail => {
         let element = detail.elementWeakRef.get();
         // Remove the unnecessary spaces
         let value = element && element.value.trim();
+
+        // Try to abbreviate the value of select element.
+        if (type == "address" &&
+            detail.fieldName == "address-level1" &&
+            element instanceof Ci.nsIDOMHTMLSelectElement) {
+          // Don't save the record when the option value is empty *OR* there
+          // are multiple options being selected. The empty option is usually
+          // assumed to be default along with a meaningless text to users.
+          if (!value || element.selectedOptions.length != 1) {
+            return;
+          }
+
+          let text = element.selectedOptions[0].text.trim();
+          value = FormAutofillUtils.getAbbreviatedStateName([value, text]) || text;
+        }
+
         if (!value) {
           return;
         }
 
         data[type].record[detail.fieldName] = value;
 
         if (detail.state == "AUTO_FILLED") {
           data[type].untouchedFields.push(detail.fieldName);
--- a/browser/extensions/formautofill/FormAutofillUtils.jsm
+++ b/browser/extensions/formautofill/FormAutofillUtils.jsm
@@ -276,16 +276,55 @@ this.FormAutofillUtils = {
         }
       }
     }
 
     return null;
   },
 
   /**
+   * Try to find the abbreviation of the given state name
+   * @param   {string[]} stateValues A list of inferable state values.
+   * @param   {string} country A country name to be identified.
+   * @returns {string} The matching state abbreviation.
+   */
+  getAbbreviatedStateName(stateValues, country = this.DEFAULT_COUNTRY_CODE) {
+    let values = Array.isArray(stateValues) ? stateValues : [stateValues];
+
+    let collators = this.getCollators(country);
+    let {sub_keys: subKeys, sub_names: subNames} = this.getCountryAddressData(country);
+
+    if (!Array.isArray(subKeys)) {
+      subKeys = subKeys.split("~");
+    }
+    if (!Array.isArray(subNames)) {
+      subNames = subNames.split("~");
+    }
+
+    let speculatedSubIndexes = [];
+    for (const val of values) {
+      let identifiedValue = this.identifyValue(subKeys, subNames, val, collators);
+      if (identifiedValue) {
+        return identifiedValue;
+      }
+
+      // Predict the possible state by partial-matching if no exact match.
+      [subKeys, subNames].forEach(sub => {
+        speculatedSubIndexes.push(sub.findIndex(token => {
+          let pattern = new RegExp("\\b" + this.escapeRegExp(token) + "\\b");
+
+          return pattern.test(val);
+        }));
+      });
+    }
+
+    return subKeys[speculatedSubIndexes.find(i => !!~i)] || null;
+  },
+
+  /**
    * Find the option element from select element.
    * 1. Try to find the locale using the country from address.
    * 2. First pass try to find exact match.
    * 3. Second pass try to identify values from address value and options,
    *    and look for a match.
    * @param   {DOMElement} selectEl
    * @param   {object} address
    * @param   {string} fieldName
--- a/browser/extensions/formautofill/test/unit/test_onFormSubmitted.js
+++ b/browser/extensions/formautofill/test/unit/test_onFormSubmitted.js
@@ -1,15 +1,28 @@
 "use strict";
 
 Cu.import("resource://formautofill/FormAutofillContent.jsm");
 
 const MOCK_DOC = MockDocument.createTestDocument("http://localhost:8080/test/",
                    `<form id="form1">
                       <input id="street-addr" autocomplete="street-address">
+                      <select id="address-level1" autocomplete="address-level1">
+                        <option value=""></option>
+                        <option value="AL">Alabama</option>
+                        <option value="AK">Alaska</option>
+                        <option value="AP">Armed Forces Pacific</option>
+
+                        <option value="ca">california</option>
+                        <option value="AR">US-Arkansas</option>
+                        <option value="US-CA">California</option>
+                        <option value="CA">California</option>
+                        <option value="US-AZ">US_Arizona</option>
+                        <option value="Ariz">Arizonac</option>
+                      </select>
                       <input id="city" autocomplete="address-level2">
                       <input id="country" autocomplete="country">
                       <input id="email" autocomplete="email">
                       <input id="tel" autocomplete="tel">
                       <input id="cc-name" autocomplete="cc-name">
                       <input id="cc-number" autocomplete="cc-number">
                       <input id="cc-exp-month" autocomplete="cc-exp-month">
                       <input id="cc-exp-year" autocomplete="cc-exp-year">
@@ -161,16 +174,195 @@ const TESTCASES = [
             "country": "USA",
             "tel": "1-650-903-0800",
           },
           untouchedFields: [],
         },
       },
     },
   },
+  {
+    description: "Save state with regular select option",
+    formValue: {
+      "address-level1": "CA",
+      "street-addr": "331 E. Evelyn Avenue",
+      "country": "USA",
+    },
+    expectedResult: {
+      formSubmission: true,
+      records: {
+        address: {
+          guid: null,
+          record: {
+            "address-level1": "CA",
+            "street-address": "331 E. Evelyn Avenue",
+            "country": "USA",
+          },
+          untouchedFields: [],
+        },
+      },
+    },
+  },
+  {
+    description: "Save state with lowercase value",
+    formValue: {
+      "address-level1": "ca",
+      "street-addr": "331 E. Evelyn Avenue",
+      "country": "USA",
+    },
+    expectedResult: {
+      formSubmission: true,
+      records: {
+        address: {
+          guid: null,
+          record: {
+            "address-level1": "CA",
+            "street-address": "331 E. Evelyn Avenue",
+            "country": "USA",
+          },
+          untouchedFields: [],
+        },
+      },
+    },
+  },
+  {
+    description: "Save state with a country code prefixed to the label",
+    formValue: {
+      "address-level1": "AR",
+      "street-addr": "331 E. Evelyn Avenue",
+      "country": "USA",
+    },
+    expectedResult: {
+      formSubmission: true,
+      records: {
+        address: {
+          guid: null,
+          record: {
+            "address-level1": "AR",
+            "street-address": "331 E. Evelyn Avenue",
+            "country": "USA",
+          },
+          untouchedFields: [],
+        },
+      },
+    },
+  },
+  {
+    description: "Save state with a country code prefixed to the value",
+    formValue: {
+      "address-level1": "US-CA",
+      "street-addr": "331 E. Evelyn Avenue",
+      "country": "USA",
+    },
+    expectedResult: {
+      formSubmission: true,
+      records: {
+        address: {
+          guid: null,
+          record: {
+            "address-level1": "CA",
+            "street-address": "331 E. Evelyn Avenue",
+            "country": "USA",
+          },
+          untouchedFields: [],
+        },
+      },
+    },
+  },
+  {
+    description: "Save state with a country code prefixed to the value and label",
+    formValue: {
+      "address-level1": "US-AZ",
+      "street-addr": "331 E. Evelyn Avenue",
+      "country": "USA",
+    },
+    expectedResult: {
+      formSubmission: true,
+      records: {
+        address: {
+          guid: null,
+          record: {
+            "address-level1": "AZ",
+            "street-address": "331 E. Evelyn Avenue",
+            "country": "USA",
+          },
+          untouchedFields: [],
+        },
+      },
+    },
+  },
+  {
+    description: "Should save select label instead when failed to abbreviate the value",
+    formValue: {
+      "address-level1": "Ariz",
+      "street-addr": "331 E. Evelyn Avenue",
+      "country": "USA",
+    },
+    expectedResult: {
+      formSubmission: true,
+      records: {
+        address: {
+          guid: null,
+          record: {
+            "address-level1": "Arizonac",
+            "street-address": "331 E. Evelyn Avenue",
+            "country": "USA",
+          },
+          untouchedFields: [],
+        },
+      },
+    },
+  },
+  {
+    description: "Shouldn't save select with multiple selections",
+    formValue: {
+      "address-level1": ["AL", "AK", "AP"],
+      "street-addr": "331 E. Evelyn Avenue",
+      "country": "USA",
+      "tel": "1-650-903-0800",
+    },
+    expectedResult: {
+      formSubmission: true,
+      records: {
+        address: {
+          guid: null,
+          record: {
+            "street-address": "331 E. Evelyn Avenue",
+            "country": "USA",
+            "tel": "1-650-903-0800",
+          },
+          untouchedFields: [],
+        },
+      },
+    },
+  },
+  {
+    description: "Shouldn't save select with empty value",
+    formValue: {
+      "address-level1": "",
+      "street-addr": "331 E. Evelyn Avenue",
+      "country": "USA",
+      "tel": "1-650-903-0800",
+    },
+    expectedResult: {
+      formSubmission: true,
+      records: {
+        address: {
+          guid: null,
+          record: {
+            "street-address": "331 E. Evelyn Avenue",
+            "country": "USA",
+            "tel": "1-650-903-0800",
+          },
+          untouchedFields: [],
+        },
+      },
+    },
+  },
+
 ];
 
 add_task(async function handle_earlyformsubmit_event() {
   do_print("Starting testcase: Test an invalid form element");
   let fakeForm = MOCK_DOC.createElement("form");
   sinon.spy(FormAutofillContent, "_onFormSubmit");
 
   do_check_eq(FormAutofillContent.notify(fakeForm), true);
@@ -181,17 +373,26 @@ add_task(async function handle_earlyform
 TESTCASES.forEach(testcase => {
   add_task(async function check_records_saving_is_called_correctly() {
     do_print("Starting testcase: " + testcase.description);
 
     let form = MOCK_DOC.getElementById("form1");
     form.reset();
     for (let key in testcase.formValue) {
       let input = MOCK_DOC.getElementById(key);
-      input.value = testcase.formValue[key];
+      let value = testcase.formValue[key];
+
+      if (input instanceof Ci.nsIDOMHTMLSelectElement && value) {
+        input.multiple = Array.isArray(value);
+        [...input.options].forEach(option => {
+          option.selected = value.includes(option.value);
+        });
+      } else {
+        input.value = testcase.formValue[key];
+      }
     }
     sinon.stub(FormAutofillContent, "_onFormSubmit");
 
     let element = MOCK_DOC.getElementById(TARGET_ELEMENT_ID);
     FormAutofillContent.identifyAutofillFields(element);
     FormAutofillContent.notify(form);
 
     do_check_eq(FormAutofillContent._onFormSubmit.called,