summaryrefslogtreecommitdiffstats
path: root/browser/components/payments/test
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/payments/test')
-rw-r--r--browser/components/payments/test/PaymentTestUtils.jsm612
-rw-r--r--browser/components/payments/test/browser/blank_page.html10
-rw-r--r--browser/components/payments/test/browser/browser.ini34
-rw-r--r--browser/components/payments/test/browser/browser_address_edit.js1029
-rw-r--r--browser/components/payments/test/browser/browser_address_edit_hidden_fields.js477
-rw-r--r--browser/components/payments/test/browser/browser_card_edit.js1227
-rw-r--r--browser/components/payments/test/browser/browser_change_shipping.js732
-rw-r--r--browser/components/payments/test/browser/browser_dropdowns.js82
-rw-r--r--browser/components/payments/test/browser/browser_host_name.js50
-rw-r--r--browser/components/payments/test/browser/browser_onboarding_wizard.js859
-rw-r--r--browser/components/payments/test/browser/browser_openPreferences.js93
-rw-r--r--browser/components/payments/test/browser/browser_payerRequestedFields.js154
-rw-r--r--browser/components/payments/test/browser/browser_payment_completion.js211
-rw-r--r--browser/components/payments/test/browser/browser_profile_storage.js303
-rw-r--r--browser/components/payments/test/browser/browser_request_serialization.js250
-rw-r--r--browser/components/payments/test/browser/browser_request_shipping.js121
-rw-r--r--browser/components/payments/test/browser/browser_retry.js169
-rw-r--r--browser/components/payments/test/browser/browser_retry_fieldErrors.js882
-rw-r--r--browser/components/payments/test/browser/browser_shippingaddresschange_error.js446
-rw-r--r--browser/components/payments/test/browser/browser_show_dialog.js400
-rw-r--r--browser/components/payments/test/browser/browser_tab_modal.js300
-rw-r--r--browser/components/payments/test/browser/browser_total.js94
-rw-r--r--browser/components/payments/test/browser/head.js880
-rw-r--r--browser/components/payments/test/mochitest/formautofill/mochitest.ini10
-rw-r--r--browser/components/payments/test/mochitest/formautofill/test_editCreditCard.html34
-rw-r--r--browser/components/payments/test/mochitest/mochitest.ini37
-rw-r--r--browser/components/payments/test/mochitest/payments_common.js154
-rw-r--r--browser/components/payments/test/mochitest/test_ObservedPropertiesMixin.html116
-rw-r--r--browser/components/payments/test/mochitest/test_PaymentStateSubscriberMixin.html79
-rw-r--r--browser/components/payments/test/mochitest/test_PaymentsStore.html168
-rw-r--r--browser/components/payments/test/mochitest/test_accepted_cards.html111
-rw-r--r--browser/components/payments/test/mochitest/test_address_form.html955
-rw-r--r--browser/components/payments/test/mochitest/test_address_option.html177
-rw-r--r--browser/components/payments/test/mochitest/test_address_picker.html278
-rw-r--r--browser/components/payments/test/mochitest/test_basic_card_form.html623
-rw-r--r--browser/components/payments/test/mochitest/test_basic_card_option.html96
-rw-r--r--browser/components/payments/test/mochitest/test_billing_address_picker.html132
-rw-r--r--browser/components/payments/test/mochitest/test_completion_error_page.html88
-rw-r--r--browser/components/payments/test/mochitest/test_currency_amount.html160
-rw-r--r--browser/components/payments/test/mochitest/test_labelled_checkbox.html71
-rw-r--r--browser/components/payments/test/mochitest/test_order_details.html215
-rw-r--r--browser/components/payments/test/mochitest/test_payer_address_picker.html323
-rw-r--r--browser/components/payments/test/mochitest/test_payment_details_item.html65
-rw-r--r--browser/components/payments/test/mochitest/test_payment_dialog.html360
-rw-r--r--browser/components/payments/test/mochitest/test_payment_dialog_required_top_level_items.html252
-rw-r--r--browser/components/payments/test/mochitest/test_payment_method_picker.html279
-rw-r--r--browser/components/payments/test/mochitest/test_rich_select.html150
-rw-r--r--browser/components/payments/test/mochitest/test_shipping_option_picker.html180
-rw-r--r--browser/components/payments/test/unit/head.js2
-rw-r--r--browser/components/payments/test/unit/test_response_creation.js149
-rw-r--r--browser/components/payments/test/unit/xpcshell.ini6
51 files changed, 14685 insertions, 0 deletions
diff --git a/browser/components/payments/test/PaymentTestUtils.jsm b/browser/components/payments/test/PaymentTestUtils.jsm
new file mode 100644
index 0000000000..fd20d834e5
--- /dev/null
+++ b/browser/components/payments/test/PaymentTestUtils.jsm
@@ -0,0 +1,612 @@
+"use strict";
+
+/* global info */
+
+var EXPORTED_SYMBOLS = ["PaymentTestUtils"];
+
+var PaymentTestUtils = {
+ /**
+ * Common content tasks functions to be used with ContentTask.spawn.
+ */
+ ContentTasks: {
+ /* eslint-env mozilla/frame-script */
+ /**
+ * Add a completion handler to the existing `showPromise` to call .complete().
+ * @returns {Object} representing the PaymentResponse
+ */
+ addCompletionHandler: async ({ result, delayMs = 0 }) => {
+ let response = await content.showPromise;
+ let completeException;
+
+ // delay the given # milliseconds
+ await new Promise(resolve => content.setTimeout(resolve, delayMs));
+
+ try {
+ await response.complete(result);
+ } catch (ex) {
+ info(`Complete error: ${ex}`);
+ completeException = {
+ name: ex.name,
+ message: ex.message,
+ };
+ }
+ return {
+ completeException,
+ response: response.toJSON(),
+ // XXX: Bug NNN: workaround for `details` not being included in `toJSON`.
+ methodDetails: response.details,
+ };
+ },
+
+ /**
+ * Add a retry handler to the existing `showPromise` to call .retry().
+ * @returns {Object} representing the PaymentResponse
+ */
+ addRetryHandler: async ({ validationErrors, delayMs = 0 }) => {
+ let response = await content.showPromise;
+ let retryException;
+
+ // delay the given # milliseconds
+ await new Promise(resolve => content.setTimeout(resolve, delayMs));
+
+ try {
+ await response.retry(Cu.cloneInto(validationErrors, content));
+ } catch (ex) {
+ info(`Retry error: ${ex}`);
+ retryException = {
+ name: ex.name,
+ message: ex.message,
+ };
+ }
+ return {
+ retryException,
+ response: response.toJSON(),
+ // XXX: Bug NNN: workaround for `details` not being included in `toJSON`.
+ methodDetails: response.details,
+ };
+ },
+
+ ensureNoPaymentRequestEvent: ({ eventName }) => {
+ content.rq.addEventListener(eventName, event => {
+ ok(false, `Unexpected ${eventName}`);
+ });
+ },
+
+ promisePaymentRequestEvent: ({ eventName }) => {
+ content[eventName + "Promise"] = new Promise(resolve => {
+ content.rq.addEventListener(eventName, () => {
+ info(`Received event: ${eventName}`);
+ resolve();
+ });
+ });
+ },
+
+ /**
+ * Used for PaymentRequest and PaymentResponse event promises.
+ */
+ awaitPaymentEventPromise: async ({ eventName }) => {
+ await content[eventName + "Promise"];
+ return true;
+ },
+
+ promisePaymentResponseEvent: async ({ eventName }) => {
+ let response = await content.showPromise;
+ content[eventName + "Promise"] = new Promise(resolve => {
+ response.addEventListener(eventName, () => {
+ info(`Received event: ${eventName}`);
+ resolve();
+ });
+ });
+ },
+
+ updateWith: async ({ eventName, details }) => {
+ /* globals ok */
+ if (
+ details.error &&
+ (!details.shippingOptions || details.shippingOptions.length)
+ ) {
+ ok(false, "Need to clear the shipping options to show error text");
+ }
+ if (!details.total) {
+ ok(
+ false,
+ "`total: { label, amount: { value, currency } }` is required for updateWith"
+ );
+ }
+
+ content[eventName + "Promise"] = new Promise(resolve => {
+ content.rq.addEventListener(
+ eventName,
+ event => {
+ event.updateWith(details);
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ },
+
+ /**
+ * Create a new payment request cached as `rq` and then show it.
+ *
+ * @param {Object} args
+ * @param {PaymentMethodData[]} methodData
+ * @param {PaymentDetailsInit} details
+ * @param {PaymentOptions} options
+ * @returns {Object}
+ */
+ createAndShowRequest: ({ methodData, details, options }) => {
+ const rq = new content.PaymentRequest(
+ Cu.cloneInto(methodData, content),
+ details,
+ options
+ );
+ content.rq = rq; // assign it so we can retrieve it later
+
+ const handle = content.windowUtils.setHandlingUserInput(true);
+ content.showPromise = rq.show();
+
+ handle.destruct();
+ return {
+ requestId: rq.id,
+ };
+ },
+ },
+
+ DialogContentTasks: {
+ getShippingOptions: () => {
+ let picker = content.document.querySelector("shipping-option-picker");
+ let popupBox = Cu.waiveXrays(picker).dropdown.popupBox;
+ let selectedOptionIndex = popupBox.selectedIndex;
+ let selectedOption = Cu.waiveXrays(picker).dropdown.selectedOption;
+
+ let result = {
+ optionCount: popupBox.children.length,
+ selectedOptionIndex,
+ };
+ if (!selectedOption) {
+ return result;
+ }
+
+ return Object.assign(result, {
+ selectedOptionID: selectedOption.getAttribute("value"),
+ selectedOptionLabel: selectedOption.getAttribute("label"),
+ selectedOptionCurrency: selectedOption.getAttribute("amount-currency"),
+ selectedOptionValue: selectedOption.getAttribute("amount-value"),
+ });
+ },
+
+ getShippingAddresses: () => {
+ let doc = content.document;
+ let addressPicker = doc.querySelector(
+ "address-picker[selected-state-key='selectedShippingAddress']"
+ );
+ let popupBox = Cu.waiveXrays(addressPicker).dropdown.popupBox;
+ let options = Array.from(popupBox.children).map(option => {
+ return {
+ guid: option.getAttribute("guid"),
+ country: option.getAttribute("country"),
+ selected: option.selected,
+ };
+ });
+ let selectedOptionIndex = options.findIndex(item => item.selected);
+ return {
+ selectedOptionIndex,
+ options,
+ };
+ },
+
+ selectShippingAddressByCountry: country => {
+ let doc = content.document;
+ let addressPicker = doc.querySelector(
+ "address-picker[selected-state-key='selectedShippingAddress']"
+ );
+ let select = Cu.waiveXrays(addressPicker).dropdown.popupBox;
+ let option = select.querySelector(`[country="${country}"]`);
+ content.fillField(select, option.value);
+ },
+
+ selectPayerAddressByGuid: guid => {
+ let doc = content.document;
+ let addressPicker = doc.querySelector(
+ "address-picker[selected-state-key='selectedPayerAddress']"
+ );
+ let select = Cu.waiveXrays(addressPicker).dropdown.popupBox;
+ content.fillField(select, guid);
+ },
+
+ selectShippingAddressByGuid: guid => {
+ let doc = content.document;
+ let addressPicker = doc.querySelector(
+ "address-picker[selected-state-key='selectedShippingAddress']"
+ );
+ let select = Cu.waiveXrays(addressPicker).dropdown.popupBox;
+ content.fillField(select, guid);
+ },
+
+ selectShippingOptionById: value => {
+ let doc = content.document;
+ let optionPicker = doc.querySelector("shipping-option-picker");
+ let select = Cu.waiveXrays(optionPicker).dropdown.popupBox;
+ content.fillField(select, value);
+ },
+
+ selectPaymentOptionByGuid: guid => {
+ let doc = content.document;
+ let methodPicker = doc.querySelector("payment-method-picker");
+ let select = Cu.waiveXrays(methodPicker).dropdown.popupBox;
+ content.fillField(select, guid);
+ },
+
+ /**
+ * Click the primary button for the current page
+ *
+ * Don't await on this method from a ContentTask when expecting the dialog to close
+ *
+ * @returns {undefined}
+ */
+ clickPrimaryButton: () => {
+ let { requestStore } = Cu.waiveXrays(
+ content.document.querySelector("payment-dialog")
+ );
+ let { page } = requestStore.getState();
+ let button = content.document.querySelector(`#${page.id} button.primary`);
+ ok(
+ !button.disabled,
+ `#${page.id} primary button should not be disabled when clicking it`
+ );
+ button.click();
+ },
+
+ /**
+ * Click the cancel button
+ *
+ * Don't await on this task since the cancel can close the dialog before
+ * ContentTask can resolve the promise.
+ *
+ * @returns {undefined}
+ */
+ manuallyClickCancel: () => {
+ content.document.getElementById("cancel").click();
+ },
+
+ /**
+ * Do the minimum possible to complete the payment succesfully.
+ *
+ * Don't await on this task since the cancel can close the dialog before
+ * ContentTask can resolve the promise.
+ *
+ * @returns {undefined}
+ */
+ completePayment: async () => {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "payment-summary";
+ },
+ "Wait for change to payment-summary before clicking Pay"
+ );
+
+ let button = content.document.getElementById("pay");
+ ok(
+ !button.disabled,
+ "Pay button should not be disabled when clicking it"
+ );
+ button.click();
+ },
+
+ setSecurityCode: ({ securityCode }) => {
+ // Waive the xray to access the untrusted `securityCodeInput` property
+ let picker = Cu.waiveXrays(
+ content.document.querySelector("payment-method-picker")
+ );
+ // Unwaive to access the ChromeOnly `setUserInput` API.
+ // setUserInput dispatches changes events.
+ Cu.unwaiveXrays(picker.securityCodeInput)
+ .querySelector("input")
+ .setUserInput(securityCode);
+ },
+ },
+
+ DialogContentUtils: {
+ waitForState: async (content, stateCheckFn, msg) => {
+ const { ContentTaskUtils } = ChromeUtils.import(
+ "resource://testing-common/ContentTaskUtils.jsm"
+ );
+ let { requestStore } = Cu.waiveXrays(
+ content.document.querySelector("payment-dialog")
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => stateCheckFn(requestStore.getState()),
+ msg
+ );
+ return requestStore.getState();
+ },
+
+ getCurrentState: async content => {
+ let { requestStore } = Cu.waiveXrays(
+ content.document.querySelector("payment-dialog")
+ );
+ return requestStore.getState();
+ },
+ },
+
+ /**
+ * Common PaymentMethodData for testing
+ */
+ MethodData: {
+ basicCard: {
+ supportedMethods: "basic-card",
+ },
+ bobPay: {
+ supportedMethods: "https://www.example.com/bobpay",
+ },
+ },
+
+ /**
+ * Common PaymentDetailsInit for testing
+ */
+ Details: {
+ total2USD: {
+ total: {
+ label: "Total due",
+ amount: { currency: "USD", value: "2.00" },
+ },
+ },
+ total32USD: {
+ total: {
+ label: "Total due",
+ amount: { currency: "USD", value: "32.00" },
+ },
+ },
+ total60USD: {
+ total: {
+ label: "Total due",
+ amount: { currency: "USD", value: "60.00" },
+ },
+ },
+ total1pt75EUR: {
+ total: {
+ label: "Total due",
+ amount: { currency: "EUR", value: "1.75" },
+ },
+ },
+ total60EUR: {
+ total: {
+ label: "Total due",
+ amount: { currency: "EUR", value: "75.00" },
+ },
+ },
+ twoDisplayItems: {
+ displayItems: [
+ {
+ label: "First",
+ amount: { currency: "USD", value: "1" },
+ },
+ {
+ label: "Second",
+ amount: { currency: "USD", value: "2" },
+ },
+ ],
+ },
+ twoDisplayItemsEUR: {
+ displayItems: [
+ {
+ label: "First",
+ amount: { currency: "EUR", value: "0.85" },
+ },
+ {
+ label: "Second",
+ amount: { currency: "EUR", value: "1.70" },
+ },
+ ],
+ },
+ twoShippingOptions: {
+ shippingOptions: [
+ {
+ id: "1",
+ label: "Meh Unreliable Shipping",
+ amount: { currency: "USD", value: "1" },
+ },
+ {
+ id: "2",
+ label: "Premium Slow Shipping",
+ amount: { currency: "USD", value: "2" },
+ selected: true,
+ },
+ ],
+ },
+ twoShippingOptionsEUR: {
+ shippingOptions: [
+ {
+ id: "1",
+ label: "Sloth Shipping",
+ amount: { currency: "EUR", value: "1.01" },
+ },
+ {
+ id: "2",
+ label: "Velociraptor Shipping",
+ amount: { currency: "EUR", value: "63545.65" },
+ selected: true,
+ },
+ ],
+ },
+ noShippingOptions: {
+ shippingOptions: [],
+ },
+ bobPayPaymentModifier: {
+ modifiers: [
+ {
+ additionalDisplayItems: [
+ {
+ label: "Credit card fee",
+ amount: { currency: "USD", value: "0.50" },
+ },
+ ],
+ supportedMethods: "basic-card",
+ total: {
+ label: "Total due",
+ amount: { currency: "USD", value: "2.50" },
+ },
+ data: {},
+ },
+ {
+ additionalDisplayItems: [
+ {
+ label: "Bob-pay fee",
+ amount: { currency: "USD", value: "1.50" },
+ },
+ ],
+ supportedMethods: "https://www.example.com/bobpay",
+ total: {
+ label: "Total due",
+ amount: { currency: "USD", value: "3.50" },
+ },
+ },
+ ],
+ },
+ additionalDisplayItemsEUR: {
+ modifiers: [
+ {
+ additionalDisplayItems: [
+ {
+ label: "Handling fee",
+ amount: { currency: "EUR", value: "1.00" },
+ },
+ ],
+ supportedMethods: "basic-card",
+ total: {
+ label: "Total due",
+ amount: { currency: "EUR", value: "2.50" },
+ },
+ },
+ ],
+ },
+ noError: {
+ error: "",
+ },
+ genericShippingError: {
+ error: "Cannot ship with option 1 on days that end with Y",
+ },
+ fieldSpecificErrors: {
+ error: "There are errors related to specific parts of the address",
+ shippingAddressErrors: {
+ addressLine:
+ "Can only ship to ROADS, not DRIVES, BOULEVARDS, or STREETS",
+ city: "Can only ship to CITIES, not TOWNSHIPS or VILLAGES",
+ country: "Can only ship to USA, not CA",
+ dependentLocality: "Can only be SUBURBS, not NEIGHBORHOODS",
+ organization: "Can only ship to CORPORATIONS, not CONSORTIUMS",
+ phone: "Only allowed to ship to area codes that start with 9",
+ postalCode: "Only allowed to ship to postalCodes that start with 0",
+ recipient: "Can only ship to names that start with J",
+ region: "Can only ship to regions that start with M",
+ regionCode:
+ "Regions must be 1 to 3 characters in length (sometimes ;) )",
+ },
+ },
+ },
+
+ Options: {
+ requestShippingOption: {
+ requestShipping: true,
+ },
+ requestPayerNameAndEmail: {
+ requestPayerName: true,
+ requestPayerEmail: true,
+ },
+ requestPayerNameEmailAndPhone: {
+ requestPayerName: true,
+ requestPayerEmail: true,
+ requestPayerPhone: true,
+ },
+ },
+
+ Addresses: {
+ TimBR: {
+ "given-name": "Timothy",
+ "additional-name": "João",
+ "family-name": "Berners-Lee",
+ organization: "World Wide Web Consortium",
+ "street-address": "Rua Adalberto Pajuaba, 404",
+ "address-level3": "Campos Elísios",
+ "address-level2": "Ribeirão Preto",
+ "address-level1": "SP",
+ "postal-code": "14055-220",
+ country: "BR",
+ tel: "+0318522222222",
+ email: "timbr@example.org",
+ },
+ TimBL: {
+ "given-name": "Timothy",
+ "additional-name": "John",
+ "family-name": "Berners-Lee",
+ organization: "World Wide Web Consortium",
+ "street-address": "32 Vassar Street\nMIT Room 32-G524",
+ "address-level2": "Cambridge",
+ "address-level1": "MA",
+ "postal-code": "02139",
+ country: "US",
+ tel: "+16172535702",
+ email: "timbl@example.org",
+ },
+ TimBL2: {
+ "given-name": "Timothy",
+ "additional-name": "Johann",
+ "family-name": "Berners-Lee",
+ organization: "World Wide Web Consortium",
+ "street-address": "1 Pommes Frittes Place",
+ "address-level2": "Berlin",
+ // address-level1 isn't used in our forms for Germany
+ "postal-code": "02138",
+ country: "DE",
+ tel: "+16172535702",
+ email: "timbl@example.com",
+ },
+ /* Used as a temporary (not persisted in autofill storage) address in tests */
+ Temp: {
+ "given-name": "Temp",
+ "family-name": "McTempFace",
+ organization: "Temps Inc.",
+ "street-address": "1a Temporary Ave.",
+ "address-level2": "Temp Town",
+ "address-level1": "CA",
+ "postal-code": "31337",
+ country: "US",
+ tel: "+15032541000",
+ email: "tempie@example.com",
+ },
+ },
+
+ BasicCards: {
+ JohnDoe: {
+ "cc-exp-month": 1,
+ "cc-exp-year": new Date().getFullYear() + 9,
+ "cc-name": "John Doe",
+ "cc-number": "4111111111111111",
+ "cc-type": "visa",
+ },
+ JaneMasterCard: {
+ "cc-exp-month": 12,
+ "cc-exp-year": new Date().getFullYear() + 9,
+ "cc-name": "Jane McMaster-Card",
+ "cc-number": "5555555555554444",
+ "cc-type": "mastercard",
+ },
+ MissingFields: {
+ "cc-name": "Missy Fields",
+ "cc-number": "340000000000009",
+ },
+ Temp: {
+ "cc-exp-month": 12,
+ "cc-exp-year": new Date().getFullYear() + 9,
+ "cc-name": "Temp Name",
+ "cc-number": "5105105105105100",
+ "cc-type": "mastercard",
+ },
+ },
+};
diff --git a/browser/components/payments/test/browser/blank_page.html b/browser/components/payments/test/browser/blank_page.html
new file mode 100644
index 0000000000..2a42a20e6e
--- /dev/null
+++ b/browser/components/payments/test/browser/blank_page.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Blank page</title>
+ </head>
+ <body>
+ BLANK PAGE
+ </body>
+</html>
diff --git a/browser/components/payments/test/browser/browser.ini b/browser/components/payments/test/browser/browser.ini
new file mode 100644
index 0000000000..241d122446
--- /dev/null
+++ b/browser/components/payments/test/browser/browser.ini
@@ -0,0 +1,34 @@
+[DEFAULT]
+head = head.js
+prefs =
+ browser.pagethumbnails.capturing_disabled=true
+ dom.payments.request.enabled=true
+ extensions.formautofill.creditCards.available=true
+ extensions.formautofill.reauth.enabled=true
+skip-if = true || !e10s # Bug 1515048 - Disable for now. Bug 1365964 - Payment Request isn't implemented for non-e10s
+support-files =
+ blank_page.html
+
+[browser_address_edit.js]
+skip-if = verify && debug && os == 'mac'
+[browser_address_edit_hidden_fields.js]
+[browser_card_edit.js]
+skip-if = debug && (os == 'mac' || os == 'linux') # bug 1465673
+[browser_change_shipping.js]
+[browser_dropdowns.js]
+[browser_host_name.js]
+[browser_onboarding_wizard.js]
+[browser_openPreferences.js]
+[browser_payerRequestedFields.js]
+[browser_payment_completion.js]
+[browser_profile_storage.js]
+[browser_request_serialization.js]
+[browser_request_shipping.js]
+[browser_retry.js]
+[browser_retry_fieldErrors.js]
+[browser_shippingaddresschange_error.js]
+[browser_show_dialog.js]
+skip-if = os == 'win' && debug # bug 1418385
+[browser_tab_modal.js]
+skip-if = os == 'linux' && !debug # bug 1508577
+[browser_total.js]
diff --git a/browser/components/payments/test/browser/browser_address_edit.js b/browser/components/payments/test/browser/browser_address_edit.js
new file mode 100644
index 0000000000..6d214b086f
--- /dev/null
+++ b/browser/components/payments/test/browser/browser_address_edit.js
@@ -0,0 +1,1029 @@
+/* eslint-disable no-shadow */
+
+"use strict";
+
+async function setup() {
+ await setupFormAutofillStorage();
+ await cleanupFormAutofillStorage();
+ // add an address and card to avoid the FTU sequence
+ let prefilledGuids = await addSampleAddressesAndBasicCard(
+ [PTU.Addresses.TimBL],
+ [PTU.BasicCards.JohnDoe]
+ );
+
+ info("associating the card with the billing address");
+ await formAutofillStorage.creditCards.update(
+ prefilledGuids.card1GUID,
+ {
+ billingAddressGUID: prefilledGuids.address1GUID,
+ },
+ true
+ );
+
+ return prefilledGuids;
+}
+
+/*
+ * Test that we can correctly add an address and elect for it to be saved or temporary
+ */
+add_task(async function test_add_link() {
+ let prefilledGuids = await setup();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+ ),
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ info("setup got prefilledGuids: " + JSON.stringify(prefilledGuids));
+ await spawnPaymentDialogTask(frame, async () => {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let {
+ tempAddresses,
+ savedAddresses,
+ } = await PTU.DialogContentUtils.getCurrentState(content);
+ is(
+ Object.keys(tempAddresses).length,
+ 0,
+ "No temporary addresses at the start of test"
+ );
+ is(
+ Object.keys(savedAddresses).length,
+ 1,
+ "1 saved address at the start of test"
+ );
+ });
+
+ let testOptions = [
+ { setPersistCheckedValue: true, expectPersist: true },
+ { setPersistCheckedValue: false, expectPersist: false },
+ ];
+ let newAddress = Object.assign({}, PTU.Addresses.TimBL2);
+ // Emails aren't part of shipping addresses
+ delete newAddress.email;
+
+ for (let options of testOptions) {
+ let shippingAddressChangePromise = SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingaddresschange",
+ },
+ ],
+ PTU.ContentTasks.awaitPaymentEventPromise
+ );
+
+ await manuallyAddShippingAddress(frame, newAddress, options);
+ await shippingAddressChangePromise;
+ info("got shippingaddresschange event");
+
+ await spawnPaymentDialogTask(
+ frame,
+ async ({ address, options, prefilledGuids }) => {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let newAddresses = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.tempAddresses && state.savedAddresses;
+ }
+ );
+ let colnName = options.expectPersist
+ ? "savedAddresses"
+ : "tempAddresses";
+ // remove any pre-filled entries
+ delete newAddresses[colnName][prefilledGuids.address1GUID];
+
+ let addressGUIDs = Object.keys(newAddresses[colnName]);
+ is(addressGUIDs.length, 1, "Check there is one address");
+ let resultAddress = newAddresses[colnName][addressGUIDs[0]];
+ for (let [key, val] of Object.entries(address)) {
+ is(resultAddress[key], val, "Check " + key);
+ }
+ },
+ { address: newAddress, options, prefilledGuids }
+ );
+ }
+
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+ await cleanupFormAutofillStorage();
+});
+
+add_task(async function test_edit_link() {
+ let prefilledGuids = await setup();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+ ),
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ let shippingAddressChangePromise = SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingaddresschange",
+ },
+ ],
+ PTU.ContentTasks.awaitPaymentEventPromise
+ );
+
+ const EXPECTED_ADDRESS = {
+ "given-name": "Jaws",
+ "family-name": "swaJ",
+ organization: "aliizoM",
+ };
+
+ info("setup got prefilledGuids: " + JSON.stringify(prefilledGuids));
+ await spawnPaymentDialogTask(frame, async () => {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let {
+ tempAddresses,
+ savedAddresses,
+ } = await PTU.DialogContentUtils.getCurrentState(content);
+ is(
+ Object.keys(tempAddresses).length,
+ 0,
+ "No temporary addresses at the start of test"
+ );
+ is(
+ Object.keys(savedAddresses).length,
+ 1,
+ "1 saved address at the start of test"
+ );
+
+ let picker = content.document.querySelector(
+ "address-picker[selected-state-key='selectedShippingAddress']"
+ );
+ Cu.waiveXrays(picker).dropdown.popupBox.focus();
+ EventUtils.synthesizeKey(
+ PTU.Addresses.TimBL["given-name"],
+ {},
+ content.window
+ );
+
+ let editLink = content.document.querySelector(
+ "address-picker .edit-link"
+ );
+ is(editLink.textContent, "Edit", "Edit link text");
+
+ editLink.click();
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ state.page.id == "shipping-address-page" &&
+ !!state["shipping-address-page"].guid
+ );
+ },
+ "Check edit page state"
+ );
+
+ let addressForm = content.document.querySelector(
+ "#shipping-address-page"
+ );
+ ok(content.isVisible(addressForm), "Shipping address form is visible");
+
+ let title = addressForm.querySelector("h2");
+ is(
+ title.textContent,
+ "Edit Shipping Address",
+ "Page title should be set"
+ );
+
+ let saveButton = addressForm.querySelector(".save-button");
+ is(
+ saveButton.textContent,
+ "Update",
+ "Save button has the correct label"
+ );
+ });
+
+ let editOptions = {
+ checkboxSelector: "#shipping-address-page .persist-checkbox",
+ isEditing: true,
+ expectPersist: true,
+ };
+ await fillInShippingAddressForm(frame, EXPECTED_ADDRESS, editOptions);
+ await verifyPersistCheckbox(frame, editOptions);
+ await submitAddressForm(frame, EXPECTED_ADDRESS, editOptions);
+
+ await spawnPaymentDialogTask(
+ frame,
+ async address => {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ let addresses = Object.entries(state.savedAddresses);
+ return (
+ addresses.length == 1 &&
+ addresses[0][1]["given-name"] == address["given-name"]
+ );
+ },
+ "Check address was edited"
+ );
+
+ let addressGUIDs = Object.keys(state.savedAddresses);
+ is(addressGUIDs.length, 1, "Check there is still one address");
+ let savedAddress = state.savedAddresses[addressGUIDs[0]];
+ for (let [key, val] of Object.entries(address)) {
+ is(savedAddress[key], val, "Check updated " + key);
+ }
+ ok(savedAddress.guid, "Address has a guid");
+ ok(savedAddress.name, "Address has a name");
+ ok(
+ savedAddress.name.includes(address["given-name"]) &&
+ savedAddress.name.includes(address["family-name"]),
+ "Address.name was computed"
+ );
+
+ state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "payment-summary";
+ },
+ "Switched back to payment-summary"
+ );
+ },
+ EXPECTED_ADDRESS
+ );
+
+ await shippingAddressChangePromise;
+ info("got shippingaddresschange event");
+
+ info("clicking cancel");
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+ await cleanupFormAutofillStorage();
+});
+
+add_task(async function test_add_payer_contact_name_email_link() {
+ await setup();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: PTU.Details.total60USD,
+ options: PTU.Options.requestPayerNameAndEmail,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ const EXPECTED_ADDRESS = {
+ "given-name": "Deraj",
+ "family-name": "Niew",
+ email: "test@example.com",
+ };
+
+ const addOptions = {
+ addLinkSelector: "address-picker.payer-related .add-link",
+ checkboxSelector: "#payer-address-page .persist-checkbox",
+ initialPageId: "payment-summary",
+ addressPageId: "payer-address-page",
+ expectPersist: true,
+ };
+
+ await spawnPaymentDialogTask(frame, async () => {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let {
+ tempAddresses,
+ savedAddresses,
+ } = await PTU.DialogContentUtils.getCurrentState(content);
+ is(
+ Object.keys(tempAddresses).length,
+ 0,
+ "No temporary addresses at the start of test"
+ );
+ is(
+ Object.keys(savedAddresses).length,
+ 1,
+ "1 saved address at the start of test"
+ );
+ });
+
+ await navigateToAddAddressPage(frame, addOptions);
+
+ await spawnPaymentDialogTask(frame, async () => {
+ let addressForm = content.document.querySelector("#payer-address-page");
+ ok(content.isVisible(addressForm), "Payer address form is visible");
+
+ let title = addressForm.querySelector("address-form h2");
+ is(title.textContent, "Add Payer Contact", "Page title should be set");
+
+ let saveButton = addressForm.querySelector("address-form .save-button");
+ is(saveButton.textContent, "Next", "Save button has the correct label");
+
+ info("check that non-payer requested fields are hidden");
+ for (let selector of ["#organization", "#tel"]) {
+ let element = addressForm.querySelector(selector);
+ ok(content.isHidden(element), selector + " should be hidden");
+ }
+ });
+
+ await fillInPayerAddressForm(frame, EXPECTED_ADDRESS, addOptions);
+ await verifyPersistCheckbox(frame, addOptions);
+ await submitAddressForm(frame, EXPECTED_ADDRESS, addOptions);
+
+ await spawnPaymentDialogTask(
+ frame,
+ async address => {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let { savedAddresses } = await PTU.DialogContentUtils.getCurrentState(
+ content
+ );
+
+ let addressGUIDs = Object.keys(savedAddresses);
+ is(addressGUIDs.length, 2, "Check there are now 2 addresses");
+ let savedAddress = savedAddresses[addressGUIDs[1]];
+ for (let [key, val] of Object.entries(address)) {
+ is(savedAddress[key], val, "Check " + key);
+ }
+ ok(savedAddress.guid, "Address has a guid");
+ ok(savedAddress.name, "Address has a name");
+ ok(
+ savedAddress.name.includes(address["given-name"]) &&
+ savedAddress.name.includes(address["family-name"]),
+ "Address.name was computed"
+ );
+ },
+ EXPECTED_ADDRESS
+ );
+
+ info("clicking cancel");
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
+
+add_task(async function test_edit_payer_contact_name_email_phone_link() {
+ await setup();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: PTU.Details.total60USD,
+ options: PTU.Options.requestPayerNameEmailAndPhone,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ const EXPECTED_ADDRESS = {
+ "given-name": "Deraj",
+ "family-name": "Niew",
+ email: "test@example.com",
+ tel: "+15555551212",
+ };
+ const editOptions = {
+ checkboxSelector: "#payer-address-page .persist-checkbox",
+ initialPageId: "payment-summary",
+ addressPageId: "payer-address-page",
+ expectPersist: true,
+ isEditing: true,
+ };
+
+ await spawnPaymentDialogTask(
+ frame,
+ async address => {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return Object.keys(state.savedAddresses).length == 1;
+ },
+ "One saved addresses when starting test"
+ );
+
+ let editLink = content.document.querySelector(
+ "address-picker.payer-related .edit-link"
+ );
+ is(editLink.textContent, "Edit", "Edit link text");
+
+ editLink.click();
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ state.page.id == "payer-address-page" &&
+ !!state["payer-address-page"].guid
+ );
+ },
+ "Check edit page state"
+ );
+
+ let addressForm = content.document.querySelector(
+ "#payer-address-page"
+ );
+ ok(content.isVisible(addressForm), "Payer address form is visible");
+
+ let title = addressForm.querySelector("h2");
+ is(
+ title.textContent,
+ "Edit Payer Contact",
+ "Page title should be set"
+ );
+
+ let saveButton = addressForm.querySelector(".save-button");
+ is(
+ saveButton.textContent,
+ "Update",
+ "Save button has the correct label"
+ );
+
+ info("check that non-payer requested fields are hidden");
+ let formElements = addressForm.querySelectorAll(
+ ":is(input, select, textarea"
+ );
+ let allowedFields = [
+ "given-name",
+ "additional-name",
+ "family-name",
+ "email",
+ "tel",
+ ];
+ for (let element of formElements) {
+ let shouldBeVisible = allowedFields.includes(element.id);
+ if (shouldBeVisible) {
+ ok(content.isVisible(element), element.id + " should be visible");
+ } else {
+ ok(content.isHidden(element), element.id + " should be hidden");
+ }
+ }
+
+ info("overwriting field values");
+ for (let [key, val] of Object.entries(address)) {
+ let field = addressForm.querySelector(`#${key}`);
+ field.value = val + "1";
+ ok(!field.disabled, `Field #${key} shouldn't be disabled`);
+ }
+ },
+ EXPECTED_ADDRESS
+ );
+
+ await verifyPersistCheckbox(frame, editOptions);
+ await submitAddressForm(frame, EXPECTED_ADDRESS, editOptions);
+
+ await spawnPaymentDialogTask(
+ frame,
+ async address => {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ let addresses = Object.entries(state.savedAddresses);
+ return (
+ addresses.length == 1 &&
+ addresses[0][1]["given-name"] == address["given-name"] + "1"
+ );
+ },
+ "Check address was edited"
+ );
+
+ let addressGUIDs = Object.keys(state.savedAddresses);
+ is(addressGUIDs.length, 1, "Check there is still one address");
+ let savedAddress = state.savedAddresses[addressGUIDs[0]];
+ for (let [key, val] of Object.entries(address)) {
+ is(savedAddress[key], val + "1", "Check updated " + key);
+ }
+ ok(savedAddress.guid, "Address has a guid");
+ ok(savedAddress.name, "Address has a name");
+ ok(
+ savedAddress.name.includes(address["given-name"]) &&
+ savedAddress.name.includes(address["family-name"]),
+ "Address.name was computed"
+ );
+ },
+ EXPECTED_ADDRESS
+ );
+
+ info("clicking cancel");
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
+
+add_task(async function test_shipping_address_picker() {
+ await setup();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: PTU.Details.total60USD,
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ async function test_picker_option_label(address) {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+ const { FormAutofillUtils } = ChromeUtils.import(
+ "resource://formautofill/FormAutofillUtils.jsm"
+ );
+
+ let state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return Object.keys(state.savedAddresses).length == 1;
+ },
+ "One saved addresses when starting test"
+ );
+ let savedAddress = Object.values(state.savedAddresses)[0];
+
+ let selector =
+ "address-picker[selected-state-key='selectedShippingAddress']";
+ let picker = content.document.querySelector(selector);
+ let option = Cu.waiveXrays(picker).dropdown.popupBox.children[0];
+ is(
+ option.textContent,
+ FormAutofillUtils.getAddressLabel(savedAddress, null),
+ "Shows correct shipping option label"
+ );
+ }
+ );
+
+ info("clicking cancel");
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
+
+add_task(async function test_payer_address_picker() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: PTU.Details.total60USD,
+ options: PTU.Options.requestPayerNameEmailAndPhone,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ async function test_picker_option_label(address) {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+ const { FormAutofillUtils } = ChromeUtils.import(
+ "resource://formautofill/FormAutofillUtils.jsm"
+ );
+
+ let state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return Object.keys(state.savedAddresses).length == 1;
+ },
+ "One saved addresses when starting test"
+ );
+ let savedAddress = Object.values(state.savedAddresses)[0];
+
+ let selector =
+ "address-picker[selected-state-key='selectedPayerAddress']";
+ let picker = content.document.querySelector(selector);
+ let option = Cu.waiveXrays(picker).dropdown.popupBox.children[0];
+ is(
+ option.textContent.includes("32 Vassar Street"),
+ false,
+ "Payer option label does not contain street address"
+ );
+ is(
+ option.textContent,
+ FormAutofillUtils.getAddressLabel(savedAddress, "name tel email"),
+ "Shows correct payer option label"
+ );
+ }
+ );
+
+ info("clicking cancel");
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
+
+/*
+ * Test that we can correctly add an address from a private window
+ */
+add_task(async function test_private_persist_addresses() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(false, "Cannot test OS key store login on official builds.");
+ return;
+ }
+ let prefilledGuids = await setup();
+
+ is(
+ (await formAutofillStorage.addresses.getAll()).length,
+ 1,
+ "Setup results in 1 stored address at start of test"
+ );
+
+ await withNewTabInPrivateWindow(
+ {
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ info("in new tab w. private window");
+ let { frame } =
+ // setupPaymentDialog from a private window.
+ await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+ ),
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+ info("/setupPaymentDialog");
+
+ let addressToAdd = Object.assign({}, PTU.Addresses.Temp);
+ // Emails aren't part of shipping addresses
+ delete addressToAdd.email;
+ const addOptions = {
+ addLinkSelector: "address-picker.shipping-related .add-link",
+ checkboxSelector: "#shipping-address-page .persist-checkbox",
+ initialPageId: "payment-summary",
+ addressPageId: "shipping-address-page",
+ expectPersist: false,
+ isPrivate: true,
+ };
+
+ await navigateToAddAddressPage(frame, addOptions);
+ await spawnPaymentDialogTask(frame, async () => {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let state = await PTU.DialogContentUtils.getCurrentState(content);
+ info("on address-page and state.isPrivate: " + state.isPrivate);
+ ok(
+ state.isPrivate,
+ "isPrivate flag is set when paymentrequest is shown in a private session"
+ );
+ });
+
+ info("wait for initialAddresses");
+ let initialAddresses = await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.getShippingAddresses
+ );
+ is(
+ initialAddresses.options.length,
+ 1,
+ "Got expected number of pre-filled shipping addresses"
+ );
+
+ await fillInShippingAddressForm(frame, addressToAdd, addOptions);
+ await verifyPersistCheckbox(frame, addOptions);
+ await submitAddressForm(frame, addressToAdd, addOptions);
+
+ let shippingAddresses = await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.getShippingAddresses
+ );
+ info("shippingAddresses", shippingAddresses);
+ let addressOptions = shippingAddresses.options;
+ // expect the prefilled address + the new temporary address
+ is(
+ addressOptions.length,
+ 2,
+ "The picker has the expected number of address options"
+ );
+ let tempAddressOption = addressOptions.find(
+ addr => addr.guid != prefilledGuids.address1GUID
+ );
+ let tempAddressGuid = tempAddressOption.guid;
+ // select the new address
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.selectShippingAddressByGuid,
+ tempAddressGuid
+ );
+
+ info("awaiting the shippingaddresschange event");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingaddresschange",
+ },
+ ],
+ PTU.ContentTasks.awaitPaymentEventPromise
+ );
+
+ await spawnPaymentDialogTask(
+ frame,
+ async args => {
+ let { address, tempAddressGuid, prefilledGuids: guids } = args;
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.selectedShippingAddress == tempAddressGuid;
+ },
+ "Check the temp address is the selectedShippingAddress"
+ );
+
+ let addressGUIDs = Object.keys(state.tempAddresses);
+ is(addressGUIDs.length, 1, "Check there is one address");
+
+ is(
+ addressGUIDs[0],
+ tempAddressGuid,
+ "guid from state and picker options match"
+ );
+ let tempAddress = state.tempAddresses[tempAddressGuid];
+ for (let [key, val] of Object.entries(address)) {
+ is(tempAddress[key], val, "Check field " + key);
+ }
+ ok(tempAddress.guid, "Address has a guid");
+ ok(tempAddress.name, "Address has a name");
+ ok(
+ tempAddress.name.includes(address["given-name"]) &&
+ tempAddress.name.includes(address["family-name"]),
+ "Address.name was computed"
+ );
+
+ let paymentMethodPicker = content.document.querySelector(
+ "payment-method-picker"
+ );
+ content.fillField(
+ Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
+ guids.card1GUID
+ );
+ },
+ { address: addressToAdd, tempAddressGuid, prefilledGuids }
+ );
+
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.setSecurityCode,
+ {
+ securityCode: "123",
+ }
+ );
+
+ info("clicking pay");
+ await loginAndCompletePayment(frame);
+
+ // Add a handler to complete the payment above.
+ info("acknowledging the completion from the merchant page");
+ let result = await SpecialPowers.spawn(
+ browser,
+ [],
+ PTU.ContentTasks.addCompletionHandler
+ );
+
+ // Verify response has the expected properties
+ info("response: " + JSON.stringify(result.response));
+ let responseAddress = result.response.shippingAddress;
+ ok(responseAddress, "response should contain the shippingAddress");
+ ok(
+ responseAddress.recipient.includes(addressToAdd["given-name"]),
+ "Check given-name matches recipient in response"
+ );
+ ok(
+ responseAddress.recipient.includes(addressToAdd["family-name"]),
+ "Check family-name matches recipient in response"
+ );
+ is(
+ responseAddress.addressLine[0],
+ addressToAdd["street-address"],
+ "Check street-address in response"
+ );
+ is(
+ responseAddress.country,
+ addressToAdd.country,
+ "Check country in response"
+ );
+ }
+ );
+ // verify formautofill store doesnt have the new temp addresses
+ is(
+ (await formAutofillStorage.addresses.getAll()).length,
+ 1,
+ "Original 1 stored address at end of test"
+ );
+});
+
+add_task(async function test_countrySpecificFieldsGetRequiredness() {
+ await setup();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+ ),
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ let addOptions = {
+ addLinkSelector: "address-picker.shipping-related .add-link",
+ checkboxSelector: "#shipping-address-page .persist-checkbox",
+ initialPageId: "payment-summary",
+ addressPageId: "shipping-address-page",
+ expectPersist: true,
+ };
+
+ await navigateToAddAddressPage(frame, addOptions);
+
+ const EXPECTED_ADDRESS = {
+ country: "MO",
+ "given-name": "First",
+ "family-name": "Last",
+ "street-address": "12345 FooFoo Bar",
+ };
+ await fillInShippingAddressForm(frame, EXPECTED_ADDRESS, addOptions);
+ await submitAddressForm(frame, EXPECTED_ADDRESS, addOptions);
+
+ await navigateToAddAddressPage(frame, addOptions);
+
+ await selectPaymentDialogShippingAddressByCountry(frame, "MO");
+
+ await spawnPaymentDialogTask(frame, async () => {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let editLink = content.document.querySelector(
+ "address-picker.shipping-related .edit-link"
+ );
+ is(editLink.textContent, "Edit", "Edit link text");
+
+ editLink.click();
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ state.page.id == "shipping-address-page" &&
+ !!state["shipping-address-page"].guid
+ );
+ },
+ "Check edit page state"
+ );
+
+ let addressForm = content.document.getElementById(
+ "shipping-address-page"
+ );
+ let provinceField = addressForm.querySelector("#address-level1");
+ let provinceContainer = provinceField.parentNode;
+ is(
+ provinceContainer.style.display,
+ "none",
+ "Province should be hidden for Macau"
+ );
+
+ let countryField = addressForm.querySelector("#country");
+ await content.fillField(countryField, "CA");
+ info("changed selected country to Canada");
+
+ isnot(
+ provinceContainer.style.display,
+ "none",
+ "Province should be visible for Canada"
+ );
+ ok(
+ provinceContainer.hasAttribute("required"),
+ "Province container should have required attribute"
+ );
+ let provinceSpan = provinceContainer.querySelector("span");
+ is(
+ provinceSpan.getAttribute("fieldRequiredSymbol"),
+ "*",
+ "Province span should have asterisk as fieldRequiredSymbol"
+ );
+ is(
+ content.window.getComputedStyle(provinceSpan, "::after").content,
+ "attr(fieldRequiredSymbol)",
+ "Asterisk should be on Province"
+ );
+
+ let addressBackButton = addressForm.querySelector(".back-button");
+ addressBackButton.click();
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "payment-summary";
+ },
+ "Switched back to payment-summary"
+ );
+ });
+
+ info("clicking cancel");
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+ await cleanupFormAutofillStorage();
+});
diff --git a/browser/components/payments/test/browser/browser_address_edit_hidden_fields.js b/browser/components/payments/test/browser/browser_address_edit_hidden_fields.js
new file mode 100644
index 0000000000..859bde1b10
--- /dev/null
+++ b/browser/components/payments/test/browser/browser_address_edit_hidden_fields.js
@@ -0,0 +1,477 @@
+/**
+ * Test saving/updating address records with fields sometimes not visible to the user.
+ */
+
+"use strict";
+
+async function setup() {
+ await setupFormAutofillStorage();
+ await cleanupFormAutofillStorage();
+ // add an address and card to avoid the FTU sequence
+ let prefilledGuids = await addSampleAddressesAndBasicCard(
+ [PTU.Addresses.TimBL],
+ [PTU.BasicCards.JohnDoe]
+ );
+
+ info("associating the card with the billing address");
+ await formAutofillStorage.creditCards.update(
+ prefilledGuids.card1GUID,
+ {
+ billingAddressGUID: prefilledGuids.address1GUID,
+ },
+ true
+ );
+
+ return prefilledGuids;
+}
+
+add_task(async function test_hiddenFieldNotSaved() {
+ await setup();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+ ),
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ let newAddress = Object.assign({}, PTU.Addresses.TimBL);
+ newAddress["given-name"] = "hiddenFields";
+
+ let shippingAddressChangePromise = SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingaddresschange",
+ },
+ ],
+ PTU.ContentTasks.awaitPaymentEventPromise
+ );
+
+ let options = {
+ addLinkSelector: "address-picker.shipping-related .add-link",
+ initialPageId: "payment-summary",
+ addressPageId: "shipping-address-page",
+ expectPersist: true,
+ isEditing: false,
+ };
+ await navigateToAddAddressPage(frame, options);
+ info("navigated to add address page");
+ await fillInShippingAddressForm(frame, newAddress, options);
+ info("filled in TimBL address");
+
+ await spawnPaymentDialogTask(frame, async args => {
+ let addressForm = content.document.getElementById(
+ "shipping-address-page"
+ );
+ let countryField = addressForm.querySelector("#country");
+ await content.fillField(countryField, "DE");
+ });
+ info("changed selected country to Germany");
+
+ await submitAddressForm(frame, null, options);
+ await shippingAddressChangePromise;
+ info("got shippingaddresschange event");
+
+ await spawnPaymentDialogTask(frame, async () => {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let { savedAddresses } = await PTU.DialogContentUtils.getCurrentState(
+ content
+ );
+ is(Object.keys(savedAddresses).length, 2, "2 saved addresses");
+ let savedAddress = Object.values(savedAddresses).find(
+ address => address["given-name"] == "hiddenFields"
+ );
+ ok(savedAddress, "found the saved address");
+ is(savedAddress.country, "DE", "check country");
+ is(
+ savedAddress["address-level2"],
+ PTU.Addresses.TimBL["address-level2"],
+ "check address-level2"
+ );
+ is(
+ savedAddress["address-level1"],
+ undefined,
+ "address-level1 should not be saved"
+ );
+ });
+
+ info("clicking cancel");
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+ await cleanupFormAutofillStorage();
+});
+
+add_task(async function test_hiddenFieldRemovedWhenCountryChanged() {
+ await setup();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+ ),
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ let shippingAddressChangePromise = SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingaddresschange",
+ },
+ ],
+ PTU.ContentTasks.awaitPaymentEventPromise
+ );
+
+ await spawnPaymentDialogTask(frame, async args => {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let picker = content.document.querySelector(
+ "address-picker[selected-state-key='selectedShippingAddress']"
+ );
+ Cu.waiveXrays(picker).dropdown.popupBox.focus();
+ EventUtils.synthesizeKey(
+ PTU.Addresses.TimBL["given-name"],
+ {},
+ content.window
+ );
+
+ let editLink = content.document.querySelector(
+ "address-picker .edit-link"
+ );
+ is(editLink.textContent, "Edit", "Edit link text");
+
+ editLink.click();
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ state.page.id == "shipping-address-page" &&
+ !!state["shipping-address-page"].guid
+ );
+ },
+ "Check edit page state"
+ );
+
+ let addressForm = content.document.getElementById(
+ "shipping-address-page"
+ );
+ let countryField = addressForm.querySelector("#country");
+ await content.fillField(countryField, "DE");
+ info("changed selected country to Germany");
+ });
+
+ let options = {
+ isEditing: true,
+ addressPageId: "shipping-address-page",
+ };
+ await submitAddressForm(frame, null, options);
+ await shippingAddressChangePromise;
+ info("got shippingaddresschange event");
+
+ await spawnPaymentDialogTask(frame, async () => {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let { savedAddresses } = await PTU.DialogContentUtils.getCurrentState(
+ content
+ );
+ is(Object.keys(savedAddresses).length, 1, "1 saved address");
+ let savedAddress = Object.values(savedAddresses)[0];
+ is(savedAddress.country, "DE", "check country");
+ is(
+ savedAddress["address-level2"],
+ PTU.Addresses.TimBL["address-level2"],
+ "check address-level2"
+ );
+ is(
+ savedAddress["address-level1"],
+ undefined,
+ "address-level1 should not be saved"
+ );
+ });
+
+ info("clicking cancel");
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+ await cleanupFormAutofillStorage();
+});
+
+add_task(async function test_hiddenNonShippingFieldsPreservedUponEdit() {
+ await setupFormAutofillStorage();
+ await cleanupFormAutofillStorage();
+ // add a Brazilian address (since it uses all fields) and card to avoid the FTU sequence
+ let prefilledGuids = await addSampleAddressesAndBasicCard(
+ [PTU.Addresses.TimBR],
+ [PTU.BasicCards.JohnDoe]
+ );
+ let expected = await formAutofillStorage.addresses.get(
+ prefilledGuids.address1GUID
+ );
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+ ),
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await selectPaymentDialogShippingAddressByCountry(frame, "BR");
+
+ await navigateToAddShippingAddressPage(frame, {
+ addLinkSelector:
+ 'address-picker[selected-state-key="selectedShippingAddress"] .edit-link',
+ addressPageId: "shipping-address-page",
+ initialPageId: "payment-summary",
+ });
+
+ await spawnPaymentDialogTask(frame, async () => {
+ let givenNameField = content.document.querySelector(
+ "#shipping-address-page #given-name"
+ );
+ await content.fillField(givenNameField, "Timothy-edit");
+ });
+
+ let options = {
+ isEditing: true,
+ };
+ await submitAddressForm(frame, null, options);
+
+ info("clicking cancel");
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+
+ Object.assign(expected, PTU.Addresses.TimBR, {
+ "given-name": "Timothy-edit",
+ name: "Timothy-edit Jo\u{00E3}o Berners-Lee",
+ });
+ let actual = await formAutofillStorage.addresses.get(
+ prefilledGuids.address1GUID
+ );
+ isnot(actual.email, "", "Check email isn't empty");
+ // timeLastModified changes and isn't relevant
+ delete actual.timeLastModified;
+ delete expected.timeLastModified;
+ SimpleTest.isDeeply(actual, expected, "Check profile didn't lose fields");
+
+ await cleanupFormAutofillStorage();
+});
+
+add_task(async function test_hiddenNonPayerFieldsPreservedUponEdit() {
+ await setupFormAutofillStorage();
+ await cleanupFormAutofillStorage();
+ // add a Brazilian address (since it uses all fields) and card to avoid the FTU sequence
+ let prefilledGuids = await addSampleAddressesAndBasicCard(
+ [PTU.Addresses.TimBR],
+ [PTU.BasicCards.JohnDoe]
+ );
+ let expected = await formAutofillStorage.addresses.get(
+ prefilledGuids.address1GUID
+ );
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign({}, PTU.Details.total2USD),
+ options: {
+ requestPayerEmail: true,
+ },
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await navigateToAddAddressPage(frame, {
+ addLinkSelector:
+ 'address-picker[selected-state-key="selectedPayerAddress"] .edit-link',
+ initialPageId: "payment-summary",
+ addressPageId: "payer-address-page",
+ });
+
+ info("Change the email address");
+ await spawnPaymentDialogTask(
+ frame,
+ `async () => {
+ let emailField = content.document.querySelector("#payer-address-page #email");
+ await content.fillField(emailField, "new@example.com");
+ }`
+ );
+
+ let options = {
+ isEditing: true,
+ };
+ await submitAddressForm(frame, null, options);
+
+ info("clicking cancel");
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+
+ Object.assign(expected, PTU.Addresses.TimBR, {
+ email: "new@example.com",
+ });
+ let actual = await formAutofillStorage.addresses.get(
+ prefilledGuids.address1GUID
+ );
+ // timeLastModified changes and isn't relevant
+ delete actual.timeLastModified;
+ delete expected.timeLastModified;
+ SimpleTest.isDeeply(
+ actual,
+ expected,
+ "Check profile didn't lose fields and change was made"
+ );
+
+ await cleanupFormAutofillStorage();
+});
+
+add_task(async function test_hiddenNonBillingAddressFieldsPreservedUponEdit() {
+ await setupFormAutofillStorage();
+ await cleanupFormAutofillStorage();
+ // add a Brazilian address (since it uses all fields) and card to avoid the FTU sequence
+ let prefilledGuids = await addSampleAddressesAndBasicCard(
+ [PTU.Addresses.TimBR],
+ [PTU.BasicCards.JohnDoe]
+ );
+ let expected = await formAutofillStorage.addresses.get(
+ prefilledGuids.address1GUID
+ );
+
+ info("associating the card with the billing address");
+ await formAutofillStorage.creditCards.update(
+ prefilledGuids.card1GUID,
+ {
+ billingAddressGUID: prefilledGuids.address1GUID,
+ },
+ true
+ );
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+ ),
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await navigateToAddCardPage(frame, {
+ addLinkSelector: "payment-method-picker .edit-link",
+ });
+
+ await navigateToAddAddressPage(frame, {
+ addLinkSelector: ".billingAddressRow .edit-link",
+ initialPageId: "basic-card-page",
+ addressPageId: "billing-address-page",
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ `async () => {
+ let givenNameField = content.document.querySelector(
+ "#billing-address-page #given-name"
+ );
+ await content.fillField(givenNameField, "Timothy-edit");
+ }`
+ );
+
+ let options = {
+ isEditing: true,
+ nextPageId: "basic-card-page",
+ };
+ await submitAddressForm(frame, null, options);
+
+ info("clicking cancel");
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+
+ Object.assign(expected, PTU.Addresses.TimBR, {
+ "given-name": "Timothy-edit",
+ name: "Timothy-edit Jo\u{00E3}o Berners-Lee",
+ });
+ let actual = await formAutofillStorage.addresses.get(
+ prefilledGuids.address1GUID
+ );
+ // timeLastModified changes and isn't relevant
+ delete actual.timeLastModified;
+ delete expected.timeLastModified;
+ SimpleTest.isDeeply(actual, expected, "Check profile didn't lose fields");
+
+ await cleanupFormAutofillStorage();
+});
diff --git a/browser/components/payments/test/browser/browser_card_edit.js b/browser/components/payments/test/browser/browser_card_edit.js
new file mode 100644
index 0000000000..9ffc6c22ff
--- /dev/null
+++ b/browser/components/payments/test/browser/browser_card_edit.js
@@ -0,0 +1,1227 @@
+/* eslint-disable no-shadow */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+async function setup(addresses = [], cards = []) {
+ await setupFormAutofillStorage();
+ await cleanupFormAutofillStorage();
+ let prefilledGuids = await addSampleAddressesAndBasicCard(addresses, cards);
+ return prefilledGuids;
+}
+
+async function add_link(aOptions = {}) {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(false, "Cannot test OS key store login on official builds.");
+ return;
+ }
+ let tabOpenFn = aOptions.isPrivate
+ ? withNewTabInPrivateWindow
+ : BrowserTestUtils.withNewTab;
+ await tabOpenFn(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign({}, PTU.Details.total60USD),
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+ info("add_link, aOptions: " + JSON.stringify(aOptions, null, 2));
+ await navigateToAddCardPage(frame);
+ info(`add_link, from the add card page,
+ verifyPersistCheckbox with expectPersist: ${aOptions.expectDefaultCardPersist}`);
+ await verifyPersistCheckbox(frame, {
+ checkboxSelector: "basic-card-form .persist-checkbox",
+ expectPersist: aOptions.expectDefaultCardPersist,
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ async function checkState(testArgs = {}) {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ Object.keys(state.savedBasicCards).length == 1 &&
+ Object.keys(state.savedAddresses).length == 1
+ );
+ },
+ "Check no cards or addresses present at beginning of test"
+ );
+
+ let title = content.document.querySelector("basic-card-form h2");
+ is(title.textContent, "Add Credit Card", "Add title should be set");
+
+ let saveButton = content.document.querySelector(
+ "basic-card-form .save-button"
+ );
+ is(
+ saveButton.textContent,
+ "Next",
+ "Save button has the correct label"
+ );
+
+ is(
+ state.isPrivate,
+ testArgs.isPrivate,
+ "isPrivate flag has expected value when shown from a private/non-private session"
+ );
+ },
+ aOptions
+ );
+
+ let cardOptions = Object.assign(
+ {},
+ {
+ checkboxSelector: "basic-card-form .persist-checkbox",
+ expectPersist: aOptions.expectCardPersist,
+ networkSelector: "basic-card-form #cc-type",
+ expectedNetwork: PTU.BasicCards.JaneMasterCard["cc-type"],
+ }
+ );
+ if (aOptions.hasOwnProperty("setCardPersistCheckedValue")) {
+ cardOptions.setPersistCheckedValue =
+ aOptions.setCardPersistCheckedValue;
+ }
+ await fillInCardForm(
+ frame,
+ {
+ ["cc-csc"]: 123,
+ ...PTU.BasicCards.JaneMasterCard,
+ },
+ cardOptions
+ );
+
+ await verifyCardNetwork(frame, cardOptions);
+ await verifyPersistCheckbox(frame, cardOptions);
+
+ await spawnPaymentDialogTask(
+ frame,
+ async function checkBillingAddressPicker(testArgs = {}) {
+ let billingAddressSelect = content.document.querySelector(
+ "#billingAddressGUID"
+ );
+ ok(
+ content.isVisible(billingAddressSelect),
+ "The billing address selector should always be visible"
+ );
+ is(
+ billingAddressSelect.childElementCount,
+ 2,
+ "Only 2 child options should exist by default"
+ );
+ is(
+ billingAddressSelect.children[0].value,
+ "",
+ "The first option should be the blank/empty option"
+ );
+ ok(
+ billingAddressSelect.children[1].value,
+ "The 2nd option is the prefilled address and should be truthy"
+ );
+ },
+ aOptions
+ );
+
+ let addressOptions = Object.assign({}, aOptions, {
+ addLinkSelector: ".billingAddressRow .add-link",
+ checkboxSelector: "#billing-address-page .persist-checkbox",
+ initialPageId: "basic-card-page",
+ addressPageId: "billing-address-page",
+ expectPersist: aOptions.expectDefaultAddressPersist,
+ });
+ if (aOptions.hasOwnProperty("setAddressPersistCheckedValue")) {
+ addressOptions.setPersistCheckedValue =
+ aOptions.setAddressPersistCheckedValue;
+ }
+
+ await navigateToAddAddressPage(frame, addressOptions);
+
+ await spawnPaymentDialogTask(
+ frame,
+ async function checkTask(testArgs = {}) {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let title = content.document.querySelector("basic-card-form h2");
+ let card = Object.assign({}, PTU.BasicCards.JaneMasterCard);
+
+ let addressForm = content.document.querySelector(
+ "#billing-address-page"
+ );
+ ok(content.isVisible(addressForm), "Billing address page is visible");
+
+ let addressTitle = addressForm.querySelector("h2");
+ is(
+ addressTitle.textContent,
+ "Add Billing Address",
+ "Address on add address page should be correct"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ let total =
+ Object.keys(state.savedBasicCards).length +
+ Object.keys(state.tempBasicCards).length;
+ return total == 1;
+ },
+ "Check card was not added when clicking the 'add' address button"
+ );
+
+ let addressBackButton = addressForm.querySelector(".back-button");
+ addressBackButton.click();
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ let total =
+ Object.keys(state.savedBasicCards).length +
+ Object.keys(state.tempBasicCards).length;
+ return (
+ state.page.id == "basic-card-page" &&
+ !state["basic-card-page"].guid &&
+ total == 1
+ );
+ },
+ "Check basic-card page, but card should not be saved and no new addresses present"
+ );
+
+ is(
+ title.textContent,
+ "Add Credit Card",
+ "Add title should be still be on credit card page"
+ );
+
+ for (let [key, val] of Object.entries(card)) {
+ let field = content.document.getElementById(key);
+ is(
+ field.value,
+ val,
+ "Field should still have previous value entered"
+ );
+ ok(!field.disabled, "Fields should still be enabled for editing");
+ }
+ },
+ aOptions
+ );
+
+ await navigateToAddAddressPage(frame, addressOptions);
+
+ await fillInBillingAddressForm(
+ frame,
+ PTU.Addresses.TimBL2,
+ addressOptions
+ );
+
+ await verifyPersistCheckbox(frame, addressOptions);
+
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.clickPrimaryButton
+ );
+
+ await spawnPaymentDialogTask(
+ frame,
+ async function checkCardPage(testArgs = {}) {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ state.page.id == "basic-card-page" &&
+ !state["basic-card-page"].guid
+ );
+ },
+ "Check address was added and we're back on basic-card page (add)"
+ );
+
+ let addressCount =
+ Object.keys(state.savedAddresses).length +
+ Object.keys(state.tempAddresses).length;
+ is(addressCount, 2, "Check address was added");
+
+ let addressColn = testArgs.expectAddressPersist
+ ? state.savedAddresses
+ : state.tempAddresses;
+
+ ok(
+ state["basic-card-page"].preserveFieldValues,
+ "preserveFieldValues should be set when coming back from address-page"
+ );
+
+ ok(
+ state["basic-card-page"].billingAddressGUID,
+ "billingAddressGUID should be set when coming back from address-page"
+ );
+
+ let billingAddressPicker = Cu.waiveXrays(
+ content.document.querySelector(
+ "basic-card-form billing-address-picker"
+ )
+ );
+
+ is(
+ billingAddressPicker.options.length,
+ 3,
+ "Three options should exist in the billingAddressPicker"
+ );
+ let selectedOption = billingAddressPicker.dropdown.selectedOption;
+ let selectedAddressGuid = selectedOption.value;
+ let lastAddress = Object.values(addressColn)[
+ Object.keys(addressColn).length - 1
+ ];
+ is(
+ selectedAddressGuid,
+ lastAddress.guid,
+ "The select should have the new address selected"
+ );
+ },
+ aOptions
+ );
+
+ cardOptions = Object.assign(
+ {},
+ {
+ checkboxSelector: "basic-card-form .persist-checkbox",
+ expectPersist: aOptions.expectCardPersist,
+ }
+ );
+
+ await verifyPersistCheckbox(frame, cardOptions);
+
+ await spawnPaymentDialogTask(
+ frame,
+ async function checkSaveButtonUpdatesOnCCNumberChange() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let button = content.document.querySelector(
+ `basic-card-form button.primary`
+ );
+ ok(!button.disabled, "Save button should not be disabled");
+
+ let field = content.document.getElementById("cc-number");
+ field.focus();
+ EventUtils.sendString("a", content.window);
+ button.focus();
+
+ ok(
+ button.disabled,
+ "Save button should be disabled with incorrect number"
+ );
+
+ field.focus();
+ content.fillField(field, PTU.BasicCards.JaneMasterCard["cc-number"]);
+ button.focus();
+
+ ok(
+ !button.disabled,
+ "Save button should be enabled with correct number"
+ );
+
+ field = content.document.getElementById("cc-csc");
+ field.focus();
+ content.fillField(field, "123");
+ button.focus();
+
+ ok(!button.disabled, "Save button should be enabled with valid CSC");
+ }
+ );
+
+ SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "paymentmethodchange",
+ },
+ ],
+ PTU.ContentTasks.promisePaymentRequestEvent
+ );
+ info("added paymentmethodchange handler");
+
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.clickPrimaryButton
+ );
+
+ info("waiting for paymentmethodchange event");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "paymentmethodchange",
+ },
+ ],
+ PTU.ContentTasks.awaitPaymentEventPromise
+ );
+
+ await spawnPaymentDialogTask(frame, async function waitForSummaryPage(
+ testArgs = {}
+ ) {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "payment-summary";
+ },
+ "Check we are back on the summary page"
+ );
+
+ let picker = Cu.waiveXrays(
+ content.document.querySelector("payment-method-picker")
+ );
+ is(
+ picker.securityCodeInput.querySelector("input").value,
+ "123",
+ "Security code should be populated using the value set from the 'add' page"
+ );
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ async function checkCardState(testArgs = {}) {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let { prefilledGuids } = testArgs;
+ let card = Object.assign({}, PTU.BasicCards.JaneMasterCard);
+ let state = await PTU.DialogContentUtils.getCurrentState(content);
+
+ let cardCount =
+ Object.keys(state.savedBasicCards).length +
+ Object.keys(state.tempBasicCards).length;
+ is(cardCount, 2, "Card was added");
+ if (testArgs.expectCardPersist) {
+ is(
+ Object.keys(state.tempBasicCards).length,
+ 0,
+ "No temporary cards addded"
+ );
+ is(
+ Object.keys(state.savedBasicCards).length,
+ 2,
+ "New card was saved"
+ );
+ } else {
+ is(
+ Object.keys(state.tempBasicCards).length,
+ 1,
+ "Card was added temporarily"
+ );
+ is(
+ Object.keys(state.savedBasicCards).length,
+ 1,
+ "No change to saved cards"
+ );
+ }
+
+ let cardCollection = testArgs.expectCardPersist
+ ? state.savedBasicCards
+ : state.tempBasicCards;
+ let addressCollection = testArgs.expectAddressPersist
+ ? state.savedAddresses
+ : state.tempAddresses;
+ let savedCardGUID = Object.keys(cardCollection).find(
+ key => key != prefilledGuids.card1GUID
+ );
+ let savedAddressGUID = Object.keys(addressCollection).find(
+ key => key != prefilledGuids.address1GUID
+ );
+ let savedCard = savedCardGUID && cardCollection[savedCardGUID];
+
+ // we should never have an un-masked cc-number in the state:
+ ok(
+ Object.values(cardCollection).every(card =>
+ card["cc-number"].startsWith("************")
+ ),
+ "All cc-numbers in state are masked"
+ );
+ card["cc-number"] = "************4444"; // Expect card number to be masked at this point
+ for (let [key, val] of Object.entries(card)) {
+ is(savedCard[key], val, "Check " + key);
+ }
+
+ is(
+ savedCard.billingAddressGUID,
+ savedAddressGUID,
+ "The saved card should be associated with the billing address"
+ );
+ },
+ aOptions
+ );
+
+ await loginAndCompletePayment(frame);
+
+ // Add a handler to complete the payment above.
+ info("acknowledging the completion from the merchant page");
+ let result = await SpecialPowers.spawn(
+ browser,
+ [],
+ PTU.ContentTasks.addCompletionHandler
+ );
+
+ // Verify response has the expected properties
+ let expectedDetails = Object.assign(
+ {
+ "cc-security-code": "123",
+ },
+ PTU.BasicCards.JaneMasterCard
+ );
+ let expectedBillingAddress = PTU.Addresses.TimBL2;
+ let cardDetails = result.response.details;
+
+ checkPaymentMethodDetailsMatchesCard(
+ cardDetails,
+ expectedDetails,
+ "Check response payment details"
+ );
+ checkPaymentAddressMatchesStorageAddress(
+ cardDetails.billingAddress,
+ expectedBillingAddress,
+ "Check response billing address"
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+}
+
+add_task(async function test_add_link() {
+ let prefilledGuids = await setup(
+ [PTU.Addresses.TimBL],
+ [PTU.BasicCards.JohnDoe]
+ );
+ let defaultPersist = Services.prefs.getBoolPref(SAVE_CREDITCARD_DEFAULT_PREF);
+
+ is(
+ defaultPersist,
+ false,
+ `Expect ${SAVE_CREDITCARD_DEFAULT_PREF} to default to false`
+ );
+ info("Calling add_link from test_add_link");
+ await add_link({
+ isPrivate: false,
+ expectDefaultCardPersist: false,
+ expectCardPersist: false,
+ expectDefaultAddressPersist: true,
+ expectAddressPersist: true,
+ prefilledGuids,
+ });
+});
+
+add_task(async function test_private_add_link() {
+ let prefilledGuids = await setup(
+ [PTU.Addresses.TimBL],
+ [PTU.BasicCards.JohnDoe]
+ );
+ info("Calling add_link from test_private_add_link");
+ await add_link({
+ isPrivate: true,
+ expectDefaultCardPersist: false,
+ expectCardPersist: false,
+ expectDefaultAddressPersist: false,
+ expectAddressPersist: false,
+ prefilledGuids,
+ });
+});
+
+add_task(async function test_persist_prefd_on_add_link() {
+ let prefilledGuids = await setup(
+ [PTU.Addresses.TimBL],
+ [PTU.BasicCards.JohnDoe]
+ );
+ Services.prefs.setBoolPref(SAVE_CREDITCARD_DEFAULT_PREF, true);
+
+ info("Calling add_link from test_persist_prefd_on_add_link");
+ await add_link({
+ isPrivate: false,
+ expectDefaultCardPersist: true,
+ expectCardPersist: true,
+ expectDefaultAddressPersist: true,
+ expectAddressPersist: true,
+ prefilledGuids,
+ });
+ Services.prefs.clearUserPref(SAVE_CREDITCARD_DEFAULT_PREF);
+});
+
+add_task(async function test_private_persist_prefd_on_add_link() {
+ let prefilledGuids = await setup(
+ [PTU.Addresses.TimBL],
+ [PTU.BasicCards.JohnDoe]
+ );
+ Services.prefs.setBoolPref(SAVE_CREDITCARD_DEFAULT_PREF, true);
+
+ info("Calling add_link from test_private_persist_prefd_on_add_link");
+ // in private window, even when the pref is set true,
+ // we should still default to not saving credit-card info
+ await add_link({
+ isPrivate: true,
+ expectDefaultCardPersist: false,
+ expectCardPersist: false,
+ expectDefaultAddressPersist: false,
+ expectAddressPersist: false,
+ prefilledGuids,
+ });
+ Services.prefs.clearUserPref(SAVE_CREDITCARD_DEFAULT_PREF);
+});
+
+add_task(async function test_optin_persist_add_link() {
+ let prefilledGuids = await setup(
+ [PTU.Addresses.TimBL],
+ [PTU.BasicCards.JohnDoe]
+ );
+ let defaultPersist = Services.prefs.getBoolPref(SAVE_CREDITCARD_DEFAULT_PREF);
+
+ is(
+ defaultPersist,
+ false,
+ `Expect ${SAVE_CREDITCARD_DEFAULT_PREF} to default to false`
+ );
+ info("Calling add_link from test_add_link");
+ // verify that explicit opt-in by checking the box results in the record being saved
+ await add_link({
+ isPrivate: false,
+ expectDefaultCardPersist: false,
+ setCardPersistCheckedValue: true,
+ expectCardPersist: true,
+ expectDefaultAddressPersist: true,
+ expectAddressPersist: true,
+ prefilledGuids,
+ });
+});
+
+add_task(async function test_optin_private_persist_add_link() {
+ let prefilledGuids = await setup(
+ [PTU.Addresses.TimBL],
+ [PTU.BasicCards.JohnDoe]
+ );
+ let defaultPersist = Services.prefs.getBoolPref(SAVE_CREDITCARD_DEFAULT_PREF);
+
+ is(
+ defaultPersist,
+ false,
+ `Expect ${SAVE_CREDITCARD_DEFAULT_PREF} to default to false`
+ );
+ // verify that explicit opt-in for the card only from a private window results
+ // in the record being saved
+ await add_link({
+ isPrivate: true,
+ expectDefaultCardPersist: false,
+ setCardPersistCheckedValue: true,
+ expectCardPersist: true,
+ expectDefaultAddressPersist: false,
+ expectAddressPersist: false,
+ prefilledGuids,
+ });
+});
+
+add_task(async function test_opt_out_persist_prefd_on_add_link() {
+ let prefilledGuids = await setup(
+ [PTU.Addresses.TimBL],
+ [PTU.BasicCards.JohnDoe]
+ );
+ Services.prefs.setBoolPref(SAVE_CREDITCARD_DEFAULT_PREF, true);
+
+ // set the pref to default to persist creditcards, but manually uncheck the checkbox
+ await add_link({
+ isPrivate: false,
+ expectDefaultCardPersist: true,
+ setCardPersistCheckedValue: false,
+ expectCardPersist: false,
+ expectDefaultAddressPersist: true,
+ expectAddressPersist: true,
+ prefilledGuids,
+ });
+ Services.prefs.clearUserPref(SAVE_CREDITCARD_DEFAULT_PREF);
+});
+
+add_task(async function test_edit_link() {
+ // add an address and card linked to this address
+ let prefilledGuids = await setup([PTU.Addresses.TimBL, PTU.Addresses.TimBL2]);
+ {
+ let card = Object.assign({}, PTU.BasicCards.JohnDoe, {
+ billingAddressGUID: prefilledGuids.address1GUID,
+ });
+ let guid = await addCardRecord(card);
+ prefilledGuids.card1GUID = guid;
+ }
+
+ const args = {
+ methodData: [PTU.MethodData.basicCard],
+ details: PTU.Details.total60USD,
+ prefilledGuids,
+ };
+ await spawnInDialogForMerchantTask(
+ PTU.ContentTasks.createAndShowRequest,
+ async function check({ prefilledGuids }) {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let paymentMethodPicker = content.document.querySelector(
+ "payment-method-picker"
+ );
+ let editLink = paymentMethodPicker.querySelector(".edit-link");
+
+ content.fillField(
+ Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
+ prefilledGuids.card1GUID
+ );
+
+ is(editLink.textContent, "Edit", "Edit link text");
+
+ editLink.click();
+
+ let state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ state.page.id == "basic-card-page" && state["basic-card-page"].guid
+ );
+ },
+ "Check edit page state"
+ );
+
+ state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ Object.keys(state.savedBasicCards).length == 1 &&
+ Object.keys(state.savedAddresses).length == 2
+ );
+ },
+ "Check card and address present at beginning of test"
+ );
+
+ let title = content.document.querySelector("basic-card-form h2");
+ is(title.textContent, "Edit Credit Card", "Edit title should be set");
+
+ let saveButton = content.document.querySelector(
+ "basic-card-form .save-button"
+ );
+ is(saveButton.textContent, "Update", "Save button has the correct label");
+
+ let card = Object.assign({}, PTU.BasicCards.JohnDoe);
+ // cc-number cannot be modified
+ delete card["cc-number"];
+ card["cc-exp-year"]++;
+ card["cc-exp-month"]++;
+
+ info("overwriting field values");
+ for (let [key, val] of Object.entries(card)) {
+ let field = content.document.getElementById(key);
+ field.value = val;
+ ok(!field.disabled, `Field #${key} shouldn't be disabled`);
+ }
+ ok(
+ content.document.getElementById("cc-number").disabled,
+ "cc-number field should be disabled"
+ );
+
+ let billingAddressPicker = Cu.waiveXrays(
+ content.document.querySelector("basic-card-form billing-address-picker")
+ );
+
+ let initialSelectedAddressGuid = billingAddressPicker.dropdown.value;
+ is(
+ billingAddressPicker.options.length,
+ 3,
+ "Three options should exist in the billingAddressPicker"
+ );
+ is(
+ initialSelectedAddressGuid,
+ prefilledGuids.address1GUID,
+ "The prefilled billing address should be selected by default"
+ );
+
+ info("Test clicking 'add' with the empty option first");
+ billingAddressPicker.dropdown.popupBox.focus();
+ content.fillField(billingAddressPicker.dropdown.popupBox, "");
+
+ let addressEditLink = content.document.querySelector(
+ ".billingAddressRow .edit-link"
+ );
+ ok(
+ addressEditLink && !content.isVisible(addressEditLink),
+ "The edit link is hidden when empty option is selected"
+ );
+
+ let addressAddLink = content.document.querySelector(
+ ".billingAddressRow .add-link"
+ );
+ addressAddLink.click();
+ state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ state.page.id == "billing-address-page" &&
+ !state["billing-address-page"].guid
+ );
+ },
+ "Clicking add button when the empty option is selected will go to 'add' page (no guid)"
+ );
+
+ let addressForm = content.document.querySelector("#billing-address-page");
+ ok(content.isVisible(addressForm), "Billing address form is showing");
+
+ let addressTitle = addressForm.querySelector("h2");
+ is(
+ addressTitle.textContent,
+ "Add Billing Address",
+ "Title on add address page should be correct"
+ );
+
+ let addressBackButton = addressForm.querySelector(".back-button");
+ addressBackButton.click();
+ state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ state.page.id == "basic-card-page" &&
+ state["basic-card-page"].guid &&
+ Object.keys(state.savedAddresses).length == 2
+ );
+ },
+ "Check we're back at basic-card page with no state changed after adding"
+ );
+
+ info(
+ "Inspect a different address and ensure it remains selected when we go back"
+ );
+ content.fillField(
+ billingAddressPicker.dropdown.popupBox,
+ prefilledGuids.address2GUID
+ );
+
+ addressEditLink.click();
+ state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ state.page.id == "billing-address-page" &&
+ state["billing-address-page"].guid
+ );
+ },
+ "Clicking edit button with selected option will go to 'edit' page"
+ );
+
+ let countryPicker = addressForm.querySelector("#country");
+ is(
+ countryPicker.value,
+ PTU.Addresses.TimBL2.country,
+ "The country value matches"
+ );
+
+ addressBackButton.click();
+ state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ state.page.id == "basic-card-page" &&
+ state["basic-card-page"].guid &&
+ Object.keys(state.savedAddresses).length == 2
+ );
+ },
+ "Check we're back at basic-card page with no state changed after editing"
+ );
+
+ is(
+ billingAddressPicker.dropdown.value,
+ prefilledGuids.address2GUID,
+ "The selected billing address is correct"
+ );
+
+ info("Go back to previously selected option before clicking 'edit' now");
+ content.fillField(
+ billingAddressPicker.dropdown.popupBox,
+ initialSelectedAddressGuid
+ );
+
+ let selectedOption = billingAddressPicker.dropdown.selectedOption;
+ ok(
+ selectedOption && selectedOption.value,
+ "select should have a selected option value"
+ );
+
+ addressEditLink.click();
+ state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ state.page.id == "billing-address-page" &&
+ state["billing-address-page"].guid
+ );
+ },
+ "Check address page state (editing)"
+ );
+
+ is(
+ addressTitle.textContent,
+ "Edit Billing Address",
+ "Address on edit address page should be correct"
+ );
+
+ state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return Object.keys(state.savedBasicCards).length == 1;
+ },
+ "Check card was not added again when clicking the 'edit' address button"
+ );
+
+ addressBackButton.click();
+ state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ state.page.id == "basic-card-page" &&
+ state["basic-card-page"].guid &&
+ Object.keys(state.savedAddresses).length == 2
+ );
+ },
+ "Check we're back at basic-card page with no state changed after editing"
+ );
+
+ for (let [key, val] of Object.entries(card)) {
+ let field = content.document.getElementById(key);
+ is(field.value, val, "Field should still have previous value entered");
+ }
+
+ selectedOption = billingAddressPicker.dropdown.selectedOption;
+ ok(
+ selectedOption && selectedOption.value,
+ "select should have a selected option value"
+ );
+
+ addressEditLink.click();
+ state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ state.page.id == "billing-address-page" &&
+ state["billing-address-page"].guid
+ );
+ },
+ "Check address page state (editing)"
+ );
+
+ info("modify some address fields");
+ for (let key of ["given-name", "tel", "organization", "street-address"]) {
+ let field = addressForm.querySelector(`#${key}`);
+ if (!field) {
+ ok(false, `${key} field not found`);
+ }
+ field.focus();
+ EventUtils.sendKey("BACK_SPACE", content.window);
+ EventUtils.sendString("7", content.window);
+ ok(!field.disabled, `Field #${key} shouldn't be disabled`);
+ }
+
+ addressForm.querySelector("button.save-button").click();
+ state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ state.page.id == "basic-card-page" &&
+ state["basic-card-page"].guid &&
+ Object.keys(state.savedAddresses).length == 2
+ );
+ },
+ "Check still only 2 addresses and we're back on basic-card page"
+ );
+
+ is(
+ Object.values(state.savedAddresses)[0].tel,
+ PTU.Addresses.TimBL.tel.slice(0, -1) + "7",
+ "Check that address was edited and saved"
+ );
+
+ content.document
+ .querySelector("basic-card-form button.save-button")
+ .click();
+
+ state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ let cards = Object.entries(state.savedBasicCards);
+ return cards.length == 1 && cards[0][1]["cc-name"] == card["cc-name"];
+ },
+ "Check card was edited"
+ );
+
+ let cardGUIDs = Object.keys(state.savedBasicCards);
+ is(cardGUIDs.length, 1, "Check there is still one card");
+ let savedCard = state.savedBasicCards[cardGUIDs[0]];
+ is(
+ savedCard["cc-number"],
+ "************1111",
+ "Card number should be masked and unmodified."
+ );
+ for (let [key, val] of Object.entries(card)) {
+ is(savedCard[key], val, "Check updated " + key);
+ }
+
+ state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "payment-summary";
+ },
+ "Switched back to payment-summary"
+ );
+ },
+ args
+ );
+});
+
+add_task(async function test_invalid_network_card_edit() {
+ // add an address and card linked to this address
+ let prefilledGuids = await setup([PTU.Addresses.TimBL]);
+ {
+ let card = Object.assign({}, PTU.BasicCards.JohnDoe, {
+ billingAddressGUID: prefilledGuids.address1GUID,
+ });
+ // create a record with an unknown network id
+ card["cc-type"] = "asiv";
+ let guid = await addCardRecord(card);
+ prefilledGuids.card1GUID = guid;
+ }
+
+ const args = {
+ methodData: [PTU.MethodData.basicCard],
+ details: PTU.Details.total60USD,
+ prefilledGuids,
+ };
+ await spawnInDialogForMerchantTask(
+ PTU.ContentTasks.createAndShowRequest,
+ async function check({ prefilledGuids }) {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let paymentMethodPicker = content.document.querySelector(
+ "payment-method-picker"
+ );
+ let editLink = paymentMethodPicker.querySelector(".edit-link");
+
+ content.fillField(
+ Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
+ prefilledGuids.card1GUID
+ );
+
+ is(editLink.textContent, "Edit", "Edit link text");
+
+ editLink.click();
+
+ let state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ state.page.id == "basic-card-page" && state["basic-card-page"].guid
+ );
+ },
+ "Check edit page state"
+ );
+
+ state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ Object.keys(state.savedBasicCards).length == 1 &&
+ Object.keys(state.savedAddresses).length == 1
+ );
+ },
+ "Check card and address present at beginning of test"
+ );
+
+ let networkSelector = content.document.querySelector(
+ "basic-card-form #cc-type"
+ );
+ is(
+ Cu.waiveXrays(networkSelector).selectedIndex,
+ -1,
+ "An invalid cc-type should result in no selection"
+ );
+ is(
+ Cu.waiveXrays(networkSelector).value,
+ "",
+ "An invalid cc-type should result in an empty string as value"
+ );
+
+ ok(
+ content.document.querySelector("basic-card-form button.save-button")
+ .disabled,
+ "Save button should be disabled due to a missing cc-type"
+ );
+
+ content.fillField(Cu.waiveXrays(networkSelector), "visa");
+
+ ok(
+ !content.document.querySelector("basic-card-form button.save-button")
+ .disabled,
+ "Save button should be enabled after fixing cc-type"
+ );
+ content.document
+ .querySelector("basic-card-form button.save-button")
+ .click();
+
+ // We expect that saving a card with a fixed network will result in the
+ // cc-type property being changed to the new value.
+ state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ let cards = Object.entries(state.savedBasicCards);
+ return cards.length == 1 && cards[0][1]["cc-type"] == "visa";
+ },
+ "Check card was edited"
+ );
+
+ let cardGUIDs = Object.keys(state.savedBasicCards);
+ is(cardGUIDs.length, 1, "Check there is still one card");
+ let savedCard = state.savedBasicCards[cardGUIDs[0]];
+ is(
+ savedCard["cc-type"],
+ "visa",
+ "We expect the cc-type value to be updated"
+ );
+
+ state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "payment-summary";
+ },
+ "Switched back to payment-summary"
+ );
+ },
+ args
+ );
+});
+
+add_task(async function test_private_card_adding() {
+ await setup([PTU.Addresses.TimBL], [PTU.BasicCards.JohnDoe]);
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: privateWin.gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: PTU.Details.total60USD,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await spawnPaymentDialogTask(frame, async function check() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let addLink = content.document.querySelector(
+ "payment-method-picker .add-link"
+ );
+ is(addLink.textContent, "Add", "Add link text");
+
+ addLink.click();
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ state.page.id == "basic-card-page" &&
+ !state["basic-card-page"].guid
+ );
+ },
+ "Check card page state"
+ );
+ });
+
+ await fillInCardForm(frame, {
+ ["cc-csc"]: "999",
+ ...PTU.BasicCards.JohnDoe,
+ });
+
+ await spawnPaymentDialogTask(frame, async function() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let card = Object.assign({}, PTU.BasicCards.JohnDoe);
+ let state = await PTU.DialogContentUtils.getCurrentState(content);
+ let savedCardCount = Object.keys(state.savedBasicCards).length;
+ let tempCardCount = Object.keys(state.tempBasicCards).length;
+ content.document
+ .querySelector("basic-card-form button.save-button")
+ .click();
+
+ state = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return Object.keys(state.tempBasicCards).length > tempCardCount;
+ },
+ "Check card was added to temp collection"
+ );
+
+ is(
+ savedCardCount,
+ Object.keys(state.savedBasicCards).length,
+ "No card was saved in state"
+ );
+ is(
+ Object.keys(state.tempBasicCards).length,
+ 1,
+ "Card was added temporarily"
+ );
+
+ let cardGUIDs = Object.keys(state.tempBasicCards);
+ is(cardGUIDs.length, 1, "Check there is one card");
+
+ let tempCard = state.tempBasicCards[cardGUIDs[0]];
+ // Card number should be masked, so skip cc-number in the compare loop below
+ delete card["cc-number"];
+ for (let [key, val] of Object.entries(card)) {
+ is(
+ tempCard[key],
+ val,
+ "Check " + key + ` ${tempCard[key]} matches ${val}`
+ );
+ }
+ // check computed fields
+ is(tempCard["cc-number"], "************1111", "cc-number is masked");
+ is(tempCard["cc-given-name"], "John", "cc-given-name was computed");
+ is(tempCard["cc-family-name"], "Doe", "cc-family-name was computed");
+ ok(tempCard["cc-exp"], "cc-exp was computed");
+ ok(tempCard["cc-number-encrypted"], "cc-number-encrypted was computed");
+ });
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+ await BrowserTestUtils.closeWindow(privateWin);
+});
diff --git a/browser/components/payments/test/browser/browser_change_shipping.js b/browser/components/payments/test/browser/browser_change_shipping.js
new file mode 100644
index 0000000000..ec6a389fe7
--- /dev/null
+++ b/browser/components/payments/test/browser/browser_change_shipping.js
@@ -0,0 +1,732 @@
+"use strict";
+
+async function setup() {
+ await setupFormAutofillStorage();
+ let prefilledGuids = await addSampleAddressesAndBasicCard();
+
+ info("associating the card with the billing address");
+ await formAutofillStorage.creditCards.update(
+ prefilledGuids.card1GUID,
+ {
+ billingAddressGUID: prefilledGuids.address1GUID,
+ },
+ true
+ );
+
+ return prefilledGuids;
+}
+
+add_task(async function test_change_shipping() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(false, "Cannot test OS key store login on official builds.");
+ return;
+ }
+ let prefilledGuids = await setup();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+ ),
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ async ({ prefilledGuids: guids }) => {
+ let paymentMethodPicker = content.document.querySelector(
+ "payment-method-picker"
+ );
+ content.fillField(
+ Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
+ guids.card1GUID
+ );
+ },
+ { prefilledGuids }
+ );
+
+ let shippingOptions = await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.getShippingOptions
+ );
+ is(
+ shippingOptions.selectedOptionCurrency,
+ "USD",
+ "Shipping options should be in USD"
+ );
+ is(
+ shippingOptions.optionCount,
+ 2,
+ "there should be two shipping options"
+ );
+ is(
+ shippingOptions.selectedOptionID,
+ "2",
+ "default selected should be '2'"
+ );
+
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.selectShippingOptionById,
+ "1"
+ );
+
+ shippingOptions = await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.getShippingOptions
+ );
+ is(
+ shippingOptions.optionCount,
+ 2,
+ "there should be two shipping options"
+ );
+ is(shippingOptions.selectedOptionID, "1", "selected should be '1'");
+
+ let paymentDetails = Object.assign(
+ {},
+ PTU.Details.twoShippingOptionsEUR,
+ PTU.Details.total1pt75EUR,
+ PTU.Details.twoDisplayItemsEUR,
+ PTU.Details.additionalDisplayItemsEUR
+ );
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingaddresschange",
+ details: paymentDetails,
+ },
+ ],
+ PTU.ContentTasks.updateWith
+ );
+ info("added shipping change handler to change to EUR");
+
+ await selectPaymentDialogShippingAddressByCountry(frame, "DE");
+ info("changed shipping address to DE country");
+
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingaddresschange",
+ },
+ ],
+ PTU.ContentTasks.awaitPaymentEventPromise
+ );
+ info("got shippingaddresschange event");
+
+ // verify update of shippingOptions
+ shippingOptions = await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.getShippingOptions
+ );
+ is(
+ shippingOptions.selectedOptionCurrency,
+ "EUR",
+ "Shipping options should be in EUR after the shippingaddresschange"
+ );
+ is(
+ shippingOptions.selectedOptionID,
+ "1",
+ "id:1 should still be selected"
+ );
+ is(
+ shippingOptions.selectedOptionValue,
+ "1.01",
+ "amount should be '1.01' after the shippingaddresschange"
+ );
+
+ await spawnPaymentDialogTask(frame, async function() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+ // verify update of total
+ // Note: The update includes a modifier, and modifiers must include a total
+ // so the expected total is that one
+ is(
+ content.document.querySelector("#total > currency-amount")
+ .textContent,
+ "\u20AC2.50 EUR",
+ "Check updated total currency amount"
+ );
+
+ let btn = content.document.querySelector("#view-all");
+ btn.click();
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.orderDetailsShowing;
+ },
+ "Order details show be showing now"
+ );
+
+ let container = content.document.querySelector("order-details");
+ let items = [
+ ...container.querySelectorAll(".main-list payment-details-item"),
+ ].map(item => Cu.waiveXrays(item));
+
+ // verify the updated displayItems
+ is(items.length, 2, "2 display items");
+ is(items[0].amountCurrency, "EUR", "First display item is in Euros");
+ is(items[1].amountCurrency, "EUR", "2nd display item is in Euros");
+ is(items[0].amountValue, "0.85", "First display item has 0.85 value");
+ is(items[1].amountValue, "1.70", "2nd display item has 1.70 value");
+
+ // verify the updated modifiers
+ items = [
+ ...container.querySelectorAll(
+ ".footer-items-list payment-details-item"
+ ),
+ ].map(item => Cu.waiveXrays(item));
+ is(items.length, 1, "1 additional display item");
+ is(items[0].amountCurrency, "EUR", "First display item is in Euros");
+ is(items[0].amountValue, "1.00", "First display item has 1.00 value");
+ btn.click();
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.setSecurityCode,
+ {
+ securityCode: "123",
+ }
+ );
+
+ info("clicking pay");
+ await loginAndCompletePayment(frame);
+
+ // Add a handler to complete the payment above.
+ info("acknowledging the completion from the merchant page");
+ let result = await SpecialPowers.spawn(
+ browser,
+ [],
+ PTU.ContentTasks.addCompletionHandler
+ );
+ is(result.response.methodName, "basic-card", "Check methodName");
+
+ let { shippingAddress } = result.response;
+ let expectedAddress = PTU.Addresses.TimBL2;
+ checkPaymentAddressMatchesStorageAddress(
+ shippingAddress,
+ expectedAddress,
+ "Shipping address"
+ );
+
+ let { methodDetails } = result;
+ checkPaymentMethodDetailsMatchesCard(
+ methodDetails,
+ PTU.BasicCards.JohnDoe,
+ "Payment method"
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+ await cleanupFormAutofillStorage();
+});
+
+add_task(async function test_default_shippingOptions_noneSelected() {
+ await setup();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let shippingOptionDetails = Object.assign(
+ deepClone(PTU.Details.twoShippingOptions),
+ PTU.Details.total2USD
+ );
+ info("make sure no shipping options are selected");
+ shippingOptionDetails.shippingOptions.forEach(opt => delete opt.selected);
+
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: shippingOptionDetails,
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ let shippingOptions = await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.getShippingOptions
+ );
+ is(
+ shippingOptions.optionCount,
+ 2,
+ "there should be two shipping options"
+ );
+ is(
+ shippingOptions.selectedOptionIndex,
+ "-1",
+ "no options should be selected"
+ );
+
+ let shippingOptionDetailsEUR = deepClone(
+ PTU.Details.twoShippingOptionsEUR
+ );
+ info("prepare EUR options by deselecting all and giving unique IDs");
+ shippingOptionDetailsEUR.shippingOptions.forEach(opt => {
+ opt.selected = false;
+ opt.id += "-EUR";
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.selectShippingOptionById,
+ "1"
+ );
+
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingaddresschange",
+ details: Object.assign(
+ shippingOptionDetailsEUR,
+ PTU.Details.total1pt75EUR
+ ),
+ },
+ ],
+ PTU.ContentTasks.updateWith
+ );
+ info("added shipping change handler to change to EUR");
+
+ await selectPaymentDialogShippingAddressByCountry(frame, "DE");
+ info("changed shipping address to DE country");
+
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingaddresschange",
+ },
+ ],
+ PTU.ContentTasks.awaitPaymentEventPromise
+ );
+ info("got shippingaddresschange event");
+
+ shippingOptions = await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.getShippingOptions
+ );
+ is(
+ shippingOptions.optionCount,
+ 2,
+ "there should be two shipping options"
+ );
+ is(
+ shippingOptions.selectedOptionIndex,
+ "-1",
+ "no options should be selected again"
+ );
+
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+ await cleanupFormAutofillStorage();
+});
+
+add_task(async function test_default_shippingOptions_allSelected() {
+ await setup();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let shippingOptionDetails = Object.assign(
+ deepClone(PTU.Details.twoShippingOptions),
+ PTU.Details.total2USD
+ );
+ info("make sure no shipping options are selected");
+ shippingOptionDetails.shippingOptions.forEach(
+ opt => (opt.selected = true)
+ );
+
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: shippingOptionDetails,
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ let shippingOptions = await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.getShippingOptions
+ );
+ is(
+ shippingOptions.selectedOptionCurrency,
+ "USD",
+ "Shipping options should be in USD"
+ );
+ is(
+ shippingOptions.optionCount,
+ 2,
+ "there should be two shipping options"
+ );
+ is(
+ shippingOptions.selectedOptionID,
+ "2",
+ "default selected should be the last selected=true"
+ );
+
+ let shippingOptionDetailsEUR = deepClone(
+ PTU.Details.twoShippingOptionsEUR
+ );
+ info("prepare EUR options by selecting all and giving unique IDs");
+ shippingOptionDetailsEUR.shippingOptions.forEach(opt => {
+ opt.selected = true;
+ opt.id += "-EUR";
+ });
+
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingaddresschange",
+ details: Object.assign(
+ shippingOptionDetailsEUR,
+ PTU.Details.total1pt75EUR
+ ),
+ },
+ ],
+ PTU.ContentTasks.updateWith
+ );
+ info("added shipping change handler to change to EUR");
+
+ await selectPaymentDialogShippingAddressByCountry(frame, "DE");
+ info("changed shipping address to DE country");
+
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingaddresschange",
+ },
+ ],
+ PTU.ContentTasks.awaitPaymentEventPromise
+ );
+ info("got shippingaddresschange event");
+
+ shippingOptions = await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.getShippingOptions
+ );
+ is(
+ shippingOptions.selectedOptionCurrency,
+ "EUR",
+ "Shipping options should be in EUR"
+ );
+ is(
+ shippingOptions.optionCount,
+ 2,
+ "there should be two shipping options"
+ );
+ is(
+ shippingOptions.selectedOptionID,
+ "2-EUR",
+ "default selected should be the last selected=true"
+ );
+
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+ await cleanupFormAutofillStorage();
+});
+
+add_task(async function test_no_shippingchange_without_shipping() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(false, "Cannot test OS key store login on official builds.");
+ return;
+ }
+ let prefilledGuids = await setup();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+ ),
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ async ({ prefilledGuids: guids }) => {
+ let paymentMethodPicker = content.document.querySelector(
+ "payment-method-picker"
+ );
+ content.fillField(
+ Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
+ guids.card1GUID
+ );
+ },
+ { prefilledGuids }
+ );
+
+ SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingaddresschange",
+ },
+ ],
+ PTU.ContentTasks.ensureNoPaymentRequestEvent
+ );
+ info("added shipping change handler");
+
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.setSecurityCode,
+ {
+ securityCode: "456",
+ }
+ );
+
+ info("clicking pay");
+ await loginAndCompletePayment(frame);
+
+ // Add a handler to complete the payment above.
+ info("acknowledging the completion from the merchant page");
+ let result = await SpecialPowers.spawn(
+ browser,
+ [],
+ PTU.ContentTasks.addCompletionHandler
+ );
+ is(result.response.methodName, "basic-card", "Check methodName");
+
+ let actualShippingAddress = result.response.shippingAddress;
+ ok(
+ actualShippingAddress === null,
+ "Check that shipping address is null with requestShipping:false"
+ );
+
+ let { methodDetails } = result;
+ checkPaymentMethodDetailsMatchesCard(
+ methodDetails,
+ PTU.BasicCards.JohnDoe,
+ "Payment method"
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+ await cleanupFormAutofillStorage();
+});
+
+add_task(async function test_address_edit() {
+ await setup();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+ ),
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ options: PTU.Options.requestShippingOption,
+ });
+
+ let addressOptions = await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.getShippingAddresses
+ );
+ info("initial addressOptions: " + JSON.stringify(addressOptions));
+ let selectedIndex = addressOptions.selectedOptionIndex;
+
+ is(selectedIndex, -1, "No address should be selected initially");
+
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingaddresschange",
+ },
+ ],
+ PTU.ContentTasks.promisePaymentRequestEvent
+ );
+
+ info("selecting the US address");
+ await selectPaymentDialogShippingAddressByCountry(frame, "US");
+
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingaddresschange",
+ },
+ ],
+ PTU.ContentTasks.awaitPaymentEventPromise
+ );
+
+ addressOptions = await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.getShippingAddresses
+ );
+ info("initial addressOptions: " + JSON.stringify(addressOptions));
+ selectedIndex = addressOptions.selectedOptionIndex;
+ let selectedAddressGuid = addressOptions.options[selectedIndex].guid;
+ let selectedAddress = await formAutofillStorage.addresses.get(
+ selectedAddressGuid
+ );
+
+ // US address is inserted first, then German address, so German address
+ // has more recent timeLastModified and will appear at the top of the list.
+ is(selectedIndex, 1, "Second address should be selected");
+ ok(
+ selectedAddress,
+ "Selected address does exist in the address collection"
+ );
+ is(selectedAddress.country, "US", "Expected initial country value");
+
+ info("Updating the address directly in the store");
+ await formAutofillStorage.addresses.update(
+ selectedAddress.guid,
+ {
+ country: "CA",
+ },
+ true
+ );
+
+ addressOptions = await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.getShippingAddresses
+ );
+ info("updated addressOptions: " + JSON.stringify(addressOptions));
+ selectedIndex = addressOptions.selectedOptionIndex;
+ let newSelectedAddressGuid = addressOptions.options[selectedIndex].guid;
+
+ is(
+ newSelectedAddressGuid,
+ selectedAddressGuid,
+ "Selected guid hasnt changed"
+ );
+ selectedAddress = await formAutofillStorage.addresses.get(
+ selectedAddressGuid
+ );
+
+ is(selectedIndex, 1, "Second address should be selected");
+ is(selectedAddress.country, "CA", "Expected changed country value");
+
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+
+ await cleanupFormAutofillStorage();
+});
+
+add_task(async function test_address_removal() {
+ await setup();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+ ),
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ options: PTU.Options.requestShippingOption,
+ });
+
+ info("selecting the US address");
+ await selectPaymentDialogShippingAddressByCountry(frame, "US");
+
+ let addressOptions = await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.getShippingAddresses
+ );
+ info("initial addressOptions: " + JSON.stringify(addressOptions));
+ let selectedIndex = addressOptions.selectedOptionIndex;
+ let selectedAddressGuid = addressOptions.options[selectedIndex].guid;
+
+ // US address is inserted first, then German address, so German address
+ // has more recent timeLastModified and will appear at the top of the list.
+ is(selectedIndex, 1, "Second address should be selected");
+ is(
+ addressOptions.options.length,
+ 2,
+ "Should be 2 address options initially"
+ );
+
+ info("Remove the selected address from the store");
+ await formAutofillStorage.addresses.remove(selectedAddressGuid);
+
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingaddresschange",
+ },
+ ],
+ PTU.ContentTasks.promisePaymentRequestEvent
+ );
+
+ addressOptions = await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.getShippingAddresses
+ );
+ info("updated addressOptions: " + JSON.stringify(addressOptions));
+ selectedIndex = addressOptions.selectedOptionIndex;
+
+ is(
+ selectedIndex,
+ -1,
+ "No replacement address should be selected after deletion"
+ );
+ is(addressOptions.options.length, 1, "Should now be 1 address option");
+
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+
+ await cleanupFormAutofillStorage();
+});
diff --git a/browser/components/payments/test/browser/browser_dropdowns.js b/browser/components/payments/test/browser/browser_dropdowns.js
new file mode 100644
index 0000000000..f84ff6e252
--- /dev/null
+++ b/browser/components/payments/test/browser/browser_dropdowns.js
@@ -0,0 +1,82 @@
+"use strict";
+
+add_task(async function test_dropdown() {
+ await addSampleAddressesAndBasicCard();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ details: PTU.Details.total60USD,
+ methodData: [PTU.MethodData.basicCard],
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ let popupset = frame.ownerDocument.querySelector("popupset");
+ ok(popupset, "popupset exists");
+ let popupshownPromise = BrowserTestUtils.waitForEvent(
+ popupset,
+ "popupshown"
+ );
+
+ info("switch to the address add page");
+ await spawnPaymentDialogTask(
+ frame,
+ async function changeToAddressAddPage() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let addLink = content.document.querySelector(
+ "address-picker.shipping-related .add-link"
+ );
+ is(addLink.textContent, "Add", "Add link text");
+
+ addLink.click();
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ state.page.id == "shipping-address-page" && !state.page.guid
+ );
+ },
+ "Check add page state"
+ );
+
+ content.document
+ .querySelector("#shipping-address-page #country")
+ .scrollIntoView();
+ }
+ );
+
+ info("going to open the country <select>");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#shipping-address-page #country",
+ {},
+ frame
+ );
+
+ let event = await popupshownPromise;
+ let expectedPopupID = "ContentSelectDropdown";
+ is(
+ event.target.parentElement.id,
+ expectedPopupID,
+ "Checked menulist of opened popup"
+ );
+
+ event.target.hidePopup(true);
+
+ info("clicking cancel");
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
diff --git a/browser/components/payments/test/browser/browser_host_name.js b/browser/components/payments/test/browser/browser_host_name.js
new file mode 100644
index 0000000000..fb88253164
--- /dev/null
+++ b/browser/components/payments/test/browser/browser_host_name.js
@@ -0,0 +1,50 @@
+"use strict";
+
+async function withBasicRequestDialogForOrigin(origin, dialogTaskFn) {
+ const args = {
+ methodData: [PTU.MethodData.basicCard],
+ details: PTU.Details.total60USD,
+ };
+ await spawnInDialogForMerchantTask(
+ PTU.ContentTasks.createAndShowRequest,
+ dialogTaskFn,
+ args,
+ {
+ origin,
+ }
+ );
+}
+
+add_task(async function test_host() {
+ await withBasicRequestDialogForOrigin("https://example.com", () => {
+ is(
+ content.document.querySelector("#host-name").textContent,
+ "example.com",
+ "Check basic host name"
+ );
+ });
+});
+
+add_task(async function test_host_subdomain() {
+ await withBasicRequestDialogForOrigin("https://test1.example.com", () => {
+ is(
+ content.document.querySelector("#host-name").textContent,
+ "test1.example.com",
+ "Check host name with subdomain"
+ );
+ });
+});
+
+add_task(async function test_host_IDN() {
+ await withBasicRequestDialogForOrigin(
+ "https://xn--hxajbheg2az3al.xn--jxalpdlp",
+ () => {
+ is(
+ content.document.querySelector("#host-name").textContent,
+ "\u03C0\u03B1\u03C1\u03AC\u03B4\u03B5\u03B9\u03B3\u03BC\u03B1." +
+ "\u03B4\u03BF\u03BA\u03B9\u03BC\u03AE",
+ "Check IDN domain"
+ );
+ }
+ );
+});
diff --git a/browser/components/payments/test/browser/browser_onboarding_wizard.js b/browser/components/payments/test/browser/browser_onboarding_wizard.js
new file mode 100644
index 0000000000..14d3d98440
--- /dev/null
+++ b/browser/components/payments/test/browser/browser_onboarding_wizard.js
@@ -0,0 +1,859 @@
+"use strict";
+
+async function addAddress() {
+ let onChanged = TestUtils.topicObserved(
+ "formautofill-storage-changed",
+ (subject, data) => data == "add"
+ );
+ formAutofillStorage.addresses.add(PTU.Addresses.TimBL);
+ await onChanged;
+}
+
+async function addBasicCard() {
+ let onChanged = TestUtils.topicObserved(
+ "formautofill-storage-changed",
+ (subject, data) => data == "add"
+ );
+ formAutofillStorage.creditCards.add(PTU.BasicCards.JohnDoe);
+ await onChanged;
+}
+
+add_task(
+ async function test_onboarding_wizard_without_saved_addresses_and_saved_cards() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ cleanupFormAutofillStorage();
+
+ info("Opening the payment dialog");
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: PTU.Details.total60USD,
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await spawnPaymentDialogTask(frame, async function() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "shipping-address-page";
+ },
+ "Address page is shown first during on-boarding if there are no saved addresses"
+ );
+
+ let addressForm = content.document.querySelector(
+ "#shipping-address-page"
+ );
+ info("Checking if the address page has been rendered");
+ let addressSaveButton = addressForm.querySelector(".save-button");
+ ok(
+ content.isVisible(addressSaveButton),
+ "Address save button is rendered"
+ );
+ is(
+ addressSaveButton.textContent,
+ "Next",
+ "Address save button has the correct label during onboarding"
+ );
+
+ info(
+ "Check if the total header is visible on the address page during on-boarding"
+ );
+ let header = content.document.querySelector("header");
+ ok(
+ content.isVisible(header),
+ "Total Header is visible on the address page during on-boarding"
+ );
+ ok(header.textContent, "Total Header contains text");
+
+ info("Check if the page title is visible on the address page");
+ let addressPageTitle = addressForm.querySelector("h2");
+ ok(
+ content.isVisible(addressPageTitle),
+ "Address page title is visible"
+ );
+ is(
+ addressPageTitle.textContent,
+ "Add Shipping Address",
+ "Address page title is correctly shown"
+ );
+
+ let addressCancelButton = addressForm.querySelector(".cancel-button");
+ ok(
+ content.isVisible(addressCancelButton),
+ "The cancel button on the address page is visible"
+ );
+ });
+
+ let addOptions = {
+ addLinkSelector: "address-picker.shipping-related .add-link",
+ initialPageId: "payment-summary",
+ addressPageId: "shipping-address-page",
+ };
+
+ await fillInShippingAddressForm(
+ frame,
+ PTU.Addresses.TimBL2,
+ addOptions
+ );
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.clickPrimaryButton
+ );
+
+ await spawnPaymentDialogTask(frame, async function() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "basic-card-page";
+ },
+ "Basic card page is shown after the address page during on boarding"
+ );
+
+ let cardSaveButton = content.document.querySelector(
+ "basic-card-form .save-button"
+ );
+ is(
+ cardSaveButton.textContent,
+ "Next",
+ "Card save button has the correct label during onboarding"
+ );
+ ok(content.isVisible(cardSaveButton), "Basic card page is rendered");
+
+ let basicCardTitle = content.document.querySelector(
+ "basic-card-form h2"
+ );
+ ok(
+ content.isVisible(basicCardTitle),
+ "Basic card page title is visible"
+ );
+ is(
+ basicCardTitle.textContent,
+ "Add Credit Card",
+ "Basic card page title is correctly shown"
+ );
+
+ info(
+ "Check if the correct billing address is selected in the basic card page"
+ );
+ PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ let billingAddressSelect = content.document.querySelector(
+ "#billingAddressGUID"
+ );
+ return (
+ state.selectedShippingAddress == billingAddressSelect.value
+ );
+ },
+ "Shipping address is selected as the billing address"
+ );
+ });
+
+ await fillInCardForm(frame, {
+ ["cc-csc"]: "123",
+ ...PTU.BasicCards.JohnDoe,
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.clickPrimaryButton
+ );
+
+ await spawnPaymentDialogTask(frame, async function() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "payment-summary";
+ },
+ "Payment summary page is shown after the basic card page during on boarding"
+ );
+
+ let cancelButton = content.document.querySelector("#cancel");
+ ok(
+ content.isVisible(cancelButton),
+ "Payment summary page is rendered"
+ );
+ });
+
+ info("Closing the payment dialog");
+ spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.manuallyClickCancel
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+ }
+);
+
+add_task(
+ async function test_onboarding_wizard_with_saved_addresses_and_saved_cards() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ addSampleAddressesAndBasicCard();
+
+ info("Opening the payment dialog");
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: PTU.Details.total60USD,
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await spawnPaymentDialogTask(frame, async function() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "payment-summary";
+ },
+ "Payment summary page is shown first when there are saved addresses and saved cards"
+ );
+
+ info("Checking if the payment summary page is now visible");
+ let cancelButton = content.document.querySelector("#cancel");
+ ok(
+ content.isVisible(cancelButton),
+ "Payment summary page is rendered"
+ );
+
+ info("Check if the total header is visible on payments summary page");
+ let header = content.document.querySelector("header");
+ ok(
+ content.isVisible(header),
+ "Total Header is visible on the payment summary page"
+ );
+ ok(header.textContent, "Total Header contains text");
+
+ // Click on the Add/Edit buttons in the payment summary page to check if
+ // the total header is visible on the address page and the basic card page.
+ let buttons = [
+ "address-picker[selected-state-key='selectedShippingAddress'] .add-link",
+ "address-picker[selected-state-key='selectedShippingAddress'] .edit-link",
+ "payment-method-picker .add-link",
+ "payment-method-picker .edit-link",
+ ];
+ for (let button of buttons) {
+ content.document.querySelector(button).click();
+ if (button.startsWith("address")) {
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "shipping-address-page";
+ },
+ "Shipping address page is shown"
+ );
+ } else {
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "basic-card-page";
+ },
+ "Basic card page is shown"
+ );
+ }
+
+ ok(
+ !content.isVisible(header),
+ "Total Header is hidden on the address/basic card page"
+ );
+
+ if (button.startsWith("address")) {
+ content.document
+ .querySelector("#shipping-address-page .back-button")
+ .click();
+ } else {
+ content.document
+ .querySelector("basic-card-form .back-button")
+ .click();
+ }
+ }
+ });
+
+ info("Closing the payment dialog");
+ spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.manuallyClickCancel
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+
+ cleanupFormAutofillStorage();
+ }
+ );
+ }
+);
+
+add_task(
+ async function test_onboarding_wizard_with_saved_addresses_and_no_saved_cards() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ addAddress();
+
+ info("Opening the payment dialog");
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: PTU.Details.total60USD,
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await spawnPaymentDialogTask(frame, async function() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "basic-card-page";
+ },
+ "Basic card page is shown first if there are saved addresses during on boarding"
+ );
+
+ info("Checking if the basic card page has been rendered");
+ let cardSaveButton = content.document.querySelector(
+ "basic-card-form .save-button"
+ );
+ ok(content.isVisible(cardSaveButton), "Basic card page is rendered");
+
+ info(
+ "Check if the total header is visible on the basic card page during on-boarding"
+ );
+ let header = content.document.querySelector("header");
+ ok(
+ content.isVisible(header),
+ "Total Header is visible on the basic card page during on-boarding"
+ );
+ ok(header.textContent, "Total Header contains text");
+
+ let cardCancelButton = content.document.querySelector(
+ "basic-card-form .cancel-button"
+ );
+ ok(
+ content.isVisible(cardCancelButton),
+ "Cancel button is visible on the basic card page"
+ );
+
+ let cardBackButton = content.document.querySelector(
+ "basic-card-form .back-button"
+ );
+ ok(
+ !content.isVisible(cardBackButton),
+ "Back button is hidden on the basic card page when it is shown first during onboarding"
+ );
+ });
+
+ // Do not await for this task since the dialog may close before the task resolves.
+ spawnPaymentDialogTask(frame, () => {
+ content.document
+ .querySelector("basic-card-form .cancel-button")
+ .click();
+ });
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+
+ cleanupFormAutofillStorage();
+ }
+ );
+ }
+);
+
+add_task(
+ async function test_onboarding_wizard_without_saved_address_with_saved_cards() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ cleanupFormAutofillStorage();
+ addBasicCard();
+
+ info("Opening the payment dialog");
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: PTU.Details.total60USD,
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ let addOptions = {
+ addLinkSelector: "address-picker.shipping-related .add-link",
+ checkboxSelector: "#shipping-address-page .persist-checkbox",
+ initialPageId: "payment-summary",
+ addressPageId: "shipping-address-page",
+ expectPersist: true,
+ };
+
+ await spawnPaymentDialogTask(frame, async function() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "shipping-address-page";
+ },
+ "Shipping address page is shown first if there are saved addresses during on boarding"
+ );
+
+ info("Checking if the address page has been rendered");
+ let addressForm = content.document.querySelector(
+ "#shipping-address-page"
+ );
+ let addressSaveButton = addressForm.querySelector(".save-button");
+ ok(
+ content.isVisible(addressSaveButton),
+ "Address save button is rendered"
+ );
+ });
+
+ await fillInShippingAddressForm(
+ frame,
+ PTU.Addresses.TimBL2,
+ addOptions
+ );
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.clickPrimaryButton
+ );
+
+ await spawnPaymentDialogTask(
+ frame,
+ async function checkSavedAndCancelButton() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "payment-summary";
+ },
+ "payment-summary is now visible"
+ );
+
+ let cancelButton = content.document.querySelector("#cancel");
+ ok(
+ content.isVisible(cancelButton),
+ "Payment summary page is shown next"
+ );
+ }
+ );
+
+ info("Closing the payment dialog");
+ spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.manuallyClickCancel
+ );
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ cleanupFormAutofillStorage();
+ }
+ );
+ }
+);
+
+add_task(
+ async function test_onboarding_wizard_with_requestShipping_turned_off() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ cleanupFormAutofillStorage();
+
+ info("Opening the payment dialog");
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: PTU.Details.total60USD,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await spawnPaymentDialogTask(frame, async function() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "billing-address-page";
+ // eslint-disable-next-line max-len
+ },
+ "Billing address page is shown first during on-boarding if requestShipping is turned off"
+ );
+
+ info("Checking if the billing address page has been rendered");
+ let addressForm = content.document.querySelector(
+ "#billing-address-page"
+ );
+ let addressSaveButton = addressForm.querySelector(".save-button");
+ ok(
+ content.isVisible(addressSaveButton),
+ "Address save button is rendered"
+ );
+
+ info("Check if the page title is visible on the address page");
+ let addressPageTitle = addressForm.querySelector("h2");
+ ok(
+ content.isVisible(addressPageTitle),
+ "Address page title is visible"
+ );
+ is(
+ addressPageTitle.textContent,
+ "Add Billing Address",
+ "Address page title is correctly shown"
+ );
+ });
+
+ let addOptions = {
+ initialPageId: "basic-card-page",
+ addressPageId: "billing-address-page",
+ };
+
+ await fillInBillingAddressForm(frame, PTU.Addresses.TimBL2, addOptions);
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.clickPrimaryButton
+ );
+
+ await spawnPaymentDialogTask(frame, async function() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "basic-card-page";
+ // eslint-disable-next-line max-len
+ },
+ "Basic card page is shown after the billing address page during onboarding if requestShipping is turned off"
+ );
+
+ let cardSaveButton = content.document.querySelector(
+ "basic-card-form .save-button"
+ );
+ ok(content.isVisible(cardSaveButton), "Basic card page is rendered");
+
+ info(
+ "Check if the correct billing address is selected in the basic card page"
+ );
+ PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ let billingAddressSelect = content.document.querySelector(
+ "#billingAddressGUID"
+ );
+ return (
+ state["basic-card-page"].billingAddressGUID ==
+ billingAddressSelect.value
+ );
+ },
+ "Billing Address is correctly shown"
+ );
+ });
+
+ await fillInCardForm(frame, {
+ ["cc-csc"]: "123",
+ ...PTU.BasicCards.JohnDoe,
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.clickPrimaryButton
+ );
+
+ await spawnPaymentDialogTask(frame, async function() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "payment-summary";
+ },
+ "payment-summary is shown after the basic card page during on boarding"
+ );
+
+ let cancelButton = content.document.querySelector("#cancel");
+ ok(
+ content.isVisible(cancelButton),
+ "Payment summary page is rendered"
+ );
+ });
+
+ info("Closing the payment dialog");
+ spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.manuallyClickCancel
+ );
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+
+ cleanupFormAutofillStorage();
+ }
+ );
+ }
+);
+
+add_task(
+ async function test_on_boarding_wizard_with_requestShipping_turned_off_with_saved_cards() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ cleanupFormAutofillStorage();
+ addBasicCard();
+
+ info("Opening the payment dialog");
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: PTU.Details.total60USD,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await spawnPaymentDialogTask(frame, async function() {
+ let cancelButton = content.document.querySelector("#cancel");
+ ok(
+ content.isVisible(cancelButton),
+ // eslint-disable-next-line max-len
+ "Payment summary page is shown if requestShipping is turned off and there's a saved card but no saved address"
+ );
+ });
+
+ info("Closing the payment dialog");
+ spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.manuallyClickCancel
+ );
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+
+ cleanupFormAutofillStorage();
+ }
+ );
+ }
+);
+
+add_task(
+ async function test_back_button_on_basic_card_page_during_onboarding() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ cleanupFormAutofillStorage();
+
+ info("Opening the payment dialog");
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: PTU.Details.total60USD,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await spawnPaymentDialogTask(frame, async function() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "billing-address-page";
+ },
+ "Billing address page is shown first if there are no saved addresses " +
+ "and requestShipping is false during on boarding"
+ );
+ info("Checking if the address page has been rendered");
+ let addressSaveButton = content.document.querySelector(
+ "#billing-address-page .save-button"
+ );
+ ok(
+ content.isVisible(addressSaveButton),
+ "Address save button is rendered"
+ );
+ });
+
+ let addOptions = {
+ addLinkSelector: "address-picker.billing-related .add-link",
+ checkboxSelector: "#billing-address-page .persist-checkbox",
+ initialPageId: "basic-card-page",
+ addressPageId: "billing-address-page",
+ expectPersist: true,
+ };
+
+ await fillInBillingAddressForm(frame, PTU.Addresses.TimBL2, addOptions);
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.clickPrimaryButton
+ );
+
+ await spawnPaymentDialogTask(frame, async function() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "basic-card-page";
+ },
+ "Basic card page is shown next"
+ );
+
+ info("Checking if basic card page is rendered");
+ let basicCardBackButton = content.document.querySelector(
+ "basic-card-form .back-button"
+ );
+ ok(
+ content.isVisible(basicCardBackButton),
+ "Back button is visible on the basic card page"
+ );
+
+ info("Partially fill basic card form");
+ let field = content.document.getElementById("cc-number");
+ content.fillField(field, PTU.BasicCards.JohnDoe["cc-number"]);
+
+ info(
+ "Clicking on the back button to edit address saved in the previous step"
+ );
+ basicCardBackButton.click();
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ state.page.id == "billing-address-page" &&
+ state["billing-address-page"].guid ==
+ state["basic-card-page"].billingAddressGUID
+ );
+ },
+ "Billing address page is shown again"
+ );
+
+ info("Checking if the address page has been rendered");
+ let addressForm = content.document.querySelector(
+ "#billing-address-page"
+ );
+ let addressSaveButton = addressForm.querySelector(".save-button");
+ ok(
+ content.isVisible(addressSaveButton),
+ "Address save button is rendered"
+ );
+
+ info(
+ "Checking if the address saved in the last step is correctly loaded in the form"
+ );
+ field = addressForm.querySelector("#given-name");
+ is(
+ field.value,
+ PTU.Addresses.TimBL2["given-name"],
+ "Given name field value is correctly loaded"
+ );
+
+ info("Editing the address and saving again");
+ content.fillField(field, "John");
+ addressSaveButton.click();
+
+ info("Checking if the address was correctly edited");
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ state.page.id == "basic-card-page" &&
+ // eslint-disable-next-line max-len
+ state.savedAddresses[
+ state["basic-card-page"].billingAddressGUID
+ ]["given-name"] == "John"
+ );
+ },
+ "Address was correctly edited and saved"
+ );
+
+ // eslint-disable-next-line max-len
+ info(
+ "Checking if the basic card form is now rendered and if the field values from before are preserved"
+ );
+ let basicCardCancelButton = content.document.querySelector(
+ "basic-card-form .cancel-button"
+ );
+ ok(
+ content.isVisible(basicCardCancelButton),
+ "Cancel button is visible on the basic card page"
+ );
+ field = content.document.getElementById("cc-number");
+ is(
+ field.value,
+ PTU.BasicCards.JohnDoe["cc-number"],
+ "Values in the form are preserved"
+ );
+ });
+
+ info("Closing the payment dialog");
+ spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.manuallyClickCancel
+ );
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+
+ cleanupFormAutofillStorage();
+ }
+ );
+ }
+);
diff --git a/browser/components/payments/test/browser/browser_openPreferences.js b/browser/components/payments/test/browser/browser_openPreferences.js
new file mode 100644
index 0000000000..5d8c71f58b
--- /dev/null
+++ b/browser/components/payments/test/browser/browser_openPreferences.js
@@ -0,0 +1,93 @@
+"use strict";
+
+const methodData = [PTU.MethodData.basicCard];
+const details = Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+);
+
+add_task(async function setup_once() {
+ // add an address and card to avoid the FTU sequence
+ await addSampleAddressesAndBasicCard(
+ [PTU.Addresses.TimBL],
+ [PTU.BasicCards.JohnDoe]
+ );
+});
+
+add_task(async function test_openPreferences() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData,
+ details,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ let prefsTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:preferences#privacy-form-autofill"
+ );
+
+ let prefsLoadedPromise = TestUtils.topicObserved("sync-pane-loaded");
+
+ await spawnPaymentDialogTask(
+ frame,
+ function verifyPrefsLink({ isMac }) {
+ let manageTextEl = content.document.querySelector(".manage-text");
+
+ let expectedVisibleEl;
+ if (isMac) {
+ expectedVisibleEl = manageTextEl.querySelector(
+ ":scope > span[data-os='mac']"
+ );
+ ok(
+ manageTextEl.innerText.includes("Preferences"),
+ "Visible string includes 'Preferences'"
+ );
+ ok(
+ !manageTextEl.innerText.includes("Options"),
+ "Visible string includes 'Options'"
+ );
+ } else {
+ expectedVisibleEl = manageTextEl.querySelector(
+ ":scope > span:not([data-os='mac'])"
+ );
+ ok(
+ !manageTextEl.innerText.includes("Preferences"),
+ "Visible string includes 'Preferences'"
+ );
+ ok(
+ manageTextEl.innerText.includes("Options"),
+ "Visible string includes 'Options'"
+ );
+ }
+
+ let prefsLink = expectedVisibleEl.querySelector("a");
+ ok(prefsLink, "Preferences link should exist");
+ prefsLink.scrollIntoView();
+ EventUtils.synthesizeMouseAtCenter(prefsLink, {}, content);
+ },
+ {
+ isMac: AppConstants.platform == "macosx",
+ }
+ );
+
+ let prefsTab = await prefsTabPromise;
+ ok(prefsTab, "Ensure a tab was opened");
+ await prefsLoadedPromise;
+
+ await BrowserTestUtils.removeTab(prefsTab);
+
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
diff --git a/browser/components/payments/test/browser/browser_payerRequestedFields.js b/browser/components/payments/test/browser/browser_payerRequestedFields.js
new file mode 100644
index 0000000000..e519a1ae22
--- /dev/null
+++ b/browser/components/payments/test/browser/browser_payerRequestedFields.js
@@ -0,0 +1,154 @@
+/* eslint-disable no-shadow */
+
+"use strict";
+
+async function setup() {
+ await setupFormAutofillStorage();
+ await cleanupFormAutofillStorage();
+ // add an address and card to avoid the FTU sequence
+ let prefilledGuids = await addSampleAddressesAndBasicCard(
+ [PTU.Addresses.TimBL],
+ [PTU.BasicCards.JohnDoe]
+ );
+
+ info("associating the card with the billing address");
+ await formAutofillStorage.creditCards.update(
+ prefilledGuids.card1GUID,
+ {
+ billingAddressGUID: prefilledGuids.address1GUID,
+ },
+ true
+ );
+
+ return prefilledGuids;
+}
+
+/*
+ * Test that the payerRequested* fields are marked as required
+ * on the payer address form but aren't marked as required on
+ * the shipping address form.
+ */
+add_task(async function test_add_link() {
+ await setup();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+ ),
+ options: {
+ ...PTU.Options.requestShipping,
+ ...PTU.Options.requestPayerNameEmailAndPhone,
+ },
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await navigateToAddAddressPage(frame, {
+ addLinkSelector: "address-picker.payer-related .add-link",
+ initialPageId: "payment-summary",
+ addressPageId: "payer-address-page",
+ expectPersist: true,
+ });
+
+ await spawnPaymentDialogTask(frame, async () => {
+ let { PaymentTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let addressForm = content.document.querySelector("#payer-address-page");
+ let title = addressForm.querySelector("h2");
+ is(title.textContent, "Add Payer Contact", "Page title should be set");
+
+ let saveButton = addressForm.querySelector(".save-button");
+ is(saveButton.textContent, "Next", "Save button has the correct label");
+
+ info("check that payer requested fields are marked as required");
+ for (let selector of [
+ "#given-name",
+ "#family-name",
+ "#email",
+ "#tel",
+ ]) {
+ let element = addressForm.querySelector(selector);
+ ok(element.required, selector + " should be required");
+ }
+
+ let backButton = addressForm.querySelector(".back-button");
+ ok(
+ content.isVisible(backButton),
+ "Back button is visible on the payer address page"
+ );
+ backButton.click();
+
+ await PaymentTestUtils.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "payment-summary";
+ },
+ "Switched back to payment-summary from payer address form"
+ );
+ });
+
+ await navigateToAddAddressPage(frame, {
+ addLinkSelector: "address-picker.shipping-related .add-link",
+ addressPageId: "shipping-address-page",
+ initialPageId: "payment-summary",
+ expectPersist: true,
+ });
+
+ await spawnPaymentDialogTask(frame, async () => {
+ let { PaymentTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let addressForm = content.document.querySelector(
+ "#shipping-address-page"
+ );
+ let title = addressForm.querySelector("address-form h2");
+ is(
+ title.textContent,
+ "Add Shipping Address",
+ "Page title should be set"
+ );
+
+ let saveButton = addressForm.querySelector(".save-button");
+ is(saveButton.textContent, "Next", "Save button has the correct label");
+
+ ok(
+ !addressForm.querySelector("#tel").required,
+ "#tel should not be required"
+ );
+
+ let backButton = addressForm.querySelector(".back-button");
+ ok(
+ content.isVisible(backButton),
+ "Back button is visible on the payer address page"
+ );
+ backButton.click();
+
+ await PaymentTestUtils.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "payment-summary";
+ },
+ "Switched back to payment-summary from payer address form"
+ );
+ });
+
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+ await cleanupFormAutofillStorage();
+});
diff --git a/browser/components/payments/test/browser/browser_payment_completion.js b/browser/components/payments/test/browser/browser_payment_completion.js
new file mode 100644
index 0000000000..66807d65e6
--- /dev/null
+++ b/browser/components/payments/test/browser/browser_payment_completion.js
@@ -0,0 +1,211 @@
+"use strict";
+
+/*
+ Test the permutations of calling complete() on the payment response and handling the case
+ where the timeout is exceeded before it is called
+*/
+
+async function setup() {
+ await setupFormAutofillStorage();
+ await cleanupFormAutofillStorage();
+ let billingAddressGUID = await addAddressRecord(PTU.Addresses.TimBL);
+ let card = Object.assign({}, PTU.BasicCards.JohnDoe, { billingAddressGUID });
+ let card1GUID = await addCardRecord(card);
+ return { address1GUID: billingAddressGUID, card1GUID };
+}
+
+add_task(async function test_complete_success() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(false, "Cannot test OS key store login on official builds.");
+ return;
+ }
+ let prefilledGuids = await setup();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign({}, PTU.Details.total60USD),
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ async ({ prefilledGuids: guids }) => {
+ let paymentMethodPicker = content.document.querySelector(
+ "payment-method-picker"
+ );
+ content.fillField(
+ Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
+ guids.card1GUID
+ );
+ },
+ { prefilledGuids }
+ );
+
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.setSecurityCode,
+ {
+ securityCode: "123",
+ }
+ );
+
+ await loginAndCompletePayment(frame);
+
+ // Add a handler to complete the payment above.
+ info("acknowledging the completion from the merchant page");
+ let { completeException } = await SpecialPowers.spawn(
+ browser,
+ [{ result: "success" }],
+ PTU.ContentTasks.addCompletionHandler
+ );
+
+ ok(
+ !completeException,
+ "Expect no exception to be thrown when calling complete()"
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
+
+add_task(async function test_complete_fail() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(false, "Cannot test OS key store login on official builds.");
+ return;
+ }
+ let prefilledGuids = await setup();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign({}, PTU.Details.total60USD),
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ async ({ prefilledGuids: guids }) => {
+ let paymentMethodPicker = content.document.querySelector(
+ "payment-method-picker"
+ );
+ content.fillField(
+ Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
+ guids.card1GUID
+ );
+ },
+ { prefilledGuids }
+ );
+
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.setSecurityCode,
+ {
+ securityCode: "456",
+ }
+ );
+
+ info("clicking pay");
+ await loginAndCompletePayment(frame);
+
+ info("acknowledging the completion from the merchant page");
+ let { completeException } = await SpecialPowers.spawn(
+ browser,
+ [{ result: "fail" }],
+ PTU.ContentTasks.addCompletionHandler
+ );
+ ok(
+ !completeException,
+ "Expect no exception to be thrown when calling complete()"
+ );
+
+ ok(!win.closed, "dialog shouldn't be closed yet");
+
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.clickPrimaryButton);
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
+
+add_task(async function test_complete_timeout() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(false, "Cannot test OS key store login on official builds.");
+ return;
+ }
+ let prefilledGuids = await setup();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ // timeout the response asap
+ Services.prefs.setIntPref(RESPONSE_TIMEOUT_PREF, 60);
+
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign({}, PTU.Details.total60USD),
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ async ({ prefilledGuids: guids }) => {
+ let paymentMethodPicker = content.document.querySelector(
+ "payment-method-picker"
+ );
+ content.fillField(
+ Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
+ guids.card1GUID
+ );
+ },
+ { prefilledGuids }
+ );
+
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.setSecurityCode,
+ {
+ securityCode: "789",
+ }
+ );
+
+ info("clicking pay");
+ await loginAndCompletePayment(frame);
+
+ info("acknowledging the completion from the merchant page after a delay");
+ let { completeException } = await SpecialPowers.spawn(
+ browser,
+ [{ result: "fail", delayMs: 1000 }],
+ PTU.ContentTasks.addCompletionHandler
+ );
+ ok(
+ completeException,
+ "Expect an exception to be thrown when calling complete() too late"
+ );
+
+ ok(!win.closed, "dialog shouldn't be closed");
+
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.clickPrimaryButton);
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
diff --git a/browser/components/payments/test/browser/browser_profile_storage.js b/browser/components/payments/test/browser/browser_profile_storage.js
new file mode 100644
index 0000000000..217ba42bb7
--- /dev/null
+++ b/browser/components/payments/test/browser/browser_profile_storage.js
@@ -0,0 +1,303 @@
+/* eslint-disable no-shadow */
+
+"use strict";
+
+const methodData = [PTU.MethodData.basicCard];
+const details = PTU.Details.total60USD;
+
+add_task(async function test_initial_state() {
+ let onChanged = TestUtils.topicObserved(
+ "formautofill-storage-changed",
+ (subject, data) => data == "add"
+ );
+ let address1GUID = await formAutofillStorage.addresses.add(
+ PTU.Addresses.TimBL
+ );
+ await onChanged;
+
+ onChanged = TestUtils.topicObserved(
+ "formautofill-storage-changed",
+ (subject, data) => data == "add"
+ );
+ let card1GUID = await formAutofillStorage.creditCards.add(
+ PTU.BasicCards.JohnDoe
+ );
+ await onChanged;
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData,
+ details,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ async function checkInitialStore({ address1GUID, card1GUID }) {
+ info("checkInitialStore");
+ let contentWin = Cu.waiveXrays(content);
+ let {
+ savedAddresses,
+ savedBasicCards,
+ } = contentWin.document
+ .querySelector("payment-dialog")
+ .requestStore.getState();
+
+ is(
+ Object.keys(savedAddresses).length,
+ 1,
+ "Initially one savedAddresses"
+ );
+ is(
+ savedAddresses[address1GUID].name,
+ "Timothy John Berners-Lee",
+ "Check full name"
+ );
+ is(
+ savedAddresses[address1GUID].guid,
+ address1GUID,
+ "Check address guid matches key"
+ );
+
+ is(
+ Object.keys(savedBasicCards).length,
+ 1,
+ "Initially one savedBasicCards"
+ );
+ is(
+ savedBasicCards[card1GUID]["cc-number"],
+ "************1111",
+ "Check cc-number"
+ );
+ is(
+ savedBasicCards[card1GUID].guid,
+ card1GUID,
+ "Check card guid matches key"
+ );
+ is(
+ savedBasicCards[card1GUID].methodName,
+ "basic-card",
+ "Check card has a methodName of basic-card"
+ );
+ },
+ {
+ address1GUID,
+ card1GUID,
+ }
+ );
+
+ let onChanged = TestUtils.topicObserved(
+ "formautofill-storage-changed",
+ (subject, data) => data == "add"
+ );
+ info("adding an address");
+ let address2GUID = await formAutofillStorage.addresses.add(
+ PTU.Addresses.TimBL2
+ );
+ await onChanged;
+
+ await spawnPaymentDialogTask(
+ frame,
+ async function checkAdd({ address1GUID, address2GUID, card1GUID }) {
+ info("checkAdd");
+
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+ let {
+ savedAddresses,
+ savedBasicCards,
+ } = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => !!state.savedAddresses[address2GUID]
+ );
+
+ let addressGUIDs = Object.keys(savedAddresses);
+ is(addressGUIDs.length, 2, "Now two savedAddresses");
+ is(addressGUIDs[0], address1GUID, "Check first address GUID");
+ is(
+ savedAddresses[address1GUID].guid,
+ address1GUID,
+ "Check address 1 guid matches key"
+ );
+ is(addressGUIDs[1], address2GUID, "Check second address GUID");
+ is(
+ savedAddresses[address2GUID].guid,
+ address2GUID,
+ "Check address 2 guid matches key"
+ );
+
+ is(
+ Object.keys(savedBasicCards).length,
+ 1,
+ "Still one savedBasicCards"
+ );
+ is(
+ savedBasicCards[card1GUID].guid,
+ card1GUID,
+ "Check card guid matches key"
+ );
+ is(
+ savedBasicCards[card1GUID].methodName,
+ "basic-card",
+ "Check card has a methodName of basic-card"
+ );
+ },
+ {
+ address1GUID,
+ address2GUID,
+ card1GUID,
+ }
+ );
+
+ onChanged = TestUtils.topicObserved(
+ "formautofill-storage-changed",
+ (subject, data) => data == "update"
+ );
+ info("updating the credit expiration");
+ await formAutofillStorage.creditCards.update(
+ card1GUID,
+ {
+ "cc-exp-month": 6,
+ "cc-exp-year": 2029,
+ },
+ true
+ );
+ await onChanged;
+
+ await spawnPaymentDialogTask(
+ frame,
+ async function checkUpdate({ address1GUID, address2GUID, card1GUID }) {
+ info("checkUpdate");
+
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+ let {
+ savedAddresses,
+ savedBasicCards,
+ } = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => !!state.savedAddresses[address2GUID]
+ );
+
+ let addressGUIDs = Object.keys(savedAddresses);
+ is(addressGUIDs.length, 2, "Still two savedAddresses");
+ is(addressGUIDs[0], address1GUID, "Check first address GUID");
+ is(
+ savedAddresses[address1GUID].guid,
+ address1GUID,
+ "Check address 1 guid matches key"
+ );
+ is(addressGUIDs[1], address2GUID, "Check second address GUID");
+ is(
+ savedAddresses[address2GUID].guid,
+ address2GUID,
+ "Check address 2 guid matches key"
+ );
+
+ is(
+ Object.keys(savedBasicCards).length,
+ 1,
+ "Still one savedBasicCards"
+ );
+ is(
+ savedBasicCards[card1GUID].guid,
+ card1GUID,
+ "Check card guid matches key"
+ );
+ is(
+ savedBasicCards[card1GUID]["cc-exp-month"],
+ 6,
+ "Check expiry month"
+ );
+ is(
+ savedBasicCards[card1GUID]["cc-exp-year"],
+ 2029,
+ "Check expiry year"
+ );
+ is(
+ savedBasicCards[card1GUID].methodName,
+ "basic-card",
+ "Check card has a methodName of basic-card"
+ );
+ },
+ {
+ address1GUID,
+ address2GUID,
+ card1GUID,
+ }
+ );
+
+ onChanged = TestUtils.topicObserved(
+ "formautofill-storage-changed",
+ (subject, data) => data == "remove"
+ );
+ info("removing the first address");
+ formAutofillStorage.addresses.remove(address1GUID);
+ await onChanged;
+
+ await spawnPaymentDialogTask(
+ frame,
+ async function checkRemove({ address2GUID, card1GUID }) {
+ info("checkRemove");
+
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+ let {
+ savedAddresses,
+ savedBasicCards,
+ } = await PTU.DialogContentUtils.waitForState(
+ content,
+ state => !!state.savedAddresses[address2GUID]
+ );
+
+ is(Object.keys(savedAddresses).length, 1, "Now one savedAddresses");
+ is(
+ savedAddresses[address2GUID].name,
+ "Timothy Johann Berners-Lee",
+ "Check full name"
+ );
+ is(
+ savedAddresses[address2GUID].guid,
+ address2GUID,
+ "Check address guid matches key"
+ );
+
+ is(
+ Object.keys(savedBasicCards).length,
+ 1,
+ "Still one savedBasicCards"
+ );
+ is(
+ savedBasicCards[card1GUID]["cc-number"],
+ "************1111",
+ "Check cc-number"
+ );
+ is(
+ savedBasicCards[card1GUID].guid,
+ card1GUID,
+ "Check card guid matches key"
+ );
+ },
+ {
+ address2GUID,
+ card1GUID,
+ }
+ );
+
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
diff --git a/browser/components/payments/test/browser/browser_request_serialization.js b/browser/components/payments/test/browser/browser_request_serialization.js
new file mode 100644
index 0000000000..63ed2d71c7
--- /dev/null
+++ b/browser/components/payments/test/browser/browser_request_serialization.js
@@ -0,0 +1,250 @@
+"use strict";
+
+add_task(async function test_serializeRequest_displayItems() {
+ const testTask = ({ methodData, details }) => {
+ let contentWin = Cu.waiveXrays(content);
+ let store = contentWin.document.querySelector("payment-dialog")
+ .requestStore;
+ let state = store && store.getState();
+ ok(state, "got request store state");
+
+ let expected = details;
+ let actual = state.request.paymentDetails;
+ if (expected.displayItems) {
+ is(
+ actual.displayItems.length,
+ expected.displayItems.length,
+ "displayItems have same length"
+ );
+ for (let i = 0; i < actual.displayItems.length; i++) {
+ let item = actual.displayItems[i],
+ expectedItem = expected.displayItems[i];
+ is(item.label, expectedItem.label, "displayItem label matches");
+ is(
+ item.amount.value,
+ expectedItem.amount.value,
+ "displayItem label matches"
+ );
+ is(
+ item.amount.currency,
+ expectedItem.amount.currency,
+ "displayItem label matches"
+ );
+ }
+ } else {
+ is(
+ actual.displayItems,
+ null,
+ "falsey input displayItems is serialized to null"
+ );
+ }
+ };
+ const args = {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.twoDisplayItems,
+ PTU.Details.total32USD
+ ),
+ };
+ await spawnInDialogForMerchantTask(
+ PTU.ContentTasks.createAndShowRequest,
+ testTask,
+ args
+ );
+});
+
+add_task(async function test_serializeRequest_shippingOptions() {
+ const testTask = ({ methodData, details, options }) => {
+ let contentWin = Cu.waiveXrays(content);
+ let store = contentWin.document.querySelector("payment-dialog")
+ .requestStore;
+ let state = store && store.getState();
+ ok(state, "got request store state");
+
+ // The following test cases are conditionally todo because
+ // the spec currently does not state the shippingOptions
+ // should be null when requestShipping is not set. A future
+ // spec change (bug 1436903 comments 7-12) will fix this.
+ let cond_is = options && options.requestShipping ? is : todo_is;
+
+ let expected = details;
+ let actual = state.request.paymentDetails;
+ if (expected.shippingOptions) {
+ cond_is(
+ actual.shippingOptions.length,
+ expected.shippingOptions.length,
+ "shippingOptions have same length"
+ );
+ for (let i = 0; i < actual.shippingOptions.length; i++) {
+ let item = actual.shippingOptions[i],
+ expectedItem = expected.shippingOptions[i];
+ cond_is(item.label, expectedItem.label, "shippingOption label matches");
+ cond_is(
+ item.amount.value,
+ expectedItem.amount.value,
+ "shippingOption label matches"
+ );
+ cond_is(
+ item.amount.currency,
+ expectedItem.amount.currency,
+ "shippingOption label matches"
+ );
+ }
+ } else {
+ cond_is(
+ actual.shippingOptions,
+ null,
+ "falsey input shippingOptions is serialized to null"
+ );
+ }
+ };
+
+ const argsTestCases = [
+ {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+ ),
+ },
+ {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+ ),
+ options: PTU.Options.requestShippingOption,
+ },
+ ];
+ for (let args of argsTestCases) {
+ await spawnInDialogForMerchantTask(
+ PTU.ContentTasks.createAndShowRequest,
+ testTask,
+ args
+ );
+ }
+});
+
+add_task(async function test_serializeRequest_paymentMethods() {
+ const testTask = ({ methodData, details }) => {
+ let contentWin = Cu.waiveXrays(content);
+ let store = contentWin.document.querySelector("payment-dialog")
+ .requestStore;
+ let state = store && store.getState();
+ ok(state, "got request store state");
+
+ let result = state.request;
+ is(result.paymentMethods.length, 2, "Correct number of payment methods");
+ ok(
+ result.paymentMethods[0].supportedMethods &&
+ result.paymentMethods[1].supportedMethods,
+ "Both payment methods look valid"
+ );
+
+ let cardMethod = result.paymentMethods.find(
+ m => m.supportedMethods == "basic-card"
+ );
+ is(
+ cardMethod.data.supportedNetworks.length,
+ 2,
+ "Correct number of supportedNetworks"
+ );
+ ok(
+ cardMethod.data.supportedNetworks.includes("visa") &&
+ cardMethod.data.supportedNetworks.includes("mastercard"),
+ "Got the expected supportedNetworks contents"
+ );
+ };
+ let basicCardMethod = Object.assign({}, PTU.MethodData.basicCard, {
+ data: {
+ supportedNetworks: ["visa", "mastercard"],
+ },
+ });
+ const args = {
+ methodData: [basicCardMethod, PTU.MethodData.bobPay],
+ details: PTU.Details.total60USD,
+ };
+ await spawnInDialogForMerchantTask(
+ PTU.ContentTasks.createAndShowRequest,
+ testTask,
+ args
+ );
+});
+
+add_task(async function test_serializeRequest_modifiers() {
+ const testTask = ({ methodData, details }) => {
+ let contentWin = Cu.waiveXrays(content);
+ let store = contentWin.document.querySelector("payment-dialog")
+ .requestStore;
+ let state = store && store.getState();
+ ok(state, "got request store state");
+
+ let expected = details;
+ let actual = state.request.paymentDetails;
+
+ is(
+ actual.modifiers.length,
+ expected.modifiers.length,
+ "modifiers have same length"
+ );
+ for (let i = 0; i < actual.modifiers.length; i++) {
+ let item = actual.modifiers[i],
+ expectedItem = expected.modifiers[i];
+ is(
+ item.supportedMethods,
+ expectedItem.supportedMethods,
+ "modifier supportedMethods matches"
+ );
+
+ is(
+ item.additionalDisplayItems[0].label,
+ expectedItem.additionalDisplayItems[0].label,
+ "additionalDisplayItems label matches"
+ );
+ is(
+ item.additionalDisplayItems[0].amount.value,
+ expectedItem.additionalDisplayItems[0].amount.value,
+ "additionalDisplayItems amount value matches"
+ );
+ is(
+ item.additionalDisplayItems[0].amount.currency,
+ expectedItem.additionalDisplayItems[0].amount.currency,
+ "additionalDisplayItems amount currency matches"
+ );
+
+ is(
+ item.total.label,
+ expectedItem.total.label,
+ "modifier total label matches"
+ );
+ is(
+ item.total.amount.value,
+ expectedItem.total.amount.value,
+ "modifier label matches"
+ );
+ is(
+ item.total.amount.currency,
+ expectedItem.total.amount.currency,
+ "modifier total currency matches"
+ );
+ }
+ };
+
+ const args = {
+ methodData: [PTU.MethodData.basicCard, PTU.MethodData.bobPay],
+ details: Object.assign(
+ {},
+ PTU.Details.twoDisplayItems,
+ PTU.Details.bobPayPaymentModifier,
+ PTU.Details.total2USD
+ ),
+ };
+ await spawnInDialogForMerchantTask(
+ PTU.ContentTasks.createAndShowRequest,
+ testTask,
+ args
+ );
+});
diff --git a/browser/components/payments/test/browser/browser_request_shipping.js b/browser/components/payments/test/browser/browser_request_shipping.js
new file mode 100644
index 0000000000..480cd313ef
--- /dev/null
+++ b/browser/components/payments/test/browser/browser_request_shipping.js
@@ -0,0 +1,121 @@
+"use strict";
+
+add_task(async function setup() {
+ await addSampleAddressesAndBasicCard();
+});
+
+add_task(async function test_request_shipping_present() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ for (let [shippingKey, shippingString] of [
+ [null, "Shipping Address"],
+ ["shipping", "Shipping Address"],
+ ["delivery", "Delivery Address"],
+ ["pickup", "Pickup Address"],
+ ]) {
+ let options = {
+ requestShipping: true,
+ };
+ if (shippingKey) {
+ options.shippingType = shippingKey;
+ }
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+ ),
+ options,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ async ([aShippingKey, aShippingString]) => {
+ let shippingOptionPicker = content.document.querySelector(
+ "shipping-option-picker"
+ );
+ ok(
+ content.isVisible(shippingOptionPicker),
+ "shipping-option-picker should be visible"
+ );
+ const addressSelector =
+ "address-picker[selected-state-key='selectedShippingAddress']";
+ let shippingAddressPicker = content.document.querySelector(
+ addressSelector
+ );
+ ok(
+ content.isVisible(shippingAddressPicker),
+ "shipping address picker should be visible"
+ );
+ let shippingOption = shippingAddressPicker.querySelector("label");
+ is(
+ shippingOption.textContent,
+ aShippingString,
+ "Label should be match shipping type: " + aShippingKey
+ );
+ },
+ [shippingKey, shippingString]
+ );
+
+ spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.manuallyClickCancel
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ }
+ );
+});
+
+add_task(async function test_request_shipping_not_present() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+ ),
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await spawnPaymentDialogTask(frame, async () => {
+ let shippingOptionPicker = content.document.querySelector(
+ "shipping-option-picker"
+ );
+ ok(
+ content.isHidden(shippingOptionPicker),
+ "shipping-option-picker should not be visible"
+ );
+ const addressSelector =
+ "address-picker[selected-state-key='selectedShippingAddress']";
+ let shippingAddress = content.document.querySelector(addressSelector);
+ ok(
+ content.isHidden(shippingAddress),
+ "shipping address picker should not be visible"
+ );
+ });
+
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
diff --git a/browser/components/payments/test/browser/browser_retry.js b/browser/components/payments/test/browser/browser_retry.js
new file mode 100644
index 0000000000..6998bac874
--- /dev/null
+++ b/browser/components/payments/test/browser/browser_retry.js
@@ -0,0 +1,169 @@
+"use strict";
+
+/**
+ * Test the merchant calling .retry().
+ */
+
+async function setup() {
+ await setupFormAutofillStorage();
+ await cleanupFormAutofillStorage();
+ let billingAddressGUID = await addAddressRecord(PTU.Addresses.TimBL);
+ let card = Object.assign({}, PTU.BasicCards.JohnDoe, { billingAddressGUID });
+ let card1GUID = await addCardRecord(card);
+ return { address1GUID: billingAddressGUID, card1GUID };
+}
+
+add_task(async function test_retry_with_genericError() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(false, "Cannot test OS key store login on official builds.");
+ return;
+ }
+ let prefilledGuids = await setup();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign({}, PTU.Details.total60USD),
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ async ({ prefilledGuids: guids }) => {
+ let paymentMethodPicker = content.document.querySelector(
+ "payment-method-picker"
+ );
+ content.fillField(
+ Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
+ guids.card1GUID
+ );
+ },
+ { prefilledGuids }
+ );
+
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.setSecurityCode,
+ {
+ securityCode: "123",
+ }
+ );
+
+ info("clicking the button to try pay the 1st time");
+ await loginAndCompletePayment(frame);
+
+ let retryUpdatePromise = spawnPaymentDialogTask(
+ frame,
+ async function checkDialog() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let state = await PTU.DialogContentUtils.waitForState(
+ content,
+ ({ request }) => {
+ return request.completeStatus === "processing";
+ },
+ "Wait for completeStatus from pay button click"
+ );
+
+ is(
+ state.request.completeStatus,
+ "processing",
+ "Check completeStatus is processing"
+ );
+ is(
+ state.request.paymentDetails.error,
+ "",
+ "Check error string is empty"
+ );
+ ok(state.changesPrevented, "Changes prevented");
+
+ state = await PTU.DialogContentUtils.waitForState(
+ content,
+ ({ request }) => {
+ return request.completeStatus === "";
+ },
+ "Wait for completeStatus from DOM update"
+ );
+
+ is(state.request.completeStatus, "", "Check completeStatus");
+ is(
+ state.request.paymentDetails.error,
+ "My generic error",
+ "Check error string in state"
+ );
+ ok(!state.changesPrevented, "Changes no longer prevented");
+ is(
+ state.page.id,
+ "payment-summary",
+ "Check still on payment-summary"
+ );
+
+ ok(
+ content.document
+ .querySelector("payment-dialog")
+ .innerText.includes("My generic error"),
+ "Check error visibility"
+ );
+ }
+ );
+
+ // Add a handler to retry the payment above.
+ info("Tell merchant page to retry with an error string");
+ let retryPromise = SpecialPowers.spawn(
+ browser,
+ [
+ {
+ delayMs: 1000,
+ validationErrors: {
+ error: "My generic error",
+ },
+ },
+ ],
+ PTU.ContentTasks.addRetryHandler
+ );
+
+ await retryUpdatePromise;
+ await loginAndCompletePayment(frame);
+
+ // We can only check the retry response after the closing as it only resolves upon complete.
+ let { retryException } = await retryPromise;
+ ok(
+ !retryException,
+ "Expect no exception to be thrown when calling retry()"
+ );
+
+ // Add a handler to complete the payment above.
+ info("acknowledging the completion from the merchant page");
+ let result = await SpecialPowers.spawn(
+ browser,
+ [],
+ PTU.ContentTasks.addCompletionHandler
+ );
+
+ // Verify response has the expected properties
+ let expectedDetails = Object.assign(
+ {
+ "cc-security-code": "123",
+ },
+ PTU.BasicCards.JohnDoe
+ );
+
+ checkPaymentMethodDetailsMatchesCard(
+ result.response.details,
+ expectedDetails,
+ "Check response payment details"
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
diff --git a/browser/components/payments/test/browser/browser_retry_fieldErrors.js b/browser/components/payments/test/browser/browser_retry_fieldErrors.js
new file mode 100644
index 0000000000..12bbee2c1d
--- /dev/null
+++ b/browser/components/payments/test/browser/browser_retry_fieldErrors.js
@@ -0,0 +1,882 @@
+"use strict";
+
+/**
+ * Test the merchant calling .retry() with field-specific errors.
+ */
+
+async function setup() {
+ await setupFormAutofillStorage();
+ await cleanupFormAutofillStorage();
+ // add 2 addresses and 2 cards to avoid the FTU sequence and test address errors
+ let prefilledGuids = await addSampleAddressesAndBasicCard(
+ [PTU.Addresses.TimBL, PTU.Addresses.TimBL2],
+ [PTU.BasicCards.JaneMasterCard, PTU.BasicCards.JohnDoe]
+ );
+
+ info("associating card1 with a billing address");
+ await formAutofillStorage.creditCards.update(
+ prefilledGuids.card1GUID,
+ {
+ billingAddressGUID: prefilledGuids.address1GUID,
+ },
+ true
+ );
+ info("associating card2 with a billing address");
+ await formAutofillStorage.creditCards.update(
+ prefilledGuids.card2GUID,
+ {
+ billingAddressGUID: prefilledGuids.address1GUID,
+ },
+ true
+ );
+
+ return prefilledGuids;
+}
+
+add_task(async function test_retry_with_shippingAddressErrors() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(false, "Cannot test OS key store login on official builds.");
+ return;
+ }
+ let prefilledGuids = await setup();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total60USD
+ ),
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await selectPaymentDialogShippingAddressByCountry(frame, "DE");
+
+ await spawnPaymentDialogTask(
+ frame,
+ async ({ prefilledGuids: guids }) => {
+ let paymentMethodPicker = content.document.querySelector(
+ "payment-method-picker"
+ );
+ content.fillField(
+ Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
+ guids.card2GUID
+ );
+ },
+ { prefilledGuids }
+ );
+
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.setSecurityCode,
+ {
+ securityCode: "123",
+ }
+ );
+
+ info("clicking the button to try pay the 1st time");
+ await loginAndCompletePayment(frame);
+
+ let retryUpdatePromise = spawnPaymentDialogTask(
+ frame,
+ async function checkDialog() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let state = await PTU.DialogContentUtils.waitForState(
+ content,
+ ({ request }) => {
+ return request.completeStatus === "processing";
+ },
+ "Wait for completeStatus from pay button click"
+ );
+
+ is(
+ state.request.completeStatus,
+ "processing",
+ "Check completeStatus is processing"
+ );
+ is(
+ state.request.paymentDetails.shippingAddressErrors.country,
+ undefined,
+ "Check country error string is empty"
+ );
+ ok(state.changesPrevented, "Changes prevented");
+
+ state = await PTU.DialogContentUtils.waitForState(
+ content,
+ ({ request }) => {
+ return request.completeStatus === "";
+ },
+ "Wait for completeStatus from DOM update"
+ );
+
+ is(state.request.completeStatus, "", "Check completeStatus");
+ is(
+ state.request.paymentDetails.shippingAddressErrors.country,
+ "Can only ship to USA",
+ "Check country error string in state"
+ );
+ ok(!state.changesPrevented, "Changes no longer prevented");
+ is(
+ state.page.id,
+ "payment-summary",
+ "Check still on payment-summary"
+ );
+
+ ok(
+ content.document
+ .querySelector("#payment-summary")
+ .innerText.includes("Can only ship to USA"),
+ "Check error visibility on summary page"
+ );
+ ok(
+ content.document.getElementById("pay").disabled,
+ "Pay button should be disabled until the field error is addressed"
+ );
+ }
+ );
+
+ // Add a handler to retry the payment above.
+ info("Tell merchant page to retry with a country error string");
+ let retryPromise = SpecialPowers.spawn(
+ browser,
+ [
+ {
+ delayMs: 1000,
+ validationErrors: {
+ shippingAddress: {
+ country: "Can only ship to USA",
+ },
+ },
+ },
+ ],
+ PTU.ContentTasks.addRetryHandler
+ );
+
+ await retryUpdatePromise;
+
+ info("Changing to a US address to clear the error");
+ await selectPaymentDialogShippingAddressByCountry(frame, "US");
+
+ info("Tell merchant page to retry with a regionCode error string");
+ let retryPromise2 = SpecialPowers.spawn(
+ browser,
+ [
+ {
+ delayMs: 1000,
+ validationErrors: {
+ shippingAddress: {
+ regionCode: "Can only ship to California",
+ },
+ },
+ },
+ ],
+ PTU.ContentTasks.addRetryHandler
+ );
+
+ await loginAndCompletePayment(frame);
+
+ await spawnPaymentDialogTask(frame, async function checkRegionError() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let state = await PTU.DialogContentUtils.waitForState(
+ content,
+ ({ request }) => {
+ return request.completeStatus === "";
+ },
+ "Wait for completeStatus from DOM update"
+ );
+
+ is(state.request.completeStatus, "", "Check completeStatus");
+ is(
+ state.request.paymentDetails.shippingAddressErrors.regionCode,
+ "Can only ship to California",
+ "Check regionCode error string in state"
+ );
+ ok(!state.changesPrevented, "Changes no longer prevented");
+ is(state.page.id, "payment-summary", "Check still on payment-summary");
+
+ ok(
+ content.document
+ .querySelector("#payment-summary")
+ .innerText.includes("Can only ship to California"),
+ "Check error visibility on summary page"
+ );
+ ok(
+ content.document.getElementById("pay").disabled,
+ "Pay button should be disabled until the field error is addressed"
+ );
+ });
+
+ info(
+ "Changing the shipping state to CA without changing selectedShippingAddress"
+ );
+ await navigateToAddShippingAddressPage(frame, {
+ addLinkSelector:
+ 'address-picker[selected-state-key="selectedShippingAddress"] .edit-link',
+ });
+ await fillInShippingAddressForm(frame, { "address-level1": "CA" });
+ await submitAddressForm(frame, null, { isEditing: true });
+
+ await loginAndCompletePayment(frame);
+
+ // We can only check the retry response after the closing as it only resolves upon complete.
+ let { retryException } = await retryPromise;
+ ok(
+ !retryException,
+ "Expect no exception to be thrown when calling retry()"
+ );
+
+ let { retryException2 } = await retryPromise2;
+ ok(
+ !retryException2,
+ "Expect no exception to be thrown when calling retry()"
+ );
+
+ // Add a handler to complete the payment above.
+ info("acknowledging the completion from the merchant page");
+ let result = await SpecialPowers.spawn(
+ browser,
+ [],
+ PTU.ContentTasks.addCompletionHandler
+ );
+
+ // Verify response has the expected properties
+ let expectedDetails = Object.assign(
+ {
+ "cc-security-code": "123",
+ },
+ PTU.BasicCards.JohnDoe
+ );
+
+ checkPaymentMethodDetailsMatchesCard(
+ result.response.details,
+ expectedDetails,
+ "Check response payment details"
+ );
+ checkPaymentAddressMatchesStorageAddress(
+ result.response.shippingAddress,
+ { ...PTU.Addresses.TimBL, ...{ "address-level1": "CA" } },
+ "Check response shipping address"
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
+
+add_task(async function test_retry_with_payerErrors() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(false, "Cannot test OS key store login on official builds.");
+ return;
+ }
+ let prefilledGuids = await setup();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: PTU.Details.total60USD,
+ options: PTU.Options.requestPayerNameEmailAndPhone,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ async ({ prefilledGuids: guids }) => {
+ let paymentMethodPicker = content.document.querySelector(
+ "payment-method-picker"
+ );
+ content.fillField(
+ Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
+ guids.card2GUID
+ );
+ },
+ { prefilledGuids }
+ );
+
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.setSecurityCode,
+ {
+ securityCode: "123",
+ }
+ );
+
+ info("clicking the button to try pay the 1st time");
+ await loginAndCompletePayment(frame);
+
+ let retryUpdatePromise = spawnPaymentDialogTask(
+ frame,
+ async function checkDialog() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let state = await PTU.DialogContentUtils.waitForState(
+ content,
+ ({ request }) => {
+ return request.completeStatus === "processing";
+ },
+ "Wait for completeStatus from pay button click"
+ );
+
+ is(
+ state.request.completeStatus,
+ "processing",
+ "Check completeStatus is processing"
+ );
+
+ is(
+ state.request.paymentDetails.payerErrors.email,
+ undefined,
+ "Check email error isn't present"
+ );
+ ok(state.changesPrevented, "Changes prevented");
+
+ state = await PTU.DialogContentUtils.waitForState(
+ content,
+ ({ request }) => {
+ return request.completeStatus === "";
+ },
+ "Wait for completeStatus from DOM update"
+ );
+
+ is(state.request.completeStatus, "", "Check completeStatus");
+ is(
+ state.request.paymentDetails.payerErrors.email,
+ "You must use your employee email address",
+ "Check email error string in state"
+ );
+ ok(!state.changesPrevented, "Changes no longer prevented");
+ is(
+ state.page.id,
+ "payment-summary",
+ "Check still on payment-summary"
+ );
+
+ ok(
+ content.document
+ .querySelector("#payment-summary")
+ .innerText.includes("You must use your employee email address"),
+ "Check error visibility on summary page"
+ );
+ ok(
+ content.document.getElementById("pay").disabled,
+ "Pay button should be disabled until the field error is addressed"
+ );
+ }
+ );
+
+ // Add a handler to retry the payment above.
+ info("Tell merchant page to retry with a country error string");
+ let retryPromise = SpecialPowers.spawn(
+ browser,
+ [
+ {
+ delayMs: 1000,
+ validationErrors: {
+ payer: {
+ email: "You must use your employee email address",
+ },
+ },
+ },
+ ],
+ PTU.ContentTasks.addRetryHandler
+ );
+
+ await retryUpdatePromise;
+
+ info("Changing to a different email address to clear the error");
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.selectPayerAddressByGuid,
+ prefilledGuids.address1GUID
+ );
+
+ info("Tell merchant page to retry with a phone error string");
+ let retryPromise2 = SpecialPowers.spawn(
+ browser,
+ [
+ {
+ delayMs: 1000,
+ validationErrors: {
+ payer: {
+ phone: "Your phone number isn't valid",
+ },
+ },
+ },
+ ],
+ PTU.ContentTasks.addRetryHandler
+ );
+
+ await loginAndCompletePayment(frame);
+
+ await spawnPaymentDialogTask(frame, async function checkRegionError() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let state = await PTU.DialogContentUtils.waitForState(
+ content,
+ ({ request }) => {
+ return request.completeStatus === "";
+ },
+ "Wait for completeStatus from DOM update"
+ );
+
+ is(state.request.completeStatus, "", "Check completeStatus");
+ is(
+ state.request.paymentDetails.payerErrors.phone,
+ "Your phone number isn't valid",
+ "Check regionCode error string in state"
+ );
+ ok(!state.changesPrevented, "Changes no longer prevented");
+ is(state.page.id, "payment-summary", "Check still on payment-summary");
+
+ ok(
+ content.document
+ .querySelector("#payment-summary")
+ .innerText.includes("Your phone number isn't valid"),
+ "Check error visibility on summary page"
+ );
+ ok(
+ content.document.getElementById("pay").disabled,
+ "Pay button should be disabled until the field error is addressed"
+ );
+ });
+
+ info(
+ "Changing the payer phone to be valid without changing selectedPayerAddress"
+ );
+ await navigateToAddAddressPage(frame, {
+ addLinkSelector:
+ 'address-picker[selected-state-key="selectedPayerAddress"] .edit-link',
+ initialPageId: "payment-summary",
+ addressPageId: "payer-address-page",
+ });
+
+ let newPhoneNumber = "+16175555555";
+ await fillInPayerAddressForm(frame, { tel: newPhoneNumber });
+
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "payerdetailchange",
+ },
+ ],
+ PTU.ContentTasks.promisePaymentResponseEvent
+ );
+
+ await submitAddressForm(frame, null, { isEditing: true });
+
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "payerdetailchange",
+ },
+ ],
+ PTU.ContentTasks.awaitPaymentEventPromise
+ );
+
+ await loginAndCompletePayment(frame);
+
+ // We can only check the retry response after the closing as it only resolves upon complete.
+ let { retryException } = await retryPromise;
+ ok(
+ !retryException,
+ "Expect no exception to be thrown when calling retry()"
+ );
+
+ let { retryException2 } = await retryPromise2;
+ ok(
+ !retryException2,
+ "Expect no exception to be thrown when calling retry()"
+ );
+
+ // Add a handler to complete the payment above.
+ info("acknowledging the completion from the merchant page");
+ let result = await SpecialPowers.spawn(
+ browser,
+ [],
+ PTU.ContentTasks.addCompletionHandler
+ );
+
+ // Verify response has the expected properties
+ let expectedDetails = Object.assign(
+ {
+ "cc-security-code": "123",
+ },
+ PTU.BasicCards.JohnDoe
+ );
+
+ checkPaymentMethodDetailsMatchesCard(
+ result.response.details,
+ expectedDetails,
+ "Check response payment details"
+ );
+ let {
+ "given-name": givenName,
+ "additional-name": additionalName,
+ "family-name": familyName,
+ email,
+ } = PTU.Addresses.TimBL;
+ is(
+ result.response.payerName,
+ `${givenName} ${additionalName} ${familyName}`,
+ "Check payer name"
+ );
+ is(result.response.payerEmail, email, "Check payer email");
+ is(result.response.payerPhone, newPhoneNumber, "Check payer phone");
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
+
+add_task(async function test_retry_with_paymentMethodErrors() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(false, "Cannot test OS key store login on official builds.");
+ return;
+ }
+ let prefilledGuids = await setup();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: PTU.Details.total60USD,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ async ({ prefilledGuids: guids }) => {
+ let paymentMethodPicker = content.document.querySelector(
+ "payment-method-picker"
+ );
+ content.fillField(
+ Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
+ guids.card1GUID
+ );
+ },
+ { prefilledGuids }
+ );
+
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.setSecurityCode,
+ {
+ securityCode: "123",
+ }
+ );
+
+ info("clicking the button to try pay the 1st time");
+ await loginAndCompletePayment(frame);
+
+ let retryUpdatePromise = spawnPaymentDialogTask(
+ frame,
+ async function checkDialog() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let state = await PTU.DialogContentUtils.waitForState(
+ content,
+ ({ request }) => {
+ return request.completeStatus === "processing";
+ },
+ "Wait for completeStatus from pay button click"
+ );
+
+ is(
+ state.request.completeStatus,
+ "processing",
+ "Check completeStatus is processing"
+ );
+
+ is(
+ state.request.paymentDetails.paymentMethodErrors,
+ null,
+ "Check no paymentMethod errors are present"
+ );
+ ok(state.changesPrevented, "Changes prevented");
+
+ state = await PTU.DialogContentUtils.waitForState(
+ content,
+ ({ request }) => {
+ return request.completeStatus === "";
+ },
+ "Wait for completeStatus from DOM update"
+ );
+
+ is(state.request.completeStatus, "", "Check completeStatus");
+ is(
+ state.request.paymentDetails.paymentMethodErrors.cardSecurityCode,
+ "Your CVV is incorrect",
+ "Check cardSecurityCode error string in state"
+ );
+
+ ok(!state.changesPrevented, "Changes no longer prevented");
+ is(
+ state.page.id,
+ "payment-summary",
+ "Check still on payment-summary"
+ );
+
+ todo(
+ content.document
+ .querySelector("#payment-summary")
+ .innerText.includes("Your CVV is incorrect"),
+ "Bug 1491815: Check error visibility on summary page"
+ );
+ todo(
+ content.document.getElementById("pay").disabled,
+ "Bug 1491815: Pay button should be disabled until the field error is addressed"
+ );
+ }
+ );
+
+ // Add a handler to retry the payment above.
+ info("Tell merchant page to retry with a cardSecurityCode error string");
+ let retryPromise = SpecialPowers.spawn(
+ browser,
+ [
+ {
+ delayMs: 1000,
+ validationErrors: {
+ paymentMethod: {
+ cardSecurityCode: "Your CVV is incorrect",
+ },
+ },
+ },
+ ],
+ PTU.ContentTasks.addRetryHandler
+ );
+
+ await retryUpdatePromise;
+
+ info("Changing to a different card to clear the error");
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.selectPaymentOptionByGuid,
+ prefilledGuids.card1GUID
+ );
+
+ info(
+ "Tell merchant page to retry with a billing postalCode error string"
+ );
+ let retryPromise2 = SpecialPowers.spawn(
+ browser,
+ [
+ {
+ delayMs: 1000,
+ validationErrors: {
+ paymentMethod: {
+ billingAddress: {
+ postalCode: "Your postal code isn't valid",
+ },
+ },
+ },
+ },
+ ],
+ PTU.ContentTasks.addRetryHandler
+ );
+
+ await loginAndCompletePayment(frame);
+
+ await spawnPaymentDialogTask(
+ frame,
+ async function checkPostalCodeError() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let state = await PTU.DialogContentUtils.waitForState(
+ content,
+ ({ request }) => {
+ return request.completeStatus === "";
+ },
+ "Wait for completeStatus from DOM update"
+ );
+
+ is(state.request.completeStatus, "", "Check completeStatus");
+ is(
+ state.request.paymentDetails.paymentMethodErrors.billingAddress
+ .postalCode,
+ "Your postal code isn't valid",
+ "Check postalCode error string in state"
+ );
+ ok(!state.changesPrevented, "Changes no longer prevented");
+ is(
+ state.page.id,
+ "payment-summary",
+ "Check still on payment-summary"
+ );
+
+ todo(
+ content.document
+ .querySelector("#payment-summary")
+ .innerText.includes("Your postal code isn't valid"),
+ "Bug 1491815: Check error visibility on summary page"
+ );
+ todo(
+ content.document.getElementById("pay").disabled,
+ "Bug 1491815: Pay button should be disabled until the field error is addressed"
+ );
+ }
+ );
+
+ info(
+ "Changing the billingAddress postalCode to be valid without changing selectedPaymentCard"
+ );
+
+ await navigateToAddCardPage(frame, {
+ addLinkSelector: "payment-method-picker .edit-link",
+ });
+
+ await navigateToAddAddressPage(frame, {
+ addLinkSelector: ".billingAddressRow .edit-link",
+ initialPageId: "basic-card-page",
+ addressPageId: "billing-address-page",
+ });
+
+ let newPostalCode = "90210";
+ await fillInBillingAddressForm(frame, { "postal-code": newPostalCode });
+
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "paymentmethodchange",
+ },
+ ],
+ PTU.ContentTasks.promisePaymentResponseEvent
+ );
+
+ await submitAddressForm(frame, null, {
+ isEditing: true,
+ nextPageId: "basic-card-page",
+ });
+
+ await spawnPaymentDialogTask(frame, async function checkErrorsCleared() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.request.paymentDetails.paymentMethodErrors == null;
+ },
+ "Check no paymentMethod errors are present"
+ );
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.clickPrimaryButton
+ );
+
+ await spawnPaymentDialogTask(frame, async function checkErrorsCleared() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.request.paymentDetails.paymentMethodErrors == null;
+ },
+ "Check no card errors are present after save"
+ );
+ });
+
+ // TODO: Add an `await` here after bug 1477113.
+ SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "paymentmethodchange",
+ },
+ ],
+ PTU.ContentTasks.awaitPaymentEventPromise
+ );
+
+ await loginAndCompletePayment(frame);
+
+ // We can only check the retry response after the closing as it only resolves upon complete.
+ let { retryException } = await retryPromise;
+ ok(
+ !retryException,
+ "Expect no exception to be thrown when calling retry()"
+ );
+
+ let { retryException2 } = await retryPromise2;
+ ok(
+ !retryException2,
+ "Expect no exception to be thrown when calling retry()"
+ );
+
+ // Add a handler to complete the payment above.
+ info("acknowledging the completion from the merchant page");
+ let result = await SpecialPowers.spawn(
+ browser,
+ [],
+ PTU.ContentTasks.addCompletionHandler
+ );
+
+ // Verify response has the expected properties
+ let expectedDetails = Object.assign(
+ {
+ "cc-security-code": "123",
+ },
+ PTU.BasicCards.JaneMasterCard
+ );
+
+ let expectedBillingAddress = Object.assign({}, PTU.Addresses.TimBL, {
+ "postal-code": newPostalCode,
+ });
+
+ checkPaymentMethodDetailsMatchesCard(
+ result.response.details,
+ expectedDetails,
+ "Check response payment details"
+ );
+ checkPaymentAddressMatchesStorageAddress(
+ result.response.details.billingAddress,
+ expectedBillingAddress,
+ "Check response billing address"
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
diff --git a/browser/components/payments/test/browser/browser_shippingaddresschange_error.js b/browser/components/payments/test/browser/browser_shippingaddresschange_error.js
new file mode 100644
index 0000000000..803296eea6
--- /dev/null
+++ b/browser/components/payments/test/browser/browser_shippingaddresschange_error.js
@@ -0,0 +1,446 @@
+/* eslint-disable no-shadow */
+
+"use strict";
+
+add_task(addSampleAddressesAndBasicCard);
+
+add_task(async function test_show_error_on_addresschange() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+ ),
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ info("setting up the event handler for shippingoptionchange");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingoptionchange",
+ details: Object.assign(
+ {},
+ PTU.Details.genericShippingError,
+ PTU.Details.noShippingOptions,
+ PTU.Details.total2USD
+ ),
+ },
+ ],
+ PTU.ContentTasks.updateWith
+ );
+
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.selectShippingOptionById,
+ "1"
+ );
+
+ info("awaiting the shippingoptionchange event");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingoptionchange",
+ },
+ ],
+ PTU.ContentTasks.awaitPaymentEventPromise
+ );
+
+ await spawnPaymentDialogTask(
+ frame,
+ expectedText => {
+ let errorText = content.document.querySelector("header .page-error");
+ is(
+ errorText.textContent,
+ expectedText,
+ "Error text should be on dialog"
+ );
+ ok(content.isVisible(errorText), "Error text should be visible");
+ },
+ PTU.Details.genericShippingError.error
+ );
+
+ info("setting up the event handler for shippingaddresschange");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingaddresschange",
+ details: Object.assign(
+ {},
+ PTU.Details.noError,
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+ ),
+ },
+ ],
+ PTU.ContentTasks.updateWith
+ );
+
+ await selectPaymentDialogShippingAddressByCountry(frame, "DE");
+
+ info("awaiting the shippingaddresschange event");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingaddresschange",
+ },
+ ],
+ PTU.ContentTasks.awaitPaymentEventPromise
+ );
+
+ await spawnPaymentDialogTask(frame, () => {
+ let errorText = content.document.querySelector("header .page-error");
+ is(errorText.textContent, "", "Error text should not be on dialog");
+ ok(content.isHidden(errorText), "Error text should not be visible");
+ });
+
+ info("clicking cancel");
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
+
+add_task(async function test_show_field_specific_error_on_addresschange() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+ ),
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ info("setting up the event handler for shippingaddresschange");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingaddresschange",
+ details: Object.assign(
+ {},
+ PTU.Details.fieldSpecificErrors,
+ PTU.Details.noShippingOptions,
+ PTU.Details.total2USD
+ ),
+ },
+ ],
+ PTU.ContentTasks.updateWith
+ );
+
+ spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.selectShippingAddressByCountry,
+ "DE"
+ );
+
+ info("awaiting the shippingaddresschange event");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingaddresschange",
+ },
+ ],
+ PTU.ContentTasks.awaitPaymentEventPromise
+ );
+
+ await spawnPaymentDialogTask(frame, async () => {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return Object.keys(
+ state.request.paymentDetails.shippingAddressErrors
+ ).length;
+ },
+ "Check that there are shippingAddressErrors"
+ );
+
+ is(
+ content.document.querySelector("header .page-error").textContent,
+ PTU.Details.fieldSpecificErrors.error,
+ "Error text should be present on dialog"
+ );
+
+ info("click the Edit link");
+ content.document
+ .querySelector("address-picker.shipping-related .edit-link")
+ .click();
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ state.page.id == "shipping-address-page" &&
+ state["shipping-address-page"].guid
+ );
+ },
+ "Check edit page state"
+ );
+
+ // check errors and make corrections
+ let addressForm = content.document.querySelector(
+ "#shipping-address-page"
+ );
+ let { shippingAddressErrors } = PTU.Details.fieldSpecificErrors;
+ is(
+ addressForm.querySelectorAll(".error-text:not(:empty)").length,
+ Object.keys(shippingAddressErrors).length - 1,
+ "Each error should be presented, but only one of region and regionCode are displayed"
+ );
+ let errorFieldMap = Cu.waiveXrays(addressForm)._errorFieldMap;
+ for (let [errorName, errorValue] of Object.entries(
+ shippingAddressErrors
+ )) {
+ if (errorName == "region" || errorName == "regionCode") {
+ errorValue = shippingAddressErrors.regionCode;
+ }
+ let fieldSelector = errorFieldMap[errorName];
+ let containerSelector = fieldSelector + "-container";
+ let container = addressForm.querySelector(containerSelector);
+ try {
+ is(
+ container.querySelector(".error-text").textContent,
+ errorValue,
+ "Field specific error should be associated with " + errorName
+ );
+ } catch (ex) {
+ ok(
+ false,
+ `no container for ${errorName}. selector= ${containerSelector}`
+ );
+ }
+ try {
+ let field = addressForm.querySelector(fieldSelector);
+ let oldValue = field.value;
+ if (field.localName == "select") {
+ // Flip between empty and the selected entry so country fields won't change.
+ content.fillField(field, "");
+ content.fillField(field, oldValue);
+ } else {
+ content.fillField(
+ field,
+ field.value
+ .split("")
+ .reverse()
+ .join("")
+ );
+ }
+ } catch (ex) {
+ ok(
+ false,
+ `no field found for ${errorName}. selector= ${fieldSelector}`
+ );
+ }
+ }
+ });
+
+ info(
+ "setting up the event handler for a 2nd shippingaddresschange with a different error"
+ );
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingaddresschange",
+ details: Object.assign(
+ {},
+ {
+ shippingAddressErrors: {
+ phone: "Invalid phone number",
+ },
+ },
+ PTU.Details.noShippingOptions,
+ PTU.Details.total2USD
+ ),
+ },
+ ],
+ PTU.ContentTasks.updateWith
+ );
+
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.clickPrimaryButton
+ );
+
+ await spawnPaymentDialogTask(frame, async function checkForNewErrors() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ state.page.id == "payment-summary" &&
+ state.request.paymentDetails.shippingAddressErrors.phone ==
+ "Invalid phone number"
+ );
+ },
+ "Check the new error is in state"
+ );
+
+ ok(
+ content.document
+ .querySelector("#payment-summary")
+ .innerText.includes("Invalid phone number"),
+ "Check error visibility on summary page"
+ );
+ ok(
+ content.document.getElementById("pay").disabled,
+ "Pay button should be disabled until the field error is addressed"
+ );
+ });
+
+ await navigateToAddShippingAddressPage(frame, {
+ addLinkSelector:
+ 'address-picker[selected-state-key="selectedShippingAddress"] .edit-link',
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ async function checkForNewErrorOnEdit() {
+ let addressForm = content.document.querySelector(
+ "#shipping-address-page"
+ );
+ is(
+ addressForm.querySelectorAll(".error-text:not(:empty)").length,
+ 1,
+ "Check one error shown"
+ );
+ }
+ );
+
+ await fillInShippingAddressForm(frame, {
+ tel: PTU.Addresses.TimBL2.tel,
+ });
+
+ info("setup updateWith to clear errors");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingaddresschange",
+ details: Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+ ),
+ },
+ ],
+ PTU.ContentTasks.updateWith
+ );
+
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.clickPrimaryButton
+ );
+
+ await spawnPaymentDialogTask(frame, async function fixLastError() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "payment-summary";
+ },
+ "Check we're back on summary view"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return !Object.keys(
+ state.request.paymentDetails.shippingAddressErrors
+ ).length;
+ },
+ "Check that there are no more shippingAddressErrors"
+ );
+
+ is(
+ content.document.querySelector("header .page-error").textContent,
+ "",
+ "Error text should not be present on dialog"
+ );
+
+ info("click the Edit link again");
+ content.document.querySelector("address-picker .edit-link").click();
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return (
+ state.page.id == "shipping-address-page" &&
+ state["shipping-address-page"].guid
+ );
+ },
+ "Check edit page state"
+ );
+
+ let addressForm = content.document.querySelector(
+ "#shipping-address-page"
+ );
+ // check no errors present
+ let errorTextSpans = addressForm.querySelectorAll(
+ ".error-text:not(:empty)"
+ );
+ for (let errorTextSpan of errorTextSpans) {
+ is(
+ errorTextSpan.textContent,
+ "",
+ "No errors should be present on the field"
+ );
+ }
+
+ info("click the Back button");
+ addressForm.querySelector(".back-button").click();
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "payment-summary";
+ },
+ "Check we're back on summary view"
+ );
+ });
+
+ info("clicking cancel");
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
diff --git a/browser/components/payments/test/browser/browser_show_dialog.js b/browser/components/payments/test/browser/browser_show_dialog.js
new file mode 100644
index 0000000000..f3492cfdd9
--- /dev/null
+++ b/browser/components/payments/test/browser/browser_show_dialog.js
@@ -0,0 +1,400 @@
+"use strict";
+
+const methodData = [PTU.MethodData.basicCard];
+const details = Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+);
+
+add_task(async function test_show_abort_dialog() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win } = await setupPaymentDialog(browser, {
+ methodData,
+ details,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ // abort the payment request
+ SpecialPowers.spawn(browser, [], async () => content.rq.abort());
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
+
+add_task(async function test_show_manualAbort_dialog() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData,
+ details,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
+
+add_task(async function test_show_completePayment() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(false, "Cannot test OS key store login on official builds.");
+ return;
+ }
+ let { address1GUID, card1GUID } = await addSampleAddressesAndBasicCard();
+
+ let onChanged = TestUtils.topicObserved(
+ "formautofill-storage-changed",
+ (subject, data) => data == "update"
+ );
+ info("associating the card with the billing address");
+ await formAutofillStorage.creditCards.update(
+ card1GUID,
+ {
+ billingAddressGUID: address1GUID,
+ },
+ true
+ );
+ await onChanged;
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData,
+ details,
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ info("select the shipping address");
+ await selectPaymentDialogShippingAddressByCountry(frame, "US");
+
+ await spawnPaymentDialogTask(
+ frame,
+ async ({ card1GUID: cardGuid }) => {
+ let paymentMethodPicker = content.document.querySelector(
+ "payment-method-picker"
+ );
+ content.fillField(
+ Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
+ cardGuid
+ );
+ },
+ { card1GUID }
+ );
+
+ info("entering CSC");
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.setSecurityCode,
+ {
+ securityCode: "999",
+ }
+ );
+ info("clicking pay");
+ await loginAndCompletePayment(frame);
+
+ // Add a handler to complete the payment above.
+ info("acknowledging the completion from the merchant page");
+ let result = await SpecialPowers.spawn(
+ browser,
+ [],
+ PTU.ContentTasks.addCompletionHandler
+ );
+
+ let { shippingAddress } = result.response;
+ checkPaymentAddressMatchesStorageAddress(
+ shippingAddress,
+ PTU.Addresses.TimBL,
+ "Shipping"
+ );
+
+ is(result.response.methodName, "basic-card", "Check methodName");
+ let { methodDetails } = result;
+ checkPaymentMethodDetailsMatchesCard(
+ methodDetails,
+ PTU.BasicCards.JohnDoe,
+ "Payment method"
+ );
+ is(methodDetails.cardSecurityCode, "999", "Check cardSecurityCode");
+ is(
+ typeof methodDetails.methodName,
+ "undefined",
+ "Check methodName wasn't included"
+ );
+
+ checkPaymentAddressMatchesStorageAddress(
+ methodDetails.billingAddress,
+ PTU.Addresses.TimBL,
+ "Billing address"
+ );
+
+ is(result.response.shippingOption, "2", "Check shipping option");
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
+
+add_task(async function test_show_completePayment2() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(false, "Cannot test OS key store login on official builds.");
+ return;
+ }
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData,
+ details,
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingoptionchange",
+ },
+ ],
+ PTU.ContentTasks.promisePaymentRequestEvent
+ );
+
+ info(
+ "changing shipping option to '1' from default selected option of '2'"
+ );
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.selectShippingOptionById,
+ "1"
+ );
+
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ eventName: "shippingoptionchange",
+ },
+ ],
+ PTU.ContentTasks.awaitPaymentEventPromise
+ );
+ info("got shippingoptionchange event");
+
+ info("select the shipping address");
+ await selectPaymentDialogShippingAddressByCountry(frame, "US");
+
+ await spawnPaymentDialogTask(frame, async () => {
+ let paymentMethodPicker = content.document.querySelector(
+ "payment-method-picker"
+ );
+ content.fillField(
+ Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
+ Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox.options[0].value
+ );
+ });
+
+ info("entering CSC");
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.setSecurityCode,
+ {
+ securityCode: "123",
+ }
+ );
+
+ info("clicking pay");
+ await loginAndCompletePayment(frame);
+
+ // Add a handler to complete the payment above.
+ info("acknowledging the completion from the merchant page");
+ let result = await SpecialPowers.spawn(
+ browser,
+ [],
+ PTU.ContentTasks.addCompletionHandler
+ );
+
+ is(result.response.shippingOption, "1", "Check shipping option");
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
+
+add_task(async function test_localized() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData,
+ details,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ await spawnPaymentDialogTask(frame, async function check_l10n() {
+ await ContentTaskUtils.waitForCondition(() => {
+ let telephoneLabel = content.document.querySelector(
+ "#tel-container > .label-text"
+ );
+ return telephoneLabel && telephoneLabel.textContent.includes("Phone");
+ }, "Check that the telephone number label is localized");
+
+ await ContentTaskUtils.waitForCondition(() => {
+ let ccNumberField = content.document.querySelector("#cc-number");
+ if (!ccNumberField) {
+ return false;
+ }
+ let ccNumberLabel = ccNumberField.parentElement.querySelector(
+ ".label-text"
+ );
+ return ccNumberLabel.textContent.includes("Number");
+ }, "Check that the cc-number label is localized");
+
+ const L10N_ATTRIBUTE_SELECTOR =
+ "[data-localization], [data-localization-region]";
+ await ContentTaskUtils.waitForCondition(() => {
+ return (
+ content.document.querySelectorAll(L10N_ATTRIBUTE_SELECTOR)
+ .length === 0
+ );
+ }, "Check that there are no unlocalized strings");
+ });
+
+ // abort the payment request
+ SpecialPowers.spawn(browser, [], async () => content.rq.abort());
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
+
+add_task(async function test_supportedNetworks() {
+ await setupFormAutofillStorage();
+ await cleanupFormAutofillStorage();
+
+ let address1GUID = await addAddressRecord(PTU.Addresses.TimBL);
+ let visaCardGUID = await addCardRecord(
+ Object.assign({}, PTU.BasicCards.JohnDoe, {
+ billingAddressGUID: address1GUID,
+ })
+ );
+ let masterCardGUID = await addCardRecord(
+ Object.assign({}, PTU.BasicCards.JaneMasterCard, {
+ billingAddressGUID: address1GUID,
+ })
+ );
+
+ let cardMethod = {
+ supportedMethods: "basic-card",
+ data: {
+ supportedNetworks: ["visa"],
+ },
+ };
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData: [cardMethod],
+ details,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ info("entering CSC");
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.setSecurityCode,
+ {
+ securityCode: "789",
+ }
+ );
+
+ await spawnPaymentDialogTask(frame, () => {
+ let acceptedCards = content.document.querySelector("accepted-cards");
+ ok(
+ acceptedCards && !content.isHidden(acceptedCards),
+ "accepted-cards element is present and visible"
+ );
+ is(
+ Cu.waiveXrays(acceptedCards).acceptedItems.length,
+ 1,
+ "accepted-cards element has 1 item"
+ );
+ });
+
+ info("select the mastercard using guid: " + masterCardGUID);
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.selectPaymentOptionByGuid,
+ masterCardGUID
+ );
+
+ info("spawn task to check pay button with mastercard selected");
+ await spawnPaymentDialogTask(frame, async () => {
+ ok(
+ content.document.getElementById("pay").disabled,
+ "pay button should be disabled"
+ );
+ });
+
+ info("select the visa using guid: " + visaCardGUID);
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.selectPaymentOptionByGuid,
+ visaCardGUID
+ );
+
+ info("spawn task to check pay button");
+ await spawnPaymentDialogTask(frame, async () => {
+ ok(
+ !content.document.getElementById("pay").disabled,
+ "pay button should not be disabled"
+ );
+ });
+
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+});
diff --git a/browser/components/payments/test/browser/browser_tab_modal.js b/browser/components/payments/test/browser/browser_tab_modal.js
new file mode 100644
index 0000000000..54a25df5e8
--- /dev/null
+++ b/browser/components/payments/test/browser/browser_tab_modal.js
@@ -0,0 +1,300 @@
+"use strict";
+
+const methodData = [PTU.MethodData.basicCard];
+const details = Object.assign(
+ {},
+ PTU.Details.twoShippingOptions,
+ PTU.Details.total2USD
+);
+
+async function checkTabModal(browser, win, msg) {
+ info(`checkTabModal: ${msg}`);
+ let doc = browser.ownerDocument;
+ await TestUtils.waitForCondition(() => {
+ return !doc.querySelector(".paymentDialogContainer").hidden;
+ }, "Waiting for container to be visible after the dialog's ready");
+ is(
+ doc.querySelectorAll(".paymentDialogContainer").length,
+ 1,
+ "Only 1 paymentDialogContainer"
+ );
+ ok(!EventUtils.isHidden(win.frameElement), "Frame should be visible");
+
+ let { bottom: toolboxBottom } = doc
+ .getElementById("navigator-toolbox")
+ .getBoundingClientRect();
+
+ let { x, y } = win.frameElement.getBoundingClientRect();
+ ok(y > 0, "Frame should have y > 0");
+ // Inset by 10px since the corner point doesn't return the frame due to the
+ // border-radius.
+ is(
+ doc.elementFromPoint(x + 10, y + 10),
+ win.frameElement,
+ "Check .paymentDialogContainerFrame is visible"
+ );
+
+ info("Click to the left of the dialog over the content area");
+ isnot(
+ doc.elementFromPoint(x - 10, y + 50),
+ browser,
+ "Check clicks on the merchant content area don't go to the browser"
+ );
+ is(
+ doc.elementFromPoint(x - 10, y + 50),
+ doc.querySelector(".paymentDialogBackground"),
+ "Check clicks on the merchant content area go to the payment dialog background"
+ );
+
+ ok(
+ y < toolboxBottom - 2,
+ "Dialog should overlap the toolbox by at least 2px"
+ );
+
+ ok(
+ browser.hasAttribute("tabmodalPromptShowing"),
+ "Check browser has @tabmodalPromptShowing"
+ );
+
+ return {
+ x,
+ y,
+ };
+}
+
+add_task(async function test_tab_modal() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async browser => {
+ let { win, frame } = await setupPaymentDialog(browser, {
+ methodData,
+ details,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ let { x, y } = await checkTabModal(browser, win, "initial dialog");
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ async newBrowser => {
+ let { x: x2, y: y2 } = win.frameElement.getBoundingClientRect();
+ is(x2, x, "Check x-coordinate is the same");
+ is(y2, y, "Check y-coordinate is the same");
+ isnot(
+ document.elementFromPoint(x + 10, y + 10),
+ win.frameElement,
+ "Check .paymentDialogContainerFrame is hidden"
+ );
+ ok(
+ !newBrowser.hasAttribute("tabmodalPromptShowing"),
+ "Check second browser doesn't have @tabmodalPromptShowing"
+ );
+ }
+ );
+
+ let { x: x3, y: y3 } = await checkTabModal(
+ browser,
+ win,
+ "after tab switch back"
+ );
+ is(x3, x, "Check x-coordinate is the same again");
+ is(y3, y, "Check y-coordinate is the same again");
+
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () => !browser.hasAttribute("tabmodalPromptShowing"),
+ "Check @tabmodalPromptShowing was removed"
+ );
+ }
+ );
+});
+
+add_task(async function test_detachToNewWindow() {
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ });
+ let browser = tab.linkedBrowser;
+
+ let { frame, requestId } = await setupPaymentDialog(browser, {
+ methodData,
+ details,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ });
+
+ is(
+ Object.values(frame.paymentDialogWrapper.temporaryStore.addresses.getAll())
+ .length,
+ 0,
+ "Check initial temp. address store"
+ );
+ is(
+ Object.values(
+ frame.paymentDialogWrapper.temporaryStore.creditCards.getAll()
+ ).length,
+ 0,
+ "Check initial temp. card store"
+ );
+
+ info("Create some temp. records so we can later check if they are preserved");
+ let address1 = { ...PTU.Addresses.Temp };
+ let card1 = { ...PTU.BasicCards.JaneMasterCard, ...{ "cc-csc": "123" } };
+
+ await fillInBillingAddressForm(frame, address1, {
+ setPersistCheckedValue: false,
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.clickPrimaryButton
+ );
+
+ await spawnPaymentDialogTask(frame, async function waitForPageChange() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "basic-card-page";
+ },
+ "Wait for basic-card-page"
+ );
+ });
+
+ await fillInCardForm(frame, card1, {
+ checkboxSelector: "basic-card-form .persist-checkbox",
+ setPersistCheckedValue: false,
+ });
+
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.clickPrimaryButton
+ );
+
+ let { temporaryStore } = frame.paymentDialogWrapper;
+ TestUtils.waitForCondition(() => {
+ return Object.values(temporaryStore.addresses.getAll()).length == 1;
+ }, "Check address store");
+ TestUtils.waitForCondition(() => {
+ return Object.values(temporaryStore.creditCards.getAll()).length == 1;
+ }, "Check card store");
+
+ let windowLoadedPromise = BrowserTestUtils.waitForNewWindow();
+ let newWin = gBrowser.replaceTabWithWindow(tab);
+ await windowLoadedPromise;
+
+ info("tab was detached");
+ let newBrowser = newWin.gBrowser.selectedBrowser;
+ ok(newBrowser, "Found new <browser>");
+
+ let widget = await TestUtils.waitForCondition(async () =>
+ getPaymentWidget(requestId)
+ );
+ await checkTabModal(newBrowser, widget, "after detach");
+
+ let state = await spawnPaymentDialogTask(
+ widget.frameElement,
+ async function checkAfterDetach() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ return PTU.DialogContentUtils.getCurrentState(content);
+ }
+ );
+
+ is(
+ Object.values(state.tempAddresses).length,
+ 1,
+ "Check still 1 temp. address in state"
+ );
+ is(
+ Object.values(state.tempBasicCards).length,
+ 1,
+ "Check still 1 temp. basic card in state"
+ );
+
+ temporaryStore = widget.frameElement.paymentDialogWrapper.temporaryStore;
+ is(
+ Object.values(temporaryStore.addresses.getAll()).length,
+ 1,
+ "Check address store in wrapper"
+ );
+ is(
+ Object.values(temporaryStore.creditCards.getAll()).length,
+ 1,
+ "Check card store in wrapper"
+ );
+
+ info(
+ "Check that the message manager and formautofill-storage-changed observer are connected"
+ );
+ is(Object.values(state.savedAddresses).length, 0, "Check 0 saved addresses");
+ await addAddressRecord(PTU.Addresses.TimBL2);
+ await spawnPaymentDialogTask(
+ widget.frameElement,
+ async function waitForSavedAddress() {
+ let { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ await PTU.DialogContentUtils.waitForState(
+ content,
+ function checkSavedAddresses(s) {
+ return Object.values(s.savedAddresses).length == 1;
+ },
+ "Check 1 saved address in state"
+ );
+ }
+ );
+
+ info(
+ "re-attach the tab back in the original window to test the event listeners were added"
+ );
+
+ let tab3 = gBrowser.adoptTab(newWin.gBrowser.selectedTab, 1, true);
+ widget = await TestUtils.waitForCondition(async () =>
+ getPaymentWidget(requestId)
+ );
+ is(
+ widget.frameElement.ownerGlobal,
+ window,
+ "Check widget is back in first window"
+ );
+ await checkTabModal(tab3.linkedBrowser, widget, "after re-attaching");
+
+ temporaryStore = widget.frameElement.paymentDialogWrapper.temporaryStore;
+ is(
+ Object.values(temporaryStore.addresses.getAll()).length,
+ 1,
+ "Check temp addresses in wrapper"
+ );
+ is(
+ Object.values(temporaryStore.creditCards.getAll()).length,
+ 1,
+ "Check temp cards in wrapper"
+ );
+
+ spawnPaymentDialogTask(
+ widget.frameElement,
+ PTU.DialogContentTasks.manuallyClickCancel
+ );
+ await BrowserTestUtils.waitForCondition(
+ () => widget.closed,
+ "dialog should be closed"
+ );
+ await BrowserTestUtils.removeTab(tab3);
+});
diff --git a/browser/components/payments/test/browser/browser_total.js b/browser/components/payments/test/browser/browser_total.js
new file mode 100644
index 0000000000..0f02d65c90
--- /dev/null
+++ b/browser/components/payments/test/browser/browser_total.js
@@ -0,0 +1,94 @@
+"use strict";
+
+add_task(async function test_total() {
+ const testTask = ({ methodData, details }) => {
+ is(
+ content.document.querySelector("#total > currency-amount").textContent,
+ "$60.00 USD",
+ "Check total currency amount"
+ );
+ };
+ const args = {
+ methodData: [PTU.MethodData.basicCard],
+ details: PTU.Details.total60USD,
+ };
+ await spawnInDialogForMerchantTask(
+ PTU.ContentTasks.createAndShowRequest,
+ testTask,
+ args
+ );
+});
+
+add_task(async function test_modifier_with_no_method_selected() {
+ const testTask = async ({ methodData, details }) => {
+ // There are no payment methods installed/setup so we expect the original (unmodified) total.
+ is(
+ content.document.querySelector("#total > currency-amount").textContent,
+ "$2.00 USD",
+ "Check unmodified total currency amount"
+ );
+ };
+ const args = {
+ methodData: [PTU.MethodData.bobPay, PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.bobPayPaymentModifier,
+ PTU.Details.total2USD
+ ),
+ };
+ await spawnInDialogForMerchantTask(
+ PTU.ContentTasks.createAndShowRequest,
+ testTask,
+ args
+ );
+});
+
+add_task(async function test_modifier_with_no_method_selected() {
+ info("adding a basic-card");
+ let prefilledGuids = await addSampleAddressesAndBasicCard();
+
+ const testTask = async ({ methodData, details, prefilledGuids: guids }) => {
+ is(
+ content.document.querySelector("#total > currency-amount").textContent,
+ "$2.00 USD",
+ "Check total currency amount before selecting the credit card"
+ );
+
+ // Select the (only) payment method.
+ let paymentMethodPicker = content.document.querySelector(
+ "payment-method-picker"
+ );
+ content.fillField(
+ Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
+ guids.card1GUID
+ );
+
+ await ContentTaskUtils.waitForCondition(() => {
+ let currencyAmount = content.document.querySelector(
+ "#total > currency-amount"
+ );
+ return currencyAmount.textContent == "$2.50 USD";
+ }, "Wait for modified total to update");
+
+ is(
+ content.document.querySelector("#total > currency-amount").textContent,
+ "$2.50 USD",
+ "Check modified total currency amount"
+ );
+ };
+ const args = {
+ methodData: [PTU.MethodData.bobPay, PTU.MethodData.basicCard],
+ details: Object.assign(
+ {},
+ PTU.Details.bobPayPaymentModifier,
+ PTU.Details.total2USD
+ ),
+ prefilledGuids,
+ };
+ await spawnInDialogForMerchantTask(
+ PTU.ContentTasks.createAndShowRequest,
+ testTask,
+ args
+ );
+ await cleanupFormAutofillStorage();
+});
diff --git a/browser/components/payments/test/browser/head.js b/browser/components/payments/test/browser/head.js
new file mode 100644
index 0000000000..b32776d5a4
--- /dev/null
+++ b/browser/components/payments/test/browser/head.js
@@ -0,0 +1,880 @@
+"use strict";
+
+/* eslint
+ "no-unused-vars": ["error", {
+ vars: "local",
+ args: "none",
+ }],
+*/
+
+const BLANK_PAGE_PATH =
+ "/browser/browser/components/payments/test/browser/blank_page.html";
+const BLANK_PAGE_URL = "https://example.com" + BLANK_PAGE_PATH;
+const RESPONSE_TIMEOUT_PREF = "dom.payments.response.timeout";
+const SAVE_CREDITCARD_DEFAULT_PREF = "dom.payments.defaults.saveCreditCard";
+const SAVE_ADDRESS_DEFAULT_PREF = "dom.payments.defaults.saveAddress";
+
+const paymentSrv = Cc[
+ "@mozilla.org/dom/payments/payment-request-service;1"
+].getService(Ci.nsIPaymentRequestService);
+const paymentUISrv = Cc[
+ "@mozilla.org/dom/payments/payment-ui-service;1"
+].getService(Ci.nsIPaymentUIService).wrappedJSObject;
+const { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+const { formAutofillStorage } = ChromeUtils.import(
+ "resource://formautofill/FormAutofillStorage.jsm"
+);
+const { OSKeyStoreTestUtils } = ChromeUtils.import(
+ "resource://testing-common/OSKeyStoreTestUtils.jsm"
+);
+const { PaymentTestUtils: PTU } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+);
+var { BrowserWindowTracker } = ChromeUtils.import(
+ "resource:///modules/BrowserWindowTracker.jsm"
+);
+var { CreditCard } = ChromeUtils.import(
+ "resource://gre/modules/CreditCard.jsm"
+);
+
+function getPaymentRequests() {
+ return Array.from(paymentSrv.enumerate());
+}
+
+/**
+ * Return the container (e.g. dialog or overlay) that the payment request contents are shown in.
+ * This abstracts away the details of the widget used so that this can more easily transition to
+ * another kind of dialog/overlay.
+ * @param {string} requestId
+ * @returns {Promise}
+ */
+async function getPaymentWidget(requestId) {
+ return BrowserTestUtils.waitForCondition(() => {
+ let { dialogContainer } = paymentUISrv.findDialog(requestId);
+ if (!dialogContainer) {
+ return false;
+ }
+ let paymentFrame = dialogContainer.querySelector(
+ ".paymentDialogContainerFrame"
+ );
+ if (!paymentFrame) {
+ return false;
+ }
+ return {
+ get closed() {
+ return !paymentFrame.isConnected;
+ },
+ frameElement: paymentFrame,
+ };
+ }, "payment dialog should be opened");
+}
+
+async function getPaymentFrame(widget) {
+ return widget.frameElement;
+}
+
+function waitForMessageFromWidget(messageType, widget = null) {
+ info("waitForMessageFromWidget: " + messageType);
+ return new Promise(resolve => {
+ Services.mm.addMessageListener(
+ "paymentContentToChrome",
+ function onMessage({ data, target }) {
+ if (data.messageType != messageType) {
+ return;
+ }
+ if (widget && widget != target) {
+ return;
+ }
+ resolve();
+ info(`Got ${messageType} from widget`);
+ Services.mm.removeMessageListener("paymentContentToChrome", onMessage);
+ }
+ );
+ });
+}
+
+async function waitForWidgetReady(widget = null) {
+ return waitForMessageFromWidget("paymentDialogReady", widget);
+}
+
+function spawnPaymentDialogTask(paymentDialogFrame, taskFn, args = null) {
+ return SpecialPowers.spawn(paymentDialogFrame.frameLoader, [args], taskFn);
+}
+
+async function withMerchantTab(
+ { browser = gBrowser, url = BLANK_PAGE_URL } = {
+ browser: gBrowser,
+ url: BLANK_PAGE_URL,
+ },
+ taskFn
+) {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: browser,
+ url,
+ },
+ taskFn
+ );
+
+ paymentSrv.cleanup(); // Temporary measure until bug 1408234 is fixed.
+
+ await new Promise(resolve => {
+ SpecialPowers.exactGC(resolve);
+ });
+}
+
+async function withNewTabInPrivateWindow(args = {}, taskFn) {
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ let tabArgs = Object.assign(args, {
+ browser: privateWin.gBrowser,
+ });
+ await withMerchantTab(tabArgs, taskFn);
+ await BrowserTestUtils.closeWindow(privateWin);
+}
+
+async function addAddressRecord(address) {
+ let onChanged = TestUtils.topicObserved(
+ "formautofill-storage-changed",
+ (subject, data) => data == "add"
+ );
+ let guid = await formAutofillStorage.addresses.add(address);
+ await onChanged;
+ return guid;
+}
+
+async function addCardRecord(card) {
+ let onChanged = TestUtils.topicObserved(
+ "formautofill-storage-changed",
+ (subject, data) => data == "add"
+ );
+ let guid = await formAutofillStorage.creditCards.add(card);
+ await onChanged;
+ return guid;
+}
+
+/**
+ * Add address and creditCard records to the formautofill store
+ *
+ * @param {array=} addresses - The addresses to add to the formautofill address store
+ * @param {array=} cards - The cards to add to the formautofill creditCards store
+ * @returns {Promise}
+ */
+async function addSampleAddressesAndBasicCard(
+ addresses = [PTU.Addresses.TimBL, PTU.Addresses.TimBL2],
+ cards = [PTU.BasicCards.JohnDoe]
+) {
+ let guids = {};
+
+ for (let i = 0; i < addresses.length; i++) {
+ guids[`address${i + 1}GUID`] = await addAddressRecord(addresses[i]);
+ }
+
+ for (let i = 0; i < cards.length; i++) {
+ guids[`card${i + 1}GUID`] = await addCardRecord(cards[i]);
+ }
+
+ return guids;
+}
+
+/**
+ * Checks that an address from autofill storage matches a Payment Request PaymentAddress.
+ * @param {PaymentAddress} paymentAddress
+ * @param {object} storageAddress
+ * @param {string} msg to describe the check
+ */
+function checkPaymentAddressMatchesStorageAddress(
+ paymentAddress,
+ storageAddress,
+ msg
+) {
+ info(msg);
+ let addressLines = storageAddress["street-address"].split("\n");
+ is(
+ paymentAddress.addressLine[0],
+ addressLines[0],
+ "Address line 1 should match"
+ );
+ is(
+ paymentAddress.addressLine[1],
+ addressLines[1],
+ "Address line 2 should match"
+ );
+ is(paymentAddress.country, storageAddress.country, "Country should match");
+ is(
+ paymentAddress.region,
+ storageAddress["address-level1"] || "",
+ "Region should match"
+ );
+ is(
+ paymentAddress.city,
+ storageAddress["address-level2"],
+ "City should match"
+ );
+ is(
+ paymentAddress.postalCode,
+ storageAddress["postal-code"],
+ "Zip code should match"
+ );
+ is(
+ paymentAddress.organization,
+ storageAddress.organization,
+ "Org should match"
+ );
+ is(
+ paymentAddress.recipient,
+ `${storageAddress["given-name"]} ${storageAddress["additional-name"]} ` +
+ `${storageAddress["family-name"]}`,
+ "Recipient name should match"
+ );
+ is(paymentAddress.phone, storageAddress.tel, "Phone should match");
+}
+
+/**
+ * Checks that a card from autofill storage matches a Payment Request MethodDetails response.
+ * @param {MethodDetails} methodDetails
+ * @param {object} card
+ * @param {string} msg to describe the check
+ */
+function checkPaymentMethodDetailsMatchesCard(methodDetails, card, msg) {
+ info(msg);
+ // The card expiry month should be a zero-padded two-digit string.
+ let cardExpiryMonth = card["cc-exp-month"].toString().padStart(2, "0");
+ is(methodDetails.cardholderName, card["cc-name"], "Check cardholderName");
+ is(methodDetails.cardNumber, card["cc-number"], "Check cardNumber");
+ is(methodDetails.expiryMonth, cardExpiryMonth, "Check expiryMonth");
+ is(methodDetails.expiryYear, card["cc-exp-year"], "Check expiryYear");
+}
+
+/**
+ * Create a PaymentRequest object with the given parameters, then
+ * run the given merchantTaskFn.
+ *
+ * @param {Object} browser
+ * @param {Object} options
+ * @param {Object} options.methodData
+ * @param {Object} options.details
+ * @param {Object} options.options
+ * @param {Function} options.merchantTaskFn
+ * @returns {Object} References to the window, requestId, and frame
+ */
+async function setupPaymentDialog(
+ browser,
+ { methodData, details, options, merchantTaskFn }
+) {
+ let dialogReadyPromise = waitForWidgetReady();
+ let { requestId } = await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ methodData,
+ details,
+ options,
+ },
+ ],
+ merchantTaskFn
+ );
+ ok(requestId, "requestId should be defined");
+
+ // get a reference to the UI dialog and the requestId
+ let [win] = await Promise.all([
+ getPaymentWidget(requestId),
+ dialogReadyPromise,
+ ]);
+ ok(win, "Got payment widget");
+ is(win.closed, false, "dialog should not be closed");
+
+ let frame = await getPaymentFrame(win);
+ ok(frame, "Got payment frame");
+
+ await dialogReadyPromise;
+ info("dialog ready");
+
+ await spawnPaymentDialogTask(frame, () => {
+ let elementHeight = element => element.getBoundingClientRect().height;
+ content.isHidden = element => elementHeight(element) == 0;
+ content.isVisible = element => elementHeight(element) > 0;
+ content.fillField = async function fillField(field, value) {
+ // Keep in-sync with the copy in payments_common.js but with EventUtils methods called on a
+ // EventUtils object.
+ field.focus();
+ if (field.localName == "select") {
+ if (field.value == value) {
+ // Do nothing
+ return;
+ }
+ field.value = value;
+ field.dispatchEvent(
+ new content.window.Event("input", { bubbles: true })
+ );
+ field.dispatchEvent(
+ new content.window.Event("change", { bubbles: true })
+ );
+ return;
+ }
+ while (field.value) {
+ EventUtils.sendKey("BACK_SPACE", content.window);
+ }
+ EventUtils.sendString(value, content.window);
+ };
+ });
+
+ return { win, requestId, frame };
+}
+
+/**
+ * Open a merchant tab with the given merchantTaskFn to create a PaymentRequest
+ * and then open the associated PaymentRequest dialog in a new tab and run the
+ * associated dialogTaskFn. The same taskArgs are passed to both functions.
+ *
+ * @param {Function} merchantTaskFn
+ * @param {Function} dialogTaskFn
+ * @param {Object} taskArgs
+ * @param {Object} options
+ * @param {string} options.origin
+ */
+async function spawnInDialogForMerchantTask(
+ merchantTaskFn,
+ dialogTaskFn,
+ taskArgs,
+ { browser, origin = "https://example.com" } = {
+ origin: "https://example.com",
+ }
+) {
+ await withMerchantTab(
+ {
+ browser,
+ url: origin + BLANK_PAGE_PATH,
+ },
+ async merchBrowser => {
+ let { win, frame } = await setupPaymentDialog(merchBrowser, {
+ ...taskArgs,
+ merchantTaskFn,
+ });
+
+ await spawnPaymentDialogTask(frame, dialogTaskFn, taskArgs);
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+ await BrowserTestUtils.waitForCondition(
+ () => win.closed,
+ "dialog should be closed"
+ );
+ }
+ );
+}
+
+async function loginAndCompletePayment(frame) {
+ let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.completePayment);
+ await osKeyStoreLoginShown;
+}
+
+async function setupFormAutofillStorage() {
+ await formAutofillStorage.initialize();
+}
+
+function cleanupFormAutofillStorage() {
+ formAutofillStorage.addresses.removeAll();
+ formAutofillStorage.creditCards.removeAll();
+}
+
+add_task(async function setup_head() {
+ SpecialPowers.registerConsoleListener(function onConsoleMessage(msg) {
+ if (msg.isWarning || !msg.errorMessage) {
+ // 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.match(/docShell is null.*BrowserUtils.jsm/)
+ ) {
+ // Bug 1478142 - Console spam from the Find Toolbar.
+ return;
+ }
+ if (msg.message && msg.message.match(/PrioEncoder is not defined/)) {
+ // Bug 1492638 - Console spam from TelemetrySession.
+ return;
+ }
+ if (
+ msg.message &&
+ msg.message.match(/devicePixelRatio.*FaviconLoader.jsm/)
+ ) {
+ return;
+ }
+ if (
+ msg.errorMessage == "AbortError: The operation was aborted. " &&
+ msg.sourceName == "" &&
+ msg.lineNumber == 0
+ ) {
+ return;
+ }
+ ok(false, msg.message || msg.errorMessage);
+ });
+ OSKeyStoreTestUtils.setup();
+ await setupFormAutofillStorage();
+ registerCleanupFunction(async function cleanup() {
+ paymentSrv.cleanup();
+ cleanupFormAutofillStorage();
+ await OSKeyStoreTestUtils.cleanup();
+ Services.prefs.clearUserPref(RESPONSE_TIMEOUT_PREF);
+ Services.prefs.clearUserPref(SAVE_CREDITCARD_DEFAULT_PREF);
+ Services.prefs.clearUserPref(SAVE_ADDRESS_DEFAULT_PREF);
+ SpecialPowers.postConsoleSentinel();
+ // CreditCard.jsm is imported into the global scope. It needs to be deleted
+ // else it outlives the test and is reported as a leak.
+ delete window.CreditCard;
+ });
+});
+
+function deepClone(obj) {
+ return JSON.parse(JSON.stringify(obj));
+}
+
+async function selectPaymentDialogShippingAddressByCountry(frame, country) {
+ await spawnPaymentDialogTask(
+ frame,
+ PTU.DialogContentTasks.selectShippingAddressByCountry,
+ country
+ );
+}
+
+async function navigateToAddAddressPage(frame, aOptions = {}) {
+ ok(aOptions.initialPageId, "initialPageId option supplied");
+ ok(aOptions.addressPageId, "addressPageId option supplied");
+ ok(aOptions.addLinkSelector, "addLinkSelector option supplied");
+
+ await spawnPaymentDialogTask(
+ frame,
+ async options => {
+ let { PaymentTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ info("navigateToAddAddressPage: check we're on the expected page first");
+ await PaymentTestUtils.DialogContentUtils.waitForState(
+ content,
+ state => {
+ info(
+ "current page state: " +
+ state.page.id +
+ " waiting for: " +
+ options.initialPageId
+ );
+ return state.page.id == options.initialPageId;
+ },
+ "Check initial page state"
+ );
+
+ // click through to add/edit address page
+ info("navigateToAddAddressPage: click the link");
+ let addLink = content.document.querySelector(options.addLinkSelector);
+ addLink.click();
+
+ info("navigateToAddAddressPage: wait for address page");
+ await PaymentTestUtils.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == options.addressPageId && !state.page.guid;
+ },
+ "Check add page state"
+ );
+ },
+ aOptions
+ );
+}
+
+async function navigateToAddShippingAddressPage(frame, aOptions = {}) {
+ let options = Object.assign(
+ {
+ addLinkSelector:
+ 'address-picker[selected-state-key="selectedShippingAddress"] .add-link',
+ initialPageId: "payment-summary",
+ addressPageId: "shipping-address-page",
+ },
+ aOptions
+ );
+ await navigateToAddAddressPage(frame, options);
+}
+
+async function fillInBillingAddressForm(frame, aAddress, aOptions = {}) {
+ // For now billing and shipping address forms have the same fields but that may
+ // change so use separarate helpers.
+ let address = Object.assign({}, aAddress);
+ // Email isn't used on address forms, only payer/contact ones.
+ delete address.email;
+ let options = Object.assign(
+ {
+ addressPageId: "billing-address-page",
+ expectedSelectedStateKey: ["basic-card-page", "billingAddressGUID"],
+ },
+ aOptions
+ );
+ return fillInAddressForm(frame, address, options);
+}
+
+async function fillInShippingAddressForm(frame, aAddress, aOptions) {
+ let address = Object.assign({}, aAddress);
+ // Email isn't used on address forms, only payer/contact ones.
+ delete address.email;
+ return fillInAddressForm(frame, address, {
+ expectedSelectedStateKey: ["selectedShippingAddress"],
+ ...aOptions,
+ });
+}
+
+async function fillInPayerAddressForm(frame, aAddress) {
+ let address = Object.assign({}, aAddress);
+ let payerFields = [
+ "given-name",
+ "additional-name",
+ "family-name",
+ "tel",
+ "email",
+ ];
+ for (let fieldName of Object.keys(address)) {
+ if (payerFields.includes(fieldName)) {
+ continue;
+ }
+ delete address[fieldName];
+ }
+ return fillInAddressForm(frame, address, {
+ expectedSelectedStateKey: ["selectedPayerAddress"],
+ });
+}
+
+/**
+ * @param {HTMLElement} frame
+ * @param {object} aAddress
+ * @param {object} [aOptions = {}]
+ * @param {boolean} [aOptions.setPersistCheckedValue = undefined] How to set the persist checkbox.
+ * @param {string[]} [expectedSelectedStateKey = undefined] The expected selectedStateKey for
+ address-page.
+ */
+async function fillInAddressForm(frame, aAddress, aOptions = {}) {
+ await spawnPaymentDialogTask(
+ frame,
+ async args => {
+ let { address, options = {} } = args;
+ let { requestStore } = Cu.waiveXrays(
+ content.document.querySelector("payment-dialog")
+ );
+ let currentState = requestStore.getState();
+ let addressForm = content.document.getElementById(currentState.page.id);
+ ok(
+ addressForm,
+ "found the addressForm: " + addressForm.getAttribute("id")
+ );
+
+ if (options.expectedSelectedStateKey) {
+ Assert.deepEqual(
+ addressForm.getAttribute("selected-state-key").split("|"),
+ options.expectedSelectedStateKey,
+ "Check address page selectedStateKey"
+ );
+ }
+
+ if (typeof address.country != "undefined") {
+ // Set the country first so that the appropriate fields are visible.
+ let countryField = addressForm.querySelector("#country");
+ ok(!countryField.disabled, "Country Field shouldn't be disabled");
+ await content.fillField(countryField, address.country);
+ is(
+ countryField.value,
+ address.country,
+ "country value is correct after fillField"
+ );
+ }
+
+ // fill the form
+ info(
+ "fillInAddressForm: fill the form with address: " +
+ JSON.stringify(address)
+ );
+ for (let [key, val] of Object.entries(address)) {
+ let field = addressForm.querySelector(`#${key}`);
+ if (!field) {
+ ok(false, `${key} field not found`);
+ }
+ ok(!field.disabled, `Field #${key} shouldn't be disabled`);
+ await content.fillField(field, val);
+ is(field.value, val, `${key} value is correct after fillField`);
+ }
+ let persistCheckbox = Cu.waiveXrays(
+ addressForm.querySelector(".persist-checkbox")
+ );
+ // only touch the checked state if explicitly told to in the options
+ if (options.hasOwnProperty("setPersistCheckedValue")) {
+ info(
+ "fillInAddressForm: Manually setting the persist checkbox checkedness to: " +
+ options.setPersistCheckedValue
+ );
+ Cu.waiveXrays(persistCheckbox).checked = options.setPersistCheckedValue;
+ }
+ info(
+ `fillInAddressForm, persistCheckbox.checked: ${persistCheckbox.checked}`
+ );
+ },
+ { address: aAddress, options: aOptions }
+ );
+}
+
+async function verifyPersistCheckbox(frame, aOptions = {}) {
+ await spawnPaymentDialogTask(
+ frame,
+ async args => {
+ let { options = {} } = args;
+ // ensure card/address is persisted or not based on the temporary option given
+ info("verifyPersistCheckbox, got options: " + JSON.stringify(options));
+ let persistCheckbox = Cu.waiveXrays(
+ content.document.querySelector(options.checkboxSelector)
+ );
+
+ if (options.isEditing) {
+ ok(
+ persistCheckbox.hidden,
+ "checkbox should be hidden when editing a record"
+ );
+ } else {
+ ok(
+ !persistCheckbox.hidden,
+ "checkbox should be visible when adding a new record"
+ );
+ is(
+ persistCheckbox.checked,
+ options.expectPersist,
+ `persist checkbox state is expected to be ${options.expectPersist}`
+ );
+ }
+ },
+ { options: aOptions }
+ );
+}
+
+async function verifyCardNetwork(frame, aOptions = {}) {
+ aOptions.supportedNetworks = CreditCard.SUPPORTED_NETWORKS;
+
+ await spawnPaymentDialogTask(
+ frame,
+ async args => {
+ let { options = {} } = args;
+ // ensure the network picker is visible, has the right contents and expected value
+ let networkSelect = Cu.waiveXrays(
+ content.document.querySelector(options.networkSelector)
+ );
+ ok(
+ content.isVisible(networkSelect),
+ "The network selector should always be visible"
+ );
+ is(
+ networkSelect.childElementCount,
+ options.supportedNetworks.length + 1,
+ "Should have one more than the number of supported networks"
+ );
+ is(
+ networkSelect.children[0].value,
+ "",
+ "The first option should be the blank/empty option"
+ );
+ is(
+ networkSelect.value,
+ options.expectedNetwork,
+ `The network picker should have the expected value`
+ );
+ },
+ { options: aOptions }
+ );
+}
+
+async function submitAddressForm(
+ frame,
+ aAddress,
+ aOptions = {
+ nextPageId: "payment-summary",
+ }
+) {
+ await spawnPaymentDialogTask(
+ frame,
+ async args => {
+ let { options = {} } = args;
+ let nextPageId = options.nextPageId || "payment-summary";
+ let { PaymentTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ let oldState = await PaymentTestUtils.DialogContentUtils.getCurrentState(
+ content
+ );
+ let pageId = oldState.page.id;
+
+ // submit the form to return to summary page
+ content.document.querySelector(`#${pageId} button.primary`).click();
+
+ let currState = await PaymentTestUtils.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == nextPageId;
+ },
+ `submitAddressForm: Switched back to ${nextPageId}`
+ );
+
+ let savedCount = Object.keys(currState.savedAddresses).length;
+ let tempCount = Object.keys(currState.tempAddresses).length;
+ let oldSavedCount = Object.keys(oldState.savedAddresses).length;
+ let oldTempCount = Object.keys(oldState.tempAddresses).length;
+
+ if (options.isEditing) {
+ is(tempCount, oldTempCount, "tempAddresses count didn't change");
+ is(savedCount, oldSavedCount, "savedAddresses count didn't change");
+ } else if (options.expectPersist) {
+ is(tempCount, oldTempCount, "tempAddresses count didn't change");
+ is(savedCount, oldSavedCount + 1, "Entry added to savedAddresses");
+ } else {
+ is(tempCount, oldTempCount + 1, "Entry added to tempAddresses");
+ is(savedCount, oldSavedCount, "savedAddresses count didn't change");
+ }
+ },
+ { address: aAddress, options: aOptions }
+ );
+}
+
+async function manuallyAddShippingAddress(frame, aAddress, aOptions = {}) {
+ let options = Object.assign(
+ {
+ expectPersist: true,
+ isEditing: false,
+ },
+ aOptions,
+ {
+ checkboxSelector: "#shipping-address-page .persist-checkbox",
+ }
+ );
+ await navigateToAddShippingAddressPage(frame);
+ info(
+ "manuallyAddShippingAddress, fill in address form with options: " +
+ JSON.stringify(options)
+ );
+ await fillInShippingAddressForm(frame, aAddress, options);
+ info(
+ "manuallyAddShippingAddress, verifyPersistCheckbox with options: " +
+ JSON.stringify(options)
+ );
+ await verifyPersistCheckbox(frame, options);
+ await submitAddressForm(frame, aAddress, options);
+}
+
+async function navigateToAddCardPage(
+ frame,
+ aOptions = {
+ addLinkSelector: "payment-method-picker .add-link",
+ }
+) {
+ await spawnPaymentDialogTask(
+ frame,
+ async options => {
+ let { PaymentTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PaymentTestUtils.jsm"
+ );
+
+ // check were on the summary page first
+ await PaymentTestUtils.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return !state.page.id || state.page.id == "payment-summary";
+ },
+ "Check summary page state"
+ );
+
+ // click through to add/edit card page
+ let addLink = content.document.querySelector(options.addLinkSelector);
+ addLink.click();
+
+ // wait for card page
+ await PaymentTestUtils.DialogContentUtils.waitForState(
+ content,
+ state => {
+ return state.page.id == "basic-card-page";
+ },
+ "Check add/edit page state"
+ );
+ },
+ aOptions
+ );
+}
+
+async function fillInCardForm(frame, aCard, aOptions = {}) {
+ await spawnPaymentDialogTask(
+ frame,
+ async args => {
+ let { card, options = {} } = args;
+
+ // fill the form
+ info("fillInCardForm: fill the form with card: " + JSON.stringify(card));
+ for (let [key, val] of Object.entries(card)) {
+ let field = content.document.getElementById(key);
+ if (!field) {
+ ok(false, `${key} field not found`);
+ }
+ ok(!field.disabled, `Field #${key} shouldn't be disabled`);
+ // Reset the value first so that we properly handle typing the value
+ // already selected which may select another option with the same prefix.
+ field.value = "";
+ ok(!field.value, "Field value should be reset before typing");
+ field.blur();
+ field.focus();
+ // Using waitForEvent here causes the test to hang, but
+ // waitForCondition and checking activeElement does the trick. The root cause
+ // of this should be investigated further.
+ await ContentTaskUtils.waitForCondition(
+ () => field == content.document.activeElement,
+ `Waiting for field #${key} to get focus`
+ );
+ if (key == "billingAddressGUID") {
+ // Can't type the value in, press Down until the value is found
+ content.fillField(field, val);
+ } else {
+ // cc-exp-* fields are numbers so convert to strings and pad left with 0
+ let fillValue = val.toString().padStart(2, "0");
+ EventUtils.synthesizeKey(
+ fillValue,
+ {},
+ Cu.waiveXrays(content.window)
+ );
+ }
+ // cc-exp-* field values are not padded, so compare with unpadded string.
+ is(
+ field.value,
+ val.toString(),
+ `${key} value is correct after sendString`
+ );
+ }
+
+ info(
+ [...content.document.getElementById("cc-exp-year").options]
+ .map(op => op.label)
+ .join(",")
+ );
+
+ let persistCheckbox = content.document.querySelector(
+ options.checkboxSelector
+ );
+ // only touch the checked state if explicitly told to in the options
+ if (options.hasOwnProperty("setPersistCheckedValue")) {
+ info(
+ "fillInCardForm: Manually setting the persist checkbox checkedness to: " +
+ options.setPersistCheckedValue
+ );
+ Cu.waiveXrays(persistCheckbox).checked = options.setPersistCheckedValue;
+ }
+ },
+ { card: aCard, options: aOptions }
+ );
+}
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,&#xA;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>
diff --git a/browser/components/payments/test/unit/head.js b/browser/components/payments/test/unit/head.js
new file mode 100644
index 0000000000..82e1e170c1
--- /dev/null
+++ b/browser/components/payments/test/unit/head.js
@@ -0,0 +1,2 @@
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
diff --git a/browser/components/payments/test/unit/test_response_creation.js b/browser/components/payments/test/unit/test_response_creation.js
new file mode 100644
index 0000000000..2b4b5dc98b
--- /dev/null
+++ b/browser/components/payments/test/unit/test_response_creation.js
@@ -0,0 +1,149 @@
+"use strict";
+
+/**
+ * Basic checks to ensure that helpers constructing responses map their
+ * destructured arguments properly to the `init` methods. Full testing of the init
+ * methods is left to the DOM code.
+ */
+
+const DIALOG_WRAPPER_URI = "chrome://payments/content/paymentDialogWrapper.js";
+let dialogGlobal = {};
+Services.scriptloader.loadSubScript(DIALOG_WRAPPER_URI, dialogGlobal);
+
+add_task(async function test_createBasicCardResponseData_basic() {
+ let expected = {
+ cardholderName: "John Smith",
+ cardNumber: "1234567890",
+ expiryMonth: "01",
+ expiryYear: "2017",
+ cardSecurityCode: "0123",
+ };
+ let actual = dialogGlobal.paymentDialogWrapper.createBasicCardResponseData(
+ expected
+ );
+ Assert.equal(
+ actual.cardholderName,
+ expected.cardholderName,
+ "Check cardholderName"
+ );
+ Assert.equal(actual.cardNumber, expected.cardNumber, "Check cardNumber");
+ Assert.equal(actual.expiryMonth, expected.expiryMonth, "Check expiryMonth");
+ Assert.equal(actual.expiryYear, expected.expiryYear, "Check expiryYear");
+ Assert.equal(
+ actual.cardSecurityCode,
+ expected.cardSecurityCode,
+ "Check cardSecurityCode"
+ );
+});
+
+add_task(async function test_createBasicCardResponseData_minimal() {
+ let expected = {
+ cardNumber: "1234567890",
+ };
+ let actual = dialogGlobal.paymentDialogWrapper.createBasicCardResponseData(
+ expected
+ );
+ info(actual.cardNumber);
+ Assert.equal(actual.cardNumber, expected.cardNumber, "Check cardNumber");
+});
+
+add_task(async function test_createBasicCardResponseData_withoutNumber() {
+ let data = {
+ cardholderName: "John Smith",
+ expiryMonth: "01",
+ expiryYear: "2017",
+ cardSecurityCode: "0123",
+ };
+ Assert.throws(
+ () => dialogGlobal.paymentDialogWrapper.createBasicCardResponseData(data),
+ /NS_ERROR_FAILURE/,
+ "Check cardNumber is required"
+ );
+});
+
+function checkAddress(actual, expected) {
+ for (let [propName, propVal] of Object.entries(expected)) {
+ if (propName == "addressLines") {
+ // Note the singular vs. plural here.
+ Assert.equal(
+ actual.addressLine.length,
+ propVal.length,
+ "Check number of address lines"
+ );
+ for (let [i, line] of expected.addressLines.entries()) {
+ Assert.equal(
+ actual.addressLine.queryElementAt(i, Ci.nsISupportsString).data,
+ line,
+ `Check ${propName} line ${i}`
+ );
+ }
+ continue;
+ }
+ Assert.equal(actual[propName], propVal, `Check ${propName}`);
+ }
+}
+
+add_task(async function test_createPaymentAddress_minimal() {
+ let data = {
+ country: "CA",
+ };
+ let actual = dialogGlobal.paymentDialogWrapper.createPaymentAddress(data);
+ checkAddress(actual, data);
+});
+
+add_task(async function test_createPaymentAddress_basic() {
+ let data = {
+ country: "CA",
+ addressLines: ["123 Sesame Street", "P.O. Box ABC"],
+ region: "ON",
+ city: "Delhi",
+ dependentLocality: "N/A",
+ postalCode: "94041",
+ sortingCode: "1234",
+ organization: "Mozilla Corporation",
+ recipient: "John Smith",
+ phone: "+15195555555",
+ };
+ let actual = dialogGlobal.paymentDialogWrapper.createPaymentAddress(data);
+ checkAddress(actual, data);
+});
+
+add_task(async function test_createShowResponse_basic() {
+ let requestId = "876hmbvfd45hb";
+ dialogGlobal.paymentDialogWrapper.request = {
+ requestId,
+ };
+
+ let cardData = {
+ cardholderName: "John Smith",
+ cardNumber: "1234567890",
+ expiryMonth: "01",
+ expiryYear: "2099",
+ cardSecurityCode: "0123",
+ };
+ let methodData = dialogGlobal.paymentDialogWrapper.createBasicCardResponseData(
+ cardData
+ );
+
+ let responseData = {
+ acceptStatus: Ci.nsIPaymentActionResponse.PAYMENT_ACCEPTED,
+ methodName: "basic-card",
+ methodData,
+ payerName: "My Name",
+ payerEmail: "my.email@example.com",
+ payerPhone: "+15195555555",
+ };
+ let actual = dialogGlobal.paymentDialogWrapper.createShowResponse(
+ responseData
+ );
+ for (let [propName, propVal] of Object.entries(actual)) {
+ if (typeof propVal != "string") {
+ continue;
+ }
+ if (propName == "requestId") {
+ Assert.equal(propVal, requestId, `Check ${propName}`);
+ continue;
+ }
+ Assert.equal(propVal, responseData[propName], `Check ${propName}`);
+ }
+});
diff --git a/browser/components/payments/test/unit/xpcshell.ini b/browser/components/payments/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..b4f0db683a
--- /dev/null
+++ b/browser/components/payments/test/unit/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+firefox-appdir = browser
+head = head.js
+skip-if = true # Bug 1515048 - Disable for now.
+
+[test_response_creation.js]