Bug 1447777 - Add tests for the complete() scenarios. r?MattN draft
authorSam Foster <sfoster@mozilla.com>
Mon, 02 Jul 2018 15:35:00 -0700
changeset 820923 65161702c1d6aad22edee15d0dbb5f29b4f2375f
parent 820722 56e634f74320e856e2843ace9df12bdc953ad5dc
push id116986
push userbmo:sfoster@mozilla.com
push dateFri, 20 Jul 2018 17:24:15 +0000
reviewersMattN
bugs1447777
milestone63.0a1
Bug 1447777 - Add tests for the complete() scenarios. r?MattN * Verify that the dialog stays open when completion fails or times out * Verify that complete() throws after the timeout * Rework completeStatus mochitest for PaymentDialog: In the current design, complete states don't use the preventChanges overlay, so test_completeStatusChangesPrevented is removed. The hidden and visibility checks are folded into tests for each complete status MozReview-Commit-ID: 4ZNVEYMp7h5
browser/components/payments/test/PaymentTestUtils.jsm
browser/components/payments/test/browser/browser.ini
browser/components/payments/test/browser/browser_payment_completion.js
browser/components/payments/test/browser/head.js
browser/components/payments/test/mochitest/test_payment_dialog.html
--- a/browser/components/payments/test/PaymentTestUtils.jsm
+++ b/browser/components/payments/test/PaymentTestUtils.jsm
@@ -7,20 +7,33 @@ 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 () => {
+    addCompletionHandler: async ({result, delayMs = 0}) => {
       let response = await content.showPromise;
-      response.complete();
+      let completeException;
+
+      // delay the given # milliseconds
+      await new Promise(resolve => content.setTimeout(resolve, delayMs));
+
+      try {
+        await response.complete(result);
+      } catch (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,
       };
     },
 
     ensureNoPaymentRequestEvent: ({eventName}) => {
       content.rq.addEventListener(eventName, (event) => {
@@ -146,16 +159,31 @@ var PaymentTestUtils = {
       let select = Cu.waiveXrays(optionPicker).dropdown.popupBox;
       let option = select.querySelector(`[value="${value}"]`);
       select.focus();
       // eslint-disable-next-line no-undef
       EventUtils.synthesizeKey(option.textContent, {}, content.window);
     },
 
     /**
+     * Click the primary button for the current page
+     *
+     * Don't await on this task since the button can close the dialog before
+     * ContentTask can resolve the promise.
+     *
+     * @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`);
+      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: () => {
@@ -188,17 +216,17 @@ var PaymentTestUtils = {
       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) => {
+    getCurrentState: (content) => {
       let {requestStore} = Cu.waiveXrays(content.document.querySelector("payment-dialog"));
       return requestStore.getState();
     },
   },
 
   /**
    * Common PaymentMethodData for testing
    */
--- a/browser/components/payments/test/browser/browser.ini
+++ b/browser/components/payments/test/browser/browser.ini
@@ -7,16 +7,17 @@ support-files =
   blank_page.html
 
 [browser_address_edit.js]
 skip-if = verify && debug && os == 'mac'
 [browser_card_edit.js]
 [browser_change_shipping.js]
 [browser_dropdowns.js]
 [browser_host_name.js]
+[browser_payment_completion.js]
 [browser_payments_onboarding_wizard.js]
 [browser_profile_storage.js]
 [browser_request_serialization.js]
 [browser_request_shipping.js]
 [browser_shippingaddresschange_error.js]
 [browser_show_dialog.js]
 skip-if = os == 'win' && debug # bug 1418385
 [browser_total.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/payments/test/browser/browser_payment_completion.js
@@ -0,0 +1,107 @@
+"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 });
+  await addCardRecord(card);
+}
+
+add_task(async function test_complete_success() {
+  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,
+      }
+    );
+
+    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.completePayment);
+
+    // Add a handler to complete the payment above.
+    info("acknowledging the completion from the merchant page");
+    let {completeException} = await ContentTask.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() {
+  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,
+      }
+    );
+
+    info("clicking pay");
+    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.completePayment);
+
+    info("acknowledging the completion from the merchant page");
+    let {completeException} = await ContentTask.spawn(browser,
+                                                      { result: "fail" },
+                                                      PTU.ContentTasks.addCompletionHandler);
+    ok(!completeException, "Expect no exception to be thrown when calling complete()");
+
+    ok(!win.closed, "dialog shouldnt be closed yet");
+
+    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.clickPrimaryButton);
+    await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
+  });
+});
+
+add_task(async function test_complete_timeout() {
+  await setup();
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url: BLANK_PAGE_URL,
+  }, async browser => {
+    // timeout the response asap
+    Services.prefs.setIntPref(RESPONSE_TIMEOUT_PREF, 1);
+
+    let {win, frame} =
+      await setupPaymentDialog(browser, {
+        methodData: [PTU.MethodData.basicCard],
+        details: Object.assign({}, PTU.Details.total60USD),
+        merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+      }
+    );
+
+    info("clicking pay");
+    await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.completePayment);
+
+    info("acknowledging the completion from the merchant page after a delay");
+    let {completeException} = await ContentTask.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 shouldnt be closed yet");
+
+    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.clickPrimaryButton);
+    await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
+  });
+});
--- a/browser/components/payments/test/browser/head.js
+++ b/browser/components/payments/test/browser/head.js
@@ -5,16 +5,17 @@
     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 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().wrappedJSObject;
 const {formAutofillStorage} = ChromeUtils.import(
   "resource://formautofill/FormAutofillStorage.jsm", {});
 const {PaymentTestUtils: PTU} = ChromeUtils.import(
@@ -311,16 +312,17 @@ function cleanupFormAutofillStorage() {
   formAutofillStorage.creditCards.removeAll();
 }
 
 add_task(async function setup_head() {
   await setupFormAutofillStorage();
   registerCleanupFunction(function cleanup() {
     paymentSrv.cleanup();
     cleanupFormAutofillStorage();
+    Services.prefs.clearUserPref(RESPONSE_TIMEOUT_PREF);
   });
 });
 
 function deepClone(obj) {
   return JSON.parse(JSON.stringify(obj));
 }
 
 async function selectPaymentDialogShippingAddressByCountry(frame, country) {
--- a/browser/components/payments/test/mochitest/test_payment_dialog.html
+++ b/browser/components/payments/test/mochitest/test_payment_dialog.html
@@ -30,31 +30,16 @@ Test the payment-dialog custom element
 /** Test the payment-dialog element **/
 
 /* global sinon */
 
 import PaymentDialog from "../../res/containers/payment-dialog.js";
 
 let el1;
 
-let completeStatuses = [
-    ["processing", "Processing"],
-    ["success", "Done"],
-    ["fail", "Fail"],
-    ["unknown", "Unknown"],
-];
-
-/* test that:
-  the view-all-items button exists
-  that clicking it changes the state on the store
-  that clicking it causes render to be called
-
-  that order details element's hidden state matches the state on the store
-*/
-
 add_task(async function setup_once() {
   let templateFrame = document.getElementById("templateFrame");
   await SimpleTest.promiseFocus(templateFrame.contentWindow);
 
   let displayEl = document.getElementById("display");
   // Import the templates from the real shipping dialog to avoid duplication.
   for (let template of templateFrame.contentDocument.querySelectorAll("template")) {
     let imported = document.importNode(template, true);
@@ -67,18 +52,21 @@ add_task(async function setup_once() {
   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: "initial"}),
+    request: Object.assign({}, request, {completeStatus: "initial"}),
     orderDetailsShowing: false,
+    page: {
+      id: "payment-summary",
+    },
   });
 
   el1.render.reset();
   el1.stateChangeCallback.reset();
 }
 
 add_task(async function test_initialState() {
   await setup();
@@ -110,17 +98,16 @@ add_task(async function test_viewAllButt
   ];
   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();
@@ -139,53 +126,102 @@ add_task(async function test_changesPrev
   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_completeStatus() {
+add_task(async function test_initial_completeStatus() {
   await setup();
-  let {request} = el1.requestStore.getState();
+  let {request, page} = el1.requestStore.getState();
   is(request.completeStatus, "initial", "completeStatus is initially initial");
+
   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 enabled");
-  for (let [completeStatus, label] of completeStatuses) {
-    request.completeStatus = completeStatus;
-    await el1.requestStore.setState({request});
+});
+
+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");
+});
+
+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();
-    is(payButton.textContent, label, "Check payButton label");
-    ok(!payButton.disabled, "Button is still enabled");
+
+    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 enabled");
   }
 });
 
-add_task(async function test_completeStatusChangesPrevented() {
-  await setup();
-  let state = el1.requestStore.getState();
-  is(state.request.completeStatus, "initial", "completeStatus is initially initial");
-  is(state.changesPrevented, false, "changesPrevented is initially false");
-  let payButton = document.getElementById("pay");
-  is(payButton.textContent, "Pay", "Check default label");
-  ok(!payButton.disabled, "Button is enabled");
-
-  for (let [status, label] of completeStatuses) {
-    await el1.requestStore.setState({
-      changesPrevented: true,
-      request: Object.assign(state.request, { completeStatus: status }),
+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();
-    is(payButton.textContent, label, "Check payButton label");
-    ok(payButton.disabled, "Button is disabled");
-    let rect = payButton.getBoundingClientRect();
+
+    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, "OK", "Check button label");
+
+    let rect = primaryButton.getBoundingClientRect();
     let visibleElement =
       document.elementFromPoint(rect.x + rect.width / 2, rect.y + rect.height / 2);
-    ok(payButton === visibleElement, "Pay button is on top of the overlay");
+    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");