diff options
Diffstat (limited to 'browser/components/payments/test/mochitest')
25 files changed, 5113 insertions, 0 deletions
diff --git a/browser/components/payments/test/mochitest/formautofill/mochitest.ini b/browser/components/payments/test/mochitest/formautofill/mochitest.ini new file mode 100644 index 0000000000..9740f9e3e8 --- /dev/null +++ b/browser/components/payments/test/mochitest/formautofill/mochitest.ini @@ -0,0 +1,10 @@ +[DEFAULT] +# This manifest mostly exists so that the support-files below can be referenced +# from a relative path of formautofill/* from the tests in the above directory +# to resemble the layout in the shipped JAR file. +support-files = + ../../../../../../browser/extensions/formautofill/content/editCreditCard.xhtml + ../../../../../../browser/extensions/formautofill/content/editAddress.xhtml + +skip-if = true # Bug 1446164 +[test_editCreditCard.html] diff --git a/browser/components/payments/test/mochitest/formautofill/test_editCreditCard.html b/browser/components/payments/test/mochitest/formautofill/test_editCreditCard.html new file mode 100644 index 0000000000..4d4a06e35a --- /dev/null +++ b/browser/components/payments/test/mochitest/formautofill/test_editCreditCard.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test that editCreditCard.xhtml is accessible for tests in the parent directory. +--> +<head> + <meta charset="utf-8"> + <title>Test that editCreditCard.xhtml is accessible</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <p id="display"> + <iframe id="editCreditCard" src="editCreditCard.xhtml"></iframe> + </p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="application/javascript"> + +add_task(async function test_editCreditCard() { + let editCreditCard = document.getElementById("editCreditCard").contentWindow; + await SimpleTest.promiseFocus(editCreditCard); + ok(editCreditCard.document.getElementById("form"), "Check form is present"); + ok(editCreditCard.document.getElementById("cc-number"), "Check cc-number is present"); +}); + +</script> + +</body> +</html> diff --git a/browser/components/payments/test/mochitest/mochitest.ini b/browser/components/payments/test/mochitest/mochitest.ini new file mode 100644 index 0000000000..d1d2907496 --- /dev/null +++ b/browser/components/payments/test/mochitest/mochitest.ini @@ -0,0 +1,37 @@ +[DEFAULT] +support-files = + !/browser/extensions/formautofill/content/editAddress.xhtml + !/browser/extensions/formautofill/content/editCreditCard.xhtml + ../../../../../browser/extensions/formautofill/content/autofillEditForms.js + ../../../../../browser/extensions/formautofill/skin/shared/editDialog-shared.css + ../../../../../testing/modules/sinon-7.2.7.js + # paymentRequest.xhtml is needed for `importDialogDependencies` so that the relative paths of + # formautofill/edit*.xhtml work from the *-form elements in paymentRequest.xhtml. + ../../res/paymentRequest.xhtml + ../../res/** + payments_common.js +skip-if = true || !e10s # Bug 1515048 - Disable for now. Bug 1365964 - Payment Request isn't implemented for non-e10s. + +[test_accepted_cards.html] +[test_address_form.html] +[test_address_option.html] +skip-if = os == "linux" || os == "win" # Bug 1493216 +[test_address_picker.html] +[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] +[test_order_details.html] +[test_payer_address_picker.html] +[test_payment_dialog.html] +[test_payment_dialog_required_top_level_items.html] +[test_payment_details_item.html] +[test_payment_method_picker.html] +[test_rich_select.html] +[test_shipping_option_picker.html] +[test_ObservedPropertiesMixin.html] +[test_PaymentsStore.html] +[test_PaymentStateSubscriberMixin.html] diff --git a/browser/components/payments/test/mochitest/payments_common.js b/browser/components/payments/test/mochitest/payments_common.js new file mode 100644 index 0000000000..8e48585318 --- /dev/null +++ b/browser/components/payments/test/mochitest/payments_common.js @@ -0,0 +1,154 @@ +"use strict"; + +/* exported asyncElementRendered, promiseStateChange, promiseContentToChromeMessage, deepClone, + PTU, registerConsoleFilter, fillField, importDialogDependencies */ + +const PTU = SpecialPowers.Cu.import( + "resource://testing-common/PaymentTestUtils.jsm", + {} +).PaymentTestUtils; + +/** + * A helper to await on while waiting for an asynchronous rendering of a Custom + * Element. + * @returns {Promise} + */ +function asyncElementRendered() { + return Promise.resolve(); +} + +function promiseStateChange(store) { + return new Promise(resolve => { + store.subscribe({ + stateChangeCallback(state) { + store.unsubscribe(this); + resolve(state); + }, + }); + }); +} + +/** + * Wait for a message of `messageType` from content to chrome and resolve with the event details. + * @param {string} messageType of the expected message + * @returns {Promise} when the message is dispatched + */ +function promiseContentToChromeMessage(messageType) { + return new Promise(resolve => { + document.addEventListener("paymentContentToChrome", function onCToC(event) { + if (event.detail.messageType != messageType) { + return; + } + document.removeEventListener("paymentContentToChrome", onCToC); + resolve(event.detail); + }); + }); +} + +/** + * Import the templates and stylesheets from the real shipping dialog to avoid + * duplication in the tests. + * @param {HTMLIFrameElement} templateFrame - Frame to copy the resources from + * @param {HTMLElement} destinationEl - Where to append the copied resources + */ +function importDialogDependencies(templateFrame, destinationEl) { + let templates = templateFrame.contentDocument.querySelectorAll("template"); + isnot(templates, null, "Check some templates found"); + for (let template of templates) { + let imported = document.importNode(template, true); + destinationEl.appendChild(imported); + } + + let baseURL = new URL("../../res/", window.location.href); + let stylesheetLinks = templateFrame.contentDocument.querySelectorAll( + "link[rel~='stylesheet']" + ); + for (let stylesheet of stylesheetLinks) { + let imported = document.importNode(stylesheet, true); + imported.href = new URL(imported.getAttribute("href"), baseURL); + destinationEl.appendChild(imported); + } +} + +function deepClone(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +/** + * @param {HTMLElement} field + * @param {string} value + * @note This is async in case we need to make it async to handle focus in the future. + * @note Keep in sync with the copy in head.js + */ +async function fillField(field, value) { + field.focus(); + if (field.localName == "select") { + if (field.value == value) { + // Do nothing + return; + } + field.value = value; + field.dispatchEvent(new Event("input", { bubbles: true })); + field.dispatchEvent(new Event("change", { bubbles: true })); + return; + } + while (field.value) { + sendKey("BACK_SPACE"); + } + sendString(value); +} + +/** + * If filterFunction is a function which returns true given a console message + * then the test won't fail from that message. + */ +let filterFunction = null; +function registerConsoleFilter(filterFn) { + filterFunction = filterFn; +} + +// Listen for errors to fail tests +SpecialPowers.registerConsoleListener(function onConsoleMessage(msg) { + if ( + msg.isWarning || + !msg.errorMessage || + msg.errorMessage == "paymentRequest.xhtml:" + ) { + // Ignore warnings and non-errors. + return; + } + if ( + msg.category == "CSP_CSPViolationWithURI" && + msg.errorMessage.includes("at inline") + ) { + // Ignore unknown CSP error. + return; + } + if ( + msg.message && + msg.message.includes("Security Error: Content at http://mochi.test:8888") + ) { + // Check for same-origin policy violations and ignore specific errors + if ( + msg.message.includes("icon-credit-card-generic.svg") || + msg.message.includes("accepted-cards.css") || + msg.message.includes("editDialog-shared.css") || + msg.message.includes("editAddress.css") || + msg.message.includes("editDialog.css") || + msg.message.includes("editCreditCard.css") + ) { + return; + } + } + if (msg.message == "SENTINEL") { + filterFunction = null; + } + if (filterFunction && filterFunction(msg)) { + return; + } + ok(false, msg.message || msg.errorMessage); +}); + +SimpleTest.registerCleanupFunction(function cleanup() { + SpecialPowers.postConsoleSentinel(); +}); diff --git a/browser/components/payments/test/mochitest/test_ObservedPropertiesMixin.html b/browser/components/payments/test/mochitest/test_ObservedPropertiesMixin.html new file mode 100644 index 0000000000..00dff76a5f --- /dev/null +++ b/browser/components/payments/test/mochitest/test_ObservedPropertiesMixin.html @@ -0,0 +1,116 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test the ObservedPropertiesMixin +--> +<head> + <meta charset="utf-8"> + <title>Test the ObservedPropertiesMixin</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="payments_common.js"></script> + + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <p id="display"> + <test-element id="el1" one="foo" two-word="bar"></test-element> + </p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="module"> +/** Test the ObservedPropertiesMixin **/ + +import ObservedPropertiesMixin from "../../res/mixins/ObservedPropertiesMixin.js"; + +class TestElement extends ObservedPropertiesMixin(HTMLElement) { + static get observedAttributes() { + return ["one", "two-word"]; + } + + render() { + this.textContent = JSON.stringify({ + one: this.one, + twoWord: this.twoWord, + }); + } +} + +customElements.define("test-element", TestElement); +let el1 = document.getElementById("el1"); + +add_task(async function test_default_properties() { + is(el1.one, "foo", "Check .one matches @one"); + is(el1.twoWord, "bar", "Check .twoWord matches @two-word"); + let expected = `{"one":"foo","twoWord":"bar"}`; + is(el1.textContent, expected, "Check textContent"); +}); + +add_task(async function test_set_properties() { + el1.one = "a"; + el1.twoWord = "b"; + is(el1.one, "a", "Check .one value"); + is(el1.getAttribute("one"), "a", "Check @one"); + is(el1.twoWord, "b", "Check .twoWord value"); + is(el1.getAttribute("two-word"), "b", "Check @two-word"); + let expected = `{"one":"a","twoWord":"b"}`; + await asyncElementRendered(); + is(el1.textContent, expected, "Check textContent"); +}); + +add_task(async function test_set_attributes() { + el1.setAttribute("one", "X"); + el1.setAttribute("two-word", "Y"); + is(el1.one, "X", "Check .one value"); + is(el1.getAttribute("one"), "X", "Check @one"); + is(el1.twoWord, "Y", "Check .twoWord value"); + is(el1.getAttribute("two-word"), "Y", "Check @two-word"); + let expected = `{"one":"X","twoWord":"Y"}`; + await asyncElementRendered(); + is(el1.textContent, expected, "Check textContent"); +}); + +add_task(async function test_async_render() { + // Setup + el1.setAttribute("one", "1"); + el1.setAttribute("two-word", "2"); + await asyncElementRendered(); // Wait for the async render + + el1.setAttribute("one", "new1"); + + is(el1.one, "new1", "Check .one value"); + is(el1.getAttribute("one"), "new1", "Check @one"); + is(el1.twoWord, "2", "Check .twoWord value"); + is(el1.getAttribute("two-word"), "2", "Check @two-word"); + let expected = `{"one":"1","twoWord":"2"}`; + is(el1.textContent, expected, "Check textContent is still old value due to async rendering"); + await asyncElementRendered(); + expected = `{"one":"new1","twoWord":"2"}`; + is(el1.textContent, expected, "Check textContent now has the new value"); +}); + +add_task(async function test_batched_render() { + // Setup + el1.setAttribute("one", "1"); + el1.setAttribute("two-word", "2"); + await asyncElementRendered(); + + el1.setAttribute("one", "new1"); + el1.setAttribute("two-word", "new2"); + + is(el1.one, "new1", "Check .one value"); + is(el1.getAttribute("one"), "new1", "Check @one"); + is(el1.twoWord, "new2", "Check .twoWord value"); + is(el1.getAttribute("two-word"), "new2", "Check @two-word"); + let expected = `{"one":"1","twoWord":"2"}`; + is(el1.textContent, expected, "Check textContent is still old value due to async rendering"); + await asyncElementRendered(); + expected = `{"one":"new1","twoWord":"new2"}`; + is(el1.textContent, expected, "Check textContent now has the new value"); +}); +</script> + +</body> +</html> diff --git a/browser/components/payments/test/mochitest/test_PaymentStateSubscriberMixin.html b/browser/components/payments/test/mochitest/test_PaymentStateSubscriberMixin.html new file mode 100644 index 0000000000..73e3786288 --- /dev/null +++ b/browser/components/payments/test/mochitest/test_PaymentStateSubscriberMixin.html @@ -0,0 +1,79 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test the PaymentStateSubscriberMixin +--> +<head> + <meta charset="utf-8"> + <title>Test the PaymentStateSubscriberMixin</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="sinon-7.2.7.js"></script> + <script src="payments_common.js"></script> + + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <p id="display"> + <test-element id="el1"></test-element> + </p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="module"> +/** Test the PaymentStateSubscriberMixin **/ + +/* global sinon */ + +import PaymentStateSubscriberMixin from "../../res/mixins/PaymentStateSubscriberMixin.js"; + +class TestElement extends PaymentStateSubscriberMixin(HTMLElement) { + render(state) { + this.textContent = JSON.stringify(state); + } +} + +// We must spy on the prototype by creating the instance in order to test Custom Element reactions. +sinon.spy(TestElement.prototype, "disconnectedCallback"); + +customElements.define("test-element", TestElement); +let el1 = document.getElementById("el1"); + +sinon.spy(el1, "render"); +sinon.spy(el1, "stateChangeCallback"); + +add_task(async function test_initialState() { + let parsedState = JSON.parse(el1.textContent); + ok(!!parsedState.request, "Check initial state contains `request`"); + ok(!!parsedState.savedAddresses, "Check initial state contains `savedAddresses`"); + ok(!!parsedState.savedBasicCards, "Check initial state contains `savedBasicCards`"); +}); + +add_task(async function test_async_batched_render() { + el1.requestStore.setState({a: 1}); + el1.requestStore.setState({b: 2}); + await asyncElementRendered(); + ok(el1.stateChangeCallback.calledOnce, "stateChangeCallback called once"); + ok(el1.render.calledOnce, "render called once"); + + let parsedState = JSON.parse(el1.textContent); + is(parsedState.a, 1, "Check a"); + is(parsedState.b, 2, "Check b"); +}); + +add_task(async function test_disconnect() { + el1.disconnectedCallback.reset(); + el1.render.reset(); + el1.stateChangeCallback.reset(); + el1.remove(); + ok(el1.disconnectedCallback.calledOnce, "disconnectedCallback called once"); + await el1.requestStore.setState({a: 3}); + await asyncElementRendered(); + ok(el1.stateChangeCallback.notCalled, "stateChangeCallback not called"); + ok(el1.render.notCalled, "render not called"); +}); +</script> + +</body> +</html> diff --git a/browser/components/payments/test/mochitest/test_PaymentsStore.html b/browser/components/payments/test/mochitest/test_PaymentsStore.html new file mode 100644 index 0000000000..26f29e96ba --- /dev/null +++ b/browser/components/payments/test/mochitest/test_PaymentsStore.html @@ -0,0 +1,168 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test the PaymentsStore +--> +<head> + <meta charset="utf-8"> + <title>Test the PaymentsStore</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + + <script src="sinon-7.2.7.js"></script> + <script src="payments_common.js"></script> + + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <p id="display"> + </p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="module"> +/** Test the PaymentsStore **/ + +/* global sinon */ + +import PaymentsStore from "../../res/PaymentsStore.js"; + +function assert_throws(block, expectedError, message) { + let actual; + try { + block(); + } catch (e) { + actual = e; + } + ok(actual, "Expecting exception: " + message); + ok(actual instanceof expectedError, + `Check error type is ${expectedError.prototype.name}: ${message}`); +} + +add_task(async function test_defaultState() { + ok(!!PaymentsStore, "Check PaymentsStore import"); + let ps = new PaymentsStore({ + foo: "bar", + }); + + let state = ps.getState(); + ok(!!state, "Check state is truthy"); + is(state.foo, "bar", "Check .foo"); + + assert_throws(() => state.foo = "new", TypeError, "Assigning to existing prop. should throw"); + assert_throws(() => state.other = "something", TypeError, "Adding a new prop. should throw"); + assert_throws(() => delete state.foo, TypeError, "Deleting a prop. should throw"); +}); + +add_task(async function test_setState() { + let ps = new PaymentsStore({}); + + ps.setState({ + one: "one", + }); + let state = ps.getState(); + is(Object.keys(state).length, 1, "Should only have 1 prop. set"); + is(state.one, "one", "Check .one"); + + ps.setState({ + two: 2, + }); + state = ps.getState(); + is(Object.keys(state).length, 2, "Should have 2 props. set"); + is(state.one, "one", "Check .one"); + is(state.two, 2, "Check .two"); + + ps.setState({ + one: "a", + two: "b", + }); + state = ps.getState(); + is(state.one, "a", "Check .one"); + is(state.two, "b", "Check .two"); + + info("check consecutive setState for the same prop"); + ps.setState({ + one: "c", + }); + ps.setState({ + one: "d", + }); + state = ps.getState(); + is(Object.keys(state).length, 2, "Should have 2 props. set"); + is(state.one, "d", "Check .one"); + is(state.two, "b", "Check .two"); +}); + +add_task(async function test_subscribe_unsubscribe() { + let ps = new PaymentsStore({}); + let subscriber = { + stateChangePromise: null, + _stateChangeResolver: null, + + reset() { + this.stateChangePromise = new Promise(resolve => { + this._stateChangeResolver = resolve; + }); + }, + + stateChangeCallback(state) { + this._stateChangeResolver(state); + this.stateChangePromise = new Promise(resolve => { + this._stateChangeResolver = resolve; + }); + }, + }; + + sinon.spy(subscriber, "stateChangeCallback"); + subscriber.reset(); + ps.subscribe(subscriber); + info("subscribe the same listener twice to ensure it still doesn't call the callback"); + ps.subscribe(subscriber); + ok(subscriber.stateChangeCallback.notCalled, + "Check not called synchronously when subscribing"); + + let changePromise = subscriber.stateChangePromise; + ps.setState({ + a: 1, + }); + ok(subscriber.stateChangeCallback.notCalled, + "Check not called synchronously for changes"); + let state = await changePromise; + is(state, subscriber.stateChangeCallback.getCall(0).args[0], + "Check resolved state is last state"); + is(JSON.stringify(state), `{"a":1}`, "Check callback state"); + + info("Testing consecutive setState"); + subscriber.reset(); + subscriber.stateChangeCallback.reset(); + changePromise = subscriber.stateChangePromise; + ps.setState({ + a: 2, + }); + ps.setState({ + a: 3, + }); + ok(subscriber.stateChangeCallback.notCalled, + "Check not called synchronously for changes"); + state = await changePromise; + is(state, subscriber.stateChangeCallback.getCall(0).args[0], + "Check resolved state is last state"); + is(JSON.stringify(subscriber.stateChangeCallback.getCall(0).args[0]), `{"a":3}`, + "Check callback state matches second setState"); + + info("test unsubscribe"); + subscriber.stateChangeCallback = function unexpectedChange() { + ok(false, "stateChangeCallback shouldn't be called after unsubscribing"); + }; + ps.unsubscribe(subscriber); + ps.setState({ + a: 4, + }); + await Promise.resolve("giving a chance for the callback to be called"); +}); +</script> + +</body> +</html> diff --git a/browser/components/payments/test/mochitest/test_accepted_cards.html b/browser/components/payments/test/mochitest/test_accepted_cards.html new file mode 100644 index 0000000000..8e1da1bf3c --- /dev/null +++ b/browser/components/payments/test/mochitest/test_accepted_cards.html @@ -0,0 +1,111 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test the accepted-cards element +--> +<head> + <meta charset="utf-8"> + <title>Test the accepted-cards element</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="sinon-7.2.7.js"></script> + <script src="payments_common.js"></script> + <script src="../../res/unprivileged-fallbacks.js"></script> + + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <link rel="stylesheet" type="text/css" href="../../res/paymentRequest.css"/> + <link rel="stylesheet" type="text/css" href="../../res/components/accepted-cards.css"/> +</head> +<body> + <p id="display"> + <accepted-cards label="Accepted:"></accepted-cards> + </p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="module"> +/** Test the accepted-cards component **/ + +/* global sinon, PaymentDialogUtils */ + +import "../../res/components/accepted-cards.js"; +import {requestStore} from "../../res/mixins/PaymentStateSubscriberMixin.js"; +let emptyState = requestStore.getState(); +let acceptedElem = document.querySelector("accepted-cards"); +let allNetworks = PaymentDialogUtils.getCreditCardNetworks(); + +add_task(async function test_reConnected() { + let itemsCount = acceptedElem.querySelectorAll(".accepted-cards-item").length; + is(itemsCount, allNetworks.length, "Same number of items as there are supported networks"); + + let container = acceptedElem.parentNode; + let removed = container.removeChild(acceptedElem); + container.appendChild(removed); + let newItemsCount = acceptedElem.querySelectorAll(".accepted-cards-item").length; + is(itemsCount, newItemsCount, "Number of items doesnt changed when re-connected"); +}); + +add_task(async function test_someAccepted() { + let supportedNetworks = ["discover", "amex"]; + let paymentMethods = [{ + supportedMethods: "basic-card", + data: { + supportedNetworks, + }, + }]; + requestStore.setState({ + request: Object.assign({}, emptyState.request, { + paymentMethods, + }), + }); + await asyncElementRendered(); + + let showingItems = acceptedElem.querySelectorAll(".accepted-cards-item:not([hidden])"); + is(showingItems.length, 2, + "Expected 2 items to be showing when 2 supportedNetworks are indicated"); + for (let network of allNetworks) { + if (supportedNetworks.includes(network)) { + ok(acceptedElem.querySelector(`[data-network-id='${network}']:not([hidden])`), + `Item for the ${network} network expected to be visible`); + } else { + ok(acceptedElem.querySelector(`[data-network-id='${network}'][hidden]`), + `Item for the ${network} network expected to be hidden`); + } + } +}); + +add_task(async function test_officialBranding() { + // verify we get the expected result when isOfficialBranding returns true + sinon.stub(PaymentDialogUtils, "isOfficialBranding").callsFake(() => { return true; }); + + let container = acceptedElem.parentNode; + let removed = container.removeChild(acceptedElem); + container.appendChild(removed); + + ok(PaymentDialogUtils.isOfficialBranding.calledOnce, + "isOfficialBranding was called"); + ok(acceptedElem.classList.contains("branded"), + "The branded class is added when isOfficialBranding returns true"); + PaymentDialogUtils.isOfficialBranding.restore(); + + // verify we get the expected result when isOfficialBranding returns false + sinon.stub(PaymentDialogUtils, "isOfficialBranding").callsFake(() => { return false; }); + + // the branded class is toggled in the 'connectedCallback', + // so remove and re-add the element to re-evaluate branded-ness + removed = container.removeChild(acceptedElem); + container.appendChild(removed); + + ok(PaymentDialogUtils.isOfficialBranding.calledOnce, + "isOfficialBranding was called"); + ok(!acceptedElem.classList.contains("branded"), + "The branded class is removed when isOfficialBranding returns false"); + PaymentDialogUtils.isOfficialBranding.restore(); +}); + +</script> + +</body> +</html> diff --git a/browser/components/payments/test/mochitest/test_address_form.html b/browser/components/payments/test/mochitest/test_address_form.html new file mode 100644 index 0000000000..906e5cc0f5 --- /dev/null +++ b/browser/components/payments/test/mochitest/test_address_form.html @@ -0,0 +1,955 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test the address-form element +--> +<head> + <meta charset="utf-8"> + <title>Test the address-form element</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="sinon-7.2.7.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="/tests/SimpleTest/test.css"/> + <link rel="stylesheet" type="text/css" href="../../res/paymentRequest.css"/> + <link rel="stylesheet" type="text/css" href="editDialog-shared.css"/> + <link rel="stylesheet" type="text/css" href="../../res/containers/address-form.css"/> +</head> +<body> + <p id="display"> + </p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="module"> +/** Test the address-form element **/ + +/* global sinon, PaymentDialogUtils */ + +import AddressForm from "../../res/containers/address-form.js"; + +let display = document.getElementById("display"); + +function checkAddressForm(customEl, expectedAddress) { + const ADDRESS_PROPERTY_NAMES = [ + "given-name", + "family-name", + "organization", + "street-address", + "address-level2", + "address-level1", + "postal-code", + "country", + "email", + "tel", + ]; + for (let propName of ADDRESS_PROPERTY_NAMES) { + let expectedVal = expectedAddress[propName] || ""; + is(document.getElementById(propName).value, + expectedVal.toString(), + `Check ${propName}`); + } +} + +function sendStringAndCheckValidity(element, string, isValid) { + fillField(element, string); + ok(element.checkValidity() == isValid, + `${element.id} should be ${isValid ? "valid" : "invalid"} ("${string}")`); +} + +add_task(async function test_initialState() { + let form = new AddressForm(); + form.id = "shipping-address-page"; + form.setAttribute("selected-state-key", "selectedShippingAddress"); + + await form.requestStore.setState({ + "test-page": {}, + }); + + let {page} = form.requestStore.getState(); + is(page.id, "payment-summary", "Check initial page"); + await form.promiseReady; + display.appendChild(form); + await asyncElementRendered(); + is(page.id, "payment-summary", "Check initial page after appending"); + + // :-moz-ui-invalid, unlike :invalid, only applies to fields showing the error outline. + let fieldsVisiblyInvalid = form.querySelectorAll(":-moz-ui-invalid"); + is(fieldsVisiblyInvalid.length, 0, "Check no fields are visibly invalid on an empty 'add' form"); + + form.remove(); +}); + +add_task(async function test_pageTitle() { + let address1 = deepClone(PTU.Addresses.TimBL); + address1.guid = "9864798564"; + + // the element can have all the data attributes. We'll add them all up front + let form = new AddressForm(); + let id = "shipping-address-page"; + form.id = id; + form.dataset.titleAdd = `Add Title`; + form.dataset.titleEdit = `Edit Title`; + form.setAttribute("selected-state-key", "selectedShippingAddress"); + + await form.promiseReady; + display.appendChild(form); + + let newState = { + page: { id }, + [id]: {}, + savedAddresses: { + [address1.guid]: address1, + }, + request: { + paymentDetails: {}, + paymentOptions: { shippingOption: "shipping" }, + }, + }; + await form.requestStore.setState(newState); + await asyncElementRendered(); + is(form.pageTitleHeading.textContent, "Add Title", "Check 'add' title"); + + // test the 'edit' variation + newState = deepClone(newState); + newState[id].guid = address1.guid; + await form.requestStore.setState(newState); + await asyncElementRendered(); + is(form.pageTitleHeading.textContent, "Edit Title", "Check 'edit' title"); + + form.remove(); +}); + +add_task(async function test_backButton() { + let form = new AddressForm(); + form.id = "test-page"; + form.dataset.titleAdd = "Sample add page title"; + form.dataset.backButtonLabel = "Back"; + form.setAttribute("selected-state-key", "selectedShippingAddress"); + + await form.promiseReady; + display.appendChild(form); + + await form.requestStore.setState({ + "test-page": {}, + page: { + id: "test-page", + }, + request: { + paymentDetails: {}, + paymentOptions: {}, + }, + }); + await asyncElementRendered(); + + let stateChangePromise = promiseStateChange(form.requestStore); + is(form.pageTitleHeading.textContent, "Sample add page title", "Check title"); + + is(form.backButton.textContent, "Back", "Check label"); + form.backButton.scrollIntoView(); + synthesizeMouseAtCenter(form.backButton, {}); + + let {page} = await stateChangePromise; + is(page.id, "payment-summary", "Check initial page after appending"); + + form.remove(); +}); + +add_task(async function test_saveButton() { + let form = new AddressForm(); + form.id = "shipping-address-page"; + form.setAttribute("selected-state-key", "selectedShippingAddress"); + form.dataset.nextButtonLabel = "Next"; + form.dataset.errorGenericSave = "Generic error"; + await form.promiseReady; + display.appendChild(form); + form.requestStore.setState({ + page: { + id: "shipping-address-page", + }, + "shipping-address-page": {}, + }); + await asyncElementRendered(); + + ok(form.saveButton.disabled, "Save button initially disabled"); + fillField(form.form.querySelector("#given-name"), "Jaws"); + fillField(form.form.querySelector("#family-name"), "Swaj"); + fillField(form.form.querySelector("#organization"), "Allizom"); + fillField(form.form.querySelector("#street-address"), "404 Internet Super Highway"); + fillField(form.form.querySelector("#address-level2"), "Firefoxity City"); + fillField(form.form.querySelector("#country"), "US"); + fillField(form.form.querySelector("#address-level1"), "CA"); + fillField(form.form.querySelector("#postal-code"), "00001"); + fillField(form.form.querySelector("#tel"), "+15555551212"); + + ok(!form.saveButton.disabled, "Save button is enabled after filling"); + + info("blanking the street-address"); + fillField(form.form.querySelector("#street-address"), ""); + ok(form.saveButton.disabled, "Save button is disabled after blanking street-address"); + form.form.querySelector("#street-address").blur(); + let fieldsVisiblyInvalid = form.querySelectorAll(":-moz-ui-invalid"); + is(fieldsVisiblyInvalid.length, 1, "Check 1 field visibly invalid after blanking and blur"); + is(fieldsVisiblyInvalid[0].id, "street-address", "Check #street-address is visibly invalid"); + + fillField(form.form.querySelector("#street-address"), "404 Internet Super Highway"); + is(form.querySelectorAll(":-moz-ui-invalid").length, 0, "Check no fields visibly invalid"); + ok(!form.saveButton.disabled, "Save button is enabled after re-filling street-address"); + + fillField(form.form.querySelector("#country"), "CA"); + ok(form.saveButton.disabled, "Save button is disabled after changing the country to Canada"); + fillField(form.form.querySelector("#country"), "US"); + ok(form.saveButton.disabled, + "Save button is disabled after changing the country back to US since address-level1 " + + "got cleared when changing countries"); + fillField(form.form.querySelector("#address-level1"), "CA"); + ok(!form.saveButton.disabled, "Save button is enabled after re-entering address-level1"); + + let messagePromise = promiseContentToChromeMessage("updateAutofillRecord"); + is(form.saveButton.textContent, "Next", "Check label"); + form.saveButton.scrollIntoView(); + synthesizeMouseAtCenter(form.saveButton, {}); + + let details = await messagePromise; + ok(typeof(details.messageID) == "number" && details.messageID > 0, "Check messageID type"); + delete details.messageID; + is(details.collectionName, "addresses", "Check collectionName"); + isDeeply(details, { + collectionName: "addresses", + guid: undefined, + messageType: "updateAutofillRecord", + record: { + "given-name": "Jaws", + "family-name": "Swaj", + "additional-name": "", + "organization": "Allizom", + "street-address": "404 Internet Super Highway", + "address-level3": "", + "address-level2": "Firefoxity City", + "address-level1": "CA", + "postal-code": "00001", + "country": "US", + "tel": "+15555551212", + }, + }, "Check event details for the message to chrome"); + form.remove(); +}); + +add_task(async function test_genericError() { + let form = new AddressForm(); + form.id = "test-page"; + form.setAttribute("selected-state-key", "selectedShippingAddress"); + await form.requestStore.setState({ + page: { + id: "test-page", + error: "Generic Error", + }, + }); + await form.promiseReady; + display.appendChild(form); + await asyncElementRendered(); + + ok(!isHidden(form.genericErrorText), "Error message should be visible"); + is(form.genericErrorText.textContent, "Generic Error", "Check error message"); + form.remove(); +}); + +add_task(async function test_edit() { + let form = new AddressForm(); + form.id = "shipping-address-page"; + form.dataset.updateButtonLabel = "Update"; + form.setAttribute("selected-state-key", "selectedShippingAddress"); + await form.promiseReady; + display.appendChild(form); + await asyncElementRendered(); + + let address1 = deepClone(PTU.Addresses.TimBL); + address1.guid = "9864798564"; + + await form.requestStore.setState({ + page: { + id: "shipping-address-page", + }, + "shipping-address-page": { + guid: address1.guid, + }, + savedAddresses: { + [address1.guid]: deepClone(address1), + }, + }); + await asyncElementRendered(); + is(form.querySelectorAll(":-moz-ui-invalid").length, 0, + "Check no fields are visibly invalid on an 'edit' form with a complete address"); + checkAddressForm(form, address1); + + ok(!form.saveButton.disabled, "Save button should be enabled upon edit for a valid address"); + + info("test change to minimal record"); + let minimalAddress = { + "given-name": address1["given-name"], + guid: "9gnjdhen46", + }; + await form.requestStore.setState({ + page: { + id: "shipping-address-page", + }, + "shipping-address-page": { + guid: minimalAddress.guid, + }, + savedAddresses: { + [minimalAddress.guid]: deepClone(minimalAddress), + }, + }); + await asyncElementRendered(); + is(form.saveButton.textContent, "Update", "Check label"); + checkAddressForm(form, minimalAddress); + ok(form.saveButton.disabled, "Save button should be disabled if only the name is filled"); + ok(form.querySelectorAll(":-moz-ui-invalid").length > 3, + "Check fields are visibly invalid on an 'edit' form with only the given-name filled"); + is(form.querySelectorAll("#country:-moz-ui-invalid").length, 1, + "Check that the country `select` is marked as invalid"); + + info("change to no selected address"); + await form.requestStore.setState({ + page: { + id: "shipping-address-page", + }, + "shipping-address-page": {}, + }); + await asyncElementRendered(); + is(form.querySelectorAll(":-moz-ui-invalid").length, 0, + "Check no fields are visibly invalid on an empty 'add' form after being an edit form"); + checkAddressForm(form, { + country: "US", + }); + ok(form.saveButton.disabled, "Save button should be disabled for an empty form"); + + form.remove(); +}); + +add_task(async function test_restricted_address_fields() { + let form = new AddressForm(); + form.id = "payer-address-page"; + form.setAttribute("selected-state-key", "selectedPayerAddress"); + form.dataset.errorGenericSave = "Generic error"; + form.dataset.fieldRequiredSymbol = "*"; + form.dataset.nextButtonLabel = "Next"; + await form.promiseReady; + form.form.dataset.extraRequiredFields = "name email tel"; + display.appendChild(form); + await form.requestStore.setState({ + page: { + id: "payer-address-page", + }, + "payer-address-page": { + addressFields: "name email tel", + }, + }); + await asyncElementRendered(); + + ok(form.saveButton.disabled, "Save button should be disabled due to empty fields"); + + ok(!isHidden(form.form.querySelector("#given-name")), + "given-name should be visible"); + ok(!isHidden(form.form.querySelector("#additional-name")), + "additional-name should be visible"); + ok(!isHidden(form.form.querySelector("#family-name")), + "family-name should be visible"); + ok(isHidden(form.form.querySelector("#organization")), + "organization should be hidden"); + ok(isHidden(form.form.querySelector("#street-address")), + "street-address should be hidden"); + ok(isHidden(form.form.querySelector("#address-level2")), + "address-level2 should be hidden"); + ok(isHidden(form.form.querySelector("#address-level1")), + "address-level1 should be hidden"); + ok(isHidden(form.form.querySelector("#postal-code")), + "postal-code should be hidden"); + ok(isHidden(form.form.querySelector("#country")), + "country should be hidden"); + ok(!isHidden(form.form.querySelector("#email")), + "email should be visible"); + let telField = form.form.querySelector("#tel"); + ok(!isHidden(telField), + "tel should be visible"); + let telContainer = telField.closest(`#${telField.id}-container`); + ok(telContainer.hasAttribute("required"), "tel container should have required attribute"); + let telSpan = telContainer.querySelector("span"); + is(telSpan.getAttribute("fieldRequiredSymbol"), "*", + "tel span should have asterisk as fieldRequiredSymbol"); + is(getComputedStyle(telSpan, "::after").content, "attr(fieldRequiredSymbol)", + "Asterisk should be on tel"); + + fillField(form.form.querySelector("#given-name"), "John"); + fillField(form.form.querySelector("#family-name"), "Smith"); + ok(form.saveButton.disabled, "Save button should be disabled due to empty fields"); + fillField(form.form.querySelector("#email"), "john@example.com"); + ok(form.saveButton.disabled, + "Save button should be disabled due to empty fields"); + fillField(form.form.querySelector("#tel"), "+15555555555"); + ok(!form.saveButton.disabled, "Save button should be enabled with all required fields filled"); + + form.remove(); + await form.requestStore.setState({ + "payer-address-page": {}, + }); +}); + +add_task(async function test_field_validation() { + let form = new AddressForm(); + form.id = "shipping-address-page"; + form.setAttribute("selected-state-key", "selectedShippingAddress"); + form.dataset.fieldRequiredSymbol = "*"; + await form.promiseReady; + display.appendChild(form); + await form.requestStore.setState({ + page: { + id: "shipping-address-page", + }, + }); + await asyncElementRendered(); + + let postalCodeInput = form.form.querySelector("#postal-code"); + let addressLevel1Input = form.form.querySelector("#address-level1"); + ok(!postalCodeInput.value, "postal-code should be empty by default"); + ok(!addressLevel1Input.value, "address-level1 should be empty by default"); + ok(!postalCodeInput.checkValidity(), "postal-code should be invalid by default"); + ok(!addressLevel1Input.checkValidity(), "address-level1 should be invalid by default"); + + let countrySelect = form.form.querySelector("#country"); + let requiredFields = [ + form.form.querySelector("#given-name"), + form.form.querySelector("#street-address"), + form.form.querySelector("#address-level2"), + postalCodeInput, + addressLevel1Input, + countrySelect, + ]; + for (let field of requiredFields) { + let container = field.closest(`#${field.id}-container`); + ok(container.hasAttribute("required"), `#${field.id} container should have required attribute`); + let span = container.querySelector("span"); + is(span.getAttribute("fieldRequiredSymbol"), "*", + "span should have asterisk as fieldRequiredSymbol"); + is(getComputedStyle(span, "::after").content, "attr(fieldRequiredSymbol)", + "Asterisk should be on " + field.id); + } + + ok(form.saveButton.disabled, "Save button should be disabled upon load"); + + fillField(countrySelect, "US"); + + sendStringAndCheckValidity(addressLevel1Input, "MI", true); + sendStringAndCheckValidity(addressLevel1Input, "", false); + sendStringAndCheckValidity(postalCodeInput, "B4N4N4", false); + sendStringAndCheckValidity(addressLevel1Input, "NS", false); + sendStringAndCheckValidity(postalCodeInput, "R3J 3C7", false); + sendStringAndCheckValidity(addressLevel1Input, "", false); + sendStringAndCheckValidity(postalCodeInput, "11109", true); + sendStringAndCheckValidity(addressLevel1Input, "NS", false); + sendStringAndCheckValidity(postalCodeInput, "06390-0001", true); + + fillField(countrySelect, "CA"); + + sendStringAndCheckValidity(postalCodeInput, "00001", false); + sendStringAndCheckValidity(addressLevel1Input, "CA", false); + sendStringAndCheckValidity(postalCodeInput, "94043", false); + sendStringAndCheckValidity(addressLevel1Input, "", false); + sendStringAndCheckValidity(postalCodeInput, "B4N4N4", true); + sendStringAndCheckValidity(addressLevel1Input, "MI", false); + sendStringAndCheckValidity(postalCodeInput, "R3J 3C7", true); + sendStringAndCheckValidity(addressLevel1Input, "", false); + sendStringAndCheckValidity(postalCodeInput, "11109", false); + sendStringAndCheckValidity(addressLevel1Input, "NS", true); + sendStringAndCheckValidity(postalCodeInput, "06390-0001", false); + + form.remove(); +}); + +add_task(async function test_merchantShippingAddressErrors() { + let form = new AddressForm(); + form.id = "shipping-address-page"; + form.setAttribute("selected-state-key", "selectedShippingAddress"); + await form.promiseReady; + + // Merchant errors only make sense when editing a record so add one. + let address1 = deepClone(PTU.Addresses.TimBR); + address1.guid = "9864798564"; + + const state = { + page: { + id: "shipping-address-page", + }, + "shipping-address-page": { + guid: address1.guid, + }, + request: { + paymentDetails: { + shippingAddressErrors: { + addressLine: "Street address needs to start with a D", + city: "City needs to start with a B", + country: "Country needs to start with a C", + dependentLocality: "Can only be SUBURBS, not NEIGHBORHOODS", + organization: "organization needs to start with an A", + phone: "Telephone needs to start with a 9", + postalCode: "Postal code needs to start with a 0", + recipient: "Name needs to start with a Z", + region: "Region needs to start with a Y", + regionCode: "Regions must be 1 to 3 characters in length (sometimes ;) )", + }, + }, + paymentOptions: {}, + }, + savedAddresses: { + [address1.guid]: deepClone(address1), + }, + }; + display.appendChild(form); + await form.requestStore.setState(state); + await asyncElementRendered(); + + function checkValidationMessage(selector, property) { + let expected = state.request.paymentDetails.shippingAddressErrors[property]; + let container = form.form.querySelector(selector + "-container"); + ok(!isHidden(container), selector + "-container should be visible"); + is(form.form.querySelector(selector).validationMessage, + expected, + "Validation message should match for " + selector); + } + + ok(form.saveButton.disabled, "Save button should be disabled due to validation errors"); + + checkValidationMessage("#street-address", "addressLine"); + checkValidationMessage("#address-level2", "city"); + checkValidationMessage("#address-level3", "dependentLocality"); + checkValidationMessage("#country", "country"); + checkValidationMessage("#organization", "organization"); + checkValidationMessage("#tel", "phone"); + checkValidationMessage("#postal-code", "postalCode"); + checkValidationMessage("#given-name", "recipient"); + checkValidationMessage("#address-level1", "regionCode"); + isnot(form.form.querySelector("#address-level1"), + state.request.paymentDetails.shippingAddressErrors.region, + "When both region and regionCode are supplied we only show the 'regionCode' error"); + + // TODO: bug 1482808 - the save button should be enabled after editing the fields + + form.remove(); +}); + +add_task(async function test_customMerchantValidity_reset() { + let form = new AddressForm(); + form.id = "shipping-address-page"; + form.setAttribute("selected-state-key", "selectedShippingAddress"); + await form.promiseReady; + + // Merchant errors only make sense when editing a record so add one. + let address1 = deepClone(PTU.Addresses.TimBL); + address1.guid = "9864798564"; + + const state = { + page: { + id: "shipping-address-page", + }, + "shipping-address-page": { + guid: address1.guid, + }, + request: { + paymentDetails: { + shippingAddressErrors: { + addressLine: "Street address needs to start with a D", + city: "City needs to start with a B", + country: "Country needs to start with a C", + organization: "organization needs to start with an A", + phone: "Telephone needs to start with a 9", + postalCode: "Postal code needs to start with a 0", + recipient: "Name needs to start with a Z", + region: "Region needs to start with a Y", + }, + }, + paymentOptions: {}, + }, + savedAddresses: { + [address1.guid]: deepClone(address1), + }, + }; + await form.requestStore.setState(state); + display.appendChild(form); + await asyncElementRendered(); + + ok(!!form.querySelectorAll(":-moz-ui-invalid").length, "Check fields are visibly invalid"); + info("merchant cleared the errors"); + await form.requestStore.setState({ + request: { + paymentDetails: { + shippingAddressErrors: {}, + }, + paymentOptions: {}, + }, + }); + await asyncElementRendered(); + is(form.querySelectorAll(":-moz-ui-invalid").length, 0, + "Check fields are visibly valid - custom validity cleared"); + + form.remove(); +}); + +add_task(async function test_customMerchantValidity_shippingAddressForm() { + let form = new AddressForm(); + form.id = "shipping-address-page"; + form.setAttribute("selected-state-key", "selectedShippingAddress"); + await form.promiseReady; + + // Merchant errors only make sense when editing a record so add one. + let address1 = deepClone(PTU.Addresses.TimBL); + address1.guid = "9864798564"; + + const state = { + page: { + id: "shipping-address-page", + }, + "shipping-address-page": { + guid: address1.guid, + }, + request: { + paymentDetails: { + billingAddressErrors: { + addressLine: "Billing Street address needs to start with a D", + city: "Billing City needs to start with a B", + country: "Billing Country needs to start with a C", + organization: "Billing organization needs to start with an A", + phone: "Billing Telephone needs to start with a 9", + postalCode: "Billing Postal code needs to start with a 0", + recipient: "Billing Name needs to start with a Z", + region: "Billing Region needs to start with a Y", + }, + }, + paymentOptions: {}, + }, + savedAddresses: { + [address1.guid]: deepClone(address1), + }, + }; + await form.requestStore.setState(state); + display.appendChild(form); + await asyncElementRendered(); + + is(form.querySelectorAll(":-moz-ui-invalid").length, 0, + "Check fields are visibly valid - billing errors are not relevant to a shipping address form"); + + // now switch in some shipping address errors + await form.requestStore.setState({ + request: { + paymentDetails: { + shippingAddressErrors: { + addressLine: "Street address needs to start with a D", + city: "City needs to start with a B", + country: "Country needs to start with a C", + organization: "organization needs to start with an A", + phone: "Telephone needs to start with a 9", + postalCode: "Postal code needs to start with a 0", + recipient: "Name needs to start with a Z", + region: "Region needs to start with a Y", + }, + }, + paymentOptions: {}, + }, + }); + await asyncElementRendered(); + + ok(form.querySelectorAll(":-moz-ui-invalid").length >= 8, "Check fields are visibly invalid"); +}); + +add_task(async function test_customMerchantValidity_billingAddressForm() { + let form = new AddressForm(); + form.id = "billing-address-page"; + form.setAttribute("selected-state-key", "basic-card-page|billingAddressGUID"); + await form.promiseReady; + + // Merchant errors only make sense when editing a record so add one. + let address1 = deepClone(PTU.Addresses.TimBL); + address1.guid = "9864798564"; + + const state = { + page: { + id: "billing-address-page", + }, + "billing-address-page": { + guid: address1.guid, + }, + request: { + paymentDetails: { + shippingAddressErrors: { + addressLine: "Street address needs to start with a D", + city: "City needs to start with a B", + country: "Country needs to start with a C", + organization: "organization needs to start with an A", + phone: "Telephone needs to start with a 9", + postalCode: "Postal code needs to start with a 0", + recipient: "Name needs to start with a Z", + region: "Region needs to start with a Y", + }, + }, + paymentOptions: {}, + }, + savedAddresses: { + [address1.guid]: deepClone(address1), + }, + }; + await form.requestStore.setState(state); + display.appendChild(form); + await asyncElementRendered(); + + is(form.querySelectorAll(":-moz-ui-invalid").length, 0, + "Check fields are visibly valid - shipping errors are not relevant to a billing address form"); + + await form.requestStore.setState({ + request: { + paymentDetails: { + paymentMethodErrors: { + billingAddress: { + addressLine: "Billing Street address needs to start with a D", + city: "Billing City needs to start with a B", + country: "Billing Country needs to start with a C", + organization: "Billing organization needs to start with an A", + phone: "Billing Telephone needs to start with a 9", + postalCode: "Billing Postal code needs to start with a 0", + recipient: "Billing Name needs to start with a Z", + region: "Billing Region needs to start with a Y", + }, + }, + }, + paymentOptions: {}, + }, + }); + await asyncElementRendered(); + ok(form.querySelectorAll(":-moz-ui-invalid").length >= 8, + "Check billing fields are visibly invalid"); + + form.remove(); +}); + +add_task(async function test_merchantPayerAddressErrors() { + let form = new AddressForm(); + form.id = "payer-address-page"; + form.setAttribute("selected-state-key", "selectedPayerAddress"); + + await form.promiseReady; + form.form.dataset.extraRequiredFields = "name email tel"; + + // Merchant errors only make sense when editing a record so add one. + let address1 = deepClone(PTU.Addresses.TimBL); + address1.guid = "9864798564"; + + const state = { + page: { + id: "payer-address-page", + }, + "payer-address-page": { + addressFields: "name email tel", + guid: address1.guid, + }, + request: { + paymentDetails: { + payerErrors: { + email: "Email must be @mozilla.org", + name: "Name needs to start with a W", + phone: "Telephone needs to start with a 1", + }, + }, + paymentOptions: {}, + }, + savedAddresses: { + [address1.guid]: deepClone(address1), + }, + }; + await form.requestStore.setState(state); + display.appendChild(form); + await asyncElementRendered(); + + function checkValidationMessage(selector, property) { + is(form.form.querySelector(selector).validationMessage, + state.request.paymentDetails.payerErrors[property], + "Validation message should match for " + selector); + } + + ok(form.saveButton.disabled, "Save button should be disabled due to validation errors"); + + checkValidationMessage("#tel", "phone"); + checkValidationMessage("#family-name", "name"); + checkValidationMessage("#email", "email"); + + is(form.querySelectorAll(":-moz-ui-invalid").length, 3, "Check payer fields are visibly invalid"); + + await form.requestStore.setState({ + request: { + paymentDetails: { + payerErrors: {}, + }, + paymentOptions: {}, + }, + }); + await asyncElementRendered(); + + is(form.querySelectorAll(":-moz-ui-invalid").length, 0, + "Check payer fields are visibly valid after clearing merchant errors"); + + form.remove(); +}); + +add_task(async function test_field_validation() { + let getFormFormatStub = sinon.stub(PaymentDialogUtils, "getFormFormat"); + getFormFormatStub.returns({ + addressLevel1Label: "state", + postalCodeLabel: "US", + fieldsOrder: [ + {fieldId: "name", newLine: true}, + {fieldId: "organization", newLine: true}, + {fieldId: "street-address", newLine: true}, + {fieldId: "address-level2"}, + ], + }); + + let form = new AddressForm(); + form.id = "shipping-address-page"; + form.setAttribute("selected-state-key", "selectedShippingAddress"); + await form.promiseReady; + const state = { + page: { + id: "shipping-address-page", + }, + "shipping-address-page": { + }, + request: { + paymentDetails: { + shippingAddressErrors: {}, + }, + paymentOptions: {}, + }, + }; + await form.requestStore.setState(state); + display.appendChild(form); + await asyncElementRendered(); + + ok(form.saveButton.disabled, "Save button should be disabled due to empty fields"); + + let postalCodeInput = form.form.querySelector("#postal-code"); + let addressLevel1Input = form.form.querySelector("#address-level1"); + ok(!postalCodeInput.value, "postal-code should be empty by default"); + ok(!addressLevel1Input.value, "address-level1 should be empty by default"); + ok(postalCodeInput.checkValidity(), + "postal-code should be valid by default when it is not visible"); + ok(addressLevel1Input.checkValidity(), + "address-level1 should be valid by default when it is not visible"); + + getFormFormatStub.restore(); + form.remove(); +}); + +add_task(async function test_field_validation_dom_popup() { + let form = new AddressForm(); + form.id = "shipping-address-page"; + form.setAttribute("selected-state-key", "selectedShippingAddress"); + await form.promiseReady; + const state = { + page: { + id: "shipping-address-page", + }, + "shipping-address-page": { + }, + }; + + await form.requestStore.setState(state); + display.appendChild(form); + await asyncElementRendered(); + + const BAD_POSTAL_CODE = "hi mom"; + let postalCode = form.querySelector("#postal-code"); + postalCode.focus(); + sendString(BAD_POSTAL_CODE, window); + postalCode.blur(); + let errorTextSpan = postalCode.parentNode.querySelector(".error-text"); + is(errorTextSpan.textContent, "Please match the requested format.", + "DOM validation messages should be reflected in the error-text #1"); + + postalCode.focus(); + while (postalCode.value) { + sendKey("BACK_SPACE", window); + } + postalCode.blur(); + is(errorTextSpan.textContent, "Please fill out this field.", + "DOM validation messages should be reflected in the error-text #2"); + + postalCode.focus(); + sendString("12345", window); + is(errorTextSpan.innerText, "", "DOM validation message should be removed when no error"); + postalCode.blur(); + + form.remove(); +}); + +add_task(async function test_hiddenMailingAddressFieldsCleared() { + let form = new AddressForm(); + form.id = "address-page"; + form.setAttribute("selected-state-key", "selectedShippingAddress"); + form.dataset.updateButtonLabel = "Update"; + await form.promiseReady; + display.appendChild(form); + await asyncElementRendered(); + + let address1 = deepClone(PTU.Addresses.TimBL); + address1.guid = "9864798564"; + + await form.requestStore.setState({ + page: { + id: "address-page", + }, + "address-page": { + guid: address1.guid, + }, + savedAddresses: { + [address1.guid]: deepClone(address1), + }, + }); + await asyncElementRendered(); + + info("Change the country to hide address-level1"); + fillField(form.form.querySelector("#country"), "DE"); + + let expectedRecord = Object.assign({}, address1, { + country: "DE", + // address-level1 & 3 aren't used for Germany so should be blanked. + "address-level1": "", + "address-level3": "", + }); + delete expectedRecord.guid; + // The following were not shown so shouldn't be part of the message: + delete expectedRecord.email; + + let messagePromise = promiseContentToChromeMessage("updateAutofillRecord"); + form.saveButton.scrollIntoView(); + synthesizeMouseAtCenter(form.saveButton, {}); + + info("Waiting for messagePromise"); + let details = await messagePromise; + info("/Waiting for messagePromise"); + delete details.messageID; + is(details.collectionName, "addresses", "Check collectionName"); + isDeeply(details, { + collectionName: "addresses", + guid: address1.guid, + messageType: "updateAutofillRecord", + record: expectedRecord, + }, "Check update event details for the message to chrome"); + + form.remove(); +}); +</script> + +</body> +</html> diff --git a/browser/components/payments/test/mochitest/test_address_option.html b/browser/components/payments/test/mochitest/test_address_option.html new file mode 100644 index 0000000000..670208a775 --- /dev/null +++ b/browser/components/payments/test/mochitest/test_address_option.html @@ -0,0 +1,177 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test the address-option component +--> +<head> + <meta charset="utf-8"> + <title>Test the address-option component</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script 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/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"> + <option id="option1" + data-field-separator=", " + address-level1="MI" + address-level2="Some City" + country="US" + email="foo@bar.com" + name="John Smith" + postal-code="90210" + street-address="123 Sesame Street,
Apt 40" + tel="+1 519 555-5555" + value="option1" + guid="option1"></option> + <option id="option2" + data-field-separator=", " + value="option2" + guid="option2"></option> + + <rich-select id="richSelect1" + option-type="address-option"></rich-select> + </p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="module"> +/** Test the address-option component **/ + +import "../../res/components/address-option.js"; +import "../../res/components/rich-select.js"; + +let option1 = document.getElementById("option1"); +let option2 = document.getElementById("option2"); +let richSelect1 = document.getElementById("richSelect1"); + +add_task(async function test_populated_option_rendering() { + richSelect1.popupBox.appendChild(option1); + richSelect1.value = option1.value; + await asyncElementRendered(); + + let richOption = richSelect1.selectedRichOption; + + is(richOption.name, "John Smith", "Check name getter"); + is(richOption.streetAddress, "123 Sesame Street,\nApt 40", "Check streetAddress getter"); + is(richOption.addressLevel2, "Some City", "Check addressLevel2 getter"); + + ok(!richOption.innerText.includes("undefined"), "Check for presence of 'undefined'"); + ok(!richOption.innerText.includes("null"), "Check for presence of 'null'"); + + ok(!richOption._line1.innerText.trim().endsWith(","), "Line 1 should not end with a comma"); + ok(!richOption._line2.innerText.trim().endsWith(","), "Line 2 should not end with a comma"); + is(richOption._line1.innerText, "John Smith, 123 Sesame Street, Apt 40", "Line 1 text"); + is(richOption._line2.innerText, "Some City, MI, 90210, US", "Line 2 text"); + + // Note that innerText takes visibility into account so that's why it's used over textContent here + is(richOption._name.innerText, "John Smith", "name text"); + is(richOption["_street-address"].innerText, "123 Sesame Street, Apt 40", "street-address text"); + is(richOption["_address-level2"].innerText, "Some City", "address-level2 text"); + + is(richOption._email.parentElement, null, + "Check email field isn't in the document for a mailing-address option"); +}); + +// Same option as the last test but with @break-after-nth-field=1 +add_task(async function test_breakAfterNthField() { + richSelect1.popupBox.appendChild(option1); + richSelect1.value = option1.value; + await asyncElementRendered(); + + let richOption = richSelect1.selectedRichOption; + richOption.breakAfterNthField = 1; + await asyncElementRendered(); + + ok(!richOption.innerText.includes("undefined"), "Check for presence of 'undefined'"); + ok(!richOption.innerText.includes("null"), "Check for presence of 'null'"); + + ok(!richOption._line1.innerText.trim().endsWith(","), "Line 1 should not end with a comma"); + ok(!richOption._line2.innerText.trim().endsWith(","), "Line 2 should not end with a comma"); + is(richOption._line1.innerText, "John Smith", "Line 1 text with breakAfterNthField = 1"); + is(richOption._line2.innerText, "123 Sesame Street, Apt 40, Some City, MI, 90210, US", + "Line 2 text with breakAfterNthField = 1"); +}); + +add_task(async function test_addressField_mailingAddress() { + richSelect1.popupBox.appendChild(option1); + richSelect1.value = option1.value; + await asyncElementRendered(); + + let richOption = richSelect1.selectedRichOption; + richOption.addressFields = "mailing-address"; + await asyncElementRendered(); + is(richOption.getAttribute("address-fields"), "mailing-address", "Check @address-fields"); + + ok(!richOption.innerText.includes("undefined"), "Check for presence of 'undefined'"); + ok(!richOption.innerText.includes("null"), "Check for presence of 'null'"); + + ok(!richOption._line1.innerText.trim().endsWith(","), "Line 1 should not end with a comma"); + ok(!richOption._line2.innerText.trim().endsWith(","), "Line 2 should not end with a comma"); + is(richOption._line1.innerText, "John Smith, 123 Sesame Street, Apt 40", "Line 1 text"); + is(richOption._line2.innerText, "Some City, MI, 90210, US", "Line 2 text"); + + ok(!isHidden(richOption._line2), "Line 2 should be visible when it's used"); + + is(richOption._email.parentElement, null, + "Check email field isn't in the document for a mailing-address option"); +}); + +add_task(async function test_addressField_nameEmail() { + richSelect1.popupBox.appendChild(option1); + richSelect1.value = option1.value; + await asyncElementRendered(); + + let richOption = richSelect1.selectedRichOption; + richOption.addressFields = "name email"; + await asyncElementRendered(); + is(richOption.getAttribute("address-fields"), "name email", "Check @address-fields"); + + ok(!richOption.innerText.includes("undefined"), "Check for presence of 'undefined'"); + ok(!richOption.innerText.includes("null"), "Check for presence of 'null'"); + + ok(!richOption._line1.innerText.trim().endsWith(","), "Line 1 should not end with a comma"); + ok(!richOption._line2.innerText.trim().endsWith(","), "Line 2 should not end with a comma"); + is(richOption._line1.innerText, "John Smith, foo@bar.com", "Line 1 text"); + is(richOption._line2.innerText, "", "Line 2 text"); + + ok(isHidden(richOption._line2), "Line 2 should be hidden when it's not used"); + + isnot(richOption._email.parentElement, null, + "Check email field is in the document for a 'name email' option"); +}); + +add_task(async function test_missing_fields_option_rendering() { + richSelect1.popupBox.appendChild(option2); + richSelect1.value = option2.value; + await asyncElementRendered(); + + let richOption = richSelect1.selectedRichOption; + is(richOption.name, null, "Check name getter"); + is(richOption.streetAddress, null, "Check streetAddress getter"); + is(richOption.addressLevel2, null, "Check addressLevel2 getter"); + + ok(!richOption.innerText.includes("undefined"), "Check for presence of 'undefined'"); + ok(!richOption.innerText.includes("null"), "Check for presence of 'null'"); + + is(richOption._name.innerText, "", "name text"); + is(window.getComputedStyle(richOption._name, "::before").content, "attr(data-missing-string)", + "Check missing field pseudo content"); + is(richOption._name.getAttribute("data-missing-string"), "Name Missing", + "Check @data-missing-string"); + is(richOption._email.parentElement, null, + "Check email field isn't in the document for a mailing-address option"); +}); + +</script> + +</body> +</html> diff --git a/browser/components/payments/test/mochitest/test_address_picker.html b/browser/components/payments/test/mochitest/test_address_picker.html new file mode 100644 index 0000000000..5a3f1b398a --- /dev/null +++ b/browser/components/payments/test/mochitest/test_address_picker.html @@ -0,0 +1,278 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test the address-picker component +--> +<head> + <meta charset="utf-8"> + <title>Test the address-picker component</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script 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"> + <address-picker id="picker1" + data-field-separator=", " + data-invalid-label="Picker1: Missing or Invalid" + selected-state-key="selectedShippingAddress"></address-picker> + </p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="module"> +/** Test the address-picker component **/ + +import "../../res/containers/address-picker.js"; + +let picker1 = document.getElementById("picker1"); + +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.dropdown.popupBox.children.length, 0, "Check dropdown is empty"); +}); + +add_task(async function test_initialSet() { + picker1.requestStore.setState({ + savedAddresses: { + "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, + }, + }, + }); + await asyncElementRendered(); + let options = picker1.dropdown.popupBox.children; + is(options.length, 3, "Check dropdown has all addresses"); + ok(options[0].textContent.includes("Mrs. Bar"), "Check first address based on timeLastUsed"); + ok(options[1].textContent.includes("Mr. Foo"), "Check second address based on timeLastUsed"); + ok(options[2].textContent.includes("Mrs. Fields"), "Check third address based on timeLastUsed"); +}); + +add_task(async function test_update() { + picker1.requestStore.setState({ + savedAddresses: { + "48bnds6854t": { + // Same GUID, different values to trigger an update + "address-level1": "MI-edit", + // address-level2 was cleared which means it's not returned + "country": "CA", + "guid": "48bnds6854t", + "name": "Mr. Foo-edit", + "postal-code": "90210-1234", + "street-address": "new-edit", + "tel": "+1 650 555-5555", + }, + "68gjdh354j": { + "address-level1": "CA", + "address-level2": "Mountain View", + "country": "US", + "guid": "68gjdh354j", + "name": "Mrs. Bar", + "postal-code": "94041", + "street-address": "P.O. Box 123", + "tel": "+1 650 555-5555", + }, + "abcde12345": { + "address-level2": "Mountain View", + "country": "US", + "guid": "abcde12345", + "name": "Mrs. Fields", + }, + }, + }); + await asyncElementRendered(); + let options = picker1.dropdown.popupBox.children; + is(options.length, 3, "Check dropdown still has all addresses"); + ok(options[0].textContent.includes("Mr. Foo-edit"), "Check updated name in first address"); + ok(!options[0].getAttribute("address-level2"), "Check removed first address-level2"); + ok(options[1].textContent.includes("Mrs. Bar"), "Check that name is the same in second address"); + ok(options[1].getAttribute("street-address").includes("P.O. Box 123"), + "Check second address is the same"); + ok(options[2].textContent.includes("Mrs. Fields"), + "Check that name is the same in third address"); + is(options[2].getAttribute("street-address"), null, "Check third address is missing"); +}); + +add_task(async function test_change_selected_address() { + let options = picker1.dropdown.popupBox.children; + is(picker1.dropdown.selectedOption, null, "Should default to no selected option"); + is(picker1.editLink.hidden, true, "Picker edit link should be hidden when no option is selected"); + let {selectedShippingAddress} = picker1.requestStore.getState(); + is(selectedShippingAddress, null, "store should have no option selected"); + ok(!picker1.classList.contains("invalid-selected-option"), "No validation on an empty selection"); + ok(isHidden(picker1.invalidLabel), "The invalid label should be hidden"); + + picker1.dropdown.popupBox.focus(); + synthesizeKey(options[2].getAttribute("name"), {}); + await asyncElementRendered(); + + let selectedOption = picker1.dropdown.selectedOption; + is(selectedOption, options[2], "Selected option should now be the third option"); + selectedShippingAddress = picker1.requestStore.getState().selectedShippingAddress; + is(selectedShippingAddress, selectedOption.getAttribute("guid"), + "store should have third option selected"); + // The third option is missing some fields. Make sure that it is marked as such. + ok(picker1.classList.contains("invalid-selected-option"), "The third option is missing fields"); + ok(!isHidden(picker1.invalidLabel), "The invalid label should be visible"); + is(picker1.invalidLabel.innerText, picker1.dataset.invalidLabel, "Check displayed error text"); + + picker1.dropdown.popupBox.focus(); + synthesizeKey(options[1].getAttribute("name"), {}); + await asyncElementRendered(); + + selectedOption = picker1.dropdown.selectedOption; + is(selectedOption, options[1], "Selected option should now be the second option"); + selectedShippingAddress = picker1.requestStore.getState().selectedShippingAddress; + is(selectedShippingAddress, selectedOption.getAttribute("guid"), + "store should have second option selected"); + ok(!picker1.classList.contains("invalid-selected-option"), "The second option has all fields"); + ok(isHidden(picker1.invalidLabel), "The invalid label should be hidden"); +}); + +add_task(async function test_address_combines_name_street_level2_level1_postalCode_country() { + let options = picker1.dropdown.popupBox.children; + let richoption1 = picker1.dropdown.querySelector(".rich-select-selected-option"); + /* eslint-disable max-len */ + is(richoption1.innerText, + `${options[1].getAttribute("name")}, ${options[1].getAttribute("street-address")} +${options[1].getAttribute("address-level2")}, ${options[1].getAttribute("address-level1")}, ${options[1].getAttribute("postal-code")}, ${options[1].getAttribute("country")}`, + "The address shown should be human readable and include all fields"); + /* eslint-enable max-len */ + + picker1.dropdown.popupBox.focus(); + synthesizeKey(options[2].getAttribute("name"), {}); + await asyncElementRendered(); + + richoption1 = picker1.dropdown.querySelector(".rich-select-selected-option"); + // "Missing …" text is rendered via a pseudo element content and isn't included in innerText + is(richoption1.innerText, "Mrs. Fields, \nMountain View, , US", + "The address shown should be human readable and include all fields"); + + picker1.dropdown.popupBox.focus(); + synthesizeKey(options[1].getAttribute("name"), {}); + await asyncElementRendered(); +}); + +add_task(async function test_delete() { + picker1.requestStore.setState({ + savedAddresses: { + // 48bnds6854t and abcde12345 was deleted + "68gjdh354j": { + "address-level1": "CA", + "address-level2": "Mountain View", + "country": "US", + "guid": "68gjdh354j", + "name": "Mrs. Bar", + "postal-code": "94041", + "street-address": "P.O. Box 123", + "tel": "+1 650 555-5555", + }, + }, + }); + await asyncElementRendered(); + let options = picker1.dropdown.popupBox.children; + is(options.length, 1, "Check dropdown has one remaining address"); + ok(options[0].textContent.includes("Mrs. Bar"), "Check remaining address"); +}); + +add_task(async function test_merchantError() { + picker1.requestStore.setState({ + selectedShippingAddress: "68gjdh354j", + }); + await asyncElementRendered(); + + is(picker1.selectedStateKey, "selectedShippingAddress", "Check selectedStateKey"); + + let state = picker1.requestStore.getState(); + let { + request, + } = state; + ok(!picker1.classList.contains("invalid-selected-option"), "No validation on a valid option"); + ok(isHidden(picker1.invalidLabel), "The invalid label should be hidden"); + + let requestWithShippingAddressErrors = deepClone(request); + Object.assign(requestWithShippingAddressErrors.paymentDetails, { + shippingAddressErrors: { + country: "Your country is not supported", + }, + }); + picker1.requestStore.setState({ + request: requestWithShippingAddressErrors, + }); + await asyncElementRendered(); + + ok(picker1.classList.contains("invalid-selected-option"), "The merchant error applies"); + ok(!isHidden(picker1.invalidLabel), "The merchant error should be visible"); + is(picker1.invalidLabel.innerText, "Your country is not supported", "Check displayed error text"); + + info("update the request to remove the errors"); + picker1.requestStore.setState({ + request, + }); + await asyncElementRendered(); + ok(!picker1.classList.contains("invalid-selected-option"), + "No errors visible when merchant errors cleared"); + ok(isHidden(picker1.invalidLabel), "The invalid label should be hidden"); + + info("Set billing address and payer errors which aren't relevant to this picker"); + let requestWithNonShippingAddressErrors = deepClone(request); + Object.assign(requestWithNonShippingAddressErrors.paymentDetails, { + payerErrors: { + name: "Your name is too short", + }, + paymentMethodErrors: { + billingAddress: { + country: "Your billing country is not supported", + }, + }, + shippingAddressErrors: {}, + }); + picker1.requestStore.setState({ + request: requestWithNonShippingAddressErrors, + }); + await asyncElementRendered(); + ok(!picker1.classList.contains("invalid-selected-option"), "No errors on a shipping picker"); + ok(isHidden(picker1.invalidLabel), "The invalid label should still be hidden"); +}); +</script> + +</body> +</html> diff --git a/browser/components/payments/test/mochitest/test_basic_card_form.html b/browser/components/payments/test/mochitest/test_basic_card_form.html new file mode 100644 index 0000000000..239e3b013d --- /dev/null +++ b/browser/components/payments/test/mochitest/test_basic_card_form.html @@ -0,0 +1,623 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test the basic-card-form element +--> +<head> + <meta charset="utf-8"> + <title>Test the basic-card-form element</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="sinon-7.2.7.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="/tests/SimpleTest/test.css"/> + <link rel="stylesheet" type="text/css" href="../../res/paymentRequest.css"/> + <link rel="stylesheet" type="text/css" href="../../res/components/accepted-cards.css"/> +</head> +<body> + <p id="display" style="height: 100vh; margin: 0;"> + <iframe id="templateFrame" src="paymentRequest.xhtml" width="0" height="0" + sandbox="allow-same-origin" + style="float: left;"></iframe> + </p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="module"> +/** Test the basic-card-form element **/ + +import BasicCardForm from "../../res/containers/basic-card-form.js"; + +let display = document.getElementById("display"); +let supportedNetworks = ["discover", "amex"]; +let paymentMethods = [{ + supportedMethods: "basic-card", + data: { + supportedNetworks, + }, +}]; + +function checkCCForm(customEl, expectedCard) { + const CC_PROPERTY_NAMES = [ + "billingAddressGUID", + "cc-number", + "cc-name", + "cc-exp-month", + "cc-exp-year", + "cc-type", + ]; + for (let propName of CC_PROPERTY_NAMES) { + let expectedVal = expectedCard[propName] || ""; + is(document.getElementById(propName).value, + expectedVal.toString(), + `Check ${propName}`); + } +} + +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); + let displayEl = document.getElementById("display"); + importDialogDependencies(templateFrame, displayEl); +}); + +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; + display.appendChild(form); + await asyncElementRendered(); + is(page.id, "payment-summary", "Check initial page after appending"); + + // :-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(); +}); + +add_task(async function test_backButton() { + let form = new BasicCardForm(); + form.dataset.backButtonLabel = "Back"; + form.dataset.addBasicCardTitle = "Sample page title 2"; + await form.requestStore.setState({ + page: { + id: "basic-card-page", + }, + "basic-card-page": { + selectedStateKey: "selectedPaymentCard", + }, + }); + await form.promiseReady; + display.appendChild(form); + await asyncElementRendered(); + + let stateChangePromise = promiseStateChange(form.requestStore); + is(form.pageTitleHeading.textContent, "Sample page title 2", "Check title"); + is(form.backButton.textContent, "Back", "Check label"); + form.backButton.scrollIntoView(); + synthesizeMouseAtCenter(form.backButton, {}); + + let {page} = await stateChangePromise; + is(page.id, "payment-summary", "Check initial page after appending"); + + form.remove(); +}); + +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 = 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), + [address2.guid]: deepClone(address2), + }, + }); + + await asyncElementRendered(); + + // when merchant provides supportedNetworks, the accepted card list should be visible + ok(!form.acceptedCardsList.hidden, "Accepted card list should be visible when adding a card"); + + ok(form.saveButton.disabled, "Save button should initially be disabled"); + fillField(form.form.querySelector("#cc-number"), "4111 1111-1111 1111"); + form.form.querySelector("#cc-name").focus(); + // Check .disabled after .focus() so that it's after both "input" and "change" events. + ok(form.saveButton.disabled, "Save button should still be disabled without a name"); + sendString("J. Smith"); + fillField(form.form.querySelector("#cc-exp-month"), "11"); + let year = (new Date()).getFullYear().toString(); + fillField(form.form.querySelector("#cc-exp-year"), year); + fillField(form.form.querySelector("#cc-type"), "visa"); + fillField(form.form.querySelector("csc-input input"), "123"); + isnot(form.form.querySelector("#billingAddressGUID").value, address2.guid, + "Check initial billing address"); + fillField(form.form.querySelector("#billingAddressGUID"), address2.guid); + is(form.form.querySelector("#billingAddressGUID").value, address2.guid, + "Check selected billing address"); + form.saveButton.focus(); + ok(!form.saveButton.disabled, + "Save button should be enabled since the required fields are filled"); + + fillField(form.form.querySelector("#cc-exp-month"), ""); + fillField(form.form.querySelector("#cc-exp-year"), ""); + form.saveButton.focus(); + ok(form.saveButton.disabled, + "Save button should be disabled since the required fields are empty"); + fillField(form.form.querySelector("#cc-exp-month"), "11"); + fillField(form.form.querySelector("#cc-exp-year"), year); + form.saveButton.focus(); + ok(!form.saveButton.disabled, + "Save button should be enabled since the required fields are filled again"); + + info("blanking the cc-number field"); + fillField(form.form.querySelector("#cc-number"), ""); + ok(form.saveButton.disabled, "Save button is disabled after blanking cc-number"); + form.form.querySelector("#cc-number").blur(); + let fieldsVisiblyInvalid = form.querySelectorAll(":-moz-ui-invalid"); + is(fieldsVisiblyInvalid.length, 1, "Check 1 field visibly invalid after blanking and blur"); + is(fieldsVisiblyInvalid[0].id, "cc-number", "Check #cc-number is visibly invalid"); + + fillField(form.form.querySelector("#cc-number"), "4111 1111-1111 1111"); + is(form.querySelectorAll(":-moz-ui-invalid").length, 0, "Check no fields visibly invalid"); + ok(!form.saveButton.disabled, "Save button is enabled after re-filling cc-number"); + + let messagePromise = promiseContentToChromeMessage("updateAutofillRecord"); + is(form.saveButton.textContent, "Next", "Check label"); + form.saveButton.scrollIntoView(); + synthesizeMouseAtCenter(form.saveButton, {}); + + let details = await messagePromise; + ok(typeof(details.messageID) == "number" && details.messageID > 0, "Check messageID type"); + delete details.messageID; + is(details.collectionName, "creditCards", "Check collectionName"); + isDeeply(details, { + collectionName: "creditCards", + guid: undefined, + messageType: "updateAutofillRecord", + record: { + "cc-exp-month": "11", + "cc-exp-year": year, + "cc-name": "J. Smith", + "cc-number": "4111 1111-1111 1111", + "cc-type": "visa", + "billingAddressGUID": address2.guid, + "isTemporary": true, + }, + }, "Check event details for the message to chrome"); + form.remove(); +}); + +add_task(async function test_requiredAttributePropagated() { + let form = new BasicCardForm(); + await form.promiseReady; + display.appendChild(form); + await asyncElementRendered(); + + let requiredElements = [...form.form.elements].filter(e => e.required && !e.disabled); + is(requiredElements.length, 7, "Number of required elements"); + for (let element of requiredElements) { + if (element.id == "billingAddressGUID") { + // The billing address has a different layout. + continue; + } + let container = element.closest("label") || element.closest("div"); + ok(container.hasAttribute("required"), + `Container ${container.id} should also be marked as required`); + } + // Now test that toggling the `required` attribute will affect the container. + let sampleRequiredElement = requiredElements[0]; + let sampleRequiredContainer = sampleRequiredElement.closest("label") || + sampleRequiredElement.closest("div"); + sampleRequiredElement.removeAttribute("required"); + await form.requestStore.setState({}); + await asyncElementRendered(); + ok(!sampleRequiredElement.hasAttribute("required"), + `"required" attribute should still be removed from element (${sampleRequiredElement.id})`); + ok(!sampleRequiredContainer.hasAttribute("required"), + `"required" attribute should be removed from container`); + sampleRequiredElement.setAttribute("required", "true"); + await form.requestStore.setState({}); + await asyncElementRendered(); + ok(sampleRequiredContainer.hasAttribute("required"), + "`required` attribute is re-added to container"); + + form.remove(); +}); + +add_task(async function test_genericError() { + let form = new BasicCardForm(); + await form.requestStore.setState({ + page: { + id: "test-page", + error: "Generic Error", + }, + }); + await form.promiseReady; + display.appendChild(form); + await asyncElementRendered(); + + ok(!isHidden(form.genericErrorText), "Error message should be visible"); + is(form.genericErrorText.textContent, "Generic Error", "Check error message"); + form.remove(); +}); + +add_task(async function test_add_selectedShippingAddress() { + let form = new BasicCardForm(); + await form.promiseReady; + display.appendChild(form); + await asyncElementRendered(); + + info("have an existing card in storage"); + let card1 = deepClone(PTU.BasicCards.JohnDoe); + card1.guid = "9864798564"; + card1["cc-exp-year"] = 2011; + + let address1 = createAddressRecord(PTU.Addresses.TimBL, { guid: "TimBLGUID" }); + let address2 = createAddressRecord(PTU.Addresses.TimBL2, { guid: "TimBL2GUID" }); + + await form.requestStore.setState({ + page: { + id: "basic-card-page", + }, + savedAddresses: { + [address1.guid]: deepClone(address1), + [address2.guid]: deepClone(address2), + }, + savedBasicCards: { + [card1.guid]: deepClone(card1), + }, + selectedShippingAddress: address2.guid, + }); + await asyncElementRendered(); + checkCCForm(form, { + billingAddressGUID: address2.guid, + }); + + form.remove(); + await form.requestStore.reset(); +}); + +add_task(async function test_add_noSelectedShippingAddress() { + let form = new BasicCardForm(); + await form.promiseReady; + display.appendChild(form); + await asyncElementRendered(); + + info("have an existing card in storage but unused"); + let card1 = deepClone(PTU.BasicCards.JohnDoe); + card1.guid = "9864798564"; + card1["cc-exp-year"] = 2011; + + let address1 = createAddressRecord(PTU.Addresses.TimBL, { guid: "TimBLGUID" }); + + await form.requestStore.setState({ + page: { + id: "basic-card-page", + }, + savedAddresses: { + [address1.guid]: deepClone(address1), + }, + savedBasicCards: { + [card1.guid]: deepClone(card1), + }, + selectedShippingAddress: null, + }); + await asyncElementRendered(); + checkCCForm(form, { + billingAddressGUID: address1.guid, + }); + + info("now test with a missing selectedShippingAddress"); + await form.requestStore.setState({ + selectedShippingAddress: "some-missing-guid", + }); + await asyncElementRendered(); + checkCCForm(form, { + billingAddressGUID: address1.guid, + }); + + form.remove(); + await form.requestStore.reset(); +}); + +add_task(async function test_edit() { + let form = new BasicCardForm(); + form.dataset.updateButtonLabel = "Update"; + await form.promiseReady; + display.appendChild(form); + await asyncElementRendered(); + + let address1 = createAddressRecord(PTU.Addresses.TimBL, { guid: "TimBLGUID" }); + + info("test year before current"); + let card1 = deepClone(PTU.BasicCards.JohnDoe); + card1.guid = "9864798564"; + card1["cc-exp-year"] = 2011; + card1.billingAddressGUID = address1.guid; + + await form.requestStore.setState({ + request: { + paymentMethods, + paymentDetails: {}, + }, + page: { + id: "basic-card-page", + }, + "basic-card-page": { + guid: card1.guid, + selectedStateKey: "selectedPaymentCard", + }, + savedAddresses: { + [address1.guid]: deepClone(address1), + }, + savedBasicCards: { + [card1.guid]: deepClone(card1), + }, + }); + await asyncElementRendered(); + 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"); + + let requiredElements = [...form.form.elements].filter(e => e.required && !e.disabled); + ok(requiredElements.length, "There should be at least one required element"); + is(requiredElements.length, 5, "Number of required elements"); + for (let element of requiredElements) { + if (element.id == "billingAddressGUID") { + // The billing address has a different layout. + continue; + } + + let container = element.closest("label") || element.closest("div"); + ok(element.hasAttribute("required"), "Element should be marked as required"); + ok(container.hasAttribute("required"), "Container should also be marked as required"); + } + + info("test future year"); + card1["cc-exp-year"] = 2100; + + await form.requestStore.setState({ + savedBasicCards: { + [card1.guid]: deepClone(card1), + }, + }); + await asyncElementRendered(); + checkCCForm(form, card1); + + info("test change to minimal record"); + let minimalCard = { + // no expiration date or name + "cc-number": "1234567690123", + guid: "9gnjdhen46", + }; + await form.requestStore.setState({ + page: { + id: "basic-card-page", + }, + "basic-card-page": { + guid: minimalCard.guid, + selectedStateKey: "selectedPaymentCard", + }, + savedBasicCards: { + [minimalCard.guid]: deepClone(minimalCard), + }, + }); + await asyncElementRendered(); + ok(!!form.querySelectorAll(":-moz-ui-invalid").length, + "Check fields are visibly invalid on an 'edit' form with missing fields"); + checkCCForm(form, minimalCard); + + info("change to no selected card"); + await form.requestStore.setState({ + page: { + id: "basic-card-page", + }, + "basic-card-page": { + guid: null, + selectedStateKey: "selectedPaymentCard", + }, + }); + await asyncElementRendered(); + is(form.querySelectorAll(":-moz-ui-invalid").length, 0, + "Check no fields are visibly invalid after converting to an 'add' form"); + checkCCForm(form, { + billingAddressGUID: address1.guid, // Default selected + }); + + form.remove(); +}); + +add_task(async function test_field_validity_updates() { + let form = new BasicCardForm(); + 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"); + let nameInput = form.form.querySelector("#cc-name"); + let typeInput = form.form.querySelector("#cc-type"); + 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"); + ok(ccNumber.checkValidity(), "cc-number field is valid with good input"); + ok(!nameInput.checkValidity(), "cc-name field is invalid when empty"); + ok(form.saveButton.disabled, "Save button should be disabled with incomplete input"); + + info("correct by adding cc-name and expiration values"); + fillField(nameInput, "First"); + fillField(monthInput, "11"); + let year = (new Date()).getFullYear().toString(); + fillField(yearInput, year); + fillField(typeInput, "visa"); + fillField(cscInput, "456"); + ok(ccNumber.checkValidity(), "cc-number field is valid with good input"); + ok(nameInput.checkValidity(), "cc-name 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(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"); + ccNumber.focus(); + sendString("aa"); + nameInput.focus(); + sendString("Surname"); + + ok(!ccNumber.checkValidity(), "cc-number field becomes invalid with bad input"); + ok(form.querySelector("#cc-number:-moz-ui-invalid"), "cc-number field is visibly invalid"); + ok(nameInput.checkValidity(), "cc-name field is valid with a value"); + ok(form.saveButton.disabled, "Save button becomes disabled with bad input"); + + info("fix the cc-number to make it all valid again"); + ccNumber.focus(); + sendKey("BACK_SPACE"); + sendKey("BACK_SPACE"); + info("after backspaces, ccNumber.value: " + ccNumber.value); + + ok(ccNumber.checkValidity(), "cc-number field becomes valid with corrected input"); + ok(nameInput.checkValidity(), "cc-name field is valid with a value"); + ok(!form.saveButton.disabled, "Save button is no longer disabled with corrected input"); + + form.remove(); +}); + +add_task(async function test_numberCustomValidityReset() { + let form = new BasicCardForm(); + 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"); + sendKey("TAB"); + ok(form.querySelector("#cc-number:-moz-ui-invalid"), "cc-number field is visibly invalid"); + + info("simulate triggering an add again to reset the form"); + await form.requestStore.setState({ + page: { + id: "basic-card-page", + }, + "basic-card-page": { + selectedStateKey: "selectedPaymentCard", + }, + }); + + ok(!form.querySelector("#cc-number:-moz-ui-invalid"), "cc-number field is not visibly invalid"); + + form.remove(); +}); + +add_task(async function test_noCardNetworkSelected() { + let form = new BasicCardForm(); + await form.promiseReady; + display.appendChild(form); + await asyncElementRendered(); + + info("have an existing card in storage, with no network id"); + let card1 = deepClone(PTU.BasicCards.JohnDoe); + card1.guid = "9864798564"; + delete card1["cc-type"]; + + await form.requestStore.setState({ + page: { + id: "basic-card-page", + }, + "basic-card-page": { + guid: card1.guid, + selectedStateKey: "selectedPaymentCard", + }, + savedBasicCards: { + [card1.guid]: deepClone(card1), + }, + }); + await asyncElementRendered(); + checkCCForm(form, card1); + is(document.getElementById("cc-type").selectedIndex, 0, "Initial empty option is selected"); + + form.remove(); + await form.requestStore.reset(); +}); + +</script> + +</body> +</html> diff --git a/browser/components/payments/test/mochitest/test_basic_card_option.html b/browser/components/payments/test/mochitest/test_basic_card_option.html new file mode 100644 index 0000000000..71a0199fec --- /dev/null +++ b/browser/components/payments/test/mochitest/test_basic_card_option.html @@ -0,0 +1,96 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test the basic-card-option component +--> +<head> + <meta charset="utf-8"> + <title>Test the basic-card-option component</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="payments_common.js"></script> + <script src="../../res/unprivileged-fallbacks.js"></script> + + <link rel="stylesheet" type="text/css" href="../../res/components/rich-select.css"/> + <link rel="stylesheet" type="text/css" href="../../res/components/basic-card-option.css"/> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <p id="display"> + <option id="option1" + value="option1" + cc-exp="2024-06" + cc-name="John Smith" + cc-number="************5461" + cc-type="visa" + guid="option1"></option> + <option id="option2" + value="option2" + cc-number="************1111" + guid="option2"></option> + + <rich-select id="richSelect1" + option-type="basic-card-option"></rich-select> + </p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="module"> +/** Test the basic-card-option component **/ + +import "../../res/components/basic-card-option.js"; +import "../../res/components/rich-select.js"; + +let option1 = document.getElementById("option1"); +let option2 = document.getElementById("option2"); +let richSelect1 = document.getElementById("richSelect1"); + +add_task(async function test_populated_option_rendering() { + richSelect1.popupBox.appendChild(option1); + richSelect1.value = option1.value; + await asyncElementRendered(); + + let richOption = richSelect1.selectedRichOption; + is(richOption.ccExp, "2024-06", "Check ccExp getter"); + is(richOption.ccName, "John Smith", "Check ccName getter"); + is(richOption.ccNumber, "************5461", "Check ccNumber getter"); + is(richOption.ccType, "visa", "Check ccType getter"); + + ok(!richOption.innerText.includes("undefined"), "Check for presence of 'undefined'"); + ok(!richOption.innerText.includes("null"), "Check for presence of 'null'"); + + // Note that innerText takes visibility into account so that's why it's used over textContent here + is(richOption["_cc-exp"].innerText, "Exp. 2024-06", "cc-exp text"); + is(richOption["_cc-name"].innerText, "John Smith", "cc-name text"); + is(richOption["_cc-number"].innerText, "****5461", "cc-number text"); + is(richOption["_cc-type"].localName, "img", "cc-type localName"); + is(richOption["_cc-type"].alt, "visa", "cc-type img alt"); +}); + +add_task(async function test_minimal_option_rendering() { + richSelect1.popupBox.appendChild(option2); + richSelect1.value = option2.value; + await asyncElementRendered(); + + let richOption = richSelect1.selectedRichOption; + is(richOption.ccExp, null, "Check ccExp getter"); + is(richOption.ccName, null, "Check ccName getter"); + is(richOption.ccNumber, "************1111", "Check ccNumber getter"); + is(richOption.ccType, null, "Check ccType getter"); + + ok(!richOption.innerText.includes("undefined"), "Check for presence of 'undefined'"); + ok(!richOption.innerText.includes("null"), "Check for presence of 'null'"); + + is(richOption["_cc-exp"].innerText, "", "cc-exp text"); + is(richOption["_cc-name"].innerText, "", "cc-name text"); + is(richOption["_cc-number"].innerText, "****1111", "cc-number text"); + is(richOption["_cc-type"].localName, "img", "cc-type localName"); + is(richOption["_cc-type"].alt, "", "cc-type img alt"); +}); + +</script> + +</body> +</html> 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 0000000000..0039c97e55 --- /dev/null +++ b/browser/components/payments/test/mochitest/test_billing_address_picker.html @@ -0,0 +1,132 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test the address-picker component +--> +<head> + <meta charset="utf-8"> + <title>Test the billing-address-picker component</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script 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> diff --git a/browser/components/payments/test/mochitest/test_completion_error_page.html b/browser/components/payments/test/mochitest/test_completion_error_page.html new file mode 100644 index 0000000000..cb8e809758 --- /dev/null +++ b/browser/components/payments/test/mochitest/test_completion_error_page.html @@ -0,0 +1,88 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test the completion-error-page component +--> +<head> + <meta charset="utf-8"> + <title>Test the completion-error-page component</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="payments_common.js"></script> + <script src="../../res/unprivileged-fallbacks.js"></script> + + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <p id="display"> + <completion-error-page id="completion-timeout-error" class="illustrated" + data-page-title="Sample Title" + data-suggestion-heading="Sample suggestion heading" + data-suggestion-1="Sample suggestion" + data-suggestion-2="Sample suggestion" + data-suggestion-3="Sample suggestion" + data-branding-label="Sample Brand" + data-done-button-label="OK"></completion-error-page> + </p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="module"> +/** Test the completion-error-page component **/ + +import "../../res/containers/completion-error-page.js"; + +let page = document.getElementById("completion-timeout-error"); + +add_task(async function test_no_values() { + ok(page, "page exists"); + is(page.dataset.pageTitle, "Sample Title", "Title set on page"); + is(page.dataset.suggestionHeading, "Sample suggestion heading", + "Suggestion heading set on page"); + is(page.dataset["suggestion-1"], "Sample suggestion", + "Suggestion 1 set on page"); + is(page.dataset["suggestion-2"], "Sample suggestion", + "Suggestion 2 set on page"); + is(page.dataset["suggestion-3"], "Sample suggestion", + "Suggestion 3 set on page"); + is(page.dataset.brandingLabel, "Sample Brand", "Branding string set"); + + page.dataset.pageTitle = "Oh noes! **host-name** is having an issue"; + page.dataset["suggestion-2"] = "You should probably blame **host-name**, not us"; + const displayHost = "allizom.com"; + let request = { topLevelPrincipal: { URI: { displayHost } } }; + await page.requestStore.setState({ + changesPrevented: false, + request: Object.assign({}, request, {completeStatus: ""}), + orderDetailsShowing: false, + page: { + id: "completion-timeout-error", + }, + }); + await asyncElementRendered(); + + is(page.requestStore.getState().request.topLevelPrincipal.URI.displayHost, displayHost, + "State should have the displayHost set properly"); + is(page.querySelector("h2").textContent, + `Oh noes! ${displayHost} is having an issue`, + "Title includes host-name"); + is(page.querySelector("p").textContent, + "Sample suggestion heading", + "Suggestion heading set on page"); + is(page.querySelector("li:nth-child(1)").textContent, "Sample suggestion", + "Suggestion 1 set on page"); + is(page.querySelector("li:nth-child(2)").textContent, + `You should probably blame ${displayHost}, not us`, + "Suggestion 2 includes host-name"); + is(page.querySelector(".branding").textContent, + "Sample Brand", + "Branding set on page"); + is(page.querySelector(".primary").textContent, + "OK", + "Primary button label set correctly"); +}); +</script> + +</body> +</html> diff --git a/browser/components/payments/test/mochitest/test_currency_amount.html b/browser/components/payments/test/mochitest/test_currency_amount.html new file mode 100644 index 0000000000..dc1cbac8f2 --- /dev/null +++ b/browser/components/payments/test/mochitest/test_currency_amount.html @@ -0,0 +1,160 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test the currency-amount component +--> +<head> + <meta charset="utf-8"> + <title>Test the currency-amount component</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="payments_common.js"></script> + + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <p id="display"> + <currency-amount id="amount1"></currency-amount> + </p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="module"> +/** Test the currency-amount component **/ + +import "../../res/components/currency-amount.js"; + +let amount1 = document.getElementById("amount1"); + +add_task(async function test_no_value() { + ok(amount1, "amount1 exists"); + is(amount1.textContent, "", "Initially empty"); + + amount1.currency = "USD"; + await asyncElementRendered(); + is(amount1.getAttribute("currency"), "USD", "Check @currency"); + ok(!amount1.hasAttribute("value"), "Check @value"); + is(amount1.currency, "USD", "Check .currency"); + is(amount1.value, null, "Check .value"); + is(amount1.textContent, "", "Empty while missing an amount"); + + amount1.currency = null; + await asyncElementRendered(); + ok(!amount1.hasAttribute("currency"), "Setting to null should remove @currency"); + ok(!amount1.hasAttribute("value"), "Check @value"); + is(amount1.currency, null, "Check .currency"); + is(amount1.value, null, "Check .value"); +}); + +add_task(async function test_no_value() { + amount1.value = 1.23; + await asyncElementRendered(); + + is(amount1.getAttribute("value"), "1.23", "Check @value"); + ok(!amount1.hasAttribute("currency"), "Check @currency"); + is(amount1.currency, null, "Check .currency"); + is(amount1.value, "1.23", "Check .value"); + is(amount1.textContent, "", "Empty while missing a currency"); + + amount1.value = null; + await asyncElementRendered(); + ok(!amount1.hasAttribute("value"), "Setting to null should remove @value"); + is(amount1.currency, null, "Check .currency"); + is(amount1.value, null, "Check .value"); +}); + +add_task(async function test_valid_currency_amount_cad() { + amount1.value = 12.34; + info("waiting to set second property"); + await asyncElementRendered(); + amount1.currency = "CAD"; + await asyncElementRendered(); + + is(amount1.getAttribute("value"), "12.34", "Check @value"); + is(amount1.value, "12.34", "Check .value"); + is(amount1.getAttribute("currency"), "CAD", "Check @currency"); + is(amount1.currency, "CAD", "Check .currency"); + is(amount1.textContent, "CA$12.34", "Check output format"); +}); + +add_task(async function test_valid_currency_amount_displayCode() { + amount1.value = 12.34; + info("showing the currency code"); + await asyncElementRendered(); + amount1.currency = "CAD"; + await asyncElementRendered(); + amount1.displayCode = true; + await asyncElementRendered(); + + is(amount1.getAttribute("value"), "12.34", "Check @value"); + is(amount1.value, "12.34", "Check .value"); + is(amount1.getAttribute("currency"), "CAD", "Check @currency"); + is(amount1.currency, "CAD", "Check .currency"); + is(amount1.textContent, "CA$12.34 CAD", "Check output format"); + + amount1.displayCode = false; + await asyncElementRendered(); +}); + + +add_task(async function test_valid_currency_amount_eur_batched_prop() { + info("setting two properties in a row synchronously"); + amount1.value = 98.76; + amount1.currency = "EUR"; + await asyncElementRendered(); + + is(amount1.getAttribute("value"), "98.76", "Check @value"); + is(amount1.value, "98.76", "Check .value"); + is(amount1.getAttribute("currency"), "EUR", "Check @currency"); + is(amount1.currency, "EUR", "Check .currency"); + is(amount1.textContent, "€98.76", "Check output format"); +}); + +add_task(async function test_valid_currency_amount_eur_batched_attr() { + info("setting two attributes in a row synchronously"); + amount1.setAttribute("value", 11.88); + amount1.setAttribute("currency", "CAD"); + await asyncElementRendered(); + + is(amount1.getAttribute("value"), "11.88", "Check @value"); + is(amount1.value, "11.88", "Check .value"); + is(amount1.getAttribute("currency"), "CAD", "Check @currency"); + is(amount1.currency, "CAD", "Check .currency"); + is(amount1.textContent, "CA$11.88", "Check output format"); +}); + +add_task(async function test_invalid_currency() { + isnot(amount1.textContent, "", "Start with initial content"); + amount1.value = 33.33; + amount1.currency = "__invalid__"; + await asyncElementRendered(); + + is(amount1.getAttribute("value"), "33.33", "Check @value"); + is(amount1.value, "33.33", "Check .value"); + is(amount1.getAttribute("currency"), "__invalid__", "Check @currency"); + is(amount1.currency, "__invalid__", "Check .currency"); + is(amount1.textContent, "", "Invalid currency should clear output"); +}); + +add_task(async function test_invalid_value() { + info("setting some initial values"); + amount1.value = 4.56; + amount1.currency = "GBP"; + await asyncElementRendered(); + isnot(amount1.textContent, "", "Start with initial content"); + + info("setting an alphabetical invalid value"); + amount1.value = "abcdef"; + await asyncElementRendered(); + + is(amount1.getAttribute("value"), "abcdef", "Check @value"); + is(amount1.value, "abcdef", "Check .value"); + is(amount1.getAttribute("currency"), "GBP", "Check @currency"); + is(amount1.currency, "GBP", "Check .currency"); + is(amount1.textContent, "", "Invalid value should clear output"); +}); +</script> + +</body> +</html> diff --git a/browser/components/payments/test/mochitest/test_labelled_checkbox.html b/browser/components/payments/test/mochitest/test_labelled_checkbox.html new file mode 100644 index 0000000000..2d6b9be98b --- /dev/null +++ b/browser/components/payments/test/mochitest/test_labelled_checkbox.html @@ -0,0 +1,71 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test the labelled-checkbox component +--> +<head> + <meta charset="utf-8"> + <title>Test the labelled-checkbox component</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="payments_common.js"></script> + + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <p id="display"> + <labelled-checkbox id="box0"></labelled-checkbox> + <labelled-checkbox id="box1" label="the label" value="the value"></labelled-checkbox> + </p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="module"> +/** Test the labelled-checkbox component **/ + +import "../../res/components/labelled-checkbox.js"; + +let box0 = document.getElementById("box0"); +let box1 = document.getElementById("box1"); + +add_task(async function test_no_values() { + ok(box0, "box0 exists"); + is(box0.label, null, "Initially un-labelled"); + is(box0.value, null, "Check .value"); + ok(!box0.checked, "Initially is not checked"); + ok(!box0.querySelector("input:checked"), "has no checked inner input"); + + box0.checked = true; + box0.value = "New value"; + box0.label = "New label"; + + await asyncElementRendered(); + + ok(box0.checked, "Becomes checked"); + ok(box0.querySelector("input:checked"), "has a checked inner input"); + is(box0.getAttribute("label"), "New label", "Assigned label"); + is(box0.getAttribute("value"), "New value", "Assigned value"); +}); + +add_task(async function test_initial_values() { + is(box1.label, "the label", "Initial label"); + is(box1.value, "the value", "Initial value"); + ok(!box1.checked, "Initially unchecked"); + ok(!box1.querySelector("input:checked"), "has no checked inner input"); + + box1.checked = false; + box1.value = "New value"; + box1.label = "New label"; + + await asyncElementRendered(); + + ok(!box1.checked, "Checked property remains falsey"); + is(box1.getAttribute("value"), "New value", "Assigned value"); + is(box1.getAttribute("label"), "New label", "Assigned label"); +}); + +</script> + +</body> +</html> diff --git a/browser/components/payments/test/mochitest/test_order_details.html b/browser/components/payments/test/mochitest/test_order_details.html new file mode 100644 index 0000000000..c97311d299 --- /dev/null +++ b/browser/components/payments/test/mochitest/test_order_details.html @@ -0,0 +1,215 @@ +<!DOCTYPE HTML> +<html> +<!-- + Test the order-details component +--> +<head> + <meta charset="utf-8"> + <title>Test the order-details component</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="payments_common.js"></script> + <script src="../../res/unprivileged-fallbacks.js"></script> + + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <link rel="stylesheet" type="text/css" href="../../res/containers/order-details.css"/> + + <template id="order-details-template"> + <ul class="main-list"></ul> + <ul class="footer-items-list"></ul> + + <div class="details-total"> + <h2 class="label">Total</h2> + <currency-amount></currency-amount> + </div> + </template> +</head> +<body> + <p id="display"> + <order-details></order-details> + </p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="module"> +/** Test the order-details component **/ + +import OrderDetails from "../../res/containers/order-details.js"; +import {requestStore} from "../../res/mixins/PaymentStateSubscriberMixin.js"; + +let orderDetails = document.querySelector("order-details"); +let emptyState = requestStore.getState(); + +function setup() { + let initialState = deepClone(emptyState); + let cardGUID = "john-doe"; + let johnDoeCard = deepClone(PTU.BasicCards.JohnDoe); + johnDoeCard.methodName = "basic-card"; + johnDoeCard.guid = cardGUID; + let savedBasicCards = { + [cardGUID]: johnDoeCard, + }; + initialState.selectedPaymentCard = cardGUID; + requestStore.setState(Object.assign(initialState, {savedBasicCards})); +} + +add_task(async function isFooterItem() { + ok(OrderDetails.isFooterItem({ + label: "Levy", + type: "tax", + amount: { currency: "USD", value: "1" }, + }, "items with type of 'tax' are footer items")); + ok(!OrderDetails.isFooterItem({ + label: "Levis", + amount: { currency: "USD", value: "1" }, + }, "items without type of 'tax' aren't footer items")); +}); + +add_task(async function test_initial_state() { + setup(); + is(orderDetails.mainItemsList.childElementCount, 0, "main items list is initially empty"); + is(orderDetails.footerItemsList.childElementCount, 0, "footer items list is initially empty"); + is(orderDetails.totalAmountElem.value, "0", "total amount is 0"); +}); + +add_task(async function test_list_population() { + setup(); + let state = requestStore.getState(); + let request = state.request; + let paymentDetails = deepClone(request.paymentDetails); + paymentDetails.displayItems = [ + { + label: "One", + amount: { currency: "USD", value: "5" }, + }, + { + label: "Two", + amount: { currency: "USD", value: "6" }, + }, + { + label: "Three", + amount: { currency: "USD", value: "7" }, + }, + ]; + + requestStore.setState({ + request: Object.assign(deepClone(request), { paymentDetails }), + }); + + await asyncElementRendered(); + is(orderDetails.mainItemsList.childElementCount, 3, "main items list has correct # children"); + is(orderDetails.footerItemsList.childElementCount, 0, "footer items list has 0 children"); + + paymentDetails.displayItems = [ + { + label: "Levy", + type: "tax", + amount: { currency: "USD", value: "1" }, + }, + { + label: "Item", + amount: { currency: "USD", value: "6" }, + }, + { + label: "Thing", + amount: { currency: "USD", value: "7" }, + }, + ]; + Object.assign(request, { paymentDetails }); + requestStore.setState({ request }); + await asyncElementRendered(); + + is(orderDetails.mainItemsList.childElementCount, 2, "main list has correct # children"); + is(orderDetails.footerItemsList.childElementCount, 1, "footer list has correct # children"); +}); + +add_task(async function test_additionalDisplayItems() { + setup(); + let request = Object.assign({}, requestStore.getState().request); + request.paymentDetails = Object.assign({}, request.paymentDetails, { + modifiers: [{ + additionalDisplayItems: [ + { + label: "Card fee", + amount: { currency: "USD", value: "1.50" }, + }, + ], + supportedMethods: "basic-card", + total: { + label: "Total due", + amount: { currency: "USD", value: "3.50" }, + }, + }], + }); + requestStore.setState({ request }); + await asyncElementRendered(); + + is(orderDetails.mainItemsList.childElementCount, 0, + "main list added 0 children from additionalDisplayItems"); + is(orderDetails.footerItemsList.childElementCount, 1, + "footer list added children from additionalDisplayItems"); +}); + + +add_task(async function test_total() { + setup(); + let request = Object.assign({}, requestStore.getState().request); + request.paymentDetails = Object.assign({}, request.paymentDetails, { + totalItem: { label: "foo", amount: { currency: "JPY", value: "5" }}, + }); + requestStore.setState({ request }); + await asyncElementRendered(); + + is(orderDetails.totalAmountElem.value, "5", "total amount gets updated"); + is(orderDetails.totalAmountElem.currency, "JPY", "total currency gets updated"); +}); + +add_task(async function test_modified_total() { + setup(); + let request = Object.assign({}, requestStore.getState().request); + request.paymentDetails = Object.assign({}, request.paymentDetails, { + totalItem: { label: "foo", amount: { currency: "JPY", value: "5" }}, + modifiers: [{ + supportedMethods: "basic-card", + total: { + label: "Total due", + amount: { currency: "USD", value: "3.5" }, + }, + }], + }); + requestStore.setState({request}); + await asyncElementRendered(); + + is(orderDetails.totalAmountElem.value, "3.5", "total amount uses modifier total"); + is(orderDetails.totalAmountElem.currency, "USD", "total currency uses modifier currency"); +}); + +// The modifier is not applied since the cc network is not supported. +add_task(async function test_non_supported_network() { + setup(); + let request = Object.assign({}, requestStore.getState().request); + request.paymentDetails = Object.assign({}, request.paymentDetails, { + totalItem: { label: "foo", amount: { currency: "JPY", value: "5" }}, + modifiers: [{ + supportedMethods: "basic-card", + total: { + label: "Total due", + amount: { currency: "USD", value: "3.5" }, + }, + data: { + supportedNetworks: ["mastercard"], + }, + }], + }); + requestStore.setState({request}); + await asyncElementRendered(); + + is(orderDetails.totalAmountElem.value, "5", "total amount uses modifier total"); + is(orderDetails.totalAmountElem.currency, "JPY", "total currency uses modifier currency"); +}); + +</script> + +</body> +</html> diff --git a/browser/components/payments/test/mochitest/test_payer_address_picker.html b/browser/components/payments/test/mochitest/test_payer_address_picker.html new file mode 100644 index 0000000000..df14857e69 --- /dev/null +++ b/browser/components/payments/test/mochitest/test_payer_address_picker.html @@ -0,0 +1,323 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test the paymentOptions address-picker +--> +<head> + <meta charset="utf-8"> + <title>Test the paymentOptions address-picker</title> + <script src="/tests/SimpleTest/SimpleTest.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/components/rich-select.css"/> + <link rel="stylesheet" type="text/css" href="../../res/components/address-option.css"/> + <link rel="stylesheet" type="text/css" href="../../res/paymentRequest.css"/> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <p id="display" style="height: 100vh; margin: 0;"> + <iframe id="templateFrame" src="../../res/paymentRequest.xhtml" width="0" height="0" + sandbox="allow-same-origin" + style="float: left;"></iframe> + </p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="module"> +/** Test the payer requested details functionality **/ + +import PaymentDialog from "../../res/containers/payment-dialog.js"; + +function isVisible(elem) { + let result = elem.getBoundingClientRect().height > 0; + return result; +} + +function setPaymentOptions(requestStore, options) { + let {request} = requestStore.getState(); + request = Object.assign({}, request, { + paymentOptions: options, + }); + return requestStore.setState({ request }); +} + +const SAVED_ADDRESSES = { + "48bnds6854t": { + "address-level1": "MI", + "address-level2": "Some City", + "country": "US", + "guid": "48bnds6854t", + "name": "Mr. Foo", + "postal-code": "90210", + "street-address": "123 Sesame Street,\nApt 40", + "tel": "+1 519 555-5555", + "email": "foo@example.com", + }, + "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", + "email": "bar@example.com", + }, +}; + +let DUPED_ADDRESSES = { + "a9e830667189": { + "street-address": "Unit 1\n1505 Northeast Kentucky Industrial Parkway \n", + "address-level2": "Greenup", + "address-level1": "KY", + "postal-code": "41144", + "country": "US", + "email": "bob@example.com", + "guid": "a9e830667189", + "name": "Bob Smith", + }, + "72a15aed206d": { + "street-address": "1 New St", + "address-level2": "York", + "address-level1": "SC", + "postal-code": "29745", + "country": "US", + "guid": "72a15aed206d", + "email": "mary@example.com", + "name": "Mary Sue", + }, + "2b4dce0fbc1f": { + "street-address": "123 Park St", + "address-level2": "Springfield", + "address-level1": "OR", + "postal-code": "97403", + "country": "US", + "email": "rita@foo.com", + "guid": "2b4dce0fbc1f", + "name": "Rita Foo", + }, + "46b2635a5b26": { + "street-address": "432 Another St", + "address-level2": "Springfield", + "address-level1": "OR", + "postal-code": "97402", + "country": "US", + "guid": "46b2635a5b26", + "name": "Rita Foo", + "tel": "+19871234567", + }, +}; + +let elPicker; +let elDialog; +let initialState; + +add_task(async function setup_once() { + registerConsoleFilter(function consoleFilter(msg) { + return msg.errorMessage && + msg.errorMessage.toString().includes("selectedPayerAddress option a9e830667189 " + + "does not exist"); + }); + + let templateFrame = document.getElementById("templateFrame"); + await SimpleTest.promiseFocus(templateFrame.contentWindow); + + let displayEl = document.getElementById("display"); + importDialogDependencies(templateFrame, displayEl); + + elDialog = new PaymentDialog(); + displayEl.appendChild(elDialog); + elPicker = elDialog.querySelector("address-picker.payer-related"); + + let {request} = elDialog.requestStore.getState(); + initialState = Object.assign({}, { + changesPrevented: false, + request: Object.assign({}, request, { completeStatus: "" }), + orderDetailsShowing: false, + }); +}); + +async function setup() { + // reset the store back to a known, default state + elDialog.requestStore.setState(deepClone(initialState)); + await asyncElementRendered(); +} + +add_task(async function test_empty() { + await setup(); + + let {request, savedAddresses} = elPicker.requestStore.getState(); + ok(!savedAddresses || !savedAddresses.length, + "Check initial state has no saved addresses"); + + let {paymentOptions} = request; + let payerRequested = paymentOptions.requestPayerName || + paymentOptions.requestPayerEmail || + paymentOptions.requestPayerPhone; + ok(!payerRequested, "Check initial state has no payer details requested"); + ok(elPicker, "Check elPicker exists"); + is(elPicker.dropdown.popupBox.children.length, 0, "Check dropdown is empty"); + is(isVisible(elPicker), false, "The address-picker is not visible"); +}); + +// paymentOptions properties are acurately reflected in the address-fields attribute +add_task(async function test_visible_fields() { + await setup(); + let requestStore = elPicker.requestStore; + setPaymentOptions(requestStore, { + requestPayerName: true, + requestPayerEmail: true, + requestPayerPhone: true, + }); + + requestStore.setState({ + savedAddresses: SAVED_ADDRESSES, + selectedPayerAddress: "48bnds6854t", + }); + + await asyncElementRendered(); + + let closedRichOption = elPicker.dropdown.querySelector(".rich-select-selected-option"); + is(elPicker.dropdown.popupBox.children.length, 2, "Check dropdown has 2 addresses"); + is(closedRichOption.getAttribute("guid"), "48bnds6854t", "expected option is visible"); + + for (let fieldName of ["name", "email", "tel"]) { + let elem = closedRichOption.querySelector(`.${fieldName}`); + ok(elem, `field ${fieldName} exists`); + ok(isVisible(elem), `field ${fieldName} is visible`); + } + ok(!closedRichOption.querySelector(".street-address"), "street-address element is not present"); +}); + +add_task(async function test_selective_fields() { + await setup(); + let requestStore = elPicker.requestStore; + + requestStore.setState({ + savedAddresses: SAVED_ADDRESSES, + selectedPayerAddress: "48bnds6854t", + }); + + let payerFieldVariations = [ + {requestPayerName: true, requestPayerEmail: true, requestPayerPhone: true }, + {requestPayerName: true, requestPayerEmail: false, requestPayerPhone: false }, + {requestPayerName: false, requestPayerEmail: true, requestPayerPhone: false }, + {requestPayerName: false, requestPayerEmail: false, requestPayerPhone: true }, + {requestPayerName: true, requestPayerEmail: true, requestPayerPhone: false }, + {requestPayerName: false, requestPayerEmail: true, requestPayerPhone: true }, + {requestPayerName: true, requestPayerEmail: false, requestPayerPhone: true }, + ]; + + for (let payerFields of payerFieldVariations) { + setPaymentOptions(requestStore, payerFields); + await asyncElementRendered(); + + let closedRichOption = elPicker.dropdown.querySelector(".rich-select-selected-option"); + let elName = closedRichOption.querySelector(".name"); + let elEmail = closedRichOption.querySelector(".email"); + let elPhone = closedRichOption.querySelector(".tel"); + + is(!!elName && isVisible(elName), payerFields.requestPayerName, + "name field is correctly toggled"); + is(!!elEmail && isVisible(elEmail), payerFields.requestPayerEmail, + "email field is correctly toggled"); + is(!!elPhone && isVisible(elPhone), payerFields.requestPayerPhone, + "tel field is correctly toggled"); + + let numPayerFieldsRequested = [...Object.values(payerFields)].filter(val => val).length; + is(elPicker.getAttribute("break-after-nth-field"), numPayerFieldsRequested == 3 ? "1" : null, + "Check @break-after-nth-field"); + if (numPayerFieldsRequested == 3) { + is(closedRichOption.breakAfterNthField, "1", + "Make sure @break-after-nth-field was propagated to <address-option>"); + } else { + is(closedRichOption.breakAfterNthField, null, "Make sure @break-after-nth-field was cleared"); + } + } +}); + +add_task(async function test_filtered_options() { + await setup(); + let requestStore = elPicker.requestStore; + setPaymentOptions(requestStore, { + requestPayerName: true, + requestPayerEmail: true, + }); + + requestStore.setState({ + savedAddresses: DUPED_ADDRESSES, + selectedPayerAddress: "a9e830667189", + }); + + await asyncElementRendered(); + + let closedRichOption = elPicker.dropdown.querySelector(".rich-select-selected-option"); + is(elPicker.dropdown.popupBox.children.length, 4, "Check dropdown has 4 addresses"); + is(closedRichOption.getAttribute("guid"), "a9e830667189", "expected option is visible"); + + for (let fieldName of ["name", "email"]) { + let elem = closedRichOption.querySelector(`.${fieldName}`); + ok(elem, `field ${fieldName} exists`); + ok(isVisible(elem), `field ${fieldName} is visible`); + } + + // The selectedPayerAddress (a9e830667189) doesn't have a phone number and + // therefore will cause an error. + SimpleTest.expectUncaughtException(true); + + setPaymentOptions(requestStore, { + requestPayerPhone: true, + }); + await asyncElementRendered(); + + is(elPicker.dropdown.popupBox.children.length, 1, "Check dropdown has 1 addresses"); + + setPaymentOptions(requestStore, {}); + await asyncElementRendered(); + + is(elPicker.dropdown.popupBox.children.length, 4, "Check dropdown has 4 addresses"); +}); + +add_task(async function test_no_matches() { + await setup(); + let requestStore = elPicker.requestStore; + setPaymentOptions(requestStore, { + requestPayerPhone: true, + }); + + // The selectedPayerAddress (a9e830667189) doesn't have a phone number and + // therefore will cause an error. + SimpleTest.expectUncaughtException(true); + + requestStore.setState({ + savedAddresses: { + "2b4dce0fbc1f": { + "email": "rita@foo.com", + "guid": "2b4dce0fbc1f", + "name": "Rita Foo", + }, + "46b2635a5b26": { + "guid": "46b2635a5b26", + "name": "Rita Foo", + }, + }, + selectedPayerAddress: "a9e830667189", + }); + + await asyncElementRendered(); + + let closedRichOption = elPicker.dropdown.querySelector(".rich-select-selected-option"); + is(elPicker.dropdown.popupBox.children.length, 0, "Check dropdown is empty"); + ok(closedRichOption.localName !== "address-option", "No option is selected and visible"); +}); + +</script> + +</body> +</html> diff --git a/browser/components/payments/test/mochitest/test_payment_details_item.html b/browser/components/payments/test/mochitest/test_payment_details_item.html new file mode 100644 index 0000000000..1a2cf302a4 --- /dev/null +++ b/browser/components/payments/test/mochitest/test_payment_details_item.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test the payment-details-item component +--> +<head> + <meta charset="utf-8"> + <title>Test the payment-details-item component</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="payments_common.js"></script> + + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <p id="display"> + <payment-details-item id="item1"></payment-details-item> + <payment-details-item id="item2" label="Some item" amount-value="2" amount-currency="USD"></payment-details-item> + </p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="module"> +/** Test the payment-details-item component **/ + +import "../../res/components/payment-details-item.js"; + +let item1 = document.getElementById("item1"); +let item2 = document.getElementById("item2"); + +add_task(async function test_no_value() { + ok(item1, "item1 exists"); + is(item1.textContent, "", "Initially empty"); + + item1.label = "New label"; + await asyncElementRendered(); + is(item1.getAttribute("label"), "New label", "Check @label"); + ok(!item1.hasAttribute("amount-value"), "Check @amount-value"); + ok(!item1.hasAttribute("amount-currency"), "Check @amount-currency"); + is(item1.label, "New label", "Check .label"); + is(item1.amountValue, null, "Check .amountValue"); + is(item1.amountCurrency, null, "Check .amountCurrency"); + + item1.label = null; + await asyncElementRendered(); + ok(!item1.hasAttribute("label"), "Setting to null should remove @label"); + is(item1.textContent, "", "Becomes empty when label is removed"); +}); + +add_task(async function test_initial_attribute_values() { + is(item2.label, "Some item", "Check .label"); + is(item2.amountValue, "2", "Check .amountValue"); + is(item2.amountCurrency, "USD", "Check .amountCurrency"); +}); + +add_task(async function test_templating() { + ok(item2.querySelector("currency-amount"), "creates currency-amount component"); + ok(item2.querySelector(".label"), "creates label"); +}); + +</script> + +</body> +</html> diff --git a/browser/components/payments/test/mochitest/test_payment_dialog.html b/browser/components/payments/test/mochitest/test_payment_dialog.html new file mode 100644 index 0000000000..200c91ec08 --- /dev/null +++ b/browser/components/payments/test/mochitest/test_payment_dialog.html @@ -0,0 +1,360 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test the payment-dialog custom element +--> +<head> + <meta charset="utf-8"> + <title>Test the payment-dialog element</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="sinon-7.2.7.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="/tests/SimpleTest/test.css"/> + <link rel="stylesheet" type="text/css" href="../../res/paymentRequest.css"/> + <link rel="stylesheet" type="text/css" href="../../res/containers/rich-picker.css"/> +</head> +<body> + <p id="display" style="height: 100vh; margin: 0;"> + <iframe id="templateFrame" src="paymentRequest.xhtml" width="0" height="0" + sandbox="allow-same-origin" + style="float: left;"></iframe> + </p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="module"> +/** Test the payment-dialog element **/ + +/* global sinon */ + +import PaymentDialog from "../../res/containers/payment-dialog.js"; + +let el1; + +add_task(async function setup_once() { + let templateFrame = document.getElementById("templateFrame"); + await SimpleTest.promiseFocus(templateFrame.contentWindow); + let displayEl = document.getElementById("display"); + importDialogDependencies(templateFrame, displayEl); + + el1 = new PaymentDialog(); + displayEl.appendChild(el1); + + sinon.spy(el1, "render"); + sinon.spy(el1, "stateChangeCallback"); +}); + +async function setup() { + let {request} = el1.requestStore.getState(); + await el1.requestStore.setState({ + changesPrevented: false, + request: Object.assign({}, request, {completeStatus: ""}), + orderDetailsShowing: false, + page: { + id: "payment-summary", + }, + }); + + el1.render.reset(); + el1.stateChangeCallback.reset(); +} + +add_task(async function test_initialState() { + await setup(); + let initialState = el1.requestStore.getState(); + let elDetails = el1._orderDetailsOverlay; + + is(initialState.orderDetailsShowing, false, "orderDetailsShowing is initially false"); + ok(elDetails.hasAttribute("hidden"), "Check details are hidden"); + is(initialState.page.id, "payment-summary", "Check initial page"); +}); + +add_task(async function test_viewAllButtonVisibility() { + await setup(); + + let button = el1._viewAllButton; + ok(button.hidden, "Button is initially hidden when there are no items to show"); + ok(isHidden(button), "Button should be visibly hidden since bug 1469464"); + + // Add a display item. + let request = deepClone(el1.requestStore.getState().request); + request.paymentDetails.displayItems = [ + { + "label": "Triangle", + "amount": { + "currency": "CAD", + "value": "3", + }, + }, + ]; + await el1.requestStore.setState({ request }); + await asyncElementRendered(); + + // Check if the "View all items" button is visible. + ok(!button.hidden, "Button is visible"); +}); + +add_task(async function test_viewAllButton() { + await setup(); + + let elDetails = el1._orderDetailsOverlay; + let button = el1._viewAllButton; + + button.click(); + await asyncElementRendered(); + + ok(el1.stateChangeCallback.calledOnce, "stateChangeCallback called once"); + ok(el1.render.calledOnce, "render called once"); + + let state = el1.requestStore.getState(); + is(state.orderDetailsShowing, true, "orderDetailsShowing becomes true"); + ok(!elDetails.hasAttribute("hidden"), "Check details aren't hidden"); +}); + +add_task(async function test_changesPrevented() { + await setup(); + let state = el1.requestStore.getState(); + is(state.changesPrevented, false, "changesPrevented is initially false"); + let disabledOverlay = document.getElementById("disabled-overlay"); + ok(disabledOverlay.hidden, "Overlay should initially be hidden"); + await el1.requestStore.setState({changesPrevented: true}); + await asyncElementRendered(); + ok(!disabledOverlay.hidden, "Overlay should prevent changes"); +}); + +add_task(async function test_initial_completeStatus() { + await setup(); + let {request, page} = el1.requestStore.getState(); + is(request.completeStatus, "", "completeStatus is initially empty"); + + let payButton = document.getElementById("pay"); + is(payButton, document.querySelector(`#${page.id} button.primary`), + "Primary button is the pay button in the initial state"); + is(payButton.textContent, "Pay", "Check default label"); + ok(payButton.disabled, "Button is disabled by default"); +}); + +add_task(async function test_generic_errors() { + await setup(); + const SHIPPING_GENERIC_ERROR = "Can't ship to that address"; + el1._errorText.dataset.shippingGenericError = SHIPPING_GENERIC_ERROR; + el1.requestStore.setState({ + savedAddresses: { + "48bnds6854t": { + "address-level1": "MI", + "address-level2": "Some City", + "country": "US", + "guid": "48bnds6854t", + "name": "Mr. Foo", + "postal-code": "90210", + "street-address": "123 Sesame Street,\nApt 40", + "tel": "+1 519 555-5555", + }, + "68gjdh354j": { + "address-level1": "CA", + "address-level2": "Mountain View", + "country": "US", + "guid": "68gjdh354j", + "name": "Mrs. Bar", + "postal-code": "94041", + "street-address": "P.O. Box 123", + "tel": "+1 650 555-5555", + }, + }, + selectedShippingAddress: "48bnds6854t", + }); + await asyncElementRendered(); + + let picker = el1._shippingAddressPicker; + ok(picker.selectedOption, "Address picker should have a selected option"); + is(el1._errorText.textContent, SHIPPING_GENERIC_ERROR, + "Generic error message should be shown when no shipping options or error are provided"); +}); + +add_task(async function test_processing_completeStatus() { + // "processing": has overlay. Check button visibility + await setup(); + let {request} = el1.requestStore.getState(); + // this a transition state, set when waiting for a response from the merchant page + el1.requestStore.setState({ + changesPrevented: true, + request: Object.assign({}, request, {completeStatus: "processing"}), + }); + await asyncElementRendered(); + + let primaryButtons = document.querySelectorAll("footer button.primary"); + ok(Array.from(primaryButtons).every(el => isHidden(el) || el.disabled), + "all primary footer buttons are hidden or disabled"); + + info("Got an update from the parent process with an error from .retry()"); + request = el1.requestStore.getState().request; + let paymentDetails = deepClone(request.paymentDetails); + paymentDetails.error = "Sample retry error"; + await el1.setStateFromParent({ + request: Object.assign({}, request, { + completeStatus: "", + paymentDetails, + }), + }); + await asyncElementRendered(); + + let {changesPrevented, page} = el1.requestStore.getState(); + ok(!changesPrevented, "Changes should no longer be prevented"); + is(page.id, "payment-summary", "Check back on payment-summary"); + ok(el1.innerText.includes("Sample retry error"), "Check error text is visible"); +}); + +add_task(async function test_success_unknown_completeStatus() { + // in the "success" and "unknown" completion states the dialog would normally be closed + // so just ensure it is left in a good state + for (let completeStatus of ["success", "unknown"]) { + await setup(); + let {request} = el1.requestStore.getState(); + el1.requestStore.setState({ + request: Object.assign({}, request, {completeStatus}), + }); + await asyncElementRendered(); + + let {page} = el1.requestStore.getState(); + + // this status doesnt change page + let payButton = document.getElementById("pay"); + is(payButton, document.querySelector(`#${page.id} button.primary`), + `Primary button is the pay button in the ${completeStatus} state`); + + if (completeStatus == "success") { + is(payButton.textContent, "Done", "Check button label"); + } + if (completeStatus == "unknown") { + is(payButton.textContent, "Unknown", "Check button label"); + } + ok(payButton.disabled, "Button is disabled by default"); + } +}); + +add_task(async function test_timeout_fail_completeStatus() { + // in these states the dialog stays open and presents a single + // button for acknowledgement + for (let completeStatus of ["fail", "timeout"]) { + await setup(); + let {request} = el1.requestStore.getState(); + el1.requestStore.setState({ + request: Object.assign({}, request, {completeStatus}), + page: { + id: `completion-${completeStatus}-error`, + }, + }); + await asyncElementRendered(); + + let {page} = el1.requestStore.getState(); + let pageElem = document.querySelector(`#${page.id}`); + let payButton = document.getElementById("pay"); + let primaryButton = pageElem.querySelector("button.primary"); + + ok(pageElem && !isHidden(pageElem, `page element for ${page.id} exists and is visible`)); + ok(!isHidden(primaryButton), "Primary button is visible"); + ok(payButton != primaryButton, + `Primary button is the not pay button in the ${completeStatus} state`); + ok(isHidden(payButton), "Pay button is not visible"); + is(primaryButton.textContent, "Close", "Check button label"); + + let rect = primaryButton.getBoundingClientRect(); + let visibleElement = + document.elementFromPoint(rect.x + rect.width / 2, rect.y + rect.height / 2); + ok(primaryButton === visibleElement, "Primary button is on top of the overlay"); + } +}); + +add_task(async function test_scrollPaymentRequestPage() { + await setup(); + info("making the payment-dialog container small to require scrolling"); + el1.parentElement.style.height = "100px"; + let summaryPageBody = document.querySelector("#payment-summary .page-body"); + is(summaryPageBody.scrollTop, 0, "Page body not scrolled initially"); + let securityCodeInput = summaryPageBody.querySelector("payment-method-picker input"); + securityCodeInput.focus(); + await new Promise(resolve => SimpleTest.executeSoon(resolve)); + ok(summaryPageBody.scrollTop > 0, "Page body scrolled after focusing the CVV field"); + el1.parentElement.style.height = ""; +}); + +add_task(async function test_acceptedCards() { + let initialState = el1.requestStore.getState(); + let paymentMethods = [{ + supportedMethods: "basic-card", + data: { + supportedNetworks: ["visa", "mastercard"], + }, + }]; + el1.requestStore.setState({ + request: Object.assign({}, initialState.request, { + paymentMethods, + }), + }); + await asyncElementRendered(); + + let acceptedCards = el1._acceptedCardsList; + ok(acceptedCards && !isHidden(acceptedCards), "Accepted cards list is present and visible"); + + paymentMethods = [{ + supportedMethods: "basic-card", + }]; + el1.requestStore.setState({ + request: Object.assign({}, initialState.request, { + paymentMethods, + }), + }); + await asyncElementRendered(); + + acceptedCards = el1._acceptedCardsList; + ok(acceptedCards && isHidden(acceptedCards), "Accepted cards list is present but hidden"); +}); + +add_task(async function test_picker_labels() { + await setup(); + let picker = el1._shippingOptionPicker; + + const SHIPPING_OPTIONS_LABEL = "Shipping options"; + const DELIVERY_OPTIONS_LABEL = "Delivery options"; + const PICKUP_OPTIONS_LABEL = "Pickup options"; + picker.dataset.shippingOptionsLabel = SHIPPING_OPTIONS_LABEL; + picker.dataset.deliveryOptionsLabel = DELIVERY_OPTIONS_LABEL; + picker.dataset.pickupOptionsLabel = PICKUP_OPTIONS_LABEL; + + for (let [shippingType, label] of [ + ["shipping", SHIPPING_OPTIONS_LABEL], + ["delivery", DELIVERY_OPTIONS_LABEL], + ["pickup", PICKUP_OPTIONS_LABEL], + ]) { + let request = deepClone(el1.requestStore.getState().request); + request.paymentOptions.requestShipping = true; + request.paymentOptions.shippingType = shippingType; + await el1.requestStore.setState({ request }); + await asyncElementRendered(); + is(picker.labelElement.textContent, label, + `Label should be appropriate for ${shippingType}`); + } +}); + +add_task(async function test_disconnect() { + await setup(); + + el1.remove(); + await el1.requestStore.setState({orderDetailsShowing: true}); + await asyncElementRendered(); + ok(el1.stateChangeCallback.notCalled, "stateChangeCallback not called"); + ok(el1.render.notCalled, "render not called"); + + let elDetails = el1._orderDetailsOverlay; + ok(elDetails.hasAttribute("hidden"), "details overlay remains hidden"); +}); +</script> + +</body> +</html> diff --git a/browser/components/payments/test/mochitest/test_payment_dialog_required_top_level_items.html b/browser/components/payments/test/mochitest/test_payment_dialog_required_top_level_items.html new file mode 100644 index 0000000000..88f7080f08 --- /dev/null +++ b/browser/components/payments/test/mochitest/test_payment_dialog_required_top_level_items.html @@ -0,0 +1,252 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test the payment-dialog custom element +--> +<head> + <meta charset="utf-8"> + <title>Test the payment-dialog element</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script 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="/tests/SimpleTest/test.css"/> + <link rel="stylesheet" type="text/css" href="../../res/paymentRequest.css"/> + <link rel="stylesheet" type="text/css" href="../../res/containers/rich-picker.css"/> +</head> +<body> + <p id="display" style="height: 100vh; margin: 0;"> + <iframe id="templateFrame" src="paymentRequest.xhtml" width="0" height="0" + sandbox="allow-same-origin" + style="float: left;"></iframe> + </p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="module"> +/** Test the payment-dialog element **/ + +import PaymentDialog from "../../res/containers/payment-dialog.js"; + +let el1; + +add_task(async function setupOnce() { + let templateFrame = document.getElementById("templateFrame"); + await SimpleTest.promiseFocus(templateFrame.contentWindow); + + let displayEl = document.getElementById("display"); + importDialogDependencies(templateFrame, displayEl); + + el1 = new PaymentDialog(); + displayEl.appendChild(el1); +}); + +async function setup({shippingRequired, payerRequired}) { + let state = deepClone(el1.requestStore.getState()); + state.request.paymentDetails.shippingOptions = shippingRequired ? [{ + id: "123", + label: "Carrier Pigeon", + amount: { + currency: "USD", + value: 10, + }, + selected: false, + }, { + id: "456", + label: "Lightspeed (default)", + amount: { + currency: "USD", + value: 20, + }, + selected: true, + }] : null; + state.request.paymentOptions.requestShipping = shippingRequired; + state.request.paymentOptions.requestPayerName = payerRequired; + state.request.paymentOptions.requestPayerPhone = payerRequired; + state.savedAddresses = shippingRequired || payerRequired ? { + "48bnds6854t": { + "address-level1": "MI", + "address-level2": "Some City", + "country": "US", + "guid": "48bnds6854t", + "name": "Mr. Foo", + "postal-code": "90210", + "street-address": "123 Sesame Street,\nApt 40", + "tel": "+1 519 555-5555", + }, + "68gjdh354j": { + "address-level1": "CA", + "address-level2": "Mountain View", + "country": "US", + "guid": "68gjdh354j", + "name": "Mrs. Bar", + "postal-code": "94041", + "street-address": "P.O. Box 123", + "tel": "+1 650 555-5555", + }, + "abcdef1234": { + "address-level1": "CA", + "address-level2": "Mountain View", + "country": "US", + "guid": "abcdef1234", + "name": "Jane Fields", + }, + } : {}; + state.savedBasicCards = { + "john-doe": Object.assign({ + "cc-exp": (new Date()).getFullYear() + 9 + "-01", + methodName: "basic-card", + guid: "aaa1", + }, deepClone(PTU.BasicCards.JohnDoe)), + "missing-fields": Object.assign({ + methodName: "basic-card", + guid: "aaa2", + }, deepClone(PTU.BasicCards.MissingFields)), + }; + state.selectedPayerAddress = null; + state.selectedPaymentCard = null; + state.selectedShippingAddress = null; + state.selectedShippingOption = null; + await el1.requestStore.setState(state); + + // Fill the security code input so it doesn't interfere with checking the pay + // button state for dropdown changes. + el1._paymentMethodPicker.securityCodeInput.querySelector("input").select(); + sendString("123"); + await asyncElementRendered(); +} + +function selectFirstItemOfPicker(picker) { + picker.dropdown.popupBox.focus(); + let options = picker.dropdown.popupBox.children; + if (options[0].selected) { + ok(false, `"${options[0].textContent}" was already selected`); + return; + } + info(`Selecting "${options[0].textContent}" from the options`); + + synthesizeKey(options[0].textContent.trim().split(/\s+/)[0], {}); + ok(picker.dropdown.selectedOption, `Option should be selected for ${picker.localName}`); +} + +function selectLastItemOfPicker(picker) { + picker.dropdown.popupBox.focus(); + let options = picker.dropdown.popupBox.children; + let lastOption = options[options.length - 1]; + if (lastOption.selected) { + ok(false, `"${lastOption.textContent}" was already selected`); + return; + } + + synthesizeKey(lastOption.textContent.trim().split(/\s+/)[0], {}); + ok(picker.dropdown.selectedOption, `Option should be selected for ${picker.localName}`); +} + +add_task(async function runTests() { + let allPickers = { + shippingAddress: el1._shippingAddressPicker, + shippingOption: el1._shippingOptionPicker, + paymentMethod: el1._paymentMethodPicker, + payerAddress: el1._payerAddressPicker, + }; + let testCases = [ + { + label: "shippingAndPayerRequired", + setup: { shippingRequired: true, payerRequired: true }, + pickers: Object.values(allPickers), + }, { + label: "payerRequired", + setup: { payerRequired: true }, + pickers: [allPickers.paymentMethod, allPickers.payerAddress], + }, { + label: "shippingRequired", + setup: { shippingRequired: true }, + pickers: [ + allPickers.shippingAddress, + allPickers.shippingOption, + allPickers.paymentMethod, + ], + }, + ]; + + for (let testCase of testCases) { + info(`Starting testcase ${testCase.label}`); + await setup(testCase.setup); + + for (let picker of testCase.pickers) { + ok(!picker.dropdown.selectedOption, `No option selected for ${picker.localName}`); + } + let hiddenPickers = Object.values(allPickers).filter(p => !testCase.pickers.includes(p)); + for (let hiddenPicker of hiddenPickers) { + ok(hiddenPicker.hidden, `${hiddenPicker.localName} should be hidden`); + } + + let payButton = document.getElementById("pay"); + ok(payButton.disabled, "Button is disabled when required options are not selected"); + + let stateChangedPromise = promiseStateChange(el1.requestStore); + testCase.pickers.forEach(selectFirstItemOfPicker); + await stateChangedPromise; + + ok(!payButton.disabled, "Button is enabled when required options are selected"); + + // Individually toggle each picker to see how the missing fields affects Pay button. + for (let picker of testCase.pickers) { + // There is no "invalid" option for shipping options. + if (picker == allPickers.shippingOption) { + continue; + } + info(`picker: ${picker.localName} with className: ${picker.className}`); + + // Setup the invalid state + stateChangedPromise = promiseStateChange(el1.requestStore); + selectLastItemOfPicker(picker); + await stateChangedPromise; + + ok(payButton.disabled, "Button is disabled when selected option has missing fields"); + + // Now setup the valid state + stateChangedPromise = promiseStateChange(el1.requestStore); + selectFirstItemOfPicker(picker); + await stateChangedPromise; + + ok(!payButton.disabled, "Button is enabled when selected option has all required fields"); + } + } +}); + +add_task(async function test_securityCodeRequired() { + await setup({ + payerRequired: false, + shippingRequired: false, + }); + + let picker = el1._paymentMethodPicker; + let payButton = document.getElementById("pay"); + + let stateChangedPromise = promiseStateChange(el1.requestStore); + selectFirstItemOfPicker(picker); + await stateChangedPromise; + + picker.securityCodeInput.querySelector("input").select(); + stateChangedPromise = promiseStateChange(el1.requestStore); + synthesizeKey("VK_DELETE"); + await stateChangedPromise; + + ok(payButton.disabled, "Button is disabled when CVV is empty"); + + picker.securityCodeInput.querySelector("input").select(); + stateChangedPromise = promiseStateChange(el1.requestStore); + sendString("123"); + await stateChangedPromise; + + ok(!payButton.disabled, "Button is enabled when CVV is filled"); +}); +</script> + +</body> +</html> diff --git a/browser/components/payments/test/mochitest/test_payment_method_picker.html b/browser/components/payments/test/mochitest/test_payment_method_picker.html new file mode 100644 index 0000000000..1e2adee856 --- /dev/null +++ b/browser/components/payments/test/mochitest/test_payment_method_picker.html @@ -0,0 +1,279 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test the payment-method-picker component +--> +<head> + <meta charset="utf-8"> + <title>Test the payment-method-picker component</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="payments_common.js"></script> + <script src="../../res/unprivileged-fallbacks.js"></script> + + <link rel="stylesheet" type="text/css" href="../../res/containers/rich-picker.css"/> + <link rel="stylesheet" type="text/css" href="../../res/components/rich-select.css"/> + <link rel="stylesheet" type="text/css" href="../../res/components/basic-card-option.css"/> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <p id="display"> + <payment-method-picker id="picker1" + data-invalid-label="picker1: Missing or invalid" + selected-state-key="selectedPaymentCard"></payment-method-picker> + </p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="module"> +/** Test the payment-method-picker component **/ + +import "../../res/components/basic-card-option.js"; +import "../../res/containers/payment-method-picker.js"; + +let picker1 = document.getElementById("picker1"); + +add_task(async function test_empty() { + ok(picker1, "Check picker1 exists"); + let {savedBasicCards} = picker1.requestStore.getState(); + is(Object.keys(savedBasicCards).length, 0, "Check empty initial state"); + is(picker1.dropdown.popupBox.children.length, 0, "Check dropdown is empty"); +}); + +add_task(async function test_initialSet() { + await picker1.requestStore.setState({ + savedBasicCards: { + "48bnds6854t": { + "cc-exp": "2017-02", + "cc-exp-month": 2, + "cc-exp-year": 2017, + "cc-name": "John Doe", + "cc-number": "************9999", + "cc-type": "mastercard", + "guid": "48bnds6854t", + timeLastUsed: 300, + }, + "68gjdh354j": { + "cc-exp": "2017-08", + "cc-exp-month": 8, + "cc-exp-year": 2017, + "cc-name": "J Smith", + "cc-number": "***********1234", + "cc-type": "visa", + "guid": "68gjdh354j", + timeLastUsed: 100, + }, + "123456789abc": { + "cc-name": "Jane Fields", + "cc-given-name": "Jane", + "cc-additional-name": "", + "cc-family-name": "Fields", + "cc-number": "************9876", + "guid": "123456789abc", + timeLastUsed: 200, + }, + }, + }); + await asyncElementRendered(); + let options = picker1.dropdown.popupBox.children; + is(options.length, 3, "Check dropdown has all three cards"); + ok(options[0].textContent.includes("John Doe"), "Check first card based on timeLastUsed"); + ok(options[1].textContent.includes("Jane Fields"), "Check second card based on timeLastUsed"); + ok(options[2].textContent.includes("J Smith"), "Check third card based on timeLastUsed"); +}); + +add_task(async function test_update() { + await picker1.requestStore.setState({ + savedBasicCards: { + "48bnds6854t": { + // Same GUID, different values to trigger an update + "cc-exp": "2017-09", + "cc-exp-month": 9, + "cc-exp-year": 2017, + // cc-name was cleared which means it's not returned + "cc-number": "************9876", + "cc-type": "amex", + "guid": "48bnds6854t", + }, + "68gjdh354j": { + "cc-exp": "2017-08", + "cc-exp-month": 8, + "cc-exp-year": 2017, + "cc-name": "J Smith", + "cc-number": "***********1234", + "cc-type": "visa", + "guid": "68gjdh354j", + }, + "123456789abc": { + "cc-name": "Jane Fields", + "cc-given-name": "Jane", + "cc-additional-name": "", + "cc-family-name": "Fields", + "cc-number": "************9876", + "guid": "123456789abc", + }, + }, + }); + await asyncElementRendered(); + let options = picker1.dropdown.popupBox.children; + is(options.length, 3, "Check dropdown still has three cards"); + ok(!options[0].textContent.includes("John Doe"), "Check cleared first cc-name"); + ok(options[0].textContent.includes("9876"), "Check updated first cc-number"); + ok(options[0].textContent.includes("09"), "Check updated first exp-month"); + + ok(options[1].textContent.includes("J Smith"), "Check second card is the same"); + ok(options[2].textContent.includes("Jane Fields"), "Check third card is the same"); +}); + +add_task(async function test_change_selected_card() { + let options = picker1.dropdown.popupBox.children; + is(picker1.dropdown.selectedOption, null, "Should default to no selected option"); + let { + selectedPaymentCard, + selectedPaymentCardSecurityCode, + } = picker1.requestStore.getState(); + is(selectedPaymentCard, null, "store should have no option selected"); + is(selectedPaymentCardSecurityCode, null, "store should have no security code"); + ok(!picker1.classList.contains("invalid-selected-option"), "No validation on an empty selection"); + ok(isHidden(picker1.invalidLabel), "The invalid label should be hidden"); + + await SimpleTest.promiseFocus(); + picker1.dropdown.popupBox.focus(); + synthesizeKey("************9876", {}); + await asyncElementRendered(); + ok(true, "Focused the security code field"); + ok(!picker1.open, "Picker should be closed"); + + let selectedOption = picker1.dropdown.selectedOption; + is(selectedOption, options[2], "Selected option should now be the third option"); + selectedPaymentCard = picker1.requestStore.getState().selectedPaymentCard; + is(selectedPaymentCard, selectedOption.getAttribute("guid"), + "store should have third option selected"); + selectedPaymentCardSecurityCode = picker1.requestStore.getState().selectedPaymentCardSecurityCode; + is(selectedPaymentCardSecurityCode, null, "store should have empty security code"); + ok(picker1.classList.contains("invalid-selected-option"), "Missing fields for the third option"); + ok(!isHidden(picker1.invalidLabel), "The invalid label should be visible"); + is(picker1.invalidLabel.innerText, picker1.dataset.invalidLabel, "Check displayed error text"); + + await SimpleTest.promiseFocus(); + picker1.dropdown.popupBox.focus(); + synthesizeKey("visa", {}); + await asyncElementRendered(); + ok(true, "Focused the security code field"); + ok(!picker1.open, "Picker should be closed"); + + selectedOption = picker1.dropdown.selectedOption; + is(selectedOption, options[1], "Selected option should now be the second option"); + selectedPaymentCard = picker1.requestStore.getState().selectedPaymentCard; + is(selectedPaymentCard, selectedOption.getAttribute("guid"), + "store should have second option selected"); + selectedPaymentCardSecurityCode = picker1.requestStore.getState().selectedPaymentCardSecurityCode; + is(selectedPaymentCardSecurityCode, null, "store should have empty security code"); + ok(!picker1.classList.contains("invalid-selected-option"), "The second option has all fields"); + ok(isHidden(picker1.invalidLabel), "The invalid label should be hidden"); + + let stateChangePromise = promiseStateChange(picker1.requestStore); + + // Type in the security code field + picker1.securityCodeInput.querySelector("input").focus(); + sendString("836"); + sendKey("Tab"); + let state = await stateChangePromise; + is(state.selectedPaymentCardSecurityCode, "836", "Check security code in state"); +}); + +add_task(async function test_delete() { + await picker1.requestStore.setState({ + savedBasicCards: { + // 48bnds6854t was deleted + "68gjdh354j": { + "cc-exp": "2017-08", + "cc-exp-month": 8, + "cc-exp-year": 2017, + "cc-name": "J Smith", + "cc-number": "***********1234", + "cc-type": "visa", + "guid": "68gjdh354j", + }, + "123456789abc": { + "cc-name": "Jane Fields", + "cc-given-name": "Jane", + "cc-additional-name": "", + "cc-family-name": "Fields", + "cc-number": "************9876", + "guid": "123456789abc", + }, + }, + }); + await asyncElementRendered(); + let options = picker1.dropdown.popupBox.children; + is(options.length, 2, "Check dropdown has two remaining cards"); + ok(options[0].textContent.includes("J Smith"), "Check remaining card #1"); + ok(options[1].textContent.includes("Jane Fields"), "Check remaining card #2"); +}); + +add_task(async function test_supportedNetworks_tempCards() { + await picker1.requestStore.reset(); + + let request = Object.assign({}, picker1.requestStore.getState().request); + request.paymentMethods = [ + { + supportedMethods: "basic-card", + data: { + supportedNetworks: [ + "mastercard", + "visa", + ], + }, + }, + ]; + + await picker1.requestStore.setState({ + request, + selectedPaymentCard: "68gjdh354j", + tempBasicCards: { + "68gjdh354j": { + "cc-exp": "2017-08", + "cc-exp-month": 8, + "cc-exp-year": 2017, + "cc-name": "J Smith", + "cc-number": "***********1234", + "cc-type": "discover", + "guid": "68gjdh354j", + }, + }, + }); + await asyncElementRendered(); + let options = picker1.dropdown.popupBox.children; + is(options.length, 1, "Check dropdown has one card"); + ok(options[0].textContent.includes("J Smith"), "Check remaining card #1"); + + ok(picker1.classList.contains("invalid-selected-option"), + "Check discover is recognized as not supported"); + is(picker1.invalidLabel.innerText, picker1.dataset.invalidLabel, "Check displayed error text"); + + info("change the card to be a visa"); + await picker1.requestStore.setState({ + tempBasicCards: { + "68gjdh354j": { + "cc-exp": "2017-08", + "cc-exp-month": 8, + "cc-exp-year": 2017, + "cc-name": "J Smith", + "cc-number": "***********1234", + "cc-type": "visa", + "guid": "68gjdh354j", + }, + }, + }); + await asyncElementRendered(); + + ok(!picker1.classList.contains("invalid-selected-option"), + "Check visa is recognized as supported"); +}); +</script> + +</body> +</html> diff --git a/browser/components/payments/test/mochitest/test_rich_select.html b/browser/components/payments/test/mochitest/test_rich_select.html new file mode 100644 index 0000000000..e071ed15e2 --- /dev/null +++ b/browser/components/payments/test/mochitest/test_rich_select.html @@ -0,0 +1,150 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test the rich-select component +--> +<head> + <meta charset="utf-8"> + <title>Test the rich-select component</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script 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/components/rich-select.css"/> + <link rel="stylesheet" type="text/css" href="../../res/components/address-option.css"/> + <link rel="stylesheet" type="text/css" href="../../res/components/basic-card-option.css"/> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <p id="display"> + </p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="module"> +/** Test the rich-select address-option component **/ + +import AddressOption from "../../res/components/address-option.js"; +import RichSelect from "../../res/components/rich-select.js"; + +let addresses = { + "58gjdh354k": { + "email": "emzembrano92@email.com", + "name": "Emily Zembrano", + "street-address": "717 Hyde Street #6", + "address-level2": "San Francisco", + "address-level1": "CA", + "tel": "415 203 0845", + "postal-code": "94109", + "country": "USA", + "guid": "58gjdh354k", + }, + "67gjdh354k": { + "email": "jenz9382@email.com", + "name": "Jennifer Zembrano", + "street-address": "42 Fairydust Lane", + "address-level2": "Lala Land", + "address-level1": "HI", + "tel": "415 439 2827", + "postal-code": "98765", + "country": "USA", + "guid": "67gjdh354k", + }, +}; + +let select1 = new RichSelect(); +for (let address of Object.values(addresses)) { + let option = document.createElement("option"); + option.textContent = address.name + " " + address["street-address"]; + option.setAttribute("value", address.guid); + option.dataset.fieldSeparator = ", "; + for (let field of Object.keys(address)) { + option.setAttribute(field, address[field]); + } + select1.popupBox.appendChild(option); +} +select1.setAttribute("option-type", "address-option"); +select1.value = ""; +document.getElementById("display").appendChild(select1); + +let options = select1.popupBox.children; +let option1 = options[0]; +let option2 = options[1]; + +function get_selected_clone() { + return select1.querySelector(".rich-select-selected-option"); +} + +function is_visible(element, message) { + ok(!isHidden(element), message); +} + +add_task(async function test_clickable_area() { + ok(select1, "select1 exists"); + isnot(document.activeElement, select1.popupBox, "<select> shouldn't have focus"); + synthesizeMouseAtCenter(select1, {}); + is(document.activeElement, select1.popupBox, "<select> should have focus when clicked"); + document.activeElement.blur(); +}); + +add_task(async function test_closed_state_on_selection() { + ok(select1, "select1 exists"); + select1.popupBox.focus(); + synthesizeKey(option1.textContent, {}); + await asyncElementRendered(); + ok(option1.selected, "option 1 is now selected"); + + let selectedClone = get_selected_clone(); + is_visible(selectedClone, "The selected clone should be visible at all times"); + is(selectedClone.getAttribute("email"), option1.getAttribute("email"), + "The selected clone email should be equivalent to the selected option 2"); + is(selectedClone.getAttribute("name"), option1.getAttribute("name"), + "The selected clone name should be equivalent to the selected option 2"); +}); + +add_task(async function test_multi_select_not_supported_in_dropdown() { + ok(option1.selected, "Option 1 should be selected from prior test"); + + select1.popupBox.focus(); + synthesizeKey(option2.textContent, {}); + await asyncElementRendered(); + + ok(!option1.selected, "Option 1 should no longer be selected after selecting option1"); + ok(option2.selected, "Option 2 should now have selected property set to true"); +}); + +add_task(async function test_selected_clone_should_equal_selected_option() { + ok(option2.selected, "option 2 should be selected"); + + let clonedOptions = select1.querySelectorAll(".rich-select-selected-option"); + is(clonedOptions.length, 1, "there should only be one cloned option"); + + let clonedOption = clonedOptions[0]; + for (let attrName of AddressOption.recordAttributes) { + is(clonedOption.attributes[attrName] && clonedOption.attributes[attrName].value, + option2.attributes[attrName] && option2.attributes[attrName].value, + "attributes should have matching value; name=" + attrName); + } + + select1.popupBox.focus(); + synthesizeKey(option1.textContent, {}); + await asyncElementRendered(); + + clonedOptions = select1.querySelectorAll(".rich-select-selected-option"); + is(clonedOptions.length, 1, "there should only be one cloned option"); + + clonedOption = clonedOptions[0]; + for (let attrName of AddressOption.recordAttributes) { + is(clonedOption.attributes[attrName] && clonedOption.attributes[attrName].value, + option1.attributes[attrName] && option1.attributes[attrName].value, + "attributes should have matching value; name=" + attrName); + } +}); +</script> + +</body> +</html> diff --git a/browser/components/payments/test/mochitest/test_shipping_option_picker.html b/browser/components/payments/test/mochitest/test_shipping_option_picker.html new file mode 100644 index 0000000000..23a075dd3a --- /dev/null +++ b/browser/components/payments/test/mochitest/test_shipping_option_picker.html @@ -0,0 +1,180 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test the shipping-option-picker component +--> +<head> + <meta charset="utf-8"> + <title>Test the shipping-option-picker component</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="payments_common.js"></script> + + <link rel="stylesheet" type="text/css" href="../../res/components/rich-select.css"/> + <link rel="stylesheet" type="text/css" href="../../res/components/shipping-option.css"/> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <p id="display"> + <shipping-option-picker id="picker1"></shipping-option-picker> + </p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="module"> +/** Test the shipping-option-picker component **/ + +import "../../res/containers/shipping-option-picker.js"; + +let picker1 = document.getElementById("picker1"); + +add_task(async function test_empty() { + ok(picker1, "Check picker1 exists"); + let state = picker1.requestStore.getState(); + let {shippingOptions} = state && state.request && state.request.paymentDetails; + is(Object.keys(shippingOptions).length, 0, "Check empty initial state"); + is(picker1.dropdown.popupBox.children.length, 0, "Check dropdown is empty"); + ok(picker1.editLink.hidden, "Check that picker edit link is always hidden"); + ok(picker1.addLink.hidden, "Check that picker add link is always hidden"); +}); + +add_task(async function test_initialSet() { + picker1.requestStore.setState({ + request: { + paymentDetails: { + shippingOptions: [ + { + id: "123", + label: "Carrier Pigeon", + amount: { + currency: "USD", + value: 10, + }, + selected: false, + }, + { + id: "456", + label: "Lightspeed (default)", + amount: { + currency: "USD", + value: 20, + }, + selected: true, + }, + ], + }, + }, + selectedShippingOption: "456", + }); + await asyncElementRendered(); + let options = picker1.dropdown.popupBox.children; + is(options.length, 2, "Check dropdown has both options"); + ok(options[0].textContent.includes("Carrier Pigeon"), "Check first option"); + is(options[0].getAttribute("amount-currency"), "USD", "Check currency"); + ok(options[1].textContent.includes("Lightspeed (default)"), "Check second option"); + is(picker1.dropdown.selectedOption, options[1], "Lightspeed selected by default"); + + let selectedClone = picker1.dropdown.querySelector(".rich-select-selected-option"); + let text = selectedClone.textContent; + ok(text.includes("$20.00"), + "Shipping option clone should include amount. Value = " + text); + ok(text.includes("Lightspeed (default)"), + "Shipping option clone should include label. Value = " + text); + ok(!isHidden(selectedClone), + "Shipping option clone should be visible"); +}); + +add_task(async function test_update() { + picker1.requestStore.setState({ + request: { + paymentDetails: { + shippingOptions: [ + { + id: "123", + label: "Tortoise", + amount: { + currency: "CAD", + value: 10, + }, + selected: false, + }, + { + id: "456", + label: "Lightspeed (default)", + amount: { + currency: "USD", + value: 20, + }, + selected: true, + }, + ], + }, + }, + selectedShippingOption: "456", + }); + + await promiseStateChange(picker1.requestStore); + let options = picker1.dropdown.popupBox.children; + is(options.length, 2, "Check dropdown still has both options"); + ok(options[0].textContent.includes("Tortoise"), "Check updated first option"); + is(options[0].getAttribute("amount-currency"), "CAD", "Check currency"); + ok(options[1].textContent.includes("Lightspeed (default)"), "Check second option is the same"); + is(picker1.dropdown.selectedOption, options[1], "Lightspeed selected by default"); +}); + +add_task(async function test_change_selected_option() { + let options = picker1.dropdown.popupBox.children; + let selectedOption = picker1.dropdown.selectedOption; + is(options[1], selectedOption, "Should default to Lightspeed option"); + is(selectedOption.value, "456", "Selected option should have correct ID"); + let state = picker1.requestStore.getState(); + let selectedOptionFromState = state.selectedShippingOption; + is(selectedOption.value, selectedOptionFromState, + "store's selected option should match selected element"); + + let stateChangedPromise = promiseStateChange(picker1.requestStore); + picker1.dropdown.popupBox.focus(); + synthesizeKey(options[0].textContent, {}); + state = await stateChangedPromise; + + selectedOption = picker1.dropdown.selectedOption; + is(selectedOption, options[0], "Selected option should now be the first option"); + is(selectedOption.value, "123", "Selected option should have correct ID"); + selectedOptionFromState = state.selectedShippingOption; + is(selectedOptionFromState, "123", "store should have first option selected"); +}); + +add_task(async function test_delete() { + let stateChangedPromise = promiseStateChange(picker1.requestStore); + picker1.requestStore.setState({ + request: { + paymentDetails: { + shippingOptions: [ + { + id: "123", + label: "Tortoise", + amount: { + currency: "CAD", + value: 10, + }, + selected: false, + }, + // 456 / Lightspeed was deleted + ], + }, + }, + selectedShippingOption: "123", + }); + + await stateChangedPromise; + let options = picker1.dropdown.popupBox.children; + is(options.length, 1, "Check dropdown has one remaining address"); + ok(options[0].textContent.includes("Tortoise"), "Check remaining address"); + is(picker1.dropdown.selectedOption, options[0], "Tortoise selected by default"); +}); +</script> + +</body> +</html> |