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
--- 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");