Bug 1435163 - Show merchant address errors on billing and payer screens. r=sfoster
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Sat, 29 Sep 2018 00:17:42 +0000
changeset 494599 e2d81abcb65594b6033aa84487403d3776e7e84d
parent 494598 7e5bf501404a0e6d8f28e7255a1ee49b0d05bacb
child 494600 e782c165223aa344fceb958409ede76b7797b6f2
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssfoster
bugs1435163
milestone64.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 1435163 - Show merchant address errors on billing and payer screens. r=sfoster Differential Revision: https://phabricator.services.mozilla.com/D7149
browser/components/payments/res/containers/address-form.js
browser/components/payments/res/debugging.html
browser/components/payments/res/debugging.js
browser/components/payments/res/mixins/PaymentStateSubscriberMixin.js
browser/components/payments/test/browser/head.js
browser/components/payments/test/mochitest/mochitest.ini
browser/components/payments/test/mochitest/payments_common.js
browser/components/payments/test/mochitest/test_address_form.html
browser/components/payments/test/mochitest/test_basic_card_form.html
browser/components/payments/test/mochitest/test_payment_dialog.html
browser/components/payments/test/mochitest/test_payment_dialog_required_top_level_items.html
--- a/browser/components/payments/res/containers/address-form.js
+++ b/browser/components/payments/res/containers/address-form.js
@@ -39,20 +39,27 @@ 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";
 
+    // Combination of AddressErrors and PayerErrorFields as keys
     this._errorFieldMap = {
       addressLine: "#street-address",
       city: "#address-level2",
       country: "#country",
+      email: "#email",
+      // Bug 1472283 is on file to support
+      // additional-name and family-name.
+      // XXX: For now payer name errors go on the family-name and address-errors
+      //      go on the given-name so they don't overwrite each other.
+      name: "#family-name",
       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",
     };
@@ -77,17 +84,17 @@ export default class AddressForm extends
       xhr.send();
     });
   }
 
   connectedCallback() {
     this.promiseReady.then(form => {
       this.body.appendChild(form);
 
-      let record = {};
+      let record = undefined;
       this.formHandler = new EditAddress({
         form,
       }, record, {
         DEFAULT_REGION: PaymentDialogUtils.DEFAULT_REGION,
         getFormFormat: PaymentDialogUtils.getFormFormat,
         countries: PaymentDialogUtils.countries,
       });
 
@@ -116,17 +123,16 @@ export default class AddressForm extends
     });
   }
 
   render(state) {
     let record;
     let {
       page,
       "address-page": addressPage,
-      request,
     } = state;
 
     if (this.id && page && page.id !== this.id) {
       log.debug(`AddressForm: no need to further render inactive page: ${page.id}`);
       return;
     }
 
     let editing = !!addressPage.guid;
@@ -174,21 +180,24 @@ export default class AddressForm extends
     } else {
       this.form.dataset.addressFields = "mailing-address tel";
     }
     this.formHandler.loadRecord(record);
 
     // Add validation to some address fields
     this.updateRequiredState();
 
-    let shippingAddressErrors = request.paymentDetails.shippingAddressErrors;
+    // Show merchant errors for the appropriate address form.
+    let merchantFieldErrors = AddressForm.merchantFieldErrorsForForm(state,
+                                                                     addressPage.selectedStateKey);
     for (let [errorName, errorSelector] of Object.entries(this._errorFieldMap)) {
       let container = this.form.querySelector(errorSelector + "-container");
       let field = this.form.querySelector(errorSelector);
-      let errorText = (shippingAddressErrors && shippingAddressErrors[errorName]) || "";
+      // Never show errors on an 'add' screen as they would be for a different address.
+      let errorText = (editing && merchantFieldErrors && merchantFieldErrors[errorName]) || "";
       field.setCustomValidity(errorText);
       let span = PaymentDialog.maybeCreateFieldErrorElement(container);
       span.textContent = errorText;
     }
 
     this.updateSaveButtonState();
   }
 
@@ -364,11 +373,39 @@ export default class AddressForm extends
         page: {
           id: "address-page",
           onboardingWizard: page.onboardingWizard,
           error: this.dataset.errorGenericSave,
         },
       });
     }
   }
