Bug 1482689 - Use AddressPicker for the card billing address UI. r=MattN

* New BillingAddressPicker subclass of AddressPicker which just overrides some of the state-related behavior that
* Allow the RichSelect's popupBox (<select>) to be assigned after the constructor
* A couple new mochitests for the new/different behavior
* Update the test to expect 'edit' to be hidden when the empty option is selected

Differential Revision: https://phabricator.services.mozilla.com/D9321

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Sam Foster 2018-11-06 00:28:46 +00:00
Родитель 04e84ad672
Коммит aa95912b8b
12 изменённых файлов: 503 добавлений и 220 удалений

Просмотреть файл

@ -24,10 +24,12 @@ export default class RichSelect extends ObservedPropertiesMixin(HTMLElement) {
constructor() { constructor() {
super(); super();
this.popupBox = document.createElement("select"); this.popupBox = document.createElement("select");
this.popupBox.addEventListener("change", this);
} }
connectedCallback() { connectedCallback() {
// the popupBox element may change in between constructor and being connected
// so wait until connected before listening to events on it
this.popupBox.addEventListener("change", this);
this.appendChild(this.popupBox); this.appendChild(this.popupBox);
this.render(); this.render();
} }
@ -74,7 +76,7 @@ export default class RichSelect extends ObservedPropertiesMixin(HTMLElement) {
if (this.value) { if (this.value) {
let optionType = this.getAttribute("option-type"); let optionType = this.getAttribute("option-type");
if (selectedRichOption.localName != optionType) { if (!selectedRichOption || selectedRichOption.localName != optionType) {
selectedRichOption = document.createElement(optionType); selectedRichOption = document.createElement(optionType);
} }

Просмотреть файл

@ -33,7 +33,10 @@ export default class AddressPicker extends RichPicker {
attributeChangedCallback(name, oldValue, newValue) { attributeChangedCallback(name, oldValue, newValue) {
super.attributeChangedCallback(name, oldValue, newValue); super.attributeChangedCallback(name, oldValue, newValue);
if (AddressPicker.pickerAttributes.includes(name) && oldValue !== newValue) { // connectedCallback may add and adjust elements & values
// so avoid calling render before the element is connected
if (this.isConnected &&
AddressPicker.pickerAttributes.includes(name) && oldValue !== newValue) {
this.render(this.requestStore.getState()); this.render(this.requestStore.getState());
} }
} }
@ -89,7 +92,26 @@ export default class AddressPicker extends RichPicker {
return result; return result;
} }
get options() {
return this.dropdown.popupBox.options;
}
/**
* @param {object} state - See `PaymentsStore.setState`
* The value of the picker is retrieved from state store rather than the DOM
* @returns {string} guid
*/
getCurrentValue(state) {
let [selectedKey, selectedLeaf] = this.selectedStateKey.split("|");
let guid = state[selectedKey];
if (selectedLeaf) {
guid = guid[selectedLeaf];
}
return guid;
}
render(state) { render(state) {
let selectedAddressGUID = this.getCurrentValue(state) || "";
let addresses = paymentRequest.getAddresses(state); let addresses = paymentRequest.getAddresses(state);
let desiredOptions = []; let desiredOptions = [];
let filteredAddresses = this.filterAddresses(addresses, this.fieldNames); let filteredAddresses = this.filterAddresses(addresses, this.fieldNames);
@ -131,12 +153,18 @@ export default class AddressPicker extends RichPicker {
} }
this.dropdown.popupBox.textContent = ""; this.dropdown.popupBox.textContent = "";
if (this._allowEmptyOption) {
let optionEl = document.createElement("option");
optionEl.value = "";
desiredOptions.unshift(optionEl);
}
for (let option of desiredOptions) { for (let option of desiredOptions) {
this.dropdown.popupBox.appendChild(option); this.dropdown.popupBox.appendChild(option);
} }
// Update selectedness after the options are updated // Update selectedness after the options are updated
let selectedAddressGUID = state[this.selectedStateKey];
this.dropdown.value = selectedAddressGUID; this.dropdown.value = selectedAddressGUID;
if (selectedAddressGUID && selectedAddressGUID !== this.dropdown.value) { if (selectedAddressGUID && selectedAddressGUID !== this.dropdown.value) {
@ -161,8 +189,8 @@ export default class AddressPicker extends RichPicker {
return ""; return "";
} }
let merchantFieldErrors = AddressForm.merchantFieldErrorsForForm(state, let merchantFieldErrors = AddressForm.merchantFieldErrorsForForm(
[this.selectedStateKey]); state, this.selectedStateKey.split("|"));
// TODO: errors in priority order. // TODO: errors in priority order.
return Object.values(merchantFieldErrors).find(msg => { return Object.values(merchantFieldErrors).find(msg => {
return typeof(msg) == "string" && msg.length; return typeof(msg) == "string" && msg.length;
@ -182,12 +210,23 @@ export default class AddressPicker extends RichPicker {
} }
onChange(event) { onChange(event) {
let selectedKey = this.selectedStateKey; let [selectedKey, selectedLeaf] = this.selectedStateKey.split("|");
if (selectedKey) { if (!selectedKey) {
this.requestStore.setState({ return;
[selectedKey]: this.dropdown.value,
});
} }
// selectedStateKey can be a '|' delimited string indicating a path into the state object
// to update with the new value
let newState = {};
if (selectedLeaf) {
let currentState = this.requestStore.getState();
newState[selectedKey] = Object.assign({},
currentState[selectedKey],
{ [selectedLeaf]: this.dropdown.value });
} else {
newState[selectedKey] = this.dropdown.value;
}
this.requestStore.setState(newState);
} }
onClick({target}) { onClick({target}) {
@ -197,7 +236,7 @@ export default class AddressPicker extends RichPicker {
}, },
"address-page": { "address-page": {
addressFields: this.getAttribute("address-fields"), addressFields: this.getAttribute("address-fields"),
selectedStateKey: [this.selectedStateKey], selectedStateKey: this.selectedStateKey.split("|"),
}, },
}; };
@ -208,9 +247,8 @@ export default class AddressPicker extends RichPicker {
break; break;
} }
case this.editLink: { case this.editLink: {
let state = this.requestStore.getState(); let currentState = this.requestStore.getState();
let selectedAddressGUID = state[this.selectedStateKey]; nextState["address-page"].guid = this.getCurrentValue(currentState);
nextState["address-page"].guid = selectedAddressGUID;
nextState["address-page"].title = this.dataset.editAddressTitle; nextState["address-page"].title = this.dataset.editAddressTitle;
break; break;
} }

Просмотреть файл

@ -31,11 +31,6 @@ basic-card-form .editCreditCardForm .persist-checkbox {
display: grid; display: grid;
} }
#billingAddressGUID {
/* XXX: temporary until converted to a rich-picker in bug 1482689 */
margin: 14px 0;
}
basic-card-form > footer > .cancel-button { basic-card-form > footer > .cancel-button {
/* When cancel is shown (during onboarding), it should always be on the left with a space after it */ /* When cancel is shown (during onboarding), it should always be on the left with a space after it */
margin-right: auto; margin-right: auto;

Просмотреть файл

@ -4,6 +4,7 @@
/* import-globals-from ../../../../../browser/extensions/formautofill/content/autofillEditForms.js*/ /* import-globals-from ../../../../../browser/extensions/formautofill/content/autofillEditForms.js*/
import AcceptedCards from "../components/accepted-cards.js"; import AcceptedCards from "../components/accepted-cards.js";
import BillingAddressPicker from "./billing-address-picker.js";
import CscInput from "../components/csc-input.js"; import CscInput from "../components/csc-input.js";
import LabelledCheckbox from "../components/labelled-checkbox.js"; import LabelledCheckbox from "../components/labelled-checkbox.js";
import PaymentRequestPage from "../components/payment-request-page.js"; import PaymentRequestPage from "../components/payment-request-page.js";
@ -27,15 +28,6 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe
this.genericErrorText.setAttribute("aria-live", "polite"); this.genericErrorText.setAttribute("aria-live", "polite");
this.genericErrorText.classList.add("page-error"); this.genericErrorText.classList.add("page-error");
this.addressAddLink = document.createElement("a");
this.addressAddLink.className = "add-link";
this.addressAddLink.href = "javascript:void(0)";
this.addressAddLink.addEventListener("click", this);
this.addressEditLink = document.createElement("a");
this.addressEditLink.className = "edit-link";
this.addressEditLink.href = "javascript:void(0)";
this.addressEditLink.addEventListener("click", this);
this.cscInput = new CscInput({ this.cscInput = new CscInput({
useAlwaysVisiblePlaceholder: true, useAlwaysVisiblePlaceholder: true,
inputId: "cc-csc", inputId: "cc-csc",
@ -85,6 +77,38 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe
}); });
} }
_upgradeBillingAddressPicker() {
let addressRow = this.form.querySelector(".billingAddressRow");
let addressPicker = this.billingAddressPicker = new BillingAddressPicker();
// Wrap the existing <select> that the formHandler manages
if (addressPicker.dropdown.popupBox) {
addressPicker.dropdown.popupBox.remove();
}
addressPicker.dropdown.popupBox = this.form.querySelector("#billingAddressGUID");
// Hide the original label as the address picker provide its own,
// but we'll copy the localized textContent from it when rendering
addressRow.querySelector(".label-text").hidden = true;
addressPicker.dataset.addLinkLabel = this.dataset.addressAddLinkLabel;
addressPicker.dataset.editLinkLabel = this.dataset.addressEditLinkLabel;
addressPicker.dataset.fieldSeparator = this.dataset.addressFieldSeparator;
addressPicker.dataset.addAddressTitle = this.dataset.billingAddressTitleAdd;
addressPicker.dataset.editAddressTitle = this.dataset.billingAddressTitleEdit;
addressPicker.dataset.invalidLabel = this.dataset.invalidAddressLabel;
// break-after-nth-field, address-fields not needed here
// this state is only used to carry the selected guid between pages;
// the select#billingAddressGUID is the source of truth for the current value
addressPicker.setAttribute("selected-state-key", "basic-card-page|billingAddressGUID");
addressPicker.addLink.addEventListener("click", this);
addressPicker.editLink.addEventListener("click", this);
addressRow.appendChild(addressPicker);
}
connectedCallback() { connectedCallback() {
this.promiseReady.then(form => { this.promiseReady.then(form => {
this.body.appendChild(form); this.body.appendChild(form);
@ -106,6 +130,8 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe
form.addEventListener("input", this); form.addEventListener("input", this);
form.addEventListener("invalid", this); form.addEventListener("invalid", this);
this._upgradeBillingAddressPicker();
// The "invalid" event does not bubble and needs to be listened for on each // The "invalid" event does not bubble and needs to be listened for on each
// form element. // form element.
for (let field of this.form.elements) { for (let field of this.form.elements) {
@ -117,18 +143,7 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe
cscContainer.textContent = ""; cscContainer.textContent = "";
cscContainer.appendChild(this.cscInput); cscContainer.appendChild(this.cscInput);
let fragment = document.createDocumentFragment();
fragment.append(" ");
fragment.append(this.addressEditLink);
fragment.append(this.addressAddLink);
let billingAddressRow = this.form.querySelector(".billingAddressRow"); let billingAddressRow = this.form.querySelector(".billingAddressRow");
// XXX: Bug 1482689 - Remove the label-text class from the billing field
// which will be removed when switching to <rich-select>.
billingAddressRow.querySelector(".label-text").classList.remove("label-text");
billingAddressRow.appendChild(fragment);
form.insertBefore(this.persistCheckbox, billingAddressRow); form.insertBefore(this.persistCheckbox, billingAddressRow);
form.insertBefore(this.acceptedCardsList, billingAddressRow); form.insertBefore(this.acceptedCardsList, billingAddressRow);
this.body.appendChild(this.genericErrorText); this.body.appendChild(this.genericErrorText);
@ -167,10 +182,13 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe
this.cscInput.frontTooltip = this.dataset.cscFrontInfoTooltip; this.cscInput.frontTooltip = this.dataset.cscFrontInfoTooltip;
this.cscInput.backTooltip = this.dataset.cscBackInfoTooltip; this.cscInput.backTooltip = this.dataset.cscBackInfoTooltip;
// The label text from the form isn't available until render() time.
let labelText = this.form.querySelector(".billingAddressRow .label-text").textContent;
this.billingAddressPicker.setAttribute("label", labelText);
this.persistCheckbox.label = this.dataset.persistCheckboxLabel; this.persistCheckbox.label = this.dataset.persistCheckboxLabel;
this.persistCheckbox.infoTooltip = this.dataset.persistCheckboxInfoTooltip; this.persistCheckbox.infoTooltip = this.dataset.persistCheckboxInfoTooltip;
this.addressAddLink.textContent = this.dataset.addressAddLinkLabel;
this.addressEditLink.textContent = this.dataset.addressEditLinkLabel;
this.acceptedCardsList.label = this.dataset.acceptedCardsLabel; this.acceptedCardsList.label = this.dataset.acceptedCardsLabel;
// The next line needs an onboarding check since we don't set previousId // The next line needs an onboarding check since we don't set previousId
@ -227,7 +245,7 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe
this.form.querySelector(".billingAddressRow").hidden = false; this.form.querySelector(".billingAddressRow").hidden = false;
let billingAddressSelect = this.form.querySelector("#billingAddressGUID"); let billingAddressSelect = this.billingAddressPicker.dropdown;
if (basicCardPage.billingAddressGUID) { if (basicCardPage.billingAddressGUID) {
billingAddressSelect.value = basicCardPage.billingAddressGUID; billingAddressSelect.value = basicCardPage.billingAddressGUID;
} else if (!editing) { } else if (!editing) {
@ -244,7 +262,7 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe
} }
// Need to recalculate the populated state since // Need to recalculate the populated state since
// billingAddressSelect is updated after loadRecord. // billingAddressSelect is updated after loadRecord.
this.formHandler.updatePopulatedState(billingAddressSelect); this.formHandler.updatePopulatedState(billingAddressSelect.popupBox);
this.updateRequiredState(); this.updateRequiredState();
this.updateSaveButtonState(); this.updateSaveButtonState();
@ -289,21 +307,18 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe
paymentRequest.cancel(); paymentRequest.cancel();
break; break;
} }
case this.addressAddLink: case this.billingAddressPicker.addLink:
case this.addressEditLink: { case this.billingAddressPicker.editLink: {
// The address-picker has set state for the page to advance to, now set up the
// necessary state for returning to and re-rendering this page
let { let {
"basic-card-page": basicCardPage, "basic-card-page": basicCardPage,
page,
} = this.requestStore.getState(); } = this.requestStore.getState();
let nextState = { let nextState = {
page: { page: Object.assign({}, page, {
id: "address-page",
previousId: "basic-card-page", previousId: "basic-card-page",
}, }),
"address-page": {
guid: null,
selectedStateKey: ["basic-card-page", "billingAddressGUID"],
title: this.dataset.billingAddressTitleAdd,
},
"basic-card-page": { "basic-card-page": {
preserveFieldValues: true, preserveFieldValues: true,
guid: basicCardPage.guid, guid: basicCardPage.guid,
@ -311,24 +326,18 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe
selectedStateKey: basicCardPage.selectedStateKey, selectedStateKey: basicCardPage.selectedStateKey,
}, },
}; };
let billingAddressGUID = this.form.querySelector("#billingAddressGUID");
let selectedOption = billingAddressGUID.selectedOptions.length &&
billingAddressGUID.selectedOptions[0];
if (evt.target == this.addressEditLink && selectedOption && selectedOption.value) {
nextState["address-page"].title = this.dataset.billingAddressTitleEdit;
nextState["address-page"].guid = selectedOption.value;
}
this.requestStore.setState(nextState); this.requestStore.setState(nextState);
break; break;
} }
case this.backButton: { case this.backButton: {
let currentState = this.requestStore.getState();
let { let {
page, page,
request, request,
"address-page": addressPage, "address-page": addressPage,
"basic-card-page": basicCardPage, "basic-card-page": basicCardPage,
selectedShippingAddress, selectedShippingAddress,
} = this.requestStore.getState(); } = currentState;
let nextState = { let nextState = {
page: { page: {
@ -391,7 +400,10 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe
} }
updateSaveButtonState() { updateSaveButtonState() {
this.saveButton.disabled = !this.form.checkValidity(); const INVALID_CLASS_NAME = "invalid-selected-option";
let isValid = this.form.checkValidity() &&
!this.billingAddressPicker.classList.contains(INVALID_CLASS_NAME);
this.saveButton.disabled = !isValid;
} }
updateRequiredState() { updateRequiredState() {

Просмотреть файл

@ -0,0 +1,33 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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/. */
import AddressPicker from "./address-picker.js";
/* import-globals-from ../unprivileged-fallbacks.js */
/**
* <billing-address-picker></billing-address-picker>
* Extends AddressPicker to treat the <select>'s value as the source of truth
*/
export default class BillingAddressPicker extends AddressPicker {
constructor() {
super();
this._allowEmptyOption = true;
}
/**
* @param {object?} state - See `PaymentsStore.setState`
* The value of the picker is the child dropdown element's value
* @returns {string} guid
*/
getCurrentValue() {
return this.dropdown.value;
}
onChange(event) {
this.render(this.requestStore.getState());
}
}
customElements.define("billing-address-picker", BillingAddressPicker);

Просмотреть файл

@ -16,29 +16,30 @@ export default class RichPicker extends PaymentStateSubscriberMixin(HTMLElement)
this.dropdown = new RichSelect(); this.dropdown = new RichSelect();
this.dropdown.addEventListener("change", this); this.dropdown.addEventListener("change", this);
this.dropdown.popupBox.id = "select-" + Math.floor(Math.random() * 1000000);
this.labelElement = document.createElement("label"); this.labelElement = document.createElement("label");
this.labelElement.setAttribute("for", this.dropdown.popupBox.id);
this.addLink = document.createElement("a"); this.addLink = document.createElement("a");
this.addLink.className = "add-link"; this.addLink.className = "add-link";
this.addLink.href = "javascript:void(0)"; this.addLink.href = "javascript:void(0)";
this.addLink.textContent = this.dataset.addLinkLabel;
this.addLink.addEventListener("click", this); this.addLink.addEventListener("click", this);
this.editLink = document.createElement("a"); this.editLink = document.createElement("a");
this.editLink.className = "edit-link"; this.editLink.className = "edit-link";
this.editLink.href = "javascript:void(0)"; this.editLink.href = "javascript:void(0)";
this.editLink.textContent = this.dataset.editLinkLabel;
this.editLink.addEventListener("click", this); this.editLink.addEventListener("click", this);
this.invalidLabel = document.createElement("label"); this.invalidLabel = document.createElement("label");
this.invalidLabel.className = "invalid-label"; this.invalidLabel.className = "invalid-label";
this.invalidLabel.setAttribute("for", this.dropdown.popupBox.id);
} }
connectedCallback() { connectedCallback() {
if (!this.dropdown.popupBox.id) {
this.dropdown.popupBox.id = "select-" + Math.floor(Math.random() * 1000000);
}
this.labelElement.setAttribute("for", this.dropdown.popupBox.id);
this.invalidLabel.setAttribute("for", this.dropdown.popupBox.id);
// The document order, by default, controls tab order so keep that in mind if changing this. // The document order, by default, controls tab order so keep that in mind if changing this.
this.appendChild(this.labelElement); this.appendChild(this.labelElement);
this.appendChild(this.dropdown); this.appendChild(this.dropdown);
@ -61,6 +62,8 @@ export default class RichPicker extends PaymentStateSubscriberMixin(HTMLElement)
this.classList.toggle("invalid-selected-option", this.classList.toggle("invalid-selected-option",
!!errorText); !!errorText);
this.invalidLabel.textContent = errorText; this.invalidLabel.textContent = errorText;
this.addLink.textContent = this.dataset.addLinkLabel;
this.editLink.textContent = this.dataset.editLinkLabel;
} }
get selectedOption() { get selectedOption() {
@ -99,7 +102,8 @@ export default class RichPicker extends PaymentStateSubscriberMixin(HTMLElement)
return []; return [];
} }
let fieldNames = this.selectedRichOption.requiredFields; let fieldNames = this.selectedRichOption.requiredFields || [];
// Return all field names that are empty or missing from the option. // Return all field names that are empty or missing from the option.
return fieldNames.filter(name => !selectedOption.getAttribute(name)); return fieldNames.filter(name => !selectedOption.getAttribute(name));
} }

Просмотреть файл

@ -193,8 +193,12 @@
data-add-basic-card-title="&basicCard.addPage.title;" data-add-basic-card-title="&basicCard.addPage.title;"
data-edit-basic-card-title="&basicCard.editPage.title;" data-edit-basic-card-title="&basicCard.editPage.title;"
data-error-generic-save="&basicCardPage.error.genericSave;" data-error-generic-save="&basicCardPage.error.genericSave;"
data-address-add-link-label="&basicCardPage.addressAddLink.label;" data-address-add-link-label="&basicCardPage.addressAddLink.label;"
data-address-edit-link-label="&basicCardPage.addressEditLink.label;" data-address-edit-link-label="&basicCardPage.addressEditLink.label;"
data-invalid-address-label="&invalidOption.label;"
data-address-field-separator="&address.fieldSeparator;"
data-billing-address-title-add="&billingAddress.addPage.title;" data-billing-address-title-add="&billingAddress.addPage.title;"
data-billing-address-title-edit="&billingAddress.editPage.title;" data-billing-address-title-edit="&billingAddress.editPage.title;"
data-back-button-label="&basicCardPage.backButton.label;" data-back-button-label="&basicCardPage.backButton.label;"

Просмотреть файл

@ -36,6 +36,7 @@ async function add_link(aOptions = {}) {
checkboxSelector: "basic-card-form .persist-checkbox", checkboxSelector: "basic-card-form .persist-checkbox",
expectPersist: aOptions.expectDefaultCardPersist, expectPersist: aOptions.expectDefaultCardPersist,
}); });
await spawnPaymentDialogTask(frame, async function checkState(testArgs = {}) { await spawnPaymentDialogTask(frame, async function checkState(testArgs = {}) {
let { let {
PaymentTestUtils: PTU, PaymentTestUtils: PTU,
@ -163,12 +164,12 @@ async function add_link(aOptions = {}) {
ok(state["basic-card-page"].billingAddressGUID, ok(state["basic-card-page"].billingAddressGUID,
"billingAddressGUID should be set when coming back from address-page"); "billingAddressGUID should be set when coming back from address-page");
let billingAddressSelect = content.document.querySelector("#billingAddressGUID"); let billingAddressPicker = Cu.waiveXrays(
content.document.querySelector("basic-card-form billing-address-picker"));
is(billingAddressSelect.childElementCount, 3, is(billingAddressPicker.options.length, 3,
"Three options should exist in the billingAddressSelect"); "Three options should exist in the billingAddressPicker");
let selectedOption = let selectedOption = billingAddressPicker.dropdown.selectedOption;
billingAddressSelect.children[billingAddressSelect.selectedIndex];
let selectedAddressGuid = selectedOption.value; let selectedAddressGuid = selectedOption.value;
let lastAddress = Object.values(addressColn)[Object.keys(addressColn).length - 1]; let lastAddress = Object.values(addressColn)[Object.keys(addressColn).length - 1];
is(selectedAddressGuid, lastAddress.guid, "The select should have the new address selected"); is(selectedAddressGuid, lastAddress.guid, "The select should have the new address selected");
@ -411,8 +412,11 @@ add_task(async function test_edit_link() {
const args = { const args = {
methodData: [PTU.MethodData.basicCard], methodData: [PTU.MethodData.basicCard],
details: PTU.Details.total60USD, details: PTU.Details.total60USD,
prefilledGuids,
}; };
await spawnInDialogForMerchantTask(PTU.ContentTasks.createAndShowRequest, async function check() { await spawnInDialogForMerchantTask(
PTU.ContentTasks.createAndShowRequest,
async function check({prefilledGuids}) {
let { let {
PaymentTestUtils: PTU, PaymentTestUtils: PTU,
} = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {}); } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
@ -449,22 +453,31 @@ add_task(async function test_edit_link() {
field.value = val; field.value = val;
ok(!field.disabled, `Field #${key} shouldn't be disabled`); ok(!field.disabled, `Field #${key} shouldn't be disabled`);
} }
ok(content.document.getElementById("cc-number").disabled, "cc-number field should be disabled"); ok(content.document.getElementById("cc-number").disabled,
"cc-number field should be disabled");
let billingAddressSelect = content.document.querySelector("#billingAddressGUID"); let billingAddressPicker = Cu.waiveXrays(
is(billingAddressSelect.childElementCount, 2, content.document.querySelector("basic-card-form billing-address-picker"));
"Two options should exist in the billingAddressSelect");
is(billingAddressSelect.selectedIndex, 1, let initialSelectedAddressGuid = billingAddressPicker.dropdown.value;
is(billingAddressPicker.options.length, 2,
"Two options should exist in the billingAddressPicker");
is(initialSelectedAddressGuid, prefilledGuids.address1GUID,
"The prefilled billing address should be selected by default"); "The prefilled billing address should be selected by default");
info("Test clicking 'edit' on the empty option first"); info("Test clicking 'add' on the empty option first");
billingAddressSelect.selectedIndex = 0; billingAddressPicker.dropdown.popupBox.focus();
content.fillField(billingAddressPicker.dropdown.popupBox, "");
let addressEditLink = content.document.querySelector(".billingAddressRow .edit-link"); let addressEditLink = content.document.querySelector(".billingAddressRow .edit-link");
addressEditLink.click(); ok(addressEditLink && !content.isVisible(addressEditLink),
"The edit link is hidden when empty option is selected");
let addressAddLink = content.document.querySelector(".billingAddressRow .add-link");
addressAddLink.click();
state = await PTU.DialogContentUtils.waitForState(content, (state) => { state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return state.page.id == "address-page" && !state["address-page"].guid; return state.page.id == "address-page" && !state["address-page"].guid;
}, "Clicking edit button when the empty option is selected will go to 'add' page (no guid)"); }, "Clicking add button when the empty option is selected will go to 'add' page (no guid)");
let addressTitle = content.document.querySelector("address-form h2"); let addressTitle = content.document.querySelector("address-form h2");
is(addressTitle.textContent, "Add Billing Address", is(addressTitle.textContent, "Add Billing Address",
@ -478,10 +491,9 @@ add_task(async function test_edit_link() {
}, "Check we're back at basic-card page with no state changed after adding"); }, "Check we're back at basic-card page with no state changed after adding");
info("Go back to previously selected option before clicking 'edit' now"); info("Go back to previously selected option before clicking 'edit' now");
billingAddressSelect.selectedIndex = 1; billingAddressPicker.dropdown.value = initialSelectedAddressGuid;
let selectedOption = billingAddressSelect.selectedOptions.length && let selectedOption = billingAddressPicker.dropdown.selectedOption;
billingAddressSelect.selectedOptions[0];
ok(selectedOption && selectedOption.value, "select should have a selected option value"); ok(selectedOption && selectedOption.value, "select should have a selected option value");
addressEditLink.click(); addressEditLink.click();
@ -507,8 +519,7 @@ add_task(async function test_edit_link() {
is(field.value, val, "Field should still have previous value entered"); is(field.value, val, "Field should still have previous value entered");
} }
selectedOption = billingAddressSelect.selectedOptions.length && selectedOption = billingAddressPicker.dropdown.selectedOption;
billingAddressSelect.selectedOptions[0];
ok(selectedOption && selectedOption.value, "select should have a selected option value"); ok(selectedOption && selectedOption.value, "select should have a selected option value");
addressEditLink.click(); addressEditLink.click();
@ -548,7 +559,8 @@ add_task(async function test_edit_link() {
let cardGUIDs = Object.keys(state.savedBasicCards); let cardGUIDs = Object.keys(state.savedBasicCards);
is(cardGUIDs.length, 1, "Check there is still one card"); is(cardGUIDs.length, 1, "Check there is still one card");
let savedCard = state.savedBasicCards[cardGUIDs[0]]; let savedCard = state.savedBasicCards[cardGUIDs[0]];
is(savedCard["cc-number"], "************1111", "Card number should be masked and unmodified."); is(savedCard["cc-number"], "************1111",
"Card number should be masked and unmodified.");
for (let [key, val] of Object.entries(card)) { for (let [key, val] of Object.entries(card)) {
is(savedCard[key], val, "Check updated " + key); is(savedCard[key], val, "Check updated " + key);
} }

Просмотреть файл

@ -20,6 +20,7 @@ skip-if = os == "linux" || os == "win" # Bug 1493216
[test_basic_card_form.html] [test_basic_card_form.html]
skip-if = debug || asan # Bug 1493349 skip-if = debug || asan # Bug 1493349
[test_basic_card_option.html] [test_basic_card_option.html]
[test_billing_address_picker.html]
[test_completion_error_page.html] [test_completion_error_page.html]
[test_currency_amount.html] [test_currency_amount.html]
[test_labelled_checkbox.html] [test_labelled_checkbox.html]

Просмотреть файл

@ -90,6 +90,9 @@ add_task(async function test_initialState() {
add_task(async function test_backButton() { add_task(async function test_backButton() {
let form = new AddressForm(); let form = new AddressForm();
form.dataset.backButtonLabel = "Back"; form.dataset.backButtonLabel = "Back";
await form.promiseReady;
display.appendChild(form);
await form.requestStore.setState({ await form.requestStore.setState({
page: { page: {
id: "address-page", id: "address-page",
@ -99,9 +102,6 @@ add_task(async function test_backButton() {
title: "Sample page title", title: "Sample page title",
}, },
}); });
await form.promiseReady;
display.appendChild(form);
await asyncElementRendered(); await asyncElementRendered();
let stateChangePromise = promiseStateChange(form.requestStore); let stateChangePromise = promiseStateChange(form.requestStore);

Просмотреть файл

@ -61,6 +61,14 @@ function checkCCForm(customEl, expectedCard) {
} }
} }
function createAddressRecord(source, props = {}) {
let address = Object.assign({}, source, props);
if (!address.name) {
address.name = `${address["given-name"]} ${address["family-name"]}`;
}
return address;
}
add_task(async function setup_once() { add_task(async function setup_once() {
let templateFrame = document.getElementById("templateFrame"); let templateFrame = document.getElementById("templateFrame");
await SimpleTest.promiseFocus(templateFrame.contentWindow); await SimpleTest.promiseFocus(templateFrame.contentWindow);
@ -70,6 +78,13 @@ add_task(async function setup_once() {
add_task(async function test_initialState() { add_task(async function test_initialState() {
let form = new BasicCardForm(); let form = new BasicCardForm();
await form.requestStore.setState({
savedAddresses: {
"TimBLGUID": createAddressRecord(PTU.Addresses.TimBL),
},
});
let {page} = form.requestStore.getState(); let {page} = form.requestStore.getState();
is(page.id, "payment-summary", "Check initial page"); is(page.id, "payment-summary", "Check initial page");
await form.promiseReady; await form.promiseReady;
@ -79,6 +94,9 @@ add_task(async function test_initialState() {
// :-moz-ui-invalid, unlike :invalid, only applies to fields showing the error outline. // :-moz-ui-invalid, unlike :invalid, only applies to fields showing the error outline.
let fieldsVisiblyInvalid = form.querySelectorAll(":-moz-ui-invalid"); let fieldsVisiblyInvalid = form.querySelectorAll(":-moz-ui-invalid");
for (let field of fieldsVisiblyInvalid) {
info("invalid field: " + field.localName + "#" + field.id + "." + field.className);
}
is(fieldsVisiblyInvalid.length, 0, "Check no fields are visibly invalid on an empty 'add' form"); is(fieldsVisiblyInvalid.length, 0, "Check no fields are visibly invalid on an empty 'add' form");
form.remove(); form.remove();
@ -116,18 +134,18 @@ add_task(async function test_saveButton() {
let form = new BasicCardForm(); let form = new BasicCardForm();
form.dataset.nextButtonLabel = "Next"; form.dataset.nextButtonLabel = "Next";
form.dataset.errorGenericSave = "Generic error"; form.dataset.errorGenericSave = "Generic error";
form.dataset.invalidAddressLabel = "Invalid";
await form.promiseReady; await form.promiseReady;
display.appendChild(form); display.appendChild(form);
let address1 = deepClone(PTU.Addresses.TimBL); let address1 = createAddressRecord(PTU.Addresses.TimBL, {guid: "TimBLGUID"});
address1.guid = "TimBLGUID"; let address2 = createAddressRecord(PTU.Addresses.TimBL2, {guid: "TimBL2GUID"});
let address2 = deepClone(PTU.Addresses.TimBL2);
address2.guid = "TimBL2GUID";
await form.requestStore.setState({ await form.requestStore.setState({
request: { request: {
paymentMethods, paymentMethods,
paymentDetails: {},
}, },
savedAddresses: { savedAddresses: {
[address1.guid]: deepClone(address1), [address1.guid]: deepClone(address1),
@ -274,10 +292,8 @@ add_task(async function test_add_selectedShippingAddress() {
card1.guid = "9864798564"; card1.guid = "9864798564";
card1["cc-exp-year"] = 2011; card1["cc-exp-year"] = 2011;
let address1 = deepClone(PTU.Addresses.TimBL); let address1 = createAddressRecord(PTU.Addresses.TimBL, { guid: "TimBLGUID" });
address1.guid = "TimBLGUID"; let address2 = createAddressRecord(PTU.Addresses.TimBL2, { guid: "TimBL2GUID" });
let address2 = deepClone(PTU.Addresses.TimBL2);
address2.guid = "TimBL2GUID";
await form.requestStore.setState({ await form.requestStore.setState({
page: { page: {
@ -312,8 +328,7 @@ add_task(async function test_add_noSelectedShippingAddress() {
card1.guid = "9864798564"; card1.guid = "9864798564";
card1["cc-exp-year"] = 2011; card1["cc-exp-year"] = 2011;
let address1 = deepClone(PTU.Addresses.TimBL); let address1 = createAddressRecord(PTU.Addresses.TimBL, { guid: "TimBLGUID" });
address1.guid = "TimBLGUID";
await form.requestStore.setState({ await form.requestStore.setState({
page: { page: {
@ -352,8 +367,7 @@ add_task(async function test_edit() {
display.appendChild(form); display.appendChild(form);
await asyncElementRendered(); await asyncElementRendered();
let address1 = deepClone(PTU.Addresses.TimBL); let address1 = createAddressRecord(PTU.Addresses.TimBL, { guid: "TimBLGUID" });
address1.guid = "TimBLGUID";
info("test year before current"); info("test year before current");
let card1 = deepClone(PTU.BasicCards.JohnDoe); let card1 = deepClone(PTU.BasicCards.JohnDoe);
@ -364,6 +378,7 @@ add_task(async function test_edit() {
await form.requestStore.setState({ await form.requestStore.setState({
request: { request: {
paymentMethods, paymentMethods,
paymentDetails: {},
}, },
page: { page: {
id: "basic-card-page", id: "basic-card-page",
@ -383,6 +398,7 @@ add_task(async function test_edit() {
is(form.saveButton.textContent, "Update", "Check label"); is(form.saveButton.textContent, "Update", "Check label");
is(form.querySelectorAll(":-moz-ui-invalid").length, 0, is(form.querySelectorAll(":-moz-ui-invalid").length, 0,
"Check no fields are visibly invalid on an 'edit' form with a complete card"); "Check no fields are visibly invalid on an 'edit' form with a complete card");
checkCCForm(form, card1); checkCCForm(form, card1);
ok(!form.saveButton.disabled, "Save button should be enabled upon edit for a valid card"); ok(!form.saveButton.disabled, "Save button should be enabled upon edit for a valid card");
ok(!form.acceptedCardsList.hidden, "Accepted card list should be visible when editing a card"); ok(!form.acceptedCardsList.hidden, "Accepted card list should be visible when editing a card");
@ -460,6 +476,17 @@ add_task(async function test_field_validity_updates() {
form.dataset.updateButtonLabel = "Update"; form.dataset.updateButtonLabel = "Update";
await form.promiseReady; await form.promiseReady;
display.appendChild(form); display.appendChild(form);
let address1 = createAddressRecord(PTU.Addresses.TimBL, {guid: "TimBLGUID"});
await form.requestStore.setState({
request: {
paymentMethods,
paymentDetails: {},
},
savedAddresses: {
[address1.guid]: deepClone(address1),
},
});
await asyncElementRendered(); await asyncElementRendered();
let ccNumber = form.form.querySelector("#cc-number"); let ccNumber = form.form.querySelector("#cc-number");
@ -468,6 +495,7 @@ add_task(async function test_field_validity_updates() {
let cscInput = form.form.querySelector("csc-input input"); let cscInput = form.form.querySelector("csc-input input");
let monthInput = form.form.querySelector("#cc-exp-month"); let monthInput = form.form.querySelector("#cc-exp-month");
let yearInput = form.form.querySelector("#cc-exp-year"); let yearInput = form.form.querySelector("#cc-exp-year");
let addressPicker = form.querySelector("#billingAddressGUID");
info("test with valid cc-number but missing cc-name"); info("test with valid cc-number but missing cc-name");
fillField(ccNumber, "4111111111111111"); fillField(ccNumber, "4111111111111111");
@ -487,6 +515,16 @@ add_task(async function test_field_validity_updates() {
ok(monthInput.checkValidity(), "cc-exp-month field is valid with a value"); ok(monthInput.checkValidity(), "cc-exp-month field is valid with a value");
ok(yearInput.checkValidity(), "cc-exp-year field is valid with a value"); ok(yearInput.checkValidity(), "cc-exp-year field is valid with a value");
ok(typeInput.checkValidity(), "cc-type field is valid with a value"); ok(typeInput.checkValidity(), "cc-type field is valid with a value");
// should auto-select the first billing address
ok(addressPicker.value, "An address is selected: " + addressPicker.value);
let fieldsVisiblyInvalid = form.querySelectorAll(":-moz-ui-invalid");
for (let field of fieldsVisiblyInvalid) {
info("invalid field: " + field.localName + "#" + field.id + "." + field.className);
}
is(fieldsVisiblyInvalid.length, 0, "No fields are visibly invalid");
ok(!form.saveButton.disabled, "Save button should not be disabled with good input"); ok(!form.saveButton.disabled, "Save button should not be disabled with good input");
info("edit to make the cc-number invalid"); info("edit to make the cc-number invalid");
@ -518,6 +556,17 @@ add_task(async function test_numberCustomValidityReset() {
form.dataset.updateButtonLabel = "Update"; form.dataset.updateButtonLabel = "Update";
await form.promiseReady; await form.promiseReady;
display.appendChild(form); display.appendChild(form);
let address1 = createAddressRecord(PTU.Addresses.TimBL, {guid: "TimBLGUID"});
await form.requestStore.setState({
request: {
paymentMethods,
paymentDetails: {},
},
savedAddresses: {
[address1.guid]: deepClone(address1),
},
});
await asyncElementRendered(); await asyncElementRendered();
fillField(form.querySelector("#cc-number"), "junk"); fillField(form.querySelector("#cc-number"), "junk");

Просмотреть файл

@ -0,0 +1,133 @@
<!DOCTYPE HTML>
<html>
<!--
Test the address-picker component
-->
<head>
<meta charset="utf-8">
<title>Test the billing-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/unprivileged-fallbacks.js"></script>
<script src="autofillEditForms.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">
<billing-address-picker id="picker1"
data-field-separator=", "
data-invalid-label="Picker1: Missing or Invalid"
selected-state-key="basic-card-page|billingAddressGUID"></billing-address-picker>
<select id="theOptions">
<option></option>
<option value="48bnds6854t">48bnds6854t</option>
<option value="68gjdh354j" selected="">68gjdh354j</option>
</select>
</p>
<div id="content" style="display: none">
</div>
<pre id="test">
</pre>
<script type="module">
/** Test the billing-address-picker component **/
import BillingAddressPicker from "../../res/containers/billing-address-picker.js";
let picker1 = document.getElementById("picker1");
let 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",
timeLastUsed: 200,
},
"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",
timeLastUsed: 300,
},
"abcde12345": {
"address-level2": "Mountain View",
"country": "US",
"guid": "abcde12345",
"name": "Mrs. Fields",
timeLastUsed: 100,
},
};
add_task(async function test_empty() {
ok(picker1, "Check picker1 exists");
let {savedAddresses} = picker1.requestStore.getState();
is(Object.keys(savedAddresses).length, 0, "Check empty initial state");
is(picker1.editLink.hidden, true, "Check that picker edit link is hidden");
is(picker1.options.length, 1, "Check only the empty option is present");
ok(picker1.dropdown.selectedOption, "Has a selectedOption");
is(picker1.dropdown.value, "", "Has empty value");
// update state to trigger render without changing available addresses
picker1.requestStore.setState({
"basic-card-page": {
"someKey": "someValue",
},
});
await asyncElementRendered();
is(picker1.dropdown.popupBox.children.length, 1, "Check only the empty option is present");
ok(picker1.dropdown.selectedOption, "Has a selectedOption");
is(picker1.dropdown.value, "", "Has empty value");
});
add_task(async function test_getCurrentValue() {
picker1.requestStore.setState({
"basic-card-page": {
"billingAddressGUID": "68gjdh354j",
},
savedAddresses: addresses,
});
await asyncElementRendered();
picker1.dropdown.popupBox.value = "abcde12345";
is(picker1.options.length, 4, "Check we have options for each address + empty one");
is(picker1.getCurrentValue(picker1.requestStore.getState()), "abcde12345",
"Initial/current value reflects the <select>.value, " +
"not whatever is in the state at the selectedStateKey");
});
add_task(async function test_wrapPopupBox() {
let picker = new BillingAddressPicker();
picker.dropdown.popupBox = document.querySelector("#theOptions");
picker.dataset.invalidLabel = "Invalid";
picker.setAttribute("label", "The label");
picker.setAttribute("selected-state-key", "basic-card-page|billingAddressGUID");
document.querySelector("#display").appendChild(picker);
is(picker.labelElement.getAttribute("for"), "theOptions",
"The label points at the right element");
is(picker.invalidLabel.getAttribute("for"), "theOptions",
"The invalidLabel points at the right element");
});
</script>
</body>
</html>