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:
Sam Foster 2018-07-20 15:51:52 -07:00
parent 4fe42163ad
commit 799c3f4923
17 changed files with 481 additions and 115 deletions

View File

@ -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;

View File

@ -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)

View File

@ -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) {

View File

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

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

View File

@ -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"];
}

View File

@ -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

View File

@ -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%;
}

View File

@ -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>

View File

@ -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 });
},
};

View File

@ -170,6 +170,10 @@ var paymentRequest = {
this.sendMessageToChrome("pay", data);
},
closeDialog() {
this.sendMessageToChrome("closeDialog");
},
changeShippingAddress(data) {
this.sendMessageToChrome("changeShippingAddress", data);
},

View File

@ -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">

View File

@ -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
*

View File

@ -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]

View File

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

View File

@ -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);
});
});

View File

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