+
+  /**
+   * Get the dictionary of field-specific merchant errors relevant to the
+   * specific form identified by the state key.
+   * @param {object} state The application state
+   * @param {string[]} stateKey The key in state to return address errors for.
+   * @returns {object} with keys as PaymentRequest field names and values of
+   *                   merchant-provided error strings.
+   */
+  static merchantFieldErrorsForForm(state, stateKey) {
+    let {paymentDetails} = state.request;
+    switch (stateKey.join("|")) {
+      case "selectedShippingAddress": {
+        return paymentDetails.shippingAddressErrors;
+      }
+      case "selectedPayerAddress": {
+        return paymentDetails.payer;
+      }
+      case "basic-card-page|billingAddressGUID": {
+        // `paymentMethod` can be null.
+        return (paymentDetails.paymentMethod
+                && paymentDetails.paymentMethod.billingAddress) || {};
+      }
+      default: {
+        throw new Error("Unknown selectedStateKey");
+      }
+    }
+  }
 }
 
 customElements.define("address-form", AddressForm);
--- a/browser/components/payments/res/debugging.html
+++ b/browser/components/payments/res/debugging.html
@@ -57,16 +57,19 @@
         </fieldset>
         <label class="block"><input type="checkbox" id="setChangesPrevented">Prevent changes</label>
 
 
         <section class="group">
           <fieldset>
             <legend>User Data Errors</legend>
             <button id="saveVisibleForm" title="Bypasses field validation">Save Visible Form</button>
+            <button id="setBasicCardErrors">Basic Card Errors</button>
+            <button id="setPayerErrors">Payer Errors</button>
             <button id="setShippingError">Shipping Error</button>
-            <button id="setAddressErrors">Address Errors</button>
+            <button id="setShippingAddressErrors">Shipping Address Errors</button>
+
           </fieldset>
         </section>
       </section>
     </div>
   </body>
 </html>
--- a/browser/components/payments/res/debugging.js
+++ b/browser/components/payments/res/debugging.js
@@ -48,16 +48,18 @@ let REQUEST_1 = {
       {
         label: "Square",
         amount: {
           currency: "USD",
           value: "5",
         },
       },
     ],
