--- a/browser/components/payments/res/components/address-option.js
+++ b/browser/components/payments/res/components/address-option.js
@@ -57,17 +57,19 @@ export default class AddressOption exten
super.connectedCallback();
}
static formatSingleLineLabel(address) {
return PaymentDialogUtils.getAddressLabel(address);
}
render() {
- this._name.textContent = this.name;
- this["_street-address"].textContent = `${this.streetAddress} ` +
- `${this.addressLevel2} ${this.addressLevel1} ${this.postalCode} ${this.country}`;
- this._email.textContent = this.email;
- this._tel.textContent = this.tel;
+ // Fall back to empty strings to prevent 'null' from appearing.
+ this._name.textContent = this.name || "";
+ this["_street-address"].textContent =
+ `${this.streetAddress || ""} ${this.addressLevel2 || ""} ` +
+ `${this.addressLevel1 || ""} ${this.postalCode || ""} ${this.country || ""}`;
+ this._email.textContent = this.email || "";
+ this._tel.textContent = this.tel || "";
}
}
customElements.define("address-option", AddressOption);
--- a/browser/components/payments/res/components/basic-card-option.js
+++ b/browser/components/payments/res/components/basic-card-option.js
@@ -38,17 +38,21 @@ export default class BasicCardOption ext
connectedCallback() {
for (let name of ["cc-name", "cc-number", "cc-exp", "type"]) {
this.appendChild(this[`_${name}`]);
}
super.connectedCallback();
}
static formatSingleLineLabel(basicCard) {
- return basicCard["cc-number"] + " " + basicCard["cc-exp"] + " " + basicCard["cc-name"];
+ // Fall back to empty strings to prevent 'undefined' from appearing.
+ let ccNumber = basicCard["cc-number"] || "";
+ let ccExp = basicCard["cc-exp"] || "";
+ let ccName = basicCard["cc-name"] || "";
+ return ccNumber + " " + ccExp + " " + ccName;
}
render() {
this["_cc-name"].textContent = this.ccName;
this["_cc-number"].textContent = this.ccNumber;
this["_cc-exp"].textContent = this.ccExp;
this._type.textContent = this.type;
}
--- a/browser/components/payments/res/containers/address-picker.js
+++ b/browser/components/payments/res/containers/address-picker.js
@@ -24,32 +24,43 @@ export default class AddressPicker exten
attributeChangedCallback(name, oldValue, newValue) {
super.attributeChangedCallback(name, oldValue, newValue);
if (name == "address-fields" && oldValue !== newValue) {
this.render(this.requestStore.getState());
}
}
+ get fieldNames() {
+ if (this.hasAttribute("address-fields")) {
+ let names = this.getAttribute("address-fields").split(/\s+/);
+ if (names.length) {
+ return names;
+ }
+ }
+
+ return [
+ "address-level1",
+ "address-level2",
+ "country",
+ "name",
+ "postal-code",
+ "street-address",
+ ];
+ }
+
/**
* De-dupe and filter addresses for the given set of fields that will be visible
*
* @param {object} addresses
* @param {array?} fieldNames - optional list of field names that be used when
* de-duping and excluding entries
* @returns {object} filtered copy of given addresses
*/
- filterAddresses(addresses, fieldNames = [
- "address-level1",
- "address-level2",
- "country",
- "name",
- "postal-code",
- "street-address",
- ]) {
+ filterAddresses(addresses, fieldNames = this.fieldNames) {
let uniques = new Set();
let result = {};
for (let [guid, address] of Object.entries(addresses)) {
let addressCopy = {};
let isMatch = false;
// exclude addresses that are missing all of the requested fields
for (let name of fieldNames) {
if (address[name]) {
@@ -67,24 +78,17 @@ export default class AddressPicker exten
}
}
return result;
}
render(state) {
let addresses = paymentRequest.getAddresses(state);
let desiredOptions = [];
- let fieldNames;
- if (this.hasAttribute("address-fields")) {
- let names = this.getAttribute("address-fields").split(/\s+/);
- if (names.length) {
- fieldNames = names;
- }
- }
- let filteredAddresses = this.filterAddresses(addresses, fieldNames);
+ let filteredAddresses = this.filterAddresses(addresses, this.fieldNames);
for (let [guid, address] of Object.entries(filteredAddresses)) {
let optionEl = this.dropdown.getOptionByValue(guid);
if (!optionEl) {
optionEl = document.createElement("option");
optionEl.value = guid;
}
for (let key of AddressOption.recordAttributes) {
--- a/browser/components/payments/res/containers/payment-dialog.js
+++ b/browser/components/payments/res/containers/payment-dialog.js
@@ -237,23 +237,27 @@ export default class PaymentDialog exten
case "unknown": {
this._payButton.disabled = true;
this._payButton.textContent = this._payButton.dataset[completeStatus + "Label"];
break;
}
case "": {
// initial/default state
this._payButton.textContent = this._payButton.dataset.label;
+ const INVALID_CLASS_NAME = "invalid-selected-option";
this._payButton.disabled =
(state.request.paymentOptions.requestShipping &&
- (!this._shippingAddressPicker.value ||
- !this._shippingOptionPicker.value)) ||
+ (!this._shippingAddressPicker.selectedOption ||
+ this._shippingAddressPicker.classList.contains(INVALID_CLASS_NAME) ||
+ !this._shippingOptionPicker.selectedOption)) ||
(this._isPayerRequested(state.request.paymentOptions) &&
- !this._payerAddressPicker.value) ||
- !this._paymentMethodPicker.value ||
+ (!this._payerAddressPicker.selectedOption ||
+ this._payerAddressPicker.classList.contains(INVALID_CLASS_NAME))) ||
+ !this._paymentMethodPicker.selectedOption ||
+ this._paymentMethodPicker.classList.contains(INVALID_CLASS_NAME) ||
state.changesPrevented;
break;
}
case "fail":
case "timeout": {
// pay button is hidden in fail/timeout states.
this._payButton.textContent = this._payButton.dataset.label;
this._payButton.disabled = true;
@@ -309,17 +313,17 @@ export default class PaymentDialog exten
totalAmountEl.currency = totalItem.amount.currency;
// Show the total header on the address and basic card pages only during
// on-boarding(FTU) and on the payment summary page.
this._header.hidden = !state.page.onboardingWizard && state.page.id != "payment-summary";
this._orderDetailsOverlay.hidden = !state.orderDetailsShowing;
let genericError = "";
- if (this._shippingAddressPicker.value &&
+ if (this._shippingAddressPicker.selectedOption &&
(!request.paymentDetails.shippingOptions ||
!request.paymentDetails.shippingOptions.length)) {
genericError = this._errorText.dataset[shippingType + "GenericError"];
}
this._errorText.textContent = paymentDetails.error || genericError;
let paymentOptions = request.paymentOptions;
for (let element of this._shippingRelatedEls) {
--- a/browser/components/payments/res/containers/payment-method-picker.js
+++ b/browser/components/payments/res/containers/payment-method-picker.js
@@ -24,16 +24,23 @@ export default class PaymentMethodPicker
this.securityCodeInput.addEventListener("change", this);
}
connectedCallback() {
super.connectedCallback();
this.dropdown.after(this.securityCodeInput);
}
+ get fieldNames() {
+ let fieldNames = [...BasicCardOption.recordAttributes];
+ // Type is not a required field though it may be present.
+ fieldNames.splice(fieldNames.indexOf("type"), 1);
+ return fieldNames;
+ }
+
render(state) {
let basicCards = paymentRequest.getBasicCards(state);
let desiredOptions = [];
for (let [guid, basicCard] of Object.entries(basicCards)) {
let optionEl = this.dropdown.getOptionByValue(guid);
if (!optionEl) {
optionEl = document.createElement("option");
optionEl.value = guid;
--- a/browser/components/payments/res/containers/rich-picker.css
+++ b/browser/components/payments/res/containers/rich-picker.css
@@ -2,17 +2,18 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
.rich-picker {
display: grid;
grid-template-columns: 5fr auto auto;
grid-template-areas:
"label edit add"
- "dropdown dropdown dropdown";
+ "dropdown dropdown dropdown"
+ "invalid invalid invalid";
}
.rich-picker > label {
color: #0c0c0d;
font-weight: 700;
grid-area: label;
}
@@ -29,22 +30,37 @@
grid-area: edit;
border-right: 1px solid #0C0C0D33;
}
.rich-picker > rich-select {
grid-area: dropdown;
}
+.invalid-selected-option > rich-select {
+ outline: 1px solid #c70011;
+}
+
+.rich-picker > .invalid-label {
+ grid-area: invalid;
+ font-weight: normal;
+ color: #c70011;
+}
+
+:not(.invalid-selected-option) > .invalid-label {
+ display: none;
+}
+
/* Payment Method Picker */
payment-method-picker.rich-picker {
grid-template-columns: 20fr 1fr auto auto;
grid-template-areas:
"label spacer edit add"
- "dropdown cvv cvv cvv";
+ "dropdown cvv cvv cvv"
+ "invalid invalid invalid invalid";
}
payment-method-picker > input {
border: 1px solid #0C0C0D33;
border-left: none;
grid-area: cvv;
margin: 14px 0; /* Has to be same as rich-select */
padding: 8px;
--- a/browser/components/payments/res/containers/rich-picker.js
+++ b/browser/components/payments/res/containers/rich-picker.js
@@ -27,34 +27,58 @@ export default class RichPicker extends
this.addLink.textContent = this.dataset.addLinkLabel;
this.addLink.addEventListener("click", this);
this.editLink = document.createElement("a");
this.editLink.className = "edit-link";
this.editLink.href = "javascript:void(0)";
this.editLink.textContent = this.dataset.editLinkLabel;
this.editLink.addEventListener("click", this);
+
+ this.invalidLabel = document.createElement("label");
+ this.invalidLabel.className = "invalid-label";
+ this.invalidLabel.setAttribute("for", this.dropdown.popupBox.id);
+ this.invalidLabel.textContent = this.dataset.invalidLabel;
}
connectedCallback() {
// The document order, by default, controls tab order so keep that in mind if changing this.
this.appendChild(this.labelElement);
this.appendChild(this.dropdown);
this.appendChild(this.editLink);
this.appendChild(this.addLink);
+ this.appendChild(this.invalidLabel);
super.connectedCallback();
}
attributeChangedCallback(name, oldValue, newValue) {
if (name == "label") {
this.labelElement.textContent = newValue;
}
}
render(state) {
this.editLink.hidden = !this.dropdown.value;
+
+ this.classList.toggle("invalid-selected-option",
+ this.missingFieldsOfSelectedOption().length);
}
- get value() {
+ get selectedOption() {
return this.dropdown &&
this.dropdown.selectedOption;
}
+
+ get fieldNames() {
+ return [];
+ }
+
+ missingFieldsOfSelectedOption() {
+ let selectedOption = this.selectedOption;
+ if (!selectedOption) {
+ return [];
+ }
+
+ let fieldNames = this.fieldNames;
+ // Return all field names that are empty or missing from the option.
+ return fieldNames.filter(name => !selectedOption.getAttribute(name));
+ }
}
--- a/browser/components/payments/res/debugging.js
+++ b/browser/components/payments/res/debugging.js
@@ -195,16 +195,22 @@ let ADDRESSES_1 = {
"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",
},
+ "abcde12345": {
+ "address-level2": "Mountain View",
+ "country": "US",
+ "guid": "abcde12345",
+ "name": "Mrs. Fields",
+ },
};
let DUPED_ADDRESSES = {
"a9e830667189": {
"street-address": "Unit 1\n1505 Northeast Kentucky Industrial Parkway \n",
"address-level2": "Greenup",
"address-level1": "KY",
"postal-code": "41144",
@@ -280,16 +286,30 @@ let BASIC_CARDS_1 = {
"cc-name": "Jane Doe",
"cc-exp-month": 5,
"cc-exp-year": 2023,
"cc-given-name": "Jane",
"cc-additional-name": "",
"cc-family-name": "Doe",
"cc-exp": "2023-05",
},
+ "123456789abc": {
+ methodName: "basic-card",
+ "cc-number": "************1234",
+ "guid": "123456789abc",
+ "version": 1,
+ "timeCreated": 1517890536491,
+ "timeLastModified": 1517890564518,
+ "timeLastUsed": 0,
+ "timesUsed": 0,
+ "cc-name": "Jane Fields",
+ "cc-given-name": "Jane",
+ "cc-additional-name": "",
+ "cc-family-name": "Fields",
+ },
};
let buttonActions = {
debugFrame() {
let event = new CustomEvent("paymentContentToChrome", {
bubbles: true,
detail: {
messageType: "debugFrame",
--- a/browser/components/payments/res/paymentRequest.xhtml
+++ b/browser/components/payments/res/paymentRequest.xhtml
@@ -69,16 +69,17 @@
<!ENTITY failErrorPage.suggestion3 "If no other solutions work, check with **host-name**.">
<!ENTITY failErrorPage.doneButton.label "OK">
<!ENTITY timeoutErrorPage.title "Whoops! **host-name** took too long to respond.">
<!ENTITY timeoutErrorPage.suggestion1 "Try again later.">
<!ENTITY timeoutErrorPage.suggestion2 "Check your network connection." >
<!ENTITY timeoutErrorPage.suggestion3 "If no other solutions work, check with **host-name**.">
<!ENTITY timeoutErrorPage.doneButton.label "OK">
<!ENTITY webPaymentsBranding.label "&brandShortName; Checkout">
+ <!ENTITY invalidOption.label "Missing or invalid information">
]>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>&paymentSummaryTitle;</title>
<!-- chrome: is needed for global.dtd -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self' chrome:"/>
@@ -123,33 +124,36 @@
<payment-request-page id="payment-summary">
<div class="page-body">
<address-picker class="shipping-related"
data-add-link-label="&address.addLink.label;"
data-edit-link-label="&address.editLink.label;"
data-shipping-address-label="&shippingAddressLabel;"
data-delivery-address-label="&deliveryAddressLabel;"
data-pickup-address-label="&pickupAddressLabel;"
+ data-invalid-label="&invalidOption.label;"
selected-state-key="selectedShippingAddress"></address-picker>
<shipping-option-picker class="shipping-related"
data-shipping-options-label="&shippingOptionsLabel;"
data-delivery-options-label="&deliveryOptionsLabel;"
data-pickup-options-label="&pickupOptionsLabel;"></shipping-option-picker>
<payment-method-picker selected-state-key="selectedPaymentCard"
data-add-link-label="&basicCard.addLink.label;"
data-edit-link-label="&basicCard.editLink.label;"
data-cvv-placeholder="&basicCard.cvv.placeholder;"
+ data-invalid-label="&invalidOption.label;"
label="&paymentMethodsLabel;">
</payment-method-picker>
<address-picker class="payer-related"
label="&payerLabel;"
data-add-link-label="&payer.addLink.label;"
data-edit-link-label="&payer.editLink.label;"
+ data-invalid-label="&invalidOption.label;"
selected-state-key="selectedPayerAddress"></address-picker>
</div>
<footer>
<span id="branding">&webPaymentsBranding.label;</span>
<button id="cancel">&cancelPaymentButton.label;</button>
<button id="pay"
class="primary"
--- a/browser/components/payments/test/PaymentTestUtils.jsm
+++ b/browser/components/payments/test/PaymentTestUtils.jsm
@@ -477,16 +477,20 @@ var PaymentTestUtils = {
"cc-number": "4111111111111111",
},
JaneMasterCard: {
"cc-exp-month": 12,
"cc-exp-year": (new Date()).getFullYear() + 9,
"cc-name": "Jane McMaster-Card",
"cc-number": "5555555555554444",
},
+ MissingFields: {
+ "cc-name": "Missy Fields",
+ "cc-number": "340000000000009",
+ },
Temp: {
"cc-exp-month": 12,
"cc-exp-year": (new Date()).getFullYear() + 9,
"cc-name": "Temp Name",
"cc-number": "5105105105105100",
},
},
};
--- a/browser/components/payments/test/mochitest/test_address_picker.html
+++ b/browser/components/payments/test/mochitest/test_address_picker.html
@@ -8,16 +8,17 @@ Test the address-picker component
<title>Test the address-picker component</title>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="/tests/SimpleTest/AddTask.js"></script>
<script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
<script src="payments_common.js"></script>
<script src="../../res/vendor/custom-elements.min.js"></script>
<script src="../../res/unprivileged-fallbacks.js"></script>
+ <link rel="stylesheet" type="text/css" href="../../res/containers/rich-picker.css"/>
<link rel="stylesheet" type="text/css" href="../../res/components/rich-select.css"/>
<link rel="stylesheet" type="text/css" href="../../res/components/address-option.css"/>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<p id="display">
<address-picker id="picker1"
selected-state-key="selectedShippingAddress"></address-picker>
@@ -60,23 +61,30 @@ add_task(async function test_initialSet(
"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",
},
+ "abcde12345": {
+ "address-level2": "Mountain View",
+ "country": "US",
+ "guid": "abcde12345",
+ "name": "Mrs. Fields",
+ },
},
});
await asyncElementRendered();
let options = picker1.dropdown.popupBox.children;
- is(options.length, 2, "Check dropdown has both addresses");
+ is(options.length, 3, "Check dropdown has all addresses");
ok(options[0].textContent.includes("Mr. Foo"), "Check first address");
ok(options[1].textContent.includes("Mrs. Bar"), "Check second address");
+ ok(options[2].textContent.includes("Mrs. Fields"), "Check third address");
});
add_task(async function test_update() {
picker1.requestStore.setState({
savedAddresses: {
"48bnds6854t": {
// Same GUID, different values to trigger an update
"address-level1": "MI-edit",
@@ -93,60 +101,97 @@ add_task(async function test_update() {
"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",
},
+ "abcde12345": {
+ "address-level2": "Mountain View",
+ "country": "US",
+ "guid": "abcde12345",
+ "name": "Mrs. Fields",
+ },
},
});
await asyncElementRendered();
let options = picker1.dropdown.popupBox.children;
- is(options.length, 2, "Check dropdown still has both addresses");
+ is(options.length, 3, "Check dropdown still has all addresses");
ok(options[0].textContent.includes("Mr. Foo-edit"), "Check updated name in first address");
ok(!options[0].getAttribute("address-level2"), "Check removed first address-level2");
ok(options[1].textContent.includes("Mrs. Bar"), "Check that name is the same in second address");
ok(options[1].getAttribute("street-address").includes("P.O. Box 123"),
"Check second address is the same");
});
add_task(async function test_change_selected_address() {
let options = picker1.dropdown.popupBox.children;
is(picker1.dropdown.selectedOption, null, "Should default to no selected option");
is(picker1.editLink.hidden, true, "Picker edit link should be hidden when no option is selected");
let {selectedShippingAddress} = picker1.requestStore.getState();
is(selectedShippingAddress, null, "store should have no option selected");
+ ok(!picker1.classList.contains("invalid-selected-option"), "No validation on an empty selection");
+ ok(isHidden(picker1.invalidLabel), "The invalid label should be hidden");
+
+ picker1.dropdown.popupBox.focus();
+ synthesizeKey(options[2].getAttribute("name"), {});
+ await asyncElementRendered();
+
+ let selectedOption = picker1.dropdown.selectedOption;
+ is(selectedOption, options[2], "Selected option should now be the third option");
+ selectedShippingAddress = picker1.requestStore.getState().selectedShippingAddress;
+ is(selectedShippingAddress, selectedOption.getAttribute("guid"),
+ "store should have third option selected");
+ // The third option is missing some fields. Make sure that it is marked as such.
+ ok(picker1.classList.contains("invalid-selected-option"), "The third option is missing fields");
+ ok(!isHidden(picker1.invalidLabel), "The invalid label should be visible");
picker1.dropdown.popupBox.focus();
synthesizeKey(options[1].getAttribute("name"), {});
await asyncElementRendered();
- let selectedOption = picker1.dropdown.selectedOption;
+ selectedOption = picker1.dropdown.selectedOption;
is(selectedOption, options[1], "Selected option should now be the second option");
selectedShippingAddress = picker1.requestStore.getState().selectedShippingAddress;
is(selectedShippingAddress, selectedOption.getAttribute("guid"),
"store should have second option selected");
+ ok(!picker1.classList.contains("invalid-selected-option"), "The second option has all fields");
+ ok(isHidden(picker1.invalidLabel), "The invalid label should be hidden");
});
add_task(async function test_streetAddress_combines_street_level2_level1_postalCode_country() {
let options = picker1.dropdown.popupBox.children;
let richoption1 = picker1.dropdown.querySelector(".rich-select-selected-option");
let streetAddress = richoption1.querySelector(".street-address");
/* eslint-disable max-len */
is(streetAddress.textContent,
- `${options[1].getAttribute("street-address")} ${options[1].getAttribute("address-level2")} ${options[1].getAttribute("address-level1")} ${options[1].getAttribute("postal-code")} ${options[1].getAttribute("country")}`);
+ `${options[1].getAttribute("street-address")} ${options[1].getAttribute("address-level2")} ${options[1].getAttribute("address-level1")} ${options[1].getAttribute("postal-code")} ${options[1].getAttribute("country")}`,
+ "The address shown should be human readable and include all fields");
/* eslint-enable max-len */
+
+ picker1.dropdown.popupBox.focus();
+ synthesizeKey(options[2].getAttribute("name"), {});
+ await asyncElementRendered();
+
+ richoption1 = picker1.dropdown.querySelector(".rich-select-selected-option");
+ streetAddress = richoption1.querySelector(".street-address");
+ is(streetAddress.textContent, " Mountain View US",
+ "The address shown should be human readable and include all fields");
+
+ picker1.dropdown.popupBox.focus();
+ synthesizeKey(options[1].getAttribute("name"), {});
+ await asyncElementRendered();
});
add_task(async function test_delete() {
picker1.requestStore.setState({
savedAddresses: {
- // 48bnds6854t was deleted
+ // 48bnds6854t and abcde12345 was deleted
"68gjdh354j": {
"address-level1": "CA",
"address-level2": "Mountain View",
"country": "US",
"guid": "68gjdh354j",
"name": "Mrs. Bar",
"postal-code": "94041",
"street-address": "P.O. Box 123",
--- a/browser/components/payments/test/mochitest/test_payment_dialog.html
+++ b/browser/components/payments/test/mochitest/test_payment_dialog.html
@@ -171,17 +171,17 @@ add_task(async function test_generic_err
"tel": "+1 650 555-5555",
},
},
selectedShippingAddress: "48bnds6854t",
});
await asyncElementRendered();
let picker = el1._shippingAddressPicker;
- ok(picker.value, "Address picker should have a selected value");
+ ok(picker.selectedOption, "Address picker should have a selected option");
is(el1._errorText.textContent, SHIPPING_GENERIC_ERROR,
"Generic error message should be shown when no shipping options or error are provided");
});
add_task(async function test_processing_completeStatus() {
// "processing": has overlay. Check button visibility
await setup();
let {request} = el1.requestStore.getState();
@@ -289,18 +289,16 @@ add_task(async function test_picker_labe
]) {
let request = deepClone(el1.requestStore.getState().request);
request.paymentOptions.requestShipping = true;
request.paymentOptions.shippingType = shippingType;
await el1.requestStore.setState({ request });
await asyncElementRendered();
is(picker.labelElement.textContent, label,
`Label should be appropriate for ${shippingType}`);
- info(JSON.stringify(el1.requestStore.getState(), null, 2));
- info(picker.outerHTML);
}
});
add_task(async function test_disconnect() {
await setup();
el1.remove();
await el1.requestStore.setState({orderDetailsShowing: true});
--- 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
@@ -66,16 +66,17 @@ async function setup({shippingRequired,
amount: {
currency: "USD",
value: 20,
},
selected: false,
}] : null;
state.request.paymentOptions.requestShipping = shippingRequired;
state.request.paymentOptions.requestPayerName = payerRequired;
+ state.request.paymentOptions.requestPayerPhone = payerRequired;
state.savedAddresses = shippingRequired || payerRequired ? {
"48bnds6854t": {
"address-level1": "MI",
"address-level2": "Some City",
"country": "US",
"guid": "48bnds6854t",
"name": "Mr. Foo",
"postal-code": "90210",
@@ -87,35 +88,59 @@ async function setup({shippingRequired,
"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",
},
+ "abcdef1234": {
+ "address-level1": "CA",
+ "address-level2": "Mountain View",
+ "country": "US",
+ "guid": "abcdef1234",
+ "name": "Mrs. Fields",
+ },
} : {};
state.savedBasicCards = {
- "john-doe": Object.assign({methodName: "basic-card"}, deepClone(PTU.BasicCards.JohnDoe)),
+ "john-doe": Object.assign({
+ "cc-exp": (new Date()).getFullYear() + 9 + "-01",
+ methodName: "basic-card",
+ guid: "aaa1",
+ }, deepClone(PTU.BasicCards.JohnDoe)),
+ "missing-fields": Object.assign({
+ methodName: "basic-card",
+ guid: "aaa2",
+ }, deepClone(PTU.BasicCards.MissingFields)),
};
state.selectedPayerAddress = null;
state.selectedPaymentCard = null;
state.selectedShippingAddress = null;
state.selectedShippingOption = null;
await el1.requestStore.setState(state);
await asyncElementRendered();
}
function selectFirstItemOfPicker(picker) {
picker.dropdown.popupBox.focus();
let options = picker.dropdown.popupBox.children;
+ info(`Selecting "${options[0].textContent}" from the options`);
synthesizeKey(options[0].textContent, {});
ok(picker.dropdown.selectedOption, `Option should be selected for ${picker.localName}`);
}
+function selectLastItemOfPicker(picker) {
+ picker.dropdown.popupBox.focus();
+ let options = picker.dropdown.popupBox.children;
+ let lastOption = options[options.length - 1];
+ synthesizeKey(lastOption.textContent, {});
+ ok(picker.dropdown.selectedOption, `Option should be selected for ${picker.localName}`);
+}
+
add_task(async function runTests() {
let allPickers = {
shippingAddress: el1._shippingAddressPicker,
shippingOption: el1._shippingOptionPicker,
paymentMethod: el1._paymentMethodPicker,
payerAddress: el1._payerAddressPicker,
};
let testCases = [
@@ -153,14 +178,32 @@ add_task(async function runTests() {
let payButton = document.getElementById("pay");
ok(payButton.disabled, "Button is disabled when required options are not selected");
let stateChangedPromise = promiseStateChange(el1.requestStore);
testCase.pickers.forEach(selectFirstItemOfPicker);
await stateChangedPromise;
ok(!payButton.disabled, "Button is enabled when required options are selected");
+
+ // Individually toggle each picker to see how the missing fields affects Pay button.
+ for (let picker of testCase.pickers) {
+ // There is no "invalid" option for shipping options.
+ if (picker != allPickers.shippingOption) {
+ stateChangedPromise = promiseStateChange(el1.requestStore);
+ selectLastItemOfPicker(picker);
+ await stateChangedPromise;
+
+ ok(payButton.disabled, "Button is disabled when selected option has missing fields");
+ }
+
+ stateChangedPromise = promiseStateChange(el1.requestStore);
+ selectFirstItemOfPicker(picker);
+ await stateChangedPromise;
+
+ ok(!payButton.disabled, "Button is enabled when selected option has all required fields");
+ }
}
});
</script>
</body>
</html>
--- a/browser/components/payments/test/mochitest/test_payment_method_picker.html
+++ b/browser/components/payments/test/mochitest/test_payment_method_picker.html
@@ -8,16 +8,17 @@ Test the payment-method-picker component
<title>Test the payment-method-picker component</title>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="/tests/SimpleTest/AddTask.js"></script>
<script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
<script src="payments_common.js"></script>
<script src="../../res/vendor/custom-elements.min.js"></script>
<script src="../../res/unprivileged-fallbacks.js"></script>
+ <link rel="stylesheet" type="text/css" href="../../res/containers/rich-picker.css"/>
<link rel="stylesheet" type="text/css" href="../../res/components/rich-select.css"/>
<link rel="stylesheet" type="text/css" href="../../res/components/basic-card-option.css"/>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<p id="display">
<payment-method-picker id="picker1"
selected-state-key="selectedPaymentCard"></payment-method-picker>
@@ -56,23 +57,32 @@ add_task(async function test_initialSet(
"68gjdh354j": {
"cc-exp": "2017-08",
"cc-exp-month": 8,
"cc-exp-year": 2017,
"cc-name": "J Smith",
"cc-number": "***********1234",
"guid": "68gjdh354j",
},
+ "123456789abc": {
+ "cc-name": "Jane Fields",
+ "cc-given-name": "Jane",
+ "cc-additional-name": "",
+ "cc-family-name": "Fields",
+ "cc-number": "************9876",
+ "guid": "123456789abc",
+ },
},
});
await asyncElementRendered();
let options = picker1.dropdown.popupBox.children;
- is(options.length, 2, "Check dropdown has both cards");
+ is(options.length, 3, "Check dropdown has all three cards");
ok(options[0].textContent.includes("John Doe"), "Check first card");
ok(options[1].textContent.includes("J Smith"), "Check second card");
+ ok(options[2].textContent.includes("Jane Fields"), "Check third card");
});
add_task(async function test_update() {
picker1.requestStore.setState({
savedBasicCards: {
"48bnds6854t": {
// Same GUID, different values to trigger an update
"cc-exp": "2017-09",
@@ -85,52 +95,82 @@ add_task(async function test_update() {
"68gjdh354j": {
"cc-exp": "2017-08",
"cc-exp-month": 8,
"cc-exp-year": 2017,
"cc-name": "J Smith",
"cc-number": "***********1234",
"guid": "68gjdh354j",
},
+ "123456789abc": {
+ "cc-name": "Jane Fields",
+ "cc-given-name": "Jane",
+ "cc-additional-name": "",
+ "cc-family-name": "Fields",
+ "cc-number": "************9876",
+ "guid": "123456789abc",
+ },
},
});
await asyncElementRendered();
let options = picker1.dropdown.popupBox.children;
- is(options.length, 2, "Check dropdown still has both cards");
+ is(options.length, 3, "Check dropdown still has three cards");
ok(!options[0].textContent.includes("John Doe"), "Check cleared first cc-name");
ok(options[0].textContent.includes("9876"), "Check updated first cc-number");
ok(options[0].textContent.includes("09"), "Check updated first exp-month");
ok(options[1].textContent.includes("J Smith"), "Check second card is the same");
+ ok(options[2].textContent.includes("Jane Fields"), "Check third card is the same");
});
add_task(async function test_change_selected_card() {
let options = picker1.dropdown.popupBox.children;
is(picker1.dropdown.selectedOption, null, "Should default to no selected option");
let {
selectedPaymentCard,
selectedPaymentCardSecurityCode,
} = picker1.requestStore.getState();
is(selectedPaymentCard, null, "store should have no option selected");
is(selectedPaymentCardSecurityCode, null, "store should have no security code");
+ ok(!picker1.classList.contains("invalid-selected-option"), "No validation on an empty selection");
+ ok(isHidden(picker1.invalidLabel), "The invalid label should be hidden");
await SimpleTest.promiseFocus();
picker1.dropdown.popupBox.focus();
- synthesizeKey("************1234", {});
+ synthesizeKey("************9876", {});
await asyncElementRendered();
ok(true, "Focused the security code field");
ok(!picker1.open, "Picker should be closed");
let selectedOption = picker1.dropdown.selectedOption;
+ is(selectedOption, options[2], "Selected option should now be the third option");
+ selectedPaymentCard = picker1.requestStore.getState().selectedPaymentCard;
+ is(selectedPaymentCard, selectedOption.getAttribute("guid"),
+ "store should have third option selected");
+ selectedPaymentCardSecurityCode = picker1.requestStore.getState().selectedPaymentCardSecurityCode;
+ is(selectedPaymentCardSecurityCode, null, "store should have empty security code");
+ ok(picker1.classList.contains("invalid-selected-option"), "Missing fields for the third option");
+ ok(!isHidden(picker1.invalidLabel), "The invalid label should be visible");
+
+ await SimpleTest.promiseFocus();
+ picker1.dropdown.popupBox.focus();
+ synthesizeKey("***********1234", {});
+ await asyncElementRendered();
+ ok(true, "Focused the security code field");
+ ok(!picker1.open, "Picker should be closed");
+
+ selectedOption = picker1.dropdown.selectedOption;
is(selectedOption, options[1], "Selected option should now be the second option");
selectedPaymentCard = picker1.requestStore.getState().selectedPaymentCard;
is(selectedPaymentCard, selectedOption.getAttribute("guid"),
"store should have second option selected");
selectedPaymentCardSecurityCode = picker1.requestStore.getState().selectedPaymentCardSecurityCode;
is(selectedPaymentCardSecurityCode, null, "store should have empty security code");
+ ok(!picker1.classList.contains("invalid-selected-option"), "The second option has all fields");
+ ok(isHidden(picker1.invalidLabel), "The invalid label should be hidden");
let stateChangePromise = promiseStateChange(picker1.requestStore);
// Type in the security code field
picker1.securityCodeInput.focus();
sendString("836");
sendKey("Tab");
let state = await stateChangePromise;
@@ -144,19 +184,28 @@ add_task(async function test_delete() {
"68gjdh354j": {
"cc-exp": "2017-08",
"cc-exp-month": 8,
"cc-exp-year": 2017,
"cc-name": "J Smith",
"cc-number": "***********1234",
"guid": "68gjdh354j",
},
+ "123456789abc": {
+ "cc-name": "Jane Fields",
+ "cc-given-name": "Jane",
+ "cc-additional-name": "",
+ "cc-family-name": "Fields",
+ "cc-number": "************9876",
+ "guid": "123456789abc",
+ },
},
});
await asyncElementRendered();
let options = picker1.dropdown.popupBox.children;
- is(options.length, 1, "Check dropdown has one remaining card");
- ok(options[0].textContent.includes("J Smith"), "Check remaining card");
+ is(options.length, 2, "Check dropdown has two remaining cards");
+ ok(options[0].textContent.includes("J Smith"), "Check remaining card #1");
+ ok(options[1].textContent.includes("Jane Fields"), "Check remaining card #2");
});
</script>
</body>
</html>