diff --git a/browser/components/payments/res/components/rich-select.js b/browser/components/payments/res/components/rich-select.js index 7f977bfd2ceb..5c92139c9c57 100644 --- a/browser/components/payments/res/components/rich-select.js +++ b/browser/components/payments/res/components/rich-select.js @@ -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); } diff --git a/browser/components/payments/res/containers/address-picker.js b/browser/components/payments/res/containers/address-picker.js index 59df65c21105..5ca9c312b6c5 100644 --- a/browser/components/payments/res/containers/address-picker.js +++ b/browser/components/payments/res/containers/address-picker.js @@ -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; } diff --git a/browser/components/payments/res/containers/basic-card-form.css b/browser/components/payments/res/containers/basic-card-form.css index d09e85015e11..f4a8721e035c 100644 --- a/browser/components/payments/res/containers/basic-card-form.css +++ b/browser/components/payments/res/containers/basic-card-form.css @@ -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; diff --git a/browser/components/payments/res/containers/basic-card-form.js b/browser/components/payments/res/containers/basic-card-form.js index d9d516faf8de..061608672864 100644 --- a/browser/components/payments/res/containers/basic-card-form.js +++ b/browser/components/payments/res/containers/basic-card-form.js @@ -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 '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); diff --git a/browser/components/payments/res/containers/rich-picker.js b/browser/components/payments/res/containers/rich-picker.js index 33e52ae6f79c..022d7c66d90f 100644 --- a/browser/components/payments/res/containers/rich-picker.js +++ b/browser/components/payments/res/containers/rich-picker.js @@ -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)); } diff --git a/browser/components/payments/res/paymentRequest.xhtml b/browser/components/payments/res/paymentRequest.xhtml index 46f3247a26c9..e136187fabd0 100644 --- a/browser/components/payments/res/paymentRequest.xhtml +++ b/browser/components/payments/res/paymentRequest.xhtml @@ -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;" diff --git a/browser/components/payments/test/browser/browser_card_edit.js b/browser/components/payments/test/browser/browser_card_edit.js index 10e429e26991..a49cd2b55e18 100644 --- a/browser/components/payments/test/browser/browser_card_edit.js +++ b/browser/components/payments/test/browser/browser_card_edit.js @@ -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() { diff --git a/browser/components/payments/test/mochitest/mochitest.ini b/browser/components/payments/test/mochitest/mochitest.ini index 5b4879465382..2a6cbf4b5bcd 100644 --- a/browser/components/payments/test/mochitest/mochitest.ini +++ b/browser/components/payments/test/mochitest/mochitest.ini @@ -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] diff --git a/browser/components/payments/test/mochitest/test_address_form.html b/browser/components/payments/test/mochitest/test_address_form.html index 5a310f52d0c6..6cfe58628664 100644 --- a/browser/components/payments/test/mochitest/test_address_form.html +++ b/browser/components/payments/test/mochitest/test_address_form.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); diff --git a/browser/components/payments/test/mochitest/test_basic_card_form.html b/browser/components/payments/test/mochitest/test_basic_card_form.html index c3a65987a269..2ec161b06e89 100644 --- a/browser/components/payments/test/mochitest/test_basic_card_form.html +++ b/browser/components/payments/test/mochitest/test_basic_card_form.html @@ -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"); diff --git a/browser/components/payments/test/mochitest/test_billing_address_picker.html b/browser/components/payments/test/mochitest/test_billing_address_picker.html new file mode 100644 index 000000000000..1d022a50b521 --- /dev/null +++ b/browser/components/payments/test/mochitest/test_billing_address_picker.html @@ -0,0 +1,133 @@ + + + + + + Test the billing-address-picker component + + + + + + + + + + + + + +

+ + +

+ +
+
+ + + +