Bug 1427961 - Basic shipping address validation for state/province and zip/postal code. r?mattn draft
authorJared Wein <jwein@mozilla.com>
Thu, 07 Jun 2018 17:02:42 -0400
changeset 812651 24d7243dc5e88f6c8c2aebc000d84719dc27c122
parent 812548 a009b5249a4b78a889fdc5ffcf55ad51715cc686
child 812652 8468f3d6e5e30ceb8cedd7831b45bbcea658b6df
push id114619
push userbmo:jaws@mozilla.com
push dateFri, 29 Jun 2018 18:32:54 +0000
reviewersmattn
bugs1427961
milestone63.0a1
Bug 1427961 - Basic shipping address validation for state/province and zip/postal code. r?mattn MozReview-Commit-ID: DCjr13QxP5Z
browser/components/payments/res/containers/address-form.js
browser/components/payments/test/browser/browser_shippingaddresschange_error.js
browser/components/payments/test/mochitest/test_address_form.html
--- a/browser/components/payments/res/containers/address-form.js
+++ b/browser/components/payments/res/containers/address-form.js
@@ -33,24 +33,26 @@ export default class AddressForm extends
     this.saveButton = document.createElement("button");
     this.saveButton.className = "save-button primary";
     this.saveButton.addEventListener("click", this);
 
     this.persistCheckbox = new LabelledCheckbox();
     this.persistCheckbox.className = "persist-checkbox";
 
     this._errorFieldMap = {
-      addressLine: "#street-address-container",
-      city: "#address-level2-container",
-      country: "#country-container",
-      organization: "#organization-container",
-      phone: "#tel-container",
-      postalCode: "#postal-code-container",
-      recipient: "#name-container",
-      region: "#address-level1-container",
+      addressLine: "#street-address",
+      city: "#address-level2",
+      country: "#country",
+      organization: "#organization",
+      phone: "#tel",
+      postalCode: "#postal-code",
+      // Bug 1472283 is on file to support
+      // additional-name and family-name.
+      recipient: "#given-name",
+      region: "#address-level1",
     };
 
     // The markup is shared with form autofill preferences.
     let url = "formautofill/editAddress.xhtml";
     this.promiseReady = this._fetchMarkup(url).then(doc => {
       this.form = doc.getElementById("form");
       return this.form;
     });
@@ -138,27 +140,37 @@ export default class AddressForm extends
     } else {
       // Adding a new record: default persistence to checked when in a not-private session
       this.persistCheckbox.hidden = false;
       this.persistCheckbox.checked = !state.isPrivate;
     }
 
     this.formHandler.loadRecord(record);
 
+    // Add validation to some address fields
+    let postalCodeInput = this.form.querySelector("#postal-code");
+    let addressLevel1Input = this.form.querySelector("#address-level1");
+    for (let element of [postalCodeInput, addressLevel1Input]) {
+      element.required = element.closest(`#${element.id}-container`).style.display != "none";
+    }
+
     let shippingAddressErrors = request.paymentDetails.shippingAddressErrors;
     for (let [errorName, errorSelector] of Object.entries(this._errorFieldMap)) {
-      let container = document.querySelector(errorSelector);
+      let container = document.querySelector(errorSelector + "-container");
+      let field = document.querySelector(errorSelector);
+      let errorText = (shippingAddressErrors && shippingAddressErrors[errorName]) || "";
+      container.classList.toggle("error", !!errorText);
+      field.setCustomValidity(errorText);
       let span = container.querySelector(".error-text");
       if (!span) {
         span = document.createElement("span");
         span.className = "error-text";
         container.appendChild(span);
       }
-      span.textContent = shippingAddressErrors[errorName];
-      container.classList.toggle("error", !!shippingAddressErrors[errorName]);
+      span.textContent = errorText;
     }
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "click": {
         this.onClick(event);
         break;
--- a/browser/components/payments/test/browser/browser_shippingaddresschange_error.js
+++ b/browser/components/payments/test/browser/browser_shippingaddresschange_error.js
@@ -126,17 +126,17 @@ add_task(async function test_show_field_
       // check errors and make corrections
       let {shippingAddressErrors} = PTU.Details.fieldSpecificErrors;
       is(content.document.querySelectorAll("address-form .error-text:not(:empty)").length,
          Object.keys(shippingAddressErrors).length,
          "Each error should be presented");
       let errorFieldMap =
         Cu.waiveXrays(content.document.querySelector("address-form"))._errorFieldMap;
       for (let [errorName, errorValue] of Object.entries(shippingAddressErrors)) {
-        let field = content.document.querySelector(errorFieldMap[errorName]);
+        let field = content.document.querySelector(errorFieldMap[errorName] + "-container");
         try {
           is(field.querySelector(".error-text").textContent, errorValue,
              "Field specific error should be associated with " + errorName);
         } catch (ex) {
           ok(false, `no field found for ${errorName}. selector= ${errorFieldMap[errorName]}`);
         }
       }
     });
--- a/browser/components/payments/test/mochitest/test_address_form.html
+++ b/browser/components/payments/test/mochitest/test_address_form.html
@@ -25,17 +25,17 @@ Test the address-form element
 <div id="content" style="display: none">
 
 </div>
 <pre id="test">
 </pre>
 <script type="module">
 /** Test the address-form element **/
 