+    payer: {},
+    paymentMethod: {},
     shippingAddressErrors: {},
     shippingOptions: [
       {
         id: "std",
         label: "Standard (3-5 business days)",
         amount: {
           currency: "USD",
           value: 10,
@@ -80,17 +82,17 @@ let REQUEST_1 = {
   },
   paymentOptions: {
     requestPayerName: true,
     requestPayerEmail: false,
     requestPayerPhone: false,
     requestShipping: true,
     shippingType: "shipping",
   },
-  shippingOption: "456",
+  shippingOption: "std",
 };
 
 let REQUEST_2 = {
   tabId: 9,
   topLevelPrincipal: {URI: {displayHost: "example.com"}},
   requestId: "3797081f-a96b-c34b-a58b-1083c6e66e25",
   completeStatus: "",
   paymentMethods: [
@@ -128,16 +130,18 @@ let REQUEST_2 = {
         label: "Tax",
         type: "tax",
         amount: {
           currency: "USD",
           value: "1.50",
         },
       },
     ],
+    payer: {},
+    paymentMethod: {},
     shippingAddressErrors: {},
     shippingOptions: [
       {
         id: "123",
         label: "Fast (default)",
         amount: {
           currency: "USD",
           value: 10,
@@ -434,22 +438,82 @@ let buttonActions = {
   setDupesAddresses() {
     paymentDialog.setStateFromParent({savedAddresses: DUPED_ADDRESSES});
   },
 
   setBasicCards1() {
     paymentDialog.setStateFromParent({savedBasicCards: BASIC_CARDS_1});
   },
 
+  setBasicCardErrors() {
+    let request = Object.assign({}, requestStore.getState().request);
+    request.paymentDetails = Object.assign({}, requestStore.getState().request.paymentDetails);
+    request.paymentDetails.paymentMethod = {
+      cardNumber: "",
+      cardholderName: "",
+      cardSecurityCode: "",
+      expiryMonth: "",
+      expiryYear: "",
+      billingAddress: {
+        addressLine: "Can only buy from ROADS, not DRIVES, BOULEVARDS, or STREETS",
+        city: "Can only buy from CITIES, not TOWNSHIPS or VILLAGES",
+        country: "Can only buy from US, not CA",
+        organization: "Can only buy from CORPORATIONS, not CONSORTIUMS",
+        phone: "Only allowed to buy from area codes that start with 9",
+        postalCode: "Only allowed to buy from postalCodes that start with 0",
+        recipient: "Can only buy from names that start with J",
+        region: "Can only buy from regions that start with M",
+      },
+    };
+    requestStore.setState({
+      request,
+    });
+  },
+
+
   setChangesPrevented(evt) {
     requestStore.setState({
       changesPrevented: evt.target.checked,
     });
   },
 
+  setCompleteStatus() {
+    let input = document.querySelector("[name='setCompleteStatus']:checked");
+    let completeStatus = input.value;
+    let request = requestStore.getState().request;
+    paymentDialog.setStateFromParent({
+      request: Object.assign({}, request, { completeStatus }),
+    });
+  },
+
+  setPayerErrors() {
+    let request = Object.assign({}, requestStore.getState().request);
+    request.paymentDetails = Object.assign({}, requestStore.getState().request.paymentDetails);
+    request.paymentDetails.payer = {
+      email: "Only @mozilla.com emails are supported",
+      name: "Payer name must start with M",
+      phone: "Payer area codes must start with 1",
+    };
+    requestStore.setState({
+      request,
+    });
+  },
+
+  setPaymentOptions() {
+    let options = {};
+    let checkboxes = document.querySelectorAll("#paymentOptions input[type='checkbox']");
+    for (let input of checkboxes) {
+      options[input.name] = input.checked;
+    }
+    let req = Object.assign({}, requestStore.getState().request, {
+      paymentOptions: options,
+    });
+    requestStore.setState({ request: req });
+  },
+
   setRequest1() {
     paymentDialog.setStateFromParent({request: REQUEST_1});
   },
 
   setRequest2() {
     paymentDialog.setStateFromParent({request: REQUEST_2});
   },
 
@@ -461,39 +525,27 @@ let buttonActions = {
   },
   setRequestPayerPhone() {
     buttonActions.setPaymentOptions();
   },
   setRequestShipping() {
     buttonActions.setPaymentOptions();
   },
 
-  setPaymentOptions() {
-    let options = {};
-    let checkboxes = document.querySelectorAll("#paymentOptions input[type='checkbox']");
-    for (let input of checkboxes) {
-      options[input.name] = input.checked;
-    }
-    let req = Object.assign({}, requestStore.getState().request, {
-      paymentOptions: options,
-    });
-    requestStore.setState({ request: req });
-  },
-
   setShippingError() {
     let request = Object.assign({}, requestStore.getState().request);
     request.paymentDetails = Object.assign({}, requestStore.getState().request.paymentDetails);
-    request.paymentDetails.error = "Error!";
+    request.paymentDetails.error = "Shipping Error!";
     request.paymentDetails.shippingOptions = [];
     requestStore.setState({
       request,
     });
   },
 
-  setAddressErrors() {
+  setShippingAddressErrors() {
     let request = Object.assign({}, requestStore.getState().request);
     request.paymentDetails = Object.assign({}, requestStore.getState().request.paymentDetails);
     request.paymentDetails.shippingAddressErrors = {
       addressLine: "Can only ship to ROADS, not DRIVES, BOULEVARDS, or STREETS",
       city: "Can only ship to CITIES, not TOWNSHIPS or VILLAGES",
       country: "Can only ship to USA, not CA",
       organization: "Can only ship to CORPORATIONS, not CONSORTIUMS",
       phone: "Only allowed to ship to area codes that start with 9",
@@ -501,25 +553,16 @@ let buttonActions = {
       recipient: "Can only ship to names that start with J",
       region: "Can only ship to regions that start with M",
     };
     requestStore.setState({
       request,
     });
   },
 
-  setCompleteStatus() {
-    let input = document.querySelector("[name='setCompleteStatus']:checked");
-    let completeStatus = input.value;
-    let request = requestStore.getState().request;
-    paymentDialog.setStateFromParent({
-      request: Object.assign({}, request, { completeStatus }),
-    });
-  },
-
   toggleDirectionality() {
     let body = paymentDialog.ownerDocument.body;
     body.dir = body.dir == "rtl" ? "ltr" : "rtl";
   },
 
   toggleBranding() {
     for (let container of paymentDialog.querySelectorAll("accepted-cards")) {
       container.classList.toggle("branded");
--- a/browser/components/payments/res/mixins/PaymentStateSubscriberMixin.js
+++ b/browser/components/payments/res/mixins/PaymentStateSubscriberMixin.js
@@ -37,16 +37,18 @@ export let requestStore = new PaymentsSt
     tabId: null,
     topLevelPrincipal: {URI: {displayHost: null}},
     requestId: null,
     paymentMethods: [],
     paymentDetails: {
       id: null,
       totalItem: {label: null, amount: {currency: null, value: 0}},
       displayItems: [],
+      payer: {},
+      paymentMethod: null,
       shippingAddressErrors: {},
       shippingOptions: [],
       modifiers: null,
       error: "",
     },
     paymentOptions: {
       requestPayerName: false,
       requestPayerEmail: false,
--- a/browser/components/payments/test/browser/head.js
+++ b/browser/components/payments/test/browser/head.js
@@ -407,42 +407,64 @@ async function navigateToAddAddressPage(
       return state.page.id == "address-page" && !state.page.guid;
     }, "Check add page state");
   }, aOptions);
 }
 
 async function fillInBillingAddressForm(frame, aAddress) {
   // For now billing and shipping address forms have the same fields but that may
   // change so use separarate helpers.
-  return fillInShippingAddressForm(frame, aAddress);
+  return fillInShippingAddressForm(frame, aAddress, {
+    expectedSelectedStateKey: ["basic-card-page", "billingAddressGUID"],
+  });
 }
 
 async function fillInShippingAddressForm(frame, aAddress, aOptions) {
   let address = Object.assign({}, aAddress);
   // Email isn't used on address forms, only payer/contact ones.
   delete address.email;
-  return fillInAddressForm(frame, address, aOptions);
+  return fillInAddressForm(frame, address, {
+    expectedSelectedStateKey: ["selectedShippingAddress"],
+    ...aOptions,
+  });
 }
 
 async function fillInPayerAddressForm(frame, aAddress) {
   let address = Object.assign({}, aAddress);
   let payerFields = ["given-name", "additional-name", "family-name", "tel", "email"];
   for (let fieldName of Object.keys(address)) {
     if (payerFields.includes(fieldName)) {
       continue;
     }
     delete address[fieldName];
   }
-  return fillInAddressForm(frame, address);
+  return fillInAddressForm(frame, address, {
+    expectedSelectedStateKey: ["selectedPayerAddress"],
+  });
 }
 
+/**
+ * @param {HTMLElement} frame
+ * @param {object} aAddress
+ * @param {object} [aOptions = {}]
+ * @param {boolean} [aOptions.setPersistCheckedValue = undefined] How to set the persist checkbox.
+ * @param {string[]} [expectedSelectedStateKey = undefined] The expected selectedStateKey for
+                                                            address-page.
+ */
 async function fillInAddressForm(frame, aAddress, aOptions = {}) {
   await spawnPaymentDialogTask(frame, async (args) => {
     let {address, options = {}} = args;
 
+    if (options.expectedSelectedStateKey) {
+      let store = Cu.waiveXrays(content.document.querySelector("address-form")).requestStore;
+      Assert.deepEqual(store.getState()["address-page"].selectedStateKey,
+                       options.expectedSelectedStateKey,
+                       "Check address page selectedStateKey");
+    }
+
     if (typeof(address.country) != "undefined") {
       // Set the country first so that the appropriate fields are visible.
       let countryField = content.document.getElementById("country");
       ok(!countryField.disabled, "Country Field shouldn't be disabled");
       await content.fillField(countryField, address.country);
       is(countryField.value, address.country, "country value is correct after fillField");
     }
 
--- a/browser/components/payments/test/mochitest/mochitest.ini
+++ b/browser/components/payments/test/mochitest/mochitest.ini
@@ -2,16 +2,19 @@
 prefs =
    dom.webcomponents.customelements.enabled=false
 support-files =
    !/browser/extensions/formautofill/content/editAddress.xhtml
    !/browser/extensions/formautofill/content/editCreditCard.xhtml
    ../../../../../browser/extensions/formautofill/content/autofillEditForms.js
    ../../../../../browser/extensions/formautofill/skin/shared/editDialog-shared.css
    ../../../../../testing/modules/sinon-2.3.2.js
+   # paymentRequest.xhtml is needed for `importDialogDependencies` so that the relative paths of
+   # formautofill/edit*.xhtml work from the *-form elements in paymentRequest.xhtml.
+   ../../res/paymentRequest.xhtml
    ../../res/**
    payments_common.js
 skip-if = !e10s
 
 [test_accepted_cards.html]
 [test_address_form.html]
 [test_address_option.html]
 skip-if = os == "linux" || os == "win" # Bug 1493216
--- a/browser/components/payments/test/mochitest/payments_common.js
+++ b/browser/components/payments/test/mochitest/payments_common.js
@@ -45,17 +45,19 @@ function promiseContentToChromeMessage(m
 
 /**
  * Import the templates and stylesheets from the real shipping dialog to avoid
  * duplication in the tests.
  * @param {HTMLIFrameElement} templateFrame - Frame to copy the resources from
  * @param {HTMLElement} destinationEl - Where to append the copied resources
  */
 function importDialogDependencies(templateFrame, destinationEl) {
-  for (let template of templateFrame.contentDocument.querySelectorAll("template")) {
+  let templates = templateFrame.contentDocument.querySelectorAll("template");
+  isnot(templates, null, "Check some templates found");
+  for (let template of templates) {
     let imported = document.importNode(template, true);
     destinationEl.appendChild(imported);
   }
 
   let baseURL = new URL("../../res/", window.location.href);
   let stylesheetLinks = templateFrame.contentDocument.querySelectorAll("link[rel~='stylesheet']");
   for (let stylesheet of stylesheetLinks) {
     let imported = document.importNode(stylesheet, true);
--- a/browser/components/payments/test/mochitest/test_address_form.html
+++ b/browser/components/payments/test/mochitest/test_address_form.html
@@ -61,16 +61,24 @@ function checkAddressForm(customEl, expe
 function sendStringAndCheckValidity(element, string, isValid) {
   fillField(element, string);
   ok(element.checkValidity() == isValid,
      `${element.id} should be ${isValid ? "valid" : "invalid"} (${string})`);
 }
 
 add_task(async function test_initialState() {
   let form = new AddressForm();
+
+  await form.requestStore.setState({
+    "address-page": {
+      selectedStateKey: ["selectedShippingAddress"],
+      title: "Sample page title",
+    },
+  });
+
   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");
 
   // :-moz-ui-invalid, unlike :invalid, only applies to fields showing the error outline.
@@ -83,25 +91,27 @@ add_task(async function test_initialStat
 add_task(async function test_backButton() {
   let form = new AddressForm();
   form.dataset.backButtonLabel = "Back";
   await form.requestStore.setState({
     page: {
       id: "address-page",
     },
     "address-page": {
+      selectedStateKey: ["selectedShippingAddress"],
       title: "Sample page title",
     },
   });
+
   await form.promiseReady;
   display.appendChild(form);
   await asyncElementRendered();
 
   let stateChangePromise = promiseStateChange(form.requestStore);
-  is(form.pageTitleHeading.textContent, "Sample page title", "Check label");
+  is(form.pageTitleHeading.textContent, "Sample page title", "Check title");
 
   is(form.backButton.textContent, "Back", "Check label");
   form.backButton.scrollIntoView();
   synthesizeMouseAtCenter(form.backButton, {});
 
   let {page} = await stateChangePromise;
   is(page.id, "payment-summary", "Check initial page after appending");
 
@@ -203,16 +213,17 @@ add_task(async function test_edit() {
   address1.guid = "9864798564";
 
   await form.requestStore.setState({
     page: {
       id: "address-page",
     },
     "address-page": {
       guid: address1.guid,
+      selectedStateKey: ["selectedShippingAddress"],
     },
     savedAddresses: {
       [address1.guid]: deepClone(address1),
     },
   });
   await asyncElementRendered();
   is(form.querySelectorAll(":-moz-ui-invalid").length, 0,
      "Check no fields are visibly invalid on an 'edit' form with a complete address");
@@ -226,16 +237,17 @@ add_task(async function test_edit() {
     guid: "9gnjdhen46",
   };
   await form.requestStore.setState({
     page: {
       id: "address-page",
     },
     "address-page": {
       guid: minimalAddress.guid,
+      selectedStateKey: ["selectedShippingAddress"],
     },
     savedAddresses: {
       [minimalAddress.guid]: deepClone(minimalAddress),
     },
   });
   await asyncElementRendered();
   is(form.saveButton.textContent, "Update", "Check label");
   checkAddressForm(form, minimalAddress);
@@ -245,17 +257,19 @@ add_task(async function test_edit() {
   ok(form.querySelectorAll("#country:-moz-ui-invalid").length, 1,
      "Check that the country `select` is marked as invalid");
 
   info("change to no selected address");
   await form.requestStore.setState({
     page: {
       id: "address-page",
     },
-    "address-page": {},
+    "address-page": {
+      selectedStateKey: ["selectedShippingAddress"],
+    },
   });
   await asyncElementRendered();
   is(form.querySelectorAll(":-moz-ui-invalid").length, 0,
      "Check no fields are visibly invalid on an empty 'add' form after being an edit form");
   checkAddressForm(form, {
     country: "US",
   });
   ok(form.saveButton.disabled, "Save button should be disabled for an empty form");
@@ -267,16 +281,17 @@ add_task(async function test_restricted_
   let form = new AddressForm();
   form.dataset.nextButtonLabel = "Next";
   form.dataset.errorGenericSave = "Generic error";
   await form.promiseReady;
   display.appendChild(form);
   await form.requestStore.setState({
     "address-page": {
       addressFields: "name email tel",
+      selectedStateKey: ["selectedPayerAddress"],
     },
   });
   await asyncElementRendered();
 
   ok(form.saveButton.disabled, "Save button should be disabled due to empty fields");
 
   ok(!isHidden(form.form.querySelector("#given-name")),
      "given-name should be visible");
@@ -307,17 +322,19 @@ add_task(async function test_restricted_
   fillField(form.form.querySelector("#email"), "john@example.com");
   todo(form.saveButton.disabled,
        "Save button should be disabled due to empty fields - Bug 1483412");
   fillField(form.form.querySelector("#tel"), "+15555555555");
   ok(!form.saveButton.disabled, "Save button should be enabled with all required fields filled");
 
   form.remove();
   await form.requestStore.setState({
-    "address-page": {},
+    "address-page": {
+      selectedStateKey: ["selectedShippingAddress"],
+    },
   });
 });
 
 add_task(async function test_field_validation() {
   let form = new AddressForm();
   form.dataset.fieldRequiredSymbol = "*";
   await form.promiseReady;
   display.appendChild(form);
@@ -375,40 +392,50 @@ add_task(async function test_field_valid
   sendStringAndCheckValidity(addressLevel1Input, "", false);
   sendStringAndCheckValidity(postalCodeInput, "11109", false);
   sendStringAndCheckValidity(addressLevel1Input, "Nova Scotia", true);
   sendStringAndCheckValidity(postalCodeInput, "06390-0001", false);
 
   form.remove();
 });
 
-add_task(async function test_customMerchantValidity() {
+add_task(async function test_merchantShippingAddressErrors() {
   let form = new AddressForm();
   await form.promiseReady;
+
+  // Merchant errors only make sense when editing a record so add one.
+  let address1 = deepClone(PTU.Addresses.TimBL);
+  address1.guid = "9864798564";
+
   const state = {
     page: {
       id: "address-page",
     },
     "address-page": {
+      guid: address1.guid,
+      selectedStateKey: ["selectedShippingAddress"],
       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",
         },
       },
     },
+    savedAddresses: {
+      [address1.guid]: deepClone(address1),
+    },
   };
   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],
@@ -429,37 +456,47 @@ add_task(async function test_customMerch
   // TODO: bug 1482808 - the save button should be enabled after editing the fields
 
   form.remove();
 });
 
 add_task(async function test_customMerchantValidity_reset() {
   let form = new AddressForm();
   await form.promiseReady;
+
+  // Merchant errors only make sense when editing a record so add one.
+  let address1 = deepClone(PTU.Addresses.TimBL);
+  address1.guid = "9864798564";
+
   const state = {
     page: {
       id: "address-page",
     },
     "address-page": {
+      guid: address1.guid,
+      selectedStateKey: ["selectedShippingAddress"],
       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",
         },
       },
     },
+    savedAddresses: {
+      [address1.guid]: deepClone(address1),
+    },
   };
   await form.requestStore.setState(state);
   display.appendChild(form);
   await asyncElementRendered();
 
   ok(form.querySelectorAll(":-moz-ui-invalid").length > 0, "Check fields are visibly invalid");
   info("merchant cleared the errors");
   await form.requestStore.setState({
@@ -467,20 +504,178 @@ add_task(async function test_customMerch
       paymentDetails: {
         shippingAddressErrors: {},
       },
     },
   });
   await asyncElementRendered();
   is(form.querySelectorAll(":-moz-ui-invalid").length, 0,
      "Check fields are visibly valid - custom validity cleared");
+
+  form.remove();
+});
+
+add_task(async function test_customMerchantValidity_correctAddressForm() {
+  let form = new AddressForm();
+  await form.promiseReady;
+
+  // Merchant errors only make sense when editing a record so add one.
+  let address1 = deepClone(PTU.Addresses.TimBL);
+  address1.guid = "9864798564";
+
+  const state = {
+    page: {
+      id: "address-page",
+    },
+    "address-page": {
+      guid: address1.guid,
+      selectedStateKey: ["selectedShippingAddress"],
+      title: "Edit Shipping Address",
+    },
+    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",
+        },
+      },
+    },
+    savedAddresses: {
+      [address1.guid]: deepClone(address1),
+    },
+  };
+  await form.requestStore.setState(state);
+  display.appendChild(form);
+  await asyncElementRendered();
+
+  ok(form.querySelectorAll(":-moz-ui-invalid").length >= 8, "Check fields are visibly invalid");
+
+  info("change to a biling address form");
+  await form.requestStore.setState({
+    "address-page": {
+      guid: address1.guid,
+      selectedStateKey: ["basic-card-page", "billingAddressGUID"],
+      title: "Edit Billing Address",
+    },
+  });
+  await asyncElementRendered();
+  is(form.querySelectorAll(":-moz-ui-invalid").length, 0,
+     "Check fields are visibly valid - shipping errors are not relevant to a billing address form");
+
+  info("change errors to billing address errors");
+  await form.requestStore.setState({
+    request: {
+      paymentDetails: {
+        paymentMethod: {
+          billingAddress: {
+            addressLine: "Billing Street address needs to start with a D",
+            city: "Billing City needs to start with a B",
+            country: "Billing Country needs to start with a C",
+            organization: "Billing organization needs to start with an A",
+            phone: "Billing Telephone needs to start with a 9",
+            postalCode: "Billing Postal code needs to start with a 0",
+            recipient: "Billing Name needs to start with a Z",
+            region: "Billing Region needs to start with a Y",
+          },
+        },
+      },
+    },
+  });
+  await asyncElementRendered();
+  ok(form.querySelectorAll(":-moz-ui-invalid").length >= 8,
+     "Check billing fields are visibly invalid");
+
+  info("change to an ADD shipping address form");
+  await form.requestStore.setState({
+    "address-page": {
+      guid: null,
+      selectedStateKey: ["selectedShippingAddress"],
+      title: "Edit Billing Address",
+    },
+  });
+  await asyncElementRendered();
+  is(form.querySelectorAll(":-moz-ui-invalid").length, 0,
+     "Check fields are visibly valid - merchant errors are not relevant to an 'add' form");
+
+  form.remove();
+});
+
+add_task(async function test_merchantPayerAddressErrors() {
+  let form = new AddressForm();
+  await form.promiseReady;
+
+  // Merchant errors only make sense when editing a record so add one.
+  let address1 = deepClone(PTU.Addresses.TimBL);
+  address1.guid = "9864798564";
+
+  const state = {
+    page: {
+      id: "address-page",
+    },
+    "address-page": {
+      addressFields: "name email tel",
+      guid: address1.guid,
+      selectedStateKey: ["selectedPayerAddress"],
+      title: "Edit Payer Address",
+    },
+    request: {
+      paymentDetails: {
+        payer: {
+          email: "Email must be @mozilla.org",
+          name: "Name needs to start with a W",
+          phone: "Telephone needs to start with a 1",
+        },
+      },
+    },
+    savedAddresses: {
+      [address1.guid]: deepClone(address1),
+    },
+  };
+  await form.requestStore.setState(state);
+  display.appendChild(form);
+  await asyncElementRendered();
+
+  function checkValidationMessage(selector, property) {
+    is(form.form.querySelector(selector).validationMessage,
+       state.request.paymentDetails.payer[property],
+       "Validation message should match for " + selector);
+  }
+
+  ok(form.saveButton.disabled, "Save button should be disabled due to validation errors");
+
+  checkValidationMessage("#tel", "phone");
+  checkValidationMessage("#family-name", "name");
+  checkValidationMessage("#email", "email");
+
+  is(form.querySelectorAll(":-moz-ui-invalid").length, 3, "Check payer fields are visibly invalid");
+
+  await form.requestStore.setState({
+    request: {
+      paymentDetails: {
+        payer: {},
+      },
+    },
+  });
+  await asyncElementRendered();
+
+  is(form.querySelectorAll(":-moz-ui-invalid").length, 0,
+     "Check payer fields are visibly valid after clearing merchant errors");
+
+  form.remove();
 });
 
 add_task(async function test_field_validation() {
-  sinon.stub(PaymentDialogUtils, "getFormFormat").returns({
+  let getFormFormatStub = sinon.stub(PaymentDialogUtils, "getFormFormat");
+  getFormFormatStub.returns({
     addressLevel1Label: "state",
     postalCodeLabel: "US",
     fieldsOrder: [
       {fieldId: "name", newLine: true},
       {fieldId: "organization", newLine: true},
       {fieldId: "street-address", newLine: true},
       {fieldId: "address-level2"},
     ],
@@ -488,16 +683,17 @@ add_task(async function test_field_valid
 
   let form = new AddressForm();
   await form.promiseReady;
   const state = {
     page: {
       id: "address-page",
     },
     "address-page": {
+      selectedStateKey: ["selectedShippingAddress"],
       title: "Sample page title",
     },
     request: {
       paymentDetails: {
         shippingAddressErrors: {},
       },
     },
   };
@@ -511,36 +707,39 @@ add_task(async function test_field_valid
   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");
 
+  getFormFormatStub.restore();
   form.remove();
 });
 
-add_task(async function test_field_validation_dom_errors() {
+add_task(async function test_field_validation_dom_popup() {
   let form = new AddressForm();
   await form.promiseReady;
   const state = {
     page: {
       id: "address-page",
     },
     "address-page": {
+      selectedStateKey: ["selectedShippingAddress"],
       title: "Sample page title",
     },
   };
+
   await form.requestStore.setState(state);
   display.appendChild(form);
   await asyncElementRendered();
 
   const BAD_POSTAL_CODE = "hi mom";
-  let postalCode = document.getElementById("postal-code");
+  let postalCode = form.querySelector("#postal-code");
   postalCode.focus();
   sendString(BAD_POSTAL_CODE, window);
   postalCode.blur();
   let errorTextSpan = postalCode.parentNode.querySelector(".error-text");
   is(errorTextSpan.textContent, "Please match the requested format.",
      "DOM validation messages should be reflected in the error-text #1");
 
   postalCode.focus();
--- a/browser/components/payments/test/mochitest/test_basic_card_form.html
+++ b/browser/components/payments/test/mochitest/test_basic_card_form.html
@@ -16,17 +16,17 @@ Test the basic-card-form element
   <script src="autofillEditForms.js"></script>
 
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
   <link rel="stylesheet" type="text/css" href="../../res/paymentRequest.css"/>
   <link rel="stylesheet" type="text/css" href="../../res/components/accepted-cards.css"/>
 </head>
 <body>
   <p id="display" style="height: 100vh; margin: 0;">
-    <iframe id="templateFrame" src="../../res/paymentRequest.xhtml" width="0" height="0"
+    <iframe id="templateFrame" src="paymentRequest.xhtml" width="0" height="0"
             style="float: left;"></iframe>
   </p>
 <div id="content" style="display: none">
 
 </div>
 <pre id="test">
 </pre>
 <script type="module">
--- a/browser/components/payments/test/mochitest/test_payment_dialog.html
+++ b/browser/components/payments/test/mochitest/test_payment_dialog.html
@@ -16,17 +16,17 @@ Test the payment-dialog custom element
   <script src="autofillEditForms.js"></script>
 
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
   <link rel="stylesheet" type="text/css" href="../../res/paymentRequest.css"/>
   <link rel="stylesheet" type="text/css" href="../../res/containers/rich-picker.css"/>
 </head>
 <body>
   <p id="display" style="height: 100vh; margin: 0;">
-    <iframe id="templateFrame" src="../../res/paymentRequest.xhtml" width="0" height="0"
+    <iframe id="templateFrame" src="paymentRequest.xhtml" width="0" height="0"
             style="float: left;"></iframe>
   </p>
 <div id="content" style="display: none">
 
 </div>
 <pre id="test">
 </pre>
 <script type="module">
--- a/browser/components/payments/test/mochitest/test_payment_dialog_required_top_level_items.html
+++ b/browser/components/payments/test/mochitest/test_payment_dialog_required_top_level_items.html
@@ -15,17 +15,17 @@ Test the payment-dialog custom element
   <script src="autofillEditForms.js"></script>
 
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
   <link rel="stylesheet" type="text/css" href="../../res/paymentRequest.css"/>
   <link rel="stylesheet" type="text/css" href="../../res/containers/rich-picker.css"/>
 </head>
 <body>
   <p id="display" style="height: 100vh; margin: 0;">
-    <iframe id="templateFrame" src="../../res/paymentRequest.xhtml" width="0" height="0"
+    <iframe id="templateFrame" src="paymentRequest.xhtml" width="0" height="0"
             style="float: left;"></iframe>
   </p>
 <div id="content" style="display: none">
 
 </div>
 <pre id="test">
 </pre>
 <script type="module">