mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-14 05:45:37 +00:00
Bug 1447777 - Add completion fail and timeout error pages. r=MattN
* A new CompletionErrorPage / completion-error-page element which represents the content of the completion error * Leave the dialog open when complete() results in a 'fail' or 'timeout'. * The 'done' button on the fail & timeout error page closes the dialog by sending a message up to the paymentDialogWrapper. * Rewrite the pay button rendering logic to ensure it is disabled when it should be * Retry handling and UI not addressed here. Will need a new bug when the DOM support has landed. * Extend completeStatus support in debugging.html and group like actions to tidy up a bit MozReview-Commit-ID: GDhJqrj14uT * Add tests to verify that the dialog stays open when completion fails or times out * Add tests to verify that complete() throws after the timeout * Rework completeStatus mochitest for PaymentDialog MozReview-Commit-ID: 4ZNVEYMp7h5 --HG-- extra : rebase_source : 1d8e691eb44e74156a956dff73e1359af2a6934a
This commit is contained in:
parent
4fe42163ad
commit
799c3f4923
@ -553,6 +553,11 @@ var paymentDialogWrapper = {
|
||||
paymentSrv.changeShippingOption(this.request.requestId, optionID);
|
||||
},
|
||||
|
||||
onCloseDialogMessage() {
|
||||
// The PR is complete(), just close the dialog
|
||||
window.close();
|
||||
},
|
||||
|
||||
async onUpdateAutofillRecord(collectionName, record, guid, {
|
||||
errorStateChange,
|
||||
preserveOldProperties,
|
||||
@ -657,6 +662,10 @@ var paymentDialogWrapper = {
|
||||
this.onChangeShippingOption(data);
|
||||
break;
|
||||
}
|
||||
case "closeDialog": {
|
||||
this.onCloseDialogMessage();
|
||||
break;
|
||||
}
|
||||
case "paymentCancel": {
|
||||
this.onPaymentCancel();
|
||||
break;
|
||||
|
@ -15,6 +15,7 @@ browser.jar:
|
||||
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)
|
||||
|
@ -68,8 +68,19 @@ PaymentUIService.prototype = {
|
||||
},
|
||||
|
||||
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;
|
||||
@ -77,6 +88,15 @@ PaymentUIService.prototype = {
|
||||
.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) {
|
||||
|
@ -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);
|
22
browser/components/payments/res/containers/error-page.css
Normal file
22
browser/components/payments/res/containers/error-page.css
Normal file
@ -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");
|
||||
}
|
@ -12,6 +12,7 @@ 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";
|
||||
@ -121,6 +122,21 @@ export default class PaymentDialog extends PaymentStateSubscriberMixin(HTMLEleme
|
||||
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`
|
||||
@ -130,6 +146,9 @@ export default class PaymentDialog extends PaymentStateSubscriberMixin(HTMLEleme
|
||||
*/
|
||||
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.
|
||||
@ -203,22 +222,31 @@ export default class PaymentDialog extends PaymentStateSubscriberMixin(HTMLEleme
|
||||
}
|
||||
|
||||
_renderPayButton(state) {
|
||||
this._payButton.disabled = state.changesPrevented;
|
||||
let completeStatus = state.request.completeStatus;
|
||||
switch (completeStatus) {
|
||||
case "initial":
|
||||
case "processing":
|
||||
case "success":
|
||||
case "fail":
|
||||
case "unknown":
|
||||
case "unknown": {
|
||||
this._payButton.disabled = state.changesPrevented;
|
||||
this._payButton.textContent = this._payButton.dataset[completeStatus + "Label"];
|
||||
break;
|
||||
case "":
|
||||
}
|
||||
case "fail":
|
||||
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 "": {
|
||||
completeStatus = "initial";
|
||||
break;
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Invalid completeStatus: ${completeStatus}`);
|
||||
}
|
||||
}
|
||||
|
||||
this._payButton.textContent = this._payButton.dataset[completeStatus + "Label"];
|
||||
}
|
||||
|
||||
|
@ -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>
|
After Width: | Height: | Size: 165 B |
@ -16,3 +16,16 @@ h1 {
|
||||
fieldset > label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.group {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
label.block {
|
||||
display: block;
|
||||
margin: 0.3em 0;
|
||||
}
|
||||
|
||||
button.wide {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -11,37 +11,60 @@
|
||||
</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>
|
||||
|
@ -406,39 +406,13 @@ let buttonActions = {
|
||||
});
|
||||
},
|
||||
|
||||
setStateDefault() {
|
||||
let request = Object.assign({}, requestStore.getState().request, {
|
||||
completeStatus: "initial",
|
||||
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 });
|
||||
},
|
||||
|
||||
setStateProcessing() {
|
||||
let request = Object.assign({}, requestStore.getState().request, {
|
||||
completeStatus: "processing",
|
||||
});
|
||||
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 });
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -170,6 +170,10 @@ var paymentRequest = {
|
||||
this.sendMessageToChrome("pay", data);
|
||||
},
|
||||
|
||||
closeDialog() {
|
||||
this.sendMessageToChrome("closeDialog");
|
||||
},
|
||||
|
||||
changeShippingAddress(data) {
|
||||
this.sendMessageToChrome("changeShippingAddress", data);
|
||||
},
|
||||
|
@ -40,7 +40,6 @@
|
||||
<!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">
|
||||
@ -55,6 +54,16 @@
|
||||
<!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>
|
||||
@ -74,6 +83,7 @@
|
||||
<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>
|
||||
|
||||
@ -127,7 +137,6 @@
|
||||
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>
|
||||
@ -158,6 +167,15 @@
|
||||
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">
|
||||
|
@ -12,10 +12,23 @@ var PaymentTestUtils = {
|
||||
* 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,
|
||||
@ -150,6 +163,20 @@ var PaymentTestUtils = {
|
||||
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
|
||||
*
|
||||
|
@ -12,6 +12,7 @@ skip-if = verify && debug && os == 'mac'
|
||||
[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]
|
||||
|
@ -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");
|
||||
});
|
||||
});
|
@ -10,6 +10,7 @@
|
||||
|
||||
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);
|
||||
@ -316,6 +317,7 @@ add_task(async function setup_head() {
|
||||
registerCleanupFunction(function cleanup() {
|
||||
paymentSrv.cleanup();
|
||||
cleanupFormAutofillStorage();
|
||||
Services.prefs.clearUserPref(RESPONSE_TIMEOUT_PREF);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -36,21 +36,6 @@ 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);
|
||||
@ -75,6 +60,9 @@ async function setup() {
|
||||
changesPrevented: false,
|
||||
request: Object.assign({}, request, {completeStatus: "initial"}),
|
||||
orderDetailsShowing: false,
|
||||
page: {
|
||||
id: "payment-summary",
|
||||
},
|
||||
});
|
||||
|
||||
el1.render.reset();
|
||||
@ -116,7 +104,6 @@ add_task(async function test_viewAllButtonVisibility() {
|
||||
ok(!button.hidden, "Button is visible");
|
||||
});
|
||||
|
||||
|
||||
add_task(async function test_viewAllButton() {
|
||||
await setup();
|
||||
|
||||
@ -145,43 +132,92 @@ add_task(async function test_changesPrevented() {
|
||||
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");
|
||||
}
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user