-/* global sinon */
+/* global sinon, PaymentDialogUtils */
 
 import AddressForm from "../../res/containers/address-form.js";
 
 let display = document.getElementById("display");
 
 function checkAddressForm(customEl, expectedAddress) {
   const ADDRESS_PROPERTY_NAMES = [
     "given-name",
@@ -52,16 +52,26 @@ function checkAddressForm(customEl, expe
   for (let propName of ADDRESS_PROPERTY_NAMES) {
     let expectedVal = expectedAddress[propName] || "";
     is(document.getElementById(propName).value,
        expectedVal.toString(),
        `Check ${propName}`);
   }
 }
 
+function sendStringAndCheckValidity(element, string, isValid) {
+  element.focus();
+  while (element.value) {
+    sendKey("BACK_SPACE");
+  }
+  sendString(string);
+  ok(element.checkValidity() == isValid,
+     `${element.id} should be ${isValid ? "valid" : "invalid"}`);
+}
+
 add_task(async function test_initialState() {
   let form = new AddressForm();
   let {page} = form.requestStore.getState();
   is(page.id, "payment-summary", "Check initial page");
   await form.promiseReady;
   display.appendChild(form);
   await asyncElementRendered();
   is(page.id, "payment-summary", "Check initial page after appending");
@@ -270,12 +280,130 @@ add_task(async function test_restricted_
      "country should be hidden");
   ok(!isHidden(form.form.querySelector("#email")),
      "email should be visible");
   ok(!isHidden(form.form.querySelector("#tel")),
      "tel should be visible");
 
   form.remove();
 });
+
+add_task(async function test_field_validation() {
+  let form = new AddressForm();
+  await form.promiseReady;
+  display.appendChild(form);
+  await asyncElementRendered();
+
+  let postalCodeInput = form.form.querySelector("#postal-code");
+  let addressLevel1Input = form.form.querySelector("#address-level1");
+  ok(!postalCodeInput.value, "postal-code should be empty by default");
+  ok(!addressLevel1Input.value, "address-level1 should be empty by default");
+  ok(!postalCodeInput.checkValidity(), "postal-code should be invalid by default");
+  ok(!addressLevel1Input.checkValidity(), "address-level1 should be invalid by default");
+
+  sendStringAndCheckValidity(postalCodeInput, "00001", true);
+  sendStringAndCheckValidity(addressLevel1Input, "CA", true);
+  sendStringAndCheckValidity(postalCodeInput, "94043", true);
+  sendStringAndCheckValidity(addressLevel1Input, "", false);
+  sendStringAndCheckValidity(postalCodeInput, "B4N4N4", true);
+  sendStringAndCheckValidity(addressLevel1Input, "MI", true);
+  sendStringAndCheckValidity(postalCodeInput, "R3J 3C7", true);
+  sendStringAndCheckValidity(addressLevel1Input, "", false);
+  sendStringAndCheckValidity(postalCodeInput, "11109", true);
+  sendStringAndCheckValidity(addressLevel1Input, "Nova Scotia", true);
+  sendStringAndCheckValidity(postalCodeInput, "06390-0001", true);
+
+  form.remove();
+});
+
+add_task(async function test_customValidity() {
+  let form = new AddressForm();
+  await form.promiseReady;
+  const state = {
+    page: {
+      id: "address-page",
+    },
+    "address-page": {
+      title: "Sample page title",
+    },
+    request: {
+      paymentDetails: {
+        shippingAddressErrors: {
+          addressLine: "Street address needs to start with a D",
+          city: "City needs to start with a B",
+          country: "Country needs to start with a C",
+          organization: "organization needs to start with an A",
+          phone: "Telephone needs to start with a 9",
+          postalCode: "Postal code needs to start with a 0",
+          recipient: "Name needs to start with a Z",
+          region: "Region needs to start with a Y",
+        },
+      },
+    },
+  };
+  await form.requestStore.setState(state);
+  display.appendChild(form);
+  await asyncElementRendered();
+
+  function checkValidationMessage(selector, property) {
+    is(form.form.querySelector(selector).validationMessage,
+       state.request.paymentDetails.shippingAddressErrors[property],
+       "Validation message should match for " + selector);
+  }
+
+  checkValidationMessage("#street-address", "addressLine");
+  checkValidationMessage("#address-level2", "city");
+  checkValidationMessage("#country", "country");
+  checkValidationMessage("#organization", "organization");
+  checkValidationMessage("#tel", "phone");
+  checkValidationMessage("#postal-code", "postalCode");
+  checkValidationMessage("#given-name", "recipient");
+  checkValidationMessage("#address-level1", "region");
+
+  form.remove();
+});
+
+add_task(async function test_field_validation() {
+  sinon.stub(PaymentDialogUtils, "getFormFormat").returns({
+    addressLevel1Label: "state",
+    postalCodeLabel: "US",
+    fieldsOrder: [
+      {fieldId: "name", newLine: true},
+      {fieldId: "organization", newLine: true},
+      {fieldId: "street-address", newLine: true},
+      {fieldId: "address-level2"},
+    ],
+  });
+
+  let form = new AddressForm();
+  await form.promiseReady;
+  const state = {
+    page: {
+      id: "address-page",
+    },
+    "address-page": {
+      title: "Sample page title",
+    },
+    request: {
+      paymentDetails: {
+        shippingAddressErrors: {},
+      },
+    },
+  };
+  await form.requestStore.setState(state);
+  display.appendChild(form);
+  await asyncElementRendered();
+
+  let postalCodeInput = form.form.querySelector("#postal-code");
+  let addressLevel1Input = form.form.querySelector("#address-level1");
+  ok(!postalCodeInput.value, "postal-code should be empty by default");
+  ok(!addressLevel1Input.value, "address-level1 should be empty by default");
+  ok(postalCodeInput.checkValidity(),
+     "postal-code should be valid by default when it is not visible");
+  ok(addressLevel1Input.checkValidity(),
+     "address-level1 should be valid by default when it is not visible");
+
+  form.remove();
+});
 </script>
 
 </body>
 </html>