--- a/browser/components/payments/content/paymentDialogWrapper.js
+++ b/browser/components/payments/content/paymentDialogWrapper.js
@@ -548,16 +548,21 @@ var paymentDialogWrapper = {
onChangeShippingOption({optionID}) {
// Note, failing here on browser_host_name.js because the test closes
// the dialog before the onChangeShippingOption is called, thus
// deleting the request and making the requestId invalid. Unclear
// why we aren't seeing the same issue with onChangeShippingAddress.
paymentSrv.changeShippingOption(this.request.requestId, optionID);
},
+ onCloseDialogMessage() {
+ // The PR is complete(), just close the dialog
+ window.close();
+ },
+
async onUpdateAutofillRecord(collectionName, record, guid, {
errorStateChange,
preserveOldProperties,
selectedStateKey,
successStateChange,
}) {
if (collectionName == "creditCards" && !guid && !record.isTemporary) {
// We need to be logged in so we can encrypt the credit card number and
@@ -652,16 +657,20 @@ var paymentDialogWrapper = {
case "changeShippingAddress": {
this.onChangeShippingAddress(data);
break;
}
case "changeShippingOption": {
this.onChangeShippingOption(data);
break;
}
+ case "closeDialog": {
+ this.onCloseDialogMessage();
+ break;
+ }
case "paymentCancel": {
this.onPaymentCancel();
break;
}
case "pay": {
this.onPay(data);
break;
}
--- a/browser/components/payments/jar.mn
+++ b/browser/components/payments/jar.mn
@@ -10,16 +10,17 @@ browser.jar:
content/payments/paymentDialogWrapper.xul (content/paymentDialogWrapper.xul)
% resource payments %res/payments/
res/payments (res/paymentRequest.*)
res/payments/components/ (res/components/*.css)
res/payments/components/ (res/components/*.js)
res/payments/containers/ (res/containers/*.js)
res/payments/containers/ (res/containers/*.css)
+ res/payments/containers/ (res/containers/*.svg)
res/payments/debugging.css (res/debugging.css)
res/payments/debugging.html (res/debugging.html)
res/payments/debugging.js (res/debugging.js)
res/payments/formautofill/autofillEditForms.js (../../../browser/extensions/formautofill/content/autofillEditForms.js)
res/payments/formautofill/editAddress.xhtml (../../../browser/extensions/formautofill/content/editAddress.xhtml)
res/payments/formautofill/editCreditCard.xhtml (../../../browser/extensions/formautofill/content/editCreditCard.xhtml)
res/payments/unprivileged-fallbacks.js (res/unprivileged-fallbacks.js)
res/payments/mixins/ (res/mixins/*.js)
--- a/browser/components/payments/paymentUIService.js
+++ b/browser/components/payments/paymentUIService.js
@@ -63,25 +63,45 @@ PaymentUIService.prototype = {
Ci.nsIPaymentActionResponse.ABORT_SUCCEEDED :
Ci.nsIPaymentActionResponse.ABORT_FAILED;
abortResponse.init(requestId, response);
paymentSrv.respondPayment(abortResponse);
},
completePayment(requestId) {
- this.log.debug("completePayment:", requestId);
- let closed = this.closeDialog(requestId);
+ // completeStatus should be one of "timeout", "success", "fail", ""
+ let {completeStatus} = paymentSrv.getPaymentRequestById(requestId);
+ this.log.debug(`completePayment: requestId: ${requestId}, completeStatus: ${completeStatus}`);
+
+ let closed;
+ switch (completeStatus) {
+ case "fail":
+ case "timeout":
+ break;
+ default:
+ closed = this.closeDialog(requestId);
+ break;
+ }
let responseCode = closed ?
Ci.nsIPaymentActionResponse.COMPLETE_SUCCEEDED :
Ci.nsIPaymentActionResponse.COMPLETE_FAILED;
let completeResponse = Cc["@mozilla.org/dom/payments/payment-complete-action-response;1"]
.createInstance(Ci.nsIPaymentCompleteActionResponse);
completeResponse.init(requestId, responseCode);
paymentSrv.respondPayment(completeResponse.QueryInterface(Ci.nsIPaymentActionResponse));
+
+ if (!closed) {
+ let dialog = this.findDialog(requestId);
+ if (!dialog) {
+ this.log.error("completePayment: no dialog found");
+ return;
+ }
+ dialog.paymentDialogWrapper.updateRequest();
+ }
},
updatePayment(requestId) {
let dialog = this.findDialog(requestId);
this.log.debug("updatePayment:", requestId);
if (!dialog) {
this.log.error("updatePayment: no dialog found");
return;
new file mode 100644
--- /dev/null
+++ b/browser/components/payments/res/containers/completion-error-page.js
@@ -0,0 +1,78 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import PaymentRequestPage from "../components/payment-request-page.js";
+import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
+import paymentRequest from "../paymentRequest.js";
+
+/* import-globals-from ../unprivileged-fallbacks.js */
+
+/**
+ * <completion-error-page></completion-error-page>
+ *
+ * XXX: Bug 1473772 - This page isn't fully localized when used via this custom element
+ * as it will be much easier to implement and share the logic once we switch to Fluent.
+ */
+
+export default class CompletionErrorPage extends PaymentStateSubscriberMixin(PaymentRequestPage) {
+ constructor() {
+ super();
+
+ this.classList.add("error-page");
+ this.suggestionsList = document.createElement("ul");
+ this.suggestions = [];
+ this.body.append(this.suggestionsList);
+
+ this.doneButton = document.createElement("button");
+ this.doneButton.classList.add("done-button", "primary");
+ this.doneButton.addEventListener("click", this);
+
+ this.footer.appendChild(this.doneButton);
+ }
+
+ render(state) {
+ let { page } = state;
+
+ if (this.id && page && page.id !== this.id) {
+ log.debug(`CompletionErrorPage: no need to further render inactive page: ${page.id}`);
+ return;
+ }
+
+ this.pageTitleHeading.textContent = this.dataset.pageTitle;
+ this.doneButton.textContent = this.dataset.doneButtonLabel;
+
+ this.suggestionsList.textContent = "";
+
+ // FIXME: should come from this.dataset.suggestionN when those strings are created
+ this.suggestions[0] = "First suggestion";
+
+ let suggestionsFragment = document.createDocumentFragment();
+ for (let suggestionText of this.suggestions) {
+ let listNode = document.createElement("li");
+ listNode.textContent = suggestionText;
+ suggestionsFragment.appendChild(listNode);
+ }
+ this.suggestionsList.appendChild(suggestionsFragment);
+ }
+
+ handleEvent(event) {
+ if (event.type == "click") {
+ switch (event.target) {
+ case this.doneButton: {
+ this.onDoneButtonClick(event);
+ break;
+ }
+ default: {
+ throw new Error("Unexpected click target");
+ }
+ }
+ }
+ }
+
+ onDoneButtonClick(event) {
+ paymentRequest.closeDialog();
+ }
+}
+
+customElements.define("completion-error-page", CompletionErrorPage);
new file mode 100644
--- /dev/null
+++ b/browser/components/payments/res/containers/error-page.css
@@ -0,0 +1,22 @@
+.error-page.illustrated > .page-body {
+ min-height: 300px;
+ background-position: left center;
+ background-repeat: no-repeat;
+ background-size: 38%;
+ padding-inline-start: 38%;
+}
+
+.error-page.illustrated > .page-body:dir(rtl) {
+ background-position: right center;
+}
+
+.error-page.illustrated > .page-body > h2 {
+ background: none;
+ padding-inline-start: 0;
+ margin-inline-start: 0;
+}
+
+.error-page#completion-timeout-error > .page-body,
+.error-page#completion-fail-error > .page-body {
+ background-image: url("./placeholder.svg");
+}
--- a/browser/components/payments/res/containers/payment-dialog.js
+++ b/browser/components/payments/res/containers/payment-dialog.js
@@ -7,16 +7,17 @@ import "../vendor/custom-elements.min.js
import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
import paymentRequest from "../paymentRequest.js";
import "../components/currency-amount.js";
import "../components/payment-request-page.js";
import "./address-picker.js";
import "./address-form.js";
import "./basic-card-form.js";
+import "./completion-error-page.js";
import "./order-details.js";
import "./payment-method-picker.js";
import "./shipping-option-picker.js";
/* import-globals-from ../unprivileged-fallbacks.js */
/**
* <payment-dialog></payment-dialog>
@@ -116,25 +117,43 @@ export default class PaymentDialog exten
let methodId = state.selectedPaymentCard;
let modifier = paymentRequest.getModifierForPaymentMethod(state, methodId);
if (modifier && modifier.additionalDisplayItems) {
return modifier.additionalDisplayItems;
}
return [];
}
+ _updateCompleteStatus(state) {
+ let {completeStatus} = state.request;
+ switch (completeStatus) {
+ case "fail":
+ case "timeout":
+ case "unknown":
+ state.page = {
+ id: `completion-${completeStatus}-error`,
+ };
+ state.changesPrevented = false;
+ break;
+ }
+ return state;
+ }
+
/**
* Set some state from the privileged parent process.
* Other elements that need to set state should use their own `this.requestStore.setState`
* method provided by the `PaymentStateSubscriberMixin`.
*
* @param {object} state - See `PaymentsStore.setState`
*/
setStateFromParent(state) {
let oldAddresses = paymentRequest.getAddresses(this.requestStore.getState());
+ if (state.request) {
+ state = this._updateCompleteStatus(state);
+ }
this.requestStore.setState(state);
// Check if any foreign-key constraints were invalidated.
state = this.requestStore.getState();
let {
selectedPayerAddress,
selectedPaymentCard,
selectedShippingAddress,
@@ -198,32 +217,41 @@ export default class PaymentDialog exten
if (!addresses[selectedPayerAddress]) {
this.requestStore.setState({
selectedPayerAddress: Object.keys(addresses)[0] || null,
});
}
}
_renderPayButton(state) {
- this._payButton.disabled = state.changesPrevented;
let completeStatus = state.request.completeStatus;
switch (completeStatus) {
case "initial":
case "processing":
case "success":
+ case "unknown": {
+ this._payButton.disabled = state.changesPrevented;
+ this._payButton.textContent = this._payButton.dataset[completeStatus + "Label"];
+ break;
+ }
case "fail":
- case "unknown":
+ case "timeout": {
+ // pay button is hidden in these states. Reset its label and disable it
+ this._payButton.textContent = this._payButton.dataset.initialLabel;
+ this._payButton.disabled = true;
break;
- case "":
+ }
+ case "": {
completeStatus = "initial";
break;
- default:
+ }
+ default: {
throw new Error(`Invalid completeStatus: ${completeStatus}`);
+ }
}
-
this._payButton.textContent = this._payButton.dataset[completeStatus + "Label"];
}
stateChangeCallback(state) {
super.stateChangeCallback(state);
// Don't dispatch change events for initial selectedShipping* changes at initialization
// if requestShipping is false.
new file mode 100644
--- /dev/null
+++ b/browser/components/payments/res/containers/placeholder.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="300" height="300" viewBox="0 0 300 300">
+ <circle cx="150" cy="150" r="100" stroke="#0a84ff" fill="#c9e4ff"/>
+</svg>
--- a/browser/components/payments/res/debugging.css
+++ b/browser/components/payments/res/debugging.css
@@ -11,8 +11,21 @@ html {
h1 {
font-size: 1em;
}
fieldset > label {
white-space: nowrap;
}
+
+.group {
+ margin: 0.5em 0;
+}
+
+label.block {
+ display: block;
+ margin: 0.3em 0;
+}
+
+button.wide {
+ width: 100%;
+}
--- a/browser/components/payments/res/debugging.html
+++ b/browser/components/payments/res/debugging.html
@@ -6,42 +6,65 @@
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
<link rel="stylesheet" href="debugging.css"/>
<script src="debugging.js"></script>
</head>
<body>
<div>
- <button id="refresh">Refresh</button>
- <button id="rerender">Re-render</button>
- <button id="logState">Log state</button>
- <button id="debugFrame" hidden>Debug frame</button>
- <h1>Requests</h1>
- <button id="setRequest1">Request 1</button>
- <button id="setRequest2">Request 2</button>
- <fieldset id="paymentOptions">
- <legend>Payment Options</legend>
- <label><input type="checkbox" autocomplete="off" name="requestPayerName" id="setRequestPayerName">requestPayerName</label>
- <label><input type="checkbox" autocomplete="off" name="requestPayerEmail" id="setRequestPayerEmail">requestPayerEmail</label>
- <label><input type="checkbox" autocomplete="off" name="requestPayerPhone" id="setRequestPayerPhone">requestPayerPhone</label>
- <label><input type="checkbox" autocomplete="off" name="requestShipping" id="setRequestShipping">requestShipping</label>
- </fieldset>
- <h1>Addresses</h1>
- <button id="setAddresses1">Set Addreses 1</button>
- <button id="setDupesAddresses">Set Duped Addresses</button>
- <button id="delete1Address">Delete 1 Address</button>
- <h1>Payment Methods</h1>
- <button id="setBasicCards1">Set Basic Cards 1</button>
- <button id="delete1Card">Delete 1 Card</button>
- <h1>States</h1>
- <button id="setChangesPrevented">Prevent changes</button>
- <button id="setChangesAllowed">Allow changes</button>
- <button id="setShippingError">Shipping Error</button>
- <button id="setAddressErrors">Address Errors</button>
- <button id="setStateDefault">Default</button>
- <button id="setStateProcessing">Processing</button>
- <button id="setStateSuccess">Success</button>
- <button id="setStateFail">Fail</button>
- <button id="setStateUnknown">Unknown</button>
+ <section class="group">
+ <button id="refresh">Refresh</button>
+ <button id="rerender">Re-render</button>
+ <button id="logState">Log state</button>
+ <button id="debugFrame" hidden>Debug frame</button>
+ </section>
+ <section class="group">
+ <h1>Requests</h1>
+ <button id="setRequest1">Request 1</button>
+ <button id="setRequest2">Request 2</button>
+ <fieldset id="paymentOptions">
+ <legend>Payment Options</legend>
+ <label><input type="checkbox" autocomplete="off" name="requestPayerName" id="setRequestPayerName">requestPayerName</label>
+ <label><input type="checkbox" autocomplete="off" name="requestPayerEmail" id="setRequestPayerEmail">requestPayerEmail</label>
+ <label><input type="checkbox" autocomplete="off" name="requestPayerPhone" id="setRequestPayerPhone">requestPayerPhone</label>
+ <label><input type="checkbox" autocomplete="off" name="requestShipping" id="setRequestShipping">requestShipping</label>
+ </fieldset>
+ </section>
+
+ <section class="group">
+ <h1>Addresses</h1>
+ <button id="setAddresses1">Set Addreses 1</button>
+ <button id="setDupesAddresses">Set Duped Addresses</button>
+ <button id="delete1Address">Delete 1 Address</button>
+ </section>
+
+ <section class="group">
+ <h1>Payment Methods</h1>
+ <button id="setBasicCards1">Set Basic Cards 1</button>
+ <button id="delete1Card">Delete 1 Card</button>
+ </section>
+
+ <section class="group">
+ <h1>States</h1>
+ <fieldset>
+ <legend>Complete Status</legend>
+ <label class="block"><input type="radio" name="completeStatus" value="initial" checked="checked">Initial (default)</label>
+ <label class="block"><input type="radio" name="completeStatus" value="processing">Processing</label>
+ <label class="block"><input type="radio" name="completeStatus" value="success">Success</label>
+ <label class="block"><input type="radio" name="completeStatus" value="fail">Fail</label>
+ <label class="block"><input type="radio" name="completeStatus" value="unknown">Unknown</label>
+ <label class="block"><input type="radio" name="completeStatus" value="timeout">Timeout</label>
+ </fieldset>
+ <label class="block"><input type="checkbox" id="setChangesPrevented">Prevent changes</label>
+ <button id="setCompleteStatus" class="wide">Set Complete Status</button>
+
+ <section class="group">
+ <fieldset>
+ <legend>User Data Errors</legend>
+ <button id="setShippingError">Shipping Error</button>
+ <button id="setAddressErrors">Address Errors</button>
+ </fieldset>
+ </section>
+ </section>
</div>
</body>
</html>
--- a/browser/components/payments/res/debugging.js
+++ b/browser/components/payments/res/debugging.js
@@ -401,49 +401,23 @@ let buttonActions = {
recipient: "Can only ship to names that start with J",
region: "Can only ship to regions that start with M",
};
requestStore.setState({
request,
});
},
- setStateDefault() {
- let request = Object.assign({}, requestStore.getState().request, {
- completeStatus: "initial",
- });
- requestStore.setState({ request });
- },
-
- setStateProcessing() {
- let request = Object.assign({}, requestStore.getState().request, {
- completeStatus: "processing",
+ setCompleteStatus(e) {
+ let input = document.querySelector("[name='completionState']:checked");
+ let completeStatus = input.value;
+ let request = requestStore.getState().request;
+ requestStore.setStateFromParent({
+ request: Object.assign({}, request, { completeStatus }),
});
- requestStore.setState({ request });
- },
-
- setStateSuccess() {
- let request = Object.assign({}, requestStore.getState().request, {
- completeStatus: "success",
- });
- requestStore.setState({ request });
- },
-
- setStateFail() {
- let request = Object.assign({}, requestStore.getState().request, {
- completeStatus: "fail",
- });
- requestStore.setState({ request });
- },
-
- setStateUnknown() {
- let request = Object.assign({}, requestStore.getState().request, {
- completeStatus: "unknown",
- });
- requestStore.setState({ request });
},
};
window.addEventListener("click", function onButtonClick(evt) {
let id = evt.target.id;
if (!id || typeof(buttonActions[id]) != "function") {
return;
}
--- a/browser/components/payments/res/paymentRequest.js
+++ b/browser/components/payments/res/paymentRequest.js
@@ -165,16 +165,20 @@ var paymentRequest = {
cancel() {
this.sendMessageToChrome("paymentCancel");
},
pay(data) {
this.sendMessageToChrome("pay", data);
},
+ closeDialog() {
+ this.sendMessageToChrome("closeDialog");
+ },
+
changeShippingAddress(data) {
this.sendMessageToChrome("changeShippingAddress", data);
},
changeShippingOption(data) {
this.sendMessageToChrome("changeShippingOption", data);
},
--- a/browser/components/payments/res/paymentRequest.xhtml
+++ b/browser/components/payments/res/paymentRequest.xhtml
@@ -35,31 +35,40 @@
<!ENTITY basicCard.editPage.title "Edit Credit Card">
<!ENTITY payer.addPage.title "Add Payer Contact">
<!ENTITY payer.editPage.title "Edit Payer Contact">
<!ENTITY payerLabel "Contact Information">
<!ENTITY cancelPaymentButton.label "Cancel">
<!ENTITY approvePaymentButton.label "Pay">
<!ENTITY processingPaymentButton.label "Processing">
<!ENTITY successPaymentButton.label "Done">
- <!ENTITY failPaymentButton.label "Fail">
<!ENTITY unknownPaymentButton.label "Unknown">
<!ENTITY orderDetailsLabel "Order Details">
<!ENTITY orderTotalLabel "Total">
<!ENTITY basicCardPage.error.genericSave "There was an error saving the payment card.">
<!ENTITY basicCardPage.addressAddLink.label "Add">
<!ENTITY basicCardPage.addressEditLink.label "Edit">
<!ENTITY basicCardPage.backButton.label "Back">
<!ENTITY basicCardPage.saveButton.label "Save">
<!ENTITY basicCardPage.persistCheckbox.label "Save credit card to &brandShortName; (Security code will not be saved)">
<!ENTITY addressPage.error.genericSave "There was an error saving the address.">
<!ENTITY addressPage.cancelButton.label "Cancel">
<!ENTITY addressPage.backButton.label "Back">
<!ENTITY addressPage.saveButton.label "Save">
<!ENTITY addressPage.persistCheckbox.label "Save address to &brandShortName;">
+ <!ENTITY failErrorPage.title "Sorry! Something went wrong with the payment process.">
+ <!ENTITY failErrorPage.suggestion1 "Check your credit card has not expired.">
+ <!ENTITY failErrorPage.suggestion2 "Make sure your credit card information is accurate.">
+ <!ENTITY failErrorPage.suggestion3 "If no other solutions work, check with shopping.com.">
+ <!ENTITY failErrorPage.doneButton.label "OK">
+ <!ENTITY timeoutErrorPage.title "Whoops! Shopping.com took too long to respond.">
+ <!ENTITY timeoutErrorPage.suggestion1 "Try again later.">
+ <!ENTITY timeoutErrorPage.suggestion2 "Check your network connection." >
+ <!ENTITY timeoutErrorPage.suggestion3 "If no other solutions work, check with shopping.com.">
+ <!ENTITY timeoutErrorPage.doneButton.label "OK">
]>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>&paymentSummaryTitle;</title>
<!-- chrome: is needed for global.dtd -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self' chrome:"/>
@@ -69,16 +78,17 @@
<link rel="stylesheet" href="components/address-option.css"/>
<link rel="stylesheet" href="components/basic-card-option.css"/>
<link rel="stylesheet" href="components/shipping-option.css"/>
<link rel="stylesheet" href="components/payment-details-item.css"/>
<link rel="stylesheet" href="containers/address-form.css"/>
<link rel="stylesheet" href="containers/basic-card-form.css"/>
<link rel="stylesheet" href="containers/order-details.css"/>
<link rel="stylesheet" href="containers/rich-picker.css"/>
+ <link rel="stylesheet" href="containers/error-page.css"/>
<script src="unprivileged-fallbacks.js"></script>
<script src="formautofill/autofillEditForms.js"></script>
<script type="module" src="containers/payment-dialog.js"></script>
<script type="module" src="paymentRequest.js"></script>
@@ -122,17 +132,16 @@
</div>
<footer>
<button id="cancel">&cancelPaymentButton.label;</button>
<button id="pay"
class="primary"
data-initial-label="&approvePaymentButton.label;"
data-processing-label="&processingPaymentButton.label;"
- data-fail-label="&failPaymentButton.label;"
data-unknown-label="&unknownPaymentButton.label;"
data-success-label="&successPaymentButton.label;"></button>
</footer>
</payment-request-page>
<section id="order-details-overlay" hidden="hidden">
<h2>&orderDetailsLabel;</h2>
<order-details></order-details>
</section>
@@ -153,16 +162,25 @@
<address-form id="address-page"
data-error-generic-save="&addressPage.error.genericSave;"
data-cancel-button-label="&addressPage.cancelButton.label;"
data-back-button-label="&addressPage.backButton.label;"
data-save-button-label="&addressPage.saveButton.label;"
data-persist-checkbox-label="&addressPage.persistCheckbox.label;"
hidden="hidden"></address-form>
+
+ <completion-error-page id="completion-timeout-error" class="illustrated"
+ data-page-title="&timeoutErrorPage.title;"
+ data-done-button-label="&timeoutErrorPage.doneButton.label;"
+ hidden="hidden"></completion-error-page>
+ <completion-error-page id="completion-fail-error" class="illustrated"
+ data-page-title="&failErrorPage.title;"
+ data-done-button-label="&failErrorPage.doneButton.label;"
+ hidden="hidden"></completion-error-page>
</div>
<div id="disabled-overlay" hidden="hidden">
<!-- overlay to prevent changes while waiting for a response from the merchant -->
</div>
</template>
<template id="order-details-template">
--- 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,30 @@ 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 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`);
+ 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: () => {
--- 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 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() {
+ 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,
+ }
+ );
+
+ 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 shouldn't be closed");
+
+ 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
@@ -31,31 +31,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);
@@ -70,16 +55,19 @@ add_task(async function setup_once() {
});
async function setup() {
let {request} = el1.requestStore.getState();
await el1.requestStore.setState({
changesPrevented: false,
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();
@@ -111,17 +99,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();
@@ -140,53 +127,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");