Bug 1440499 - Add payerName/Email/Phone contact picker. draft
authorSam Foster <sfoster@mozilla.com>
Fri, 02 Mar 2018 10:30:44 -0800
changeset 763396 083bc76802532dbff9624f904608b5a88dee72f7
parent 763395 fc63066c695eea45c741a63f4c0df47892ee5af3
push id101450
push userbmo:sfoster@mozilla.com
push dateMon, 05 Mar 2018 23:14:11 +0000
bugs1440499
milestone60.0a1
Bug 1440499 - Add payerName/Email/Phone contact picker. * WIP, tests to ensure the paymentOptions values propagate to the picker underway * Add a field-names attribute to the payer address-picker, populated from the request paymentOptions * Initial/placeholder CSS to selectively render address fields MozReview-Commit-ID: Br8i5MVyeQ3
toolkit/components/payments/res/components/address-option.css
toolkit/components/payments/res/containers/address-picker.js
toolkit/components/payments/res/containers/payment-dialog.js
toolkit/components/payments/test/mochitest/mochitest.ini
toolkit/components/payments/test/mochitest/test_payer_address_picker.html
--- a/toolkit/components/payments/res/components/address-option.css
+++ b/toolkit/components/payments/res/components/address-option.css
@@ -12,35 +12,50 @@ address-option {
 
 rich-select[open] > .rich-select-popup-box > address-option {
   grid-template-areas:
     "name           name          "
     "street-address street-address"
     "email          tel           ";
 }
 
+address-picker.payer-related address-option {
+  grid-template-areas:
+    "name name"
+    "tel  email";
+}
+
 address-option > .name {
   grid-area: name;
 }
 
 address-option > .street-address {
   grid-area: street-address;
 }
 
 address-option > .email {
   grid-area: email;
 }
 
 address-option > .tel {
   grid-area: tel;
 }
 
+/* Hide all the fields by default, and enable them explicitly for each format case */
 address-option > .name,
 address-option > .street-address,
 address-option > .email,
 address-option > .tel {
   white-space: nowrap;
+  display: none;
 }
 
-.rich-select-selected-clone > .email,
-.rich-select-selected-clone > .tel {
-  display: none;
+address-picker:not([address-fields]) address-option > .name,
+address-picker:not([address-fields]) address-option > .street-address {
+  display: inline-block;
 }
+
+address-picker[address-fields~='name'] address-option > .name,
+address-picker[address-fields~='email'] address-option > .email,
+address-picker[address-fields~='tel'] address-option > .tel {
+  display: inline-block;
+}
+
--- a/toolkit/components/payments/res/containers/address-picker.js
+++ b/toolkit/components/payments/res/containers/address-picker.js
@@ -28,21 +28,23 @@ class AddressPicker extends PaymentState
     let {savedAddresses} = state;
     let desiredOptions = [];
     for (let [guid, address] of Object.entries(savedAddresses)) {
       let optionEl = this.dropdown.getOptionByValue(guid);
       if (!optionEl) {
         optionEl = document.createElement("address-option");
         optionEl.value = guid;
       }
+
       for (let [key, val] of Object.entries(address)) {
         optionEl.setAttribute(key, val);
       }
       desiredOptions.push(optionEl);
     }
+
     let el = null;
     while ((el = this.dropdown.popupBox.querySelector(":scope > address-option"))) {
       el.remove();
     }
     for (let option of desiredOptions) {
       this.dropdown.popupBox.appendChild(option);
     }
 
--- a/toolkit/components/payments/res/containers/payment-dialog.js
+++ b/toolkit/components/payments/res/containers/payment-dialog.js
@@ -29,16 +29,18 @@ class PaymentDialog extends PaymentState
 
     this._viewAllButton = contents.querySelector("#view-all");
     this._viewAllButton.addEventListener("click", this);
 
     this._orderDetailsOverlay = contents.querySelector("#order-details-overlay");
     this._shippingTypeLabel = contents.querySelector("#shipping-type-label");
     this._shippingRelatedEls = contents.querySelectorAll(".shipping-related");
     this._payerRelatedEls = contents.querySelectorAll(".payer-related");
+    this._payerAddressPicker = contents.querySelector("address-picker.payer-related");
+
     this._errorText = contents.querySelector("#error-text");
 
     this._disabledOverlay = contents.getElementById("disabled-overlay");
 
     this.appendChild(contents);
 
     super.connectedCallback();
   }
@@ -204,26 +206,44 @@ class PaymentDialog extends PaymentState
 
     let totalItem = paymentDetails.totalItem;
     let totalAmountEl = this.querySelector("#total > currency-amount");
     totalAmountEl.value = totalItem.amount.value;
     totalAmountEl.currency = totalItem.amount.currency;
 
     this._orderDetailsOverlay.hidden = !state.orderDetailsShowing;
     this._errorText.textContent = paymentDetails.error;
+
     let paymentOptions = request.paymentOptions;
     for (let element of this._shippingRelatedEls) {
       element.hidden = !paymentOptions.requestShipping;
     }
     let payerRequested = paymentOptions.requestPayerName ||
                          paymentOptions.requestPayerEmail ||
                          paymentOptions.requestPayerPhone;
     for (let element of this._payerRelatedEls) {
       element.hidden = !payerRequested;
     }
+
+    if (payerRequested) {
+      let fieldNames = new Set(); // default: ["name", "tel", "email"]
+      if (paymentOptions.requestPayerName) {
+        fieldNames.add("name");
+      }
+      if (paymentOptions.requestPayerEmail) {
+        fieldNames.add("email");
+      }
+      if (paymentOptions.requestPayerPhone) {
+        fieldNames.add("tel");
+      }
+      this._payerAddressPicker.setAttribute("address-fields", [...fieldNames].join(" "));
+    } else {
+      this._payerAddressPicker.removeAttribute("address-fields");
+    }
+
     let shippingType = paymentOptions.shippingType || "shipping";
     this._shippingTypeLabel.querySelector("label").textContent =
       this._shippingTypeLabel.dataset[shippingType + "AddressLabel"];
 
     this._renderPayButton(state);
 
     let {
       changesPrevented,
--- a/toolkit/components/payments/test/mochitest/mochitest.ini
+++ b/toolkit/components/payments/test/mochitest/mochitest.ini
@@ -26,15 +26,16 @@ support-files =
    ../../res/mixins/PaymentStateSubscriberMixin.js
    ../../res/vendor/custom-elements.min.js
    ../../res/vendor/custom-elements.min.js.map
    payments_common.js
 
 [test_address_picker.html]
 [test_currency_amount.html]
 [test_order_details.html]
+[test_payer_address_picker.html]
 [test_payment_dialog.html]
 [test_payment_details_item.html]
 [test_payment_method_picker.html]
 [test_rich_select.html]
 [test_shipping_option_picker.html]
 [test_ObservedPropertiesMixin.html]
 [test_PaymentStateSubscriberMixin.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/test/mochitest/test_payer_address_picker.html
@@ -0,0 +1,174 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the paymentOptions address-picker
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test the paymentOptions address-picker</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <script src="sinon-2.3.2.js"></script>
+  <script src="payments_common.js"></script>
+
+  <script src="custom-elements.min.js"></script>
+  <script src="PaymentsStore.js"></script>
+  <script src="ObservedPropertiesMixin.js"></script>
+  <script src="PaymentStateSubscriberMixin.js"></script>
+  <script src="payment-dialog.js"></script>
+
+  <script src="rich-select.js"></script>
+  <script src="address-picker.js"></script>
+  <script src="rich-option.js"></script>
+  <script src="address-option.js"></script>
+  <script src="currency-amount.js"></script>
+  <link rel="stylesheet" type="text/css" href="rich-select.css"/>
+  <link rel="stylesheet" type="text/css" href="address-option.css"/>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <link rel="stylesheet" type="text/css" href="paymentRequest.css"/>
+</head>
+<body>
+  <p id="display">
+    <iframe id="templateFrame" src="paymentRequest.xhtml" width="0" height="0"></iframe>
+  </p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+<script type="application/javascript">
+/** Test the payer requested details functionality **/
+
+// (WIP) test that:
+// payer picker is not shown when there are no addresses
+// payer picker is not shown when all paymentOptions are false
+// payer picker is shown when one or more paymentOptions is given AND when an address is available
+// the correct fields are visible to correspond with paymentOptions values.
+
+
+/* global sinon */
+/* import-globals-from payments_common.js */
+/* import-globals-from ../../res/mixins/PaymentStateSubscriberMixin.js */
+
+function getVisiblePickerOptions(picker) {
+  let select = picker.querySelector(":scope > rich-select");
+  // let popupBox = Cu.waiveXrays(select).popupBox;
+  let popupBox = select.popupBox;
+  let visibleOptions = Array.from(popupBox.children).filter(item => {
+    return item.getBoundingClientRect().height > 0;
+  });
+  // Cu.unwaiveXrays(picker);
+  return visibleOptions;
+}
+
+function isVisible(elem) {
+  return elem.getBoundingClientRect().height > 0;
+}
+
+const SAVED_ADDRESSES = {
+  "48bnds6854t": {
+    "address-level1": "MI",
+    "address-level2": "Some City",
+    "country": "US",
+    "guid": "48bnds6854t",
+    "name": "Mr. Foo",
+    "postal-code": "90210",
+    "street-address": "123 Sesame Street,\nApt 40",
+    "tel": "+1 519 555-5555",
+  },
+  "68gjdh354j": {
+    "address-level1": "CA",
+    "address-level2": "Mountain View",
+    "country": "US",
+    "guid": "68gjdh354j",
+    "name": "Mrs. Bar",
+    "postal-code": "94041",
+    "street-address": "P.O. Box 123",
+    "tel": "+1 650 555-5555",
+  },
+};
+
+let picker1;
+let el1;
+
+add_task(async function setup_once() {
+  let templateFrame = document.getElementById("templateFrame");
+  await SimpleTest.promiseFocus(templateFrame.contentWindow);
+
+  let displayEl = document.getElementById("display");
+  // Import the templates from the real shipping dialog to avoid duplication.
+  for (let template of templateFrame.contentDocument.querySelectorAll("template")) {
+    let imported = document.importNode(template, true);
+    displayEl.appendChild(imported);
+  }
+
+  el1 = document.createElement("payment-dialog");
+  displayEl.appendChild(el1);
+
+  sinon.spy(el1, "render");
+  sinon.spy(el1, "stateChangeCallback");
+});
+
+async function setup() {
+  el1.requestStore.setState({
+    changesPrevented: false,
+    completionState: "initial",
+    orderDetailsShowing: false,
+  });
+  await asyncElementRendered();
+  el1.render.reset();
+  el1.stateChangeCallback.reset();
+  picker1 = el1.querySelector("address-picker.payer-related");
+}
+
+add_task(async function test_empty() {
+  await setup();
+
+  ok(picker1, "Check picker1 exists");
+  let state = picker1.requestStore.getState();
+  let {paymentOptions} = state && state.request;
+  let payerRequested = paymentOptions.requestPayerName ||
+                   paymentOptions.requestPayerEmail ||
+                   paymentOptions.requestPayerPhone;
+  ok(!payerRequested, "Check initial state has no payer details requested");
+  is(picker1.dropdown.popupBox.children.length, 0, "Check dropdown is empty");
+});
+
+// paymentOptions properties are acurately reflected in the address-fields attribute
+add_task(async function test_visible_fields() {
+  await setup();
+  let request = picker1.requestStore.getState().request;
+  request = Object.assign({}, request, {
+    paymentOptions: {
+      requestPayerName: true,
+      requestPayerEmail: true,
+      requestPayerPhone: true,
+    },
+  });
+  picker1.requestStore.setState({
+    savedAddresses: SAVED_ADDRESSES,
+    selectedPayerAddress: "48bnds6854t",
+    request,
+  });
+
+  await asyncElementRendered();
+
+  is(picker1.dropdown.popupBox.children.length, 2, "Check dropdown has 2 addresses");
+
+  let visibleOptions = getVisiblePickerOptions(picker1);
+  let visibleOption = visibleOptions[0];
+  is(visibleOptions.length, 1, "One option should be visible");
+  is(visibleOption.getAttribute("guid"), "48bnds6854t", "expected option is visible");
+
+  for (let fieldName of ["name", "email", "tel"]) {
+    let elem = visibleOption.querySelector(`.${fieldName}`);
+    ok(elem, `field ${fieldName} exists`);
+    ok(isVisible(elem), `field ${fieldName} is visible`);
+  }
+  ok(!isVisible(visibleOption.querySelector(".street-address")), "street-address is not visible");
+});
+
+</script>
+
+</body>
+</html>