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() {
super();
this.popupBox = document.createElement("select");
this.popupBox.addEventListener("change", this);
}
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.render();
}
@ -74,7 +76,7 @@ export default class RichSelect extends ObservedPropertiesMixin(HTMLElement) {
if (this.value) {
let optionType = this.getAttribute("option-type");
if (selectedRichOption.localName != optionType) {
if (!selectedRichOption || selectedRichOption.localName != optionType) {
selectedRichOption = document.createElement(optionType);
}

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

@ -33,7 +33,10 @@ export default class AddressPicker extends RichPicker {
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());
}
}
@ -89,7 +92,26 @@ export default class AddressPicker extends RichPicker {
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) {
let selectedAddressGUID = this.getCurrentValue(state) || "";
let addresses = paymentRequest.getAddresses(state);
let desiredOptions = [];
let filteredAddresses = this.filterAddresses(addresses, this.fieldNames);
@ -131,12 +153,18 @@ export default class AddressPicker extends RichPicker {
}
this.dropdown.popupBox.textContent = "";
if (this._allowEmptyOption) {
let optionEl = document.createElement("option");
optionEl.value = "";
desiredOptions.unshift(optionEl);
}
for (let option of desiredOptions) {
this.dropdown.popupBox.appendChild(option);
}
// Update selectedness after the options are updated
let selectedAddressGUID = state[this.selectedStateKey];
this.dropdown.value = selectedAddressGUID;
if (selectedAddressGUID && selectedAddressGUID !== this.dropdown.value) {
@ -161,8 +189,8 @@ export default class AddressPicker extends RichPicker {
return "";
}
let merchantFieldErrors = AddressForm.merchantFieldErrorsForForm(state,
[this.selectedStateKey]);
let merchantFieldErrors = AddressForm.merchantFieldErrorsForForm(
state, this.selectedStateKey.split("|"));
// TODO: errors in priority order.
return Object.values(merchantFieldErrors).find(msg => {
return typeof(msg) == "string" && msg.length;
@ -182,12 +210,23 @@ export default class AddressPicker extends RichPicker {
}
onChange(event) {
let selectedKey = this.selectedStateKey;
if (selectedKey) {
this.requestStore.setState({
[selectedKey]: this.dropdown.value,
});
let [selectedKey, selectedLeaf] = this.selectedStateKey.split("|");
if (!selectedKey) {
return;
}
// 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}) {
@ -197,7 +236,7 @@ export default class AddressPicker extends RichPicker {
},
"address-page": {
addressFields: this.getAttribute("address-fields"),
selectedStateKey: [this.selectedStateKey],
selectedStateKey: this.selectedStateKey.split("|"),
},
};
@ -208,9 +247,8 @@ export default class AddressPicker extends RichPicker {
break;
}
case this.editLink: {
let state = this.requestStore.getState();
let selectedAddressGUID = state[this.selectedStateKey];
nextState["address-page"].guid = selectedAddressGUID;
let currentState = this.requestStore.getState();
nextState["address-page"].guid = this.getCurrentValue(currentState);
nextState["address-page"].title = this.dataset.editAddressTitle;
break;
}

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

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

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

@ -4,6 +4,7 @@
/* import-globals-from ../../../../../browser/extensions/formautofill/content/autofillEditForms.js*/
import AcceptedCards from "../components/accepted-cards.js";
import BillingAddressPicker from "./billing-address-picker.js";
import CscInput from "../components/csc-input.js";
import LabelledCheckbox from "../components/labelled-checkbox.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.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({
useAlwaysVisiblePlaceholder: true,
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() {
this.promiseReady.then(form => {
this.body.appendChild(form);
@ -106,6 +130,8 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe
form.addEventListener("input", this);
form.addEventListener("invalid", this);
this._upgradeBillingAddressPicker();
// The "invalid" event does not bubble and needs to be listened for on each
// form element.
for (let field of this.form.elements) {
@ -117,18 +143,7 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe
cscContainer.textContent = "";
cscContainer.appendChild(this.cscInput);
let fragment = document.createDocumentFragment();
fragment.append(" ");
fragment.append(this.addressEditLink);
fragment.append(this.addressAddLink);
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.acceptedCardsList, billingAddressRow);
this.body.appendChild(this.genericErrorText);
@ -167,10 +182,13 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe
this.cscInput.frontTooltip = this.dataset.cscFrontInfoTooltip;
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.infoTooltip = this.dataset.persistCheckboxInfoTooltip;
this.addressAddLink.textContent = this.dataset.addressAddLinkLabel;
this.addressEditLink.textContent = this.dataset.addressEditLinkLabel;
this.acceptedCardsList.label = this.dataset.acceptedCardsLabel;
// 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;
let billingAddressSelect = this.form.querySelector("#billingAddressGUID");
let billingAddressSelect = this.billingAddressPicker.dropdown;
if (basicCardPage.billingAddressGUID) {
billingAddressSelect.value = basicCardPage.billingAddressGUID;
} else if (!editing) {
@ -244,7 +262,7 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe
}
// Need to recalculate the populated state since
// billingAddressSelect is updated after loadRecord.
this.formHandler.updatePopulatedState(billingAddressSelect);
this.formHandler.updatePopulatedState(billingAddressSelect.popupBox);
this.updateRequiredState();
this.updateSaveButtonState();
@ -289,21 +307,18 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe
paymentRequest.cancel();
break;
}
case this.addressAddLink:
case this.addressEditLink: {
case this.billingAddressPicker.addLink:
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 {
"basic-card-page": basicCardPage,
page,
} = this.requestStore.getState();
let nextState = {
page: {
id: "address-page",
page: Object.assign({}, page, {
previousId: "basic-card-page",
},
"address-page": {
guid: null,
selectedStateKey: ["basic-card-page", "billingAddressGUID"],
title: this.dataset.billingAddressTitleAdd,
},
}),
"basic-card-page": {
preserveFieldValues: true,
guid: basicCardPage.guid,
@ -311,24 +326,18 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe
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);
break;
}
case this.backButton: {
let currentState = this.requestStore.getState();
let {
page,
request,
"address-page": addressPage,
"basic-card-page": basicCardPage,
selectedShippingAddress,
} = this.requestStore.getState();
} = currentState;
let nextState = {
page: {
@ -391,7 +400,10 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe
}
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() {

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

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

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

@ -193,8 +193,12 @@
data-add-basic-card-title="&basicCard.addPage.title;"
data-edit-basic-card-title="&basicCard.editPage.title;"
data-error-generic-save="&basicCardPage.error.genericSave;"
data-address-add-link-label="&basicCardPage.addressAddLink.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-edit="&billingAddress.editPage.title;"
data-back-button-label="&basicCardPage.backButton.label;"

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

@ -36,6 +36,7 @@ async function add_link(aOptions = {}) {
checkboxSelector: "basic-card-form .persist-checkbox",
expectPersist: aOptions.expectDefaultCardPersist,
});
await spawnPaymentDialogTask(frame, async function checkState(testArgs = {}) {
let {
PaymentTestUtils: PTU,
@ -163,12 +164,12 @@ async function add_link(aOptions = {}) {
ok(state["basic-card-page"].billingAddressGUID,
"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,
"Three options should exist in the billingAddressSelect");
let selectedOption =
billingAddressSelect.children[billingAddressSelect.selectedIndex];
is(billingAddressPicker.options.length, 3,
"Three options should exist in the billingAddressPicker");
let selectedOption = billingAddressPicker.dropdown.selectedOption;
let selectedAddressGuid = selectedOption.value;
let lastAddress = Object.values(addressColn)[Object.keys(addressColn).length - 1];
is(selectedAddressGuid, lastAddress.guid, "The select should have the new address selected");
@ -411,152 +412,163 @@ add_task(async function test_edit_link() {
const args = {
methodData: [PTU.MethodData.basicCard],
details: PTU.Details.total60USD,
prefilledGuids,
};
await spawnInDialogForMerchantTask(PTU.ContentTasks.createAndShowRequest, async function check() {
let {
PaymentTestUtils: PTU,
} = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
await spawnInDialogForMerchantTask(
PTU.ContentTasks.createAndShowRequest,
async function check({prefilledGuids}) {
let {
PaymentTestUtils: PTU,
} = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
let editLink = content.document.querySelector("payment-method-picker .edit-link");
is(editLink.textContent, "Edit", "Edit link text");
let editLink = content.document.querySelector("payment-method-picker .edit-link");
is(editLink.textContent, "Edit", "Edit link text");
editLink.click();
editLink.click();
let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return state.page.id == "basic-card-page" && state["basic-card-page"].guid;
}, "Check edit page state");
let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return state.page.id == "basic-card-page" && state["basic-card-page"].guid;
}, "Check edit page state");
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return Object.keys(state.savedBasicCards).length == 1 &&
Object.keys(state.savedAddresses).length == 1;
}, "Check card and address present at beginning of test");
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return Object.keys(state.savedBasicCards).length == 1 &&
Object.keys(state.savedAddresses).length == 1;
}, "Check card and address present at beginning of test");
let title = content.document.querySelector("basic-card-form h2");
is(title.textContent, "Edit Credit Card", "Edit title should be set");
let title = content.document.querySelector("basic-card-form h2");
is(title.textContent, "Edit Credit Card", "Edit title should be set");
let saveButton = content.document.querySelector("basic-card-form .save-button");
is(saveButton.textContent, "Update", "Save button has the correct label");
let saveButton = content.document.querySelector("basic-card-form .save-button");
is(saveButton.textContent, "Update", "Save button has the correct label");
let card = Object.assign({}, PTU.BasicCards.JohnDoe);
// cc-number cannot be modified
delete card["cc-number"];
card["cc-exp-year"]++;
card["cc-exp-month"]++;
let card = Object.assign({}, PTU.BasicCards.JohnDoe);
// cc-number cannot be modified
delete card["cc-number"];
card["cc-exp-year"]++;
card["cc-exp-month"]++;
info("overwriting field values");
for (let [key, val] of Object.entries(card)) {
let field = content.document.getElementById(key);
field.value = val;
ok(!field.disabled, `Field #${key} shouldn't be disabled`);
}
ok(content.document.getElementById("cc-number").disabled, "cc-number field should be disabled");
let billingAddressSelect = content.document.querySelector("#billingAddressGUID");
is(billingAddressSelect.childElementCount, 2,
"Two options should exist in the billingAddressSelect");
is(billingAddressSelect.selectedIndex, 1,
"The prefilled billing address should be selected by default");
info("Test clicking 'edit' on the empty option first");
billingAddressSelect.selectedIndex = 0;
let addressEditLink = content.document.querySelector(".billingAddressRow .edit-link");
addressEditLink.click();
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
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)");
let addressTitle = content.document.querySelector("address-form h2");
is(addressTitle.textContent, "Add Billing Address",
"Address on add address page should be correct");
let addressBackButton = content.document.querySelector("address-form .back-button");
addressBackButton.click();
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return state.page.id == "basic-card-page" && state["basic-card-page"].guid &&
Object.keys(state.savedAddresses).length == 1;
}, "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");
billingAddressSelect.selectedIndex = 1;
let selectedOption = billingAddressSelect.selectedOptions.length &&
billingAddressSelect.selectedOptions[0];
ok(selectedOption && selectedOption.value, "select should have a selected option value");
addressEditLink.click();
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return state.page.id == "address-page" && state["address-page"].guid;
}, "Check address page state (editing)");
is(addressTitle.textContent, "Edit Billing Address",
"Address on edit address page should be correct");
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return Object.keys(state.savedBasicCards).length == 1;
}, "Check card was not added again when clicking the 'edit' address button");
addressBackButton.click();
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return state.page.id == "basic-card-page" && state["basic-card-page"].guid &&
Object.keys(state.savedAddresses).length == 1;
}, "Check we're back at basic-card page with no state changed after editing");
for (let [key, val] of Object.entries(card)) {
let field = content.document.getElementById(key);
is(field.value, val, "Field should still have previous value entered");
}
selectedOption = billingAddressSelect.selectedOptions.length &&
billingAddressSelect.selectedOptions[0];
ok(selectedOption && selectedOption.value, "select should have a selected option value");
addressEditLink.click();
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return state.page.id == "address-page" && state["address-page"].guid;
}, "Check address page state (editing)");
info("modify some address fields");
for (let key of ["given-name", "tel", "organization", "street-address"]) {
let field = content.document.getElementById(key);
if (!field) {
ok(false, `${key} field not found`);
info("overwriting field values");
for (let [key, val] of Object.entries(card)) {
let field = content.document.getElementById(key);
field.value = val;
ok(!field.disabled, `Field #${key} shouldn't be disabled`);
}
field.focus();
EventUtils.sendKey("BACK_SPACE", content.window);
EventUtils.sendString("7", content.window);
ok(!field.disabled, `Field #${key} shouldn't be disabled`);
}
ok(content.document.getElementById("cc-number").disabled,
"cc-number field should be disabled");
content.document.querySelector("address-form button.save-button").click();
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return state.page.id == "basic-card-page" && state["basic-card-page"].guid &&
Object.keys(state.savedAddresses).length == 1;
}, "Check still only one address and we're back on basic-card page");
let billingAddressPicker = Cu.waiveXrays(
content.document.querySelector("basic-card-form billing-address-picker"));
is(Object.values(state.savedAddresses)[0].tel, PTU.Addresses.TimBL.tel.slice(0, -1) + "7",
"Check that address was edited and saved");
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");
content.document.querySelector("basic-card-form button.save-button").click();
info("Test clicking 'add' on the empty option first");
billingAddressPicker.dropdown.popupBox.focus();
content.fillField(billingAddressPicker.dropdown.popupBox, "");
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
let cards = Object.entries(state.savedBasicCards);
return cards.length == 1 &&
cards[0][1]["cc-name"] == card["cc-name"];
}, "Check card was edited");
let addressEditLink = content.document.querySelector(".billingAddressRow .edit-link");
ok(addressEditLink && !content.isVisible(addressEditLink),
"The edit link is hidden when empty option is selected");
let cardGUIDs = Object.keys(state.savedBasicCards);
is(cardGUIDs.length, 1, "Check there is still one card");
let savedCard = state.savedBasicCards[cardGUIDs[0]];
is(savedCard["cc-number"], "************1111", "Card number should be masked and unmodified.");
for (let [key, val] of Object.entries(card)) {
is(savedCard[key], val, "Check updated " + key);
}
let addressAddLink = content.document.querySelector(".billingAddressRow .add-link");
addressAddLink.click();
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return state.page.id == "address-page" && !state["address-page"].guid;
}, "Clicking add button when the empty option is selected will go to 'add' page (no guid)");
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return state.page.id == "payment-summary";
}, "Switched back to payment-summary");
}, args);
let addressTitle = content.document.querySelector("address-form h2");
is(addressTitle.textContent, "Add Billing Address",
"Address on add address page should be correct");
let addressBackButton = content.document.querySelector("address-form .back-button");
addressBackButton.click();
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return state.page.id == "basic-card-page" && state["basic-card-page"].guid &&
Object.keys(state.savedAddresses).length == 1;
}, "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");
billingAddressPicker.dropdown.value = initialSelectedAddressGuid;
let selectedOption = billingAddressPicker.dropdown.selectedOption;
ok(selectedOption && selectedOption.value, "select should have a selected option value");
addressEditLink.click();
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return state.page.id == "address-page" && state["address-page"].guid;
}, "Check address page state (editing)");
is(addressTitle.textContent, "Edit Billing Address",
"Address on edit address page should be correct");
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return Object.keys(state.savedBasicCards).length == 1;
}, "Check card was not added again when clicking the 'edit' address button");
addressBackButton.click();
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return state.page.id == "basic-card-page" && state["basic-card-page"].guid &&
Object.keys(state.savedAddresses).length == 1;
}, "Check we're back at basic-card page with no state changed after editing");
for (let [key, val] of Object.entries(card)) {
let field = content.document.getElementById(key);
is(field.value, val, "Field should still have previous value entered");
}
selectedOption = billingAddressPicker.dropdown.selectedOption;
ok(selectedOption && selectedOption.value, "select should have a selected option value");
addressEditLink.click();
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return state.page.id == "address-page" && state["address-page"].guid;
}, "Check address page state (editing)");
info("modify some address fields");
for (let key of ["given-name", "tel", "organization", "street-address"]) {
let field = content.document.getElementById(key);
if (!field) {
ok(false, `${key} field not found`);
}
field.focus();
EventUtils.sendKey("BACK_SPACE", content.window);
EventUtils.sendString("7", content.window);
ok(!field.disabled, `Field #${key} shouldn't be disabled`);
}
content.document.querySelector("address-form button.save-button").click();
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return state.page.id == "basic-card-page" && state["basic-card-page"].guid &&
Object.keys(state.savedAddresses).length == 1;
}, "Check still only one address and we're back on basic-card page");
is(Object.values(state.savedAddresses)[0].tel, PTU.Addresses.TimBL.tel.slice(0, -1) + "7",
"Check that address was edited and saved");
content.document.querySelector("basic-card-form button.save-button").click();
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
let cards = Object.entries(state.savedBasicCards);
return cards.length == 1 &&
cards[0][1]["cc-name"] == card["cc-name"];
}, "Check card was edited");
let cardGUIDs = Object.keys(state.savedBasicCards);
is(cardGUIDs.length, 1, "Check there is still one card");
let savedCard = state.savedBasicCards[cardGUIDs[0]];
is(savedCard["cc-number"], "************1111",
"Card number should be masked and unmodified.");
for (let [key, val] of Object.entries(card)) {
is(savedCard[key], val, "Check updated " + key);
}
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return state.page.id == "payment-summary";
}, "Switched back to payment-summary");
}, args);
});
add_task(async function test_invalid_network_card_edit() {

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

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

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

@ -90,6 +90,9 @@ add_task(async function test_initialState() {
add_task(async function test_backButton() {
let form = new AddressForm();
form.dataset.backButtonLabel = "Back";
await form.promiseReady;
display.appendChild(form);
await form.requestStore.setState({
page: {
id: "address-page",
@ -99,9 +102,6 @@ add_task(async function test_backButton() {
title: "Sample page title",
},
});
await form.promiseReady;
display.appendChild(form);
await asyncElementRendered();
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() {
let templateFrame = document.getElementById("templateFrame");
await SimpleTest.promiseFocus(templateFrame.contentWindow);
@ -70,6 +78,13 @@ add_task(async function setup_once() {
add_task(async function test_initialState() {
let form = new BasicCardForm();
await form.requestStore.setState({
savedAddresses: {
"TimBLGUID": createAddressRecord(PTU.Addresses.TimBL),
},
});
let {page} = form.requestStore.getState();
is(page.id, "payment-summary", "Check initial page");
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.
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");
form.remove();
@ -116,18 +134,18 @@ add_task(async function test_saveButton() {
let form = new BasicCardForm();
form.dataset.nextButtonLabel = "Next";
form.dataset.errorGenericSave = "Generic error";
form.dataset.invalidAddressLabel = "Invalid";
await form.promiseReady;
display.appendChild(form);
let address1 = deepClone(PTU.Addresses.TimBL);
address1.guid = "TimBLGUID";
let address2 = deepClone(PTU.Addresses.TimBL2);
address2.guid = "TimBL2GUID";
let address1 = createAddressRecord(PTU.Addresses.TimBL, {guid: "TimBLGUID"});
let address2 = createAddressRecord(PTU.Addresses.TimBL2, {guid: "TimBL2GUID"});
await form.requestStore.setState({
request: {
paymentMethods,
paymentDetails: {},
},
savedAddresses: {
[address1.guid]: deepClone(address1),
@ -274,10 +292,8 @@ add_task(async function test_add_selectedShippingAddress() {
card1.guid = "9864798564";
card1["cc-exp-year"] = 2011;
let address1 = deepClone(PTU.Addresses.TimBL);
address1.guid = "TimBLGUID";
let address2 = deepClone(PTU.Addresses.TimBL2);
address2.guid = "TimBL2GUID";
let address1 = createAddressRecord(PTU.Addresses.TimBL, { guid: "TimBLGUID" });
let address2 = createAddressRecord(PTU.Addresses.TimBL2, { guid: "TimBL2GUID" });
await form.requestStore.setState({
page: {
@ -312,8 +328,7 @@ add_task(async function test_add_noSelectedShippingAddress() {
card1.guid = "9864798564";
card1["cc-exp-year"] = 2011;
let address1 = deepClone(PTU.Addresses.TimBL);
address1.guid = "TimBLGUID";
let address1 = createAddressRecord(PTU.Addresses.TimBL, { guid: "TimBLGUID" });
await form.requestStore.setState({
page: {
@ -352,8 +367,7 @@ add_task(async function test_edit() {
display.appendChild(form);
await asyncElementRendered();
let address1 = deepClone(PTU.Addresses.TimBL);
address1.guid = "TimBLGUID";
let address1 = createAddressRecord(PTU.Addresses.TimBL, { guid: "TimBLGUID" });
info("test year before current");
let card1 = deepClone(PTU.BasicCards.JohnDoe);
@ -364,6 +378,7 @@ add_task(async function test_edit() {
await form.requestStore.setState({
request: {
paymentMethods,
paymentDetails: {},
},
page: {
id: "basic-card-page",
@ -383,6 +398,7 @@ add_task(async function test_edit() {
is(form.saveButton.textContent, "Update", "Check label");
is(form.querySelectorAll(":-moz-ui-invalid").length, 0,
"Check no fields are visibly invalid on an 'edit' form with a complete card");
checkCCForm(form, card1);
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");
@ -460,6 +476,17 @@ add_task(async function test_field_validity_updates() {
form.dataset.updateButtonLabel = "Update";
await form.promiseReady;
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();
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 monthInput = form.form.querySelector("#cc-exp-month");
let yearInput = form.form.querySelector("#cc-exp-year");
let addressPicker = form.querySelector("#billingAddressGUID");
info("test with valid cc-number but missing cc-name");
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(yearInput.checkValidity(), "cc-exp-year 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");
info("edit to make the cc-number invalid");
@ -518,6 +556,17 @@ add_task(async function test_numberCustomValidityReset() {
form.dataset.updateButtonLabel = "Update";
await form.promiseReady;
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();
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>