author | Sam Foster <sfoster@mozilla.com> |
Fri, 20 Jul 2018 15:51:52 -0700 | |
changeset 427619 | 9daa53881b7ae80bf6b093dac5d7744cf7fd18b1 |
parent 427618 | b4ecdfcd8f9f39a3993905762f42aa0284920ffb |
child 427632 | 83e2419a36379d0b380f67647b6749ba9116021e |
child 427690 | 3de5d538b2d44bcf3315970af7ed5443b4ecc308 |
push id | 34308 |
push user | shindli@mozilla.com |
push date | Sat, 21 Jul 2018 09:36:16 +0000 |
treeherder | mozilla-central@9daa53881b7a [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | MattN |
bugs | 1447777 |
milestone | 63.0a1 |
first release with | nightly linux32
9daa53881b7a
/
63.0a1
/
20180721100146
/
files
nightly linux64
9daa53881b7a
/
63.0a1
/
20180721100146
/
files
nightly mac
9daa53881b7a
/
63.0a1
/
20180721100146
/
files
nightly win32
9daa53881b7a
/
63.0a1
/
20180721100146
/
files
nightly win64
9daa53881b7a
/
63.0a1
/
20180721100146
/
files
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
releases | nightly linux32
63.0a1
/
20180721100146
/
pushlog to previous
nightly linux64
63.0a1
/
20180721100146
/
pushlog to previous
nightly mac
63.0a1
/
20180721100146
/
pushlog to previous
nightly win32
63.0a1
/
20180721100146
/
pushlog to previous
nightly win64
63.0a1
/
20180721100146
/
pushlog to previous
|
--- 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");